diff --git a/examples/age-calculator-app/.gitignore b/examples/age-calculator-app/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/examples/age-calculator-app/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/examples/age-calculator-app/.npmrc b/examples/age-calculator-app/.npmrc
new file mode 100644
index 00000000..6b5f38e8
--- /dev/null
+++ b/examples/age-calculator-app/.npmrc
@@ -0,0 +1,2 @@
+save-exact = true
+package-lock = false
diff --git a/examples/age-calculator-app/README.md b/examples/age-calculator-app/README.md
new file mode 100644
index 00000000..d20b46f4
--- /dev/null
+++ b/examples/age-calculator-app/README.md
@@ -0,0 +1,23 @@
+# Frontend Mentor Age Calculator App
+
+Here is the implementation in [Bau.js](https://github.com/grucloud/bau) of the [Frontend MentorAge Calculator App code challenge](https://www.frontendmentor.io/challenges/age-calculator-app-with-success-message-3FC1AZbNrv/hub)
+
+## Workflow
+
+Install the dependencies:
+
+```sh
+npm install
+```
+
+Start a development server:
+
+```sh
+npm run dev
+```
+
+Build a production version:
+
+```sh
+npm run build
+```
diff --git a/examples/age-calculator-app/index.html b/examples/age-calculator-app/index.html
new file mode 100644
index 00000000..b3b4167c
--- /dev/null
+++ b/examples/age-calculator-app/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ Age Calculator App | FrontendMentor
+
+
+
+
+
+
+
diff --git a/examples/age-calculator-app/package.json b/examples/age-calculator-app/package.json
new file mode 100644
index 00000000..7713f940
--- /dev/null
+++ b/examples/age-calculator-app/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "frontendmentor-age-calculator-app",
+ "homepage": "https://grucloud.github.io/bau/frontendmentor/age-calculator-app/",
+ "private": true,
+ "version": "0.97.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "deploy": "gh-pages -d ../../dist"
+ },
+ "devDependencies": {
+ "gh-pages": "6.1.1",
+ "typescript": "^5.0.2",
+ "vite": "^5.2.11"
+ },
+ "dependencies": {
+ "@grucloud/bau": "^0.97.0",
+ "@grucloud/bau-css": "^0.97.0",
+ "@grucloud/bau-ui": "^0.97.0"
+ }
+}
diff --git a/examples/age-calculator-app/public/assets/images/favicon-32x32.png b/examples/age-calculator-app/public/assets/images/favicon-32x32.png
new file mode 100644
index 00000000..1e2df7f0
Binary files /dev/null and b/examples/age-calculator-app/public/assets/images/favicon-32x32.png differ
diff --git a/examples/age-calculator-app/public/assets/images/icon-arrow.svg b/examples/age-calculator-app/public/assets/images/icon-arrow.svg
new file mode 100644
index 00000000..ea0a8b8a
--- /dev/null
+++ b/examples/age-calculator-app/public/assets/images/icon-arrow.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/age-calculator-app/src/ageCalculator.ts b/examples/age-calculator-app/src/ageCalculator.ts
new file mode 100644
index 00000000..f54f698e
--- /dev/null
+++ b/examples/age-calculator-app/src/ageCalculator.ts
@@ -0,0 +1,173 @@
+import { type Context } from "@grucloud/bau-ui/context";
+
+export default function (context: Context) {
+ const { bau, css } = context;
+ const { p, label, input, form, i, img, hr, span, section, button } = bau.tags;
+
+ const yearsState = bau.state("--");
+ const monthsState = bau.state("--");
+ const daysState = bau.state("--");
+
+ const className = css`
+ display: grid;
+ gap: 0.5rem;
+ padding: 2rem;
+ border-radius: 1rem 1rem 7rem 1rem;
+ background-color: var(--White);
+ min-width: 600px;
+ @media (max-width: 600px) {
+ min-width: unset;
+ }
+ .dob {
+ display: flex;
+ gap: 1rem;
+ & label {
+ display: grid;
+ text-transform: uppercase;
+ font-size: 0.7rem;
+ font-weight: 600;
+ color: var(--Smokey-grey);
+ letter-spacing: 0.1rem;
+ gap: 0.3rem;
+
+ & input {
+ @media (min-width: 600px) {
+ min-width: 5rem;
+ }
+ padding: 1rem;
+ border-radius: 0.5rem;
+ border: 1px solid var(--Light-grey);
+ font-size: 1.2rem;
+ font-weight: 600;
+ &:focus {
+ outline: 1px auto var(--color-primary);
+ }
+ }
+ }
+ }
+ .submit {
+ display: flex;
+ align-items: center;
+
+ & hr {
+ height: 1px;
+ background-color: var(--Light-grey);
+ border: none;
+ width: 100%;
+ @media (min-width: 600px) {
+ &:last-child {
+ display: none;
+ }
+ }
+ }
+ & button {
+ border: none;
+ border-radius: 50%;
+ background-color: var(--color-primary);
+ cursor: pointer;
+ @media (max-width: 600px) {
+ img {
+ width: 42px;
+ height: 42px;
+ }
+ }
+ }
+ }
+
+ .age-result {
+ font-size: 32px;
+ font-weight: 800;
+ font-style: italic;
+ @media (min-width: 600px) {
+ font-size: 60px;
+ }
+ .timeunit {
+ color: var(--color-primary);
+ }
+ }
+ `;
+
+ const onsubmit = (event: any) => {
+ event.preventDefault();
+ const { day, month, year } = Object.fromEntries(new FormData(event.target));
+ console.log(day, month, year);
+
+ const dob = Date.parse(`${year}-${month}-${day}`);
+ const now = Date.now();
+ let age = (now - dob) / 1000;
+
+ const secondsInYear = 31536000;
+ const secondsInMonth = 2628000;
+ const secondsInDay = 86400;
+
+ yearsState.val = String(Math.floor(age / secondsInYear));
+ let remainingSeconds = age % secondsInYear;
+
+ monthsState.val = String(Math.floor(remainingSeconds / secondsInMonth));
+ remainingSeconds %= secondsInMonth;
+
+ daysState.val = String(Math.floor(remainingSeconds / secondsInDay));
+ };
+
+ return () => {
+ return form(
+ { class: className, onsubmit },
+ section(
+ { class: "dob" },
+ label(
+ "Day",
+ input({
+ type: "number",
+ name: "day",
+ placeholder: "DD",
+ min: 1,
+ max: 31,
+ required: true,
+ })
+ ),
+ label(
+ "Month",
+ input({
+ type: "number",
+ name: "month",
+ placeholder: "MM",
+ min: 1,
+ max: 12,
+ required: true,
+ })
+ ),
+ label(
+ "Year",
+ input({
+ type: "number",
+ name: "year",
+ placeholder: "YYYY",
+ min: 1900,
+ max: new Date().getFullYear(),
+ required: true,
+ })
+ )
+ ),
+ section(
+ { class: "submit" },
+ hr(),
+ button(
+ { type: "submit" },
+ img({
+ src: "./assets/images/icon-arrow.svg",
+ alt: "submit",
+ height: 80,
+ width: 80,
+ })
+ ),
+ hr()
+ ),
+ section(
+ { class: "age-result" },
+ p(span({ class: "timeunit" }, yearsState), i(" years")),
+ p(span({ class: "timeunit" }, monthsState), i(" months")),
+ p(span({ class: "timeunit" }, daysState), i(" days"))
+ )
+ );
+ };
+}
diff --git a/examples/age-calculator-app/src/main.ts b/examples/age-calculator-app/src/main.ts
new file mode 100644
index 00000000..e4b69cda
--- /dev/null
+++ b/examples/age-calculator-app/src/main.ts
@@ -0,0 +1,18 @@
+import { createContext, type Context } from "@grucloud/bau-ui/context";
+import ageCalculator from "./ageCalculator";
+import "./style.css";
+
+const context = createContext();
+
+const app = (context: Context) => {
+ const { bau } = context;
+ const { main } = bau.tags;
+ const AgeCalculator = ageCalculator(context);
+
+ return function () {
+ return main(AgeCalculator());
+ };
+};
+
+const App = app(context);
+document.getElementById("app")?.replaceChildren(App());
diff --git a/examples/age-calculator-app/src/style.css b/examples/age-calculator-app/src/style.css
new file mode 100644
index 00000000..82df585a
--- /dev/null
+++ b/examples/age-calculator-app/src/style.css
@@ -0,0 +1,50 @@
+@import url("https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,700;1,400;1,800&display=swap");
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ --Purple: hsl(259, 100%, 65%);
+ --Light-red: hsl(0, 100%, 67%);
+
+ --White: hsl(0, 0%, 100%);
+ --Off-white: hsl(0, 0%, 94%);
+ --Light-grey: hsl(0, 0%, 86%);
+ --Smokey-grey: hsl(0, 1%, 44%);
+ --Off-black: hsl(0, 0%, 8%);
+
+ --color-primary-h: 259;
+ --color-primary-base-s: 100%;
+ --color-primary-l: 65%;
+
+ --color-neutral-h: 245;
+ --color-neutral-base-s: 18%;
+ --color-neutral-l: 26%;
+
+ --color-danger-h: 358;
+ --color-danger-base-s: 79%;
+ --color-danger-l: 66%;
+
+ --font-color-primary: white;
+ --background-color: white;
+}
+body {
+ background-color: var(--Light-grey);
+ font: 400 16px/1.6 "Poppins", sans-serif;
+ min-height: 100vh;
+ display: grid;
+ place-content: center;
+}
+
+input[type="number"]::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+}
+
+input[type="number"] {
+ -moz-appearance: textfield;
+ appearance: textfield;
+ margin: 0;
+}
diff --git a/examples/age-calculator-app/src/vite-env.d.ts b/examples/age-calculator-app/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/examples/age-calculator-app/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/age-calculator-app/tsconfig.json b/examples/age-calculator-app/tsconfig.json
new file mode 100644
index 00000000..75abdef2
--- /dev/null
+++ b/examples/age-calculator-app/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/examples/age-calculator-app/vite.config.js b/examples/age-calculator-app/vite.config.js
new file mode 100644
index 00000000..a378cfc4
--- /dev/null
+++ b/examples/age-calculator-app/vite.config.js
@@ -0,0 +1,11 @@
+import { defineConfig } from "vite";
+
+export default defineConfig(({ command, mode, ssrBuild }) => {
+ return {
+ base: "/bau/frontendmentor/age-calculator-app/",
+ build: { outDir: "../../dist/frontendmentor/age-calculator-app" },
+ server: {
+ open: true,
+ },
+ };
+});
diff --git a/examples/newsletter-signup-form/src/newsletterSignupForm.ts b/examples/newsletter-signup-form/src/newsletterSignupForm.ts
index 56e7faf6..d790b79f 100644
--- a/examples/newsletter-signup-form/src/newsletterSignupForm.ts
+++ b/examples/newsletter-signup-form/src/newsletterSignupForm.ts
@@ -30,7 +30,7 @@ export default function (context: Context) {
grid-template-areas: "image" "form";
border-radius: 0;
}
- padding: 1rem;
+ padding-inline: 1rem;
margin-inline: 1rem;
background-color: var(--White);
border-radius: 2rem;