diff --git a/README.md b/README.md
index 03f842c16..53fd07abf 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,53 @@
# :wind_face: Keywind
-Keywind is a component-based Keycloak Login Theme built with [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss) and [Alpine.js](https://github.com/alpinejs/alpine).
+Keywind is a component-based Keycloak Login & E-Mail Theme built with [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss), [Mailwind](https://www.mailwind.io/) and [Alpine.js](https://github.com/alpinejs/alpine).
-![Preview](./preview.png)
+![Preview Login](./preview-login.png)
+
+![Preview Login](./preview-email.png)
+
+### Styled Pages
+
+- Error
+- Login
+- Login Config TOTP
+- Login IDP Link Confirm
+- Login OAuth Grant
+- Login OTP
+- Login Page Expired
+- Login Password
+- Login Recovery Authn Code Config
+- Login Recovery Authn Code Input
+- Login Reset Password
+- Login Update Password
+- Login Update Profile
+- Login Username
+- Logout Confirm
+- Register
+- Select Authenticator
+- Terms and Conditions
+- WebAuthn Authenticate
+- WebAuthn Error
+- WebAuthn Register
+
+### Identity Provider Icons
+
+- Apple
+- Bitbucket
+- Discord
+- Facebook
+- GitHub
+- GitLab
+- Google
+- Instagram
+- LinkedIn
+- Microsoft
+- OpenID
+- Red Hat OpenShift
+- PayPal
+- Slack
+- Stack Overflow
+- Twitter
### Styled Pages
@@ -52,8 +97,8 @@ Keywind is a component-based Keycloak Login Theme built with [Tailwind CSS](http
Keywind has been designed with component-based architecture from the start, and **you can customize as little or as much Keywind as you need**:
-1. [Deploy Keywind Login Theme](https://www.keycloak.org/docs/latest/server_development/#deploying-themes)
-2. [Create your own Login Theme](https://www.keycloak.org/docs/latest/server_development/#creating-a-theme)
+1. [Deploy Keywind Themes](https://www.keycloak.org/docs/latest/server_development/#deploying-themes)
+2. [Create your own Themes](https://www.keycloak.org/docs/latest/server_development/#creating-a-theme)
3. Specify parent theme in [theme properties](https://www.keycloak.org/docs/latest/server_development/#theme-properties):
```
@@ -98,6 +143,10 @@ You can update Keywind components in your own child theme. For example, create a
#macro>
```
+If you want to customize your own email components, update the files located in:
+
+`theme/mytheme/email/html-src`
+
## Build
When you're ready to deploy your own theme, run the build command to generate a static production build.
diff --git a/package.json b/package.json
index fb0faa4ab..578afb58e 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,9 @@
"$schema": "https://json.schemastore.org/package",
"name": "keywind",
"scripts": {
- "build": "tsc && vite build",
+ "build": "npm run build:email && npm run build:login",
+ "build:email": "vite-node scripts/build.email",
+ "build:login": "tsc && vite build",
"build:jar": "vite-node scripts/build",
"dev": "vite build --watch",
"test": "mvn test"
@@ -18,6 +20,8 @@
"@types/node": "^20.6.5",
"archiver": "^6.0.1",
"autoprefixer": "^10.4.16",
+ "child-process-promise": "^2.2.1",
+ "mailwind": "^2.2.0",
"postcss": "^8.4.30",
"tailwindcss": "^3.3.3",
"typescript": "^5.2.2",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3c60020e0..c03d7733f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -31,6 +31,12 @@ devDependencies:
autoprefixer:
specifier: ^10.4.16
version: 10.4.16(postcss@8.4.30)
+ child-process-promise:
+ specifier: ^2.2.1
+ version: 2.2.1
+ mailwind:
+ specifier: ^2.2.0
+ version: 2.2.0(tailwindcss@3.3.3)
postcss:
specifier: ^8.4.30
version: 8.4.30
@@ -354,6 +360,23 @@ packages:
'@vue/reactivity': 3.1.5
dev: false
+ /ansi-colors@4.1.3:
+ resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+ dependencies:
+ color-convert: 2.0.1
+ dev: true
+
/any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
dev: true
@@ -428,6 +451,10 @@ packages:
engines: {node: '>=8'}
dev: true
+ /boolbase@1.0.0:
+ resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
+ dev: true
+
/brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
dependencies:
@@ -477,6 +504,37 @@ packages:
resolution: {integrity: sha512-hfS5tE8bnNiNvEOEkm8HElUHroYwlqMMENEzELymy77+tJ6m+gA2krtHl5hxJaj71OlpC2cHZbdSMX1/YEqEkA==}
dev: true
+ /cheerio-select@1.6.0:
+ resolution: {integrity: sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==}
+ dependencies:
+ css-select: 4.3.0
+ css-what: 6.1.0
+ domelementtype: 2.3.0
+ domhandler: 4.3.1
+ domutils: 2.8.0
+ dev: true
+
+ /cheerio@1.0.0-rc.10:
+ resolution: {integrity: sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==}
+ engines: {node: '>= 6'}
+ dependencies:
+ cheerio-select: 1.6.0
+ dom-serializer: 1.4.1
+ domhandler: 4.3.1
+ htmlparser2: 6.1.0
+ parse5: 6.0.1
+ parse5-htmlparser2-tree-adapter: 6.0.1
+ tslib: 2.6.2
+ dev: true
+
+ /child-process-promise@2.2.1:
+ resolution: {integrity: sha512-Fi4aNdqBsr0mv+jgWxcZ/7rAIC2mgihrptyVI4foh/rrjY/3BNjfP9+oaiFx/fzim+1ZyCNBae0DlyfQhSugog==}
+ dependencies:
+ cross-spawn: 4.0.2
+ node-version: 1.2.0
+ promise-polyfill: 6.1.0
+ dev: true
+
/chokidar@3.5.3:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
engines: {node: '>= 8.10.0'}
@@ -492,11 +550,36 @@ packages:
fsevents: 2.3.3
dev: true
+ /cliui@8.0.1:
+ resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 7.0.0
+ dev: true
+
+ /color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+ dependencies:
+ color-name: 1.1.4
+ dev: true
+
+ /color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+ dev: true
+
/commander@4.1.1:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
dev: true
+ /commander@6.2.1:
+ resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==}
+ engines: {node: '>= 6'}
+ dev: true
+
/compress-commons@5.0.1:
resolution: {integrity: sha512-MPh//1cERdLtqwO3pOFLeXtpuai0Y2WCd5AhtKxznqM7WtaMYaOEMSgn45d9D10sIHSfIKE603HlOp8OPGrvag==}
engines: {node: '>= 12.0.0'}
@@ -529,6 +612,28 @@ packages:
readable-stream: 3.6.2
dev: true
+ /cross-spawn@4.0.2:
+ resolution: {integrity: sha512-yAXz/pA1tD8Gtg2S98Ekf/sewp3Lcp3YoFKJ4Hkp5h5yLWnKVTDU0kwjKJ8NDCYcfTLfyGkzTikst+jWypT1iA==}
+ dependencies:
+ lru-cache: 4.1.5
+ which: 1.3.1
+ dev: true
+
+ /css-select@4.3.0:
+ resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
+ dependencies:
+ boolbase: 1.0.0
+ css-what: 6.1.0
+ domhandler: 4.3.1
+ domutils: 2.8.0
+ nth-check: 2.1.1
+ dev: true
+
+ /css-what@6.1.0:
+ resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
+ engines: {node: '>= 6'}
+ dev: true
+
/cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
@@ -555,10 +660,52 @@ packages:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
dev: true
+ /dom-serializer@1.4.1:
+ resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==}
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 4.3.1
+ entities: 2.2.0
+ dev: true
+
+ /domelementtype@2.3.0:
+ resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
+ dev: true
+
+ /domhandler@3.3.0:
+ resolution: {integrity: sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==}
+ engines: {node: '>= 4'}
+ dependencies:
+ domelementtype: 2.3.0
+ dev: true
+
+ /domhandler@4.3.1:
+ resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==}
+ engines: {node: '>= 4'}
+ dependencies:
+ domelementtype: 2.3.0
+ dev: true
+
+ /domutils@2.8.0:
+ resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==}
+ dependencies:
+ dom-serializer: 1.4.1
+ domelementtype: 2.3.0
+ domhandler: 4.3.1
+ dev: true
+
/electron-to-chromium@1.4.528:
resolution: {integrity: sha512-UdREXMXzLkREF4jA8t89FQjA8WHI6ssP38PMY4/4KhXFQbtImnghh4GkCgrtiZwLKUKVD2iTVXvDVQjfomEQuA==}
dev: true
+ /emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+ dev: true
+
+ /entities@2.2.0:
+ resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
+ dev: true
+
/esbuild@0.18.20:
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
engines: {node: '>=12'}
@@ -594,6 +741,11 @@ packages:
engines: {node: '>=6'}
dev: true
+ /escape-goat@3.0.0:
+ resolution: {integrity: sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==}
+ engines: {node: '>=10'}
+ dev: true
+
/fast-fifo@1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
dev: true
@@ -642,6 +794,11 @@ packages:
resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==}
dev: true
+ /get-caller-file@2.0.5:
+ resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
+ engines: {node: 6.* || 8.* || >= 10.*}
+ dev: true
+
/glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@@ -689,6 +846,24 @@ packages:
function-bind: 1.1.1
dev: true
+ /htmlparser2@5.0.1:
+ resolution: {integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==}
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 3.3.0
+ domutils: 2.8.0
+ entities: 2.2.0
+ dev: true
+
+ /htmlparser2@6.1.0:
+ resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==}
+ dependencies:
+ domelementtype: 2.3.0
+ domhandler: 4.3.1
+ domutils: 2.8.0
+ entities: 2.2.0
+ dev: true
+
/inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
dependencies:
@@ -718,6 +893,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+ dev: true
+
/is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
@@ -734,6 +914,10 @@ packages:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
dev: true
+ /isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ dev: true
+
/jiti@1.20.0:
resolution: {integrity: sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==}
hasBin: true
@@ -743,6 +927,20 @@ packages:
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
dev: true
+ /juice@8.1.0:
+ resolution: {integrity: sha512-FLzurJrx5Iv1e7CfBSZH68dC04EEvXvvVvPYB7Vx1WAuhCp1ZPIMtqxc+WTWxVkpTIC2Ach/GAv0rQbtGf6YMA==}
+ engines: {node: '>=10.0.0'}
+ hasBin: true
+ dependencies:
+ cheerio: 1.0.0-rc.10
+ commander: 6.2.1
+ mensch: 0.3.4
+ slick: 1.12.2
+ web-resource-inliner: 6.0.1
+ transitivePeerDependencies:
+ - encoding
+ dev: true
+
/lazystream@1.0.1:
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
engines: {node: '>= 0.6.3'}
@@ -763,6 +961,30 @@ packages:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
dev: true
+ /lru-cache@4.1.5:
+ resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==}
+ dependencies:
+ pseudomap: 1.0.2
+ yallist: 2.1.2
+ dev: true
+
+ /mailwind@2.2.0(tailwindcss@3.3.3):
+ resolution: {integrity: sha512-DueTDipAb2TVsbDixDZD57E2wE55Uk3i+VsDGF8G46MPeEu5cz2ALdsdgec5PhE2W0Fq0TFl1ihpZuuItdR+bA==}
+ hasBin: true
+ peerDependencies:
+ tailwindcss: 3.x
+ dependencies:
+ juice: 8.1.0
+ tailwindcss: 3.3.3
+ yargs: 17.6.2
+ transitivePeerDependencies:
+ - encoding
+ dev: true
+
+ /mensch@0.3.4:
+ resolution: {integrity: sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==}
+ dev: true
+
/merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@@ -776,6 +998,12 @@ packages:
picomatch: 2.3.1
dev: true
+ /mime@2.6.0:
+ resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
+ engines: {node: '>=4.0.0'}
+ hasBin: true
+ dev: true
+
/mini-svg-data-uri@1.4.4:
resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
hasBin: true
@@ -821,10 +1049,27 @@ packages:
hasBin: true
dev: true
+ /node-fetch@2.7.0:
+ resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+ engines: {node: 4.x || >=6.0.0}
+ peerDependencies:
+ encoding: ^0.1.0
+ peerDependenciesMeta:
+ encoding:
+ optional: true
+ dependencies:
+ whatwg-url: 5.0.0
+ dev: true
+
/node-releases@2.0.13:
resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==}
dev: true
+ /node-version@1.2.0:
+ resolution: {integrity: sha512-ma6oU4Sk0qOoKEAymVoTvk8EdXEobdS7m/mAGhDJ8Rouugho48crHBORAmy5BoOcv8wraPM6xumapQp5hl4iIQ==}
+ engines: {node: '>=6.0.0'}
+ dev: true
+
/normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
@@ -835,6 +1080,12 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /nth-check@2.1.1:
+ resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
+ dependencies:
+ boolbase: 1.0.0
+ dev: true
+
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@@ -851,6 +1102,16 @@ packages:
wrappy: 1.0.2
dev: true
+ /parse5-htmlparser2-tree-adapter@6.0.1:
+ resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==}
+ dependencies:
+ parse5: 6.0.1
+ dev: true
+
+ /parse5@6.0.1:
+ resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
+ dev: true
+
/path-is-absolute@1.0.1:
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==}
engines: {node: '>=0.10.0'}
@@ -965,6 +1226,14 @@ packages:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
dev: true
+ /promise-polyfill@6.1.0:
+ resolution: {integrity: sha512-g0LWaH0gFsxovsU7R5LrrhHhWAWiHRnh1GPrhXnPgYsDkIqjRYUYSZEsej/wtleDrz5xVSIDbeKfidztp2XHFQ==}
+ dev: true
+
+ /pseudomap@1.0.2:
+ resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==}
+ dev: true
+
/queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true
@@ -1013,6 +1282,11 @@ packages:
picomatch: 2.3.1
dev: true
+ /require-directory@2.1.1:
+ resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
/resolve@1.22.6:
resolution: {integrity: sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==}
hasBin: true
@@ -1053,6 +1327,10 @@ packages:
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
dev: true
+ /slick@1.12.2:
+ resolution: {integrity: sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==}
+ dev: true
+
/source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
@@ -1065,6 +1343,15 @@ packages:
queue-tick: 1.0.1
dev: true
+ /string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+ dev: true
+
/string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
dependencies:
@@ -1077,6 +1364,13 @@ packages:
safe-buffer: 5.2.1
dev: true
+ /strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+ dependencies:
+ ansi-regex: 5.0.1
+ dev: true
+
/sucrase@3.34.0:
resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==}
engines: {node: '>=8'}
@@ -1155,10 +1449,18 @@ packages:
is-number: 7.0.0
dev: true
+ /tr46@0.0.3:
+ resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+ dev: true
+
/ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
dev: true
+ /tslib@2.6.2:
+ resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
+ dev: true
+
/typescript@5.2.2:
resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==}
engines: {node: '>=14.17'}
@@ -1184,6 +1486,11 @@ packages:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
+ /valid-data-url@3.0.1:
+ resolution: {integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==}
+ engines: {node: '>=10'}
+ dev: true
+
/vite-node@0.34.5(@types/node@20.6.5):
resolution: {integrity: sha512-RNZ+DwbCvDoI5CbCSQSyRyzDTfFvFauvMs6Yq4ObJROKlIKuat1KgSX/Ako5rlDMfVCyMcpMRMTkJBxd6z8YRA==}
engines: {node: '>=v14.18.0'}
@@ -1242,15 +1549,83 @@ packages:
fsevents: 2.3.3
dev: true
+ /web-resource-inliner@6.0.1:
+ resolution: {integrity: sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==}
+ engines: {node: '>=10.0.0'}
+ dependencies:
+ ansi-colors: 4.1.3
+ escape-goat: 3.0.0
+ htmlparser2: 5.0.1
+ mime: 2.6.0
+ node-fetch: 2.7.0
+ valid-data-url: 3.0.1
+ transitivePeerDependencies:
+ - encoding
+ dev: true
+
+ /webidl-conversions@3.0.1:
+ resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
+ dev: true
+
+ /whatwg-url@5.0.0:
+ resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
+ dependencies:
+ tr46: 0.0.3
+ webidl-conversions: 3.0.1
+ dev: true
+
+ /which@1.3.1:
+ resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
+ hasBin: true
+ dependencies:
+ isexe: 2.0.0
+ dev: true
+
+ /wrap-ansi@7.0.0:
+ resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
+ engines: {node: '>=10'}
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ dev: true
+
/wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
dev: true
+ /y18n@5.0.8:
+ resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /yallist@2.1.2:
+ resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==}
+ dev: true
+
/yaml@2.3.2:
resolution: {integrity: sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==}
engines: {node: '>= 14'}
dev: true
+ /yargs-parser@21.1.1:
+ resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
+ engines: {node: '>=12'}
+ dev: true
+
+ /yargs@17.6.2:
+ resolution: {integrity: sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==}
+ engines: {node: '>=12'}
+ dependencies:
+ cliui: 8.0.1
+ escalade: 3.1.1
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ string-width: 4.2.3
+ y18n: 5.0.8
+ yargs-parser: 21.1.1
+ dev: true
+
/zip-stream@5.0.1:
resolution: {integrity: sha512-UfZ0oa0C8LI58wJ+moL46BDIMgCQbnsb+2PoiJYtonhBsMh2bq1eRBVkvjfVsqbEHd9/EgKPUuL9saSSsec8OA==}
engines: {node: '>= 12.0.0'}
diff --git a/preview-email.png b/preview-email.png
new file mode 100644
index 000000000..bae4e8f9c
Binary files /dev/null and b/preview-email.png differ
diff --git a/preview.png b/preview-login.png
similarity index 100%
rename from preview.png
rename to preview-login.png
diff --git a/scripts/build.email.ts b/scripts/build.email.ts
new file mode 100644
index 000000000..1e7db6a95
--- /dev/null
+++ b/scripts/build.email.ts
@@ -0,0 +1,34 @@
+import { exec } from 'child-process-promise';
+import * as path from 'node:path';
+import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
+
+//BUILD:Head-Files
+const directoryPath = path.join(__dirname, '../theme/keywind/email/html-src');
+const directoryPathComponents = path.join(
+ __dirname,
+ '../theme/keywind/email/html-src/components/atoms'
+);
+const files = [
+ ...readdirSync(directoryPath),
+ ...readdirSync(directoryPathComponents).map((e) => `components/atoms/${e}`),
+];
+
+for (let file of files) {
+ if (file.includes('.ftl')) {
+ // Build using mailwind
+ await exec(
+ `mailwind --input-html ${path.join(
+ __dirname,
+ `../theme/keywind/email/html-src/${file}`
+ )} --output-html ${path.join(__dirname, `../theme/keywind/email/html/${file}`)}`
+ );
+ // fix: wrong build result for ftl-file exception
+ const data = readFileSync(path.join(__dirname, `../theme/keywind/email/html/${file}`));
+ const result = data
+ .toString()
+ .replaceAll('', '>');
+ writeFileSync(path.join(__dirname, `../theme/keywind/email/html/${file}`), result);
+ }
+}
diff --git a/theme/keywind/email/html-src/components/atoms/background.ftl b/theme/keywind/email/html-src/components/atoms/background.ftl
new file mode 100644
index 000000000..f05db209b
--- /dev/null
+++ b/theme/keywind/email/html-src/components/atoms/background.ftl
@@ -0,0 +1,11 @@
+<#macro kw>
+
+#macro>
diff --git a/theme/keywind/email/html-src/components/atoms/card.ftl b/theme/keywind/email/html-src/components/atoms/card.ftl
new file mode 100644
index 000000000..54899ac5b
--- /dev/null
+++ b/theme/keywind/email/html-src/components/atoms/card.ftl
@@ -0,0 +1,9 @@
+<#macro kw>
+
+#macro>
diff --git a/theme/keywind/email/html-src/components/atoms/container.ftl b/theme/keywind/email/html-src/components/atoms/container.ftl
new file mode 100644
index 000000000..eab6b693f
--- /dev/null
+++ b/theme/keywind/email/html-src/components/atoms/container.ftl
@@ -0,0 +1,9 @@
+<#macro kw>
+
+#macro>
diff --git a/theme/keywind/email/html-src/components/atoms/footer.ftl b/theme/keywind/email/html-src/components/atoms/footer.ftl
new file mode 100644
index 000000000..1d04a6fac
--- /dev/null
+++ b/theme/keywind/email/html-src/components/atoms/footer.ftl
@@ -0,0 +1,9 @@
+<#macro kw>
+
+
+
+ © ${.now?string('yyyy')} ${kcSanitize(realmName!"")}.
+ |
+
+
+#macro>
diff --git a/theme/keywind/email/html-src/components/atoms/header.ftl b/theme/keywind/email/html-src/components/atoms/header.ftl
new file mode 100644
index 000000000..63d075c5d
--- /dev/null
+++ b/theme/keywind/email/html-src/components/atoms/header.ftl
@@ -0,0 +1,15 @@
+<#macro kw>
+
+
+
+ <#if (properties.headingLogoUrl!"") != "">
+
+ <#else>
+
+ #if>
+ |
+
+
+#macro>
diff --git a/theme/keywind/email/html-src/components/atoms/main.ftl b/theme/keywind/email/html-src/components/atoms/main.ftl
new file mode 100644
index 000000000..5e64f4cfd
--- /dev/null
+++ b/theme/keywind/email/html-src/components/atoms/main.ftl
@@ -0,0 +1,9 @@
+<#macro kw>
+
+#macro>
diff --git a/theme/keywind/email/html-src/document.ftl b/theme/keywind/email/html-src/document.ftl
new file mode 100644
index 000000000..2f2485110
--- /dev/null
+++ b/theme/keywind/email/html-src/document.ftl
@@ -0,0 +1,13 @@
+<#macro kw script="">
+
+
+
+
+
+
+ <#if properties.meta?has_content>
+ <#list properties.meta?split(" ") as meta>
+
+ #list>
+ #if>
+#macro>
diff --git a/theme/keywind/email/html-src/email-test.ftl b/theme/keywind/email/html-src/email-test.ftl
new file mode 100644
index 000000000..bca248043
--- /dev/null
+++ b/theme/keywind/email/html-src/email-test.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("emailTestBodyHtml",realmName))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html-src/email-update-confirmation.ftl b/theme/keywind/email/html-src/email-update-confirmation.ftl
new file mode 100644
index 000000000..d618ed7a4
--- /dev/null
+++ b/theme/keywind/email/html-src/email-update-confirmation.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("emailUpdateConfirmationBodyHtml",link, newEmail, realmName, linkExpirationFormatter(linkExpiration)))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html-src/email-verification-with-code.ftl b/theme/keywind/email/html-src/email-verification-with-code.ftl
new file mode 100644
index 000000000..6a73f50ad
--- /dev/null
+++ b/theme/keywind/email/html-src/email-verification-with-code.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("emailVerificationBodyCodeHtml",code))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html-src/email-verification.ftl b/theme/keywind/email/html-src/email-verification.ftl
new file mode 100644
index 000000000..9a2e318c8
--- /dev/null
+++ b/theme/keywind/email/html-src/email-verification.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("emailVerificationBodyHtml",link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration)))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html-src/event-login_error.ftl b/theme/keywind/email/html-src/event-login_error.ftl
new file mode 100644
index 000000000..7ab9401d8
--- /dev/null
+++ b/theme/keywind/email/html-src/event-login_error.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("eventLoginErrorBodyHtml",event.date,event.ipAddress))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html-src/event-remove_totp.ftl b/theme/keywind/email/html-src/event-remove_totp.ftl
new file mode 100644
index 000000000..6058bbe00
--- /dev/null
+++ b/theme/keywind/email/html-src/event-remove_totp.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("eventRemoveTotpBodyHtml",event.date, event.ipAddress))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html-src/event-update_password.ftl b/theme/keywind/email/html-src/event-update_password.ftl
new file mode 100644
index 000000000..8a802ecc4
--- /dev/null
+++ b/theme/keywind/email/html-src/event-update_password.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("eventUpdatePasswordBodyHtml",event.date, event.ipAddress))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html-src/event-update_totp.ftl b/theme/keywind/email/html-src/event-update_totp.ftl
new file mode 100644
index 000000000..f58c38ab6
--- /dev/null
+++ b/theme/keywind/email/html-src/event-update_totp.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("eventUpdateTotpBodyHtml",event.date, event.ipAddress))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html-src/executeActions.ftl b/theme/keywind/email/html-src/executeActions.ftl
new file mode 100644
index 000000000..73d407550
--- /dev/null
+++ b/theme/keywind/email/html-src/executeActions.ftl
@@ -0,0 +1,8 @@
+<#outputformat "plainText">
+<#assign requiredActionsText><#if requiredActions??><#list requiredActions><#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, #sep>#items>#list>#if>#assign>
+#outputformat>
+
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("executeActionsBodyHtml",link, linkExpiration, realmName, requiredActionsText, linkExpirationFormatter(linkExpiration)))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html-src/identity-provider-link.ftl b/theme/keywind/email/html-src/identity-provider-link.ftl
new file mode 100644
index 000000000..6b4381e1a
--- /dev/null
+++ b/theme/keywind/email/html-src/identity-provider-link.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("identityProviderLinkBodyHtml", identityProviderDisplayName, realmName, identityProviderContext.username, link, linkExpiration, linkExpirationFormatter(linkExpiration)))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html-src/password-reset.ftl b/theme/keywind/email/html-src/password-reset.ftl
new file mode 100644
index 000000000..b0de26329
--- /dev/null
+++ b/theme/keywind/email/html-src/password-reset.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("passwordResetBodyHtml",link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration)))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html-src/template.ftl b/theme/keywind/email/html-src/template.ftl
new file mode 100644
index 000000000..838b89997
--- /dev/null
+++ b/theme/keywind/email/html-src/template.ftl
@@ -0,0 +1,28 @@
+<#import "document.ftl" as document>
+<#import "components/atoms/background.ftl" as background>
+<#import "components/atoms/card.ftl" as card>
+<#import "components/atoms/container.ftl" as container>
+<#import "components/atoms/footer.ftl" as footer>
+<#import "components/atoms/header.ftl" as header>
+<#import "components/atoms/main.ftl" as main>
+
+<#macro emailLayout>
+
+
+
+ <@document.kw script=script />
+
+
+ <@background.kw />
+ <@container.kw>
+ <@card.kw>
+ <@header.kw />
+ <@main.kw>
+ <#nested>
+ @main.kw>
+ @card.kw>
+ <@footer.kw />
+ @container.kw>
+
+
+#macro>
diff --git a/theme/keywind/email/html/components/atoms/background.ftl b/theme/keywind/email/html/components/atoms/background.ftl
new file mode 100644
index 000000000..a42ff964e
--- /dev/null
+++ b/theme/keywind/email/html/components/atoms/background.ftl
@@ -0,0 +1,8 @@
+<#macro kw>
+
+#macro>
diff --git a/theme/keywind/email/html/components/atoms/body.ftl b/theme/keywind/email/html/components/atoms/body.ftl
new file mode 100644
index 000000000..a4526d31d
--- /dev/null
+++ b/theme/keywind/email/html/components/atoms/body.ftl
@@ -0,0 +1,10 @@
+<#macro kw>
+
+
+
+ <#nested>
+
+
+
+#macro>
+
\ No newline at end of file
diff --git a/theme/keywind/email/html/components/atoms/card.ftl b/theme/keywind/email/html/components/atoms/card.ftl
new file mode 100644
index 000000000..29f40b9de
--- /dev/null
+++ b/theme/keywind/email/html/components/atoms/card.ftl
@@ -0,0 +1,9 @@
+<#macro kw>
+
+#macro>
diff --git a/theme/keywind/email/html/components/atoms/container.ftl b/theme/keywind/email/html/components/atoms/container.ftl
new file mode 100644
index 000000000..04ee64f64
--- /dev/null
+++ b/theme/keywind/email/html/components/atoms/container.ftl
@@ -0,0 +1,9 @@
+<#macro kw>
+
+#macro>
diff --git a/theme/keywind/email/html/components/atoms/footer.ftl b/theme/keywind/email/html/components/atoms/footer.ftl
new file mode 100644
index 000000000..80881690c
--- /dev/null
+++ b/theme/keywind/email/html/components/atoms/footer.ftl
@@ -0,0 +1,9 @@
+<#macro kw>
+
+
+
+ © ${.now?string('yyyy')} ${kcSanitize(realmName!"")}.
+ |
+
+
+#macro>
diff --git a/theme/keywind/email/html/components/atoms/header.ftl b/theme/keywind/email/html/components/atoms/header.ftl
new file mode 100644
index 000000000..a8d522c61
--- /dev/null
+++ b/theme/keywind/email/html/components/atoms/header.ftl
@@ -0,0 +1,15 @@
+<#macro kw>
+
+
+
+ <#if (properties.headingLogoUrl!"") != "">
+
+ <#else>
+
+ #if>
+ |
+
+
+#macro>
diff --git a/theme/keywind/email/html/components/atoms/main.ftl b/theme/keywind/email/html/components/atoms/main.ftl
new file mode 100644
index 000000000..5b9f05e72
--- /dev/null
+++ b/theme/keywind/email/html/components/atoms/main.ftl
@@ -0,0 +1,9 @@
+<#macro kw>
+
+#macro>
diff --git a/theme/keywind/email/html/document.ftl b/theme/keywind/email/html/document.ftl
new file mode 100644
index 000000000..6a0831dfa
--- /dev/null
+++ b/theme/keywind/email/html/document.ftl
@@ -0,0 +1,13 @@
+<#macro kw script="">
+
+
+
+
+
+
+ <#if properties.meta?has_content>
+ <#list properties.meta?split(" ") as meta>
+
+ #list>
+ #if>
+#macro>
diff --git a/theme/keywind/email/html/email-test.ftl b/theme/keywind/email/html/email-test.ftl
new file mode 100644
index 000000000..bca248043
--- /dev/null
+++ b/theme/keywind/email/html/email-test.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("emailTestBodyHtml",realmName))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html/email-update-confirmation.ftl b/theme/keywind/email/html/email-update-confirmation.ftl
new file mode 100644
index 000000000..d618ed7a4
--- /dev/null
+++ b/theme/keywind/email/html/email-update-confirmation.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("emailUpdateConfirmationBodyHtml",link, newEmail, realmName, linkExpirationFormatter(linkExpiration)))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html/email-verification-with-code.ftl b/theme/keywind/email/html/email-verification-with-code.ftl
new file mode 100644
index 000000000..6a73f50ad
--- /dev/null
+++ b/theme/keywind/email/html/email-verification-with-code.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("emailVerificationBodyCodeHtml",code))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html/email-verification.ftl b/theme/keywind/email/html/email-verification.ftl
new file mode 100644
index 000000000..9a2e318c8
--- /dev/null
+++ b/theme/keywind/email/html/email-verification.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("emailVerificationBodyHtml",link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration)))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html/event-login_error.ftl b/theme/keywind/email/html/event-login_error.ftl
new file mode 100644
index 000000000..7ab9401d8
--- /dev/null
+++ b/theme/keywind/email/html/event-login_error.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("eventLoginErrorBodyHtml",event.date,event.ipAddress))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html/event-remove_totp.ftl b/theme/keywind/email/html/event-remove_totp.ftl
new file mode 100644
index 000000000..6058bbe00
--- /dev/null
+++ b/theme/keywind/email/html/event-remove_totp.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("eventRemoveTotpBodyHtml",event.date, event.ipAddress))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html/event-update_password.ftl b/theme/keywind/email/html/event-update_password.ftl
new file mode 100644
index 000000000..8a802ecc4
--- /dev/null
+++ b/theme/keywind/email/html/event-update_password.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("eventUpdatePasswordBodyHtml",event.date, event.ipAddress))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html/event-update_totp.ftl b/theme/keywind/email/html/event-update_totp.ftl
new file mode 100644
index 000000000..f58c38ab6
--- /dev/null
+++ b/theme/keywind/email/html/event-update_totp.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("eventUpdateTotpBodyHtml",event.date, event.ipAddress))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html/executeActions.ftl b/theme/keywind/email/html/executeActions.ftl
new file mode 100644
index 000000000..73d407550
--- /dev/null
+++ b/theme/keywind/email/html/executeActions.ftl
@@ -0,0 +1,8 @@
+<#outputformat "plainText">
+<#assign requiredActionsText><#if requiredActions??><#list requiredActions><#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, #sep>#items>#list>#if>#assign>
+#outputformat>
+
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("executeActionsBodyHtml",link, linkExpiration, realmName, requiredActionsText, linkExpirationFormatter(linkExpiration)))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html/identity-provider-link.ftl b/theme/keywind/email/html/identity-provider-link.ftl
new file mode 100644
index 000000000..6b4381e1a
--- /dev/null
+++ b/theme/keywind/email/html/identity-provider-link.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("identityProviderLinkBodyHtml", identityProviderDisplayName, realmName, identityProviderContext.username, link, linkExpiration, linkExpirationFormatter(linkExpiration)))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html/password-reset.ftl b/theme/keywind/email/html/password-reset.ftl
new file mode 100644
index 000000000..b0de26329
--- /dev/null
+++ b/theme/keywind/email/html/password-reset.ftl
@@ -0,0 +1,4 @@
+<#import "template.ftl" as layout>
+<@layout.emailLayout>
+${kcSanitize(msg("passwordResetBodyHtml",link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration)))?no_esc}
+@layout.emailLayout>
diff --git a/theme/keywind/email/html/template.ftl b/theme/keywind/email/html/template.ftl
new file mode 100644
index 000000000..838b89997
--- /dev/null
+++ b/theme/keywind/email/html/template.ftl
@@ -0,0 +1,28 @@
+<#import "document.ftl" as document>
+<#import "components/atoms/background.ftl" as background>
+<#import "components/atoms/card.ftl" as card>
+<#import "components/atoms/container.ftl" as container>
+<#import "components/atoms/footer.ftl" as footer>
+<#import "components/atoms/header.ftl" as header>
+<#import "components/atoms/main.ftl" as main>
+
+<#macro emailLayout>
+
+
+
+ <@document.kw script=script />
+
+
+ <@background.kw />
+ <@container.kw>
+ <@card.kw>
+ <@header.kw />
+ <@main.kw>
+ <#nested>
+ @main.kw>
+ @card.kw>
+ <@footer.kw />
+ @container.kw>
+
+
+#macro>
diff --git a/theme/keywind/email/text/email-test.ftl b/theme/keywind/email/text/email-test.ftl
new file mode 100644
index 000000000..f1becdd2a
--- /dev/null
+++ b/theme/keywind/email/text/email-test.ftl
@@ -0,0 +1,2 @@
+<#ftl output_format="plainText">
+${msg("emailTestBody", realmName)}
\ No newline at end of file
diff --git a/theme/keywind/email/text/email-update-confirmation.ftl b/theme/keywind/email/text/email-update-confirmation.ftl
new file mode 100644
index 000000000..335d9a848
--- /dev/null
+++ b/theme/keywind/email/text/email-update-confirmation.ftl
@@ -0,0 +1,2 @@
+<#ftl output_format="plainText">
+${msg("emailUpdateConfirmationBody",link, newEmail, realmName, linkExpirationFormatter(linkExpiration))}
diff --git a/theme/keywind/email/text/email-verification-with-code.ftl b/theme/keywind/email/text/email-verification-with-code.ftl
new file mode 100644
index 000000000..4ffb7d879
--- /dev/null
+++ b/theme/keywind/email/text/email-verification-with-code.ftl
@@ -0,0 +1,2 @@
+<#ftl output_format="plainText">
+${msg("emailVerificationBodyCode",code)}
\ No newline at end of file
diff --git a/theme/keywind/email/text/email-verification.ftl b/theme/keywind/email/text/email-verification.ftl
new file mode 100644
index 000000000..9e3969632
--- /dev/null
+++ b/theme/keywind/email/text/email-verification.ftl
@@ -0,0 +1,2 @@
+<#ftl output_format="plainText">
+${msg("emailVerificationBody",link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration))}
\ No newline at end of file
diff --git a/theme/keywind/email/text/event-login_error.ftl b/theme/keywind/email/text/event-login_error.ftl
new file mode 100644
index 000000000..bfb4036f5
--- /dev/null
+++ b/theme/keywind/email/text/event-login_error.ftl
@@ -0,0 +1,2 @@
+<#ftl output_format="plainText">
+${msg("eventLoginErrorBody",event.date,event.ipAddress)}
\ No newline at end of file
diff --git a/theme/keywind/email/text/event-remove_totp.ftl b/theme/keywind/email/text/event-remove_totp.ftl
new file mode 100644
index 000000000..a7e3b6860
--- /dev/null
+++ b/theme/keywind/email/text/event-remove_totp.ftl
@@ -0,0 +1,2 @@
+<#ftl output_format="plainText">
+${msg("eventRemoveTotpBody",event.date, event.ipAddress)}
\ No newline at end of file
diff --git a/theme/keywind/email/text/event-update_password.ftl b/theme/keywind/email/text/event-update_password.ftl
new file mode 100644
index 000000000..2ec7ea01e
--- /dev/null
+++ b/theme/keywind/email/text/event-update_password.ftl
@@ -0,0 +1,2 @@
+<#ftl output_format="plainText">
+${msg("eventUpdatePasswordBody",event.date, event.ipAddress)}
\ No newline at end of file
diff --git a/theme/keywind/email/text/event-update_totp.ftl b/theme/keywind/email/text/event-update_totp.ftl
new file mode 100644
index 000000000..14778b5ff
--- /dev/null
+++ b/theme/keywind/email/text/event-update_totp.ftl
@@ -0,0 +1,2 @@
+<#ftl output_format="plainText">
+${msg("eventUpdateTotpBody",event.date, event.ipAddress)}
\ No newline at end of file
diff --git a/theme/keywind/email/text/executeActions.ftl b/theme/keywind/email/text/executeActions.ftl
new file mode 100644
index 000000000..6610c7ada
--- /dev/null
+++ b/theme/keywind/email/text/executeActions.ftl
@@ -0,0 +1,4 @@
+<#ftl output_format="plainText">
+<#assign requiredActionsText><#if requiredActions??><#list requiredActions><#items as reqActionItem>${msg("requiredAction.${reqActionItem}")}<#sep>, #items>#list><#else>#if>#assign>
+
+${msg("executeActionsBody",link, linkExpiration, realmName, requiredActionsText, linkExpirationFormatter(linkExpiration))}
\ No newline at end of file
diff --git a/theme/keywind/email/text/identity-provider-link.ftl b/theme/keywind/email/text/identity-provider-link.ftl
new file mode 100644
index 000000000..17e3cc45a
--- /dev/null
+++ b/theme/keywind/email/text/identity-provider-link.ftl
@@ -0,0 +1,2 @@
+<#ftl output_format="plainText">
+${msg("identityProviderLinkBody", identityProviderDisplayName, realmName, identityProviderContext.username, link, linkExpiration, linkExpirationFormatter(linkExpiration))}
\ No newline at end of file
diff --git a/theme/keywind/email/text/password-reset.ftl b/theme/keywind/email/text/password-reset.ftl
new file mode 100644
index 000000000..27405c9a4
--- /dev/null
+++ b/theme/keywind/email/text/password-reset.ftl
@@ -0,0 +1,2 @@
+<#ftl output_format="plainText">
+${msg("passwordResetBody",link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration))}
\ No newline at end of file
diff --git a/theme/keywind/email/theme.properties b/theme/keywind/email/theme.properties
new file mode 100644
index 000000000..5012e0a31
--- /dev/null
+++ b/theme/keywind/email/theme.properties
@@ -0,0 +1,3 @@
+parent=base
+
+headingLogoUrl=