rift-ds 1.13.0 → 1.14.0

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 (173) hide show
  1. package/README.md +13 -5
  2. package/behaviors/ScrollRegion.d.ts +8 -0
  3. package/behaviors/ScrollRegion.js +51 -0
  4. package/behaviors/devWarning.d.ts +9 -0
  5. package/behaviors/devWarning.js +16 -0
  6. package/behaviors/useAnchoredPosition.d.ts +111 -0
  7. package/behaviors/useAnchoredPosition.js +153 -0
  8. package/behaviors/useFocusScope.d.ts +9 -2
  9. package/behaviors/useFocusScope.js +5 -1
  10. package/bin/rift-ds.mjs +181 -25
  11. package/bin/templates/app/components/rift/ColorModeScript.tsx +10 -0
  12. package/bin/templates/app/components/rift/RiftRoot.tsx +13 -0
  13. package/bin/templates/app/components/rift/font.css +7 -0
  14. package/bin/templates/app/components/rift/font.ts +14 -0
  15. package/bin/templates/blocks/app/dashboard/page.module.css +90 -0
  16. package/bin/templates/blocks/app/dashboard/page.tsx +141 -0
  17. package/bin/templates/blocks/app/sign-in/page.module.css +91 -0
  18. package/bin/templates/blocks/app/sign-in/page.tsx +95 -0
  19. package/bin/templates/blocks/app/starter/page.module.css +7 -0
  20. package/bin/templates/blocks/app/starter/page.tsx +31 -0
  21. package/bin/templates/blocks/components/blocks/AppShell.module.css +160 -0
  22. package/bin/templates/blocks/components/blocks/AppShell.tsx +113 -0
  23. package/bin/templates/blocks/registry.json +39 -0
  24. package/chat/AssistantTurn.js +4 -2
  25. package/chat/types.d.ts +8 -7
  26. package/chat/useChat.js +2 -2
  27. package/components/AgentStatus/AgentRing.d.ts +27 -0
  28. package/components/AgentStatus/AgentRing.js +9 -0
  29. package/components/AgentStatus/AgentStatus.css +637 -9
  30. package/components/AgentStatus/AgentStatus.d.ts +31 -3
  31. package/components/AgentStatus/AgentStatus.js +22 -4
  32. package/components/AgentStatus/AgentStatusPatterns.d.ts +1 -1
  33. package/components/AgentStatus/AgentStatusPatterns.js +2 -1
  34. package/components/AiButton/AiButton.css +211 -38
  35. package/components/AiButton/AiButton.d.ts +34 -0
  36. package/components/AiButton/AiButton.js +129 -9
  37. package/components/Avatar/Avatar.css +54 -0
  38. package/components/Avatar/Avatar.d.ts +9 -1
  39. package/components/Avatar/Avatar.js +5 -1
  40. package/components/Avatar/avatarFill.d.ts +9 -0
  41. package/components/Avatar/avatarFill.js +13 -0
  42. package/components/AvatarGroup/AvatarGroup.js +13 -1
  43. package/components/BottomNav/BottomNav.css +2 -2
  44. package/components/Button/Button.css +7 -0
  45. package/components/Button/Button.d.ts +8 -1
  46. package/components/Button/Button.js +10 -1
  47. package/components/Checkbox/Checkbox.js +1 -1
  48. package/components/CircularButton/CircularButton.css +8 -7
  49. package/components/CodeBlock/CodeBlock.d.ts +2 -0
  50. package/components/CodeBlock/CodeBlock.js +2 -0
  51. package/components/ColorPicker/ColorPicker.css +3 -4
  52. package/components/ColorPicker/ColorPicker.js +11 -0
  53. package/components/Combobox/Combobox.css +20 -5
  54. package/components/Combobox/Combobox.js +75 -55
  55. package/components/CommandPaletteTrigger/CommandPaletteTrigger.css +88 -0
  56. package/components/CommandPaletteTrigger/CommandPaletteTrigger.d.ts +31 -0
  57. package/components/CommandPaletteTrigger/CommandPaletteTrigger.js +49 -0
  58. package/components/Composer/Composer.css +3 -3
  59. package/components/DataTable/CellEditor.d.ts +37 -0
  60. package/components/DataTable/CellEditor.js +153 -0
  61. package/components/DataTable/DataTable.css +1180 -0
  62. package/components/DataTable/DataTable.d.ts +498 -27
  63. package/components/DataTable/DataTable.js +2363 -189
  64. package/components/DataTable/FilterEditor.d.ts +10 -0
  65. package/components/DataTable/FilterEditor.js +181 -0
  66. package/components/DataTable/TypedCell.d.ts +22 -0
  67. package/components/DataTable/TypedCell.js +80 -0
  68. package/components/DataTable/VirtualRows.d.ts +21 -0
  69. package/components/DataTable/VirtualRows.js +38 -0
  70. package/components/DataTable/aggregates.d.ts +29 -0
  71. package/components/DataTable/aggregates.js +52 -0
  72. package/components/DataTable/columnTypes.d.ts +49 -0
  73. package/components/DataTable/columnTypes.js +96 -0
  74. package/components/DataTable/dragGesture.d.ts +24 -0
  75. package/components/DataTable/dragGesture.js +62 -0
  76. package/components/DataTable/editorKinds.d.ts +24 -0
  77. package/components/DataTable/editorKinds.js +20 -0
  78. package/components/DataTable/filterModel.d.ts +84 -0
  79. package/components/DataTable/filterModel.js +159 -0
  80. package/components/DataTable/tableContext.d.ts +82 -0
  81. package/components/DataTable/tableContext.js +7 -0
  82. package/components/DataTable/views.d.ts +50 -0
  83. package/components/DataTable/views.js +39 -0
  84. package/components/DateInput/DateInput.css +6 -0
  85. package/components/Drawer/Drawer.js +2 -1
  86. package/components/Dropdown/Dropdown.css +22 -6
  87. package/components/Dropdown/Dropdown.d.ts +6 -0
  88. package/components/Dropdown/Dropdown.js +37 -7
  89. package/components/DropdownMenu/DropdownMenu.css +30 -12
  90. package/components/DropdownMenu/DropdownMenu.d.ts +4 -0
  91. package/components/DropdownMenu/DropdownMenu.js +65 -14
  92. package/components/FilterBar/FilterBar.css +10 -6
  93. package/components/FilterBar/FilterBar.js +68 -44
  94. package/components/HoverCard/HoverCard.css +24 -44
  95. package/components/HoverCard/HoverCard.js +18 -2
  96. package/components/Input/Input.css +6 -0
  97. package/components/InterruptCard/InterruptCard.css +8 -0
  98. package/components/InterruptCard/InterruptCard.d.ts +2 -0
  99. package/components/InterruptCard/InterruptCard.js +7 -1
  100. package/components/ModelPicker/ModelPicker.css +14 -11
  101. package/components/ModelPicker/ModelPicker.d.ts +2 -0
  102. package/components/ModelPicker/ModelPicker.js +15 -1
  103. package/components/Pagination/Pagination.css +8 -6
  104. package/components/PixelAvatar/pixelInks.d.ts +6 -0
  105. package/components/PixelAvatar/pixelInks.js +7 -3
  106. package/components/Popover/Popover.css +32 -30
  107. package/components/Popover/Popover.d.ts +8 -0
  108. package/components/Popover/Popover.js +16 -2
  109. package/components/Prose/Prose.css +47 -8
  110. package/components/Prose/Prose.d.ts +2 -0
  111. package/components/Prose/Prose.js +7 -2
  112. package/components/RichDropdown/RichDropdown.css +17 -5
  113. package/components/RichDropdown/RichDropdown.js +13 -0
  114. package/components/SegmentedControl/SegmentedControl.css +8 -0
  115. package/components/ShaderField/useShaderField.js +13 -1
  116. package/components/SourceChip/SourceChip.css +6 -12
  117. package/components/SourceChip/SourceChip.js +10 -35
  118. package/components/Table/Table.css +61 -19
  119. package/components/Table/Table.d.ts +8 -0
  120. package/components/Table/Table.js +31 -11
  121. package/components/TableToolbar/TableToolbar.css +494 -0
  122. package/components/TableToolbar/TableToolbar.d.ts +281 -0
  123. package/components/TableToolbar/TableToolbar.js +1321 -0
  124. package/components/ThemePicker/ThemePicker.css +189 -0
  125. package/components/ThemePicker/ThemePicker.d.ts +67 -0
  126. package/components/ThemePicker/ThemePicker.js +165 -0
  127. package/components/ThemeToggle/ThemeToggle.css +82 -0
  128. package/components/ThemeToggle/ThemeToggle.d.ts +42 -0
  129. package/components/ThemeToggle/ThemeToggle.js +98 -0
  130. package/components/ThemeToggle/colorMode.d.ts +42 -0
  131. package/components/ThemeToggle/colorMode.js +26 -0
  132. package/components/ThemeToggle/useColorMode.d.ts +8 -0
  133. package/components/ThemeToggle/useColorMode.js +44 -0
  134. package/components/TimePicker/TimePicker.css +11 -5
  135. package/components/TimePicker/TimePicker.js +11 -0
  136. package/components/ToggleGroup/ToggleGroup.css +10 -0
  137. package/components/Tooltip/Tooltip.css +24 -25
  138. package/components/Tooltip/Tooltip.js +16 -60
  139. package/components/TopAppBar/TopAppBar.css +5 -3
  140. package/components/registry.json +36 -4
  141. package/components/registry.json.d.ts +36 -4
  142. package/components/registry.json.js +1 -1
  143. package/index.d.ts +11 -0
  144. package/index.js +31 -2
  145. package/package.json +5 -1
  146. package/tokens/base.css +84 -0
  147. package/tokens/motion.d.ts +2 -0
  148. package/tokens/motion.js +2 -0
  149. package/tokens/presets/contrast.css +6 -2
  150. package/tokens/presets/coral.css +4 -2
  151. package/tokens/presets/forest.css +6 -0
  152. package/tokens/presets/gold.css +10 -2
  153. package/tokens/presets/mono.css +4 -2
  154. package/tokens/presets/pink.css +10 -0
  155. package/tokens/presets/terminal.css +8 -2
  156. package/tokens/presets/violet.css +10 -0
  157. package/tokens/presets/warm.css +4 -0
  158. package/tokens/presets/zest.css +6 -0
  159. package/tokens/registry.d.ts +1 -1
  160. package/tokens/registry.json +27 -1
  161. package/tokens/registry.json.d.ts +27 -1
  162. package/tokens/registry.json.js +1 -1
  163. package/tokens/tokens-dark.css +36 -0
  164. package/tokens/tokens-forced-colors.css +31 -0
  165. package/tokens/tokens-light.css +49 -0
  166. package/tokens/tokens-primitives.css +9 -0
  167. package/tokens/tokens.css +1 -0
  168. package/cli/templates/chat/app/api/chat/feedback/route.d.ts +0 -3
  169. package/cli/templates/chat/app/api/chat/followups/route.d.ts +0 -3
  170. package/cli/templates/chat/app/api/chat/guardrails.d.ts +0 -10
  171. package/cli/templates/chat/app/api/chat/route.d.ts +0 -4
  172. package/cli/templates/chat/components/chat/ChatMount.d.ts +0 -1
  173. package/cli/templates/chat/components/chat/models.d.ts +0 -11
package/bin/rift-ds.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * The package's one bin: `npx rift-ds init` and `npx rift-ds init chat`.
3
+ * The package's one bin: `npx rift-ds init`, `npx rift-ds init app`,
4
+ * `npx rift-ds init chat` and `npx rift-ds init block`.
4
5
  *
5
6
  * `init` fetches the generated agent skills from the live site into the
6
7
  * project's .claude/skills/ folder, one folder per skill, then prints the
@@ -20,6 +21,22 @@
20
21
  * installed version's own subpaths and must match it. It never overwrites:
21
22
  * a file that exists is skipped and listed, so re-running is safe.
22
23
  *
24
+ * `init app` sets a Next.js app router project up for the library the same
25
+ * way, from dist/bin/templates/app/: a root wrapper that hands Next's Link
26
+ * to every link-rendering component, a component that renders the
27
+ * colour-mode script before first paint, and the base theme's face through
28
+ * next/font, all under components/rift/. It never edits the root layout
29
+ * (every app already has one): it prints the whole layout to use instead,
30
+ * importing tokens.css, base.css and the font, rendering the script and
31
+ * wrapping the body, and points at init block for a first page.
32
+ *
33
+ * `init block <name>` copies one block, a whole starting page, from
34
+ * dist/bin/templates/blocks/: the files its registry.json entry lists,
35
+ * held at their target paths so the relative imports between them resolve
36
+ * as they do there. Blocks share files (the app shell), and the
37
+ * never-overwrite rule means a shared file is written by whichever block
38
+ * comes first. With no name it lists the blocks.
39
+ *
23
40
  * The manifest is data from the network that decides where this command
24
41
  * writes, so every name and path in it is checked before anything touches
25
42
  * the disk: a skill name is one kebab-case folder, a file is a plain
@@ -45,7 +62,10 @@ const PACKAGE_NAME = 'rift-ds';
45
62
  const CHAT_SLUG = 'ai-chat';
46
63
  const MANIFEST_URL = `${SITE_ORIGIN}/skill/manifest.json`;
47
64
  const DEFAULT_DIR = join('.claude', 'skills');
48
- const TEMPLATES_DIR = join(dirname(fileURLToPath(import.meta.url)), 'templates', 'chat');
65
+ const TEMPLATES_ROOT = join(dirname(fileURLToPath(import.meta.url)), 'templates');
66
+ const TEMPLATES_DIR = join(TEMPLATES_ROOT, 'chat');
67
+ const APP_TEMPLATES_DIR = join(TEMPLATES_ROOT, 'app');
68
+ const BLOCKS_DIR = join(TEMPLATES_ROOT, 'blocks');
49
69
 
50
70
  /* What `init chat` needs installed beside the package. The optional peers
51
71
  each chat subpath imports; the demo's recharts is only for the scripted
@@ -60,7 +80,9 @@ const CHAT_PEERS = [
60
80
  ];
61
81
 
62
82
  const USAGE = `Usage: npx ${PACKAGE_NAME} init [--skill <name>] [--dir <dir>]
83
+ npx ${PACKAGE_NAME} init app [--app <dir>] [--dry-run]
63
84
  npx ${PACKAGE_NAME} init chat [--app <dir>] [--dry-run]
85
+ npx ${PACKAGE_NAME} init block [<name>] [--app <dir>] [--dry-run]
64
86
 
65
87
  init
66
88
  Fetches the agent skills for this package from ${SITE_ORIGIN} into
@@ -74,6 +96,17 @@ init
74
96
  --out <dir> Deprecated: write one skill's files straight into <dir>
75
97
  (${SKILL_NAME} unless --skill names another). Use --dir.
76
98
 
99
+ init app
100
+ Sets a Next.js app router project up for the library, under
101
+ components/rift/: a root wrapper that routes every link through Next's
102
+ Link, a component that applies the stored colour mode before first paint,
103
+ and the base theme's font. Prints the root layout to use; it never edits
104
+ the layout itself.
105
+ Existing files are never overwritten.
106
+
107
+ --app <dir> The app router folder (default: app/ or src/app/, whichever exists)
108
+ --dry-run Print what would be written and write nothing
109
+
77
110
  init chat
78
111
  Scaffolds the chat widget into a Next.js app router project: the chat,
79
112
  follow-ups and feedback routes under app/api/chat/, the guardrails beside
@@ -83,6 +116,16 @@ init chat
83
116
  --app <dir> The app router folder (default: app/ or src/app/, whichever exists)
84
117
  --dry-run Print what would be written and write nothing
85
118
 
119
+ init block [<name>]
120
+ Copies a block into a Next.js app router project: a whole starting page
121
+ (an app shell, a dashboard, a sign-in) built from the library, its page
122
+ under app/ and any shared parts under components/blocks/. The files are
123
+ yours from then on. With no name, lists the blocks. Run init app first.
124
+ Existing files are never overwritten.
125
+
126
+ --app <dir> The app router folder (default: app/ or src/app/, whichever exists)
127
+ --dry-run Print what would be written and write nothing
128
+
86
129
  --help Show this message
87
130
 
88
131
  The list of skills is at ${MANIFEST_URL}`;
@@ -162,38 +205,31 @@ async function init({ only, dir, out }) {
162
205
  console.log(` claude mcp add --transport http ${MCP_SERVER_NAME} ${SITE_ORIGIN}/api/mcp`);
163
206
  }
164
207
 
165
- /** Every template file, as a path relative to the templates folder (forward slashes). */
166
- function templateFiles(dir = TEMPLATES_DIR) {
208
+ /** Every template file under `base`, as a path relative to it (forward slashes). */
209
+ function templateFiles(base = TEMPLATES_DIR, dir = base) {
167
210
  const out = [];
168
211
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
169
212
  const full = join(dir, entry.name);
170
- if (entry.isDirectory()) out.push(...templateFiles(full));
171
- else out.push(relative(TEMPLATES_DIR, full).split('\\').join('/'));
213
+ if (entry.isDirectory()) out.push(...templateFiles(base, full));
214
+ else out.push(relative(base, full).split('\\').join('/'));
172
215
  }
173
216
  return out.sort();
174
217
  }
175
218
 
176
- /**
177
- * Where a template lands. `app/**` goes into the app router folder,
178
- * `components/**` beside it (src/components when the app is src/app), and
179
- * the env example at the project root.
180
- */
181
- function destinationFor(file, appDir) {
182
- if (file.startsWith('app/')) return join(appDir, ...file.slice('app/'.length).split('/'));
183
- if (file.startsWith('components/')) return join(dirname(appDir), ...file.split('/'));
184
- return join(process.cwd(), ...file.split('/'));
185
- }
186
-
187
- function initChat({ app, dryRun }) {
188
- if (!existsSync(TEMPLATES_DIR)) {
189
- fail('this copy carries no chat templates: run the published bin (npx ' + PACKAGE_NAME + ' init chat).');
190
- }
219
+ /** The app router folder: the one passed, else app/ or src/app/, whichever exists. */
220
+ function findAppDir(app) {
191
221
  const appDir = app ?? ['app', join('src', 'app')].map((d) => resolve(d)).find((d) => existsSync(d));
192
222
  if (!appDir) {
193
223
  fail('no app/ or src/app/ folder found. Run this inside a Next.js app router project, or pass --app <dir>.');
194
224
  }
225
+ return appDir;
226
+ }
195
227
 
196
- const files = templateFiles();
228
+ /**
229
+ * Writes every template under `base` (or only `files`, paths relative to
230
+ * it) that does not exist yet; returns what was written and skipped.
231
+ */
232
+ function scaffold(base, appDir, dryRun, files = templateFiles(base)) {
197
233
  const written = [];
198
234
  const skipped = [];
199
235
  for (const file of files) {
@@ -204,7 +240,7 @@ function initChat({ app, dryRun }) {
204
240
  }
205
241
  if (!dryRun) {
206
242
  mkdirSync(dirname(dest), { recursive: true });
207
- writeFileSync(dest, readFileSync(join(TEMPLATES_DIR, ...file.split('/'))));
243
+ writeFileSync(dest, readFileSync(join(base, ...file.split('/'))));
208
244
  }
209
245
  written.push(dest);
210
246
  console.log(`${dryRun ? '○' : '▸'} ${relative(process.cwd(), dest)}`);
@@ -212,6 +248,87 @@ function initChat({ app, dryRun }) {
212
248
  for (const dest of skipped) {
213
249
  console.log(`· ${relative(process.cwd(), dest)} exists, left as it is`);
214
250
  }
251
+ return { written, skipped };
252
+ }
253
+
254
+ function initApp({ app, dryRun }) {
255
+ if (!existsSync(APP_TEMPLATES_DIR)) {
256
+ fail('this copy carries no app templates: run the published bin (npx ' + PACKAGE_NAME + ' init app).');
257
+ }
258
+ const appDir = findAppDir(app);
259
+ const { written, skipped } = scaffold(APP_TEMPLATES_DIR, appDir, dryRun);
260
+ console.log(
261
+ dryRun
262
+ ? `\n✓ Dry run: ${written.length} files would be written, ${skipped.length} left alone.`
263
+ : `\n✓ App set up: ${written.length} files written, ${skipped.length} left alone.`
264
+ );
265
+ const rift = relative(appDir, join(dirname(appDir), 'components', 'rift')).split('\\').join('/');
266
+ const layoutFile = relative(process.cwd(), join(appDir, 'layout.tsx'));
267
+ console.log(`\nNext, make your root layout (${layoutFile}) this, keeping any metadata you have:\n`);
268
+ for (const line of [
269
+ `import '${PACKAGE_NAME}/tokens/tokens.css';`,
270
+ `import '${PACKAGE_NAME}/tokens/base.css';`,
271
+ `import '${rift}/font.css';`,
272
+ `import { riftFont } from '${rift}/font';`,
273
+ `import { ColorModeScript } from '${rift}/ColorModeScript';`,
274
+ `import { RiftRoot } from '${rift}/RiftRoot';`,
275
+ ``,
276
+ `export default function RootLayout({ children }: { children: React.ReactNode }) {`,
277
+ ` return (`,
278
+ ` <html lang="en" className={riftFont.variable} suppressHydrationWarning>`,
279
+ ` <head>`,
280
+ ` <ColorModeScript />`,
281
+ ` </head>`,
282
+ ` <body>`,
283
+ ` <RiftRoot>{children}</RiftRoot>`,
284
+ ` </body>`,
285
+ ` </html>`,
286
+ ` );`,
287
+ `}`,
288
+ ]) {
289
+ console.log(` ${line}`);
290
+ }
291
+ console.log(`\nIf the layout imports a globals.css from create-next-app, drop that import (or empty the file):`);
292
+ console.log(`its body colours and font would cover the library's.`);
293
+ console.log(`\nThen add a first page: npx ${PACKAGE_NAME} init block dashboard (no name lists them).`);
294
+ console.log(`Setup and theming: ${SITE_ORIGIN}/docs/get-started`);
295
+ }
296
+
297
+ /**
298
+ * Where a template lands. `app/**` goes into the app router folder,
299
+ * `components/**` beside it (src/components when the app is src/app), and
300
+ * the env example at the project root.
301
+ */
302
+ function destinationFor(file, appDir) {
303
+ if (file.startsWith('app/')) return join(appDir, ...file.slice('app/'.length).split('/'));
304
+ if (file.startsWith('components/')) return join(dirname(appDir), ...file.split('/'));
305
+ return join(process.cwd(), ...file.split('/'));
306
+ }
307
+
308
+ /**
309
+ * Whether the project's .gitignore would hide the scaffolded env example:
310
+ * a `.env*`-style rule with no `!.env.chat.example` exception after it.
311
+ * create-next-app writes `.env*`, so a fresh app's example never reaches
312
+ * a commit unless the scaffold says so.
313
+ */
314
+ function ignoresEnvExample() {
315
+ const gitignore = resolve('.gitignore');
316
+ if (!existsSync(gitignore)) return false;
317
+ let ignored = false;
318
+ for (const raw of readFileSync(gitignore, 'utf8').split('\n')) {
319
+ const line = raw.trim();
320
+ if (line === '.env*' || line === '.env.*' || line === '.env.chat.example') ignored = true;
321
+ if (line === '!.env.chat.example' || line === '!.env*.example' || line === '!.env.*.example') ignored = false;
322
+ }
323
+ return ignored;
324
+ }
325
+
326
+ function initChat({ app, dryRun }) {
327
+ if (!existsSync(TEMPLATES_DIR)) {
328
+ fail('this copy carries no chat templates: run the published bin (npx ' + PACKAGE_NAME + ' init chat).');
329
+ }
330
+ const appDir = findAppDir(app);
331
+ const { written, skipped } = scaffold(TEMPLATES_DIR, appDir, dryRun);
215
332
 
216
333
  console.log(
217
334
  dryRun
@@ -228,6 +345,12 @@ function initChat({ app, dryRun }) {
228
345
  ` 3. Copy .env.chat.example into .env.local. With NEXT_PUBLIC_CHAT_DEMO=true the widget plays a scripted\n` +
229
346
  ` conversation and needs no key; set ANTHROPIC_API_KEY and CHAT_ENABLED=true to go live.`
230
347
  );
348
+ if (ignoresEnvExample()) {
349
+ console.log(
350
+ ` Your .gitignore ignores .env* (create-next-app's default), so git will not see the example.\n` +
351
+ ` Commit it on purpose with: git add -f .env.chat.example`
352
+ );
353
+ }
231
354
  console.log(
232
355
  ` 4. Your layout's viewport should set interactiveWidget: "resizes-content", so the phone takeover ends above the keyboard.`
233
356
  );
@@ -235,6 +358,36 @@ function initChat({ app, dryRun }) {
235
358
  console.log(`Every prop: ${SITE_ORIGIN}/components/${CHAT_SLUG}`);
236
359
  }
237
360
 
361
+ function initBlock({ name, app, dryRun }) {
362
+ const registryFile = join(BLOCKS_DIR, 'registry.json');
363
+ if (!existsSync(registryFile)) {
364
+ fail('this copy carries no blocks: run the published bin (npx ' + PACKAGE_NAME + ' init block).');
365
+ }
366
+ const { blocks } = JSON.parse(readFileSync(registryFile, 'utf8'));
367
+ if (!name) {
368
+ console.log('Blocks:\n');
369
+ for (const block of blocks) console.log(` ${block.name.padEnd(12)} ${block.description}`);
370
+ console.log(`\nAdd one with: npx ${PACKAGE_NAME} init block <name>`);
371
+ console.log(`See them: ${SITE_ORIGIN}/docs/blocks`);
372
+ return;
373
+ }
374
+ const block = blocks.find((b) => b.name === name);
375
+ if (!block) {
376
+ fail(`no block named "${name}". The blocks: ${blocks.map((b) => b.name).join(', ')}.`);
377
+ }
378
+ const appDir = findAppDir(app);
379
+ const { written, skipped } = scaffold(BLOCKS_DIR, appDir, dryRun, block.files);
380
+ console.log(
381
+ dryRun
382
+ ? `\n✓ Dry run: ${written.length} files would be written, ${skipped.length} left alone.`
383
+ : `\n✓ ${block.label} block added: ${written.length} files written, ${skipped.length} left alone.`
384
+ );
385
+ console.log(`\nNext:`);
386
+ console.log(` 1. If you have not, run npx ${PACKAGE_NAME} init app and make your root layout the one it prints,`);
387
+ console.log(` so the tokens, the base sheet, the font and the colour mode are in place.`);
388
+ console.log(` 2. Open ${block.route} in your dev server. The files are yours: rename, restyle and grow them.`);
389
+ }
390
+
238
391
  const args = process.argv.slice(2);
239
392
  if (args.includes('--help') || args.includes('-h') || args.length === 0) {
240
393
  console.log(USAGE);
@@ -255,9 +408,12 @@ function option(flag, what) {
255
408
  return value;
256
409
  }
257
410
 
258
- if (args[1] === 'chat') {
411
+ if (args[1] === 'chat' || args[1] === 'app' || args[1] === 'block') {
259
412
  const appFlag = option('--app', 'a directory');
260
- initChat({ app: appFlag ? resolve(appFlag) : undefined, dryRun: args.includes('--dry-run') });
413
+ const scaffoldArgs = { app: appFlag ? resolve(appFlag) : undefined, dryRun: args.includes('--dry-run') };
414
+ if (args[1] === 'chat') initChat(scaffoldArgs);
415
+ else if (args[1] === 'app') initApp(scaffoldArgs);
416
+ else initBlock({ ...scaffoldArgs, name: args[2] && !args[2].startsWith('--') ? args[2] : undefined });
261
417
  } else {
262
418
  if (args[1] && !args[1].startsWith('--')) fail(`unknown init target "${args[1]}"\n\n${USAGE}`);
263
419
  const only = option('--skill', 'a skill name');
@@ -0,0 +1,10 @@
1
+ import { colorModeScript } from 'rift-ds/components/ThemeToggle/colorMode';
2
+
3
+ /**
4
+ * Applies the visitor's stored colour mode before first paint, so a
5
+ * returning visitor never sees the wrong mode flash. Render it in your root
6
+ * layout's <head>. ThemeToggle reads and changes the same setting.
7
+ */
8
+ export function ColorModeScript() {
9
+ return <script dangerouslySetInnerHTML={{ __html: colorModeScript() }} />;
10
+ }
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+ import { LinkProvider } from 'rift-ds/behaviors/LinkProvider';
5
+
6
+ /**
7
+ * The app's root for rift-ds: hands Next's Link to every rift-ds component
8
+ * that renders a link, so they navigate through the router instead of
9
+ * reloading the page. Wrap your root layout's body content in it.
10
+ */
11
+ export function RiftRoot({ children }: { children: React.ReactNode }) {
12
+ return <LinkProvider linkComponent={Link}>{children}</LinkProvider>;
13
+ }
@@ -0,0 +1,7 @@
1
+ /* Points the library's primary face at the one font.ts loads. The class
2
+ that defines --font-rift-primary sits on <html>, which is :root, so the
3
+ variable resolves here. A shipped theme (data-brand on <html>) sets its
4
+ own face and wins over this rule. */
5
+ :root {
6
+ --font-family-primary: var(--font-rift-primary), 'Nunito Sans', sans-serif;
7
+ }
@@ -0,0 +1,14 @@
1
+ import { Nunito_Sans } from 'next/font/google';
2
+
3
+ /**
4
+ * The base theme's typeface, Nunito Sans, self-hosted by Next at build time.
5
+ * Put `riftFont.variable` on <html> in your root layout and import
6
+ * ./font.css after the tokens: together they point the library's
7
+ * --font-family-primary at this face. To use your own face, swap the import
8
+ * here and keep the variable name.
9
+ */
10
+ export const riftFont = Nunito_Sans({
11
+ subsets: ['latin'],
12
+ weight: ['300', '400', '600', '700'],
13
+ variable: '--font-rift-primary',
14
+ });
@@ -0,0 +1,90 @@
1
+ /* The dashboard's grid. Four stats across on a desktop, two from the
2
+ laptop width down to a phone; the two panels sit side by side until the
3
+ tablet width, then stack. */
4
+
5
+ .stats {
6
+ display: grid;
7
+ grid-template-columns: repeat(4, minmax(0, 1fr));
8
+ gap: var(--gap-500);
9
+ }
10
+
11
+ .panels {
12
+ display: grid;
13
+ grid-template-columns: repeat(2, minmax(0, 1fr));
14
+ gap: var(--gap-500);
15
+ }
16
+
17
+ /* The projects table: its title, then the table on the page itself. */
18
+ .tableSection {
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: var(--gap-300);
22
+ min-width: 0;
23
+ }
24
+
25
+ .panelTitle {
26
+ margin: 0;
27
+ font-family: var(--font-title-body-family);
28
+ font-size: var(--font-title-body-size);
29
+ font-weight: var(--font-title-body-weight);
30
+ line-height: var(--font-title-body-line-height);
31
+ letter-spacing: var(--font-title-body-letter-spacing);
32
+ color: var(--color-text-primary);
33
+ }
34
+
35
+ .list {
36
+ margin: 0;
37
+ padding: 0;
38
+ list-style: none;
39
+ display: flex;
40
+ flex-direction: column;
41
+ gap: var(--gap-400);
42
+ }
43
+
44
+ .progressRow {
45
+ display: flex;
46
+ flex-direction: column;
47
+ gap: var(--gap-200);
48
+ }
49
+
50
+ .progressMeta,
51
+ .activityRow {
52
+ display: flex;
53
+ align-items: baseline;
54
+ justify-content: space-between;
55
+ gap: var(--gap-400);
56
+ font-size: var(--font-paragraph-sm-size);
57
+ line-height: var(--font-paragraph-sm-line-height);
58
+ color: var(--color-text-primary);
59
+ }
60
+
61
+ .muted {
62
+ flex-shrink: 0;
63
+ color: var(--color-text-secondary);
64
+ }
65
+
66
+ .strong {
67
+ font-weight: var(--font-paragraph-sm-emphasis-weight);
68
+ }
69
+
70
+ @media (max-width: 1279px) {
71
+ .stats {
72
+ grid-template-columns: repeat(2, minmax(0, 1fr));
73
+ }
74
+ }
75
+
76
+ @media (max-width: 959px) {
77
+ .panels {
78
+ grid-template-columns: minmax(0, 1fr);
79
+ }
80
+ }
81
+
82
+ @media (max-width: 600px) {
83
+ .stats {
84
+ gap: var(--gap-400);
85
+ }
86
+
87
+ .panels {
88
+ gap: var(--gap-400);
89
+ }
90
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * The dashboard block: the app shell with a row of stats, two panels and a
3
+ * table. The numbers are placeholders; swap each array for your data.
4
+ *
5
+ * The table is a DataTable with typed columns, so it needs no render
6
+ * functions: the status is a Badge in its option's role, the owner a
7
+ * person, the due date a date, each sorting and filtering by its type.
8
+ * Plain column objects can pass from this server page to the client
9
+ * table, which a render function could not. TableToolbar is its control
10
+ * bar: search, filters, sort and the column panel.
11
+ */
12
+
13
+ import { Button } from 'rift-ds/components/Button/Button';
14
+ import { Panel } from 'rift-ds/components/Panel/Panel';
15
+ import { ProgressBar } from 'rift-ds/components/ProgressBar/ProgressBar';
16
+ import { Stat } from 'rift-ds/components/Stat/Stat';
17
+ import { DataTable, type DataTableColumn, type DataTableRow } from 'rift-ds/components/DataTable/DataTable';
18
+ import { TableToolbar } from 'rift-ds/components/TableToolbar/TableToolbar';
19
+ import { AppShell } from '../../components/blocks/AppShell';
20
+ import styles from './page.module.css';
21
+
22
+ const STATS = [
23
+ { label: 'Open tasks', value: '128', delta: '+12', trend: 'up' },
24
+ { label: 'Due this week', value: '24', delta: '-3', trend: 'down' },
25
+ { label: 'Completed', value: '342', delta: '+18%', trend: 'up' },
26
+ { label: 'Avg. cycle time', value: '3.2d', delta: '-0.4d', trend: 'up' },
27
+ ] as const;
28
+
29
+ const PROGRESS = [
30
+ { name: 'Website refresh', done: 72 },
31
+ { name: 'Mobile app', done: 45 },
32
+ { name: 'Billing migration', done: 88 },
33
+ { name: 'Onboarding emails', done: 20 },
34
+ ];
35
+
36
+ const ACTIVITY = [
37
+ { who: 'Priya', what: 'closed', item: 'Fix login redirect', when: '10 min ago' },
38
+ { who: 'Sam', what: 'commented on', item: 'Pricing page copy', when: '1 hr ago' },
39
+ { who: 'Jordan', what: 'created', item: 'Q4 roadmap review', when: '3 hr ago' },
40
+ { who: 'Lee', what: 'moved', item: 'Invoice export to Review', when: 'Yesterday' },
41
+ ];
42
+
43
+ const STATUSES = [
44
+ { value: 'Planned', label: 'Planned', status: 'neutral' },
45
+ { value: 'On track', label: 'On track', status: 'positive' },
46
+ { value: 'At risk', label: 'At risk', status: 'warning' },
47
+ { value: 'Off track', label: 'Off track', status: 'error' },
48
+ ] as const;
49
+
50
+ const COLUMNS: DataTableColumn[] = [
51
+ { key: 'project', header: 'Project', rowHeader: true, sortable: true },
52
+ { key: 'owner', header: 'Owner', type: 'person', sortable: true },
53
+ { key: 'status', header: 'Status', type: 'status', sortable: true, options: [...STATUSES] },
54
+ { key: 'due', header: 'Due', type: 'date', sortable: true },
55
+ ];
56
+
57
+ const PROJECTS = [
58
+ { project: 'Website refresh', owner: 'Priya Shah', status: 'On track', due: '2026-11-14' },
59
+ { project: 'Mobile app', owner: 'Sam Rivera', status: 'At risk', due: '2026-12-02' },
60
+ { project: 'Billing migration', owner: 'Jordan Lee', status: 'On track', due: '2026-11-08' },
61
+ { project: 'Onboarding emails', owner: 'Lee Chen', status: 'Off track', due: '2026-10-30' },
62
+ { project: 'Partner portal', owner: 'Alex Morgan', status: 'Planned', due: '2027-01-15' },
63
+ ];
64
+
65
+ const ROWS: DataTableRow[] = PROJECTS.map((p) => ({ id: p.project, values: p }));
66
+
67
+ export default function DashboardPage() {
68
+ return (
69
+ <AppShell
70
+ activeKey="dashboard"
71
+ breadcrumb={[{ label: 'Workspace', href: '#' }, { label: 'Dashboard' }]}
72
+ title="Dashboard"
73
+ actions={
74
+ <>
75
+ <Button variant="secondary" size="compact" label="Export" iconLeft="download" />
76
+ <Button variant="primary" size="compact" label="New task" iconLeft="add" />
77
+ </>
78
+ }
79
+ >
80
+ <div className={styles.stats}>
81
+ {STATS.map((stat) => (
82
+ <Panel key={stat.label}>
83
+ <Stat
84
+ label={stat.label}
85
+ value={stat.value}
86
+ delta={stat.delta}
87
+ trend={stat.trend}
88
+ deltaPlacement="inline"
89
+ />
90
+ </Panel>
91
+ ))}
92
+ </div>
93
+
94
+ <div className={styles.panels}>
95
+ <Panel>
96
+ <h2 className={styles.panelTitle}>Progress by project</h2>
97
+ <ul className={styles.list}>
98
+ {PROGRESS.map((row) => (
99
+ <li key={row.name} className={styles.progressRow}>
100
+ <div className={styles.progressMeta}>
101
+ <span>{row.name}</span>
102
+ <span className={styles.muted}>{row.done}%</span>
103
+ </div>
104
+ <ProgressBar value={row.done} size="compact" ariaLabel={`${row.name} progress`} />
105
+ </li>
106
+ ))}
107
+ </ul>
108
+ </Panel>
109
+
110
+ <Panel>
111
+ <h2 className={styles.panelTitle}>Recent activity</h2>
112
+ <ul className={styles.list}>
113
+ {ACTIVITY.map((row) => (
114
+ <li key={row.item} className={styles.activityRow}>
115
+ <span>
116
+ <strong className={styles.strong}>{row.who}</strong> {row.what} {row.item}
117
+ </span>
118
+ <span className={styles.muted}>{row.when}</span>
119
+ </li>
120
+ ))}
121
+ </ul>
122
+ </Panel>
123
+ </div>
124
+
125
+ {/* The table sits on the page, not in a Panel: it brings its own bordered frame. */}
126
+ <section className={styles.tableSection} aria-labelledby="projects-title">
127
+ <h2 id="projects-title" className={styles.panelTitle}>
128
+ Projects
129
+ </h2>
130
+ <DataTable
131
+ columns={COLUMNS}
132
+ rows={ROWS}
133
+ caption="Projects"
134
+ defaultSort={{ key: 'due', direction: 'asc' }}
135
+ phoneLayout="stack"
136
+ toolbar={<TableToolbar />}
137
+ />
138
+ </section>
139
+ </AppShell>
140
+ );
141
+ }
@@ -0,0 +1,91 @@
1
+ /* A card centred on the page at every width: 400px wide on a desktop, the
2
+ full width less the gutter on a phone, where it drops its fill and sits
3
+ straight on the page. */
4
+
5
+ .page {
6
+ min-height: 100dvh;
7
+ display: grid;
8
+ place-items: center;
9
+ padding: var(--padding-1000) var(--padding-500);
10
+ background-color: var(--color-bg-page-primary);
11
+ }
12
+
13
+ .card {
14
+ width: 100%;
15
+ max-width: 400px;
16
+ display: flex;
17
+ flex-direction: column;
18
+ gap: var(--gap-500);
19
+ padding: var(--padding-1000);
20
+ border-radius: var(--radius-600);
21
+ background-color: var(--color-bg-container-primary);
22
+ }
23
+
24
+ .intro {
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: var(--gap-200);
28
+ }
29
+
30
+ .product {
31
+ font-family: var(--font-title-body-family);
32
+ font-size: var(--font-title-body-size);
33
+ font-weight: var(--font-title-body-weight);
34
+ line-height: var(--font-title-body-line-height);
35
+ color: var(--color-text-primary);
36
+ }
37
+
38
+ .title {
39
+ margin: 0;
40
+ font-family: var(--font-heading-2-family);
41
+ font-size: var(--font-heading-2-size);
42
+ font-weight: var(--font-heading-2-weight);
43
+ line-height: var(--font-heading-2-line-height);
44
+ letter-spacing: var(--font-heading-2-letter-spacing);
45
+ color: var(--color-text-primary);
46
+ }
47
+
48
+ .subhead,
49
+ .footer {
50
+ margin: 0;
51
+ font-size: var(--font-paragraph-sm-size);
52
+ line-height: var(--font-paragraph-sm-line-height);
53
+ color: var(--color-text-secondary);
54
+ }
55
+
56
+ .form {
57
+ display: flex;
58
+ flex-direction: column;
59
+ gap: var(--gap-400);
60
+ }
61
+
62
+ .row {
63
+ display: flex;
64
+ align-items: center;
65
+ justify-content: space-between;
66
+ flex-wrap: wrap;
67
+ gap: var(--gap-200);
68
+ }
69
+
70
+ .full {
71
+ width: 100%;
72
+ }
73
+
74
+ .footer {
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ gap: var(--gap-100);
79
+ }
80
+
81
+ @media (max-width: 600px) {
82
+ .page {
83
+ place-items: start center;
84
+ padding: var(--padding-1000) var(--padding-400);
85
+ }
86
+
87
+ .card {
88
+ padding: 0;
89
+ background-color: transparent;
90
+ }
91
+ }