@cogito.ai/astryx-web 0.0.0-stage → 0.2.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 (33) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +34 -2
  3. package/astryx.integration.mjs +15 -0
  4. package/bin/astryx-web.mjs +353 -0
  5. package/docs/dock-accessibility.doc.mjs +24 -0
  6. package/docs/dock-components.doc.mjs +26 -0
  7. package/docs/dock-copy.doc.mjs +23 -0
  8. package/docs/dock-design-base.doc.mjs +35 -0
  9. package/docs/dock-environment.doc.mjs +24 -0
  10. package/docs/dock-layout.doc.mjs +25 -0
  11. package/docs/dock-product-stance.doc.mjs +25 -0
  12. package/docs/dock-refuse-list.doc.mjs +34 -0
  13. package/docs/dock-states.doc.mjs +23 -0
  14. package/docs/dock-style-archetypes.doc.mjs +104 -0
  15. package/docs/dock-touch-mobile.doc.mjs +27 -0
  16. package/docs/dock-visual-acceptance.doc.mjs +23 -0
  17. package/package.json +51 -3
  18. package/skills/astryx-web-design/SKILL.md +21 -0
  19. package/skills/astryx-web-design/references/acceptance.md +12 -0
  20. package/skills/astryx-web-design/references/design-direction.md +52 -0
  21. package/skills/astryx-web-design/references/page-building.md +18 -0
  22. package/templates/dock-editor-frame.doc.mjs +19 -0
  23. package/templates/dock-editor-frame.tsx +229 -0
  24. package/templates/dock-form-flow.doc.mjs +19 -0
  25. package/templates/dock-form-flow.tsx +169 -0
  26. package/templates/dock-landing.doc.mjs +11 -0
  27. package/templates/dock-landing.tsx +104 -0
  28. package/templates/dock-list.doc.mjs +20 -0
  29. package/templates/dock-list.tsx +223 -0
  30. package/templates/dock-settings.doc.mjs +19 -0
  31. package/templates/dock-settings.tsx +272 -0
  32. package/templates/dock-workbench.doc.mjs +11 -0
  33. package/templates/dock-workbench.tsx +193 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AgentDock Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,35 @@
1
- # Temporary Holding Version
1
+ # @cogito.ai/astryx-web
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Mobile-first page archetypes and shared web design guidance for AgentDock projects.
4
+
5
+ ## Contents
6
+
7
+ - `dock-landing`, `dock-workbench`, `dock-list`, `dock-form-flow`, `dock-settings`, and
8
+ `dock-editor-frame` page templates
9
+ - Design-base documentation, starting with `pnpm exec astryx docs dock-design-base`
10
+ - Agent guidance for creating pages from templates
11
+ - `astryx-web samples` for repeatable visual review
12
+
13
+ The list, form, settings, and editor templates replace Core templates with the same intent. Use
14
+ `--package @astryxdesign/core` when the original is deliberately needed.
15
+
16
+ ## Visual samples
17
+
18
+ ```bash
19
+ astryx-web samples \
20
+ --base-url http://localhost:3000 \
21
+ --route /zh-CN/settings \
22
+ --theme neutral \
23
+ --mode light \
24
+ --mode dark \
25
+ --size 390x844 \
26
+ --size 1440x900 \
27
+ --out design-samples
28
+ ```
29
+
30
+ Set `CHROME_PATH` to the system Chrome executable. The command writes one PNG per combination and
31
+ an `index.json` containing the rendered routes and source commit.
32
+
33
+ ## License
34
+
35
+ MIT
@@ -0,0 +1,15 @@
1
+ /** @type {import('@astryxdesign/cli/authoring').AstryxIntegration} */
2
+ export default {
3
+ docs: './docs',
4
+ templates: './templates',
5
+
6
+ agentDocs: {
7
+ append: [
8
+ 'Create every page from a template. Put the page type last: write "mobile settings page", not "settings page on mobile".',
9
+ 'Prefer the dock-* archetypes; when you know the one you need, run astryx template <id> directly.',
10
+ 'Read astryx docs dock-design-base before UI work. Phone (390px) and desktop (1440px) are both first-class; touch targets are at least 44px.',
11
+ 'Keep every string in the app catalogs, every color a token, every breakpoint a container query; no style attributes and no viewport media queries.',
12
+ 'Before calling UI work done, render it at 390x844 and 1440x900 in light and dark and check it against astryx docs dock-visual-acceptance.',
13
+ ],
14
+ },
15
+ }
@@ -0,0 +1,353 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn, spawnSync } from 'node:child_process'
4
+ import { once } from 'node:events'
5
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
6
+ import { tmpdir } from 'node:os'
7
+ import { join, resolve } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ const COMMAND = 'samples'
11
+ const APPEARANCE_STORAGE_KEY = 'agentdock.appearance'
12
+
13
+ function requiredValue(args, index, flag) {
14
+ const value = args[index + 1]
15
+ if (!value || value.startsWith('--')) {
16
+ throw new Error(`${flag} needs a value`)
17
+ }
18
+ return value
19
+ }
20
+
21
+ function parseSize(value) {
22
+ const match = /^(\d+)x(\d+)$/.exec(value)
23
+ if (!match) throw new Error(`invalid --size ${value}; expected WIDTHxHEIGHT`)
24
+ return { width: Number(match[1]), height: Number(match[2]) }
25
+ }
26
+
27
+ function parseStorage(value) {
28
+ const at = value.indexOf('=')
29
+ if (at <= 0) throw new Error(`invalid --storage ${value}; expected key=value`)
30
+ return [value.slice(0, at), value.slice(at + 1)]
31
+ }
32
+
33
+ export function parseArgs(argv) {
34
+ if (argv[0] !== COMMAND) {
35
+ throw new Error(`astryx-web only provides the ${COMMAND} command`)
36
+ }
37
+
38
+ const config = {
39
+ baseUrl: null,
40
+ routes: [],
41
+ themes: [],
42
+ modes: [],
43
+ sizes: [],
44
+ storage: {},
45
+ out: null,
46
+ }
47
+
48
+ for (let index = 1; index < argv.length; index += 1) {
49
+ const flag = argv[index]
50
+ if (flag === '--base-url') {
51
+ config.baseUrl = requiredValue(argv, index, flag)
52
+ index += 1
53
+ } else if (flag === '--route') {
54
+ config.routes.push(requiredValue(argv, index, flag))
55
+ index += 1
56
+ } else if (flag === '--theme') {
57
+ config.themes.push(requiredValue(argv, index, flag))
58
+ index += 1
59
+ } else if (flag === '--mode') {
60
+ config.modes.push(requiredValue(argv, index, flag))
61
+ index += 1
62
+ } else if (flag === '--size') {
63
+ config.sizes.push(parseSize(requiredValue(argv, index, flag)))
64
+ index += 1
65
+ } else if (flag === '--out') {
66
+ config.out = requiredValue(argv, index, flag)
67
+ index += 1
68
+ } else if (flag === '--storage') {
69
+ const first = requiredValue(argv, index, flag)
70
+ const [key, value] = parseStorage(first)
71
+ config.storage[key] = value
72
+ index += 1
73
+ while (argv[index + 1] && !argv[index + 1].startsWith('--')) {
74
+ const [nextKey, nextValue] = parseStorage(argv[index + 1])
75
+ config.storage[nextKey] = nextValue
76
+ index += 1
77
+ }
78
+ } else {
79
+ throw new Error(`unknown option ${flag}`)
80
+ }
81
+ }
82
+
83
+ for (const [name, value] of [
84
+ ['--base-url', config.baseUrl],
85
+ ['--out', config.out],
86
+ ]) {
87
+ if (!value) throw new Error(`${name} is required`)
88
+ }
89
+ for (const [name, values] of [
90
+ ['--route', config.routes],
91
+ ['--theme', config.themes],
92
+ ['--mode', config.modes],
93
+ ['--size', config.sizes],
94
+ ]) {
95
+ if (values.length === 0) throw new Error(`${name} is required at least once`)
96
+ }
97
+
98
+ new URL(config.baseUrl)
99
+ return config
100
+ }
101
+
102
+ export function routeSlug(route) {
103
+ const slug = route
104
+ .split(/[?#]/, 1)[0]
105
+ .replace(/^\/+|\/+$/g, '')
106
+ .replace(/[^a-zA-Z0-9]+/g, '-')
107
+ .replace(/^-+|-+$/g, '')
108
+ return slug || 'root'
109
+ }
110
+
111
+ export function sampleFileName(route, theme, mode, size) {
112
+ return `${routeSlug(route)}__${theme}__${mode}__${size.width}x${size.height}.png`
113
+ }
114
+
115
+ function gitCommit(cwd) {
116
+ const result = spawnSync('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf8' })
117
+ return result.status === 0 ? result.stdout.trim() : null
118
+ }
119
+
120
+ async function waitForJson(url, timeoutMs = 15_000) {
121
+ const started = Date.now()
122
+ while (Date.now() - started < timeoutMs) {
123
+ try {
124
+ const response = await fetch(url)
125
+ if (response.ok) return await response.json()
126
+ } catch {
127
+ // Chrome has not opened the debugging endpoint yet.
128
+ }
129
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 100))
130
+ }
131
+ throw new Error(`Chrome debugging endpoint did not start: ${url}`)
132
+ }
133
+
134
+ class Cdp {
135
+ constructor(url) {
136
+ this.nextId = 1
137
+ this.pending = new Map()
138
+ this.listeners = new Set()
139
+ this.socket = new WebSocket(url)
140
+ }
141
+
142
+ async open() {
143
+ await new Promise((resolveOpen, rejectOpen) => {
144
+ this.socket.addEventListener('open', resolveOpen, { once: true })
145
+ this.socket.addEventListener('error', rejectOpen, { once: true })
146
+ })
147
+ this.socket.addEventListener('message', (event) => {
148
+ const message = JSON.parse(String(event.data))
149
+ if (message.id) {
150
+ const pending = this.pending.get(message.id)
151
+ if (!pending) return
152
+ this.pending.delete(message.id)
153
+ if (message.error) pending.reject(new Error(message.error.message))
154
+ else pending.resolve(message.result)
155
+ return
156
+ }
157
+ for (const listener of this.listeners) listener(message)
158
+ })
159
+ }
160
+
161
+ send(method, params = {}, sessionId) {
162
+ const id = this.nextId
163
+ this.nextId += 1
164
+ const payload = { id, method, params }
165
+ if (sessionId) payload.sessionId = sessionId
166
+ this.socket.send(JSON.stringify(payload))
167
+ return new Promise((resolveSend, rejectSend) => {
168
+ this.pending.set(id, { resolve: resolveSend, reject: rejectSend })
169
+ })
170
+ }
171
+
172
+ onMessage(listener) {
173
+ this.listeners.add(listener)
174
+ return () => this.listeners.delete(listener)
175
+ }
176
+
177
+ close() {
178
+ this.socket.close()
179
+ }
180
+ }
181
+
182
+ function waitForNetworkIdle(cdp, sessionId, timeoutMs = 30_000) {
183
+ return new Promise((resolveIdle, rejectIdle) => {
184
+ let timeoutTimer = null
185
+
186
+ const finish = (error) => {
187
+ clearTimeout(timeoutTimer)
188
+ off()
189
+ if (error) rejectIdle(error)
190
+ else resolveIdle()
191
+ }
192
+ const off = cdp.onMessage((message) => {
193
+ if (message.sessionId !== sessionId) return
194
+ if (message.method === 'Page.lifecycleEvent' && message.params?.name === 'networkIdle') {
195
+ finish()
196
+ }
197
+ })
198
+ timeoutTimer = setTimeout(
199
+ () => finish(new Error('page did not reach network idle within 30 seconds')),
200
+ timeoutMs,
201
+ )
202
+ })
203
+ }
204
+
205
+ async function openSamplePage(cdp) {
206
+ const { targetId } = await cdp.send('Target.createTarget', { url: 'about:blank' })
207
+ const { sessionId } = await cdp.send('Target.attachToTarget', { targetId, flatten: true })
208
+ await cdp.send('Page.enable', {}, sessionId)
209
+ await cdp.send('Page.setLifecycleEventsEnabled', { enabled: true }, sessionId)
210
+ await cdp.send('Network.enable', {}, sessionId)
211
+ return { targetId, sessionId }
212
+ }
213
+
214
+ async function renderSample(cdp, page, config, storage, route, theme, mode, size, out) {
215
+ const { sessionId } = page
216
+ await cdp.send(
217
+ 'Emulation.setDeviceMetricsOverride',
218
+ {
219
+ width: size.width,
220
+ height: size.height,
221
+ deviceScaleFactor: 1,
222
+ mobile: size.width < 600,
223
+ },
224
+ sessionId,
225
+ )
226
+
227
+ for (const [name, value] of Object.entries(storage)) {
228
+ await cdp.send(
229
+ 'Network.setCookie',
230
+ {
231
+ name,
232
+ value,
233
+ url: config.baseUrl,
234
+ path: '/',
235
+ sameSite: 'Lax',
236
+ },
237
+ sessionId,
238
+ )
239
+ }
240
+
241
+ const idle = waitForNetworkIdle(cdp, sessionId)
242
+ const url = new URL(route, config.baseUrl).href
243
+ await cdp.send('Page.navigate', { url }, sessionId)
244
+ await idle
245
+
246
+ const screenshot = await cdp.send(
247
+ 'Page.captureScreenshot',
248
+ { format: 'png', fromSurface: true, captureBeyondViewport: false },
249
+ sessionId,
250
+ )
251
+ const file = sampleFileName(route, theme, mode, size)
252
+ writeFileSync(join(out, file), Buffer.from(screenshot.data, 'base64'))
253
+ return {
254
+ file,
255
+ route,
256
+ theme,
257
+ mode,
258
+ size: `${size.width}x${size.height}`,
259
+ width: size.width,
260
+ height: size.height,
261
+ }
262
+ }
263
+
264
+ export async function runSamples(config, env = process.env) {
265
+ const chromePath = env.CHROME_PATH
266
+ if (!chromePath) {
267
+ throw new Error('CHROME_PATH must name the system Chrome executable')
268
+ }
269
+
270
+ const out = resolve(config.out)
271
+ mkdirSync(out, { recursive: true })
272
+ const profile = mkdtempSync(join(tmpdir(), 'astryx-web-chrome-'))
273
+ const port = 20_000 + Math.floor(Math.random() * 20_000)
274
+ const chrome = spawn(
275
+ chromePath,
276
+ [
277
+ '--headless=new',
278
+ '--disable-gpu',
279
+ '--no-first-run',
280
+ '--no-default-browser-check',
281
+ `--remote-debugging-port=${port}`,
282
+ `--user-data-dir=${profile}`,
283
+ 'about:blank',
284
+ ],
285
+ { stdio: 'ignore' },
286
+ )
287
+
288
+ let cdp
289
+ let page
290
+ try {
291
+ const version = await waitForJson(`http://127.0.0.1:${port}/json/version`)
292
+ cdp = new Cdp(version.webSocketDebuggerUrl)
293
+ await cdp.open()
294
+ page = await openSamplePage(cdp)
295
+
296
+ const entries = []
297
+ for (const route of config.routes) {
298
+ for (const theme of config.themes) {
299
+ for (const mode of config.modes) {
300
+ for (const size of config.sizes) {
301
+ const storage = {
302
+ ...config.storage,
303
+ [APPEARANCE_STORAGE_KEY]: `${theme}.${mode}`,
304
+ }
305
+ entries.push(
306
+ await renderSample(cdp, page, config, storage, route, theme, mode, size, out),
307
+ )
308
+ }
309
+ }
310
+ }
311
+ }
312
+
313
+ const index = {
314
+ commit: gitCommit(process.cwd()),
315
+ routes: config.routes,
316
+ files: entries,
317
+ }
318
+ writeFileSync(join(out, 'index.json'), `${JSON.stringify(index, null, 2)}\n`)
319
+ return index
320
+ } finally {
321
+ if (page) {
322
+ await cdp.send('Target.closeTarget', { targetId: page.targetId }).catch(() => undefined)
323
+ }
324
+ cdp?.close()
325
+ if (chrome.exitCode === null) {
326
+ chrome.kill('SIGTERM')
327
+ await Promise.race([
328
+ once(chrome, 'exit'),
329
+ new Promise((resolveDelay) => setTimeout(resolveDelay, 5_000)),
330
+ ])
331
+ }
332
+ if (chrome.exitCode === null) chrome.kill('SIGKILL')
333
+ rmSync(profile, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
334
+ }
335
+ }
336
+
337
+ async function main() {
338
+ try {
339
+ const config = parseArgs(process.argv.slice(2))
340
+ const index = await runSamples(config)
341
+ console.log(`Rendered ${index.files.length} samples to ${resolve(config.out)}`)
342
+ } catch (error) {
343
+ console.error(error instanceof Error ? error.message : String(error))
344
+ process.exitCode = 1
345
+ }
346
+ }
347
+
348
+ if (
349
+ process.argv[1] &&
350
+ realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))
351
+ ) {
352
+ await main()
353
+ }
@@ -0,0 +1,24 @@
1
+ /** @type {import('@astryxdesign/cli/authoring').ReferenceDoc} */
2
+ export default {
3
+ type: 'generic',
4
+ name: 'dock-accessibility',
5
+ title: 'Dock Accessibility',
6
+ description: 'Heading, focus, status, and contrast requirements for every page.',
7
+ sections: [
8
+ {
9
+ title: 'Accessibility',
10
+ content: [
11
+ {
12
+ type: 'list',
13
+ style: 'unordered',
14
+ items: [
15
+ 'Exactly one `h1` per page (visually hidden when it would repeat navigation); headings in order.',
16
+ 'Visible focus on every control; every action reachable by keyboard.',
17
+ 'Status is never color-only.',
18
+ 'Contrast at least 4.5:1 for body text and 3:1 for large text and controls.',
19
+ ],
20
+ },
21
+ ],
22
+ },
23
+ ],
24
+ }
@@ -0,0 +1,26 @@
1
+ /** @type {import('@astryxdesign/cli/authoring').ReferenceDoc} */
2
+ export default {
3
+ type: 'generic',
4
+ name: 'dock-components',
5
+ title: 'Dock Components',
6
+ description: 'Component selection and layout rules for Astryx and AgentDock pages.',
7
+ sections: [
8
+ {
9
+ title: 'Components',
10
+ content: [
11
+ {
12
+ type: 'list',
13
+ style: 'unordered',
14
+ items: [
15
+ 'Use Astryx first: {@command component <Name>}, {@command search <words>}.',
16
+ 'Layout uses `Stack`, `Grid`, `Section`, `List`, and the app’s layout primitives, never raw `<div>`s.',
17
+ 'Cards are the exception: only for an independently enterable unit (a module entry, a record awaiting a decision), one level deep, never around a form, list, section, or page.',
18
+ 'Row actions: the recurring one stays visible; rare or destructive ones go into one end-aligned `MoreMenu` with icon and text. Never three buttons across a row.',
19
+ 'Pick the control from the value’s shape: on/off → `Switch`; 2–3 named choices → `SegmentedControl`; 4 or more → `Selector`; a number → `NumberInput`; free text → `TextInput`; an action → `Button`.',
20
+ 'No `style` attributes, no hard-coded colors, no viewport media queries.',
21
+ ],
22
+ },
23
+ ],
24
+ },
25
+ ],
26
+ }
@@ -0,0 +1,23 @@
1
+ /** @type {import('@astryxdesign/cli/authoring').ReferenceDoc} */
2
+ export default {
3
+ type: 'generic',
4
+ name: 'dock-copy',
5
+ title: 'Dock Copy',
6
+ description: 'Catalog, naming, and concision rules for user-facing text.',
7
+ sections: [
8
+ {
9
+ title: 'Copy',
10
+ content: [
11
+ {
12
+ type: 'list',
13
+ style: 'unordered',
14
+ items: [
15
+ 'Every user-facing string lives in the app catalogs, with the same keys in every locale.',
16
+ 'Delete paragraphs that only introduce a section. Keep the one line attached to a thing: a value’s status, a field’s format rule, an option’s description.',
17
+ 'Name things by what they do, not by the technology behind them.',
18
+ ],
19
+ },
20
+ ],
21
+ },
22
+ ],
23
+ }
@@ -0,0 +1,35 @@
1
+ /** @type {import('@astryxdesign/cli/authoring').ReferenceDoc} */
2
+ export default {
3
+ type: 'generic',
4
+ name: 'dock-design-base',
5
+ title: 'Dock Design Base',
6
+ description:
7
+ 'The product, layout, interaction, content, state, accessibility, and acceptance rules for AgentDock web pages.',
8
+ sections: [
9
+ {
10
+ title: 'Read the design base',
11
+ content: [
12
+ {
13
+ type: 'prose',
14
+ text: 'Read these sections before UI work. They apply to the same Web in the desktop App, the mobile App WebView, and a standalone browser.',
15
+ },
16
+ {
17
+ type: 'list',
18
+ style: 'unordered',
19
+ items: [
20
+ '{@link generic:dock-product-stance}',
21
+ '{@link generic:dock-environment}',
22
+ '{@link generic:dock-layout}',
23
+ '{@link generic:dock-touch-mobile}',
24
+ '{@link generic:dock-components}',
25
+ '{@link generic:dock-copy}',
26
+ '{@link generic:dock-states}',
27
+ '{@link generic:dock-accessibility}',
28
+ '{@link generic:dock-refuse-list}',
29
+ '{@link generic:dock-visual-acceptance}',
30
+ ],
31
+ },
32
+ ],
33
+ },
34
+ ],
35
+ }
@@ -0,0 +1,24 @@
1
+ /** @type {import('@astryxdesign/cli/authoring').ReferenceDoc} */
2
+ export default {
3
+ type: 'generic',
4
+ name: 'dock-environment',
5
+ title: 'Dock Environment',
6
+ description: 'The App, WebView, and browser surfaces that every page must support.',
7
+ sections: [
8
+ {
9
+ title: 'Environment',
10
+ content: [
11
+ {
12
+ type: 'list',
13
+ style: 'unordered',
14
+ items: [
15
+ 'The same Web runs in three places: the TheFoolAI desktop App, where the package Web is the main surface; the mobile App’s WebView, on a phone with touch; and a standalone browser.',
16
+ 'Phone (390×844) and desktop (1440×900) are both first-class design targets. The narrowest supported width is 360px.',
17
+ 'Widths come from container queries, not the viewport: below 420px compact, 420–639px standard, 640–959px enhanced, 960px and up wide.',
18
+ 'No horizontal scrolling at any width. A wide table becomes key–value rows or a named scrollable region.',
19
+ ],
20
+ },
21
+ ],
22
+ },
23
+ ],
24
+ }
@@ -0,0 +1,25 @@
1
+ /** @type {import('@astryxdesign/cli/authoring').ReferenceDoc} */
2
+ export default {
3
+ type: 'generic',
4
+ name: 'dock-layout',
5
+ title: 'Dock Layout',
6
+ description: 'Page structure, spacing, reading width, and responsive layout rules.',
7
+ sections: [
8
+ {
9
+ title: 'Layout',
10
+ content: [
11
+ {
12
+ type: 'list',
13
+ style: 'unordered',
14
+ items: [
15
+ 'Inline padding 12px below 420px, 20px from 420px. Section gap 16px below 420px, 24px from 420px.',
16
+ 'Reading content is capped at 44rem; grids may use 56rem from 960px.',
17
+ 'Second-level pages use the one-screen skeleton (`PageShell`): the top bar stays, only the body scrolls.',
18
+ 'Vertical first: put secondary detail behind a disclosure, a drawer, a sheet, or a sub-route instead of extending one long scroll.',
19
+ 'Never print a title that repeats the navigation; keep a visually hidden `h1` instead.',
20
+ ],
21
+ },
22
+ ],
23
+ },
24
+ ],
25
+ }
@@ -0,0 +1,25 @@
1
+ /** @type {import('@astryxdesign/cli/authoring').ReferenceDoc} */
2
+ export default {
3
+ type: 'generic',
4
+ name: 'dock-product-stance',
5
+ title: 'Dock Product Stance',
6
+ description: 'Product rules that keep every page focused on the user’s next action.',
7
+ sections: [
8
+ {
9
+ title: 'Product stance',
10
+ content: [
11
+ {
12
+ type: 'list',
13
+ style: 'unordered',
14
+ items: [
15
+ 'One page, one job. A first-level route is an index; the work happens one route deeper.',
16
+ 'Hierarchy over decoration. Headings, dividers, and spacing carry structure; borders, shadows, and card chrome are exceptions.',
17
+ 'The first screen answers "what do I do now" with a control, not a paragraph.',
18
+ 'Fewer, larger surfaces. If a page reads as a wall of boxes, remove boxes.',
19
+ 'Every label, counter, and tip must earn its line.',
20
+ ],
21
+ },
22
+ ],
23
+ },
24
+ ],
25
+ }
@@ -0,0 +1,34 @@
1
+ /** @type {import('@astryxdesign/cli/authoring').ReferenceDoc} */
2
+ export default {
3
+ type: 'generic',
4
+ name: 'dock-refuse-list',
5
+ title: 'Dock Refuse List',
6
+ description: 'Visual and interaction patterns that must never ship.',
7
+ sections: [
8
+ {
9
+ title: 'Refuse list',
10
+ content: [
11
+ {
12
+ type: 'list',
13
+ style: 'dont',
14
+ items: [
15
+ 'Gradient text',
16
+ 'Glass or blur used as decoration',
17
+ 'Neon glow on dark backgrounds',
18
+ 'Colored side stripes on cards',
19
+ 'Emoji used as icons',
20
+ 'Cards inside cards',
21
+ 'A page body that is a grid of identical cards',
22
+ 'Big-number "hero metrics" on a page that is not a dashboard',
23
+ 'Small caps "eyebrow" labels above every heading',
24
+ 'Purple-to-blue gradients',
25
+ 'Flows that open with a modal (use an inline step, a sheet, or a drawer)',
26
+ 'More than one accent color',
27
+ 'Reading lines longer than about 75 characters',
28
+ 'Centered body paragraphs',
29
+ ],
30
+ },
31
+ ],
32
+ },
33
+ ],
34
+ }
@@ -0,0 +1,23 @@
1
+ /** @type {import('@astryxdesign/cli/authoring').ReferenceDoc} */
2
+ export default {
3
+ type: 'generic',
4
+ name: 'dock-states',
5
+ title: 'Dock States',
6
+ description: 'Loading, empty, error, disabled, and success state requirements.',
7
+ sections: [
8
+ {
9
+ title: 'States',
10
+ content: [
11
+ {
12
+ type: 'list',
13
+ style: 'unordered',
14
+ items: [
15
+ 'Every asynchronous surface has loading, empty, error (readable, with a retry), disabled, and success states.',
16
+ 'An empty section is one line of text. `EmptyState` is only for a page whose whole purpose is empty.',
17
+ 'A failed save never silently drops the user’s edit.',
18
+ ],
19
+ },
20
+ ],
21
+ },
22
+ ],
23
+ }