@prestack/thread-ui 0.0.1 → 0.0.2

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 (269) hide show
  1. package/dist/ui/animations/anm_appear.ts +13 -0
  2. package/dist/ui/animations/anm_blur_in.ts +19 -0
  3. package/dist/ui/animations/anm_blur_out.ts +18 -0
  4. package/dist/ui/animations/anm_dismiss.ts +18 -0
  5. package/dist/ui/animations/anm_drop.ts +19 -0
  6. package/dist/ui/animations/anm_fall.ts +18 -0
  7. package/dist/ui/animations/anm_flash.ts +18 -0
  8. package/dist/ui/animations/anm_glow.ts +16 -0
  9. package/dist/ui/animations/anm_lift.ts +16 -0
  10. package/dist/ui/animations/anm_nudge.ts +16 -0
  11. package/dist/ui/animations/anm_pop_in.ts +23 -0
  12. package/dist/ui/animations/anm_pop_out.ts +22 -0
  13. package/dist/ui/animations/anm_press.ts +12 -0
  14. package/dist/ui/animations/anm_pulse.ts +13 -0
  15. package/dist/ui/animations/anm_rise.ts +19 -0
  16. package/dist/ui/animations/anm_shake.ts +13 -0
  17. package/dist/ui/animations/anm_slide_in_left.ts +19 -0
  18. package/dist/ui/animations/anm_slide_in_right.ts +19 -0
  19. package/dist/ui/animations/anm_slide_out_left.ts +18 -0
  20. package/dist/ui/animations/anm_slide_out_right.ts +18 -0
  21. package/dist/ui/animations/anm_spin.ts +12 -0
  22. package/dist/ui/animations/anm_swell.ts +16 -0
  23. package/dist/ui/animations/anm_wobble.ts +24 -0
  24. package/dist/ui/animations/anm_zoom_in.ts +19 -0
  25. package/dist/ui/animations/anm_zoom_out.ts +18 -0
  26. package/dist/ui/animations.ts +317 -0
  27. package/dist/ui/attach.js +13 -4
  28. package/dist/ui/attach.ts +613 -0
  29. package/dist/ui/charts.tsx +74 -0
  30. package/dist/ui/checked.d.ts +2 -2
  31. package/dist/ui/checked.ts +32 -0
  32. package/dist/ui/collector.ts +140 -0
  33. package/dist/ui/compositions.d.ts +10 -1
  34. package/dist/ui/compositions.js +262 -23
  35. package/dist/ui/compositions.ts +640 -0
  36. package/dist/ui/consent.ts +55 -0
  37. package/dist/ui/content.tsx +39 -0
  38. package/dist/ui/contrast.ts +27 -0
  39. package/dist/ui/controls.tsx +30 -0
  40. package/dist/ui/disclosure.tsx +22 -0
  41. package/dist/ui/dismiss-layer.ts +87 -0
  42. package/dist/ui/door.ts +140 -0
  43. package/dist/ui/drag.ts +316 -0
  44. package/dist/ui/elements/accordion.js +2 -2
  45. package/dist/ui/elements/accordion.tsx +47 -0
  46. package/dist/ui/elements/accordion.works.ts +66 -0
  47. package/dist/ui/elements/aspect-ratio.js +1 -1
  48. package/dist/ui/elements/aspect-ratio.tsx +30 -0
  49. package/dist/ui/elements/aspect-ratio.works.ts +5 -0
  50. package/dist/ui/elements/avatar.js +1 -1
  51. package/dist/ui/elements/avatar.tsx +33 -0
  52. package/dist/ui/elements/avatar.works.ts +16 -0
  53. package/dist/ui/elements/badge.js +1 -1
  54. package/dist/ui/elements/badge.tsx +32 -0
  55. package/dist/ui/elements/badge.works.ts +9 -0
  56. package/dist/ui/elements/bars.js +2 -2
  57. package/dist/ui/elements/bars.tsx +49 -0
  58. package/dist/ui/elements/bars.works.ts +29 -0
  59. package/dist/ui/elements/box.js +3 -2
  60. package/dist/ui/elements/box.tsx +35 -0
  61. package/dist/ui/elements/breadcrumb.js +2 -2
  62. package/dist/ui/elements/breadcrumb.tsx +37 -0
  63. package/dist/ui/elements/breadcrumb.works.ts +24 -0
  64. package/dist/ui/elements/button.js +1 -1
  65. package/dist/ui/elements/button.tsx +45 -0
  66. package/dist/ui/elements/button.works.tsx +52 -0
  67. package/dist/ui/elements/column.js +1 -1
  68. package/dist/ui/elements/column.tsx +28 -0
  69. package/dist/ui/elements/consent.js +2 -2
  70. package/dist/ui/elements/consent.tsx +70 -0
  71. package/dist/ui/elements/consent.works.tsx +132 -0
  72. package/dist/ui/elements/donut.js +2 -2
  73. package/dist/ui/elements/donut.tsx +44 -0
  74. package/dist/ui/elements/donut.works.ts +42 -0
  75. package/dist/ui/elements/each.js +1 -1
  76. package/dist/ui/elements/each.tsx +33 -0
  77. package/dist/ui/elements/each.works.tsx +82 -0
  78. package/dist/ui/elements/empty.js +1 -1
  79. package/dist/ui/elements/empty.tsx +32 -0
  80. package/dist/ui/elements/field.js +1 -1
  81. package/dist/ui/elements/field.tsx +44 -0
  82. package/dist/ui/elements/field.works.ts +65 -0
  83. package/dist/ui/elements/funnel.js +2 -2
  84. package/dist/ui/elements/funnel.tsx +49 -0
  85. package/dist/ui/elements/funnel.works.ts +37 -0
  86. package/dist/ui/elements/gauge.js +1 -1
  87. package/dist/ui/elements/gauge.tsx +39 -0
  88. package/dist/ui/elements/gauge.works.ts +42 -0
  89. package/dist/ui/elements/glyph.js +1 -1
  90. package/dist/ui/elements/glyph.tsx +29 -0
  91. package/dist/ui/elements/glyph.works.tsx +42 -0
  92. package/dist/ui/elements/grid.js +1 -1
  93. package/dist/ui/elements/grid.tsx +28 -0
  94. package/dist/ui/elements/heatmap.js +2 -2
  95. package/dist/ui/elements/heatmap.tsx +47 -0
  96. package/dist/ui/elements/heatmap.works.ts +24 -0
  97. package/dist/ui/elements/hit-box.js +1 -1
  98. package/dist/ui/elements/hit-box.tsx +46 -0
  99. package/dist/ui/elements/hit-box.works.tsx +576 -0
  100. package/dist/ui/elements/image.js +1 -1
  101. package/dist/ui/elements/image.tsx +28 -0
  102. package/dist/ui/elements/line-chart.js +2 -2
  103. package/dist/ui/elements/line-chart.tsx +44 -0
  104. package/dist/ui/elements/line-chart.works.ts +41 -0
  105. package/dist/ui/elements/link.js +1 -1
  106. package/dist/ui/elements/link.tsx +37 -0
  107. package/dist/ui/elements/link.works.ts +25 -0
  108. package/dist/ui/elements/media.js +1 -1
  109. package/dist/ui/elements/media.tsx +56 -0
  110. package/dist/ui/elements/menu.js +2 -2
  111. package/dist/ui/elements/menu.tsx +50 -0
  112. package/dist/ui/elements/menu.works.tsx +153 -0
  113. package/dist/ui/elements/money.js +1 -1
  114. package/dist/ui/elements/money.tsx +28 -0
  115. package/dist/ui/elements/money.works.ts +9 -0
  116. package/dist/ui/elements/pagination.js +2 -2
  117. package/dist/ui/elements/pagination.tsx +38 -0
  118. package/dist/ui/elements/pagination.works.ts +20 -0
  119. package/dist/ui/elements/progress.js +1 -1
  120. package/dist/ui/elements/progress.tsx +33 -0
  121. package/dist/ui/elements/progress.works.ts +8 -0
  122. package/dist/ui/elements/radio-group.js +2 -2
  123. package/dist/ui/elements/radio-group.tsx +49 -0
  124. package/dist/ui/elements/radio-group.works.ts +33 -0
  125. package/dist/ui/elements/row.js +1 -1
  126. package/dist/ui/elements/row.tsx +28 -0
  127. package/dist/ui/elements/scatter.js +1 -1
  128. package/dist/ui/elements/scatter.tsx +41 -0
  129. package/dist/ui/elements/scatter.works.ts +38 -0
  130. package/dist/ui/elements/select.js +2 -2
  131. package/dist/ui/elements/select.tsx +42 -0
  132. package/dist/ui/elements/select.works.ts +5 -0
  133. package/dist/ui/elements/separator.js +1 -1
  134. package/dist/ui/elements/separator.tsx +30 -0
  135. package/dist/ui/elements/skeleton.js +1 -1
  136. package/dist/ui/elements/skeleton.tsx +30 -0
  137. package/dist/ui/elements/slider.js +1 -1
  138. package/dist/ui/elements/slider.tsx +33 -0
  139. package/dist/ui/elements/slider.works.ts +5 -0
  140. package/dist/ui/elements/sparkline.js +1 -1
  141. package/dist/ui/elements/sparkline.tsx +36 -0
  142. package/dist/ui/elements/sparkline.works.ts +32 -0
  143. package/dist/ui/elements/spinner.js +1 -1
  144. package/dist/ui/elements/spinner.tsx +32 -0
  145. package/dist/ui/elements/stack.js +1 -1
  146. package/dist/ui/elements/stack.tsx +28 -0
  147. package/dist/ui/elements/stat.js +1 -1
  148. package/dist/ui/elements/stat.tsx +35 -0
  149. package/dist/ui/elements/stat.works.ts +14 -0
  150. package/dist/ui/elements/surface.js +1 -1
  151. package/dist/ui/elements/surface.tsx +35 -0
  152. package/dist/ui/elements/surface.works.ts +9 -0
  153. package/dist/ui/elements/table.js +2 -2
  154. package/dist/ui/elements/table.tsx +61 -0
  155. package/dist/ui/elements/table.works.ts +30 -0
  156. package/dist/ui/elements/tabs.js +2 -2
  157. package/dist/ui/elements/tabs.tsx +47 -0
  158. package/dist/ui/elements/tabs.works.ts +84 -0
  159. package/dist/ui/elements/text.js +1 -1
  160. package/dist/ui/elements/text.tsx +32 -0
  161. package/dist/ui/elements/textarea.js +1 -1
  162. package/dist/ui/elements/textarea.tsx +37 -0
  163. package/dist/ui/elements/textarea.works.ts +5 -0
  164. package/dist/ui/elements/time.js +1 -1
  165. package/dist/ui/elements/time.tsx +34 -0
  166. package/dist/ui/elements/time.works.ts +25 -0
  167. package/dist/ui/elements/toggle.js +1 -1
  168. package/dist/ui/elements/toggle.tsx +33 -0
  169. package/dist/ui/elements/toggle.works.ts +5 -0
  170. package/dist/ui/elements/tooltip.js +1 -1
  171. package/dist/ui/elements/tooltip.tsx +39 -0
  172. package/dist/ui/elements/tooltip.works.ts +77 -0
  173. package/dist/ui/elements/value.js +1 -1
  174. package/dist/ui/elements/value.tsx +30 -0
  175. package/dist/ui/elements/value.works.ts +9 -0
  176. package/dist/ui/elements.ts +127 -0
  177. package/dist/ui/focus-scope.ts +121 -0
  178. package/dist/ui/fonts.ts +355 -0
  179. package/dist/ui/generators/dev.ts +79 -0
  180. package/dist/ui/generators/elements.d.ts +1 -11
  181. package/dist/ui/generators/elements.js +43 -13
  182. package/dist/ui/generators/elements.ts +794 -0
  183. package/dist/ui/generators/i18n.ts +301 -0
  184. package/dist/ui/generators/icons.ts +214 -0
  185. package/dist/ui/generators/react.js +10 -3
  186. package/dist/ui/generators/react.ts +268 -0
  187. package/dist/ui/generators/routes.d.ts +1 -0
  188. package/dist/ui/generators/routes.js +89 -69
  189. package/dist/ui/generators/routes.ts +739 -0
  190. package/dist/ui/generators/show.ts +259 -0
  191. package/dist/ui/generators/styles.js +1 -1
  192. package/dist/ui/generators/styles.ts +670 -0
  193. package/dist/ui/generators/variants.ts +205 -0
  194. package/dist/ui/i18n/catalogs/en.ts +191 -0
  195. package/dist/ui/i18n/format.ts +221 -0
  196. package/dist/ui/i18n/locales.ts +33 -0
  197. package/dist/ui/i18n/parse.ts +315 -0
  198. package/dist/ui/i18n/translator.ts +31 -0
  199. package/dist/ui/i18n/types.ts +96 -0
  200. package/dist/ui/icons.ts +331 -0
  201. package/dist/ui/islands/map.ts +58 -0
  202. package/dist/ui/localizations.ts +75 -0
  203. package/dist/ui/makers/message.ts +292 -0
  204. package/dist/ui/material.ts +236 -0
  205. package/dist/ui/materials/mat_bordered.ts +13 -0
  206. package/dist/ui/materials/mat_ceramic.ts +13 -0
  207. package/dist/ui/materials/mat_clay.ts +13 -0
  208. package/dist/ui/materials/mat_elevated.ts +24 -0
  209. package/dist/ui/materials/mat_flat.ts +34 -0
  210. package/dist/ui/materials/mat_glass.ts +20 -0
  211. package/dist/ui/materials/mat_lit.ts +13 -0
  212. package/dist/ui/materials/mat_moulded.ts +11 -0
  213. package/dist/ui/materials.ts +206 -0
  214. package/dist/ui/modes.ts +44 -0
  215. package/dist/ui/navigation.tsx +32 -0
  216. package/dist/ui/positioning.ts +90 -0
  217. package/dist/ui/presented.tsx +119 -0
  218. package/dist/ui/provider.d.ts +2 -0
  219. package/dist/ui/provider.js +7 -1
  220. package/dist/ui/provider.tsx +182 -0
  221. package/dist/ui/render-loop.ts +133 -0
  222. package/dist/ui/roving-focus.ts +161 -0
  223. package/dist/ui/screens.js +17 -8
  224. package/dist/ui/screens.ts +142 -0
  225. package/dist/ui/scroll-lock.ts +28 -0
  226. package/dist/ui/series.ts +97 -0
  227. package/dist/ui/signals.ts +386 -0
  228. package/dist/ui/styleguide.ts +231 -0
  229. package/dist/ui/styleguides.ts +374 -0
  230. package/dist/ui/themes.ts +22 -0
  231. package/dist/ui/token.js +1 -1
  232. package/dist/ui/token.ts +78 -0
  233. package/dist/ui/tokens/shadow.ts +25 -0
  234. package/dist/ui/tokens.ts +235 -0
  235. package/dist/ui/validators.ts +232 -0
  236. package/dist/ui/variant.d.ts +1 -0
  237. package/dist/ui/variant.js +3 -0
  238. package/dist/ui/variant.ts +352 -0
  239. package/dist/ui/variants/var_avatar-round.ts +14 -0
  240. package/dist/ui/variants/var_bubble-me.ts +14 -0
  241. package/dist/ui/variants/var_bubble-them.ts +13 -0
  242. package/dist/ui/variants/var_button-border.ts +76 -0
  243. package/dist/ui/variants/var_button-cta.ts +76 -0
  244. package/dist/ui/variants/var_button-destructive-ghost.ts +72 -0
  245. package/dist/ui/variants/var_button-destructive.ts +76 -0
  246. package/dist/ui/variants/var_button-fill.ts +76 -0
  247. package/dist/ui/variants/var_button-ghost.ts +72 -0
  248. package/dist/ui/variants/var_button-glass.ts +55 -0
  249. package/dist/ui/variants/var_button-normal.ts +76 -0
  250. package/dist/ui/variants/var_button-quiet.ts +74 -0
  251. package/dist/ui/variants/var_card.ts +17 -0
  252. package/dist/ui/variants/var_chip.ts +13 -0
  253. package/dist/ui/variants/var_choice.ts +15 -0
  254. package/dist/ui/variants/var_framed.ts +10 -0
  255. package/dist/ui/variants/var_glyph-plain.ts +11 -0
  256. package/dist/ui/variants/var_input.ts +43 -0
  257. package/dist/ui/variants/var_link-inline.ts +29 -0
  258. package/dist/ui/variants/var_list-row.ts +34 -0
  259. package/dist/ui/variants/var_rule.ts +10 -0
  260. package/dist/ui/variants/var_skeleton-block.ts +12 -0
  261. package/dist/ui/variants/var_spinner-turning.ts +12 -0
  262. package/dist/ui/variants/var_stat-tile.ts +13 -0
  263. package/dist/ui/variants/var_tab-bar.ts +15 -0
  264. package/dist/ui/variants/var_tap-target.ts +25 -0
  265. package/dist/ui/variants.ts +64 -0
  266. package/dist/ui/wrappers.d.ts +4 -1
  267. package/dist/ui/wrappers.js +60 -20
  268. package/dist/ui/wrappers.tsx +426 -0
  269. package/package.json +8 -2
@@ -0,0 +1,352 @@
1
+ import { BOUND as MATERIAL_BOUND, compose, composed, FREE, type Material, type Setting } from './material.ts'
2
+ import type { MATERIALS } from './materials.ts'
3
+ import type { Holds, Token, Wearing } from './token.ts'
4
+ import { coexist, holds, isSignal, selectorOf, signalNames } from './signals.ts'
5
+
6
+ interface Stateful {
7
+ readonly id: string
8
+
9
+ readonly states?: readonly {
10
+ readonly when: readonly string[]
11
+ readonly sets: readonly Setting[]
12
+ }[]
13
+ }
14
+
15
+ type SignalId = (typeof holds)[number]['id'] | (string & {})
16
+
17
+ type MaterialId = (typeof MATERIALS)['declares'][number]['id'] | (string & {})
18
+
19
+ const BOUND = {
20
+ background: ['colour', 'gradient', 'texture', 'noise', 'img'],
21
+ borderColor: ['colour'],
22
+ color: ['colour'],
23
+ borderRadius: ['radius'],
24
+ borderWidth: ['edge'],
25
+
26
+ borderStyle: ['line'],
27
+ boxShadow: ['shadow'],
28
+ cursor: ['cursor'],
29
+ gap: ['space'],
30
+ padding: ['space'],
31
+ outlineOffset: ['space'],
32
+ minHeight: ['control', 'space'],
33
+ minWidth: ['space'],
34
+ maxWidth: ['space'],
35
+ opacity: ['opacity'],
36
+ transform: ['transform'],
37
+ animation: ['animation'],
38
+ width: ['icon'],
39
+ height: ['control', 'icon'],
40
+
41
+ display: ['layout'],
42
+ flexDirection: ['flow'],
43
+ alignItems: ['align'],
44
+ justifyContent: ['align'],
45
+ flexWrap: ['wrap'],
46
+
47
+ position: ['position'],
48
+ visibility: ['visibility'],
49
+ pointerEvents: ['reach'],
50
+ userSelect: ['picking'],
51
+ touchAction: ['touch'],
52
+ willChange: ['readying'],
53
+ transformOrigin: ['origin'],
54
+ overflow: ['overflow'],
55
+ zIndex: ['layer'],
56
+ top: ['space'],
57
+ left: ['space'],
58
+ aspectRatio: ['ratio'],
59
+ } as const
60
+
61
+ type Property = keyof typeof BOUND
62
+
63
+ type BoundSettingFor<P extends Property> = {
64
+ readonly property: P
65
+
66
+ readonly is?: string
67
+
68
+ readonly token?: (typeof BOUND)[P][number]
69
+
70
+ readonly asks?: Readonly<Record<string, string | number>>
71
+ }
72
+
73
+ type FreeProperty = (typeof FREE)[number]
74
+
75
+ type FreeSettingFor<P extends FreeProperty> = {
76
+ readonly property: P
77
+
78
+ readonly is: string
79
+ }
80
+
81
+ type VariantSetting =
82
+ | { [P in Property]: BoundSettingFor<P> }[Property]
83
+ | { [P in FreeProperty]: FreeSettingFor<P> }[FreeProperty]
84
+
85
+ export interface Variant {
86
+ readonly id: string
87
+
88
+ readonly version?: number
89
+
90
+ readonly material?: MaterialId
91
+
92
+ readonly type?: string
93
+
94
+ readonly icon?: string
95
+
96
+ readonly sets: readonly VariantSetting[]
97
+
98
+ readonly states?: readonly {
99
+ readonly when: readonly SignalId[]
100
+ readonly sets: readonly VariantSetting[]
101
+ }[]
102
+
103
+ readonly reacts: readonly SignalId[]
104
+
105
+ readonly plays?: {
106
+ readonly in?: string
107
+ readonly out?: string
108
+ }
109
+ }
110
+
111
+ export function propertiesHeld(): Readonly<Record<string, readonly string[]>> {
112
+ return BOUND
113
+ }
114
+
115
+ export function freeHeld(): readonly string[] {
116
+ return FREE
117
+ }
118
+
119
+ export const structural = new Set(['tag', 'shows', 'reads', 'holds', 'each', 'by', 'takes', 'marks', 'gives', 'keyed', 'when'])
120
+
121
+ export function looking(made: Readonly<Record<string, string>>): boolean {
122
+ return Object.keys(made).some((key) => !structural.has(key))
123
+ }
124
+
125
+ export const variant = <const T extends Variant>(
126
+ one: T & {
127
+ readonly sets: {
128
+ readonly [I in keyof T['sets']]: T['sets'][I] extends { readonly is: infer Said }
129
+ ? { readonly is: Wearing<Said & string> }
130
+ : unknown
131
+ }
132
+ } & (T extends { readonly states: infer States }
133
+ ? {
134
+ readonly states: {
135
+ readonly [S in keyof States]: States[S] extends { readonly sets: infer Sets }
136
+ ? {
137
+ readonly sets: {
138
+ readonly [I in keyof Sets]: Sets[I] extends { readonly is: infer Said }
139
+ ? { readonly is: Wearing<Said & string> }
140
+ : unknown
141
+ }
142
+ }
143
+ : unknown
144
+ }
145
+ }
146
+ : unknown),
147
+ ): T => one
148
+
149
+ export function boundHeld(): Readonly<Record<string, readonly string[]>> {
150
+ return BOUND
151
+ }
152
+ export function camel(id: string): string {
153
+ return id.replace(/-([a-z0-9])/g, (_, one: string) => one.toUpperCase())
154
+ }
155
+
156
+ export function classOf(id: string): string {
157
+ return `thread-${id}`
158
+ }
159
+
160
+ export function classOfElement(id: string): string {
161
+ return `e-${id}`
162
+ }
163
+
164
+ export function classOfComposition(id: string): string {
165
+ return `c-${id}`
166
+ }
167
+
168
+ export function classOfScreen(id: string): string {
169
+ return `s-${id}`
170
+ }
171
+
172
+ export function unreacted(
173
+ variant: Variant,
174
+ materials: Readonly<Record<string, Material>>,
175
+ ): readonly string[] {
176
+ const whole = statesOf(variant, materials)
177
+ const described = new Set((whole.states ?? []).flatMap((one) => one.when))
178
+ const claimed = new Set<string>(variant.reacts)
179
+ const missing: string[] = []
180
+
181
+ for (const name of [...described].sort()) {
182
+ if (claimed.has(name)) continue
183
+ missing.push(
184
+ `\`${variant.id}\` looks different under "${name}" and does not say it reacts to it. A ` +
185
+ 'variant states what it answers to, so reading one tells you without opening its material.',
186
+ )
187
+ }
188
+
189
+ for (const name of [...claimed].sort()) {
190
+ if (!isSignal(name)) {
191
+ missing.push(
192
+ `\`${variant.id}\` says it reacts to "${name}", and no signal by that name is declared. ` +
193
+ `Declared: ${signalNames().join(' · ')}.`,
194
+ )
195
+ continue
196
+ }
197
+ if (described.has(name)) continue
198
+ missing.push(
199
+ `\`${variant.id}\` says it reacts to "${name}" and never says how it looks under it, so it ` +
200
+ 'reacts to nothing.',
201
+ )
202
+ }
203
+
204
+ return missing
205
+ }
206
+
207
+ export function complete(
208
+ variant: Variant,
209
+ materials: Readonly<Record<string, Material>>,
210
+ named: ReadonlySet<string>,
211
+ tokens: Readonly<Record<string, Token>>,
212
+ kinds: Readonly<Record<string, string>>,
213
+ ): Readonly<Record<string, Holds>> {
214
+ const made: Record<string, Holds> = {}
215
+
216
+ if (variant.material !== undefined) {
217
+ const material = materials[variant.material]
218
+ if (material === undefined) {
219
+ throw new Error(
220
+ `\`${variant.id}\` is made of \`${variant.material}\`, and no material by that name is ` +
221
+ 'declared. A variant is material plus the rest, so without the material there is no rest.',
222
+ )
223
+ }
224
+ Object.assign(made, compose(material, named, tokens, kinds))
225
+ }
226
+
227
+ Object.assign(
228
+ made,
229
+ composed(variant.sets, named, tokens, variant.id, kinds, { ...MATERIAL_BOUND, ...BOUND }),
230
+ )
231
+
232
+ return made
233
+ }
234
+
235
+ export function statesOf(
236
+ variant: Variant,
237
+ materials: Readonly<Record<string, Material>>,
238
+ ): Stateful {
239
+ const material = variant.material === undefined ? undefined : materials[variant.material]
240
+ const merged = new Map<string, { when: readonly string[]; sets: Setting[] }>()
241
+
242
+ for (const state of [...(material?.states ?? []), ...(variant.states ?? [])]) {
243
+ const at = [...state.when].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join('+')
244
+ const already = merged.get(at)
245
+ if (already === undefined) merged.set(at, { when: state.when, sets: [...state.sets] })
246
+ else already.sets.push(...state.sets)
247
+ }
248
+
249
+ return { id: variant.id, states: [...merged.values()] }
250
+ }
251
+
252
+ function marked(when: readonly string[]): string {
253
+ return [...when]
254
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
255
+ .map((one) => selectorOf(one) ?? '')
256
+ .join('')
257
+ }
258
+
259
+ export function under(
260
+ variant: Stateful,
261
+ named: ReadonlySet<string>,
262
+ tokens: Readonly<Record<string, Token>>,
263
+ kinds: Readonly<Record<string, string>>,
264
+ ): readonly { readonly marks: string; readonly made: Readonly<Record<string, Holds>> }[] {
265
+ const found: { marks: string; made: Readonly<Record<string, Holds>> }[] = []
266
+
267
+ for (const state of variant.states ?? []) {
268
+ if (state.when.length === 0) {
269
+ throw new Error(
270
+ `\`${variant.id}\` declares a state that names no signal. A state is the set of truths it ` +
271
+ 'holds under, and the empty set is the resting look, which is already declared.',
272
+ )
273
+ }
274
+
275
+ for (const name of state.when) {
276
+ if (isSignal(name)) continue
277
+ throw new Error(
278
+ `\`${variant.id}\` declares a state under \`${name}\`, and no signal by that name is ` +
279
+ `declared. Declared: ${signalNames().join(' · ')}.`,
280
+ )
281
+ }
282
+
283
+ found.push({
284
+ marks: marked(state.when),
285
+ made: composed(state.sets, named, tokens, variant.id, kinds, { ...MATERIAL_BOUND, ...BOUND }),
286
+ })
287
+ }
288
+
289
+ return found
290
+ }
291
+
292
+ const key = (when: readonly string[]): string =>
293
+ [...when].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).join('+')
294
+
295
+ function sized(all: readonly string[], size: number): readonly (readonly string[])[] {
296
+ if (size === 0) return [[]]
297
+ const found: (readonly string[])[] = []
298
+ for (let at = 0; at <= all.length - size; at += 1) {
299
+ const head = all[at]
300
+ if (head === undefined) continue
301
+ for (const rest of sized(all.slice(at + 1), size - 1)) found.push([head, ...rest])
302
+ }
303
+ return found
304
+ }
305
+
306
+ function together(all: readonly string[]): boolean {
307
+ for (let i = 0; i < all.length; i += 1) {
308
+ for (let j = i + 1; j < all.length; j += 1) {
309
+ if (!coexist(all[i] ?? '', all[j] ?? '')) return false
310
+ }
311
+ }
312
+ return true
313
+ }
314
+
315
+ export function undeclaredStates(variant: Stateful): readonly string[] {
316
+ const states = variant.states ?? []
317
+ const declared = new Set(states.map((one) => key(one.when)))
318
+ const named = [...new Set(states.flatMap((one) => one.when))].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
319
+ const missing: string[] = []
320
+
321
+ for (let size = 2; size <= named.length; size += 1) {
322
+ for (const combination of sized(named, size)) {
323
+ if (!together(combination)) continue
324
+ if (declared.has(key(combination))) continue
325
+
326
+ const applying = states.filter((one) => one.when.every((name) => combination.includes(name)))
327
+ const deepest = new Map<string, number>()
328
+ const tied = new Map<string, number>()
329
+ for (const state of applying) {
330
+ for (const property of new Set(state.sets.map((set) => set.property))) {
331
+ const depth = deepest.get(property) ?? 0
332
+ if (state.when.length > depth) {
333
+ deepest.set(property, state.when.length)
334
+ tied.set(property, 1)
335
+ } else if (state.when.length === depth) {
336
+ tied.set(property, (tied.get(property) ?? 0) + 1)
337
+ }
338
+ }
339
+ }
340
+ const clash = new Set([...tied.entries()].filter(([, held]) => held > 1).map(([property]) => property))
341
+ if (clash.size === 0) continue
342
+
343
+ missing.push(
344
+ `${combination.map((one) => `"${one}"`).join(' and ')} can all hold at once and they all ` +
345
+ `set ${[...clash].sort().join(', ')}, so which one wins would be an accident of file ` +
346
+ `order. "${key(combination)}" is not declared — declare it, or say the signals cannot coexist`,
347
+ )
348
+ }
349
+ }
350
+
351
+ return missing
352
+ }
@@ -0,0 +1,14 @@
1
+ import { variant } from '../variant.ts'
2
+
3
+ export const AVATAR_ROUND = variant({
4
+ id: 'avatar-round',
5
+ sets: [
6
+ { property: 'width', is: 'icon-lg' },
7
+ { property: 'height', is: 'icon-lg' },
8
+ { property: 'borderRadius', is: 'radius-pill' },
9
+ { property: 'background', is: 'colour-sunken' },
10
+ { property: 'display', is: 'layout-flex' },
11
+ { property: 'alignItems', is: 'align-centre' },
12
+ ],
13
+ reacts: [],
14
+ } as const)
@@ -0,0 +1,14 @@
1
+ import { variant } from '../variant.ts'
2
+
3
+ export const BUBBLE_ME = variant({
4
+ id: 'bubble-me',
5
+ material: 'flat',
6
+ sets: [
7
+ { property: 'background', is: 'colour-accent' },
8
+ { property: 'color', is: 'colour-on-accent' },
9
+ { property: 'borderRadius', is: 'radius-lg' },
10
+ { property: 'padding', is: 'space-3 space-4' },
11
+ { property: 'gap', is: 'space-2' },
12
+ ],
13
+ reacts: ['focus', 'hover'],
14
+ } as const)
@@ -0,0 +1,13 @@
1
+ import { variant } from '../variant.ts'
2
+
3
+ export const BUBBLE_THEM = variant({
4
+ id: 'bubble-them',
5
+ material: 'flat',
6
+ sets: [
7
+ { property: 'color', is: 'colour-ink' },
8
+ { property: 'borderRadius', is: 'radius-lg' },
9
+ { property: 'padding', is: 'space-3 space-4' },
10
+ { property: 'gap', is: 'space-2' },
11
+ ],
12
+ reacts: ['focus', 'hover'],
13
+ } as const)
@@ -0,0 +1,76 @@
1
+ import { variant } from '../variant.ts'
2
+
3
+ export const BUTTON_BORDER = variant({
4
+ id: 'button-border',
5
+ type: 'label',
6
+ icon: 'md',
7
+ sets: [
8
+ { property: 'display', is: 'layout-flex' },
9
+ { property: 'alignItems', is: 'align-centre' },
10
+ { property: 'justifyContent', is: 'align-centre' },
11
+ { property: 'borderRadius', is: 'control-radius' },
12
+ { property: 'height', is: 'control-md' },
13
+ { property: 'padding', is: 'control-pad-y control-pad-x' },
14
+ { property: 'gap', is: 'control-gap' },
15
+ { property: 'cursor', is: 'cursor-press' },
16
+ { property: 'transition', is: 'motion-quick ease-out' },
17
+ { property: 'background', is: 'colour-clear' },
18
+ { property: 'color', is: 'colour-accent' },
19
+ { property: 'borderWidth', is: 'control-edge' },
20
+ { property: 'borderStyle', is: 'line-solid' },
21
+ { property: 'borderColor', is: 'colour-accent' },
22
+ { property: 'boxShadow', is: 'shadow-0' },
23
+ ],
24
+ states: [
25
+ {
26
+ when: ['hover'],
27
+ sets: [
28
+ { property: 'background', is: 'colour-surface-lit' },
29
+ { property: 'transform', is: 'transform-lift' },
30
+ ],
31
+ },
32
+ {
33
+ when: ['active'],
34
+ sets: [
35
+ { property: 'background', is: 'colour-surface-lit' },
36
+ { property: 'transform', is: 'transform-press' },
37
+ ],
38
+ },
39
+ {
40
+ when: ['active', 'hover'],
41
+ sets: [
42
+ { property: 'background', is: 'colour-surface-lit' },
43
+ { property: 'transform', is: 'transform-press' },
44
+ ],
45
+ },
46
+ {
47
+ when: ['focus'],
48
+ sets: [
49
+ { property: 'outline', is: 'control-ring line-solid colour-accent' },
50
+ { property: 'outlineOffset', is: 'control-ring-offset' },
51
+ ],
52
+ },
53
+ {
54
+ when: ['loading'],
55
+ sets: [
56
+ { property: 'opacity', is: 'opacity-muted' },
57
+ ],
58
+ },
59
+ {
60
+ when: ['disabled'],
61
+ sets: [
62
+ { property: 'cursor', is: 'cursor-refuse' },
63
+ { property: 'borderColor', is: 'colour-edge' },
64
+ { property: 'color', is: 'colour-ink-faint' },
65
+ ],
66
+ },
67
+ {
68
+ when: ['disabled', 'hover'],
69
+ sets: [
70
+ { property: 'background', is: 'colour-clear' },
71
+ { property: 'transform', is: 'transform-rest' },
72
+ ],
73
+ },
74
+ ],
75
+ reacts: ['active', 'disabled', 'focus', 'hover', 'loading'],
76
+ } as const)
@@ -0,0 +1,76 @@
1
+ import { variant } from '../variant.ts'
2
+
3
+ export const BUTTON_CTA = variant({
4
+ id: 'button-cta',
5
+ type: 'body',
6
+ icon: 'md',
7
+ sets: [
8
+ { property: 'display', is: 'layout-flex' },
9
+ { property: 'alignItems', is: 'align-centre' },
10
+ { property: 'justifyContent', is: 'align-centre' },
11
+ { property: 'borderRadius', is: 'control-radius' },
12
+ { property: 'height', is: 'control-lg' },
13
+ { property: 'padding', is: 'control-pad-y control-pad-x' },
14
+ { property: 'gap', is: 'control-gap' },
15
+ { property: 'cursor', is: 'cursor-press' },
16
+ { property: 'transition', is: 'motion-quick ease-out' },
17
+ { property: 'background', is: 'colour-accent' },
18
+ { property: 'color', is: 'colour-on-accent' },
19
+ { property: 'borderWidth', is: 'edge-hair' },
20
+ { property: 'borderStyle', is: 'line-off' },
21
+ { property: 'borderColor', is: 'colour-clear' },
22
+ { property: 'boxShadow', is: 'shadow-2' },
23
+ ],
24
+ states: [
25
+ {
26
+ when: ['hover'],
27
+ sets: [
28
+ { property: 'background', is: 'colour-accent-hover' },
29
+ { property: 'transform', is: 'transform-lift' },
30
+ ],
31
+ },
32
+ {
33
+ when: ['active'],
34
+ sets: [
35
+ { property: 'background', is: 'colour-accent-press' },
36
+ { property: 'transform', is: 'transform-press' },
37
+ ],
38
+ },
39
+ {
40
+ when: ['active', 'hover'],
41
+ sets: [
42
+ { property: 'background', is: 'colour-accent-press' },
43
+ { property: 'transform', is: 'transform-press' },
44
+ ],
45
+ },
46
+ {
47
+ when: ['focus'],
48
+ sets: [
49
+ { property: 'outline', is: 'control-ring line-solid colour-accent' },
50
+ { property: 'outlineOffset', is: 'control-ring-offset' },
51
+ ],
52
+ },
53
+ {
54
+ when: ['loading'],
55
+ sets: [
56
+ { property: 'opacity', is: 'opacity-muted' },
57
+ ],
58
+ },
59
+ {
60
+ when: ['disabled'],
61
+ sets: [
62
+ { property: 'cursor', is: 'cursor-refuse' },
63
+ { property: 'background', is: 'colour-sunken' },
64
+ { property: 'color', is: 'colour-ink-faint' },
65
+ ],
66
+ },
67
+ {
68
+ when: ['disabled', 'hover'],
69
+ sets: [
70
+ { property: 'background', is: 'colour-sunken' },
71
+ { property: 'transform', is: 'transform-rest' },
72
+ ],
73
+ },
74
+ ],
75
+ reacts: ['active', 'disabled', 'focus', 'hover', 'loading'],
76
+ } as const)
@@ -0,0 +1,72 @@
1
+ import { variant } from '../variant.ts'
2
+
3
+ export const BUTTON_DESTRUCTIVE_GHOST = variant({
4
+ id: 'button-destructive-ghost',
5
+ type: 'label',
6
+ icon: 'md',
7
+ sets: [
8
+ { property: 'display', is: 'layout-flex' },
9
+ { property: 'alignItems', is: 'align-centre' },
10
+ { property: 'justifyContent', is: 'align-centre' },
11
+ { property: 'borderRadius', is: 'control-radius' },
12
+ { property: 'height', is: 'control-md' },
13
+ { property: 'padding', is: 'control-pad-y control-pad-x' },
14
+ { property: 'gap', is: 'control-gap' },
15
+ { property: 'cursor', is: 'cursor-press' },
16
+ { property: 'transition', is: 'motion-quick ease-out' },
17
+ { property: 'background', is: 'colour-clear' },
18
+ { property: 'color', is: 'colour-negative' },
19
+ { property: 'borderWidth', is: 'edge-hair' },
20
+ { property: 'borderStyle', is: 'line-off' },
21
+ { property: 'borderColor', is: 'colour-clear' },
22
+ { property: 'boxShadow', is: 'shadow-0' },
23
+ ],
24
+ states: [
25
+ {
26
+ when: ['hover'],
27
+ sets: [
28
+ { property: 'background', is: 'colour-surface-alert' },
29
+ { property: 'transform', is: 'transform-lift' },
30
+ ],
31
+ },
32
+ {
33
+ when: ['active'],
34
+ sets: [
35
+ { property: 'background', is: 'colour-surface-alert' },
36
+ { property: 'transform', is: 'transform-press' },
37
+ ],
38
+ },
39
+ {
40
+ when: ['active', 'hover'],
41
+ sets: [
42
+ { property: 'background', is: 'colour-surface-alert' },
43
+ { property: 'transform', is: 'transform-press' },
44
+ ],
45
+ },
46
+ {
47
+ when: ['focus'],
48
+ sets: [
49
+ { property: 'outline', is: 'control-ring line-solid colour-negative' },
50
+ { property: 'outlineOffset', is: 'control-ring-offset' },
51
+ ],
52
+ },
53
+ {
54
+ when: ['loading'],
55
+ sets: [
56
+ { property: 'opacity', is: 'opacity-muted' },
57
+ ],
58
+ },
59
+ {
60
+ when: ['disabled'],
61
+ sets: [{ property: 'color', is: 'colour-ink-faint' }, { property: 'cursor', is: 'cursor-refuse' }],
62
+ },
63
+ {
64
+ when: ['disabled', 'hover'],
65
+ sets: [
66
+ { property: 'background', is: 'colour-clear' },
67
+ { property: 'transform', is: 'transform-rest' },
68
+ ],
69
+ },
70
+ ],
71
+ reacts: ['active', 'disabled', 'focus', 'hover', 'loading'],
72
+ } as const)
@@ -0,0 +1,76 @@
1
+ import { variant } from '../variant.ts'
2
+
3
+ export const BUTTON_DESTRUCTIVE = variant({
4
+ id: 'button-destructive',
5
+ type: 'label',
6
+ icon: 'md',
7
+ sets: [
8
+ { property: 'display', is: 'layout-flex' },
9
+ { property: 'alignItems', is: 'align-centre' },
10
+ { property: 'justifyContent', is: 'align-centre' },
11
+ { property: 'borderRadius', is: 'control-radius' },
12
+ { property: 'height', is: 'control-md' },
13
+ { property: 'padding', is: 'control-pad-y control-pad-x' },
14
+ { property: 'gap', is: 'control-gap' },
15
+ { property: 'cursor', is: 'cursor-press' },
16
+ { property: 'transition', is: 'motion-quick ease-out' },
17
+ { property: 'background', is: 'colour-negative' },
18
+ { property: 'color', is: 'colour-on-negative' },
19
+ { property: 'borderWidth', is: 'edge-hair' },
20
+ { property: 'borderStyle', is: 'line-off' },
21
+ { property: 'borderColor', is: 'colour-clear' },
22
+ { property: 'boxShadow', is: 'shadow-0' },
23
+ ],
24
+ states: [
25
+ {
26
+ when: ['hover'],
27
+ sets: [
28
+ { property: 'background', is: 'colour-negative-hover' },
29
+ { property: 'transform', is: 'transform-lift' },
30
+ ],
31
+ },
32
+ {
33
+ when: ['active'],
34
+ sets: [
35
+ { property: 'background', is: 'colour-negative-press' },
36
+ { property: 'transform', is: 'transform-press' },
37
+ ],
38
+ },
39
+ {
40
+ when: ['active', 'hover'],
41
+ sets: [
42
+ { property: 'background', is: 'colour-negative-press' },
43
+ { property: 'transform', is: 'transform-press' },
44
+ ],
45
+ },
46
+ {
47
+ when: ['focus'],
48
+ sets: [
49
+ { property: 'outline', is: 'control-ring line-solid colour-negative' },
50
+ { property: 'outlineOffset', is: 'control-ring-offset' },
51
+ ],
52
+ },
53
+ {
54
+ when: ['loading'],
55
+ sets: [
56
+ { property: 'opacity', is: 'opacity-muted' },
57
+ ],
58
+ },
59
+ {
60
+ when: ['disabled'],
61
+ sets: [
62
+ { property: 'cursor', is: 'cursor-refuse' },
63
+ { property: 'background', is: 'colour-sunken' },
64
+ { property: 'color', is: 'colour-ink-faint' },
65
+ ],
66
+ },
67
+ {
68
+ when: ['disabled', 'hover'],
69
+ sets: [
70
+ { property: 'background', is: 'colour-sunken' },
71
+ { property: 'transform', is: 'transform-rest' },
72
+ ],
73
+ },
74
+ ],
75
+ reacts: ['active', 'disabled', 'focus', 'hover', 'loading'],
76
+ } as const)