diff --git a/bau/bau.js b/bau/bau.js
index b8002bfb..c336ff47 100644
--- a/bau/bau.js
+++ b/bau/bau.js
@@ -85,16 +85,17 @@ export default function Bau(input) {
isAttribute,
op = [],
} = binding;
- const [method, result, args, data, parentProp] = op;
+ const [method, result, args, data, parentProp = []] = op;
if (method && renderItem) {
- methodToActionMapping(
- element,
- args,
- (...args) => toDom(renderItem(...args)),
- result,
- data,
- parentProp
- )[method]?.call();
+ !parentProp.length &&
+ methodToActionMapping(
+ element,
+ args,
+ (...args) => toDom(renderItem(...args)),
+ result,
+ data,
+ parentProp
+ )[method]?.call();
} else {
let newElement = renderInferred
? renderInferred({
diff --git a/bau/test/state-array-nested.test.js b/bau/test/state-array-nested.test.js
new file mode 100644
index 00000000..c1e38648
--- /dev/null
+++ b/bau/test/state-array-nested.test.js
@@ -0,0 +1,46 @@
+import { describe, vi, it, assert, expect } from "vitest";
+
+const sleep = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms));
+
+import Bau from "../bau";
+const comments = [
+ { id: 1, replies: [] },
+ { id: 2, replies: [] },
+];
+describe("nested array", async () => {
+ const bau = Bau();
+ const { h1, div, button, ul, li } = bau.tags;
+
+ const commentsState = bau.state(comments);
+
+ const Comment = (comment) => {
+ const repliesState = bau.state(comment.replies);
+ return li(
+ h1(comment.id),
+ button(
+ {
+ id: `button-${comment.id}`,
+ onclick: () => {
+ repliesState.val.push({ id: 3, comment: "hi" });
+ },
+ },
+ "Add reply"
+ ),
+ bau.loop(repliesState, ul(), (r) => li(r.comment))
+ );
+ };
+
+ const TestComponent = () => div(bau.loop(commentsState, ul(), Comment));
+
+ it("click", async () => {
+ const el = TestComponent();
+ document.body.appendChild(el);
+
+ document.getElementById("button-1").click();
+ await sleep();
+ const ulEl = el.querySelector("ul");
+
+ assert.equal(ulEl.childNodes.length, 2);
+ document.body.removeChild(el);
+ });
+});
diff --git a/examples/README.md b/examples/README.md
index c64e4876..532f3702 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -15,6 +15,7 @@ Below is a list of projects implemented using _Bau_.
| Contact Form | [live](https://grucloud.github.io/bau/frontendmentor/contact-form/) | [code](./contact-form) |
| E-commerce Product Page | [live](https://grucloud.github.io/bau/frontendmentor/e-commerce-product-page/) | [code](./e-commerce-product-page) |
| Faq Accordion | [live](https://grucloud.github.io/bau/frontendmentor/faq-accordion/) | [code](./faq-accordion) |
+| Interactive Comments Section | [live](https://grucloud.github.io/bau/frontendmentor/interactive-comments-section/) | [code](./interactive-comments-section) |
| Interactive Rating Component | [live](https://grucloud.github.io/bau/frontendmentor/interactive-rating-component/) | [code](./interactive-rating-component) |
| IP Address Tracker | [live](https://grucloud.github.io/bau/frontendmentor/ip-address-tracker/) | [code](./ip-address-tracker) |
| Job Listing With Filtering | [live](https://grucloud.github.io/bau/frontendmentor/job-listings-with-filtering/) | [code](./job-listings-with-filtering) |
diff --git a/examples/interactive-comments-section/.gitignore b/examples/interactive-comments-section/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/examples/interactive-comments-section/.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/interactive-comments-section/.npmrc b/examples/interactive-comments-section/.npmrc
new file mode 100644
index 00000000..6b5f38e8
--- /dev/null
+++ b/examples/interactive-comments-section/.npmrc
@@ -0,0 +1,2 @@
+save-exact = true
+package-lock = false
diff --git a/examples/interactive-comments-section/README.md b/examples/interactive-comments-section/README.md
new file mode 100644
index 00000000..8f01613d
--- /dev/null
+++ b/examples/interactive-comments-section/README.md
@@ -0,0 +1,23 @@
+# Frontend Mentor Interactive Comments Section
+
+Here is the implementation in [Bau.js](https://github.com/grucloud/bau) of the [Frontend Mentor Interactive Comments Section code challenge](https://www.frontendmentor.io/challenges/interactive-comments-section-iG1RugEG9)
+
+## 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/interactive-comments-section/index.html b/examples/interactive-comments-section/index.html
new file mode 100644
index 00000000..020045bf
--- /dev/null
+++ b/examples/interactive-comments-section/index.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ Interactive Comments Section | FrontendMentor
+
+
+
+
+
+
+
diff --git a/examples/interactive-comments-section/package.json b/examples/interactive-comments-section/package.json
new file mode 100644
index 00000000..df2536ca
--- /dev/null
+++ b/examples/interactive-comments-section/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "frontendmentor-interactive-comments-section",
+ "homepage": "https://grucloud.github.io/bau/frontendmentor/interactive-comments-section/",
+ "private": true,
+ "version": "0.95.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.95.0",
+ "@grucloud/bau-css": "^0.95.0",
+ "@grucloud/bau-ui": "^0.95.0",
+ "dayjs": "1.11.13"
+ }
+}
diff --git a/examples/interactive-comments-section/public/assets/images/avatars/image-amyrobson.png b/examples/interactive-comments-section/public/assets/images/avatars/image-amyrobson.png
new file mode 100644
index 00000000..00ebf938
Binary files /dev/null and b/examples/interactive-comments-section/public/assets/images/avatars/image-amyrobson.png differ
diff --git a/examples/interactive-comments-section/public/assets/images/avatars/image-amyrobson.webp b/examples/interactive-comments-section/public/assets/images/avatars/image-amyrobson.webp
new file mode 100644
index 00000000..f46f920c
Binary files /dev/null and b/examples/interactive-comments-section/public/assets/images/avatars/image-amyrobson.webp differ
diff --git a/examples/interactive-comments-section/public/assets/images/avatars/image-juliusomo.png b/examples/interactive-comments-section/public/assets/images/avatars/image-juliusomo.png
new file mode 100644
index 00000000..0c0ac447
Binary files /dev/null and b/examples/interactive-comments-section/public/assets/images/avatars/image-juliusomo.png differ
diff --git a/examples/interactive-comments-section/public/assets/images/avatars/image-juliusomo.webp b/examples/interactive-comments-section/public/assets/images/avatars/image-juliusomo.webp
new file mode 100644
index 00000000..d5da1be0
Binary files /dev/null and b/examples/interactive-comments-section/public/assets/images/avatars/image-juliusomo.webp differ
diff --git a/examples/interactive-comments-section/public/assets/images/avatars/image-maxblagun.png b/examples/interactive-comments-section/public/assets/images/avatars/image-maxblagun.png
new file mode 100644
index 00000000..111c9644
Binary files /dev/null and b/examples/interactive-comments-section/public/assets/images/avatars/image-maxblagun.png differ
diff --git a/examples/interactive-comments-section/public/assets/images/avatars/image-maxblagun.webp b/examples/interactive-comments-section/public/assets/images/avatars/image-maxblagun.webp
new file mode 100644
index 00000000..8aa9646a
Binary files /dev/null and b/examples/interactive-comments-section/public/assets/images/avatars/image-maxblagun.webp differ
diff --git a/examples/interactive-comments-section/public/assets/images/avatars/image-ramsesmiron.png b/examples/interactive-comments-section/public/assets/images/avatars/image-ramsesmiron.png
new file mode 100644
index 00000000..94458a8d
Binary files /dev/null and b/examples/interactive-comments-section/public/assets/images/avatars/image-ramsesmiron.png differ
diff --git a/examples/interactive-comments-section/public/assets/images/avatars/image-ramsesmiron.webp b/examples/interactive-comments-section/public/assets/images/avatars/image-ramsesmiron.webp
new file mode 100644
index 00000000..8fc47fdb
Binary files /dev/null and b/examples/interactive-comments-section/public/assets/images/avatars/image-ramsesmiron.webp differ
diff --git a/examples/interactive-comments-section/public/assets/images/favicon-32x32.png b/examples/interactive-comments-section/public/assets/images/favicon-32x32.png
new file mode 100644
index 00000000..1e2df7f0
Binary files /dev/null and b/examples/interactive-comments-section/public/assets/images/favicon-32x32.png differ
diff --git a/examples/interactive-comments-section/public/assets/images/icon-delete.svg b/examples/interactive-comments-section/public/assets/images/icon-delete.svg
new file mode 100644
index 00000000..609cdf23
--- /dev/null
+++ b/examples/interactive-comments-section/public/assets/images/icon-delete.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/interactive-comments-section/public/assets/images/icon-edit.svg b/examples/interactive-comments-section/public/assets/images/icon-edit.svg
new file mode 100644
index 00000000..49731489
--- /dev/null
+++ b/examples/interactive-comments-section/public/assets/images/icon-edit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/interactive-comments-section/public/assets/images/icon-minus.svg b/examples/interactive-comments-section/public/assets/images/icon-minus.svg
new file mode 100644
index 00000000..a95c0283
--- /dev/null
+++ b/examples/interactive-comments-section/public/assets/images/icon-minus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/interactive-comments-section/public/assets/images/icon-plus.svg b/examples/interactive-comments-section/public/assets/images/icon-plus.svg
new file mode 100644
index 00000000..985d6566
--- /dev/null
+++ b/examples/interactive-comments-section/public/assets/images/icon-plus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/interactive-comments-section/public/assets/images/icon-reply.svg b/examples/interactive-comments-section/public/assets/images/icon-reply.svg
new file mode 100644
index 00000000..e30a634a
--- /dev/null
+++ b/examples/interactive-comments-section/public/assets/images/icon-reply.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/interactive-comments-section/src/data.json b/examples/interactive-comments-section/src/data.json
new file mode 100644
index 00000000..2fc20e1c
--- /dev/null
+++ b/examples/interactive-comments-section/src/data.json
@@ -0,0 +1,68 @@
+{
+ "currentUser": {
+ "image": {
+ "png": "./assets/images/avatars/image-juliusomo.png",
+ "webp": "./assets/images/avatars/image-juliusomo.webp"
+ },
+ "username": "juliusomo"
+ },
+ "comments": [
+ {
+ "id": 1,
+ "content": "Impressive! Though it seems the drag feature could be improved. But overall it looks incredible. You've nailed the design and the responsiveness at various breakpoints works really well.",
+ "createdAt": "1 month ago",
+ "score": 12,
+ "user": {
+ "image": {
+ "png": "./assets/images/avatars/image-amyrobson.png",
+ "webp": "./assets/images/avatars/image-amyrobson.webp"
+ },
+ "username": "amyrobson"
+ },
+ "replies": []
+ },
+ {
+ "id": 2,
+ "content": "Woah, your project looks awesome! How long have you been coding for? I'm still new, but think I want to dive into React as well soon. Perhaps you can give me an insight on where I can learn React? Thanks!",
+ "createdAt": "2 weeks ago",
+ "score": 5,
+ "user": {
+ "image": {
+ "png": "./assets/images/avatars/image-maxblagun.png",
+ "webp": "./assets/images/avatars/image-maxblagun.webp"
+ },
+ "username": "maxblagun"
+ },
+ "replies": [
+ {
+ "id": 3,
+ "content": "If you're still new, I'd recommend focusing on the fundamentals of HTML, CSS, and JS before considering React. It's very tempting to jump ahead but lay a solid foundation first.",
+ "createdAt": "1 week ago",
+ "score": 4,
+ "replyingTo": "maxblagun",
+ "user": {
+ "image": {
+ "png": "./assets/images/avatars/image-ramsesmiron.png",
+ "webp": "./assets/images/avatars/image-ramsesmiron.webp"
+ },
+ "username": "ramsesmiron"
+ }
+ },
+ {
+ "id": 4,
+ "content": "I couldn't agree more with this. Everything moves so fast and it always seems like everyone knows the newest library/framework. But the fundamentals are what stay constant.",
+ "createdAt": "2 days ago",
+ "score": 2,
+ "replyingTo": "ramsesmiron",
+ "user": {
+ "image": {
+ "png": "./assets/images/avatars/image-juliusomo.png",
+ "webp": "./assets/images/avatars/image-juliusomo.webp"
+ },
+ "username": "juliusomo"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/examples/interactive-comments-section/src/interactiveCommentsSection.ts b/examples/interactive-comments-section/src/interactiveCommentsSection.ts
new file mode 100644
index 00000000..da6fe008
--- /dev/null
+++ b/examples/interactive-comments-section/src/interactiveCommentsSection.ts
@@ -0,0 +1,436 @@
+import { type Context } from "@grucloud/bau-ui/context";
+import dayjs from "dayjs";
+import relativeTime from "dayjs/plugin/relativeTime";
+
+import data from "./data.json";
+
+dayjs.extend(relativeTime);
+
+export default function (context: Context) {
+ const { bau, css } = context;
+ const {
+ form,
+ ul,
+ li,
+ header,
+ footer,
+ img,
+ figure,
+ figcaption,
+ span,
+ p,
+ div,
+ button,
+ time,
+ textarea,
+ dialog,
+ a,
+ h1,
+ } = bau.tags;
+
+ const className = css`
+ padding: 1rem;
+ max-width: 750px;
+ margin: auto;
+
+ > ul.comments {
+ margin: 0;
+ > li {
+ .comment {
+ background-color: var(--White);
+ border-radius: 0.5rem;
+ margin-block: 1rem;
+ padding: 1rem;
+ display: grid;
+ grid-template-areas: "likes header reply" "likes content content";
+ grid-template-columns: min-content auto;
+ @media (max-width: 600px) {
+ grid-template-areas: "header header" "content content" "likes reply";
+ }
+
+ gap: 1rem;
+
+ > header {
+ grid-area: header;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 1.2rem;
+ & figure {
+ display: inline-flex;
+ align-items: center;
+ gap: 1rem;
+ & figcaption {
+ font-weight: bold;
+ }
+ }
+ .you-badge {
+ background-color: var(--color-primary);
+ color: var(--font-color-primary);
+ padding-inline: 0.5rem;
+ font-size: 0.85rem;
+ }
+ & time {
+ color: var(--font-color-secondary);
+ }
+ }
+ .content {
+ grid-area: content;
+ }
+ .controls-button {
+ grid-area: reply;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ > button {
+ padding-inline: 0.5rem;
+ }
+ }
+ > footer {
+ display: flex;
+ justify-content: space-between;
+ }
+ }
+ .replies {
+ border-left: 2px solid var(--Light-grayish-blue);
+ padding-left: 2rem;
+ }
+ .reply {
+ display: grid;
+ gap: 1rem;
+ background-color: var(--White);
+ border-radius: 0.5rem;
+ padding: 1rem;
+ grid-template-areas: "avatar text send-button" ". text .";
+ grid-template-columns: min-content auto min-content;
+ @media (max-width: 600px) {
+ grid-template-areas: "text text text" "avatar ... send-button";
+ grid-template-rows: auto min-content;
+ }
+
+ & figure {
+ grid-area: avatar;
+ }
+ .send-button-container {
+ grid-area: send-button;
+ }
+ & footer {
+ display: inline-flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+ }
+ }
+ }
+ `;
+
+ const LikesButtons =
+ ({ doVote }: any) =>
+ ({ score, vote }: any) =>
+ div(
+ {
+ class: css`
+ grid-area: likes;
+ `,
+ },
+ div(
+ {
+ class: css`
+ display: inline-flex;
+ align-items: center;
+ flex-direction: column;
+ @media (max-width: 600px) {
+ flex-direction: row;
+ justify-content: flex-start;
+ }
+ border-radius: 0.5rem;
+ gap: 0.5rem;
+ background-color: var(--Very-light-gray);
+ & button {
+ background: transparent;
+ color: var(--color-primary-lightest);
+ font-size: 1.3rem;
+ transition: all 0.3s;
+ &:disabled {
+ cursor: not-allowed;
+ }
+ &:hover,
+ &:disabled {
+ color: var(--Moderate-blue);
+ }
+ }
+ & span {
+ font-weight: 500;
+ font-size: 1.2rem;
+ color: var(--Moderate-blue);
+ }
+ `,
+ },
+ button(
+ { type: "button", onclick: doVote("up"), disabled: vote == "up" },
+ "+"
+ ),
+ span(score),
+ button(
+ {
+ type: "button",
+ onclick: doVote("down"),
+ disabled: vote == "down",
+ },
+ "−"
+ )
+ )
+ );
+
+ const ReplyButton = ({ onclick }: any) =>
+ div(
+ {
+ class: "reply-button",
+ },
+ button(
+ {
+ type: "button",
+ onclick,
+ },
+ img({ src: "./assets/images/icon-reply.svg", alt: "" }),
+ "Reply"
+ )
+ );
+
+ const EditButton = ({ onclick }: any) =>
+ button(
+ {
+ type: "button",
+ onclick,
+ },
+ img({ src: "./assets/images/icon-edit.svg", alt: "" }),
+ "Edit"
+ );
+
+ const DeleteButton = ({ onclick }: any) =>
+ button(
+ {
+ class: "danger",
+ type: "button",
+ onclick,
+ },
+ img({ src: "./assets/images/icon-delete.svg", alt: "" }),
+ "Delete"
+ );
+
+ const Comment =
+ ({
+ updateComment = () => {},
+ deleteComment = () => {},
+ depth = 0,
+ }: any = {}) =>
+ (comment: any) => {
+ const showWrite = bau.state(false);
+ const showEdit = bau.state(false);
+ const repliesState = bau.state(comment.replies ?? []);
+
+ const commentState = bau.state(comment);
+ const saveNewComment =
+ ({}: any) =>
+ (event: any) => {
+ event.preventDefault();
+ const { content } = Object.fromEntries(new FormData(event.target));
+ const reply = {
+ id: `${new Date().getTime()}`,
+ createdAt: new Date(),
+ score: 0,
+ user: data.currentUser,
+ content,
+ };
+ repliesState.val.push(reply);
+ showWrite.val = false;
+ };
+
+ const WriteComment = (comment: any) =>
+ form(
+ { class: "reply", onsubmit: saveNewComment(comment) },
+ textarea({
+ autofocus: true,
+ required: true,
+ name: "content",
+ rows: 5,
+ placeholder: "Insert a comment",
+ }),
+ figure(
+ img({
+ src: data.currentUser.image.webp,
+ alt: data.currentUser.username,
+ width: 36,
+ height: 36,
+ })
+ ),
+ div(
+ { class: "send-button-container" },
+ button({ type: "submit", class: "primary" }, "SEND")
+ )
+ );
+
+ const doDeleteComment =
+ ({ id }: any) =>
+ () => {
+ const index = repliesState.val.findIndex((r: any) => r.id == id);
+ if (index >= 0) {
+ repliesState.val.splice(index, 1);
+ }
+ deleteDialog.close();
+ };
+
+ const DeleteDialog = () =>
+ dialog(
+ form(
+ header(h1("Delete comment")),
+ p(
+ "Are you sure you want to delete this comment? This will remove the comment and can't be undone."
+ ),
+ footer(
+ button(
+ {
+ type: "button",
+ class: ["neutral", "solid"],
+ onclick: () => deleteDialog.close(),
+ },
+ "NO, CANCEL"
+ ),
+ button(
+ {
+ type: "button",
+ class: ["danger", "solid"],
+ onclick: deleteComment(comment),
+ },
+ "YES, DELETE"
+ )
+ )
+ )
+ );
+
+ const deleteDialog = DeleteDialog();
+
+ const showDeleteDialog =
+ ({}: any) =>
+ () => {
+ deleteDialog.showModal();
+ };
+
+ const showCommentEdit =
+ ({}: any) =>
+ () => {
+ showEdit.val = true;
+ };
+
+ const doUpdateComment =
+ ({ id }: any) =>
+ (event: any) => {
+ event.preventDefault();
+ const { content } = Object.fromEntries(new FormData(event.target));
+ const index = repliesState.val.findIndex((r: any) => r.id == id);
+ if (index >= 0) {
+ repliesState.val[index].content = content;
+ }
+ };
+
+ const ContentView = (comment: any) => [
+ comment.replyingTo &&
+ a({ href: `/users/${comment.replyingTo}` }, `@${comment.replyingTo}`),
+ " ",
+ comment.content,
+ ];
+
+ const ContentEdit = (comment: any) =>
+ form(
+ {
+ onsubmit: (event: any) => {
+ updateComment(comment)(event);
+ showEdit.val = false;
+ },
+ },
+ textarea({
+ name: "content",
+ rows: 8,
+ value: comment.content,
+ required: true,
+ autofocus: true,
+ }),
+ footer(
+ button({ type: "submit", class: ["solid", "primary"] }, "UPDATE")
+ )
+ );
+
+ return li(
+ div(
+ { class: "comment" },
+ header(
+ figure(
+ img({
+ src: comment.user.image.webp,
+ height: 36,
+ width: 36,
+ alt: "",
+ }),
+ figcaption(comment.user.username)
+ ),
+ isOwnComent(comment) && span({ class: "you-badge" }, "you"),
+ time(dayjs(comment.createdAt).fromNow())
+ ),
+ p({ class: "content" }, () =>
+ showEdit.val ? ContentEdit(comment) : ContentView(comment)
+ ),
+ () =>
+ LikesButtons({
+ doVote: (type: string) => () => {
+ commentState.val.vote = type;
+ if (type == "up") {
+ commentState.val.score++;
+ } else {
+ commentState.val.score--;
+ }
+ },
+ })(commentState.val),
+ div(
+ { class: "controls-button" },
+ isOwnComent(comment)
+ ? [
+ DeleteButton({ onclick: showDeleteDialog(comment) }),
+ () =>
+ showEdit.val
+ ? ""
+ : EditButton({
+ onclick: showCommentEdit(comment),
+ }),
+ ]
+ : ReplyButton({ onclick: () => (showWrite.val = !showWrite.val) })
+ ),
+ deleteDialog
+ ),
+ bau.loop(
+ repliesState,
+ ul({ class: "replies" }),
+ Comment({
+ updateComment: doUpdateComment,
+ deleteComment: doDeleteComment,
+ depth: depth + 1,
+ })
+ ),
+ () => showWrite.val && WriteComment(comment)
+ );
+ };
+
+ const sortComment = (comments: any) =>
+ comments.sort((a: any, b: any) => (b.score > a.score ? 1 : -1));
+
+ const isOwnComent = (comment: any) =>
+ comment.user.username == data.currentUser.username;
+
+ const commentsState = bau.state(sortComment(data.comments));
+
+ return () => {
+ return form(
+ { class: className },
+ bau.loop(commentsState, ul({ class: "comments" }), Comment())
+ );
+ };
+}
diff --git a/examples/interactive-comments-section/src/main.ts b/examples/interactive-comments-section/src/main.ts
new file mode 100644
index 00000000..7c0e243e
--- /dev/null
+++ b/examples/interactive-comments-section/src/main.ts
@@ -0,0 +1,18 @@
+import { createContext, type Context } from "@grucloud/bau-ui/context";
+import interactiveCommentsSection from "./interactiveCommentsSection";
+import "./style.css";
+
+const context = createContext();
+
+const app = (context: Context) => {
+ const { bau } = context;
+ const { main } = bau.tags;
+ const InteractiveCommentsSection = interactiveCommentsSection(context);
+
+ return function () {
+ return main(InteractiveCommentsSection());
+ };
+};
+
+const App = app(context);
+document.getElementById("app")?.replaceChildren(App());
diff --git a/examples/interactive-comments-section/src/style.css b/examples/interactive-comments-section/src/style.css
new file mode 100644
index 00000000..af0a4c19
--- /dev/null
+++ b/examples/interactive-comments-section/src/style.css
@@ -0,0 +1,119 @@
+@import url("https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;700&display=swap");
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+:root {
+ --Moderate-blue: hsl(238, 40%, 52%);
+ --Soft-Red: hsl(358, 79%, 66%);
+ --Light-grayish-blue: hsl(239, 57%, 85%);
+ --Pale-red: hsl(357, 100%, 86%);
+ --Dark-blue: hsl(212, 24%, 26%);
+ --Grayish-Blue: hsl(211, 10%, 45%);
+ --Light-gray: hsl(223, 19%, 93%);
+ --Very-light-gray: hsl(228, 33%, 97%);
+ --White: hsl(0, 0%, 100%);
+
+ --color-primary-h: 238;
+ --color-primary-base-s: 40%;
+ --color-primary-l: 52%;
+
+ --color-primary-lightest: #c5c6ef;
+
+ --color-neutral-h: 211;
+ --color-neutral-base-s: 10%;
+ --color-neutral-l: 45%;
+
+ --color-danger-h: 358;
+ --color-danger-base-s: 79%;
+ --color-danger-l: 66%;
+
+ --font-color-primary: white;
+ --background-color: var(--Very-light-gray);
+ --shadow: 0px 10px 20px -10px hsla(180, 29%, 50%, 0.4);
+}
+
+body {
+ background-color: var(--background-color);
+ font: 400 16px/1.6 "Rubik", sans-serif;
+ min-height: 100vh;
+ display: grid;
+}
+
+h1 {
+ color: var(--Dark-blue);
+}
+
+button {
+ cursor: pointer;
+ padding-inline: 1.2rem;
+ padding-block: 0.5rem;
+ font-weight: 600;
+ font-size: 1rem;
+ border: none;
+ border-radius: 0.3rem;
+ display: inline-flex;
+ gap: 0.5rem;
+ align-items: center;
+ font-weight: 600;
+ background: transparent;
+ border: none;
+ color: var(--Moderate-blue);
+
+ &.primary {
+ color: white;
+ background-color: var(--Moderate-blue);
+ }
+
+ &.solid.neutral {
+ color: white;
+ background-color: var(--Moderate-blue);
+ }
+ &.danger {
+ color: var(--Soft-Red);
+ }
+}
+
+ul {
+ list-style: none;
+}
+
+dialog {
+ margin: auto;
+ padding-inline: 2rem;
+ padding-block: 2rem;
+ border: none;
+ box-shadow: var(--shadow);
+ border-radius: 0.5rem;
+ min-width: 300px;
+ max-width: 400px;
+ > form {
+ display: grid;
+ gap: 1rem;
+ & h1 {
+ font-size: 1.3rem;
+ font-weight: 500;
+ }
+ > p {
+ font-size: 0.95rem;
+ }
+ & footer {
+ display: flex;
+ justify-content: space-around;
+ }
+ }
+}
+p {
+ color: var(--font-color-secondary);
+}
+
+textarea {
+ grid-area: text;
+ width: 100%;
+ padding: 0.5rem;
+ border-radius: 0.4rem;
+ border: 1px solid var(--Light-gray);
+ resize: none;
+}
diff --git a/examples/interactive-comments-section/src/vite-env.d.ts b/examples/interactive-comments-section/src/vite-env.d.ts
new file mode 100644
index 00000000..11f02fe2
--- /dev/null
+++ b/examples/interactive-comments-section/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/examples/interactive-comments-section/tsconfig.json b/examples/interactive-comments-section/tsconfig.json
new file mode 100644
index 00000000..75abdef2
--- /dev/null
+++ b/examples/interactive-comments-section/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/interactive-comments-section/vite.config.js b/examples/interactive-comments-section/vite.config.js
new file mode 100644
index 00000000..245c81ec
--- /dev/null
+++ b/examples/interactive-comments-section/vite.config.js
@@ -0,0 +1,11 @@
+import { defineConfig } from "vite";
+
+export default defineConfig(({ command, mode, ssrBuild }) => {
+ return {
+ base: "/bau/frontendmentor/interactive-comments-section/",
+ build: { outDir: "../../dist/frontendmentor/interactive-comments-section" },
+ server: {
+ open: true,
+ },
+ };
+});