@vertz/create-vertz-app 0.2.20 → 0.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/create-vertz-app.ts
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
3
5
|
import { Command } from 'commander';
|
|
4
|
-
|
|
6
|
+
|
|
7
|
+
const pkg = JSON.parse(readFileSync(resolve(import.meta.dir, '../package.json'), 'utf-8'));
|
|
5
8
|
|
|
6
9
|
const program = new Command();
|
|
7
10
|
|
|
8
11
|
program
|
|
9
12
|
.name('create-vertz-app')
|
|
10
13
|
.description('Scaffold a new Vertz project')
|
|
11
|
-
.version(
|
|
14
|
+
.version(pkg.version)
|
|
12
15
|
.argument('[name]', 'Project name')
|
|
13
16
|
.action(async (name: string | undefined) => {
|
|
17
|
+
const { resolveOptions, scaffold } = await import('../dist/index.js');
|
|
14
18
|
try {
|
|
15
19
|
const resolved = await resolveOptions({ projectName: name });
|
|
16
20
|
|
|
17
|
-
console.log(`Creating Vertz app: ${resolved.projectName}`);
|
|
21
|
+
console.log(`Creating Vertz app: ${resolved.projectName} (v${pkg.version})`);
|
|
18
22
|
|
|
19
23
|
const targetDir = process.cwd();
|
|
20
24
|
await scaffold(targetDir, resolved);
|
|
@@ -59,7 +59,7 @@ export declare function serverTemplate(): string;
|
|
|
59
59
|
*/
|
|
60
60
|
export declare function schemaTemplate(): string;
|
|
61
61
|
/**
|
|
62
|
-
* src/api/db.ts —
|
|
62
|
+
* src/api/db.ts — createDb with local SQLite and autoApply migrations
|
|
63
63
|
*/
|
|
64
64
|
export declare function dbTemplate(): string;
|
|
65
65
|
/**
|
|
@@ -79,11 +79,13 @@ export declare function appComponentTemplate(): string;
|
|
|
79
79
|
*/
|
|
80
80
|
export declare function entryClientTemplate(): string;
|
|
81
81
|
/**
|
|
82
|
-
* src/styles/theme.ts —
|
|
82
|
+
* src/styles/theme.ts — configureTheme from @vertz/theme-shadcn
|
|
83
83
|
*/
|
|
84
84
|
export declare function themeTemplate(): string;
|
|
85
85
|
/**
|
|
86
|
-
* src/pages/home.tsx — task list
|
|
86
|
+
* src/pages/home.tsx — full CRUD task list with form, checkbox toggle,
|
|
87
|
+
* delete confirmation dialog, and animated list transitions.
|
|
88
|
+
* Demonstrates theme components (Button, Input, AlertDialog) over raw HTML.
|
|
87
89
|
*/
|
|
88
90
|
export declare function homePageTemplate(): string;
|
|
89
91
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CA6B5D;AAED;;GAEG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAyJnD;AAED;;GAEG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CA6B5D;AAED;;GAEG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAyJnD;AAED;;GAEG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CAuQlD;AAID;;GAEG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAGxC;AAID;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CA6B/D;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAoBzC;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAa5C;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,MAAM,CAIpC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAI3C;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAIvC;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAc9C;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CA6B1C;AAID;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAW1C;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAoBvC;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAavC;AAED;;GAEG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAWnC;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAe5C;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAOvC;AAED;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAgD7C;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAW5C;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAYtC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CA4LzC"}
|
package/dist/templates/index.js
CHANGED
|
@@ -266,7 +266,7 @@ Never use \`appendChild\`, \`innerHTML\`, \`textContent\`, \`document.createElem
|
|
|
266
266
|
|
|
267
267
|
\`\`\`tsx
|
|
268
268
|
// RIGHT
|
|
269
|
-
return <div
|
|
269
|
+
return <div className={styles.panel}>{title}</div>;
|
|
270
270
|
|
|
271
271
|
// WRONG — no imperative DOM
|
|
272
272
|
const el = document.createElement('div');
|
|
@@ -288,7 +288,7 @@ TaskCard({ task, onClick: handleClick });
|
|
|
288
288
|
\`\`\`tsx
|
|
289
289
|
{isLoading && <div>Loading...</div>}
|
|
290
290
|
|
|
291
|
-
{error ? <div
|
|
291
|
+
{error ? <div className={styles.error}>{error.message}</div> : <div>{content}</div>}
|
|
292
292
|
|
|
293
293
|
{tasks.map((task) => (
|
|
294
294
|
<TaskItem key={task.id} task={task} />
|
|
@@ -328,40 +328,85 @@ After mutations (\`create\`, \`update\`, \`delete\`), related queries are automa
|
|
|
328
328
|
refetched in the background. No manual \`refetch()\` calls needed — the framework
|
|
329
329
|
handles cache invalidation via optimistic updates.
|
|
330
330
|
|
|
331
|
-
##
|
|
331
|
+
## Theme Components — Prefer Over Raw HTML
|
|
332
|
+
|
|
333
|
+
When a themed component exists, use it instead of raw HTML elements with manual class names.
|
|
334
|
+
Theme components are pre-configured with the app's design tokens and provide consistent styling.
|
|
332
335
|
|
|
333
|
-
###
|
|
336
|
+
### Using Components
|
|
334
337
|
|
|
335
338
|
\`\`\`tsx
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
339
|
+
import { themeComponents } from '../styles/theme';
|
|
340
|
+
|
|
341
|
+
const { Button, Input } = themeComponents;
|
|
342
|
+
const { AlertDialog } = themeComponents.primitives;
|
|
343
|
+
|
|
344
|
+
// RIGHT — use theme components
|
|
345
|
+
<Button intent="primary" size="md">Submit</Button>
|
|
346
|
+
<Input placeholder="Enter text" />
|
|
347
|
+
|
|
348
|
+
// WRONG — raw HTML with manual styles
|
|
349
|
+
<button className={button({ intent: 'primary', size: 'md' })}>Submit</button>
|
|
350
|
+
<input className={inputStyles.base} placeholder="Enter text" />
|
|
351
|
+
\`\`\`
|
|
352
|
+
|
|
353
|
+
### Available Components
|
|
354
|
+
|
|
355
|
+
**Direct** (from \`themeComponents\`): \`Button\`, \`Input\`, \`Label\`, \`Badge\`, \`Textarea\`,
|
|
356
|
+
\`Card\` suite, \`Table\` suite, \`Avatar\` suite, \`FormGroup\` suite
|
|
341
357
|
|
|
342
|
-
|
|
358
|
+
**Primitives** (from \`themeComponents.primitives\`): \`AlertDialog\`, \`Dialog\`, \`Tabs\`,
|
|
359
|
+
\`Select\`, \`DropdownMenu\`, \`Popover\`, \`Sheet\`, \`Tooltip\`, \`Accordion\`
|
|
360
|
+
— all with sub-components (\`.Trigger\`, \`.Content\`, \`.Footer\`, etc.)
|
|
361
|
+
|
|
362
|
+
## Dialogs
|
|
363
|
+
|
|
364
|
+
### Composable \`<AlertDialog>\` for inline confirmations
|
|
365
|
+
|
|
366
|
+
\`\`\`tsx
|
|
367
|
+
const { Button } = themeComponents;
|
|
368
|
+
const { AlertDialog } = themeComponents.primitives;
|
|
369
|
+
|
|
370
|
+
<AlertDialog>
|
|
371
|
+
<AlertDialog.Trigger>
|
|
372
|
+
<Button intent="danger" size="sm">Delete</Button>
|
|
373
|
+
</AlertDialog.Trigger>
|
|
374
|
+
<AlertDialog.Content>
|
|
375
|
+
<AlertDialog.Title>Delete task?</AlertDialog.Title>
|
|
376
|
+
<AlertDialog.Description>This action cannot be undone.</AlertDialog.Description>
|
|
377
|
+
<AlertDialog.Footer>
|
|
378
|
+
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
|
|
379
|
+
<AlertDialog.Action onClick={handleDelete}>Delete</AlertDialog.Action>
|
|
380
|
+
</AlertDialog.Footer>
|
|
381
|
+
</AlertDialog.Content>
|
|
382
|
+
</AlertDialog>
|
|
343
383
|
\`\`\`
|
|
344
384
|
|
|
345
|
-
### \`
|
|
385
|
+
### \`useDialogStack()\` for imperative/stacked dialogs
|
|
386
|
+
|
|
387
|
+
Use when you need promise-based results or dialogs opened from event handlers:
|
|
346
388
|
|
|
347
389
|
\`\`\`tsx
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
390
|
+
import { useDialogStack } from 'vertz/ui';
|
|
391
|
+
|
|
392
|
+
const dialogs = useDialogStack();
|
|
393
|
+
const confirmed = await dialogs.open(ConfirmDialog, { message: 'Delete?' });
|
|
394
|
+
if (confirmed) handleDelete();
|
|
395
|
+
\`\`\`
|
|
396
|
+
|
|
397
|
+
## Styling
|
|
398
|
+
|
|
399
|
+
### \`css()\` for layout and custom styles
|
|
400
|
+
|
|
401
|
+
Use \`css()\` for layout-specific styles that don't correspond to a theme component:
|
|
402
|
+
|
|
403
|
+
\`\`\`tsx
|
|
404
|
+
const styles = css({
|
|
405
|
+
container: ['flex', 'flex-col', 'gap:4', 'p:6'],
|
|
406
|
+
heading: ['font:xl', 'font:bold', 'text:foreground'],
|
|
362
407
|
});
|
|
363
408
|
|
|
364
|
-
<
|
|
409
|
+
return <div className={styles.container}>...</div>;
|
|
365
410
|
\`\`\`
|
|
366
411
|
|
|
367
412
|
### Style Tokens
|
|
@@ -615,7 +660,7 @@ export function schemaTemplate() {
|
|
|
615
660
|
|
|
616
661
|
export const tasksTable = d.table('tasks', {
|
|
617
662
|
id: d.uuid().primary(),
|
|
618
|
-
title: d.text(),
|
|
663
|
+
title: d.text().min(1),
|
|
619
664
|
completed: d.boolean().default(false),
|
|
620
665
|
createdAt: d.timestamp().default('now').readOnly(),
|
|
621
666
|
updatedAt: d.timestamp().autoUpdate().readOnly(),
|
|
@@ -625,14 +670,16 @@ export const tasksModel = d.model(tasksTable);
|
|
|
625
670
|
`;
|
|
626
671
|
}
|
|
627
672
|
/**
|
|
628
|
-
* src/api/db.ts —
|
|
673
|
+
* src/api/db.ts — createDb with local SQLite and autoApply migrations
|
|
629
674
|
*/
|
|
630
675
|
export function dbTemplate() {
|
|
631
|
-
return `import {
|
|
632
|
-
import {
|
|
676
|
+
return `import { createDb } from 'vertz/db';
|
|
677
|
+
import { tasksModel } from './schema';
|
|
633
678
|
|
|
634
|
-
export const db =
|
|
635
|
-
|
|
679
|
+
export const db = createDb({
|
|
680
|
+
models: { tasks: tasksModel },
|
|
681
|
+
dialect: 'sqlite',
|
|
682
|
+
path: '.vertz/data/app.db',
|
|
636
683
|
migrations: { autoApply: true },
|
|
637
684
|
});
|
|
638
685
|
`;
|
|
@@ -705,11 +752,11 @@ export function App() {
|
|
|
705
752
|
return (
|
|
706
753
|
<div data-testid="app-root">
|
|
707
754
|
<ThemeProvider theme="light">
|
|
708
|
-
<div
|
|
709
|
-
<header
|
|
710
|
-
<div
|
|
755
|
+
<div className={styles.shell}>
|
|
756
|
+
<header className={styles.header}>
|
|
757
|
+
<div className={styles.title}>My Vertz App</div>
|
|
711
758
|
</header>
|
|
712
|
-
<main
|
|
759
|
+
<main className={styles.main}>
|
|
713
760
|
<HomePage />
|
|
714
761
|
</main>
|
|
715
762
|
</div>
|
|
@@ -735,22 +782,25 @@ mount(App, {
|
|
|
735
782
|
`;
|
|
736
783
|
}
|
|
737
784
|
/**
|
|
738
|
-
* src/styles/theme.ts —
|
|
785
|
+
* src/styles/theme.ts — configureTheme from @vertz/theme-shadcn
|
|
739
786
|
*/
|
|
740
787
|
export function themeTemplate() {
|
|
741
|
-
return `import {
|
|
788
|
+
return `import { configureTheme } from '@vertz/theme-shadcn';
|
|
742
789
|
|
|
743
|
-
const { theme, globals } =
|
|
790
|
+
const { theme, globals, components } = configureTheme({
|
|
744
791
|
palette: 'zinc',
|
|
745
792
|
radius: 'md',
|
|
746
793
|
});
|
|
747
794
|
|
|
748
795
|
export const appTheme = theme;
|
|
749
796
|
export const themeGlobals = globals;
|
|
797
|
+
export const themeComponents = components;
|
|
750
798
|
`;
|
|
751
799
|
}
|
|
752
800
|
/**
|
|
753
|
-
* src/pages/home.tsx — task list
|
|
801
|
+
* src/pages/home.tsx — full CRUD task list with form, checkbox toggle,
|
|
802
|
+
* delete confirmation dialog, and animated list transitions.
|
|
803
|
+
* Demonstrates theme components (Button, Input, AlertDialog) over raw HTML.
|
|
754
804
|
*/
|
|
755
805
|
export function homePageTemplate() {
|
|
756
806
|
return `import {
|
|
@@ -766,8 +816,12 @@ export function homePageTemplate() {
|
|
|
766
816
|
slideInFromTop,
|
|
767
817
|
} from 'vertz/ui';
|
|
768
818
|
import { api } from '../client';
|
|
819
|
+
import { themeComponents } from '../styles/theme';
|
|
769
820
|
|
|
770
|
-
|
|
821
|
+
const { Button } = themeComponents;
|
|
822
|
+
const { AlertDialog } = themeComponents.primitives;
|
|
823
|
+
|
|
824
|
+
// Global CSS for list item enter/exit animations
|
|
771
825
|
void globalCss({
|
|
772
826
|
'[data-presence="enter"]': {
|
|
773
827
|
animation: \`\${slideInFromTop} \${ANIMATION_DURATION} \${ANIMATION_EASING}\`,
|
|
@@ -778,31 +832,23 @@ void globalCss({
|
|
|
778
832
|
},
|
|
779
833
|
});
|
|
780
834
|
|
|
781
|
-
const
|
|
835
|
+
const styles = css({
|
|
782
836
|
container: ['py:2', 'w:full'],
|
|
783
837
|
heading: ['font:xl', 'font:bold', 'text:foreground', 'mb:4'],
|
|
784
|
-
form: ['flex', '
|
|
838
|
+
form: ['flex', 'items:start', 'gap:2', 'mb:6'],
|
|
785
839
|
inputWrap: ['flex-1'],
|
|
786
840
|
input: [
|
|
787
841
|
'w:full',
|
|
842
|
+
'h:10',
|
|
788
843
|
'px:3',
|
|
789
|
-
'py:2',
|
|
790
844
|
'rounded:md',
|
|
791
845
|
'border:1',
|
|
792
846
|
'border:border',
|
|
793
847
|
'bg:background',
|
|
794
848
|
'text:foreground',
|
|
849
|
+
'text:sm',
|
|
795
850
|
],
|
|
796
851
|
fieldError: ['text:destructive', 'font:xs', 'mt:1'],
|
|
797
|
-
button: [
|
|
798
|
-
'px:4',
|
|
799
|
-
'py:2',
|
|
800
|
-
'rounded:md',
|
|
801
|
-
'bg:primary',
|
|
802
|
-
'text:primary-foreground',
|
|
803
|
-
'font:medium',
|
|
804
|
-
'cursor:pointer',
|
|
805
|
-
],
|
|
806
852
|
list: ['flex', 'flex-col', 'gap:2'],
|
|
807
853
|
item: [
|
|
808
854
|
'flex',
|
|
@@ -814,12 +860,63 @@ const pageStyles = css({
|
|
|
814
860
|
'border:1',
|
|
815
861
|
'border:border',
|
|
816
862
|
'bg:card',
|
|
863
|
+
'hover:bg:accent',
|
|
864
|
+
'transition:colors',
|
|
817
865
|
],
|
|
866
|
+
checkbox: ['w:4', 'h:4', 'cursor:pointer', 'rounded:sm'],
|
|
867
|
+
label: ['flex-1', 'text:sm', 'text:foreground'],
|
|
868
|
+
labelDone: ['flex-1', 'text:sm', 'text:muted-foreground', 'decoration:line-through'],
|
|
818
869
|
loading: ['text:muted-foreground'],
|
|
819
870
|
error: ['text:destructive'],
|
|
820
871
|
empty: ['text:muted-foreground', 'text:center', 'py:8'],
|
|
872
|
+
count: ['text:xs', 'text:muted-foreground', 'mt:4'],
|
|
821
873
|
});
|
|
822
874
|
|
|
875
|
+
interface TaskItemProps {
|
|
876
|
+
id: string;
|
|
877
|
+
title: string;
|
|
878
|
+
completed: boolean;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function TaskItem({ id, title, completed }: TaskItemProps) {
|
|
882
|
+
const handleToggle = async () => {
|
|
883
|
+
await api.tasks.update(id, { completed: !completed });
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
const handleDelete = async () => {
|
|
887
|
+
await api.tasks.delete(id);
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
return (
|
|
891
|
+
<div className={styles.item}>
|
|
892
|
+
<input
|
|
893
|
+
type="checkbox"
|
|
894
|
+
className={styles.checkbox}
|
|
895
|
+
checked={completed}
|
|
896
|
+
onChange={handleToggle}
|
|
897
|
+
/>
|
|
898
|
+
<span className={completed ? styles.labelDone : styles.label}>
|
|
899
|
+
{title}
|
|
900
|
+
</span>
|
|
901
|
+
<AlertDialog onAction={handleDelete}>
|
|
902
|
+
<AlertDialog.Trigger>
|
|
903
|
+
<Button intent="ghost" size="sm">Delete</Button>
|
|
904
|
+
</AlertDialog.Trigger>
|
|
905
|
+
<AlertDialog.Content>
|
|
906
|
+
<AlertDialog.Title>Delete task?</AlertDialog.Title>
|
|
907
|
+
<AlertDialog.Description>
|
|
908
|
+
This action cannot be undone.
|
|
909
|
+
</AlertDialog.Description>
|
|
910
|
+
<AlertDialog.Footer>
|
|
911
|
+
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
|
|
912
|
+
<AlertDialog.Action>Delete</AlertDialog.Action>
|
|
913
|
+
</AlertDialog.Footer>
|
|
914
|
+
</AlertDialog.Content>
|
|
915
|
+
</AlertDialog>
|
|
916
|
+
</div>
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
|
|
823
920
|
export function HomePage() {
|
|
824
921
|
const tasksQuery = query(api.tasks.list());
|
|
825
922
|
|
|
@@ -828,61 +925,64 @@ export function HomePage() {
|
|
|
828
925
|
});
|
|
829
926
|
|
|
830
927
|
return (
|
|
831
|
-
<div
|
|
832
|
-
<h1
|
|
928
|
+
<div className={styles.container} data-testid="home-page">
|
|
929
|
+
<h1 className={styles.heading}>Tasks</h1>
|
|
833
930
|
|
|
834
931
|
<form
|
|
835
|
-
|
|
932
|
+
className={styles.form}
|
|
836
933
|
action={taskForm.action}
|
|
837
934
|
method={taskForm.method}
|
|
838
935
|
onSubmit={taskForm.onSubmit}
|
|
839
936
|
>
|
|
840
|
-
<div
|
|
937
|
+
<div className={styles.inputWrap}>
|
|
841
938
|
<input
|
|
842
939
|
name={taskForm.fields.title}
|
|
843
|
-
|
|
940
|
+
className={styles.input}
|
|
844
941
|
placeholder="What needs to be done?"
|
|
845
942
|
/>
|
|
846
|
-
<span
|
|
943
|
+
<span className={styles.fieldError}>
|
|
847
944
|
{taskForm.title.error}
|
|
848
945
|
</span>
|
|
849
946
|
</div>
|
|
850
|
-
<
|
|
851
|
-
type="submit"
|
|
852
|
-
class={pageStyles.button}
|
|
853
|
-
disabled={taskForm.submitting}
|
|
854
|
-
>
|
|
947
|
+
<Button type="submit" disabled={taskForm.submitting}>
|
|
855
948
|
{taskForm.submitting.value ? 'Adding...' : 'Add'}
|
|
856
|
-
</
|
|
949
|
+
</Button>
|
|
857
950
|
</form>
|
|
858
951
|
|
|
859
952
|
{queryMatch(tasksQuery, {
|
|
860
953
|
loading: () => (
|
|
861
|
-
<div
|
|
954
|
+
<div className={styles.loading}>Loading tasks...</div>
|
|
862
955
|
),
|
|
863
956
|
error: (err) => (
|
|
864
|
-
<div
|
|
957
|
+
<div className={styles.error}>
|
|
865
958
|
{err instanceof Error ? err.message : String(err)}
|
|
866
959
|
</div>
|
|
867
960
|
),
|
|
868
961
|
data: (response) => (
|
|
869
962
|
<>
|
|
870
963
|
{response.items.length === 0 && (
|
|
871
|
-
<div
|
|
964
|
+
<div className={styles.empty}>
|
|
872
965
|
No tasks yet. Add one above!
|
|
873
966
|
</div>
|
|
874
967
|
)}
|
|
875
|
-
<div data-testid="task-list"
|
|
968
|
+
<div data-testid="task-list" className={styles.list}>
|
|
876
969
|
<ListTransition
|
|
877
970
|
each={response.items}
|
|
878
971
|
keyFn={(task) => task.id}
|
|
879
972
|
children={(task) => (
|
|
880
|
-
<
|
|
881
|
-
|
|
882
|
-
|
|
973
|
+
<TaskItem
|
|
974
|
+
id={task.id}
|
|
975
|
+
title={task.title}
|
|
976
|
+
completed={task.completed}
|
|
977
|
+
/>
|
|
883
978
|
)}
|
|
884
979
|
/>
|
|
885
980
|
</div>
|
|
981
|
+
{response.items.length > 0 && (
|
|
982
|
+
<div className={styles.count}>
|
|
983
|
+
{response.items.filter((t) => !t.completed).length} remaining
|
|
984
|
+
</div>
|
|
985
|
+
)}
|
|
886
986
|
</>
|
|
887
987
|
),
|
|
888
988
|
})}
|