@prestack/thread-ui 0.1.10 → 0.1.12

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 (209) hide show
  1. package/CHANGELOG.md +375 -0
  2. package/dist/ui/attach.js +6 -7
  3. package/dist/ui/compositions.d.ts +0 -8
  4. package/dist/ui/compositions.js +3 -166
  5. package/dist/ui/generators/routes.js +1 -0
  6. package/package.json +2 -2
  7. package/dist/ui/animations/anm_appear.ts +0 -13
  8. package/dist/ui/animations/anm_blur_in.ts +0 -19
  9. package/dist/ui/animations/anm_blur_out.ts +0 -18
  10. package/dist/ui/animations/anm_dismiss.ts +0 -18
  11. package/dist/ui/animations/anm_drop.ts +0 -19
  12. package/dist/ui/animations/anm_fall.ts +0 -18
  13. package/dist/ui/animations/anm_flash.ts +0 -18
  14. package/dist/ui/animations/anm_glow.ts +0 -16
  15. package/dist/ui/animations/anm_lift.ts +0 -16
  16. package/dist/ui/animations/anm_nudge.ts +0 -16
  17. package/dist/ui/animations/anm_pop_in.ts +0 -23
  18. package/dist/ui/animations/anm_pop_out.ts +0 -22
  19. package/dist/ui/animations/anm_press.ts +0 -12
  20. package/dist/ui/animations/anm_pulse.ts +0 -13
  21. package/dist/ui/animations/anm_rise.ts +0 -19
  22. package/dist/ui/animations/anm_shake.ts +0 -13
  23. package/dist/ui/animations/anm_slide_in_left.ts +0 -19
  24. package/dist/ui/animations/anm_slide_in_right.ts +0 -19
  25. package/dist/ui/animations/anm_slide_out_left.ts +0 -18
  26. package/dist/ui/animations/anm_slide_out_right.ts +0 -18
  27. package/dist/ui/animations/anm_spin.ts +0 -12
  28. package/dist/ui/animations/anm_swell.ts +0 -16
  29. package/dist/ui/animations/anm_wobble.ts +0 -24
  30. package/dist/ui/animations/anm_zoom_in.ts +0 -19
  31. package/dist/ui/animations/anm_zoom_out.ts +0 -18
  32. package/dist/ui/animations.ts +0 -308
  33. package/dist/ui/attach.ts +0 -553
  34. package/dist/ui/charts.tsx +0 -69
  35. package/dist/ui/checked.ts +0 -32
  36. package/dist/ui/collector.ts +0 -140
  37. package/dist/ui/compositions.ts +0 -613
  38. package/dist/ui/consent.ts +0 -55
  39. package/dist/ui/content.tsx +0 -27
  40. package/dist/ui/contrast.ts +0 -27
  41. package/dist/ui/controls.tsx +0 -25
  42. package/dist/ui/disclosure.tsx +0 -20
  43. package/dist/ui/dismiss-layer.ts +0 -87
  44. package/dist/ui/door.ts +0 -140
  45. package/dist/ui/drag.ts +0 -316
  46. package/dist/ui/elements/accordion.tsx +0 -47
  47. package/dist/ui/elements/accordion.works.ts +0 -66
  48. package/dist/ui/elements/aspect-ratio.tsx +0 -31
  49. package/dist/ui/elements/aspect-ratio.works.ts +0 -5
  50. package/dist/ui/elements/avatar.tsx +0 -34
  51. package/dist/ui/elements/avatar.works.ts +0 -16
  52. package/dist/ui/elements/badge.tsx +0 -33
  53. package/dist/ui/elements/badge.works.ts +0 -9
  54. package/dist/ui/elements/bars.tsx +0 -50
  55. package/dist/ui/elements/bars.works.ts +0 -29
  56. package/dist/ui/elements/box.tsx +0 -36
  57. package/dist/ui/elements/breadcrumb.tsx +0 -38
  58. package/dist/ui/elements/breadcrumb.works.ts +0 -24
  59. package/dist/ui/elements/button.tsx +0 -46
  60. package/dist/ui/elements/button.works.tsx +0 -52
  61. package/dist/ui/elements/column.tsx +0 -29
  62. package/dist/ui/elements/consent.tsx +0 -71
  63. package/dist/ui/elements/consent.works.tsx +0 -132
  64. package/dist/ui/elements/donut.tsx +0 -45
  65. package/dist/ui/elements/donut.works.ts +0 -42
  66. package/dist/ui/elements/each.tsx +0 -34
  67. package/dist/ui/elements/each.works.tsx +0 -82
  68. package/dist/ui/elements/empty.tsx +0 -33
  69. package/dist/ui/elements/field.tsx +0 -45
  70. package/dist/ui/elements/field.works.ts +0 -65
  71. package/dist/ui/elements/funnel.tsx +0 -50
  72. package/dist/ui/elements/funnel.works.ts +0 -37
  73. package/dist/ui/elements/gauge.tsx +0 -40
  74. package/dist/ui/elements/gauge.works.ts +0 -42
  75. package/dist/ui/elements/glyph.tsx +0 -30
  76. package/dist/ui/elements/glyph.works.tsx +0 -42
  77. package/dist/ui/elements/grid.tsx +0 -29
  78. package/dist/ui/elements/heatmap.tsx +0 -48
  79. package/dist/ui/elements/heatmap.works.ts +0 -24
  80. package/dist/ui/elements/hit-box.tsx +0 -47
  81. package/dist/ui/elements/hit-box.works.tsx +0 -576
  82. package/dist/ui/elements/image.tsx +0 -29
  83. package/dist/ui/elements/line-chart.tsx +0 -45
  84. package/dist/ui/elements/line-chart.works.ts +0 -41
  85. package/dist/ui/elements/link.tsx +0 -38
  86. package/dist/ui/elements/link.works.ts +0 -25
  87. package/dist/ui/elements/media.tsx +0 -57
  88. package/dist/ui/elements/menu.tsx +0 -51
  89. package/dist/ui/elements/menu.works.tsx +0 -154
  90. package/dist/ui/elements/money.tsx +0 -36
  91. package/dist/ui/elements/money.works.ts +0 -9
  92. package/dist/ui/elements/pagination.tsx +0 -39
  93. package/dist/ui/elements/pagination.works.ts +0 -20
  94. package/dist/ui/elements/progress.tsx +0 -34
  95. package/dist/ui/elements/progress.works.ts +0 -8
  96. package/dist/ui/elements/radio-group.tsx +0 -50
  97. package/dist/ui/elements/radio-group.works.ts +0 -33
  98. package/dist/ui/elements/row.tsx +0 -29
  99. package/dist/ui/elements/scatter.tsx +0 -42
  100. package/dist/ui/elements/scatter.works.ts +0 -38
  101. package/dist/ui/elements/select.tsx +0 -43
  102. package/dist/ui/elements/select.works.ts +0 -5
  103. package/dist/ui/elements/separator.tsx +0 -31
  104. package/dist/ui/elements/skeleton.tsx +0 -31
  105. package/dist/ui/elements/slider.tsx +0 -34
  106. package/dist/ui/elements/slider.works.ts +0 -5
  107. package/dist/ui/elements/sparkline.tsx +0 -37
  108. package/dist/ui/elements/sparkline.works.ts +0 -32
  109. package/dist/ui/elements/spinner.tsx +0 -33
  110. package/dist/ui/elements/stack.tsx +0 -29
  111. package/dist/ui/elements/stat.tsx +0 -36
  112. package/dist/ui/elements/stat.works.ts +0 -14
  113. package/dist/ui/elements/surface.tsx +0 -36
  114. package/dist/ui/elements/surface.works.ts +0 -9
  115. package/dist/ui/elements/table.tsx +0 -62
  116. package/dist/ui/elements/table.works.ts +0 -30
  117. package/dist/ui/elements/tabs.tsx +0 -47
  118. package/dist/ui/elements/tabs.works.ts +0 -84
  119. package/dist/ui/elements/text.tsx +0 -33
  120. package/dist/ui/elements/textarea.tsx +0 -38
  121. package/dist/ui/elements/textarea.works.ts +0 -5
  122. package/dist/ui/elements/time.tsx +0 -40
  123. package/dist/ui/elements/time.works.ts +0 -29
  124. package/dist/ui/elements/toggle.tsx +0 -34
  125. package/dist/ui/elements/toggle.works.ts +0 -5
  126. package/dist/ui/elements/tooltip.tsx +0 -40
  127. package/dist/ui/elements/tooltip.works.ts +0 -77
  128. package/dist/ui/elements/value.tsx +0 -38
  129. package/dist/ui/elements/value.works.ts +0 -9
  130. package/dist/ui/elements.ts +0 -127
  131. package/dist/ui/focus-scope.ts +0 -121
  132. package/dist/ui/fonts.ts +0 -355
  133. package/dist/ui/generators/dev.d.ts +0 -1
  134. package/dist/ui/generators/dev.js +0 -62
  135. package/dist/ui/generators/dev.ts +0 -75
  136. package/dist/ui/generators/elements.ts +0 -796
  137. package/dist/ui/generators/i18n.ts +0 -283
  138. package/dist/ui/generators/icons.ts +0 -207
  139. package/dist/ui/generators/react.ts +0 -267
  140. package/dist/ui/generators/routes.ts +0 -847
  141. package/dist/ui/generators/show.ts +0 -261
  142. package/dist/ui/generators/styles.ts +0 -677
  143. package/dist/ui/generators/variants.ts +0 -196
  144. package/dist/ui/i18n/catalogs/en.ts +0 -191
  145. package/dist/ui/i18n/format.ts +0 -221
  146. package/dist/ui/i18n/locales.ts +0 -33
  147. package/dist/ui/i18n/parse.ts +0 -315
  148. package/dist/ui/i18n/translator.ts +0 -31
  149. package/dist/ui/i18n/types.ts +0 -96
  150. package/dist/ui/icons.ts +0 -331
  151. package/dist/ui/islands/map.ts +0 -58
  152. package/dist/ui/localizations.ts +0 -75
  153. package/dist/ui/material.ts +0 -236
  154. package/dist/ui/materials/mat_bordered.ts +0 -13
  155. package/dist/ui/materials/mat_ceramic.ts +0 -13
  156. package/dist/ui/materials/mat_clay.ts +0 -13
  157. package/dist/ui/materials/mat_elevated.ts +0 -24
  158. package/dist/ui/materials/mat_flat.ts +0 -34
  159. package/dist/ui/materials/mat_glass.ts +0 -20
  160. package/dist/ui/materials/mat_lit.ts +0 -13
  161. package/dist/ui/materials/mat_moulded.ts +0 -11
  162. package/dist/ui/materials.ts +0 -197
  163. package/dist/ui/modes.ts +0 -44
  164. package/dist/ui/navigation.tsx +0 -29
  165. package/dist/ui/positioning.ts +0 -90
  166. package/dist/ui/presented.tsx +0 -119
  167. package/dist/ui/provider.tsx +0 -184
  168. package/dist/ui/render-loop.ts +0 -133
  169. package/dist/ui/roving-focus.ts +0 -161
  170. package/dist/ui/screens.ts +0 -141
  171. package/dist/ui/scroll-lock.ts +0 -28
  172. package/dist/ui/series.ts +0 -97
  173. package/dist/ui/signals.ts +0 -386
  174. package/dist/ui/styleguide.ts +0 -231
  175. package/dist/ui/styleguides.ts +0 -374
  176. package/dist/ui/themes.ts +0 -22
  177. package/dist/ui/token.ts +0 -78
  178. package/dist/ui/tokens/shadow.ts +0 -25
  179. package/dist/ui/tokens.ts +0 -234
  180. package/dist/ui/validators.ts +0 -232
  181. package/dist/ui/variant.ts +0 -349
  182. package/dist/ui/variants/var_avatar-round.ts +0 -14
  183. package/dist/ui/variants/var_bubble-me.ts +0 -14
  184. package/dist/ui/variants/var_bubble-them.ts +0 -13
  185. package/dist/ui/variants/var_button-border.ts +0 -76
  186. package/dist/ui/variants/var_button-cta.ts +0 -76
  187. package/dist/ui/variants/var_button-destructive-ghost.ts +0 -72
  188. package/dist/ui/variants/var_button-destructive.ts +0 -76
  189. package/dist/ui/variants/var_button-fill.ts +0 -76
  190. package/dist/ui/variants/var_button-ghost.ts +0 -72
  191. package/dist/ui/variants/var_button-glass.ts +0 -55
  192. package/dist/ui/variants/var_button-normal.ts +0 -76
  193. package/dist/ui/variants/var_button-quiet.ts +0 -74
  194. package/dist/ui/variants/var_card.ts +0 -17
  195. package/dist/ui/variants/var_chip.ts +0 -13
  196. package/dist/ui/variants/var_choice.ts +0 -15
  197. package/dist/ui/variants/var_framed.ts +0 -10
  198. package/dist/ui/variants/var_glyph-plain.ts +0 -11
  199. package/dist/ui/variants/var_input.ts +0 -43
  200. package/dist/ui/variants/var_link-inline.ts +0 -29
  201. package/dist/ui/variants/var_list-row.ts +0 -34
  202. package/dist/ui/variants/var_rule.ts +0 -10
  203. package/dist/ui/variants/var_skeleton-block.ts +0 -12
  204. package/dist/ui/variants/var_spinner-turning.ts +0 -12
  205. package/dist/ui/variants/var_stat-tile.ts +0 -13
  206. package/dist/ui/variants/var_tab-bar.ts +0 -15
  207. package/dist/ui/variants/var_tap-target.ts +0 -25
  208. package/dist/ui/variants.ts +0 -64
  209. package/dist/ui/wrappers.tsx +0 -429
@@ -1,677 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from '@prestack/thread/integrations/disk'
2
- import { dirname, join, relative, resolve } from 'node:path'
3
- import { argv, stderr, stdout } from 'node:process'
4
- import { fileURLToPath, pathToFileURL } from 'node:url'
5
-
6
- import { exit } from '@prestack/thread/core/invoked'
7
- import { findTree, inTree } from '@prestack/thread/core/tree'
8
- import { CHECKED_AT } from '@prestack/thread/operations/capabilities'
9
-
10
- import { keyframeName, shorthandOf, stopsOf, type Animation } from '../animations.ts'
11
- import { classOfRole, familyFor, fontStack, fontVariable, setNamed, settingsOf, type FontRole } from '../fonts.ts'
12
- import { sizeNamed, sizeRows, SIZE_PREFIX } from '../icons.ts'
13
- import { compose, keyframesOf, type Material } from '../material.ts'
14
- import { checkInteractions, knowSignals } from '../signals.ts'
15
- import { chosen, shifted, themedBy, type StyleGuide } from '../styleguide.ts'
16
- import { naming, nameOf, sheeted, type Holds, type Token } from '../token.ts'
17
- import { classOf, classOfComposition, classOfElement, classOfScreen, complete, statesOf, structural, under, undeclaredStates, unreacted, boundHeld, type Variant } from '../variant.ts'
18
-
19
- interface Row {
20
- readonly id: string
21
- readonly at?: string
22
- readonly token?: string
23
- readonly is?: string
24
- readonly asks?: Readonly<Record<string, string | number>>
25
- readonly wears?: readonly string[]
26
- readonly sets?: Readonly<Record<string, string>>
27
- readonly places?: Readonly<Record<string, Readonly<Record<string, string>>>>
28
- readonly modes?: Readonly<Record<string, string | Readonly<Record<string, string>>>>
29
- readonly themes?: Readonly<Record<string, { readonly is?: string; readonly modes?: Readonly<Record<string, string>> }>>
30
- readonly plays?: { readonly in?: string; readonly out?: string }
31
- readonly measures?: readonly string[]
32
- }
33
-
34
- interface Node {
35
- readonly id?: string
36
- readonly at?: string
37
- readonly declares?: readonly Row[]
38
- readonly holds?: readonly Row[]
39
- }
40
-
41
- function inside(node: Node): readonly Row[] {
42
- return [...(node.holds ?? []), ...(node.declares ?? [])]
43
- }
44
-
45
- const LAYERS = ['token', 'font', 'animation', 'material', 'element', 'variant', 'composition', 'screen'] as const
46
-
47
- const compare = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0)
48
-
49
- const exported = (id: string): string => id.replace(/-/g, '_').toUpperCase()
50
-
51
- function dashed(property: string): string {
52
- return property.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
53
- }
54
-
55
- function block(selector: string, made: Readonly<Record<string, Holds>>, indent: string): readonly string[] {
56
- const properties = Object.keys(made).sort(compare)
57
- if (properties.length === 0) return []
58
- const lines = [`${indent}${selector} {`]
59
- for (const property of properties) lines.push(`${indent} ${dashed(property)}: ${made[property] ?? ''};`)
60
- lines.push(`${indent}}`, '')
61
- return lines
62
- }
63
-
64
- function sheet(
65
- tokens: Readonly<Record<string, Token>>,
66
- guide: StyleGuide,
67
- materials: Readonly<Record<string, Material>>,
68
- variants: Readonly<Record<string, Variant>>,
69
- modes: readonly { readonly id: string; readonly is: string }[],
70
- themes: readonly { readonly id: string }[],
71
- screens: readonly Row[],
72
- compositions: readonly Row[],
73
- elements: readonly Row[],
74
- fonts: Node,
75
- icons: Node,
76
- animations: Readonly<Record<string, Animation>>,
77
- ): string {
78
- const wrong = checkInteractions()[0]
79
- if (wrong !== undefined) throw new Error(`${wrong.path}: ${wrong.message}`)
80
-
81
- const sizes = sizeRows(inside(icons))
82
- if (sizes.length === 0) {
83
- throw new Error(
84
- 'The icons sheet declares no size row, so nothing says how large a symbol is drawn. The style ' +
85
- 'guide holds no icon size — one row per size in `icons.ts`, holding a length.',
86
- )
87
- }
88
-
89
- const values = { ...chosen(guide, tokens), ...Object.fromEntries(sizes.map((one) => [one.id, sheeted(one.id, one.is)])) }
90
- const named = new Set(Object.keys(values))
91
- const themeIds = new Set(themes.map((theme) => theme.id))
92
-
93
- const kinds: Record<string, string> = {}
94
- for (const pick of guide.picks) kinds[pick.id] = pick.token
95
- for (const size of sizes) kinds[size.id] = 'icon'
96
-
97
- const played = (made: Readonly<Record<string, Holds>>): Readonly<Record<string, Holds>> => {
98
- const out: Record<string, Holds> = {}
99
- for (const [id, value] of Object.entries(made)) {
100
- if (kinds[id] !== 'animation') {
101
- out[id] = value
102
- continue
103
- }
104
- const animation = animations[value]
105
- if (animation === undefined) {
106
- const has = Object.keys(animations).sort().map((one) => `\`${one}\``)
107
- throw new Error(
108
- `\`${id}\` names the animation \`${value}\`, and no row declares one by that name. ` +
109
- (has.length === 0
110
- ? 'The app declares no animations.'
111
- : `The declared animations are ${has.join(', ')}.`),
112
- )
113
- }
114
- out[id] = sheeted(value, shorthandOf(animation, named))
115
- }
116
- return out
117
- }
118
-
119
- const variables = (
120
- made: Readonly<Record<string, Holds>>,
121
- selector: string,
122
- indent: string,
123
- ): readonly string[] => {
124
- const names = Object.keys(made).sort(compare)
125
- if (names.length === 0) return []
126
- const held = [`${indent}${selector} {`]
127
- for (const id of names) held.push(`${indent} ${nameOf(id)}: ${made[id] ?? ''};`)
128
- held.push(`${indent}}`, '')
129
- return held
130
- }
131
-
132
- const lines: string[] = [
133
- `@layer ${LAYERS.join(', ')};`,
134
- '',
135
- '@layer token {',
136
- ...variables(played(values), ':root', ' '),
137
- ]
138
-
139
- for (const mode of modes) {
140
- const held = variables(played(shifted(guide, tokens, mode.id, undefined, themeIds)), ':root', ' ')
141
- if (held.length === 0) continue
142
- lines.push(` @media ${mode.is} {`, ...held, ' }', '')
143
- }
144
-
145
- for (const theme of themes) {
146
- lines.push(...variables(played(shifted(guide, tokens, undefined, theme.id, themeIds)), themedBy(theme.id), ' '))
147
- for (const mode of modes) {
148
- const held = variables(played(shifted(guide, tokens, mode.id, theme.id, themeIds)), themedBy(theme.id), ' ')
149
- if (held.length === 0) continue
150
- lines.push(` @media ${mode.is} {`, ...held, ' }', '')
151
- }
152
- }
153
-
154
- const asked = modes.map((mode) => mode.id).join(' ')
155
- lines.push(
156
- ...(asked === '' ? [] : [' :root {', ` color-scheme: ${asked};`, ' }', '']),
157
- ' body {',
158
- ' background: var(--colour-page);',
159
- ' color: var(--colour-ink);',
160
- ' }',
161
- '',
162
- )
163
-
164
- lines.push('}', '')
165
-
166
- const type = typography(fonts)
167
- lines.push('@layer font {', ...type.lines, '}', '')
168
-
169
- lines.push('@layer animation {')
170
- for (const id of Object.keys(animations).sort(compare)) {
171
- const animation = animations[id]
172
- if (animation === undefined) continue
173
- lines.push(` @keyframes ${keyframeName(id)} {`)
174
- for (const stop of stopsOf(animation, named, tokens, kinds)) lines.push(...block(stop.stop, stop.made, ' '))
175
- lines.push(' }', '')
176
- }
177
- lines.push('}', '')
178
-
179
- const moving = new Map<
180
- string,
181
- {
182
- readonly by: string
183
- readonly stops: readonly { readonly stop: string; readonly made: Readonly<Record<string, Holds>> }[]
184
- }
185
- >()
186
- for (const id of Object.keys(materials).sort(compare)) {
187
- const material = materials[id]
188
- if (material === undefined) continue
189
- for (const one of keyframesOf(material, named, tokens, kinds)) {
190
- const already = moving.get(one.id)
191
- if (already !== undefined) {
192
- throw new Error(
193
- `\`${id}\` and \`${already.by}\` both declare keyframes called \`${one.id}\`, and a ` +
194
- 'stylesheet holds one rule per name, so whichever came last would silently win.',
195
- )
196
- }
197
- moving.set(one.id, { by: id, stops: one.stops })
198
- }
199
- }
200
-
201
- lines.push('@layer material {')
202
- for (const name of [...moving.keys()].sort(compare)) {
203
- lines.push(` @keyframes ${name} {`)
204
- for (const stop of moving.get(name)?.stops ?? []) lines.push(...block(stop.stop, stop.made, ' '))
205
- lines.push(' }', '')
206
- }
207
- for (const id of Object.keys(materials).sort(compare)) {
208
- const material = materials[id]
209
- if (material === undefined) continue
210
- const gap = undeclaredStates(material)[0]
211
- if (gap !== undefined) throw new Error(`\`${id}\`: ${gap}`)
212
- compose(material, named, tokens, kinds)
213
- under(material, named, tokens, kinds)
214
- }
215
- lines.push('}', '')
216
-
217
- lines.push('@layer variant {')
218
- for (const id of Object.keys(variants).sort(compare)) {
219
- const variant = variants[id]
220
- if (variant === undefined) continue
221
- const whole = statesOf(variant, materials)
222
- const first = undeclaredStates(whole)[0]
223
- if (first !== undefined) throw new Error(`\`${id}\`: ${first}`)
224
- const unclaimed = unreacted(variant, materials)[0]
225
- if (unclaimed !== undefined) throw new Error(unclaimed)
226
-
227
- let typed: Readonly<Record<string, Holds>> = {}
228
- if (variant.type !== undefined) {
229
- const role = type.roles.get(variant.type)
230
- if (role === undefined) {
231
- const has = [...type.roles.keys()].sort(compare).map((one) => `\`${one}\``)
232
- throw new Error(
233
- `\`${id}\` sets its wording in the \`${variant.type}\` role, and the fonts sheet declares ` +
234
- `no role by that name. The declared roles are ${has.join(', ')}.`,
235
- )
236
- }
237
- typed = role
238
- }
239
-
240
- const made = { ...typed, ...complete(variant, materials, named, tokens, kinds) }
241
- lines.push(...block(`.${classOf(id)}`, made, ' '))
242
- lines.push(...playing(`.${classOf(id)}`, variant.plays, named, id))
243
- lines.push(...symbols(id, variant.icon, named, made['display'] !== undefined))
244
- for (const state of under(whole, named, tokens, kinds)) {
245
- lines.push(...block(`.${classOf(id)}${state.marks}`, state.made, ' '))
246
- }
247
- }
248
- lines.push('}', '')
249
-
250
- for (const held of [
251
-
252
- { layer: 'element', named: classOfElement, rows: elements },
253
- { layer: 'composition', named: classOfComposition, rows: compositions },
254
- { layer: 'screen', named: classOfScreen, rows: screens },
255
- ]) {
256
- lines.push(`@layer ${held.layer} {`)
257
- for (const row of [...held.rows].sort((one, two) => compare(one.id, two.id))) {
258
- const selector = `.${held.named(row.id)}`
259
- lines.push(...block(selector, resolved(row.sets ?? {}, guide, row.id, row.measures), ' '))
260
- lines.push(...playing(selector, row.plays, named, row.id))
261
- for (const [id, made] of Object.entries(row.places ?? {})) {
262
- if (!PLACE.test(id)) {
263
- throw new Error(
264
- `\`${row.id}\` gives a place the name "${id}", and a place is named in the markup as \`at="${id}"\` ` +
265
- 'and becomes part of a class, so it has to be lowercase words joined by dashes. Anything else ' +
266
- 'produces a class the child can never match, and nothing would look wrong until you saw the screen.',
267
- )
268
- }
269
-
270
- const looks = Object.fromEntries(Object.entries(made).filter(([key]) => !SAID.has(key)))
271
- lines.push(...block(`${selector}-${id}`, resolved(looks, guide, `${row.id} place \`${id}\``, row.measures), ' '))
272
- }
273
- for (const [id, made] of Object.entries(row.modes ?? {})) {
274
- if (typeof made === 'string') continue
275
- const query = modes.find((one) => one.id === id)?.is
276
- if (query === undefined) {
277
- throw new Error(
278
- `\`${row.id}\` arranges itself under "${id}", and no mode by that name is declared, so nothing ` +
279
- 'can tell when it applies. A breakpoint is a mode — declare it beside dark and light, with the ' +
280
- 'width as its query.',
281
- )
282
- }
283
- lines.push(` @media ${query} {`, ...block(selector, resolved(made, guide, row.id), ' '), ' }', '')
284
- }
285
- }
286
- lines.push('}', '')
287
- }
288
-
289
- return lines.join('\n')
290
- }
291
-
292
- const LAID = 'layout-flex'
293
-
294
- const CENTRED = 'align-centre'
295
-
296
- function symbols(id: string, size: string | undefined, named: ReadonlySet<string>, laid: boolean): readonly string[] {
297
- if (size === undefined) return []
298
- const row = sizeNamed(size)
299
- if (!named.has(row)) {
300
- const has = [...named].filter((one) => one.startsWith(SIZE_PREFIX)).sort(compare).map((one) => `\`${one}\``)
301
- throw new Error(
302
- `\`${id}\` draws the symbol inside it at \`${row}\`, and the icons sheet declares no size by ` +
303
- `that name. The declared sizes are ${has.join(', ')}.`,
304
- )
305
- }
306
- const drawn = naming(named, row, id, 'the size its symbol is drawn at')
307
-
308
- const centred =
309
- laid || !named.has(LAID) || !named.has(CENTRED)
310
- ? []
311
- : block(
312
- `.${classOf(id)}`,
313
- {
314
- display: naming(named, LAID, id, 'how what it holds is laid out'),
315
- alignItems: naming(named, CENTRED, id, 'where its symbol sits across that'),
316
- },
317
- ' ',
318
- )
319
-
320
- return [
321
- ...centred,
322
- ...block(`.${classOf(id)} > svg`, { width: drawn, height: drawn, minWidth: drawn, minHeight: drawn }, ' '),
323
- ]
324
- }
325
-
326
- function typography(node: Node): {
327
- readonly lines: readonly string[]
328
- readonly roles: ReadonlyMap<string, Readonly<Record<string, Holds>>>
329
- } {
330
- const rows = inside(node)
331
- const roles = new Map<string, Readonly<Record<string, Holds>>>()
332
-
333
- const picked = rows.find((row) => row.id === 'family-set')
334
- if (picked?.is === undefined) {
335
- throw new Error(
336
- 'The fonts sheet declares no `family-set` row, so nothing says which set of families the app ' +
337
- 'draws from. One row, naming a registered set — every face is drawn from it.',
338
- )
339
- }
340
- const set = setNamed(picked.is)
341
-
342
- const faces: Record<string, string> = {}
343
- const classes: string[] = []
344
-
345
- for (const row of rows) {
346
- if (row.id === 'family-set') continue
347
- if (row.sets === undefined) {
348
- throw new Error(
349
- `\`${row.id}\` is declared a font role and states nothing about how it is set. A role names a ` +
350
- 'face and states its size, weight, leading and tracking.',
351
- )
352
- }
353
- const role = row.sets as unknown as FontRole
354
- const family = familyFor(set, role.face, row.id)
355
- faces[role.face] = fontStack(family)
356
-
357
- const made = settingsOf(row.id, role)
358
- roles.set(row.id, made)
359
-
360
- classes.push(...block(`.${classOfRole(row.id)}`, made, ' '))
361
-
362
- }
363
-
364
- const held = [' :root {']
365
- for (const face of Object.keys(faces).sort(compare)) held.push(` ${fontVariable(face)}: ${faces[face] ?? ''};`)
366
- held.push(' }', '')
367
-
368
- return { lines: [...held, ...classes], roles }
369
- }
370
-
371
- function playing(
372
- selector: string,
373
- plays: { readonly in?: string; readonly out?: string } | undefined,
374
- named: ReadonlySet<string>,
375
- by: string,
376
- ): readonly string[] {
377
- const held: string[] = []
378
- for (const [moment, row, on] of [
379
- ['in', plays?.in, `${selector}[data-arriving]`],
380
- ['out', plays?.out, `${selector}[data-leaving]`],
381
- ] as const) {
382
- if (row === undefined) continue
383
- if (!named.has(row)) {
384
- throw new Error(
385
- `\`${by}\` plays \`${row}\` on the way ${moment}, and the style guide holds no row by that name. ` +
386
- 'What a thing plays is a style guide value like any other — name a row, and the row names the animation.',
387
- )
388
- }
389
- held.push(...block(on, { animation: naming(named, row, by, `what it plays on the way ${moment}`) }, ' '))
390
- }
391
- return held
392
- }
393
-
394
- const PLACE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/
395
-
396
- const SAID = structural
397
-
398
- const RAW = /\d\s*(px|rem|em|ch|vh|vw)\b|\d%|#[0-9a-fA-F]{3,8}\b|\brgba?\(|\bhsla?\(/
399
-
400
- function resolved(
401
- sets: Readonly<Record<string, string>>,
402
- guide: StyleGuide,
403
- by: string,
404
- measures: readonly string[] = [],
405
- ): Readonly<Record<string, Holds>> {
406
- const made: Record<string, Holds> = {}
407
- const bound = boundHeld()
408
- for (const [property, whole] of Object.entries(sets)) {
409
-
410
- const words = whole.trim().split(/\s+/)
411
- const held: string[] = []
412
- for (const value of words) {
413
- if (measures.includes(value)) {
414
- held.push(`var(--${value})`)
415
- continue
416
- }
417
- if (RAW.test(value)) {
418
- throw new Error(
419
- `\`${by}\` sets ${property} to "${value}", and that is a value written where a name belongs. ` +
420
- 'An arrangement names a style guide row and the row holds the value — so the whole app moves ' +
421
- 'when the row moves. Add a row for it, or name one that exists.',
422
- )
423
- }
424
- const row = guide.picks.find((one) => one.id === value)
425
- if (row === undefined) {
426
- throw new Error(
427
- `\`${by}\` sets ${property} to "${value}", and that names neither a style guide row nor one ` +
428
- "of this element's measures. A name that is neither is written into the stylesheet as-is " +
429
- 'and thrown away by the browser. Declare it in `measures`, or name a row.',
430
- )
431
- }
432
-
433
- const kinds = bound[property]
434
- if (kinds !== undefined && !kinds.includes(String(row.token))) {
435
- throw new Error(
436
- `\`${by}\` sets ${property} to \`${value}\`, which is a \`${String(row.token)}\` row. ` +
437
- `${property} takes ${kinds.map((one) => `\`${one}\``).join(' or ')}. A row of the wrong kind ` +
438
- 'is written into the stylesheet and dropped by the browser without a word.',
439
- )
440
- }
441
-
442
- held.push(`var(--${value})`)
443
- }
444
- made[property] = sheeted(whole, held.join(' '))
445
- }
446
- return made
447
- }
448
-
449
- async function fileFor<T>(root: string, at: string, id: string, what: string): Promise<T> {
450
- const module = (await import(pathToFileURL(resolve(root, at)).href)) as Record<string, unknown>
451
- const one = module[exported(id)]
452
- if (one === undefined) {
453
- throw new Error(
454
- `${what} \`${id}\` is declared at ${at}, and that file exports no \`${exported(id)}\`. ` +
455
- 'A row names a file, and the file is the thing.',
456
- )
457
- }
458
- return one as T
459
- }
460
-
461
- async function loaded<T>(root: string, node: Node, what: string): Promise<Readonly<Record<string, T>>> {
462
- const found: Record<string, T> = {}
463
- for (const row of inside(node)) {
464
- if (row.at === undefined) {
465
- throw new Error(`${what} \`${row.id}\` names no file, so there is nothing to read it from.`)
466
- }
467
- found[row.id] = await fileFor<T>(root, row.at, row.id, what)
468
- }
469
- return found
470
- }
471
-
472
- async function tokensIn(root: string, node: Node): Promise<Readonly<Record<string, Token>>> {
473
- const found: Record<string, Token> = {}
474
- for (const row of inside(node)) {
475
- found[row.id] =
476
- row.at === undefined || row.at === node.at
477
- ? { id: row.id }
478
- : await fileFor<Token>(root, row.at, row.id, 'token')
479
- }
480
- return found
481
- }
482
-
483
- function guideIn(node: Node): StyleGuide {
484
- return {
485
- id: node.id ?? 'styleguide',
486
- picks: inside(node).map((row) => {
487
- if (row.token === undefined) {
488
- throw new Error(
489
- `\`${row.id}\` names no token. A style guide row is one value OF a token, so it has to ` +
490
- 'say which token it is one of.',
491
- )
492
- }
493
- return {
494
- id: row.id,
495
-
496
- token: row.token as StyleGuide['picks'][number]['token'],
497
- ...(row.is === undefined ? {} : { is: row.is }),
498
- ...(row.asks === undefined ? {} : { asks: row.asks }),
499
- ...(row.modes === undefined
500
- ? {}
501
- : {
502
- modes: Object.fromEntries(
503
- Object.entries(row.modes).filter((one): one is [string, string] => typeof one[1] === 'string'),
504
- ),
505
- }),
506
-
507
- ...(row.themes === undefined ? {} : { themes: row.themes }),
508
- }
509
- }),
510
- }
511
- }
512
-
513
- function pascal(id: string): string {
514
- return id
515
- .split('-')
516
- .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
517
- .join('')
518
- }
519
-
520
- function unwearable(variants: Node, elements: Node): readonly string[] {
521
- const declared = new Set((elements.declares ?? []).map((row) => pascal(row.id)))
522
- const missing: string[] = []
523
- for (const row of variants.declares ?? []) {
524
- for (const element of row.wears ?? []) {
525
- if (declared.has(element)) continue
526
- missing.push(
527
- `\`${row.id}\` says \`${element}\` accepts it, and no element by that name is declared, ` +
528
- 'so nothing would ever wear it.',
529
- )
530
- }
531
- }
532
- return missing
533
- }
534
-
535
- function modesIn(node: Node): readonly { readonly id: string; readonly is: string }[] {
536
- return inside(node).map((row) => {
537
- if (row.is === undefined) {
538
- throw new Error(
539
- `\`${row.id}\` is declared a mode and states no query, so nothing could tell when it applies. ` +
540
- 'A mode is the question a device asks — give it the media query that answers it.',
541
- )
542
- }
543
- return { id: row.id, is: row.is }
544
- })
545
- }
546
-
547
- const OWN_AT = '../..'
548
-
549
- async function node(root: string, file: string, name: string): Promise<Node> {
550
- const own = argv.includes('--own')
551
- const at = own ? resolve(fileURLToPath(import.meta.url), OWN_AT, file) : inTree(findTree(), file)
552
- if (!own && !existsSync(at)) {
553
- stdout.write(`styles: this tree has no ${findTree().name}/${file}, so it declares nothing to style and there is no stylesheet to write or check.\n`)
554
- exit(0)
555
- }
556
- const module = (await import(pathToFileURL(at).href)) as Record<string, unknown>
557
- const one = module[own ? `${name}S` : name]
558
- if (one === undefined) {
559
- throw new Error(
560
- own ? `this package's own ${file} exports no \`${name}S\`.` : `_threads/${file} exports no \`${name}\`.`,
561
- )
562
- }
563
- return one as Node
564
- }
565
-
566
- const STYLESHEET = 'app/_generated/tokens.css'
567
-
568
- const DECLARED = 'app/_generated/declared.ts'
569
-
570
- async function run(): Promise<number> {
571
- const own = argv.includes('--own')
572
- const root = own ? resolve(fileURLToPath(import.meta.url), '../../../../..') : findTree().at
573
-
574
- knowSignals(inside(await node(root, 'signals.ts', 'SIGNAL')).map((one) => one.id))
575
-
576
- const tokens = await tokensIn(root, await node(root, 'tokens.ts', 'TOKEN'))
577
- const guide = guideIn(await node(root, 'styleguides.ts', 'STYLEGUIDE'))
578
- const materials = await loaded<Material>(root, await node(root, 'materials.ts', 'MATERIAL'), 'material')
579
- const declaring = await node(root, 'variants.ts', 'VARIANT')
580
- const variants = await loaded<Variant>(root, declaring, 'variant')
581
-
582
- const unworn = unwearable(declaring, await node(root, 'elements.ts', 'ELEMENT'))[0]
583
- if (unworn !== undefined) throw new Error(unworn)
584
-
585
- const fonts = await node(root, 'fonts.ts', 'FONT')
586
- const icons = await node(root, `icons.${'ts'}`, 'ICON')
587
- const animations = await loaded<Animation>(root, await node(root, 'animations.ts', 'ANIMATION'), 'animation')
588
-
589
- const modes = modesIn(await node(root, 'modes.ts', 'MODE'))
590
- const themes = inside(await node(root, 'themes.ts', 'THEME'))
591
- const screens = (await node(root, 'screens.ts', 'SCREEN')).declares ?? []
592
- const compositions = (await node(root, 'compositions.ts', 'COMPOSITION')).declares ?? []
593
-
594
- const elements = ((await node(root, 'elements.ts', 'ELEMENT')).declares ?? []).filter(
595
- (one) => one.sets !== undefined || one.places !== undefined,
596
- )
597
-
598
- const camelOf = (id: string): string => id.replace(/-([a-z0-9])/g, (_, c: string) => c.toUpperCase())
599
-
600
- const worn = ((declaring.declares ?? []) as readonly { readonly id?: string; readonly wears?: readonly string[] }[])
601
- .filter((one) => typeof one.id === 'string' && (one.wears ?? []).length > 0)
602
- .map((one) => ` readonly ${camelOf(String(one.id))}: ${(one.wears ?? []).map((e) => `'${e}'`).join(' | ')}`)
603
-
604
- const asking = (((await node(root, 'signals.ts', 'SIGNAL')).holds ?? []) as readonly {
605
- readonly id?: string
606
- readonly selector?: string
607
- readonly when?: string
608
- }[]).filter((one) => typeof one.id === 'string')
609
-
610
- const checked = `./${relative(dirname(DECLARED), CHECKED_AT)}`
611
-
612
- const wearingSource = [
613
- `import type {} from '${checked}'`,
614
- '',
615
- `declare module '${checked}' {`,
616
- ...(worn.length === 0 ? [' interface Wearing {}'] : [' interface Wearing {', ...worn, ' }']),
617
- '}',
618
- '',
619
- 'export const ASKING = [',
620
- ...asking.map(
621
- (one) =>
622
- ` { id: '${String(one.id)}', selector: '${one.selector ?? `[data-${String(one.id)}]`}'${typeof one.when === 'string' ? `, when: '${one.when}'` : ''} },`,
623
- ),
624
- '] as const',
625
- '',
626
- ].join('\n')
627
-
628
- const at = join(root, STYLESHEET)
629
- const made = sheet(tokens, guide, materials, variants, modes, themes, screens, compositions, elements, fonts, icons, animations)
630
-
631
- if (own) {
632
- stdout.write(
633
- `styles: this framework's own declarations build a stylesheet — ${String(guide.picks.length)} values, ` +
634
- `${String(Object.keys(materials).length)} materials, ${String(Object.keys(variants).length)} variants, ` +
635
- `${String(Object.keys(animations).length)} animations, every one naming rows that exist. ` +
636
- 'Nothing is written: a stylesheet belongs to an app, and building it is the check.\n',
637
- )
638
- return 0
639
- }
640
-
641
- if (argv.includes('--check')) {
642
- const drifted = [
643
- { named: STYLESHEET, text: made },
644
- { named: DECLARED, text: wearingSource },
645
- ].filter((one) => {
646
- try {
647
- return readFileSync(join(root, one.named), 'utf8') !== one.text
648
- } catch {
649
- return true
650
- }
651
- })
652
- if (drifted.length === 0) {
653
- stdout.write(`styles: ${STYLESHEET} and ${DECLARED} are the bytes the declarations produce\n`)
654
- return 0
655
- }
656
- stderr.write(
657
- `styles: ${drifted.map((one) => one.named).join(' and ')} ${drifted.length === 1 ? 'is' : 'are'} not what the declarations produce. ` +
658
- 'A derived file that differs from its generator is a hand edit wearing a generated name, and every reader ' +
659
- 'downstream believes the wrong one. Run `thread styles` and commit what it writes.\n',
660
- )
661
- return 1
662
- }
663
-
664
- writeFileSync(at, made, 'utf8')
665
- writeFileSync(join(root, DECLARED), wearingSource, 'utf8')
666
-
667
- stdout.write(
668
- `thread: wrote ${STYLESHEET} and ${DECLARED} — ${String(Object.keys(tokens).length)} tokens, ` +
669
- `${String(guide.picks.length)} values, ${String(Object.keys(materials).length)} materials, ` +
670
- `${String(Object.keys(variants).length)} variants, ${String(inside(fonts).length - 1)} font roles, ` +
671
- `${String(Object.keys(animations).length)} animations, ${String(modes.length)} modes, ` +
672
- `${String(themes.length)} themes, ${String(compositions.length)} compositions, ${String(screens.length)} screens\n`,
673
- )
674
- return 0
675
- }
676
-
677
- if (argv[1] !== undefined) exit(await run())