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, + }, + }; +});