@sableui/react 0.0.0-stage → 0.1.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/AGENTS.md +157 -0
- package/CHANGELOG.md +50 -0
- package/CONTRIBUTING.md +221 -0
- package/DESIGN.md +548 -0
- package/LICENSE +21 -0
- package/README.md +194 -2
- package/THIRD_PARTY_NOTICES.md +44 -0
- package/bin/sable-lint.mjs +0 -0
- package/bin/sable.mjs +1223 -0
- package/dist/_contract-CYScOdf5.d.ts +150 -0
- package/dist/chunk-3IWZYBGH.js +398 -0
- package/dist/chunk-5Z5KLOAU.js +167 -0
- package/dist/chunk-7MG4SHYX.js +15 -0
- package/dist/chunk-ADLLDWLW.js +15 -0
- package/dist/chunk-EP55KSYA.js +14 -0
- package/dist/chunk-J23ELQQZ.js +9396 -0
- package/dist/chunk-JI7WJP2L.js +286 -0
- package/dist/chunk-K7RUHUGC.js +14 -0
- package/dist/chunk-KO3NAEBF.js +15 -0
- package/dist/chunk-L4IEIQQA.js +14 -0
- package/dist/chunk-L6Y46C62.js +270 -0
- package/dist/chunk-NEQNXI2G.js +25 -0
- package/dist/chunk-PAOZBZME.js +40 -0
- package/dist/chunk-RWP4XBSK.js +139 -0
- package/dist/chunk-SRQWQUTW.js +63 -0
- package/dist/chunk-YXBOYWBN.js +15 -0
- package/dist/foundations/index.d.ts +88 -0
- package/dist/foundations/index.js +792 -0
- package/dist/icon/index.d.ts +60 -0
- package/dist/icon/index.js +2 -0
- package/dist/icon-sets/brands.d.ts +44 -0
- package/dist/icon-sets/brands.js +147 -0
- package/dist/icon-sets/reicon.d.ts +8 -0
- package/dist/icon-sets/reicon.js +1 -0
- package/dist/index.d.ts +7013 -0
- package/dist/index.js +15462 -0
- package/dist/ios/chrome.css +83 -0
- package/dist/ios/frame.css +5 -0
- package/dist/ios/index.d.ts +70 -0
- package/dist/ios/index.js +313 -0
- package/dist/ios/preview.d.ts +64 -0
- package/dist/ios/preview.js +133 -0
- package/dist/ios/styles.css +77 -0
- package/dist/layout/index.d.ts +280 -0
- package/dist/layout/index.js +2 -0
- package/dist/meta-6dUzgC6M.d.ts +205 -0
- package/dist/patterns/index.d.ts +36 -0
- package/dist/patterns/index.js +934 -0
- package/dist/presets/index.d.ts +37 -0
- package/dist/presets/index.js +117 -0
- package/dist/registry-CyL28OzJ.d.ts +28 -0
- package/dist/styles.css +1893 -0
- package/dist/theme-provider-BWcizhAt.d.ts +105 -0
- package/dist/themes/atelier/atelier.css +213 -0
- package/dist/themes/atelier/index.d.ts +9 -0
- package/dist/themes/atelier/index.js +1 -0
- package/dist/themes/base/base.css +443 -0
- package/dist/themes/base/index.d.ts +9 -0
- package/dist/themes/base/index.js +1 -0
- package/dist/themes/ember/ember.css +270 -0
- package/dist/themes/ember/index.d.ts +9 -0
- package/dist/themes/ember/index.js +1 -0
- package/dist/themes/index.d.ts +10 -0
- package/dist/themes/index.js +8 -0
- package/dist/themes/meridian/index.d.ts +9 -0
- package/dist/themes/meridian/index.js +1 -0
- package/dist/themes/meridian/meridian.css +255 -0
- package/dist/themes/poppy/index.d.ts +6 -0
- package/dist/themes/poppy/index.js +1 -0
- package/dist/themes/poppy/poppy.css +174 -0
- package/dist/themes/prism/index.d.ts +6 -0
- package/dist/themes/prism/index.js +1 -0
- package/dist/themes/prism/prism.css +183 -0
- package/dist/themes/verdant/index.d.ts +9 -0
- package/dist/themes/verdant/index.js +1 -0
- package/dist/themes/verdant/verdant.css +210 -0
- package/dist/themes.css +30 -0
- package/llms/components/accordion.md +93 -0
- package/llms/components/app-header.md +70 -0
- package/llms/components/app-shell.md +117 -0
- package/llms/components/attachment.md +105 -0
- package/llms/components/avatar.md +141 -0
- package/llms/components/badge.md +117 -0
- package/llms/components/banner.md +119 -0
- package/llms/components/bottom-nav.md +80 -0
- package/llms/components/breadcrumbs.md +75 -0
- package/llms/components/browser.md +67 -0
- package/llms/components/bubble.md +119 -0
- package/llms/components/button.md +199 -0
- package/llms/components/calendar.md +75 -0
- package/llms/components/card.md +135 -0
- package/llms/components/carousel.md +87 -0
- package/llms/components/chat-composer.md +126 -0
- package/llms/components/checkbox-card.md +98 -0
- package/llms/components/checkbox.md +90 -0
- package/llms/components/code-block.md +81 -0
- package/llms/components/code.md +62 -0
- package/llms/components/collapsible.md +83 -0
- package/llms/components/color-field.md +100 -0
- package/llms/components/combobox.md +119 -0
- package/llms/components/command-menu.md +158 -0
- package/llms/components/context-card.md +73 -0
- package/llms/components/context-menu.md +79 -0
- package/llms/components/data-table.md +99 -0
- package/llms/components/date-picker.md +97 -0
- package/llms/components/description.md +96 -0
- package/llms/components/destructive-action-modal.md +76 -0
- package/llms/components/dialog.md +127 -0
- package/llms/components/dots-menu.md +64 -0
- package/llms/components/empty-state.md +113 -0
- package/llms/components/entity.md +67 -0
- package/llms/components/feedback.md +63 -0
- package/llms/components/fieldset.md +106 -0
- package/llms/components/file-dropzone.md +103 -0
- package/llms/components/form.md +111 -0
- package/llms/components/gliding-highlight.md +84 -0
- package/llms/components/inline-status.md +62 -0
- package/llms/components/input.md +127 -0
- package/llms/components/kbd.md +57 -0
- package/llms/components/label.md +73 -0
- package/llms/components/marker.md +99 -0
- package/llms/components/marquee.md +66 -0
- package/llms/components/menu.md +122 -0
- package/llms/components/message-scroller.md +110 -0
- package/llms/components/message.md +114 -0
- package/llms/components/meter.md +124 -0
- package/llms/components/middle-truncate.md +62 -0
- package/llms/components/note.md +81 -0
- package/llms/components/notification-badge.md +67 -0
- package/llms/components/number-field.md +70 -0
- package/llms/components/otp-field.md +101 -0
- package/llms/components/page-header.md +76 -0
- package/llms/components/page-loader.md +67 -0
- package/llms/components/pagination.md +92 -0
- package/llms/components/panel-group.md +120 -0
- package/llms/components/panel-section.md +122 -0
- package/llms/components/phone.md +70 -0
- package/llms/components/pill.md +73 -0
- package/llms/components/popover.md +79 -0
- package/llms/components/progress.md +98 -0
- package/llms/components/radio-card.md +90 -0
- package/llms/components/radio.md +86 -0
- package/llms/components/relative-time.md +60 -0
- package/llms/components/scroller.md +88 -0
- package/llms/components/scrub-field.md +97 -0
- package/llms/components/search-input.md +84 -0
- package/llms/components/select.md +106 -0
- package/llms/components/separator.md +70 -0
- package/llms/components/settings-menu.md +139 -0
- package/llms/components/sheet.md +97 -0
- package/llms/components/show-more.md +59 -0
- package/llms/components/side-nav.md +106 -0
- package/llms/components/sidebar.md +87 -0
- package/llms/components/skeleton.md +62 -0
- package/llms/components/slider.md +114 -0
- package/llms/components/spinner.md +87 -0
- package/llms/components/status-bar.md +91 -0
- package/llms/components/status-dot.md +70 -0
- package/llms/components/stepper.md +125 -0
- package/llms/components/streaming-text.md +86 -0
- package/llms/components/switch.md +87 -0
- package/llms/components/table.md +112 -0
- package/llms/components/tabs.md +84 -0
- package/llms/components/textarea.md +88 -0
- package/llms/components/theme-selector.md +95 -0
- package/llms/components/theme-switcher.md +71 -0
- package/llms/components/thinking-orb.md +91 -0
- package/llms/components/toast.md +130 -0
- package/llms/components/toggle.md +95 -0
- package/llms/components/toolbar.md +168 -0
- package/llms/components/tooltip.md +93 -0
- package/llms/components/tree.md +227 -0
- package/llms/components/video.md +66 -0
- package/llms/components/workspace.md +138 -0
- package/llms/foundations/accessibility.md +60 -0
- package/llms/foundations/motion.md +96 -0
- package/llms/foundations/scrolling.md +41 -0
- package/llms/foundations/shape.md +73 -0
- package/llms/foundations/spacing.md +82 -0
- package/llms/foundations/surfaces.md +69 -0
- package/llms/foundations/typography.md +120 -0
- package/llms/layout/aspect-box.md +37 -0
- package/llms/layout/bleed.md +31 -0
- package/llms/layout/center.md +37 -0
- package/llms/layout/columns.md +51 -0
- package/llms/layout/grid-overlay.md +40 -0
- package/llms/layout/grid.md +47 -0
- package/llms/layout/page.md +39 -0
- package/llms/layout/region.md +49 -0
- package/llms/layout/row.md +40 -0
- package/llms/layout/safe-area.md +37 -0
- package/llms/layout/spacer.md +39 -0
- package/llms/layout/split-pane.md +41 -0
- package/llms/layout/stack.md +39 -0
- package/llms/patterns/auth-page.md +68 -0
- package/llms/patterns/brand-landing-page.md +423 -0
- package/llms/patterns/chat-panel.md +210 -0
- package/llms/patterns/dashboard-page.md +170 -0
- package/llms/patterns/feed-page.md +103 -0
- package/llms/patterns/landing-page.md +122 -0
- package/llms/patterns/list-detail-page.md +100 -0
- package/llms/patterns/settings-page.md +121 -0
- package/llms/reference/token-intent.md +324 -0
- package/llms/reference/utilities.md +129 -0
- package/llms/tasks/adding-a-component.md +113 -0
- package/llms/tasks/auditing-an-app.md +216 -0
- package/llms/tasks/building-a-page.md +109 -0
- package/llms/tasks/building-a-tool.md +141 -0
- package/llms/tasks/building-an-ios-prototype.md +105 -0
- package/llms/tasks/building-with-sable.md +73 -0
- package/llms/tasks/choosing-a-component.md +109 -0
- package/llms/tasks/converting-an-app.md +139 -0
- package/llms/tasks/creating-a-preset.md +43 -0
- package/llms/tasks/creating-design-assets.md +96 -0
- package/llms/tasks/designing-a-surface.md +115 -0
- package/llms/tasks/developing-taste.md +157 -0
- package/llms/tasks/getting-started.md +63 -0
- package/llms/tasks/learning-from-builds.md +74 -0
- package/llms/tasks/reviewing-a-design.md +114 -0
- package/llms/tasks/reviewing-a-journey.md +84 -0
- package/llms/tasks/reviewing-motion.md +92 -0
- package/llms/tasks/using-presets.md +87 -0
- package/llms/taste/asset-plan.template.json +53 -0
- package/llms/taste/composition-follows-content.md +20 -0
- package/llms/taste/hierarchy-is-a-relationship.md +22 -0
- package/llms/taste/lessons.json +57 -0
- package/llms/taste/motion-needs-a-reason.md +21 -0
- package/llms/taste/surface-states-travel-together.md +20 -0
- package/llms-full.txt +14906 -0
- package/llms.txt +405 -0
- package/package.json +185 -3
- package/registry.json +18812 -0
- package/scripts/agent-setup.mjs +54 -0
- package/scripts/design-atlas.mjs +221 -0
- package/scripts/design-directions.mjs +123 -0
- package/scripts/design-memory.mjs +166 -0
- package/scripts/design-preferences.mjs +227 -0
- package/scripts/design-presets.mjs +48 -0
- package/scripts/grade.mjs +509 -0
- package/scripts/lint-rules.mjs +1065 -0
- package/scripts/review-evidence.mjs +120 -0
- package/scripts/skeleton.mjs +248 -0
- package/skills/entry/SKILL.md +34 -0
- package/skills/sable/SKILL.md +187 -0
- package/skills/sable/agents/openai.yaml +6 -0
- package/targets/ios.json +982 -0
- package/third-party/reicon-LICENSE.txt +20 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** Review completeness is measurable; visual quality still requires a reviewer. */
|
|
2
|
+
import { createHash } from "node:crypto"
|
|
3
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs"
|
|
4
|
+
import { resolve } from "node:path"
|
|
5
|
+
|
|
6
|
+
export const DIMENSIONS = [
|
|
7
|
+
"purpose-story", "hierarchy-type", "layout-rhythm", "color-material",
|
|
8
|
+
"interaction", "motion", "responsive", "accessibility",
|
|
9
|
+
]
|
|
10
|
+
export const CHECKS = ["primary-flow", "keyboard", "overflow", "reduced-motion"]
|
|
11
|
+
|
|
12
|
+
export function reviewTemplate() {
|
|
13
|
+
return {
|
|
14
|
+
schemaVersion: 1,
|
|
15
|
+
build: { name: "", brief: "", revision: "", sourceHash: "" },
|
|
16
|
+
reviewer: { name: "", method: "agent" },
|
|
17
|
+
artifacts: [],
|
|
18
|
+
dimensions: DIMENSIONS.map((id) => ({ id, status: "untested", observation: "", evidence: [] })),
|
|
19
|
+
checks: CHECKS.map((id) => ({ id, status: "untested", observation: "", evidence: [] })),
|
|
20
|
+
iterations: [],
|
|
21
|
+
openIssues: [],
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const filled = (value) => typeof value === "string" && value.trim().length > 0
|
|
26
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
27
|
+
|
|
28
|
+
export function assessReview(review, { baseDir, sourceHash }) {
|
|
29
|
+
const limitations = "Validates recorded evidence and review completeness. It does not inspect images or certify visual quality."
|
|
30
|
+
if (review == null) return { status: "unreviewed", issues: ["No review supplied. Use sable grade --template."], limitations }
|
|
31
|
+
const issues = []
|
|
32
|
+
let revisions = false
|
|
33
|
+
const issue = (message) => issues.push(message)
|
|
34
|
+
if (!object(review)) return { status: "evidence-incomplete", issues: ["Review must be a JSON object."], limitations }
|
|
35
|
+
if (review.schemaVersion !== 1) issue("schemaVersion must be 1.")
|
|
36
|
+
if (!filled(review.build?.name) || !filled(review.build?.brief)) issue("build.name and build.brief are required.")
|
|
37
|
+
if (review.build?.sourceHash !== sourceHash) issue("build.sourceHash must match the current graded sourceHash; recapture and review after source changes.")
|
|
38
|
+
if (!filled(review.reviewer?.name) || !["human", "agent"].includes(review.reviewer?.method)) issue("reviewer needs a name and method (human or agent).")
|
|
39
|
+
|
|
40
|
+
const artifacts = Array.isArray(review.artifacts) ? review.artifacts : []
|
|
41
|
+
const artifactHashes = {}
|
|
42
|
+
if (!artifacts.length) issue("Provide artifacts from the rendered build.")
|
|
43
|
+
const byId = new Map()
|
|
44
|
+
for (const artifact of artifacts) {
|
|
45
|
+
if (!object(artifact) || !filled(artifact.id)) { issue("Every artifact needs an id."); continue }
|
|
46
|
+
if (byId.has(artifact.id)) { issue(`Duplicate artifact id: ${artifact.id}.`); continue }
|
|
47
|
+
let valid = true
|
|
48
|
+
let resolvedPath
|
|
49
|
+
const invalid = (message) => { issue(`Artifact ${artifact.id}: ${message}`); valid = false }
|
|
50
|
+
if (!["screenshot", "recording", "report"].includes(artifact.kind)) invalid("kind must be screenshot, recording, or report.")
|
|
51
|
+
if (!filled(artifact.state)) invalid("state is required.")
|
|
52
|
+
if (!filled(artifact.path)) invalid("path is required.")
|
|
53
|
+
else {
|
|
54
|
+
const path = resolve(baseDir, artifact.path)
|
|
55
|
+
try {
|
|
56
|
+
if (!existsSync(path) || !statSync(path).isFile() || statSync(path).size === 0) invalid(`missing or empty file: ${artifact.path}.`)
|
|
57
|
+
else {
|
|
58
|
+
resolvedPath = realpathSync(path)
|
|
59
|
+
if (process.platform === "win32") resolvedPath = resolvedPath.toLowerCase()
|
|
60
|
+
const sha256 = createHash("sha256").update(readFileSync(path)).digest("hex")
|
|
61
|
+
artifactHashes[artifact.id] = sha256
|
|
62
|
+
if (artifact.sha256 !== sha256) invalid("sha256 must match the inspected file. See artifactHashes for current bytes; inspect changed artifacts before accepting a new hash.")
|
|
63
|
+
}
|
|
64
|
+
} catch { invalid(`cannot read file: ${artifact.path}.`) }
|
|
65
|
+
}
|
|
66
|
+
if (artifact.kind === "screenshot" || artifact.kind === "recording") {
|
|
67
|
+
const { width, height } = artifact.viewport ?? {}
|
|
68
|
+
if (!Number.isInteger(width) || width <= 0 || !Number.isInteger(height) || height <= 0) invalid("viewport needs positive integer width and height.")
|
|
69
|
+
if (!["light", "dark"].includes(artifact.colorMode)) invalid("colorMode must be light or dark.")
|
|
70
|
+
if (!["full", "reduced"].includes(artifact.motion)) invalid("motion must be full or reduced.")
|
|
71
|
+
}
|
|
72
|
+
byId.set(artifact.id, { ...artifact, valid, resolvedPath })
|
|
73
|
+
}
|
|
74
|
+
const validArtifacts = [...byId.values()].filter((artifact) => artifact.valid)
|
|
75
|
+
const screenshots = validArtifacts.filter((artifact) => artifact.kind === "screenshot")
|
|
76
|
+
if (!screenshots.some((artifact) => artifact.viewport.width >= 1024)) issue("Include a wide screenshot (viewport width >= 1024).")
|
|
77
|
+
if (!screenshots.some((artifact) => artifact.viewport.width <= 480)) issue("Include a narrow screenshot (viewport width <= 480).")
|
|
78
|
+
if (!validArtifacts.some((artifact) => artifact.motion === "reduced")) issue("Include evidence captured with reduced motion.")
|
|
79
|
+
|
|
80
|
+
function evidence(ids, label) {
|
|
81
|
+
if (!Array.isArray(ids) || !ids.length) { issue(`${label}: reference at least one artifact.`); return }
|
|
82
|
+
for (const id of ids) if (!byId.get(id)?.valid) issue(`${label}: unknown or invalid artifact ${String(id)}.`)
|
|
83
|
+
}
|
|
84
|
+
function observations(entries, required, statuses, label) {
|
|
85
|
+
const list = Array.isArray(entries) ? entries : []
|
|
86
|
+
for (const id of required) {
|
|
87
|
+
const matches = list.filter((entry) => object(entry) && entry.id === id)
|
|
88
|
+
if (matches.length !== 1) { issue(`${label}: expected one ${id} observation.`); continue }
|
|
89
|
+
const entry = matches[0]
|
|
90
|
+
if (!statuses.includes(entry.status)) issue(`${label} ${id}: invalid status.`)
|
|
91
|
+
if (entry.status === "untested") issue(`${label} ${id}: untested.`)
|
|
92
|
+
if (entry.status === "revise" || entry.status === "fail") revisions = true
|
|
93
|
+
if (!filled(entry.observation)) issue(`${label} ${id}: describe what was observed.`)
|
|
94
|
+
evidence(entry.evidence, `${label} ${id}`)
|
|
95
|
+
}
|
|
96
|
+
for (const entry of list) if (!required.includes(entry?.id)) issue(`${label}: unknown id ${String(entry?.id)}.`)
|
|
97
|
+
}
|
|
98
|
+
observations(review.dimensions, DIMENSIONS, ["pass", "revise", "untested"], "Dimension")
|
|
99
|
+
observations(review.checks, CHECKS, ["pass", "fail", "untested"], "Check")
|
|
100
|
+
if (!Array.isArray(review.iterations)) issue("iterations must be an array; use [] if no revision was needed.")
|
|
101
|
+
else for (const [index, iteration] of review.iterations.entries()) {
|
|
102
|
+
if (!filled(iteration?.defect) || !filled(iteration?.action)) issue(`Iteration ${index + 1}: describe the defect and action.`)
|
|
103
|
+
evidence(iteration?.before, `Iteration ${index + 1} before`)
|
|
104
|
+
evidence(iteration?.after, `Iteration ${index + 1} after`)
|
|
105
|
+
if (Array.isArray(iteration?.before) && Array.isArray(iteration?.after) && iteration.after.some((id) => iteration.before.includes(id))) issue(`Iteration ${index + 1}: before and after must use distinct captures.`)
|
|
106
|
+
if (Array.isArray(iteration?.before) && Array.isArray(iteration?.after)) {
|
|
107
|
+
const beforePaths = new Set(iteration.before.map((id) => byId.get(id)?.resolvedPath).filter(Boolean))
|
|
108
|
+
if (iteration.after.some((id) => beforePaths.has(byId.get(id)?.resolvedPath))) issue(`Iteration ${index + 1}: before and after reference the same file; changing artifact ids does not create a new capture.`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (!Array.isArray(review.openIssues)) issue("openIssues must be an array; use [] only when no issues remain.")
|
|
112
|
+
else if (review.openIssues.length) revisions = true
|
|
113
|
+
return {
|
|
114
|
+
status: issues.length ? "evidence-incomplete" : revisions ? "revision-needed" : "review-recorded",
|
|
115
|
+
issues,
|
|
116
|
+
openIssues: review.openIssues ?? [],
|
|
117
|
+
artifactHashes,
|
|
118
|
+
limitations,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skeleton — reduce a pattern's source to its composition tree.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: the patterns tier answers "how does a page compose", but it
|
|
5
|
+
* answers it inside 130 lines of realistic demo source — fixture arrays, copy,
|
|
6
|
+
* per-cell formatting. An agent picking a starting point has to read all of it
|
|
7
|
+
* to see the four decisions that actually matter (which shell, which region cap,
|
|
8
|
+
* which grid, what nests where), and a 782KB registry means it often reads the
|
|
9
|
+
* pattern it already guessed instead of comparing all eight.
|
|
10
|
+
*
|
|
11
|
+
* A skeleton is the same tree with the content removed: elements, the props that
|
|
12
|
+
* carry layout meaning, and nesting. DashboardPage goes from 127 lines to ~25.
|
|
13
|
+
* Cheap enough to print all eight and choose, which is the point — composition
|
|
14
|
+
* is the decision that has to be right before any component is picked.
|
|
15
|
+
*
|
|
16
|
+
* This is lexical, not a parser. It leans on the fact that pattern source is
|
|
17
|
+
* ours and is formatted by Biome: one element per line, tabs for depth, so the
|
|
18
|
+
* indentation already IS the tree. That trade buys zero dependencies in a file
|
|
19
|
+
* that ships to consumers. It is exact for the patterns in this package (a test
|
|
20
|
+
* pins every one of them) and makes no claim about arbitrary JSX.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Props that survive. These are the ones that change the SHAPE of the page —
|
|
25
|
+
* caps, spans, rhythm, direction, emphasis tier. Everything else (`label`,
|
|
26
|
+
* `icon`, `href`, `placeholder`, event handlers) describes content, and content
|
|
27
|
+
* is the thing the reader is trying to see past.
|
|
28
|
+
*/
|
|
29
|
+
const STRUCTURAL = new Set([
|
|
30
|
+
"align",
|
|
31
|
+
"as",
|
|
32
|
+
"aside",
|
|
33
|
+
"bleed",
|
|
34
|
+
"className",
|
|
35
|
+
"cols",
|
|
36
|
+
"count",
|
|
37
|
+
"density",
|
|
38
|
+
"direction",
|
|
39
|
+
"fill",
|
|
40
|
+
"footer",
|
|
41
|
+
"gap",
|
|
42
|
+
"header",
|
|
43
|
+
"inset",
|
|
44
|
+
"justify",
|
|
45
|
+
"level",
|
|
46
|
+
"max",
|
|
47
|
+
"minItemWidth",
|
|
48
|
+
"orientation",
|
|
49
|
+
"ratio",
|
|
50
|
+
"register",
|
|
51
|
+
"safeArea",
|
|
52
|
+
"side",
|
|
53
|
+
"sidebar",
|
|
54
|
+
"size",
|
|
55
|
+
"span",
|
|
56
|
+
"start",
|
|
57
|
+
"sticky",
|
|
58
|
+
"tone",
|
|
59
|
+
"variant",
|
|
60
|
+
"wrap",
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
/** Props whose value is a nested subtree — rendered as a branch label, not a prop. */
|
|
64
|
+
const SLOT_HINT = /^([a-zA-Z][a-zA-Z0-9]*)=\{\(?$/
|
|
65
|
+
/** A whole attribute on one line: `max="form"`, `gap={3}`, `cols={{ base: 1 }}`. */
|
|
66
|
+
const SIMPLE_ATTR = /^([a-zA-Z][a-zA-Z0-9]*)=(?:"([^"]*)"|\{(.*)\})$/
|
|
67
|
+
const OPEN_TAG = /^<([A-Za-z][\w.]*)/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Pull the structural props out of a tag's attribute text.
|
|
71
|
+
*
|
|
72
|
+
* Hand-rolled rather than a regex because prop values nest braces —
|
|
73
|
+
* `cols={{ base: 1, sm: 2 }}` — and the obvious `\{([^}]*)\}` truncates at the
|
|
74
|
+
* first `}`, which silently turns a responsive breakpoint map (the single most
|
|
75
|
+
* important thing a Grid line has to say) into a malformed fragment.
|
|
76
|
+
*/
|
|
77
|
+
function structuralProps(text) {
|
|
78
|
+
const kept = []
|
|
79
|
+
let i = 0
|
|
80
|
+
while (i < text.length) {
|
|
81
|
+
const rest = text.slice(i)
|
|
82
|
+
const m = rest.match(/^\s*([a-zA-Z][a-zA-Z0-9]*)/)
|
|
83
|
+
if (!m) {
|
|
84
|
+
i += 1
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
const key = m[1]
|
|
88
|
+
i += m[0].length
|
|
89
|
+
if (text[i] !== "=") {
|
|
90
|
+
if (STRUCTURAL.has(key)) kept.push(key) // boolean shorthand
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
i += 1
|
|
94
|
+
let value
|
|
95
|
+
if (text[i] === '"') {
|
|
96
|
+
const end = text.indexOf('"', i + 1)
|
|
97
|
+
value = text.slice(i + 1, end === -1 ? text.length : end)
|
|
98
|
+
i = end === -1 ? text.length : end + 1
|
|
99
|
+
if (STRUCTURAL.has(key)) kept.push(`${key}="${value}"`)
|
|
100
|
+
} else if (text[i] === "{") {
|
|
101
|
+
let depth = 0
|
|
102
|
+
const from = i
|
|
103
|
+
for (; i < text.length; i++) {
|
|
104
|
+
if (text[i] === "{") depth++
|
|
105
|
+
else if (text[i] === "}" && --depth === 0) {
|
|
106
|
+
i++
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
value = text.slice(from + 1, i - 1)
|
|
111
|
+
// A JSX value (`header={<AppHeader … />}`) is a slot, not a prop: the
|
|
112
|
+
// element is composition, and printing it inline dumps its whole
|
|
113
|
+
// subtree onto the parent's line. Multi-line slots become branches in
|
|
114
|
+
// skeletonize(); a one-line JSX prop on a one-line tag is just dropped.
|
|
115
|
+
if (STRUCTURAL.has(key) && !value.trimStart().startsWith("<")) kept.push(`${key}={${value}}`)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return kept
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** `<Grid cols={…} gap={3}` → `{ name, props }`, ignoring content-carrying props. */
|
|
122
|
+
function readTag(trimmed) {
|
|
123
|
+
const name = trimmed.match(OPEN_TAG)[1]
|
|
124
|
+
// Cut at the `>` that ends the OPEN tag, tracking quotes and braces. Trimming
|
|
125
|
+
// a trailing `>` instead is wrong for the very common one-line element
|
|
126
|
+
// `<span className="x">text</span>`, where the leftover `</span` scans as a
|
|
127
|
+
// bare attribute and every such line prints a phantom `span` prop.
|
|
128
|
+
const rest = trimmed.slice(1 + name.length)
|
|
129
|
+
let end = rest.length
|
|
130
|
+
let depth = 0
|
|
131
|
+
let quote = null
|
|
132
|
+
for (let i = 0; i < rest.length; i++) {
|
|
133
|
+
const c = rest[i]
|
|
134
|
+
if (quote) {
|
|
135
|
+
if (c === quote) quote = null
|
|
136
|
+
} else if (c === '"' || c === "'") quote = c
|
|
137
|
+
else if (c === "{") depth++
|
|
138
|
+
else if (c === "}") depth--
|
|
139
|
+
else if (c === ">" && depth === 0) {
|
|
140
|
+
end = rest[i - 1] === "/" ? i - 1 : i
|
|
141
|
+
break
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { name, props: structuralProps(rest.slice(0, end)) }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {string} code A pattern's `code` string from registry.json.
|
|
149
|
+
* @returns {{ tree: string, elided: number }} `tree` is the skeleton; `elided`
|
|
150
|
+
* counts module-scope fixture bindings dropped ahead of the component.
|
|
151
|
+
*/
|
|
152
|
+
export function skeletonize(code) {
|
|
153
|
+
const lines = code.split("\n")
|
|
154
|
+
|
|
155
|
+
// Drop module scope. Patterns declare their demo data above the component;
|
|
156
|
+
// none of it is composition.
|
|
157
|
+
const start = lines.findIndex((l) => /^(export )?function [A-Z]/.test(l))
|
|
158
|
+
const head = start === -1 ? [] : lines.slice(0, start)
|
|
159
|
+
const elided = head.filter((l) => /^const [A-Za-z]/.test(l)).length
|
|
160
|
+
const body = start === -1 ? lines : lines.slice(start)
|
|
161
|
+
|
|
162
|
+
const out = []
|
|
163
|
+
let baseIndent = null
|
|
164
|
+
// A tag whose props Biome broke across lines is still one element. Track it
|
|
165
|
+
// so those lines land back on the tag instead of appearing to be its children
|
|
166
|
+
// — `density="compact"` indented under `<Entity>` reads as a child element,
|
|
167
|
+
// which is the one thing a composition tree must never get wrong.
|
|
168
|
+
let open = null
|
|
169
|
+
|
|
170
|
+
for (const line of body) {
|
|
171
|
+
const indent = line.match(/^\t*/)[0].length
|
|
172
|
+
const t = line.trim()
|
|
173
|
+
if (!t) continue
|
|
174
|
+
|
|
175
|
+
if (open) {
|
|
176
|
+
if (/^\/?>$/.test(t)) {
|
|
177
|
+
open = null
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
const attr = t.match(SIMPLE_ATTR)
|
|
181
|
+
if (attr && indent > open.indent && attr[3] !== undefined && attr[3].trimStart().startsWith("<")) {
|
|
182
|
+
// `header={<AppHeader title={…} />}` on its own line: the same branch
|
|
183
|
+
// shape as a multi-line slot — a label, then the element beneath it.
|
|
184
|
+
if (baseIndent === null) baseIndent = open.indent
|
|
185
|
+
const depth = Math.max(0, indent - baseIndent)
|
|
186
|
+
const inner = attr[3].trim()
|
|
187
|
+
if (OPEN_TAG.test(inner)) {
|
|
188
|
+
const { name, props } = readTag(inner)
|
|
189
|
+
out.push({ depth, text: `${attr[1]}:` }, { depth: depth + 1, text: null, name, props })
|
|
190
|
+
}
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
if (attr && indent > open.indent) {
|
|
194
|
+
if (STRUCTURAL.has(attr[1])) {
|
|
195
|
+
open.node.props.push(
|
|
196
|
+
attr[2] !== undefined ? `${attr[1]}="${attr[2]}"` : `${attr[1]}={${attr[3]}}`,
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
if (indent <= open.indent) open = null
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
let node = null
|
|
205
|
+
if (OPEN_TAG.test(t)) {
|
|
206
|
+
const { name, props } = readTag(t)
|
|
207
|
+
node = { text: null, name, props }
|
|
208
|
+
// No terminator on this line → its remaining props are still coming.
|
|
209
|
+
if (!/\/?>$/.test(t)) open = { indent, node }
|
|
210
|
+
} else if (/^\{[^}]*\.map\(/.test(t)) {
|
|
211
|
+
// `{items.map((item) => (` — one element repeated. The reader needs to
|
|
212
|
+
// know a repeat happens here; the iteration variable is noise.
|
|
213
|
+
node = { text: "{…repeat}" }
|
|
214
|
+
} else {
|
|
215
|
+
const slot = t.match(SLOT_HINT)
|
|
216
|
+
if (slot) node = { text: `${slot[1]}:` }
|
|
217
|
+
}
|
|
218
|
+
if (node === null) continue
|
|
219
|
+
|
|
220
|
+
if (baseIndent === null) baseIndent = indent
|
|
221
|
+
out.push({ depth: Math.max(0, indent - baseIndent), ...node })
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Everything prints self-closing: depth comes from indentation, so a closing
|
|
225
|
+
// tag would be a second line saying what the first line already said.
|
|
226
|
+
for (const n of out) {
|
|
227
|
+
if (n.text === null) n.text = `<${n.name}${n.props.length ? ` ${n.props.join(" ")}` : ""} />`
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Collapse runs of identical siblings. Five <SidebarItem /> lines say exactly
|
|
231
|
+
// as much as one line saying there are five, and the difference is the whole
|
|
232
|
+
// nav fitting on screen next to the rest of the page.
|
|
233
|
+
const collapsed = []
|
|
234
|
+
for (const node of out) {
|
|
235
|
+
const prev = collapsed[collapsed.length - 1]
|
|
236
|
+
if (prev && prev.depth === node.depth && prev.text === node.text) {
|
|
237
|
+
prev.count = (prev.count ?? 1) + 1
|
|
238
|
+
continue
|
|
239
|
+
}
|
|
240
|
+
collapsed.push({ ...node })
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const tree = collapsed
|
|
244
|
+
.map((n) => `${" ".repeat(n.depth)}${n.text}${n.count ? ` ×${n.count}` : ""}`)
|
|
245
|
+
.join("\n")
|
|
246
|
+
|
|
247
|
+
return { tree, elided }
|
|
248
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sable
|
|
3
|
+
description: Build websites and apps with Sable, develop reusable presets from inspiration, audit interfaces, and polish designs using the installed library.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Sable
|
|
7
|
+
|
|
8
|
+
Load the maintained instructions from this project's installed Sable package
|
|
9
|
+
before carrying out the request. This entry delegates so a package upgrade
|
|
10
|
+
supplies the updated skill, contracts, and guides together.
|
|
11
|
+
|
|
12
|
+
From the target project's dependency context, run:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
node -p "require.resolve('@sableui/react/registry.json')"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Read `skills/sable/SKILL.md` under the returned registry's parent directory and
|
|
19
|
+
follow it for the user's request. Use that package's `bin/sable.mjs` for retrieval.
|
|
20
|
+
In the Sable monorepo, read `packages/sable/skills/sable/SKILL.md` directly.
|
|
21
|
+
|
|
22
|
+
If the package is missing, install `@sableui/react` from npm with the project's
|
|
23
|
+
package manager.
|
|
24
|
+
Read the installed registry's `consume` block and `AGENTS.md` to wire React,
|
|
25
|
+
Tailwind, CSS source scanning, and fonts. Do not fetch an unrelated public package
|
|
26
|
+
named `sable` or `sable-ui`. Explain missing access and continue
|
|
27
|
+
independent brief work when installation is blocked.
|
|
28
|
+
|
|
29
|
+
Primary modes are `build <idea> [using <preset>]`, `inspire <references or brief>`,
|
|
30
|
+
`audit <existing interface>`, and `polish <target>`. `create` aliases `build`.
|
|
31
|
+
These are agent modes, not terminal subcommands. Every build must query and use
|
|
32
|
+
the installed Sable components, variants, utilities, tokens, layouts, and guides,
|
|
33
|
+
then run its design lint and inspect the working result. Do not begin UI code
|
|
34
|
+
without loading the package skill.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sable
|
|
3
|
+
description: Build original websites and apps with Sable, turn inspiration into reusable presets, audit existing interfaces, and polish a selected design. Use for Sable design and UI work, not unrelated backend tasks.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Sable
|
|
7
|
+
|
|
8
|
+
Build the user's idea with the installed Sable library. The product brief determines
|
|
9
|
+
content, structure, and behavior; a preset supplies theme values and design guidance.
|
|
10
|
+
References inform an original direction. A reference or preset is optional.
|
|
11
|
+
|
|
12
|
+
## Route the request
|
|
13
|
+
|
|
14
|
+
These are agent skill modes, not shell commands. Use `$sable` in Codex and
|
|
15
|
+
`/sable` in Claude Code or Cursor; natural requests work in compatible agents.
|
|
16
|
+
|
|
17
|
+
| Mode | Outcome |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| `build <idea> [using <preset or path>]` | Develop the brief, implement the requested product with Sable, inspect and operate it, repair observed issues, and deliver the working result. |
|
|
20
|
+
| `inspire <references or description>` | Study the inspiration, develop a direction, and save a previewable, editable preset. Stop before a full product build. |
|
|
21
|
+
| `audit <existing interface>` | Inspect existing work and report prioritized design, behavior, accessibility, and Sable-contract findings. Apply repairs only when requested. |
|
|
22
|
+
| `polish <target or instruction>` | Improve the current product or preset while preserving its selected direction, then verify affected states. |
|
|
23
|
+
|
|
24
|
+
`create` is an alias for `build`. A bare new-product brief means `build`; an editing
|
|
25
|
+
follow-up means `polish`. A request for direction only stops before implementation.
|
|
26
|
+
“Show alternatives” produces bounded rendered studies and stops at comparison when
|
|
27
|
+
the user reserves the choice. Explore only when requested or when competing choices
|
|
28
|
+
materially affect the brief; do not require three designs for every build.
|
|
29
|
+
|
|
30
|
+
An explicit iOS/iPhone **app prototype** request selects the optional iOS target.
|
|
31
|
+
Read `sable docs building-an-ios-prototype` before composing. The rule is **Sable
|
|
32
|
+
first**: every screen is Sable components in the chosen preset — settings rows are
|
|
33
|
+
Card + CardList + Entity with Switch, forms are Form + Field + Input, search is
|
|
34
|
+
SearchInput, feedback is toast. `@sableui/react/ios` supplies only system chrome
|
|
35
|
+
(NativeApp, IosPage, IosNavbar, IosBackLink, IosIconButton, IosTabbar, IosTab,
|
|
36
|
+
IosSheet, NavigationStack). Never import from `konsta`; sable-lint flags it
|
|
37
|
+
(`native-ui-import`). `sable target ios --component <Name>` confirms a decision;
|
|
38
|
+
almost every component is `preserve`. Every rule below applies to the prototype.
|
|
39
|
+
|
|
40
|
+
For inspiration, extract useful visual relationships into a preset. For an audit,
|
|
41
|
+
evaluate the target product. A URL does not automatically mean copying its page.
|
|
42
|
+
An explicit reconstruction request fixes reference fidelity; preserve that scope
|
|
43
|
+
and document necessary departures. Current user instructions override older presets
|
|
44
|
+
and preferences. Infer the target from the project and conversation; ask only for
|
|
45
|
+
missing information that materially changes the work.
|
|
46
|
+
|
|
47
|
+
## Resolve and set up the installed library
|
|
48
|
+
|
|
49
|
+
Start in the target app, not this skill's folder. Read project instructions. Resolve
|
|
50
|
+
`@sableui/react/registry.json` from that project's dependency context:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
node -p "require.resolve('@sableui/react/registry.json')"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The registry's parent is the package root; read its `AGENTS.md` and use its
|
|
57
|
+
`bin/sable.mjs`. In the Sable repository use `packages/sable/bin/sable.mjs` and
|
|
58
|
+
`packages/sable/registry.json`. Below, `sable` means this resolved CLI invoked with
|
|
59
|
+
the target project as its working directory. Never fetch an unrelated public
|
|
60
|
+
package with bare `npx sable`, or invent shell `build`, `inspire`, or `audit` commands.
|
|
61
|
+
|
|
62
|
+
If Sable is absent, read the trusted repository or package README's install recipe
|
|
63
|
+
and registry `consume` block. Install from npm with the project's package
|
|
64
|
+
manager, for example `pnpm add @sableui/react`; no registry setup or token is
|
|
65
|
+
needed. Do not substitute an unrelated package with a similar name.
|
|
66
|
+
|
|
67
|
+
Complete setup before implementing UI:
|
|
68
|
+
|
|
69
|
+
- Match the installed package's React, React DOM, Tailwind, and Node requirements.
|
|
70
|
+
- Dedupe React in the bundler. Import Tailwind, then Sable's stylesheet; add the
|
|
71
|
+
package `dist` glob to Tailwind `@source`, relative to the consumer stylesheet.
|
|
72
|
+
- Load the selected fonts through the project's font setup. Presets declare stacks;
|
|
73
|
+
they do not load files. Reicon works by default; IconProvider can replace or extend the set.
|
|
74
|
+
- Run `sable doctor`, repair wiring issues, and verify the app renders a real
|
|
75
|
+
Sable component. Reuse an existing app server; never start duplicate instances.
|
|
76
|
+
|
|
77
|
+
Run `sable setup --agent` once from the project root to install discoverable entry
|
|
78
|
+
files for Codex/Cursor and Claude Code. `--target codex|claude|cursor|all` selects
|
|
79
|
+
hosts. The entry files read this installed package's instructions on each task;
|
|
80
|
+
upgrading the package updates the workflow. After an upgrade, use
|
|
81
|
+
`sable setup --agent --update` to refresh managed entry metadata when needed.
|
|
82
|
+
Keep project-specific preferences in project instructions rather than editing
|
|
83
|
+
package files. Agents without skill discovery can read this file directly.
|
|
84
|
+
|
|
85
|
+
## Build with the actual Sable resources
|
|
86
|
+
|
|
87
|
+
Before UI code, run `sable manifest --json`, read `sable docs building-with-sable`,
|
|
88
|
+
`sable docs designing-a-surface`, and `sable docs choosing-a-component`. Read relevant
|
|
89
|
+
foundations through `sable docs typography|spacing|shape|surfaces|motion|scrolling|accessibility`.
|
|
90
|
+
The installed catalog and guides are authoritative for API and token names.
|
|
91
|
+
|
|
92
|
+
**Required for every build and UI repair:**
|
|
93
|
+
|
|
94
|
+
1. Search the installed catalog for every recognizable element or behavior:
|
|
95
|
+
`sable search "<need>" --json`. Inspect the matching
|
|
96
|
+
`sable component <Name> --props` and `--examples` before using it. Use its public
|
|
97
|
+
component and composed anatomy. A native element wearing a component's classes
|
|
98
|
+
is a reinvention, even if it uses valid tokens.
|
|
99
|
+
2. Resolve styling in this order: Sable component/variant, Sable utility or recipe,
|
|
100
|
+
semantic token, then app-owned CSS for a genuinely uncovered need. Retrieve
|
|
101
|
+
`sable utility <name>` and `sable token <group>` rather than guessing names.
|
|
102
|
+
Use paired typography, spacing steps, density/control ladders, relative surfaces,
|
|
103
|
+
concentric geometry, scroll policies, and supported motion recipes.
|
|
104
|
+
3. Inspect a relevant `sable pattern <Name> --skeleton` and layout contracts when
|
|
105
|
+
composing a page. Reuse a pattern when its information architecture fits;
|
|
106
|
+
otherwise compose an original layout from Sable primitives. Do not force a
|
|
107
|
+
dashboard or marketing shell onto a different product brief.
|
|
108
|
+
4. Put identity in app-owned theme tokens, composition, and assets. Component
|
|
109
|
+
`className` is for layout; fill, edges, corners, ink, and interaction appearance
|
|
110
|
+
come from supported variants. Do not override controls to imitate a reference.
|
|
111
|
+
Entity rows use `CardList`; edge-to-edge tables use `CardContent flush`.
|
|
112
|
+
5. When the catalog has no fit, document the search, missing capability, and owner.
|
|
113
|
+
In the Sable monorepo, follow `CONTRIBUTING.md`, add the library capability, then
|
|
114
|
+
consume it. In another app, implement only genuinely app-specific structure;
|
|
115
|
+
report a missing library hook. Do not modify installed package files or build
|
|
116
|
+
parallel controls as a shortcut. Preserve existing non-Sable work unless a
|
|
117
|
+
migration was requested.
|
|
118
|
+
|
|
119
|
+
## Develop or use a preset
|
|
120
|
+
|
|
121
|
+
Save a short brief in the project's existing design location: audience, primary
|
|
122
|
+
task, content, constraints, selected identity, and meaningful states. Label invented
|
|
123
|
+
sample content. A supplied preset is an explicit input: read its exact record and
|
|
124
|
+
revision, validate it through `sable preset --check <path> --json`, and read
|
|
125
|
+
`sable docs using-presets`. Preserve its tokens and guidance unless the user requests
|
|
126
|
+
a change or the new task requires a documented adaptation.
|
|
127
|
+
|
|
128
|
+
Without a preset, develop an appropriate direction from the brief. Use the bundled
|
|
129
|
+
presets as starting points or create original app-owned tokens. When the project has
|
|
130
|
+
its own visual atlas, retrieve references with `sable atlas --search "<problem>"
|
|
131
|
+
--contrast --limit 3 --json` and `--show <id>`, and actually view returned images;
|
|
132
|
+
the package ships none, so otherwise work from the user's references. Retrieve applicable lessons with
|
|
133
|
+
`sable learn --search "<problem>" --json`. Optional preference retrieval uses an
|
|
134
|
+
explicit scope and distinguishes human choices from agent observations. Empty
|
|
135
|
+
memory is valid; opening tools or completing a build does not supply human taste.
|
|
136
|
+
|
|
137
|
+
For `inspire`, read `sable docs creating-a-preset`. Inspect supplied references with
|
|
138
|
+
available browser/image/design tools. Distinguish measured, estimated, derived,
|
|
139
|
+
and user-edited decisions; do not claim a screenshot proves animation timing or
|
|
140
|
+
hidden behavior. Record transferable typography, surfaces, spacing, imagery, and
|
|
141
|
+
motion principles. The user's future product determines its own layout and content.
|
|
142
|
+
Save a versioned preset in `.sable/presets/<id>.json`, validate it, and provide a
|
|
143
|
+
Sable-component preview or import it into Canvas. Stop at the preset.
|
|
144
|
+
|
|
145
|
+
Canvas can import and download the same preset format. A downloaded file must be
|
|
146
|
+
placed in the target project before the agent can read it; a browser export does
|
|
147
|
+
not itself write into that project. Use `sable preset --css <path>` to generate
|
|
148
|
+
scoped CSS, load fonts, and apply the preset ID with Sable's theme APIs. Never
|
|
149
|
+
register a personal preset as a bundled library theme just to build one app.
|
|
150
|
+
|
|
151
|
+
When alternatives matter, keep content and primary behavior fixed, vary meaningful
|
|
152
|
+
composition and another relevant dimension, and inspect desktop/mobile/interaction
|
|
153
|
+
states. `sable explore` supports three to six evidence-bound directions; a smaller
|
|
154
|
+
comparison can be delivered directly. Respect a reserved user choice; otherwise
|
|
155
|
+
make and identify an agent selection and continue the authorized build.
|
|
156
|
+
|
|
157
|
+
When imagery or fonts carry the direction, read `sable docs creating-design-assets`.
|
|
158
|
+
Produce or obtain actual assets within authorization, verify loading and responsive
|
|
159
|
+
crops, and retain source/readiness notes. Atlas images are reference evidence, not
|
|
160
|
+
product assets. A palette alone does not establish a finished design.
|
|
161
|
+
|
|
162
|
+
## Verify and deliver
|
|
163
|
+
|
|
164
|
+
For `audit`, read `sable docs auditing-an-app`; report by default and distinguish
|
|
165
|
+
contract violations, design judgments, intentional choices, and verified library
|
|
166
|
+
gaps. A visual-only review leaves code and unobserved behavior untested. For
|
|
167
|
+
`polish`, reuse the existing direction and inspect the affected product or preset.
|
|
168
|
+
|
|
169
|
+
Every build includes `sable docs reviewing-a-design`, `sable doctor`,
|
|
170
|
+
`sable lint <source paths> --json`, and the project's build/type checks. Repair
|
|
171
|
+
findings instead of suppressing them to finish. Inspect real desktop/mobile
|
|
172
|
+
layouts, operate the primary task and recovery path, check keyboard/touch and
|
|
173
|
+
loading/empty/error/success states, font loading, focus, and reduced motion.
|
|
174
|
+
Audit source ownership as well as lint: identify the Sable components and composed
|
|
175
|
+
parts actually imported, the utilities/token groups used, and every app-owned UI
|
|
176
|
+
or CSS addition. Ordinary semantic content and domain logic are app-owned; a
|
|
177
|
+
recognizable control or surface must use its library implementation. Explain any
|
|
178
|
+
uncovered capability with the catalog search and the smallest necessary addition.
|
|
179
|
+
Include this reuse inventory with the build's evidence so conformance is reviewable.
|
|
180
|
+
For connected flows read `reviewing-a-journey`; for meaningful temporal behavior
|
|
181
|
+
read `reviewing-motion` and observe interruption as well as playback.
|
|
182
|
+
|
|
183
|
+
Iterate on observed defects and recapture changed states. Contract checks cannot
|
|
184
|
+
judge beauty: assess brief fit, composition, hierarchy, rhythm, imagery, and
|
|
185
|
+
interaction in the rendered product. Deliver the result, preset/revision used,
|
|
186
|
+
checks performed, and material limitations. Preserve useful conditional lessons
|
|
187
|
+
through `learning-from-builds`; never fabricate human approval or preference records.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "Sable"
|
|
3
|
+
short_description: "Build, develop presets, audit, and polish with Sable"
|
|
4
|
+
default_prompt: "Use $sable build to create my idea with the installed Sable components, tokens, and design guides, then inspect and polish the working result."
|
|
5
|
+
policy:
|
|
6
|
+
allow_implicit_invocation: true
|