@vertz/create-vertz-app 0.2.20 → 0.2.22
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,CAqQlD;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,CActC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CA0LzC"}
|
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,83 @@ 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.
|
|
335
|
+
|
|
336
|
+
### Using Components
|
|
332
337
|
|
|
333
|
-
|
|
338
|
+
Import components from \`@vertz/ui/components\` — the centralized entrypoint:
|
|
334
339
|
|
|
335
340
|
\`\`\`tsx
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
+
import { Button, Input, AlertDialog } from '@vertz/ui/components';
|
|
342
|
+
|
|
343
|
+
// RIGHT — use theme components
|
|
344
|
+
<Button intent="primary" size="md">Submit</Button>
|
|
345
|
+
<Input placeholder="Enter text" />
|
|
346
|
+
|
|
347
|
+
// WRONG — raw HTML with manual styles
|
|
348
|
+
<button className={button({ intent: 'primary', size: 'md' })}>Submit</button>
|
|
349
|
+
<input className={inputStyles.base} placeholder="Enter text" />
|
|
350
|
+
\`\`\`
|
|
351
|
+
|
|
352
|
+
### Available Components
|
|
353
|
+
|
|
354
|
+
**Direct**: \`Button\`, \`Input\`, \`Label\`, \`Badge\`, \`Textarea\`,
|
|
355
|
+
\`Card\` suite, \`Table\` suite, \`Avatar\` suite, \`FormGroup\` suite
|
|
341
356
|
|
|
342
|
-
|
|
357
|
+
**Primitives**: \`AlertDialog\`, \`Dialog\`, \`Tabs\`,
|
|
358
|
+
\`Select\`, \`DropdownMenu\`, \`Popover\`, \`Sheet\`, \`Tooltip\`, \`Accordion\`
|
|
359
|
+
— all with sub-components (\`.Trigger\`, \`.Content\`, \`.Footer\`, etc.)
|
|
360
|
+
|
|
361
|
+
## Dialogs
|
|
362
|
+
|
|
363
|
+
### Composable \`<AlertDialog>\` for inline confirmations
|
|
364
|
+
|
|
365
|
+
\`\`\`tsx
|
|
366
|
+
import { Button, AlertDialog } from '@vertz/ui/components';
|
|
367
|
+
|
|
368
|
+
<AlertDialog>
|
|
369
|
+
<AlertDialog.Trigger>
|
|
370
|
+
<Button intent="danger" size="sm">Delete</Button>
|
|
371
|
+
</AlertDialog.Trigger>
|
|
372
|
+
<AlertDialog.Content>
|
|
373
|
+
<AlertDialog.Title>Delete task?</AlertDialog.Title>
|
|
374
|
+
<AlertDialog.Description>This action cannot be undone.</AlertDialog.Description>
|
|
375
|
+
<AlertDialog.Footer>
|
|
376
|
+
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
|
|
377
|
+
<AlertDialog.Action onClick={handleDelete}>Delete</AlertDialog.Action>
|
|
378
|
+
</AlertDialog.Footer>
|
|
379
|
+
</AlertDialog.Content>
|
|
380
|
+
</AlertDialog>
|
|
343
381
|
\`\`\`
|
|
344
382
|
|
|
345
|
-
### \`
|
|
383
|
+
### \`useDialogStack()\` for imperative/stacked dialogs
|
|
384
|
+
|
|
385
|
+
Use when you need promise-based results or dialogs opened from event handlers:
|
|
346
386
|
|
|
347
387
|
\`\`\`tsx
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
388
|
+
import { useDialogStack } from 'vertz/ui';
|
|
389
|
+
|
|
390
|
+
const dialogs = useDialogStack();
|
|
391
|
+
const confirmed = await dialogs.open(ConfirmDialog, { message: 'Delete?' });
|
|
392
|
+
if (confirmed) handleDelete();
|
|
393
|
+
\`\`\`
|
|
394
|
+
|
|
395
|
+
## Styling
|
|
396
|
+
|
|
397
|
+
### \`css()\` for layout and custom styles
|
|
398
|
+
|
|
399
|
+
Use \`css()\` for layout-specific styles that don't correspond to a theme component:
|
|
400
|
+
|
|
401
|
+
\`\`\`tsx
|
|
402
|
+
const styles = css({
|
|
403
|
+
container: ['flex', 'flex-col', 'gap:4', 'p:6'],
|
|
404
|
+
heading: ['font:xl', 'font:bold', 'text:foreground'],
|
|
362
405
|
});
|
|
363
406
|
|
|
364
|
-
<
|
|
407
|
+
return <div className={styles.container}>...</div>;
|
|
365
408
|
\`\`\`
|
|
366
409
|
|
|
367
410
|
### Style Tokens
|
|
@@ -615,7 +658,7 @@ export function schemaTemplate() {
|
|
|
615
658
|
|
|
616
659
|
export const tasksTable = d.table('tasks', {
|
|
617
660
|
id: d.uuid().primary(),
|
|
618
|
-
title: d.text(),
|
|
661
|
+
title: d.text().min(1),
|
|
619
662
|
completed: d.boolean().default(false),
|
|
620
663
|
createdAt: d.timestamp().default('now').readOnly(),
|
|
621
664
|
updatedAt: d.timestamp().autoUpdate().readOnly(),
|
|
@@ -625,14 +668,16 @@ export const tasksModel = d.model(tasksTable);
|
|
|
625
668
|
`;
|
|
626
669
|
}
|
|
627
670
|
/**
|
|
628
|
-
* src/api/db.ts —
|
|
671
|
+
* src/api/db.ts — createDb with local SQLite and autoApply migrations
|
|
629
672
|
*/
|
|
630
673
|
export function dbTemplate() {
|
|
631
|
-
return `import {
|
|
632
|
-
import {
|
|
674
|
+
return `import { createDb } from 'vertz/db';
|
|
675
|
+
import { tasksModel } from './schema';
|
|
633
676
|
|
|
634
|
-
export const db =
|
|
635
|
-
|
|
677
|
+
export const db = createDb({
|
|
678
|
+
models: { tasks: tasksModel },
|
|
679
|
+
dialect: 'sqlite',
|
|
680
|
+
path: '.vertz/data/app.db',
|
|
636
681
|
migrations: { autoApply: true },
|
|
637
682
|
});
|
|
638
683
|
`;
|
|
@@ -705,11 +750,11 @@ export function App() {
|
|
|
705
750
|
return (
|
|
706
751
|
<div data-testid="app-root">
|
|
707
752
|
<ThemeProvider theme="light">
|
|
708
|
-
<div
|
|
709
|
-
<header
|
|
710
|
-
<div
|
|
753
|
+
<div className={styles.shell}>
|
|
754
|
+
<header className={styles.header}>
|
|
755
|
+
<div className={styles.title}>My Vertz App</div>
|
|
711
756
|
</header>
|
|
712
|
-
<main
|
|
757
|
+
<main className={styles.main}>
|
|
713
758
|
<HomePage />
|
|
714
759
|
</main>
|
|
715
760
|
</div>
|
|
@@ -735,22 +780,27 @@ mount(App, {
|
|
|
735
780
|
`;
|
|
736
781
|
}
|
|
737
782
|
/**
|
|
738
|
-
* src/styles/theme.ts —
|
|
783
|
+
* src/styles/theme.ts — configureTheme from @vertz/theme-shadcn
|
|
739
784
|
*/
|
|
740
785
|
export function themeTemplate() {
|
|
741
|
-
return `import {
|
|
786
|
+
return `import { configureTheme } from '@vertz/theme-shadcn';
|
|
787
|
+
import { registerTheme } from 'vertz/ui';
|
|
742
788
|
|
|
743
|
-
const
|
|
789
|
+
const config = configureTheme({
|
|
744
790
|
palette: 'zinc',
|
|
745
791
|
radius: 'md',
|
|
746
792
|
});
|
|
747
793
|
|
|
748
|
-
|
|
749
|
-
|
|
794
|
+
registerTheme(config);
|
|
795
|
+
|
|
796
|
+
export const appTheme = config.theme;
|
|
797
|
+
export const themeGlobals = config.globals;
|
|
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 {
|
|
@@ -765,9 +815,11 @@ export function homePageTemplate() {
|
|
|
765
815
|
queryMatch,
|
|
766
816
|
slideInFromTop,
|
|
767
817
|
} from 'vertz/ui';
|
|
818
|
+
import { Button } from '@vertz/ui/components';
|
|
819
|
+
import { AlertDialog } from '@vertz/ui/components';
|
|
768
820
|
import { api } from '../client';
|
|
769
821
|
|
|
770
|
-
//
|
|
822
|
+
// Global CSS for list item enter/exit animations
|
|
771
823
|
void globalCss({
|
|
772
824
|
'[data-presence="enter"]': {
|
|
773
825
|
animation: \`\${slideInFromTop} \${ANIMATION_DURATION} \${ANIMATION_EASING}\`,
|
|
@@ -778,31 +830,23 @@ void globalCss({
|
|
|
778
830
|
},
|
|
779
831
|
});
|
|
780
832
|
|
|
781
|
-
const
|
|
833
|
+
const styles = css({
|
|
782
834
|
container: ['py:2', 'w:full'],
|
|
783
835
|
heading: ['font:xl', 'font:bold', 'text:foreground', 'mb:4'],
|
|
784
|
-
form: ['flex', '
|
|
836
|
+
form: ['flex', 'items:start', 'gap:2', 'mb:6'],
|
|
785
837
|
inputWrap: ['flex-1'],
|
|
786
838
|
input: [
|
|
787
839
|
'w:full',
|
|
840
|
+
'h:10',
|
|
788
841
|
'px:3',
|
|
789
|
-
'py:2',
|
|
790
842
|
'rounded:md',
|
|
791
843
|
'border:1',
|
|
792
844
|
'border:border',
|
|
793
845
|
'bg:background',
|
|
794
846
|
'text:foreground',
|
|
847
|
+
'text:sm',
|
|
795
848
|
],
|
|
796
849
|
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
850
|
list: ['flex', 'flex-col', 'gap:2'],
|
|
807
851
|
item: [
|
|
808
852
|
'flex',
|
|
@@ -814,12 +858,63 @@ const pageStyles = css({
|
|
|
814
858
|
'border:1',
|
|
815
859
|
'border:border',
|
|
816
860
|
'bg:card',
|
|
861
|
+
'hover:bg:accent',
|
|
862
|
+
'transition:colors',
|
|
817
863
|
],
|
|
864
|
+
checkbox: ['w:4', 'h:4', 'cursor:pointer', 'rounded:sm'],
|
|
865
|
+
label: ['flex-1', 'text:sm', 'text:foreground'],
|
|
866
|
+
labelDone: ['flex-1', 'text:sm', 'text:muted-foreground', 'decoration:line-through'],
|
|
818
867
|
loading: ['text:muted-foreground'],
|
|
819
868
|
error: ['text:destructive'],
|
|
820
869
|
empty: ['text:muted-foreground', 'text:center', 'py:8'],
|
|
870
|
+
count: ['text:xs', 'text:muted-foreground', 'mt:4'],
|
|
821
871
|
});
|
|
822
872
|
|
|
873
|
+
interface TaskItemProps {
|
|
874
|
+
id: string;
|
|
875
|
+
title: string;
|
|
876
|
+
completed: boolean;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function TaskItem({ id, title, completed }: TaskItemProps) {
|
|
880
|
+
const handleToggle = async () => {
|
|
881
|
+
await api.tasks.update(id, { completed: !completed });
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
const handleDelete = async () => {
|
|
885
|
+
await api.tasks.delete(id);
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
return (
|
|
889
|
+
<div className={styles.item}>
|
|
890
|
+
<input
|
|
891
|
+
type="checkbox"
|
|
892
|
+
className={styles.checkbox}
|
|
893
|
+
checked={completed}
|
|
894
|
+
onChange={handleToggle}
|
|
895
|
+
/>
|
|
896
|
+
<span className={completed ? styles.labelDone : styles.label}>
|
|
897
|
+
{title}
|
|
898
|
+
</span>
|
|
899
|
+
<AlertDialog onAction={handleDelete}>
|
|
900
|
+
<AlertDialog.Trigger>
|
|
901
|
+
<Button intent="ghost" size="sm">Delete</Button>
|
|
902
|
+
</AlertDialog.Trigger>
|
|
903
|
+
<AlertDialog.Content>
|
|
904
|
+
<AlertDialog.Title>Delete task?</AlertDialog.Title>
|
|
905
|
+
<AlertDialog.Description>
|
|
906
|
+
This action cannot be undone.
|
|
907
|
+
</AlertDialog.Description>
|
|
908
|
+
<AlertDialog.Footer>
|
|
909
|
+
<AlertDialog.Cancel>Cancel</AlertDialog.Cancel>
|
|
910
|
+
<AlertDialog.Action>Delete</AlertDialog.Action>
|
|
911
|
+
</AlertDialog.Footer>
|
|
912
|
+
</AlertDialog.Content>
|
|
913
|
+
</AlertDialog>
|
|
914
|
+
</div>
|
|
915
|
+
);
|
|
916
|
+
}
|
|
917
|
+
|
|
823
918
|
export function HomePage() {
|
|
824
919
|
const tasksQuery = query(api.tasks.list());
|
|
825
920
|
|
|
@@ -828,61 +923,64 @@ export function HomePage() {
|
|
|
828
923
|
});
|
|
829
924
|
|
|
830
925
|
return (
|
|
831
|
-
<div
|
|
832
|
-
<h1
|
|
926
|
+
<div className={styles.container} data-testid="home-page">
|
|
927
|
+
<h1 className={styles.heading}>Tasks</h1>
|
|
833
928
|
|
|
834
929
|
<form
|
|
835
|
-
|
|
930
|
+
className={styles.form}
|
|
836
931
|
action={taskForm.action}
|
|
837
932
|
method={taskForm.method}
|
|
838
933
|
onSubmit={taskForm.onSubmit}
|
|
839
934
|
>
|
|
840
|
-
<div
|
|
935
|
+
<div className={styles.inputWrap}>
|
|
841
936
|
<input
|
|
842
937
|
name={taskForm.fields.title}
|
|
843
|
-
|
|
938
|
+
className={styles.input}
|
|
844
939
|
placeholder="What needs to be done?"
|
|
845
940
|
/>
|
|
846
|
-
<span
|
|
941
|
+
<span className={styles.fieldError}>
|
|
847
942
|
{taskForm.title.error}
|
|
848
943
|
</span>
|
|
849
944
|
</div>
|
|
850
|
-
<
|
|
851
|
-
type="submit"
|
|
852
|
-
class={pageStyles.button}
|
|
853
|
-
disabled={taskForm.submitting}
|
|
854
|
-
>
|
|
945
|
+
<Button type="submit" disabled={taskForm.submitting}>
|
|
855
946
|
{taskForm.submitting.value ? 'Adding...' : 'Add'}
|
|
856
|
-
</
|
|
947
|
+
</Button>
|
|
857
948
|
</form>
|
|
858
949
|
|
|
859
950
|
{queryMatch(tasksQuery, {
|
|
860
951
|
loading: () => (
|
|
861
|
-
<div
|
|
952
|
+
<div className={styles.loading}>Loading tasks...</div>
|
|
862
953
|
),
|
|
863
954
|
error: (err) => (
|
|
864
|
-
<div
|
|
955
|
+
<div className={styles.error}>
|
|
865
956
|
{err instanceof Error ? err.message : String(err)}
|
|
866
957
|
</div>
|
|
867
958
|
),
|
|
868
959
|
data: (response) => (
|
|
869
960
|
<>
|
|
870
961
|
{response.items.length === 0 && (
|
|
871
|
-
<div
|
|
962
|
+
<div className={styles.empty}>
|
|
872
963
|
No tasks yet. Add one above!
|
|
873
964
|
</div>
|
|
874
965
|
)}
|
|
875
|
-
<div data-testid="task-list"
|
|
966
|
+
<div data-testid="task-list" className={styles.list}>
|
|
876
967
|
<ListTransition
|
|
877
968
|
each={response.items}
|
|
878
969
|
keyFn={(task) => task.id}
|
|
879
970
|
children={(task) => (
|
|
880
|
-
<
|
|
881
|
-
|
|
882
|
-
|
|
971
|
+
<TaskItem
|
|
972
|
+
id={task.id}
|
|
973
|
+
title={task.title}
|
|
974
|
+
completed={task.completed}
|
|
975
|
+
/>
|
|
883
976
|
)}
|
|
884
977
|
/>
|
|
885
978
|
</div>
|
|
979
|
+
{response.items.length > 0 && (
|
|
980
|
+
<div className={styles.count}>
|
|
981
|
+
{response.items.filter((t) => !t.completed).length} remaining
|
|
982
|
+
</div>
|
|
983
|
+
)}
|
|
886
984
|
</>
|
|
887
985
|
),
|
|
888
986
|
})}
|