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.
- package/README.md +13 -5
- package/behaviors/ScrollRegion.d.ts +8 -0
- package/behaviors/ScrollRegion.js +51 -0
- package/behaviors/devWarning.d.ts +9 -0
- package/behaviors/devWarning.js +16 -0
- package/behaviors/useAnchoredPosition.d.ts +111 -0
- package/behaviors/useAnchoredPosition.js +153 -0
- package/behaviors/useFocusScope.d.ts +9 -2
- package/behaviors/useFocusScope.js +5 -1
- package/bin/rift-ds.mjs +181 -25
- package/bin/templates/app/components/rift/ColorModeScript.tsx +10 -0
- package/bin/templates/app/components/rift/RiftRoot.tsx +13 -0
- package/bin/templates/app/components/rift/font.css +7 -0
- package/bin/templates/app/components/rift/font.ts +14 -0
- package/bin/templates/blocks/app/dashboard/page.module.css +90 -0
- package/bin/templates/blocks/app/dashboard/page.tsx +141 -0
- package/bin/templates/blocks/app/sign-in/page.module.css +91 -0
- package/bin/templates/blocks/app/sign-in/page.tsx +95 -0
- package/bin/templates/blocks/app/starter/page.module.css +7 -0
- package/bin/templates/blocks/app/starter/page.tsx +31 -0
- package/bin/templates/blocks/components/blocks/AppShell.module.css +160 -0
- package/bin/templates/blocks/components/blocks/AppShell.tsx +113 -0
- package/bin/templates/blocks/registry.json +39 -0
- package/chat/AssistantTurn.js +4 -2
- package/chat/types.d.ts +8 -7
- package/chat/useChat.js +2 -2
- package/components/AgentStatus/AgentRing.d.ts +27 -0
- package/components/AgentStatus/AgentRing.js +9 -0
- package/components/AgentStatus/AgentStatus.css +637 -9
- package/components/AgentStatus/AgentStatus.d.ts +31 -3
- package/components/AgentStatus/AgentStatus.js +22 -4
- package/components/AgentStatus/AgentStatusPatterns.d.ts +1 -1
- package/components/AgentStatus/AgentStatusPatterns.js +2 -1
- package/components/AiButton/AiButton.css +211 -38
- package/components/AiButton/AiButton.d.ts +34 -0
- package/components/AiButton/AiButton.js +129 -9
- package/components/Avatar/Avatar.css +54 -0
- package/components/Avatar/Avatar.d.ts +9 -1
- package/components/Avatar/Avatar.js +5 -1
- package/components/Avatar/avatarFill.d.ts +9 -0
- package/components/Avatar/avatarFill.js +13 -0
- package/components/AvatarGroup/AvatarGroup.js +13 -1
- package/components/BottomNav/BottomNav.css +2 -2
- package/components/Button/Button.css +7 -0
- package/components/Button/Button.d.ts +8 -1
- package/components/Button/Button.js +10 -1
- package/components/Checkbox/Checkbox.js +1 -1
- package/components/CircularButton/CircularButton.css +8 -7
- package/components/CodeBlock/CodeBlock.d.ts +2 -0
- package/components/CodeBlock/CodeBlock.js +2 -0
- package/components/ColorPicker/ColorPicker.css +3 -4
- package/components/ColorPicker/ColorPicker.js +11 -0
- package/components/Combobox/Combobox.css +20 -5
- package/components/Combobox/Combobox.js +75 -55
- package/components/CommandPaletteTrigger/CommandPaletteTrigger.css +88 -0
- package/components/CommandPaletteTrigger/CommandPaletteTrigger.d.ts +31 -0
- package/components/CommandPaletteTrigger/CommandPaletteTrigger.js +49 -0
- package/components/Composer/Composer.css +3 -3
- package/components/DataTable/CellEditor.d.ts +37 -0
- package/components/DataTable/CellEditor.js +153 -0
- package/components/DataTable/DataTable.css +1180 -0
- package/components/DataTable/DataTable.d.ts +498 -27
- package/components/DataTable/DataTable.js +2363 -189
- package/components/DataTable/FilterEditor.d.ts +10 -0
- package/components/DataTable/FilterEditor.js +181 -0
- package/components/DataTable/TypedCell.d.ts +22 -0
- package/components/DataTable/TypedCell.js +80 -0
- package/components/DataTable/VirtualRows.d.ts +21 -0
- package/components/DataTable/VirtualRows.js +38 -0
- package/components/DataTable/aggregates.d.ts +29 -0
- package/components/DataTable/aggregates.js +52 -0
- package/components/DataTable/columnTypes.d.ts +49 -0
- package/components/DataTable/columnTypes.js +96 -0
- package/components/DataTable/dragGesture.d.ts +24 -0
- package/components/DataTable/dragGesture.js +62 -0
- package/components/DataTable/editorKinds.d.ts +24 -0
- package/components/DataTable/editorKinds.js +20 -0
- package/components/DataTable/filterModel.d.ts +84 -0
- package/components/DataTable/filterModel.js +159 -0
- package/components/DataTable/tableContext.d.ts +82 -0
- package/components/DataTable/tableContext.js +7 -0
- package/components/DataTable/views.d.ts +50 -0
- package/components/DataTable/views.js +39 -0
- package/components/DateInput/DateInput.css +6 -0
- package/components/Drawer/Drawer.js +2 -1
- package/components/Dropdown/Dropdown.css +22 -6
- package/components/Dropdown/Dropdown.d.ts +6 -0
- package/components/Dropdown/Dropdown.js +37 -7
- package/components/DropdownMenu/DropdownMenu.css +30 -12
- package/components/DropdownMenu/DropdownMenu.d.ts +4 -0
- package/components/DropdownMenu/DropdownMenu.js +65 -14
- package/components/FilterBar/FilterBar.css +10 -6
- package/components/FilterBar/FilterBar.js +68 -44
- package/components/HoverCard/HoverCard.css +24 -44
- package/components/HoverCard/HoverCard.js +18 -2
- package/components/Input/Input.css +6 -0
- package/components/InterruptCard/InterruptCard.css +8 -0
- package/components/InterruptCard/InterruptCard.d.ts +2 -0
- package/components/InterruptCard/InterruptCard.js +7 -1
- package/components/ModelPicker/ModelPicker.css +14 -11
- package/components/ModelPicker/ModelPicker.d.ts +2 -0
- package/components/ModelPicker/ModelPicker.js +15 -1
- package/components/Pagination/Pagination.css +8 -6
- package/components/PixelAvatar/pixelInks.d.ts +6 -0
- package/components/PixelAvatar/pixelInks.js +7 -3
- package/components/Popover/Popover.css +32 -30
- package/components/Popover/Popover.d.ts +8 -0
- package/components/Popover/Popover.js +16 -2
- package/components/Prose/Prose.css +47 -8
- package/components/Prose/Prose.d.ts +2 -0
- package/components/Prose/Prose.js +7 -2
- package/components/RichDropdown/RichDropdown.css +17 -5
- package/components/RichDropdown/RichDropdown.js +13 -0
- package/components/SegmentedControl/SegmentedControl.css +8 -0
- package/components/ShaderField/useShaderField.js +13 -1
- package/components/SourceChip/SourceChip.css +6 -12
- package/components/SourceChip/SourceChip.js +10 -35
- package/components/Table/Table.css +61 -19
- package/components/Table/Table.d.ts +8 -0
- package/components/Table/Table.js +31 -11
- package/components/TableToolbar/TableToolbar.css +494 -0
- package/components/TableToolbar/TableToolbar.d.ts +281 -0
- package/components/TableToolbar/TableToolbar.js +1321 -0
- package/components/ThemePicker/ThemePicker.css +189 -0
- package/components/ThemePicker/ThemePicker.d.ts +67 -0
- package/components/ThemePicker/ThemePicker.js +165 -0
- package/components/ThemeToggle/ThemeToggle.css +82 -0
- package/components/ThemeToggle/ThemeToggle.d.ts +42 -0
- package/components/ThemeToggle/ThemeToggle.js +98 -0
- package/components/ThemeToggle/colorMode.d.ts +42 -0
- package/components/ThemeToggle/colorMode.js +26 -0
- package/components/ThemeToggle/useColorMode.d.ts +8 -0
- package/components/ThemeToggle/useColorMode.js +44 -0
- package/components/TimePicker/TimePicker.css +11 -5
- package/components/TimePicker/TimePicker.js +11 -0
- package/components/ToggleGroup/ToggleGroup.css +10 -0
- package/components/Tooltip/Tooltip.css +24 -25
- package/components/Tooltip/Tooltip.js +16 -60
- package/components/TopAppBar/TopAppBar.css +5 -3
- package/components/registry.json +36 -4
- package/components/registry.json.d.ts +36 -4
- package/components/registry.json.js +1 -1
- package/index.d.ts +11 -0
- package/index.js +31 -2
- package/package.json +5 -1
- package/tokens/base.css +84 -0
- package/tokens/motion.d.ts +2 -0
- package/tokens/motion.js +2 -0
- package/tokens/presets/contrast.css +6 -2
- package/tokens/presets/coral.css +4 -2
- package/tokens/presets/forest.css +6 -0
- package/tokens/presets/gold.css +10 -2
- package/tokens/presets/mono.css +4 -2
- package/tokens/presets/pink.css +10 -0
- package/tokens/presets/terminal.css +8 -2
- package/tokens/presets/violet.css +10 -0
- package/tokens/presets/warm.css +4 -0
- package/tokens/presets/zest.css +6 -0
- package/tokens/registry.d.ts +1 -1
- package/tokens/registry.json +27 -1
- package/tokens/registry.json.d.ts +27 -1
- package/tokens/registry.json.js +1 -1
- package/tokens/tokens-dark.css +36 -0
- package/tokens/tokens-forced-colors.css +31 -0
- package/tokens/tokens-light.css +49 -0
- package/tokens/tokens-primitives.css +9 -0
- package/tokens/tokens.css +1 -0
- package/cli/templates/chat/app/api/chat/feedback/route.d.ts +0 -3
- package/cli/templates/chat/app/api/chat/followups/route.d.ts +0 -3
- package/cli/templates/chat/app/api/chat/guardrails.d.ts +0 -10
- package/cli/templates/chat/app/api/chat/route.d.ts +0 -4
- package/cli/templates/chat/components/chat/ChatMount.d.ts +0 -1
- 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
|
|
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
|
|
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
|
|
166
|
-
function templateFiles(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(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
+
}
|