@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.
@@ -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
- import { resolveOptions, scaffold } from '../dist/index.js';
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('0.1.0')
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);
@@ -1 +1 @@
1
- {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAyBA,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,CA6DzF"}
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 — createSqliteAdapter with autoApply migrations
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 — configureThemeBase from @vertz/theme-shadcn/base
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 + create form with query + css
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,CA0NlD;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,CASnC;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,CAWtC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CA0IzC"}
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"}
@@ -266,7 +266,7 @@ Never use \`appendChild\`, \`innerHTML\`, \`textContent\`, \`document.createElem
266
266
 
267
267
  \`\`\`tsx
268
268
  // RIGHT
269
- return <div class={styles.panel}>{title}</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 class={styles.error}>{error.message}</div> : <div>{content}</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
- ## Styling
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
- ### \`css()\` for scoped styles
336
+ ### Using Components
334
337
 
335
338
  \`\`\`tsx
336
- const styles = css({
337
- container: ['flex', 'flex-col', 'gap:4', 'p:6'],
338
- title: ['font:xl', 'font:bold', 'text:foreground'],
339
- card: ['rounded:md', 'border:1', 'border:border', 'bg:card', 'p:4'],
340
- });
339
+ import { themeComponents } from '../styles/theme';
340
+
341
+ const { Button, Input } = themeComponents;
342
+ const { AlertDialog } = themeComponents.primitives;
341
343
 
342
- return <div class={styles.container}>...</div>;
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
- ### \`variants()\` for parameterized styles
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 button = variants({
349
- base: ['inline-flex', 'items:center', 'rounded:md', 'font:medium'],
350
- variants: {
351
- intent: {
352
- primary: ['bg:primary.600', 'text:white'],
353
- secondary: ['bg:secondary', 'text:secondary-foreground'],
354
- danger: ['bg:destructive', 'text:white'],
355
- },
356
- size: {
357
- sm: ['text:xs', 'px:3', 'py:1'],
358
- md: ['text:sm', 'px:4', 'py:2'],
359
- },
360
- },
361
- defaultVariants: { intent: 'primary', size: 'md' },
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
- <button class={button({ intent: 'danger', size: 'sm' })}>Delete</button>
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 — createSqliteAdapter with autoApply migrations
673
+ * src/api/db.ts — createDb with local SQLite and autoApply migrations
621
674
  */
622
675
  export function dbTemplate() {
623
- return `import { createSqliteAdapter } from 'vertz/db/sqlite';
624
- import { tasksTable } from './schema';
676
+ return `import { createDb } from 'vertz/db';
677
+ import { tasksModel } from './schema';
625
678
 
626
- export const db = await createSqliteAdapter({
627
- schema: tasksTable,
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 class={styles.shell}>
701
- <header class={styles.header}>
702
- <div class={styles.title}>My Vertz App</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 class={styles.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 — configureThemeBase from @vertz/theme-shadcn/base
785
+ * src/styles/theme.ts — configureTheme from @vertz/theme-shadcn
731
786
  */
732
787
  export function themeTemplate() {
733
- return `import { configureThemeBase } from '@vertz/theme-shadcn/base';
788
+ return `import { configureTheme } from '@vertz/theme-shadcn';
734
789
 
735
- const { theme, globals } = configureThemeBase({
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 + create form with query + css
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
- // Inject global CSS for list item enter/exit animations
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 pageStyles = css({
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', 'gap:2', 'items:start', 'mb:6'],
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 class={pageStyles.container} data-testid="home-page">
824
- <h1 class={pageStyles.heading}>Tasks</h1>
928
+ <div className={styles.container} data-testid="home-page">
929
+ <h1 className={styles.heading}>Tasks</h1>
825
930
 
826
931
  <form
827
- class={pageStyles.form}
932
+ className={styles.form}
828
933
  action={taskForm.action}
829
934
  method={taskForm.method}
830
935
  onSubmit={taskForm.onSubmit}
831
936
  >
832
- <div class={pageStyles.inputWrap}>
937
+ <div className={styles.inputWrap}>
833
938
  <input
834
939
  name={taskForm.fields.title}
835
- class={pageStyles.input}
940
+ className={styles.input}
836
941
  placeholder="What needs to be done?"
837
942
  />
838
- <span class={pageStyles.fieldError}>
943
+ <span className={styles.fieldError}>
839
944
  {taskForm.title.error}
840
945
  </span>
841
946
  </div>
842
- <button
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
- </button>
949
+ </Button>
849
950
  </form>
850
951
 
851
952
  {queryMatch(tasksQuery, {
852
953
  loading: () => (
853
- <div class={pageStyles.loading}>Loading tasks...</div>
954
+ <div className={styles.loading}>Loading tasks...</div>
854
955
  ),
855
956
  error: (err) => (
856
- <div class={pageStyles.error}>
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 class={pageStyles.empty}>
964
+ <div className={styles.empty}>
864
965
  No tasks yet. Add one above!
865
966
  </div>
866
967
  )}
867
- <div data-testid="task-list" class={pageStyles.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
- <div class={pageStyles.item}>
873
- <span>{task.title}</span>
874
- </div>
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
  })}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertz/create-vertz-app",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Create a new Vertz application",