@notis_ai/cli 0.2.6 → 0.2.8

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 (151) hide show
  1. package/README.md +17 -0
  2. package/dist/scaffolds/notis-database/app/page.tsx +38 -41
  3. package/dist/scaffolds/notis-database/lib/types.ts +5 -7
  4. package/dist/scaffolds/notis-database/metadata/screenshot-1.png +0 -0
  5. package/dist/scaffolds/notis-database/metadata/screenshot-2.png +0 -0
  6. package/dist/scaffolds/notis-database/metadata/screenshot-3.png +0 -0
  7. package/dist/scaffolds/notis-database/metadata/screenshot-4.png +0 -0
  8. package/dist/scaffolds/notis-database/metadata/screenshot-5.png +0 -0
  9. package/dist/scaffolds/notis-database/metadata/screenshot-fixtures.json +1839 -0
  10. package/dist/scaffolds/notis-database/notis.config.ts +48 -1
  11. package/dist/scaffolds/notis-database/package-lock.json +3935 -0
  12. package/dist/scaffolds/notis-database/package.json +2 -1
  13. package/dist/scaffolds/notis-database/packages/sdk/package.json +36 -0
  14. package/dist/scaffolds/notis-database/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  15. package/dist/scaffolds/notis-database/packages/sdk/src/components/Markdown.tsx +60 -0
  16. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  17. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  18. package/dist/scaffolds/notis-database/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  19. package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +196 -0
  20. package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +229 -0
  21. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useBackend.ts +41 -0
  22. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  23. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocument.ts +78 -0
  24. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDocuments.ts +121 -0
  25. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  26. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotis.ts +34 -0
  27. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  28. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTool.ts +64 -0
  29. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTools.ts +56 -0
  30. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  31. package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  32. package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +83 -0
  33. package/dist/scaffolds/notis-database/packages/sdk/src/provider.tsx +43 -0
  34. package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +220 -0
  35. package/dist/scaffolds/notis-database/packages/sdk/src/styles.css +186 -0
  36. package/dist/scaffolds/notis-database/packages/sdk/src/ui.ts +15 -0
  37. package/dist/scaffolds/notis-database/packages/sdk/src/vite.ts +56 -0
  38. package/dist/scaffolds/notis-database/packages/sdk/tsconfig.json +15 -0
  39. package/dist/scaffolds/notis-database/src/mock-runtime.ts +4 -1
  40. package/dist/scaffolds/notis-journal/CHANGELOG.md +4 -0
  41. package/dist/scaffolds/notis-journal/app/page.tsx +1 -0
  42. package/dist/scaffolds/notis-journal/package-lock.json +4615 -0
  43. package/dist/scaffolds/notis-journal/package.json +2 -2
  44. package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +52 -1
  45. package/dist/scaffolds/notis-notes/app/page.tsx +39 -46
  46. package/dist/scaffolds/notis-notes/app/phosphor-icons.ts +596 -0
  47. package/dist/scaffolds/notis-notes/lib/visible-properties.ts +144 -0
  48. package/dist/scaffolds/notis-notes/metadata/screenshot-1.png +0 -0
  49. package/dist/scaffolds/notis-notes/metadata/screenshot-2.png +0 -0
  50. package/dist/scaffolds/notis-notes/metadata/screenshot-3.png +0 -0
  51. package/dist/scaffolds/notis-notes/metadata/screenshot-4.png +0 -0
  52. package/dist/scaffolds/notis-notes/metadata/screenshot-5.png +0 -0
  53. package/dist/scaffolds/notis-notes/metadata/screenshot-fixtures.json +752 -0
  54. package/dist/scaffolds/notis-notes/notis.config.ts +48 -3
  55. package/dist/scaffolds/notis-notes/package-lock.json +4636 -0
  56. package/dist/scaffolds/notis-notes/package.json +7 -3
  57. package/dist/scaffolds/notis-notes/packages/sdk/package.json +36 -0
  58. package/dist/scaffolds/notis-notes/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  59. package/dist/scaffolds/notis-notes/packages/sdk/src/components/Markdown.tsx +60 -0
  60. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  61. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  62. package/dist/scaffolds/notis-notes/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  63. package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +196 -0
  64. package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +229 -0
  65. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useBackend.ts +41 -0
  66. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  67. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocument.ts +78 -0
  68. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDocuments.ts +121 -0
  69. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  70. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotis.ts +34 -0
  71. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  72. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTool.ts +64 -0
  73. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTools.ts +56 -0
  74. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  75. package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  76. package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +83 -0
  77. package/dist/scaffolds/notis-notes/packages/sdk/src/provider.tsx +43 -0
  78. package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +220 -0
  79. package/dist/scaffolds/notis-notes/packages/sdk/src/styles.css +186 -0
  80. package/dist/scaffolds/notis-notes/packages/sdk/src/ui.ts +15 -0
  81. package/dist/scaffolds/notis-notes/packages/sdk/src/vite.ts +56 -0
  82. package/dist/scaffolds/notis-notes/packages/sdk/tsconfig.json +15 -0
  83. package/dist/scaffolds/notis-random/CHANGELOG.md +12 -2
  84. package/dist/scaffolds/notis-random/app/history/page.tsx +5 -4
  85. package/dist/scaffolds/notis-random/app/page.tsx +141 -74
  86. package/dist/scaffolds/notis-random/components/ui/button.tsx +50 -0
  87. package/dist/scaffolds/notis-random/components/ui/card.tsx +16 -0
  88. package/dist/scaffolds/notis-random/components/ui/input.tsx +23 -0
  89. package/dist/scaffolds/notis-random/components.json +20 -0
  90. package/dist/scaffolds/notis-random/lib/notis-tools.ts +26 -7
  91. package/dist/scaffolds/notis-random/lib/rng.ts +179 -19
  92. package/dist/scaffolds/notis-random/lib/roll-record.ts +103 -16
  93. package/dist/scaffolds/notis-random/metadata/screenshot-1.png +0 -0
  94. package/dist/scaffolds/notis-random/metadata/screenshot-2.png +0 -0
  95. package/dist/scaffolds/notis-random/metadata/screenshot-3.png +0 -0
  96. package/dist/scaffolds/notis-random/metadata/screenshot-4.png +0 -0
  97. package/dist/scaffolds/notis-random/metadata/screenshot-5.png +0 -0
  98. package/dist/scaffolds/notis-random/metadata/screenshot-fixtures.json +753 -0
  99. package/dist/scaffolds/notis-random/notis.config.ts +47 -3
  100. package/dist/scaffolds/notis-random/package-lock.json +4513 -0
  101. package/dist/scaffolds/notis-random/package.json +6 -2
  102. package/dist/scaffolds/notis-random/packages/sdk/package.json +36 -0
  103. package/dist/scaffolds/notis-random/packages/sdk/src/components/DocumentEditor.tsx +93 -0
  104. package/dist/scaffolds/notis-random/packages/sdk/src/components/Markdown.tsx +60 -0
  105. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectActionBar.tsx +278 -0
  106. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectCheckbox.tsx +91 -0
  107. package/dist/scaffolds/notis-random/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  108. package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +196 -0
  109. package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +229 -0
  110. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useBackend.ts +41 -0
  111. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSchema.ts +85 -0
  112. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocument.ts +78 -0
  113. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDocuments.ts +121 -0
  114. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useMultiSelect.ts +539 -0
  115. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotis.ts +34 -0
  116. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useNotisNavigation.ts +49 -0
  117. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTool.ts +64 -0
  118. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTools.ts +56 -0
  119. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useTopBarSearch.ts +73 -0
  120. package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  121. package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +83 -0
  122. package/dist/scaffolds/notis-random/packages/sdk/src/provider.tsx +43 -0
  123. package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +220 -0
  124. package/dist/scaffolds/notis-random/packages/sdk/src/styles.css +186 -0
  125. package/dist/scaffolds/notis-random/packages/sdk/src/ui.ts +15 -0
  126. package/dist/scaffolds/notis-random/packages/sdk/src/vite.ts +56 -0
  127. package/dist/scaffolds/notis-random/packages/sdk/tsconfig.json +15 -0
  128. package/dist/scaffolds/notis-random/tailwind.config.ts +8 -1
  129. package/dist/scaffolds.json +8 -6
  130. package/package.json +2 -1
  131. package/skills/notis-apps/SKILL.md +449 -144
  132. package/skills/notis-apps/cli.md +15 -0
  133. package/skills/notis-cli/SKILL.md +8 -4
  134. package/skills/notis-onboarding/BRIEF.md +93 -0
  135. package/src/cli.js +52 -7
  136. package/src/command-specs/apps.js +154 -21
  137. package/src/command-specs/diagnostics.js +674 -0
  138. package/src/command-specs/helpers.js +4 -1
  139. package/src/command-specs/index.js +8 -0
  140. package/src/command-specs/onboarding.js +216 -0
  141. package/src/command-specs/smoke.js +386 -0
  142. package/src/command-specs/tools.js +189 -5
  143. package/src/runtime/app-platform.js +65 -4
  144. package/src/runtime/desktop-auth.js +66 -17
  145. package/src/runtime/errors.js +1 -0
  146. package/src/runtime/output.js +21 -3
  147. package/src/runtime/profiles.js +214 -18
  148. package/src/runtime/telemetry.js +92 -0
  149. package/src/runtime/transport.js +29 -1
  150. package/template/.harness/index.html.tmpl +30 -2
  151. package/template/packages/sdk/src/config.ts +38 -1
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Notis theme CSS variables.
3
+ *
4
+ * Import this in your app layout:
5
+ *
6
+ * import '@notis/sdk/styles.css';
7
+ *
8
+ * The portal injects the live Notis theme variables at render time.
9
+ */
10
+
11
+ @tailwind base;
12
+ @tailwind components;
13
+ @tailwind utilities;
14
+
15
+ @layer base {
16
+ * {
17
+ @apply border-border;
18
+ }
19
+ [data-notis-app-root] {
20
+ color: hsl(var(--foreground));
21
+ display: block;
22
+ min-height: 100%;
23
+ }
24
+ }
25
+
26
+ @layer components {
27
+ .notis-app-shell {
28
+ @apply mx-auto w-full max-w-[var(--portal-page-max-width)] px-4 py-6 md:px-4 md:py-8;
29
+ }
30
+
31
+ .notis-app-surface {
32
+ @apply rounded-2xl border border-border bg-card text-card-foreground shadow-sm;
33
+ }
34
+
35
+ .notis-app-section {
36
+ @apply p-5 md:p-6;
37
+ }
38
+ }
39
+
40
+ /*
41
+ * Markdown value-type rendering (see the `Markdown` component). Plain CSS on
42
+ * theme variables so it works inside the portal shadow root and the dev
43
+ * harness without depending on the app's Tailwind content scan.
44
+ */
45
+ .notis-markdown {
46
+ font-size: 14px;
47
+ line-height: 1.65;
48
+ color: hsl(var(--foreground));
49
+ overflow-wrap: break-word;
50
+ }
51
+
52
+ .notis-markdown--sm {
53
+ font-size: 13px;
54
+ line-height: 1.55;
55
+ }
56
+
57
+ .notis-markdown > :first-child {
58
+ margin-top: 0;
59
+ }
60
+
61
+ .notis-markdown > :last-child {
62
+ margin-bottom: 0;
63
+ }
64
+
65
+ .notis-markdown p {
66
+ margin: 0.625em 0;
67
+ }
68
+
69
+ .notis-markdown h1,
70
+ .notis-markdown h2,
71
+ .notis-markdown h3,
72
+ .notis-markdown h4,
73
+ .notis-markdown h5,
74
+ .notis-markdown h6 {
75
+ margin: 1.25em 0 0.5em;
76
+ font-weight: 650;
77
+ letter-spacing: -0.01em;
78
+ line-height: 1.3;
79
+ color: hsl(var(--foreground));
80
+ }
81
+
82
+ .notis-markdown h1 { font-size: 1.5em; }
83
+ .notis-markdown h2 { font-size: 1.25em; }
84
+ .notis-markdown h3 { font-size: 1.1em; }
85
+ .notis-markdown h4,
86
+ .notis-markdown h5,
87
+ .notis-markdown h6 { font-size: 1em; }
88
+
89
+ .notis-markdown ul,
90
+ .notis-markdown ol {
91
+ margin: 0.625em 0;
92
+ padding-left: 1.5em;
93
+ }
94
+
95
+ .notis-markdown ul { list-style-type: disc; }
96
+ .notis-markdown ol { list-style-type: decimal; }
97
+
98
+ .notis-markdown li {
99
+ margin: 0.25em 0;
100
+ }
101
+
102
+ .notis-markdown li > ul,
103
+ .notis-markdown li > ol {
104
+ margin: 0.25em 0;
105
+ }
106
+
107
+ .notis-markdown ul.contains-task-list {
108
+ list-style: none;
109
+ padding-left: 0.375em;
110
+ }
111
+
112
+ .notis-markdown li.task-list-item input[type='checkbox'] {
113
+ margin-right: 0.5em;
114
+ accent-color: hsl(var(--primary));
115
+ }
116
+
117
+ .notis-markdown blockquote {
118
+ margin: 0.75em 0;
119
+ padding: 0.125em 0 0.125em 0.875em;
120
+ border-left: 3px solid hsl(var(--border));
121
+ color: hsl(var(--muted-foreground));
122
+ }
123
+
124
+ .notis-markdown a {
125
+ color: hsl(var(--primary));
126
+ text-decoration: underline;
127
+ text-decoration-color: hsl(var(--primary) / 0.35);
128
+ text-underline-offset: 2px;
129
+ }
130
+
131
+ .notis-markdown a:hover {
132
+ text-decoration-color: hsl(var(--primary));
133
+ }
134
+
135
+ .notis-markdown code {
136
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
137
+ font-size: 0.875em;
138
+ padding: 0.125em 0.375em;
139
+ border-radius: 0.25rem;
140
+ background: hsl(var(--muted));
141
+ color: hsl(var(--foreground));
142
+ }
143
+
144
+ .notis-markdown pre {
145
+ margin: 0.75em 0;
146
+ padding: 0.75em 1em;
147
+ border-radius: 0.5rem;
148
+ background: hsl(var(--muted));
149
+ overflow-x: auto;
150
+ }
151
+
152
+ .notis-markdown pre code {
153
+ padding: 0;
154
+ background: transparent;
155
+ font-size: 0.875em;
156
+ }
157
+
158
+ .notis-markdown hr {
159
+ margin: 1.25em 0;
160
+ border: 0;
161
+ border-top: 1px solid hsl(var(--border));
162
+ }
163
+
164
+ .notis-markdown img {
165
+ max-width: 100%;
166
+ border-radius: 0.5rem;
167
+ }
168
+
169
+ .notis-markdown table {
170
+ margin: 0.75em 0;
171
+ width: 100%;
172
+ border-collapse: collapse;
173
+ font-size: 0.95em;
174
+ }
175
+
176
+ .notis-markdown th,
177
+ .notis-markdown td {
178
+ padding: 0.375em 0.75em;
179
+ border: 1px solid hsl(var(--border));
180
+ text-align: left;
181
+ }
182
+
183
+ .notis-markdown th {
184
+ background: hsl(var(--muted));
185
+ font-weight: 600;
186
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @notis/sdk/ui - Re-exported shadcn components for Notis apps.
3
+ *
4
+ * Apps include shadcn via their own `components.json` and `@/components/ui/*`
5
+ * imports. This entrypoint is provided as a convenience for apps that want to
6
+ * import directly from the SDK without setting up shadcn locally.
7
+ *
8
+ * For now, this is a placeholder. The scaffold template sets up shadcn
9
+ * directly in the app project, so components come from `@/components/ui/*`.
10
+ * This file can be expanded later to re-export a curated set of components
11
+ * pre-themed for Notis.
12
+ */
13
+
14
+ // Placeholder -- apps use shadcn directly via their project's components/ui/
15
+ export {};
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Vite config builder for Notis apps.
3
+ *
4
+ * Usage:
5
+ * ```ts
6
+ * // vite.config.ts
7
+ * import { notisViteConfig } from '@notis/sdk/vite';
8
+ * import appConfig from './notis.config';
9
+ * export default notisViteConfig(appConfig);
10
+ * ```
11
+ *
12
+ * Produces a library-mode ES module bundle with React externalized.
13
+ * Output: .notis/output/bundle/app.js + app.css
14
+ */
15
+
16
+ import type { NotisAppConfig } from './config';
17
+
18
+ export function notisViteConfig(appConfig: NotisAppConfig) {
19
+ return {
20
+ plugins: [
21
+ // @vitejs/plugin-react is added by the consumer's vite.config.ts
22
+ // or auto-detected. We provide the config shape only.
23
+ ],
24
+ build: {
25
+ lib: {
26
+ entry: '.notis/_entry.tsx',
27
+ formats: ['es'] as const,
28
+ fileName: () => 'app.js',
29
+ },
30
+ rollupOptions: {
31
+ external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
32
+ output: {
33
+ globals: {
34
+ react: 'window.React',
35
+ 'react-dom': 'window.ReactDOM',
36
+ 'react-dom/client': 'window.ReactDOMClient',
37
+ 'react/jsx-runtime': 'window.React',
38
+ },
39
+ assetFileNames: 'app[extname]',
40
+ inlineDynamicImports: true,
41
+ },
42
+ },
43
+ outDir: '.notis/output/bundle',
44
+ emptyOutDir: true,
45
+ cssCodeSplit: false,
46
+ },
47
+ resolve: {
48
+ alias: {
49
+ '@': process.cwd(),
50
+ },
51
+ },
52
+ define: {
53
+ 'process.env.NODE_ENV': JSON.stringify('production'),
54
+ },
55
+ };
56
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "declaration": true,
11
+ "outDir": "dist",
12
+ "rootDir": "src"
13
+ },
14
+ "include": ["src"]
15
+ }
@@ -1,5 +1,15 @@
1
1
  # Random Number Generator Changelog
2
2
 
3
- ## [Initial Release] - {PR_MERGE_DATE}
3
+ ## [Reliable Range Editing] - 2026-07-29
4
4
 
5
- - First Store release.
5
+ - Keeps empty and negative range drafts intact until they form a valid number.
6
+
7
+ ## [Generator modes and history] - 2026-07-29
8
+
9
+ - Added integer, decimal, and d4–d20 dice modes with live bounds validation.
10
+ - Added a complete roll history with reliable saving and in-app navigation.
11
+ - Refreshed the generator with native Notis controls and light and dark Store media.
12
+
13
+ ## [Initial Release] - 2026-04-23
14
+
15
+ - Generated random numbers with configurable bounds and saved recent rolls.
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useMemo } from 'react';
4
4
  import { DiceFiveIcon as Dices, ClockCounterClockwiseIcon as History, HashIcon as Hash, PercentIcon as Percent } from '@phosphor-icons/react';
5
+ import { Card } from '@/components/ui/card';
5
6
  import { formatNumber, relativeTime } from '@/lib/utils';
6
7
  import type { Mode } from '@/lib/rng';
7
8
  import { normalizeRollRecord, sortRollRecordsDesc } from '@/lib/roll-record';
@@ -14,7 +15,7 @@ const MODE_ICONS: Record<Mode, typeof Hash> = {
14
15
  };
15
16
 
16
17
  export default function HistoryPage() {
17
- const { documents, loading } = useRollDocuments();
18
+ const { documents, loading } = useRollDocuments(100, true);
18
19
 
19
20
  const rows = useMemo(
20
21
  () => documents
@@ -24,7 +25,7 @@ export default function HistoryPage() {
24
25
  );
25
26
 
26
27
  return (
27
- <main className="notis-random-shell space-y-6">
28
+ <main data-store-screenshot="history" className="notis-random-shell space-y-6">
28
29
  <header className="flex items-center gap-2 text-sm text-muted-foreground">
29
30
  <History className="h-4 w-4" strokeWidth={1.5} />
30
31
  <span>Roll history</span>
@@ -40,7 +41,7 @@ export default function HistoryPage() {
40
41
  Nothing rolled yet. Head back to the generator and press Generate.
41
42
  </div>
42
43
  ) : (
43
- <div className="divide-y divide-border rounded-xl border border-border bg-card">
44
+ <Card className="divide-y divide-border">
44
45
  {rows.map((row) => {
45
46
  const Icon = MODE_ICONS[row.mode] ?? Hash;
46
47
  return (
@@ -59,7 +60,7 @@ export default function HistoryPage() {
59
60
  </div>
60
61
  );
61
62
  })}
62
- </div>
63
+ </Card>
63
64
  )}
64
65
  </main>
65
66
  );
@@ -1,29 +1,35 @@
1
1
  'use client';
2
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';
3
+ import { useCallback, useMemo, useState } from 'react';
4
+ import { useNotis, useNotisNavigation } 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, WarningCircleIcon as WarningCircle } from '@phosphor-icons/react';
6
+ import { Button } from '@/components/ui/button';
7
+ import { Card } from '@/components/ui/card';
8
+ import { Input } from '@/components/ui/input';
6
9
  import { cn, formatNumber, relativeTime } from '@/lib/utils';
7
- import { type Mode, type Roll, roll } from '@/lib/rng';
10
+ import { DICE_SIDES, DEFAULT_DICE_SIDES, type DiceSides, type Mode, type Roll, checkBounds, parseBoundDraft, roll } from '@/lib/rng';
8
11
  import { normalizeRollRecord, sortRollRecordsDesc } from '@/lib/roll-record';
9
12
  import { usePersistRoll, useRollDocuments } from '@/lib/notis-tools';
10
13
 
11
14
  const MODES: Array<{ id: Mode; label: string; icon: typeof Hash; hint: string }> = [
12
15
  { id: 'integer', label: 'Integer', icon: Hash, hint: 'Whole numbers only' },
13
16
  { id: 'decimal', label: 'Decimal', icon: Percent, hint: 'Continuous range' },
14
- { id: 'dice', label: 'Dice', icon: Dices, hint: 'Classic 1–N roll' },
17
+ { id: 'dice', label: 'Dice', icon: Dices, hint: 'Standard polyhedral dice' },
15
18
  ];
16
19
 
17
20
  export default function HomePage() {
18
21
  const { app, ready } = useNotis();
19
- const { documents, loading, refetch } = useRollDocuments();
22
+ const nav = useNotisNavigation();
23
+ const { documents, loading, refetch } = useRollDocuments(5);
20
24
  const { persist: persistRoll } = usePersistRoll();
21
25
 
22
26
  const [mode, setMode] = useState<Mode>('integer');
23
- const [min, setMin] = useState(1);
24
- const [max, setMax] = useState(100);
27
+ const [minDraft, setMinDraft] = useState('1');
28
+ const [maxDraft, setMaxDraft] = useState('100');
29
+ const [sides, setSides] = useState<DiceSides>(DEFAULT_DICE_SIDES);
25
30
  const [current, setCurrent] = useState<Roll | null>(null);
26
31
  const [rolling, setRolling] = useState(false);
32
+ const [saveError, setSaveError] = useState<string | null>(null);
27
33
 
28
34
  const recent = useMemo(
29
35
  () => documents
@@ -33,32 +39,43 @@ export default function HomePage() {
33
39
  [documents],
34
40
  );
35
41
 
36
- const persist = useCallback(async (next: Roll) => {
37
- await persistRoll(next);
38
- refetch();
39
- }, [persistRoll, refetch]);
42
+ // Dice mode draws from 1..sides; the manual range inputs drive the other modes.
43
+ const bounds = useMemo(
44
+ () => (
45
+ mode === 'dice'
46
+ ? { min: 1, max: sides }
47
+ : {
48
+ min: parseBoundDraft(minDraft),
49
+ max: parseBoundDraft(maxDraft),
50
+ }
51
+ ),
52
+ [mode, sides, minDraft, maxDraft],
53
+ );
54
+ const check = useMemo(
55
+ () => checkBounds(bounds.min, bounds.max, mode),
56
+ [bounds.min, bounds.max, mode],
57
+ );
40
58
 
41
59
  const generate = useCallback(async () => {
42
- if (rolling) return;
60
+ if (rolling || !check.ok) return;
43
61
  setRolling(true);
44
- const next = roll(min, max, mode);
45
- setCurrent(next);
62
+ setSaveError(null);
46
63
  try {
47
- await persist(next);
64
+ // roll() throws on unusable bounds, and persist() can fail on the network.
65
+ // Both have to leave `rolling` false or the button would stay stuck.
66
+ const next = roll(bounds.min, bounds.max, mode);
67
+ setCurrent(next);
68
+ await persistRoll(next);
69
+ refetch();
70
+ } catch (err) {
71
+ setSaveError(err instanceof Error ? err.message : 'Could not save this roll.');
48
72
  } finally {
49
73
  setRolling(false);
50
74
  }
51
- }, [rolling, min, max, mode, persist]);
52
-
53
- useEffect(() => {
54
- if (mode === 'dice') {
55
- setMin(1);
56
- setMax(6);
57
- }
58
- }, [mode]);
75
+ }, [rolling, check.ok, bounds.min, bounds.max, mode, persistRoll, refetch]);
59
76
 
60
77
  return (
61
- <main className="notis-random-shell space-y-8">
78
+ <main data-store-screenshot="generator" className="notis-random-shell space-y-6">
62
79
  <header className="flex items-start justify-between gap-6">
63
80
  <div className="space-y-1.5">
64
81
  <div className="flex items-center gap-2 text-sm text-muted-foreground">
@@ -70,22 +87,26 @@ export default function HomePage() {
70
87
  Pick a mode, set the range, and press generate. Every roll is saved to the History view.
71
88
  </p>
72
89
  </div>
73
- <button
90
+ <Button
74
91
  type="button"
92
+ variant="outline"
93
+ size="sm"
75
94
  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"
95
+ className="gap-2 border-border text-muted-foreground hover:text-foreground"
77
96
  >
78
97
  <RefreshCw className={cn('h-3.5 w-3.5', loading && 'animate-spin')} />
79
98
  Refresh
80
- </button>
99
+ </Button>
81
100
  </header>
82
101
 
83
- <section className="rounded-2xl border border-border bg-card p-8">
84
- <div className="flex flex-col items-center gap-6">
102
+ {/* The dial and the controls sit side by side so the whole generator —
103
+ result, mode, range, and recent rolls — stays above the fold. */}
104
+ <section className="grid gap-6 lg:grid-cols-[1.05fr_1fr]">
105
+ <Card className="flex flex-col items-center justify-center gap-5 rounded-2xl p-6">
85
106
  <div
86
107
  className={cn(
87
108
  'flex items-center justify-center rounded-full border border-border bg-background',
88
- 'h-48 w-48 transition-transform',
109
+ 'h-40 w-40 transition-transform',
89
110
  rolling && 'animate-pulse',
90
111
  )}
91
112
  >
@@ -97,38 +118,49 @@ export default function HomePage() {
97
118
  <InfinityIcon className="h-10 w-10 text-muted-foreground" strokeWidth={1.2} />
98
119
  )}
99
120
  </div>
100
- <button
121
+ <Button
101
122
  type="button"
123
+ size="lg"
124
+ data-action="generate"
102
125
  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
- )}
126
+ disabled={rolling || !check.ok}
127
+ className="gap-2 rounded-full px-6"
108
128
  >
109
129
  <Dices className="h-4 w-4" />
110
130
  {rolling ? 'Rolling…' : current ? 'Roll again' : 'Generate'}
111
- </button>
112
- {current && (
131
+ </Button>
132
+ {check.issue ? (
133
+ <p className="flex items-center gap-1.5 text-xs text-destructive">
134
+ <WarningCircle className="h-3.5 w-3.5 shrink-0" />
135
+ {check.issue.message}
136
+ </p>
137
+ ) : current ? (
113
138
  <p className="text-xs text-muted-foreground">
114
139
  <span className="font-mono">[{formatNumber(current.min)}, {formatNumber(current.max)}]</span>
115
140
  {' · '}{current.mode}
116
141
  </p>
142
+ ) : null}
143
+ {saveError && (
144
+ <p className="flex items-center gap-1.5 text-xs text-destructive">
145
+ <WarningCircle className="h-3.5 w-3.5 shrink-0" />
146
+ {saveError}
147
+ </p>
117
148
  )}
118
- </div>
119
- </section>
149
+ </Card>
120
150
 
121
- <section className="grid gap-6 md:grid-cols-2">
122
- <div className="rounded-xl border border-border bg-card p-5">
151
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-1">
152
+ <Card className="p-5">
123
153
  <h2 className="mb-3 text-sm font-medium text-foreground">Mode</h2>
124
154
  <div className="grid gap-2">
125
155
  {MODES.map(({ id, label, icon: Icon, hint }) => (
126
- <button
156
+ <Button
127
157
  key={id}
128
158
  type="button"
159
+ variant="outline"
160
+ data-mode={id}
129
161
  onClick={() => setMode(id)}
130
162
  className={cn(
131
- 'flex items-center gap-3 rounded-md border px-3 py-2.5 text-left transition',
163
+ 'h-auto w-full justify-start gap-3 px-3 py-2.5 text-left whitespace-normal',
132
164
  mode === id
133
165
  ? 'border-foreground bg-accent text-accent-foreground'
134
166
  : 'border-border hover:bg-accent/50',
@@ -139,35 +171,72 @@ export default function HomePage() {
139
171
  <div className="text-sm font-medium">{label}</div>
140
172
  <div className="text-xs text-muted-foreground">{hint}</div>
141
173
  </div>
142
- </button>
174
+ </Button>
143
175
  ))}
144
176
  </div>
145
- </div>
177
+ </Card>
146
178
 
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>
179
+ <Card className="space-y-4 p-5">
180
+ <h2 className="text-sm font-medium text-foreground">
181
+ {mode === 'dice' ? 'Die' : 'Range'}
182
+ </h2>
183
+ {mode === 'dice' ? (
184
+ <>
185
+ <div className="flex flex-wrap gap-2">
186
+ {DICE_SIDES.map((n) => (
187
+ <Button
188
+ key={n}
189
+ type="button"
190
+ variant="outline"
191
+ data-die={n}
192
+ onClick={() => setSides(n)}
193
+ className={cn(
194
+ 'h-10 min-w-[3.25rem] px-3 font-mono',
195
+ sides === n
196
+ ? 'border-foreground bg-accent text-accent-foreground'
197
+ : 'border-border hover:bg-accent/50',
198
+ )}
199
+ >
200
+ d{n}
201
+ </Button>
202
+ ))}
203
+ </div>
204
+ <p className="text-xs text-muted-foreground">
205
+ Rolls a whole number from 1 to {sides}, each equally likely.
206
+ </p>
207
+ </>
208
+ ) : (
209
+ <>
210
+ <div className="grid grid-cols-2 gap-3">
211
+ <RangeInput label="Minimum" name="min" value={minDraft} onChange={setMinDraft} />
212
+ <RangeInput label="Maximum" name="max" value={maxDraft} onChange={setMaxDraft} />
213
+ </div>
214
+ <p className="text-xs text-muted-foreground">
215
+ {mode === 'decimal'
216
+ ? 'Decimal draws include the minimum but never reach the maximum.'
217
+ : 'Integer draws include both ends of the range.'}
218
+ </p>
219
+ </>
155
220
  )}
221
+ </Card>
156
222
  </div>
157
223
  </section>
158
224
 
159
- <section className="rounded-xl border border-border bg-card p-5">
225
+ <Card className="p-5">
160
226
  <header className="mb-4 flex items-center justify-between gap-3">
161
227
  <h2 className="flex items-center gap-2 text-sm font-medium text-foreground">
162
228
  <History className="h-4 w-4 text-muted-foreground" strokeWidth={1.5} />
163
229
  Recent rolls
164
230
  </h2>
165
- <a
166
- href="/history"
167
- className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground"
231
+ <Button
232
+ type="button"
233
+ variant="ghost"
234
+ size="sm"
235
+ onClick={() => nav.toRoute('/history')}
236
+ className="h-auto gap-1 px-2 py-1 text-xs text-muted-foreground hover:text-foreground"
168
237
  >
169
238
  View all <ArrowRight className="h-3 w-3" />
170
- </a>
239
+ </Button>
171
240
  </header>
172
241
  {loading && recent.length === 0 ? (
173
242
  <p className="text-sm text-muted-foreground">Loading…</p>
@@ -188,34 +257,32 @@ export default function HomePage() {
188
257
  ))}
189
258
  </ul>
190
259
  )}
191
- </section>
260
+ </Card>
192
261
  </main>
193
262
  );
194
263
  }
195
264
 
196
265
  function RangeInput({
197
266
  label,
267
+ name,
198
268
  value,
199
269
  onChange,
200
- disabled,
201
270
  }: {
202
271
  label: string;
203
- value: number;
204
- onChange: (v: number) => void;
205
- disabled?: boolean;
272
+ name: string;
273
+ value: string;
274
+ onChange: (v: string) => void;
206
275
  }) {
207
276
  return (
208
- <label className={cn('block space-y-1.5', disabled && 'opacity-50')}>
277
+ <label className="block space-y-1.5">
209
278
  <span className="text-xs font-medium text-muted-foreground">{label}</span>
210
- <input
211
- type="number"
279
+ <Input
280
+ type="text"
281
+ inputMode="decimal"
282
+ data-bound={name}
212
283
  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"
284
+ onChange={(e) => onChange(e.target.value)}
285
+ className="border-border font-mono"
219
286
  />
220
287
  </label>
221
288
  );