@vertz/create-vertz-app 0.2.19 → 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);
|
package/dist/scaffold.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;GAEG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,WAAW,EAAE,MAAM;CAIhC;AAED;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAkEzF"}
|
package/dist/scaffold.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { apiDevelopmentRuleTemplate, appComponentTemplate, bunfigTemplate, bunPluginShimTemplate, claudeMdTemplate, clientTemplate, dbTemplate, entryClientTemplate, envExampleTemplate, envModuleTemplate, envTemplate, gitignoreTemplate, homePageTemplate, packageJsonTemplate, schemaTemplate, serverTemplate, tasksEntityTemplate, themeTemplate, tsconfigTemplate, uiDevelopmentRuleTemplate, vertzConfigTemplate, } from './templates/index.js';
|
|
3
|
+
import { apiDevelopmentRuleTemplate, appComponentTemplate, bunfigTemplate, bunPluginShimTemplate, claudeMdTemplate, clientTemplate, dbTemplate, entryClientTemplate, envExampleTemplate, envModuleTemplate, envTemplate, faviconTemplate, gitignoreTemplate, homePageTemplate, packageJsonTemplate, schemaTemplate, serverTemplate, tasksEntityTemplate, themeTemplate, tsconfigTemplate, uiDevelopmentRuleTemplate, vertzConfigTemplate, } from './templates/index.js';
|
|
4
4
|
/**
|
|
5
5
|
* Error thrown when the project directory already exists
|
|
6
6
|
*/
|
|
@@ -36,11 +36,13 @@ export async function scaffold(parentDir, options) {
|
|
|
36
36
|
const pagesDir = path.join(srcDir, 'pages');
|
|
37
37
|
const stylesDir = path.join(srcDir, 'styles');
|
|
38
38
|
const claudeRulesDir = path.join(projectDir, '.claude', 'rules');
|
|
39
|
+
const publicDir = path.join(projectDir, 'public');
|
|
39
40
|
await Promise.all([
|
|
40
41
|
fs.mkdir(entitiesDir, { recursive: true }),
|
|
41
42
|
fs.mkdir(pagesDir, { recursive: true }),
|
|
42
43
|
fs.mkdir(stylesDir, { recursive: true }),
|
|
43
44
|
fs.mkdir(claudeRulesDir, { recursive: true }),
|
|
45
|
+
fs.mkdir(publicDir, { recursive: true }),
|
|
44
46
|
]);
|
|
45
47
|
// Write all files in parallel
|
|
46
48
|
await Promise.all([
|
|
@@ -65,6 +67,8 @@ export async function scaffold(parentDir, options) {
|
|
|
65
67
|
writeFile(srcDir, 'entry-client.ts', entryClientTemplate()),
|
|
66
68
|
writeFile(pagesDir, 'home.tsx', homePageTemplate()),
|
|
67
69
|
writeFile(stylesDir, 'theme.ts', themeTemplate()),
|
|
70
|
+
// Static assets
|
|
71
|
+
writeFile(publicDir, 'favicon.svg', faviconTemplate()),
|
|
68
72
|
// LLM rules
|
|
69
73
|
writeFile(projectDir, 'CLAUDE.md', claudeMdTemplate(projectName)),
|
|
70
74
|
writeFile(claudeRulesDir, 'api-development.md', apiDevelopmentRuleTemplate()),
|
|
@@ -10,6 +10,10 @@ export declare function apiDevelopmentRuleTemplate(): string;
|
|
|
10
10
|
* .claude/rules/ui-development.md — UI conventions for LLMs
|
|
11
11
|
*/
|
|
12
12
|
export declare function uiDevelopmentRuleTemplate(): string;
|
|
13
|
+
/**
|
|
14
|
+
* public/favicon.svg — Vertz logo on dark background
|
|
15
|
+
*/
|
|
16
|
+
export declare function faviconTemplate(): string;
|
|
13
17
|
/**
|
|
14
18
|
* Package.json template — full-stack deps + #generated imports map
|
|
15
19
|
*/
|
|
@@ -55,7 +59,7 @@ export declare function serverTemplate(): string;
|
|
|
55
59
|
*/
|
|
56
60
|
export declare function schemaTemplate(): string;
|
|
57
61
|
/**
|
|
58
|
-
* src/api/db.ts —
|
|
62
|
+
* src/api/db.ts — createDb with local SQLite and autoApply migrations
|
|
59
63
|
*/
|
|
60
64
|
export declare function dbTemplate(): string;
|
|
61
65
|
/**
|
|
@@ -75,11 +79,13 @@ export declare function appComponentTemplate(): string;
|
|
|
75
79
|
*/
|
|
76
80
|
export declare function entryClientTemplate(): string;
|
|
77
81
|
/**
|
|
78
|
-
* src/styles/theme.ts —
|
|
82
|
+
* src/styles/theme.ts — configureTheme from @vertz/theme-shadcn
|
|
79
83
|
*/
|
|
80
84
|
export declare function themeTemplate(): string;
|
|
81
85
|
/**
|
|
82
|
-
* 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.
|
|
83
89
|
*/
|
|
84
90
|
export declare function homePageTemplate(): string;
|
|
85
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;
|
|
341
343
|
|
|
342
|
-
|
|
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" />
|
|
343
351
|
\`\`\`
|
|
344
352
|
|
|
345
|
-
###
|
|
353
|
+
### Available Components
|
|
354
|
+
|
|
355
|
+
**Direct** (from \`themeComponents\`): \`Button\`, \`Input\`, \`Label\`, \`Badge\`, \`Textarea\`,
|
|
356
|
+
\`Card\` suite, \`Table\` suite, \`Avatar\` suite, \`FormGroup\` suite
|
|
357
|
+
|
|
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
|
|
346
365
|
|
|
347
366
|
\`\`\`tsx
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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>
|
|
383
|
+
\`\`\`
|
|
384
|
+
|
|
385
|
+
### \`useDialogStack()\` for imperative/stacked dialogs
|
|
386
|
+
|
|
387
|
+
Use when you need promise-based results or dialogs opened from event handlers:
|
|
388
|
+
|
|
389
|
+
\`\`\`tsx
|
|
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
|
|
@@ -411,6 +456,14 @@ export function TaskDetailPage() {
|
|
|
411
456
|
\`\`\`
|
|
412
457
|
`;
|
|
413
458
|
}
|
|
459
|
+
// ── Static asset templates ─────────────────────────────────
|
|
460
|
+
/**
|
|
461
|
+
* public/favicon.svg — Vertz logo on dark background
|
|
462
|
+
*/
|
|
463
|
+
export function faviconTemplate() {
|
|
464
|
+
return `<svg width="32" height="32" viewBox="0 0 298 298" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="298" height="298" rx="60" fill="#0a0a0b"/><path d="M120.277 77H26L106.5 185.5L151.365 124.67L120.277 77Z" fill="white"/><path d="M147.986 243L125.5 210.5L190.467 124.67L160.731 77H272L147.986 243Z" fill="white"/></svg>
|
|
465
|
+
`;
|
|
466
|
+
}
|
|
414
467
|
// ── Config file templates ──────────────────────────────────
|
|
415
468
|
/**
|
|
416
469
|
* Package.json template — full-stack deps + #generated imports map
|
|
@@ -607,7 +660,7 @@ export function schemaTemplate() {
|
|
|
607
660
|
|
|
608
661
|
export const tasksTable = d.table('tasks', {
|
|
609
662
|
id: d.uuid().primary(),
|
|
610
|
-
title: d.text(),
|
|
663
|
+
title: d.text().min(1),
|
|
611
664
|
completed: d.boolean().default(false),
|
|
612
665
|
createdAt: d.timestamp().default('now').readOnly(),
|
|
613
666
|
updatedAt: d.timestamp().autoUpdate().readOnly(),
|
|
@@ -617,14 +670,16 @@ export const tasksModel = d.model(tasksTable);
|
|
|
617
670
|
`;
|
|
618
671
|
}
|
|
619
672
|
/**
|
|
620
|
-
* src/api/db.ts —
|
|
673
|
+
* src/api/db.ts — createDb with local SQLite and autoApply migrations
|
|
621
674
|
*/
|
|
622
675
|
export function dbTemplate() {
|
|
623
|
-
return `import {
|
|
624
|
-
import {
|
|
676
|
+
return `import { createDb } from 'vertz/db';
|
|
677
|
+
import { tasksModel } from './schema';
|
|
625
678
|
|
|
626
|
-
export const db =
|
|
627
|
-
|
|
679
|
+
export const db = createDb({
|
|
680
|
+
models: { tasks: tasksModel },
|
|
681
|
+
dialect: 'sqlite',
|
|
682
|
+
path: '.vertz/data/app.db',
|
|
628
683
|
migrations: { autoApply: true },
|
|
629
684
|
});
|
|
630
685
|
`;
|
|
@@ -697,11 +752,11 @@ export function App() {
|
|
|
697
752
|
return (
|
|
698
753
|
<div data-testid="app-root">
|
|
699
754
|
<ThemeProvider theme="light">
|
|
700
|
-
<div
|
|
701
|
-
<header
|
|
702
|
-
<div
|
|
755
|
+
<div className={styles.shell}>
|
|
756
|
+
<header className={styles.header}>
|
|
757
|
+
<div className={styles.title}>My Vertz App</div>
|
|
703
758
|
</header>
|
|
704
|
-
<main
|
|
759
|
+
<main className={styles.main}>
|
|
705
760
|
<HomePage />
|
|
706
761
|
</main>
|
|
707
762
|
</div>
|
|
@@ -727,22 +782,25 @@ mount(App, {
|
|
|
727
782
|
`;
|
|
728
783
|
}
|
|
729
784
|
/**
|
|
730
|
-
* src/styles/theme.ts —
|
|
785
|
+
* src/styles/theme.ts — configureTheme from @vertz/theme-shadcn
|
|
731
786
|
*/
|
|
732
787
|
export function themeTemplate() {
|
|
733
|
-
return `import {
|
|
788
|
+
return `import { configureTheme } from '@vertz/theme-shadcn';
|
|
734
789
|
|
|
735
|
-
const { theme, globals } =
|
|
790
|
+
const { theme, globals, components } = configureTheme({
|
|
736
791
|
palette: 'zinc',
|
|
737
792
|
radius: 'md',
|
|
738
793
|
});
|
|
739
794
|
|
|
740
795
|
export const appTheme = theme;
|
|
741
796
|
export const themeGlobals = globals;
|
|
797
|
+
export const themeComponents = components;
|
|
742
798
|
`;
|
|
743
799
|
}
|
|
744
800
|
/**
|
|
745
|
-
* 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.
|
|
746
804
|
*/
|
|
747
805
|
export function homePageTemplate() {
|
|
748
806
|
return `import {
|
|
@@ -758,8 +816,12 @@ export function homePageTemplate() {
|
|
|
758
816
|
slideInFromTop,
|
|
759
817
|
} from 'vertz/ui';
|
|
760
818
|
import { api } from '../client';
|
|
819
|
+
import { themeComponents } from '../styles/theme';
|
|
820
|
+
|
|
821
|
+
const { Button } = themeComponents;
|
|
822
|
+
const { AlertDialog } = themeComponents.primitives;
|
|
761
823
|
|
|
762
|
-
//
|
|
824
|
+
// Global CSS for list item enter/exit animations
|
|
763
825
|
void globalCss({
|
|
764
826
|
'[data-presence="enter"]': {
|
|
765
827
|
animation: \`\${slideInFromTop} \${ANIMATION_DURATION} \${ANIMATION_EASING}\`,
|
|
@@ -770,31 +832,23 @@ void globalCss({
|
|
|
770
832
|
},
|
|
771
833
|
});
|
|
772
834
|
|
|
773
|
-
const
|
|
835
|
+
const styles = css({
|
|
774
836
|
container: ['py:2', 'w:full'],
|
|
775
837
|
heading: ['font:xl', 'font:bold', 'text:foreground', 'mb:4'],
|
|
776
|
-
form: ['flex', '
|
|
838
|
+
form: ['flex', 'items:start', 'gap:2', 'mb:6'],
|
|
777
839
|
inputWrap: ['flex-1'],
|
|
778
840
|
input: [
|
|
779
841
|
'w:full',
|
|
842
|
+
'h:10',
|
|
780
843
|
'px:3',
|
|
781
|
-
'py:2',
|
|
782
844
|
'rounded:md',
|
|
783
845
|
'border:1',
|
|
784
846
|
'border:border',
|
|
785
847
|
'bg:background',
|
|
786
848
|
'text:foreground',
|
|
849
|
+
'text:sm',
|
|
787
850
|
],
|
|
788
851
|
fieldError: ['text:destructive', 'font:xs', 'mt:1'],
|
|
789
|
-
button: [
|
|
790
|
-
'px:4',
|
|
791
|
-
'py:2',
|
|
792
|
-
'rounded:md',
|
|
793
|
-
'bg:primary',
|
|
794
|
-
'text:primary-foreground',
|
|
795
|
-
'font:medium',
|
|
796
|
-
'cursor:pointer',
|
|
797
|
-
],
|
|
798
852
|
list: ['flex', 'flex-col', 'gap:2'],
|
|
799
853
|
item: [
|
|
800
854
|
'flex',
|
|
@@ -806,12 +860,63 @@ const pageStyles = css({
|
|
|
806
860
|
'border:1',
|
|
807
861
|
'border:border',
|
|
808
862
|
'bg:card',
|
|
863
|
+
'hover:bg:accent',
|
|
864
|
+
'transition:colors',
|
|
809
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'],
|
|
810
869
|
loading: ['text:muted-foreground'],
|
|
811
870
|
error: ['text:destructive'],
|
|
812
871
|
empty: ['text:muted-foreground', 'text:center', 'py:8'],
|
|
872
|
+
count: ['text:xs', 'text:muted-foreground', 'mt:4'],
|
|
813
873
|
});
|
|
814
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
|
+
|
|
815
920
|
export function HomePage() {
|
|
816
921
|
const tasksQuery = query(api.tasks.list());
|
|
817
922
|
|
|
@@ -820,61 +925,64 @@ export function HomePage() {
|
|
|
820
925
|
});
|
|
821
926
|
|
|
822
927
|
return (
|
|
823
|
-
<div
|
|
824
|
-
<h1
|
|
928
|
+
<div className={styles.container} data-testid="home-page">
|
|
929
|
+
<h1 className={styles.heading}>Tasks</h1>
|
|
825
930
|
|
|
826
931
|
<form
|
|
827
|
-
|
|
932
|
+
className={styles.form}
|
|
828
933
|
action={taskForm.action}
|
|
829
934
|
method={taskForm.method}
|
|
830
935
|
onSubmit={taskForm.onSubmit}
|
|
831
936
|
>
|
|
832
|
-
<div
|
|
937
|
+
<div className={styles.inputWrap}>
|
|
833
938
|
<input
|
|
834
939
|
name={taskForm.fields.title}
|
|
835
|
-
|
|
940
|
+
className={styles.input}
|
|
836
941
|
placeholder="What needs to be done?"
|
|
837
942
|
/>
|
|
838
|
-
<span
|
|
943
|
+
<span className={styles.fieldError}>
|
|
839
944
|
{taskForm.title.error}
|
|
840
945
|
</span>
|
|
841
946
|
</div>
|
|
842
|
-
<
|
|
843
|
-
type="submit"
|
|
844
|
-
class={pageStyles.button}
|
|
845
|
-
disabled={taskForm.submitting}
|
|
846
|
-
>
|
|
947
|
+
<Button type="submit" disabled={taskForm.submitting}>
|
|
847
948
|
{taskForm.submitting.value ? 'Adding...' : 'Add'}
|
|
848
|
-
</
|
|
949
|
+
</Button>
|
|
849
950
|
</form>
|
|
850
951
|
|
|
851
952
|
{queryMatch(tasksQuery, {
|
|
852
953
|
loading: () => (
|
|
853
|
-
<div
|
|
954
|
+
<div className={styles.loading}>Loading tasks...</div>
|
|
854
955
|
),
|
|
855
956
|
error: (err) => (
|
|
856
|
-
<div
|
|
957
|
+
<div className={styles.error}>
|
|
857
958
|
{err instanceof Error ? err.message : String(err)}
|
|
858
959
|
</div>
|
|
859
960
|
),
|
|
860
961
|
data: (response) => (
|
|
861
962
|
<>
|
|
862
963
|
{response.items.length === 0 && (
|
|
863
|
-
<div
|
|
964
|
+
<div className={styles.empty}>
|
|
864
965
|
No tasks yet. Add one above!
|
|
865
966
|
</div>
|
|
866
967
|
)}
|
|
867
|
-
<div data-testid="task-list"
|
|
968
|
+
<div data-testid="task-list" className={styles.list}>
|
|
868
969
|
<ListTransition
|
|
869
970
|
each={response.items}
|
|
870
971
|
keyFn={(task) => task.id}
|
|
871
972
|
children={(task) => (
|
|
872
|
-
<
|
|
873
|
-
|
|
874
|
-
|
|
973
|
+
<TaskItem
|
|
974
|
+
id={task.id}
|
|
975
|
+
title={task.title}
|
|
976
|
+
completed={task.completed}
|
|
977
|
+
/>
|
|
875
978
|
)}
|
|
876
979
|
/>
|
|
877
980
|
</div>
|
|
981
|
+
{response.items.length > 0 && (
|
|
982
|
+
<div className={styles.count}>
|
|
983
|
+
{response.items.filter((t) => !t.completed).length} remaining
|
|
984
|
+
</div>
|
|
985
|
+
)}
|
|
878
986
|
</>
|
|
879
987
|
),
|
|
880
988
|
})}
|