@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.
Files changed (247) hide show
  1. package/AGENTS.md +157 -0
  2. package/CHANGELOG.md +50 -0
  3. package/CONTRIBUTING.md +221 -0
  4. package/DESIGN.md +548 -0
  5. package/LICENSE +21 -0
  6. package/README.md +194 -2
  7. package/THIRD_PARTY_NOTICES.md +44 -0
  8. package/bin/sable-lint.mjs +0 -0
  9. package/bin/sable.mjs +1223 -0
  10. package/dist/_contract-CYScOdf5.d.ts +150 -0
  11. package/dist/chunk-3IWZYBGH.js +398 -0
  12. package/dist/chunk-5Z5KLOAU.js +167 -0
  13. package/dist/chunk-7MG4SHYX.js +15 -0
  14. package/dist/chunk-ADLLDWLW.js +15 -0
  15. package/dist/chunk-EP55KSYA.js +14 -0
  16. package/dist/chunk-J23ELQQZ.js +9396 -0
  17. package/dist/chunk-JI7WJP2L.js +286 -0
  18. package/dist/chunk-K7RUHUGC.js +14 -0
  19. package/dist/chunk-KO3NAEBF.js +15 -0
  20. package/dist/chunk-L4IEIQQA.js +14 -0
  21. package/dist/chunk-L6Y46C62.js +270 -0
  22. package/dist/chunk-NEQNXI2G.js +25 -0
  23. package/dist/chunk-PAOZBZME.js +40 -0
  24. package/dist/chunk-RWP4XBSK.js +139 -0
  25. package/dist/chunk-SRQWQUTW.js +63 -0
  26. package/dist/chunk-YXBOYWBN.js +15 -0
  27. package/dist/foundations/index.d.ts +88 -0
  28. package/dist/foundations/index.js +792 -0
  29. package/dist/icon/index.d.ts +60 -0
  30. package/dist/icon/index.js +2 -0
  31. package/dist/icon-sets/brands.d.ts +44 -0
  32. package/dist/icon-sets/brands.js +147 -0
  33. package/dist/icon-sets/reicon.d.ts +8 -0
  34. package/dist/icon-sets/reicon.js +1 -0
  35. package/dist/index.d.ts +7013 -0
  36. package/dist/index.js +15462 -0
  37. package/dist/ios/chrome.css +83 -0
  38. package/dist/ios/frame.css +5 -0
  39. package/dist/ios/index.d.ts +70 -0
  40. package/dist/ios/index.js +313 -0
  41. package/dist/ios/preview.d.ts +64 -0
  42. package/dist/ios/preview.js +133 -0
  43. package/dist/ios/styles.css +77 -0
  44. package/dist/layout/index.d.ts +280 -0
  45. package/dist/layout/index.js +2 -0
  46. package/dist/meta-6dUzgC6M.d.ts +205 -0
  47. package/dist/patterns/index.d.ts +36 -0
  48. package/dist/patterns/index.js +934 -0
  49. package/dist/presets/index.d.ts +37 -0
  50. package/dist/presets/index.js +117 -0
  51. package/dist/registry-CyL28OzJ.d.ts +28 -0
  52. package/dist/styles.css +1893 -0
  53. package/dist/theme-provider-BWcizhAt.d.ts +105 -0
  54. package/dist/themes/atelier/atelier.css +213 -0
  55. package/dist/themes/atelier/index.d.ts +9 -0
  56. package/dist/themes/atelier/index.js +1 -0
  57. package/dist/themes/base/base.css +443 -0
  58. package/dist/themes/base/index.d.ts +9 -0
  59. package/dist/themes/base/index.js +1 -0
  60. package/dist/themes/ember/ember.css +270 -0
  61. package/dist/themes/ember/index.d.ts +9 -0
  62. package/dist/themes/ember/index.js +1 -0
  63. package/dist/themes/index.d.ts +10 -0
  64. package/dist/themes/index.js +8 -0
  65. package/dist/themes/meridian/index.d.ts +9 -0
  66. package/dist/themes/meridian/index.js +1 -0
  67. package/dist/themes/meridian/meridian.css +255 -0
  68. package/dist/themes/poppy/index.d.ts +6 -0
  69. package/dist/themes/poppy/index.js +1 -0
  70. package/dist/themes/poppy/poppy.css +174 -0
  71. package/dist/themes/prism/index.d.ts +6 -0
  72. package/dist/themes/prism/index.js +1 -0
  73. package/dist/themes/prism/prism.css +183 -0
  74. package/dist/themes/verdant/index.d.ts +9 -0
  75. package/dist/themes/verdant/index.js +1 -0
  76. package/dist/themes/verdant/verdant.css +210 -0
  77. package/dist/themes.css +30 -0
  78. package/llms/components/accordion.md +93 -0
  79. package/llms/components/app-header.md +70 -0
  80. package/llms/components/app-shell.md +117 -0
  81. package/llms/components/attachment.md +105 -0
  82. package/llms/components/avatar.md +141 -0
  83. package/llms/components/badge.md +117 -0
  84. package/llms/components/banner.md +119 -0
  85. package/llms/components/bottom-nav.md +80 -0
  86. package/llms/components/breadcrumbs.md +75 -0
  87. package/llms/components/browser.md +67 -0
  88. package/llms/components/bubble.md +119 -0
  89. package/llms/components/button.md +199 -0
  90. package/llms/components/calendar.md +75 -0
  91. package/llms/components/card.md +135 -0
  92. package/llms/components/carousel.md +87 -0
  93. package/llms/components/chat-composer.md +126 -0
  94. package/llms/components/checkbox-card.md +98 -0
  95. package/llms/components/checkbox.md +90 -0
  96. package/llms/components/code-block.md +81 -0
  97. package/llms/components/code.md +62 -0
  98. package/llms/components/collapsible.md +83 -0
  99. package/llms/components/color-field.md +100 -0
  100. package/llms/components/combobox.md +119 -0
  101. package/llms/components/command-menu.md +158 -0
  102. package/llms/components/context-card.md +73 -0
  103. package/llms/components/context-menu.md +79 -0
  104. package/llms/components/data-table.md +99 -0
  105. package/llms/components/date-picker.md +97 -0
  106. package/llms/components/description.md +96 -0
  107. package/llms/components/destructive-action-modal.md +76 -0
  108. package/llms/components/dialog.md +127 -0
  109. package/llms/components/dots-menu.md +64 -0
  110. package/llms/components/empty-state.md +113 -0
  111. package/llms/components/entity.md +67 -0
  112. package/llms/components/feedback.md +63 -0
  113. package/llms/components/fieldset.md +106 -0
  114. package/llms/components/file-dropzone.md +103 -0
  115. package/llms/components/form.md +111 -0
  116. package/llms/components/gliding-highlight.md +84 -0
  117. package/llms/components/inline-status.md +62 -0
  118. package/llms/components/input.md +127 -0
  119. package/llms/components/kbd.md +57 -0
  120. package/llms/components/label.md +73 -0
  121. package/llms/components/marker.md +99 -0
  122. package/llms/components/marquee.md +66 -0
  123. package/llms/components/menu.md +122 -0
  124. package/llms/components/message-scroller.md +110 -0
  125. package/llms/components/message.md +114 -0
  126. package/llms/components/meter.md +124 -0
  127. package/llms/components/middle-truncate.md +62 -0
  128. package/llms/components/note.md +81 -0
  129. package/llms/components/notification-badge.md +67 -0
  130. package/llms/components/number-field.md +70 -0
  131. package/llms/components/otp-field.md +101 -0
  132. package/llms/components/page-header.md +76 -0
  133. package/llms/components/page-loader.md +67 -0
  134. package/llms/components/pagination.md +92 -0
  135. package/llms/components/panel-group.md +120 -0
  136. package/llms/components/panel-section.md +122 -0
  137. package/llms/components/phone.md +70 -0
  138. package/llms/components/pill.md +73 -0
  139. package/llms/components/popover.md +79 -0
  140. package/llms/components/progress.md +98 -0
  141. package/llms/components/radio-card.md +90 -0
  142. package/llms/components/radio.md +86 -0
  143. package/llms/components/relative-time.md +60 -0
  144. package/llms/components/scroller.md +88 -0
  145. package/llms/components/scrub-field.md +97 -0
  146. package/llms/components/search-input.md +84 -0
  147. package/llms/components/select.md +106 -0
  148. package/llms/components/separator.md +70 -0
  149. package/llms/components/settings-menu.md +139 -0
  150. package/llms/components/sheet.md +97 -0
  151. package/llms/components/show-more.md +59 -0
  152. package/llms/components/side-nav.md +106 -0
  153. package/llms/components/sidebar.md +87 -0
  154. package/llms/components/skeleton.md +62 -0
  155. package/llms/components/slider.md +114 -0
  156. package/llms/components/spinner.md +87 -0
  157. package/llms/components/status-bar.md +91 -0
  158. package/llms/components/status-dot.md +70 -0
  159. package/llms/components/stepper.md +125 -0
  160. package/llms/components/streaming-text.md +86 -0
  161. package/llms/components/switch.md +87 -0
  162. package/llms/components/table.md +112 -0
  163. package/llms/components/tabs.md +84 -0
  164. package/llms/components/textarea.md +88 -0
  165. package/llms/components/theme-selector.md +95 -0
  166. package/llms/components/theme-switcher.md +71 -0
  167. package/llms/components/thinking-orb.md +91 -0
  168. package/llms/components/toast.md +130 -0
  169. package/llms/components/toggle.md +95 -0
  170. package/llms/components/toolbar.md +168 -0
  171. package/llms/components/tooltip.md +93 -0
  172. package/llms/components/tree.md +227 -0
  173. package/llms/components/video.md +66 -0
  174. package/llms/components/workspace.md +138 -0
  175. package/llms/foundations/accessibility.md +60 -0
  176. package/llms/foundations/motion.md +96 -0
  177. package/llms/foundations/scrolling.md +41 -0
  178. package/llms/foundations/shape.md +73 -0
  179. package/llms/foundations/spacing.md +82 -0
  180. package/llms/foundations/surfaces.md +69 -0
  181. package/llms/foundations/typography.md +120 -0
  182. package/llms/layout/aspect-box.md +37 -0
  183. package/llms/layout/bleed.md +31 -0
  184. package/llms/layout/center.md +37 -0
  185. package/llms/layout/columns.md +51 -0
  186. package/llms/layout/grid-overlay.md +40 -0
  187. package/llms/layout/grid.md +47 -0
  188. package/llms/layout/page.md +39 -0
  189. package/llms/layout/region.md +49 -0
  190. package/llms/layout/row.md +40 -0
  191. package/llms/layout/safe-area.md +37 -0
  192. package/llms/layout/spacer.md +39 -0
  193. package/llms/layout/split-pane.md +41 -0
  194. package/llms/layout/stack.md +39 -0
  195. package/llms/patterns/auth-page.md +68 -0
  196. package/llms/patterns/brand-landing-page.md +423 -0
  197. package/llms/patterns/chat-panel.md +210 -0
  198. package/llms/patterns/dashboard-page.md +170 -0
  199. package/llms/patterns/feed-page.md +103 -0
  200. package/llms/patterns/landing-page.md +122 -0
  201. package/llms/patterns/list-detail-page.md +100 -0
  202. package/llms/patterns/settings-page.md +121 -0
  203. package/llms/reference/token-intent.md +324 -0
  204. package/llms/reference/utilities.md +129 -0
  205. package/llms/tasks/adding-a-component.md +113 -0
  206. package/llms/tasks/auditing-an-app.md +216 -0
  207. package/llms/tasks/building-a-page.md +109 -0
  208. package/llms/tasks/building-a-tool.md +141 -0
  209. package/llms/tasks/building-an-ios-prototype.md +105 -0
  210. package/llms/tasks/building-with-sable.md +73 -0
  211. package/llms/tasks/choosing-a-component.md +109 -0
  212. package/llms/tasks/converting-an-app.md +139 -0
  213. package/llms/tasks/creating-a-preset.md +43 -0
  214. package/llms/tasks/creating-design-assets.md +96 -0
  215. package/llms/tasks/designing-a-surface.md +115 -0
  216. package/llms/tasks/developing-taste.md +157 -0
  217. package/llms/tasks/getting-started.md +63 -0
  218. package/llms/tasks/learning-from-builds.md +74 -0
  219. package/llms/tasks/reviewing-a-design.md +114 -0
  220. package/llms/tasks/reviewing-a-journey.md +84 -0
  221. package/llms/tasks/reviewing-motion.md +92 -0
  222. package/llms/tasks/using-presets.md +87 -0
  223. package/llms/taste/asset-plan.template.json +53 -0
  224. package/llms/taste/composition-follows-content.md +20 -0
  225. package/llms/taste/hierarchy-is-a-relationship.md +22 -0
  226. package/llms/taste/lessons.json +57 -0
  227. package/llms/taste/motion-needs-a-reason.md +21 -0
  228. package/llms/taste/surface-states-travel-together.md +20 -0
  229. package/llms-full.txt +14906 -0
  230. package/llms.txt +405 -0
  231. package/package.json +185 -3
  232. package/registry.json +18812 -0
  233. package/scripts/agent-setup.mjs +54 -0
  234. package/scripts/design-atlas.mjs +221 -0
  235. package/scripts/design-directions.mjs +123 -0
  236. package/scripts/design-memory.mjs +166 -0
  237. package/scripts/design-preferences.mjs +227 -0
  238. package/scripts/design-presets.mjs +48 -0
  239. package/scripts/grade.mjs +509 -0
  240. package/scripts/lint-rules.mjs +1065 -0
  241. package/scripts/review-evidence.mjs +120 -0
  242. package/scripts/skeleton.mjs +248 -0
  243. package/skills/entry/SKILL.md +34 -0
  244. package/skills/sable/SKILL.md +187 -0
  245. package/skills/sable/agents/openai.yaml +6 -0
  246. package/targets/ios.json +982 -0
  247. 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