@notis_ai/cli 0.2.2 → 0.2.4

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.
Files changed (81) hide show
  1. package/README.md +19 -18
  2. package/config/notis_app_boundary_rules.json +50 -0
  3. package/dist/scaffolds/notis-database/app/globals.css +44 -0
  4. package/dist/scaffolds/notis-database/app/page.tsx +1091 -0
  5. package/dist/scaffolds/notis-database/components/ui/table.tsx +120 -0
  6. package/dist/scaffolds/notis-database/index.html +12 -0
  7. package/dist/scaffolds/notis-database/lib/types.ts +134 -0
  8. package/dist/scaffolds/notis-database/metadata/cover.png +0 -0
  9. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  10. package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
  11. package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
  12. package/dist/scaffolds/notis-database/notis.config.ts +27 -0
  13. package/dist/scaffolds/notis-database/package.json +31 -0
  14. package/dist/scaffolds/notis-database/src/dev-main.tsx +23 -0
  15. package/dist/scaffolds/notis-database/src/mock-runtime.ts +557 -0
  16. package/dist/scaffolds/notis-database/tailwind.config.ts +59 -0
  17. package/dist/scaffolds/{notes → notis-database}/tsconfig.json +2 -1
  18. package/dist/scaffolds/notis-database/vite.config.ts +22 -0
  19. package/dist/scaffolds/notis-notes/app/layout.tsx +6 -0
  20. package/dist/scaffolds/{notes → notis-notes}/app/page.tsx +783 -80
  21. package/dist/scaffolds/notis-notes/components/ui/badge.tsx +28 -0
  22. package/dist/scaffolds/notis-notes/components/ui/button.tsx +53 -0
  23. package/dist/scaffolds/notis-notes/components/ui/card.tsx +56 -0
  24. package/dist/scaffolds/notis-notes/components.json +20 -0
  25. package/dist/scaffolds/notis-notes/lib/utils.ts +6 -0
  26. package/dist/scaffolds/notis-notes/metadata/cover.png +0 -0
  27. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  28. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  29. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  30. package/dist/scaffolds/{notes → notis-notes}/notis.config.ts +12 -7
  31. package/dist/scaffolds/{notes → notis-notes}/package.json +2 -2
  32. package/dist/scaffolds/notis-notes/postcss.config.mjs +8 -0
  33. package/dist/scaffolds/notis-notes/tsconfig.json +23 -0
  34. package/dist/scaffolds/notis-random/README.md +33 -0
  35. package/dist/scaffolds/notis-random/app/globals.css +11 -0
  36. package/dist/scaffolds/notis-random/app/history/page.tsx +66 -0
  37. package/dist/scaffolds/notis-random/app/layout.tsx +7 -0
  38. package/dist/scaffolds/notis-random/app/page.tsx +222 -0
  39. package/dist/scaffolds/notis-random/index.html +12 -0
  40. package/dist/scaffolds/notis-random/lib/notis-tools.ts +109 -0
  41. package/dist/scaffolds/notis-random/lib/rng.ts +42 -0
  42. package/dist/scaffolds/notis-random/lib/roll-record.ts +102 -0
  43. package/dist/scaffolds/notis-random/lib/utils.ts +25 -0
  44. package/dist/scaffolds/notis-random/metadata/cover.png +0 -0
  45. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  46. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  47. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  48. package/dist/scaffolds/notis-random/notis.config.ts +43 -0
  49. package/dist/scaffolds/notis-random/package.json +32 -0
  50. package/dist/scaffolds/notis-random/postcss.config.mjs +6 -0
  51. package/dist/scaffolds/notis-random/src/dev-main.tsx +70 -0
  52. package/dist/scaffolds/notis-random/src/mock-runtime.ts +128 -0
  53. package/dist/scaffolds/notis-random/tailwind.config.ts +43 -0
  54. package/dist/scaffolds/notis-random/tsconfig.json +23 -0
  55. package/dist/scaffolds/notis-random/vite.config.ts +11 -0
  56. package/dist/scaffolds.json +27 -4
  57. package/package.json +3 -1
  58. package/skills/notis-apps/SKILL.md +2 -2
  59. package/skills/notis-cli/SKILL.md +18 -16
  60. package/skills/notis-query/cli.md +9 -10
  61. package/src/cli.js +30 -2
  62. package/src/command-specs/apps.js +42 -15
  63. package/src/command-specs/helpers.js +73 -45
  64. package/src/command-specs/meta.js +7 -7
  65. package/src/command-specs/tools.js +196 -83
  66. package/src/runtime/app-boundary-validator.js +65 -14
  67. package/src/runtime/profiles.js +10 -4
  68. package/src/runtime/transport.js +122 -37
  69. package/template/app/page.tsx +1 -1
  70. package/template/notis.config.ts +1 -1
  71. package/template/packages/sdk/src/hooks/useTool.ts +1 -1
  72. /package/dist/scaffolds/{notes → notis-database}/app/layout.tsx +0 -0
  73. /package/dist/scaffolds/{notes → notis-database}/components/ui/badge.tsx +0 -0
  74. /package/dist/scaffolds/{notes → notis-database}/components/ui/button.tsx +0 -0
  75. /package/dist/scaffolds/{notes → notis-database}/components/ui/card.tsx +0 -0
  76. /package/dist/scaffolds/{notes → notis-database}/components.json +0 -0
  77. /package/dist/scaffolds/{notes → notis-database}/lib/utils.ts +0 -0
  78. /package/dist/scaffolds/{notes → notis-database}/postcss.config.mjs +0 -0
  79. /package/dist/scaffolds/{notes → notis-notes}/app/globals.css +0 -0
  80. /package/dist/scaffolds/{notes → notis-notes}/tailwind.config.ts +0 -0
  81. /package/dist/scaffolds/{notes → notis-notes}/vite.config.ts +0 -0
@@ -0,0 +1,28 @@
1
+ import * as React from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+
4
+ import { cn } from '@/lib/utils';
5
+
6
+ const badgeVariants = cva(
7
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: 'border-transparent bg-primary/10 text-primary',
12
+ secondary: 'border-transparent bg-secondary text-secondary-foreground',
13
+ outline: 'text-foreground',
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: 'default',
18
+ },
19
+ },
20
+ );
21
+
22
+ export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
23
+
24
+ function Badge({ className, variant, ...props }: BadgeProps) {
25
+ return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
26
+ }
27
+
28
+ export { Badge, badgeVariants };
@@ -0,0 +1,53 @@
1
+ import * as React from 'react';
2
+ import { Slot } from '@radix-ui/react-slot';
3
+ import { cva, type VariantProps } from 'class-variance-authority';
4
+
5
+ import { cn } from '@/lib/utils';
6
+
7
+ const buttonVariants = cva(
8
+ 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13
+ destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
14
+ outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
15
+ secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
16
+ ghost: 'hover:bg-accent hover:text-accent-foreground',
17
+ link: 'text-primary underline-offset-4 hover:underline',
18
+ },
19
+ size: {
20
+ default: 'h-10 px-4 py-2',
21
+ sm: 'h-9 rounded-md px-3',
22
+ lg: 'h-11 rounded-md px-8',
23
+ icon: 'h-10 w-10',
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ variant: 'default',
28
+ size: 'default',
29
+ },
30
+ },
31
+ );
32
+
33
+ export interface ButtonProps
34
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
35
+ VariantProps<typeof buttonVariants> {
36
+ asChild?: boolean;
37
+ }
38
+
39
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
40
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
41
+ const Comp = asChild ? Slot : 'button';
42
+ return (
43
+ <Comp
44
+ className={cn(buttonVariants({ variant, size, className }))}
45
+ ref={ref}
46
+ {...props}
47
+ />
48
+ );
49
+ },
50
+ );
51
+ Button.displayName = 'Button';
52
+
53
+ export { Button, buttonVariants };
@@ -0,0 +1,56 @@
1
+ import * as React from 'react';
2
+
3
+ import { cn } from '@/lib/utils';
4
+
5
+ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
6
+ ({ className, ...props }, ref) => (
7
+ <div
8
+ ref={ref}
9
+ className={cn('rounded-2xl border bg-card text-card-foreground shadow-sm', className)}
10
+ {...props}
11
+ />
12
+ ),
13
+ );
14
+ Card.displayName = 'Card';
15
+
16
+ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
17
+ ({ className, ...props }, ref) => (
18
+ <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
19
+ ),
20
+ );
21
+ CardHeader.displayName = 'CardHeader';
22
+
23
+ const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
24
+ ({ className, ...props }, ref) => (
25
+ <h3
26
+ ref={ref}
27
+ className={cn('text-xl font-semibold leading-none tracking-tight', className)}
28
+ {...props}
29
+ />
30
+ ),
31
+ );
32
+ CardTitle.displayName = 'CardTitle';
33
+
34
+ const CardDescription = React.forwardRef<
35
+ HTMLParagraphElement,
36
+ React.HTMLAttributes<HTMLParagraphElement>
37
+ >(({ className, ...props }, ref) => (
38
+ <p ref={ref} className={cn('text-sm text-muted-foreground', className)} {...props} />
39
+ ));
40
+ CardDescription.displayName = 'CardDescription';
41
+
42
+ const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
43
+ ({ className, ...props }, ref) => (
44
+ <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
45
+ ),
46
+ );
47
+ CardContent.displayName = 'CardContent';
48
+
49
+ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
50
+ ({ className, ...props }, ref) => (
51
+ <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
52
+ ),
53
+ );
54
+ CardFooter.displayName = 'CardFooter';
55
+
56
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "tailwind.config.ts",
8
+ "css": "app/globals.css",
9
+ "baseColor": "zinc",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "aliases": {
14
+ "components": "@/components",
15
+ "utils": "@/lib/utils",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "hooks": "@/hooks"
19
+ }
20
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -1,16 +1,21 @@
1
1
  import { defineNotisApp } from '@notis/sdk/config';
2
2
 
3
3
  export default defineNotisApp({
4
- name: 'Notis Notes',
4
+ name: 'notis-notes',
5
+ title: 'Notis Notes',
5
6
  description: 'Browse notes by documents, table, or calendar with folders in the sidebar.',
6
- icon: 'lucide:notebook-pen',
7
+ icon: 'phosphor:note-pencil',
8
+ author: { name: 'Notis' },
9
+ categories: ['Productivity'],
10
+ tagline: 'Folder-based notes with gallery, table, and calendar views.',
11
+ versionNotes: 'Initial scaffold release.',
7
12
  databases: ['note_folders', 'notes'],
8
13
  routes: [
9
14
  {
10
15
  path: '/',
11
- slug: 'notes',
16
+ slug: 'notis-notes',
12
17
  name: 'Notes',
13
- icon: 'lucide:notebook-pen',
18
+ icon: 'phosphor:note-pencil',
14
19
  default: true,
15
20
  collection: {
16
21
  database: 'note_folders',
@@ -24,8 +29,8 @@ export default defineNotisApp({
24
29
  },
25
30
  ],
26
31
  tools: [
27
- 'notis_query_database',
28
- 'notis_get_document',
29
- 'notis_upsert_document',
32
+ 'LOCAL_NOTIS_DATABASE_QUERY',
33
+ 'LOCAL_NOTIS_DATABASE_GET_DATABASE',
34
+ 'LOCAL_NOTIS_DATABASE_UPSERT_NOTES',
30
35
  ],
31
36
  });
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "notis-notes-app",
2
+ "name": "@notis-apps/notis-notes",
3
3
  "version": "0.1.0",
4
4
  "private": true,
5
5
  "type": "module",
@@ -9,10 +9,10 @@
9
9
  },
10
10
  "dependencies": {
11
11
  "@notis/sdk": "file:../../packages/sdk",
12
+ "@phosphor-icons/react": "^2.1.10",
12
13
  "@radix-ui/react-slot": "^1.1.0",
13
14
  "class-variance-authority": "^0.7.0",
14
15
  "clsx": "^2.1.1",
15
- "lucide-react": "^0.474.0",
16
16
  "react": "^19.0.0",
17
17
  "react-dom": "^19.0.0",
18
18
  "tailwind-merge": "^2.6.0",
@@ -0,0 +1,8 @@
1
+ /** @type {import('postcss-load-config').Config} */
2
+ const config = {
3
+ plugins: {
4
+ tailwindcss: {},
5
+ },
6
+ };
7
+
8
+ export default config;
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "preserveSymlinks": true,
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "jsx": "react-jsx",
16
+ "types": ["vite/client", "node"],
17
+ "paths": {
18
+ "@/*": ["./*"]
19
+ }
20
+ },
21
+ "include": ["**/*.ts", "**/*.tsx"],
22
+ "exclude": ["node_modules"]
23
+ }
@@ -0,0 +1,33 @@
1
+ # Random Number Generator
2
+
3
+ A simple, well-designed generator that lets you roll integers, decimals, or dice and keeps a history of every result in a Notis database.
4
+
5
+ ## Manifest
6
+
7
+ - **Slug**: `notis-random`
8
+ - **Database**: `rolls` — each roll persists as one row with `Value`, `Mode`, `Min`, `Max`, and `Rolled At`.
9
+ - **Routes**: `/` (Generator) and `/history` (History, renders the `rolls` database as a flat collection).
10
+
11
+ ## Database setup
12
+
13
+ `notis.config.ts` only references the `rolls` slug. It does not create the database schema automatically.
14
+
15
+ Provision the native schema before testing fresh installs:
16
+
17
+ ```bash
18
+ notis db upsert --operation update --database-id <rolls-db-id> --properties '[
19
+ {"name":"Value","type":"number","action":"add"},
20
+ {"name":"Mode","type":"select","action":"add","config":{"options":[
21
+ {"name":"Integer","color":"blue"},
22
+ {"name":"Decimal","color":"purple"},
23
+ {"name":"Dice","color":"orange"}
24
+ ]}},
25
+ {"name":"Min","type":"number","action":"add"},
26
+ {"name":"Max","type":"number","action":"add"},
27
+ {"name":"Rolled At","type":"date","action":"add"}
28
+ ]'
29
+ ```
30
+
31
+ ## Properties
32
+
33
+ _None yet._ Once `app.properties` lands in the SDK, this app will declare `default_min`, `default_max`, and `default_mode` at install time.
@@ -0,0 +1,11 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ .notis-random-shell {
6
+ /* Portal already provides background / foreground tokens via @notis/sdk styles.
7
+ This shell just enforces generous padding and centers content. */
8
+ padding: clamp(1rem, 4vw, 2.5rem);
9
+ max-width: 960px;
10
+ margin-inline: auto;
11
+ }
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+ import { DiceFiveIcon as Dices, ClockCounterClockwiseIcon as History, HashIcon as Hash, PercentIcon as Percent } from '@phosphor-icons/react';
5
+ import { formatNumber, relativeTime } from '@/lib/utils';
6
+ import type { Mode } from '@/lib/rng';
7
+ import { normalizeRollRecord, sortRollRecordsDesc } from '@/lib/roll-record';
8
+ import { useRollDocuments } from '@/lib/notis-tools';
9
+
10
+ const MODE_ICONS: Record<Mode, typeof Hash> = {
11
+ integer: Hash,
12
+ decimal: Percent,
13
+ dice: Dices,
14
+ };
15
+
16
+ export default function HistoryPage() {
17
+ const { documents, loading } = useRollDocuments();
18
+
19
+ const rows = useMemo(
20
+ () => documents
21
+ .map(normalizeRollRecord)
22
+ .sort(sortRollRecordsDesc),
23
+ [documents],
24
+ );
25
+
26
+ return (
27
+ <main className="notis-random-shell space-y-6">
28
+ <header className="flex items-center gap-2 text-sm text-muted-foreground">
29
+ <History className="h-4 w-4" strokeWidth={1.5} />
30
+ <span>Roll history</span>
31
+ </header>
32
+ <h1 className="text-2xl font-semibold tracking-tight text-foreground">
33
+ {rows.length} {rows.length === 1 ? 'roll' : 'rolls'}
34
+ </h1>
35
+
36
+ {loading && rows.length === 0 ? (
37
+ <p className="text-sm text-muted-foreground">Loading…</p>
38
+ ) : rows.length === 0 ? (
39
+ <div className="rounded-md border border-dashed border-border px-4 py-12 text-center text-sm text-muted-foreground">
40
+ Nothing rolled yet. Head back to the generator and press Generate.
41
+ </div>
42
+ ) : (
43
+ <div className="divide-y divide-border rounded-xl border border-border bg-card">
44
+ {rows.map((row) => {
45
+ const Icon = MODE_ICONS[row.mode] ?? Hash;
46
+ return (
47
+ <div key={row.id} className="flex items-center gap-4 px-5 py-3.5">
48
+ <Icon className="h-4 w-4 text-muted-foreground shrink-0" strokeWidth={1.5} />
49
+ <span className="font-mono text-lg tabular-nums text-foreground">{formatNumber(row.value)}</span>
50
+ <div className="flex-1 min-w-0 text-xs text-muted-foreground">
51
+ {row.min != null && row.max != null ? (
52
+ <span className="font-mono">[{formatNumber(row.min)}, {formatNumber(row.max)}]</span>
53
+ ) : (
54
+ <span>Legacy roll</span>
55
+ )}
56
+ {' · '}{row.mode}
57
+ </div>
58
+ <span className="text-xs text-muted-foreground shrink-0">{relativeTime(row.at)}</span>
59
+ </div>
60
+ );
61
+ })}
62
+ </div>
63
+ )}
64
+ </main>
65
+ );
66
+ }
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from 'react';
2
+ import '@notis/sdk/styles.css';
3
+ import './globals.css';
4
+
5
+ export default function AppShell({ children }: { children: ReactNode }) {
6
+ return children;
7
+ }
@@ -0,0 +1,222 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
4
+ import { useNotis } from '@notis/sdk';
5
+ import { DiceFiveIcon as Dices, SparkleIcon as Sparkles, ClockCounterClockwiseIcon as History, ArrowRightIcon as ArrowRight, HashIcon as Hash, PercentIcon as Percent, InfinityIcon, ArrowClockwiseIcon as RefreshCw } from '@phosphor-icons/react';
6
+ import { cn, formatNumber, relativeTime } from '@/lib/utils';
7
+ import { type Mode, type Roll, roll } from '@/lib/rng';
8
+ import { normalizeRollRecord, sortRollRecordsDesc } from '@/lib/roll-record';
9
+ import { usePersistRoll, useRollDocuments } from '@/lib/notis-tools';
10
+
11
+ const MODES: Array<{ id: Mode; label: string; icon: typeof Hash; hint: string }> = [
12
+ { id: 'integer', label: 'Integer', icon: Hash, hint: 'Whole numbers only' },
13
+ { id: 'decimal', label: 'Decimal', icon: Percent, hint: 'Continuous range' },
14
+ { id: 'dice', label: 'Dice', icon: Dices, hint: 'Classic 1–N roll' },
15
+ ];
16
+
17
+ export default function HomePage() {
18
+ const { app, ready } = useNotis();
19
+ const { documents, loading, refetch } = useRollDocuments();
20
+ const { persist: persistRoll } = usePersistRoll();
21
+
22
+ const [mode, setMode] = useState<Mode>('integer');
23
+ const [min, setMin] = useState(1);
24
+ const [max, setMax] = useState(100);
25
+ const [current, setCurrent] = useState<Roll | null>(null);
26
+ const [rolling, setRolling] = useState(false);
27
+
28
+ const recent = useMemo(
29
+ () => documents
30
+ .map(normalizeRollRecord)
31
+ .sort(sortRollRecordsDesc)
32
+ .slice(0, 5),
33
+ [documents],
34
+ );
35
+
36
+ const persist = useCallback(async (next: Roll) => {
37
+ await persistRoll(next);
38
+ refetch();
39
+ }, [persistRoll, refetch]);
40
+
41
+ const generate = useCallback(async () => {
42
+ if (rolling) return;
43
+ setRolling(true);
44
+ const next = roll(min, max, mode);
45
+ setCurrent(next);
46
+ try {
47
+ await persist(next);
48
+ } finally {
49
+ setRolling(false);
50
+ }
51
+ }, [rolling, min, max, mode, persist]);
52
+
53
+ useEffect(() => {
54
+ if (mode === 'dice') {
55
+ setMin(1);
56
+ setMax(6);
57
+ }
58
+ }, [mode]);
59
+
60
+ return (
61
+ <main className="notis-random-shell space-y-8">
62
+ <header className="flex items-start justify-between gap-6">
63
+ <div className="space-y-1.5">
64
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
65
+ <Sparkles className="h-3.5 w-3.5" />
66
+ <span>{ready ? app?.name : 'Loading…'}</span>
67
+ </div>
68
+ <h1 className="text-2xl font-semibold tracking-tight text-foreground">Generator</h1>
69
+ <p className="text-sm text-muted-foreground max-w-md">
70
+ Pick a mode, set the range, and press generate. Every roll is saved to the History view.
71
+ </p>
72
+ </div>
73
+ <button
74
+ type="button"
75
+ onClick={() => refetch()}
76
+ className="inline-flex h-9 items-center gap-2 rounded-md border border-border px-3 text-sm text-muted-foreground hover:text-foreground hover:bg-accent transition"
77
+ >
78
+ <RefreshCw className={cn('h-3.5 w-3.5', loading && 'animate-spin')} />
79
+ Refresh
80
+ </button>
81
+ </header>
82
+
83
+ <section className="rounded-2xl border border-border bg-card p-8">
84
+ <div className="flex flex-col items-center gap-6">
85
+ <div
86
+ className={cn(
87
+ 'flex items-center justify-center rounded-full border border-border bg-background',
88
+ 'h-48 w-48 transition-transform',
89
+ rolling && 'animate-pulse',
90
+ )}
91
+ >
92
+ {current ? (
93
+ <span className="font-mono text-5xl font-semibold tabular-nums text-foreground">
94
+ {formatNumber(current.value)}
95
+ </span>
96
+ ) : (
97
+ <InfinityIcon className="h-10 w-10 text-muted-foreground" strokeWidth={1.2} />
98
+ )}
99
+ </div>
100
+ <button
101
+ type="button"
102
+ onClick={generate}
103
+ disabled={rolling}
104
+ className={cn(
105
+ 'inline-flex h-11 items-center gap-2 rounded-full px-6 text-sm font-medium',
106
+ 'bg-primary text-primary-foreground hover:opacity-90 disabled:opacity-50 transition',
107
+ )}
108
+ >
109
+ <Dices className="h-4 w-4" />
110
+ {rolling ? 'Rolling…' : current ? 'Roll again' : 'Generate'}
111
+ </button>
112
+ {current && (
113
+ <p className="text-xs text-muted-foreground">
114
+ <span className="font-mono">[{formatNumber(current.min)}, {formatNumber(current.max)}]</span>
115
+ {' · '}{current.mode}
116
+ </p>
117
+ )}
118
+ </div>
119
+ </section>
120
+
121
+ <section className="grid gap-6 md:grid-cols-2">
122
+ <div className="rounded-xl border border-border bg-card p-5">
123
+ <h2 className="mb-3 text-sm font-medium text-foreground">Mode</h2>
124
+ <div className="grid gap-2">
125
+ {MODES.map(({ id, label, icon: Icon, hint }) => (
126
+ <button
127
+ key={id}
128
+ type="button"
129
+ onClick={() => setMode(id)}
130
+ className={cn(
131
+ 'flex items-center gap-3 rounded-md border px-3 py-2.5 text-left transition',
132
+ mode === id
133
+ ? 'border-foreground bg-accent text-accent-foreground'
134
+ : 'border-border hover:bg-accent/50',
135
+ )}
136
+ >
137
+ <Icon className="h-4 w-4 shrink-0" strokeWidth={1.5} />
138
+ <div className="flex-1 min-w-0">
139
+ <div className="text-sm font-medium">{label}</div>
140
+ <div className="text-xs text-muted-foreground">{hint}</div>
141
+ </div>
142
+ </button>
143
+ ))}
144
+ </div>
145
+ </div>
146
+
147
+ <div className="rounded-xl border border-border bg-card p-5 space-y-4">
148
+ <h2 className="text-sm font-medium text-foreground">Range</h2>
149
+ <div className="grid grid-cols-2 gap-3">
150
+ <RangeInput label="Minimum" value={min} onChange={setMin} disabled={mode === 'dice'} />
151
+ <RangeInput label="Maximum" value={max} onChange={setMax} disabled={mode === 'dice'} />
152
+ </div>
153
+ {mode === 'dice' && (
154
+ <p className="text-xs text-muted-foreground">Dice mode locks to 1–6.</p>
155
+ )}
156
+ </div>
157
+ </section>
158
+
159
+ <section className="rounded-xl border border-border bg-card p-5">
160
+ <header className="mb-4 flex items-center justify-between gap-3">
161
+ <h2 className="flex items-center gap-2 text-sm font-medium text-foreground">
162
+ <History className="h-4 w-4 text-muted-foreground" strokeWidth={1.5} />
163
+ Recent rolls
164
+ </h2>
165
+ <a
166
+ href="/history"
167
+ className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
168
+ >
169
+ View all <ArrowRight className="h-3 w-3" />
170
+ </a>
171
+ </header>
172
+ {loading && recent.length === 0 ? (
173
+ <p className="text-sm text-muted-foreground">Loading…</p>
174
+ ) : recent.length === 0 ? (
175
+ <div className="rounded-md border border-dashed border-border px-4 py-8 text-center text-sm text-muted-foreground">
176
+ Nothing rolled yet. Press Generate to see history here.
177
+ </div>
178
+ ) : (
179
+ <ul className="divide-y divide-border">
180
+ {recent.map((r) => (
181
+ <li key={r.id} className="flex items-center justify-between py-2.5">
182
+ <div className="flex items-center gap-3">
183
+ <span className="font-mono text-lg tabular-nums text-foreground">{formatNumber(r.value)}</span>
184
+ <span className="text-xs text-muted-foreground">{r.mode}</span>
185
+ </div>
186
+ <span className="text-xs text-muted-foreground">{relativeTime(r.at)}</span>
187
+ </li>
188
+ ))}
189
+ </ul>
190
+ )}
191
+ </section>
192
+ </main>
193
+ );
194
+ }
195
+
196
+ function RangeInput({
197
+ label,
198
+ value,
199
+ onChange,
200
+ disabled,
201
+ }: {
202
+ label: string;
203
+ value: number;
204
+ onChange: (v: number) => void;
205
+ disabled?: boolean;
206
+ }) {
207
+ return (
208
+ <label className={cn('block space-y-1.5', disabled && 'opacity-50')}>
209
+ <span className="text-xs font-medium text-muted-foreground">{label}</span>
210
+ <input
211
+ type="number"
212
+ value={value}
213
+ disabled={disabled}
214
+ onChange={(e) => {
215
+ const parsed = Number(e.target.value);
216
+ if (Number.isFinite(parsed)) onChange(parsed);
217
+ }}
218
+ className="h-10 w-full rounded-md border border-border bg-background px-3 font-mono text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/40"
219
+ />
220
+ </label>
221
+ );
222
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Random Number Generator (dev preview)</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/dev-main.tsx"></script>
11
+ </body>
12
+ </html>