diff --git a/docs/components/radio.md b/docs/components/radio.md index ea7991f..b11dbd0 100644 --- a/docs/components/radio.md +++ b/docs/components/radio.md @@ -1,6 +1,6 @@ # Radio -Radio button component with label, tooltip support, and reactive group selection. +Radio button component with label, tooltip support, and reactive group selection. All radios in the same group share a common `name` attribute for proper HTML semantics. ## Tag @@ -11,8 +11,9 @@ Radio button component with label, tooltip support, and reactive group selection | Prop | Type | Default | Description | | :----------- | :--------------------------- | :---------- | :----------------------------------------------- | | `label` | `string` | `-` | Label text for the radio button | -| `value` | `string \| Signal` | `-` | Selected value (for group) | +| `value` | `string \| Signal` | `-` | Selected value signal for the group | | `radioValue` | `string` | `-` | Value of this radio button | +| `name` | `string` | `-` | Group name (all radios in group should share this) | | `tooltip` | `string` | `-` | Tooltip text on hover | | `disabled` | `boolean \| Signal` | `false` | Disabled state | | `class` | `string` | `''` | Additional CSS classes (DaisyUI + Tailwind) | @@ -36,18 +37,21 @@ const BasicDemo = () => { return Div({ class: 'flex flex-col gap-3' }, [ Radio({ label: 'Option 1', + name: 'basic-group', value: selected, radioValue: 'option1', onclick: () => selected('option1') }), Radio({ label: 'Option 2', + name: 'basic-group', value: selected, radioValue: 'option2', onclick: () => selected('option2') }), Radio({ label: 'Option 3', + name: 'basic-group', value: selected, radioValue: 'option3', onclick: () => selected('option3') @@ -74,6 +78,7 @@ const TooltipDemo = () => { return Div({ class: 'flex flex-col gap-3' }, [ Radio({ label: 'Light mode', + name: 'theme-group', value: selected, radioValue: 'light', tooltip: 'Light theme with white background', @@ -81,6 +86,7 @@ const TooltipDemo = () => { }), Radio({ label: 'Dark mode', + name: 'theme-group', value: selected, radioValue: 'dark', tooltip: 'Dark theme with black background', @@ -107,12 +113,14 @@ const DisabledDemo = () => { return Div({ class: 'flex flex-col gap-3' }, [ Radio({ label: 'Enabled option', + name: 'disabled-group', value: selected, radioValue: 'enabled', onclick: () => selected('enabled') }), Radio({ label: 'Disabled option (cannot select)', + name: 'disabled-group', value: selected, radioValue: 'disabled', disabled: true, @@ -144,9 +152,9 @@ const ReactiveDemo = () => { ]; const colors = [ - { value: 'blue', label: 'Blue', class: 'text-blue-600' }, - { value: 'green', label: 'Green', class: 'text-green-600' }, - { value: 'red', label: 'Red', class: 'text-red-600' } + { value: 'blue', label: 'Blue' }, + { value: 'green', label: 'Green' }, + { value: 'red', label: 'Red' } ]; return Div({ class: 'flex flex-col gap-4' }, [ @@ -154,6 +162,7 @@ const ReactiveDemo = () => { Div({ class: 'flex gap-4' }, sizes.map(s => Radio({ label: s.label, + name: 'size-group', value: size, radioValue: s.value, onclick: () => size(s.value) @@ -163,6 +172,7 @@ const ReactiveDemo = () => { Div({ class: 'flex gap-4' }, colors.map(c => Radio({ label: c.label, + name: 'color-group', value: color, radioValue: c.value, onclick: () => color(c.value) @@ -199,18 +209,21 @@ const PaymentDemo = () => { Div({ class: 'flex flex-col gap-3' }, [ Radio({ label: '💳 Credit Card', + name: 'payment-group', value: method, radioValue: 'credit', onclick: () => method('credit') }), Radio({ label: '🏦 Bank Transfer', + name: 'payment-group', value: method, radioValue: 'bank', onclick: () => method('bank') }), Radio({ label: '📱 Digital Wallet', + name: 'payment-group', value: method, radioValue: 'wallet', onclick: () => method('wallet') @@ -250,6 +263,7 @@ const VariantsDemo = () => { Div({ class: 'flex gap-4' }, [ Radio({ label: 'Option A', + name: 'primary-group', value: primary, radioValue: 'primary1', class: 'radio-primary', @@ -257,6 +271,7 @@ const VariantsDemo = () => { }), Radio({ label: 'Option B', + name: 'primary-group', value: primary, radioValue: 'primary2', class: 'radio-primary', @@ -269,6 +284,7 @@ const VariantsDemo = () => { Div({ class: 'flex gap-4' }, [ Radio({ label: 'Option A', + name: 'secondary-group', value: secondary, radioValue: 'secondary1', class: 'radio-secondary', @@ -276,6 +292,7 @@ const VariantsDemo = () => { }), Radio({ label: 'Option B', + name: 'secondary-group', value: secondary, radioValue: 'secondary2', class: 'radio-secondary', @@ -288,6 +305,7 @@ const VariantsDemo = () => { Div({ class: 'flex gap-4' }, [ Radio({ label: 'Option A', + name: 'accent-group', value: accent, radioValue: 'accent1', class: 'radio-accent', @@ -295,6 +313,7 @@ const VariantsDemo = () => { }), Radio({ label: 'Option B', + name: 'accent-group', value: accent, radioValue: 'accent2', class: 'radio-accent', @@ -338,6 +357,7 @@ const DynamicDemo = () => { Div({ class: 'flex gap-4' }, [ Radio({ label: 'Cars', + name: 'category-group', value: category, radioValue: 'cars', onclick: () => { @@ -347,6 +367,7 @@ const DynamicDemo = () => { }), Radio({ label: 'Colors', + name: 'category-group', value: category, radioValue: 'colors', onclick: () => { @@ -361,6 +382,7 @@ const DynamicDemo = () => { options[category()].map(opt => Radio({ label: opt.label, + name: 'dynamic-option-group', value: selected, radioValue: opt.value, onclick: () => selected(opt.value) @@ -388,18 +410,21 @@ $mount(DynamicDemo, '#demo-dynamic'); return Div({ class: 'flex flex-col gap-3' }, [ Radio({ label: 'Option 1', + name: 'basic-group', value: selected, radioValue: 'option1', onclick: () => selected('option1') }), Radio({ label: 'Option 2', + name: 'basic-group', value: selected, radioValue: 'option2', onclick: () => selected('option2') }), Radio({ label: 'Option 3', + name: 'basic-group', value: selected, radioValue: 'option3', onclick: () => selected('option3') @@ -419,6 +444,7 @@ $mount(DynamicDemo, '#demo-dynamic'); return Div({ class: 'flex flex-col gap-3' }, [ Radio({ label: 'Light mode', + name: 'theme-group', value: selected, radioValue: 'light', tooltip: 'Light theme with white background', @@ -426,6 +452,7 @@ $mount(DynamicDemo, '#demo-dynamic'); }), Radio({ label: 'Dark mode', + name: 'theme-group', value: selected, radioValue: 'dark', tooltip: 'Dark theme with black background', @@ -445,12 +472,14 @@ $mount(DynamicDemo, '#demo-dynamic'); return Div({ class: 'flex flex-col gap-3' }, [ Radio({ label: 'Enabled option', + name: 'disabled-group', value: selected, radioValue: 'enabled', onclick: () => selected('enabled') }), Radio({ label: 'Disabled option (cannot select)', + name: 'disabled-group', value: selected, radioValue: 'disabled', disabled: true, @@ -475,9 +504,9 @@ $mount(DynamicDemo, '#demo-dynamic'); ]; const colors = [ - { value: 'blue', label: 'Blue', class: 'text-blue-600' }, - { value: 'green', label: 'Green', class: 'text-green-600' }, - { value: 'red', label: 'Red', class: 'text-red-600' } + { value: 'blue', label: 'Blue' }, + { value: 'green', label: 'Green' }, + { value: 'red', label: 'Red' } ]; return Div({ class: 'flex flex-col gap-4' }, [ @@ -485,6 +514,7 @@ $mount(DynamicDemo, '#demo-dynamic'); Div({ class: 'flex gap-4' }, sizes.map(s => Radio({ label: s.label, + name: 'size-group', value: size, radioValue: s.value, onclick: () => size(s.value) @@ -494,6 +524,7 @@ $mount(DynamicDemo, '#demo-dynamic'); Div({ class: 'flex gap-4' }, colors.map(c => Radio({ label: c.label, + name: 'color-group', value: color, radioValue: c.value, onclick: () => color(c.value) @@ -523,18 +554,21 @@ $mount(DynamicDemo, '#demo-dynamic'); Div({ class: 'flex flex-col gap-3' }, [ Radio({ label: '💳 Credit Card', + name: 'payment-group', value: method, radioValue: 'credit', onclick: () => method('credit') }), Radio({ label: '🏦 Bank Transfer', + name: 'payment-group', value: method, radioValue: 'bank', onclick: () => method('bank') }), Radio({ label: '📱 Digital Wallet', + name: 'payment-group', value: method, radioValue: 'wallet', onclick: () => method('wallet') @@ -567,6 +601,7 @@ $mount(DynamicDemo, '#demo-dynamic'); Div({ class: 'flex gap-4' }, [ Radio({ label: 'Option A', + name: 'primary-group', value: primary, radioValue: 'primary1', class: 'radio-primary', @@ -574,6 +609,7 @@ $mount(DynamicDemo, '#demo-dynamic'); }), Radio({ label: 'Option B', + name: 'primary-group', value: primary, radioValue: 'primary2', class: 'radio-primary', @@ -586,6 +622,7 @@ $mount(DynamicDemo, '#demo-dynamic'); Div({ class: 'flex gap-4' }, [ Radio({ label: 'Option A', + name: 'secondary-group', value: secondary, radioValue: 'secondary1', class: 'radio-secondary', @@ -593,6 +630,7 @@ $mount(DynamicDemo, '#demo-dynamic'); }), Radio({ label: 'Option B', + name: 'secondary-group', value: secondary, radioValue: 'secondary2', class: 'radio-secondary', @@ -605,6 +643,7 @@ $mount(DynamicDemo, '#demo-dynamic'); Div({ class: 'flex gap-4' }, [ Radio({ label: 'Option A', + name: 'accent-group', value: accent, radioValue: 'accent1', class: 'radio-accent', @@ -612,6 +651,7 @@ $mount(DynamicDemo, '#demo-dynamic'); }), Radio({ label: 'Option B', + name: 'accent-group', value: accent, radioValue: 'accent2', class: 'radio-accent', @@ -648,6 +688,7 @@ $mount(DynamicDemo, '#demo-dynamic'); Div({ class: 'flex gap-4' }, [ Radio({ label: 'Cars', + name: 'category-group', value: category, radioValue: 'cars', onclick: () => { @@ -657,6 +698,7 @@ $mount(DynamicDemo, '#demo-dynamic'); }), Radio({ label: 'Colors', + name: 'category-group', value: category, radioValue: 'colors', onclick: () => { @@ -671,6 +713,7 @@ $mount(DynamicDemo, '#demo-dynamic'); options[category()].map(opt => Radio({ label: opt.label, + name: 'dynamic-option-group', value: selected, radioValue: opt.value, onclick: () => selected(opt.value) diff --git a/docs/components/rating.md b/docs/components/rating.md index e69de29..d816cae 100644 --- a/docs/components/rating.md +++ b/docs/components/rating.md @@ -0,0 +1,536 @@ +# Rating + +Star rating component with customizable count, icons, and read-only mode. + +## Tag + +`Rating` + +## Props + +| Prop | Type | Default | Description | +| :----------- | :--------------------------- | :--------------- | :----------------------------------------------- | +| `value` | `number \| Signal` | `0` | Current rating value | +| `count` | `number \| Signal` | `5` | Number of stars/items | +| `mask` | `string` | `'mask-star'` | Mask shape (mask-star, mask-star-2, mask-heart) | +| `readonly` | `boolean \| Signal` | `false` | Disable interaction | +| `class` | `string` | `''` | Additional CSS classes (DaisyUI + Tailwind) | + +## Live Examples + +### Basic Rating + +
+
+

Live Demo

+
+
+
+ +```javascript +const BasicDemo = () => { + const rating = $(3); + + return Div({ class: 'flex flex-col gap-2 items-center' }, [ + Rating({ + value: rating, + count: 5, + onchange: (value) => rating(value) + }), + Div({ class: 'text-sm opacity-70' }, () => `Rating: ${rating()} / 5`) + ]); +}; +$mount(BasicDemo, '#demo-basic'); +``` + +### Heart Rating + +
+
+

Live Demo

+
+
+
+ +```javascript +const HeartDemo = () => { + const rating = $(4); + + return Div({ class: 'flex flex-col gap-2 items-center' }, [ + Rating({ + value: rating, + count: 5, + mask: 'mask-heart', + onchange: (value) => rating(value) + }), + Div({ class: 'text-sm opacity-70' }, () => `${rating()} hearts`) + ]); +}; +$mount(HeartDemo, '#demo-heart'); +``` + +### Star with Outline + +
+
+

Live Demo

+
+
+
+ +```javascript +const Star2Demo = () => { + const rating = $(2); + + return Div({ class: 'flex flex-col gap-2 items-center' }, [ + Rating({ + value: rating, + count: 5, + mask: 'mask-star-2', + onchange: (value) => rating(value) + }), + Div({ class: 'text-sm opacity-70' }, () => `${rating()} stars`) + ]); +}; +$mount(Star2Demo, '#demo-star2'); +``` + +### Read-only Rating + +
+
+

Live Demo

+
+
+
+ +```javascript +const ReadonlyDemo = () => { + const rating = $(4.5); + + return Div({ class: 'flex flex-col gap-2 items-center' }, [ + Rating({ + value: rating, + count: 5, + readonly: true + }), + Div({ class: 'text-sm opacity-70' }, 'Average rating: 4.5/5 (read-only)') + ]); +}; +$mount(ReadonlyDemo, '#demo-readonly'); +``` + +### Custom Count + +
+
+

Live Demo

+
+
+
+ +```javascript +const CustomDemo = () => { + const rating = $(3); + const count = $(10); + + return Div({ class: 'flex flex-col gap-4 w-full' }, [ + Div({ class: 'flex items-center gap-4' }, [ + Span({ class: 'text-sm' }, 'Number of stars:'), + Input({ + type: 'number', + value: count, + class: 'input input-sm w-24', + oninput: (e) => count(parseInt(e.target.value) || 1) + }) + ]), + Rating({ + value: rating, + count: count, + onchange: (value) => rating(value) + }), + Div({ class: 'text-sm opacity-70' }, () => `Rating: ${rating()} / ${count()}`) + ]); +}; +$mount(CustomDemo, '#demo-custom'); +``` + +### Product Review + +
+
+

Live Demo

+
+
+
+ +```javascript +const ReviewDemo = () => { + const quality = $(4); + const price = $(3); + const support = $(5); + + const average = () => Math.round(((quality() + price() + support()) / 3) * 10) / 10; + + return Div({ class: 'flex flex-col gap-4' }, [ + Div({ class: 'text-lg font-bold' }, 'Product Review'), + Div({ class: 'flex flex-col gap-2' }, [ + Div({ class: 'flex justify-between items-center' }, [ + Span({ class: 'text-sm w-24' }, 'Quality:'), + Rating({ + value: quality, + count: 5, + size: 'sm', + onchange: (v) => quality(v) + }) + ]), + Div({ class: 'flex justify-between items-center' }, [ + Span({ class: 'text-sm w-24' }, 'Price:'), + Rating({ + value: price, + count: 5, + size: 'sm', + onchange: (v) => price(v) + }) + ]), + Div({ class: 'flex justify-between items-center' }, [ + Span({ class: 'text-sm w-24' }, 'Support:'), + Rating({ + value: support, + count: 5, + size: 'sm', + onchange: (v) => support(v) + }) + ]) + ]), + Div({ class: 'divider my-1' }), + Div({ class: 'flex justify-between items-center' }, [ + Span({ class: 'font-bold' }, 'Overall:'), + Div({ class: 'text-2xl font-bold text-primary' }, () => average()) + ]) + ]); +}; +$mount(ReviewDemo, '#demo-review'); +``` + +### All Variants + +
+
+

Live Demo

+
+
+
+ +```javascript +const VariantsDemo = () => { + return Div({ class: 'flex flex-col gap-6' }, [ + Div({ class: 'text-center' }, [ + Div({ class: 'text-sm mb-2' }, 'Mask Star'), + Rating({ value: $(3), count: 5, mask: 'mask-star' }) + ]), + Div({ class: 'text-center' }, [ + Div({ class: 'text-sm mb-2' }, 'Mask Star 2 (yellow)' ), + Rating({ value: $(4), count: 5, mask: 'mask-star-2', class: 'rating-warning' }) + ]), + Div({ class: 'text-center' }, [ + Div({ class: 'text-sm mb-2' }, 'Mask Heart'), + Rating({ value: $(5), count: 5, mask: 'mask-heart', class: 'rating-error' }) + ]), + Div({ class: 'text-center' }, [ + Div({ class: 'text-sm mb-2' }, 'Half Stars (read-only)'), + Rating({ value: $(3.5), count: 5, readonly: true }) + ]) + ]); +}; +$mount(VariantsDemo, '#demo-variants'); +``` + +### Interactive Feedback + +
+
+

Live Demo

+
+
+
+ +```javascript +const FeedbackDemo = () => { + const rating = $(0); + const feedback = $(false); + + const messages = { + 1: 'Very disappointed 😞', + 2: 'Could be better 😕', + 3: 'Good 👍', + 4: 'Very good 😊', + 5: 'Excellent! 🎉' + }; + + return Div({ class: 'flex flex-col gap-4 items-center' }, [ + Div({ class: 'text-center' }, [ + Div({ class: 'text-sm mb-2' }, 'How was your experience?'), + Rating({ + value: rating, + count: 5, + onchange: (value) => { + rating(value); + feedback(true); + if (value >= 4) { + Toast('Thank you for your positive feedback!', 'alert-success', 2000); + } else if (value <= 2) { + Toast('We appreciate your feedback and will improve!', 'alert-warning', 2000); + } else { + Toast('Thanks for your rating!', 'alert-info', 2000); + } + } + }) + ]), + () => rating() > 0 + ? Div({ class: 'alert alert-soft text-center' }, [ + messages[rating()] || `Rating: ${rating()} stars` + ]) + : null + ]); +}; +$mount(FeedbackDemo, '#demo-feedback'); +``` + + diff --git a/docs/components/swap.md b/docs/components/swap.md index e69de29..afb8d45 100644 --- a/docs/components/swap.md +++ b/docs/components/swap.md @@ -0,0 +1,500 @@ +# Swap + +Toggle component that swaps between two states (on/off) with customizable icons or content. + +## Tag + +`Swap` + +## Props + +| Prop | Type | Default | Description | +| :----------- | :--------------------------- | :---------- | :----------------------------------------------- | +| `value` | `boolean \| Signal` | `false` | Swap state (true = on, false = off) | +| `on` | `string \| VNode` | `-` | Content to show when state is on | +| `off` | `string \| VNode` | `-` | Content to show when state is off | +| `class` | `string` | `''` | Additional CSS classes (DaisyUI + Tailwind) | +| `onclick` | `function` | `-` | Click event handler | + +## Live Examples + +### Basic Swap + +
+
+

Live Demo

+
+
+
+ +```javascript +const BasicDemo = () => { + const isOn = $(false); + + return Swap({ + value: isOn, + on: "🌟 ON", + off: "💫 OFF", + onclick: () => isOn(!isOn()) + }); +}; +$mount(BasicDemo, '#demo-basic'); +``` + +### Icon Swap + +
+
+

Live Demo

+
+
+
+ +```javascript +const IconsDemo = () => { + const isOn = $(false); + + return Swap({ + value: isOn, + on: Icons.iconShow, + off: Icons.iconHide, + onclick: () => isOn(!isOn()) + }); +}; +$mount(IconsDemo, '#demo-icons'); +``` + +### Emoji Swap + +
+
+

Live Demo

+
+
+
+ +```javascript +const EmojiDemo = () => { + const isOn = $(false); + + return Swap({ + value: isOn, + on: "❤️", + off: "🖤", + onclick: () => isOn(!isOn()) + }); +}; +$mount(EmojiDemo, '#demo-emoji'); +``` + +### Custom Content Swap + +
+
+

Live Demo

+
+
+
+ +```javascript +const CustomDemo = () => { + const isOn = $(false); + + return Swap({ + value: isOn, + on: Div({ class: "badge badge-success gap-1" }, ["✅", " Active"]), + off: Div({ class: "badge badge-ghost gap-1" }, ["⭕", " Inactive"]), + onclick: () => isOn(!isOn()) + }); +}; +$mount(CustomDemo, '#demo-custom'); +``` + +### With Reactive State + +
+
+

Live Demo

+
+
+
+ +```javascript +const ReactiveDemo = () => { + const isOn = $(false); + + return Div({ class: 'flex flex-col gap-4 items-center' }, [ + Swap({ + value: isOn, + on: Icons.iconShow, + off: Icons.iconHide, + onclick: () => isOn(!isOn()) + }), + Div({ class: 'text-center' }, () => + isOn() + ? Div({ class: 'alert alert-success' }, 'Content is visible') + : Div({ class: 'alert alert-soft' }, 'Content is hidden') + ) + ]); +}; +$mount(ReactiveDemo, '#demo-reactive'); +``` + +### Toggle Mode Swap + +
+
+

Live Demo

+
+
+
+ +```javascript +const ModeDemo = () => { + const darkMode = $(false); + const notifications = $(true); + const sound = $(false); + + return Div({ class: 'flex flex-col gap-4 w-full' }, [ + Div({ class: 'flex justify-between items-center' }, [ + Span({}, 'Dark mode'), + Swap({ + value: darkMode, + on: "🌙", + off: "☀️", + onclick: () => darkMode(!darkMode()) + }) + ]), + Div({ class: 'flex justify-between items-center' }, [ + Span({}, 'Notifications'), + Swap({ + value: notifications, + on: "🔔", + off: "🔕", + onclick: () => notifications(!notifications()) + }) + ]), + Div({ class: 'flex justify-between items-center' }, [ + Span({}, 'Sound effects'), + Swap({ + value: sound, + on: "🔊", + off: "🔇", + onclick: () => sound(!sound()) + }) + ]), + Div({ class: 'mt-2 p-3 rounded-lg', style: () => darkMode() ? 'background: #1f2937; color: white' : 'background: #f3f4f6' }, [ + Div({ class: 'text-sm' }, () => `Mode: ${darkMode() ? 'Dark' : 'Light'} | Notifications: ${notifications() ? 'On' : 'Off'} | Sound: ${sound() ? 'On' : 'Off'}`) + ]) + ]); +}; +$mount(ModeDemo, '#demo-mode'); +``` + +### All Variants + +
+
+

Live Demo

+
+
+
+ +```javascript +const VariantsDemo = () => { + return Div({ class: 'flex flex-wrap gap-8 justify-center items-center' }, [ + Div({ class: 'text-center' }, [ + Div({ class: 'text-xs mb-2' }, 'Volume'), + Swap({ + value: $(false), + on: "🔊", + off: "🔇" + }) + ]), + Div({ class: 'text-center' }, [ + Div({ class: 'text-xs mb-2' }, 'Like'), + Swap({ + value: $(true), + on: "❤️", + off: "🤍" + }) + ]), + Div({ class: 'text-center' }, [ + Div({ class: 'text-xs mb-2' }, 'Star'), + Swap({ + value: $(false), + on: "⭐", + off: "☆" + }) + ]), + Div({ class: 'text-center' }, [ + Div({ class: 'text-xs mb-2' }, 'Check'), + Swap({ + value: $(true), + on: Icons.iconSuccess, + off: Icons.iconError + }) + ]) + ]); +}; +$mount(VariantsDemo, '#demo-variants'); +``` + +### Simple Todo Toggle + +
+
+

Live Demo

+
+
+
+ +```javascript +const TodoDemo = () => { + const todos = [ + { id: 1, text: 'Complete documentation', completed: $(true) }, + { id: 2, text: 'Review pull requests', completed: $(false) }, + { id: 3, text: 'Deploy to production', completed: $(false) } + ]; + + return Div({ class: 'flex flex-col gap-3' }, [ + Div({ class: 'text-sm font-bold mb-2' }, 'Todo list'), + ...todos.map(todo => + Div({ class: 'flex items-center justify-between p-2 bg-base-200 rounded-lg' }, [ + Span({ class: todo.completed() ? 'line-through opacity-50' : '' }, todo.text), + Swap({ + value: todo.completed, + on: Icons.iconSuccess, + off: Icons.iconClose, + onclick: () => todo.completed(!todo.completed()) + }) + ]) + ), + Div({ class: 'mt-2 text-sm opacity-70' }, () => { + const completed = todos.filter(t => t.completed()).length; + return `${completed} of ${todos.length} tasks completed`; + }) + ]); +}; +$mount(TodoDemo, '#demo-todo'); +``` + +