@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,317 @@
1
+ import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs'
2
+ import { basename, dirname, join } from 'node:path'
3
+ import { argv, stdout } from 'node:process'
4
+ import { pathToFileURL } from 'node:url'
5
+
6
+ import { exit, invoked } from '@prestack/thread/core/invoked'
7
+ import type { Threaded } from '@prestack/thread/core/thread'
8
+ import { findTree, inTree, strayFiles } from '@prestack/thread/core/tree'
9
+
10
+ import { held, sheeted, type Holds, type Token } from './token.ts'
11
+
12
+ const here = 'thread/ui/animations.ts'
13
+
14
+ export const BOUND = {
15
+ opacity: ['opacity'],
16
+ transform: ['transform'],
17
+ filter: ['filter'],
18
+ backdropFilter: ['blur'],
19
+ background: ['colour', 'gradient'],
20
+ color: ['colour'],
21
+ borderColor: ['colour'],
22
+ boxShadow: ['shadow'],
23
+ } as const
24
+
25
+ type Property = keyof typeof BOUND
26
+
27
+ type SettingFor<P extends Property> = {
28
+ readonly property: P
29
+ readonly is?: string
30
+ readonly token?: (typeof BOUND)[P][number]
31
+ readonly asks?: Readonly<Record<string, string | number>>
32
+ }
33
+
34
+ export type AnimationSetting = { [P in Property]: SettingFor<P> }[Property]
35
+
36
+ export interface Animation {
37
+ readonly id: string
38
+
39
+ readonly runs: string
40
+
41
+ readonly eases: string
42
+
43
+ readonly repeats?: number | 'infinite'
44
+
45
+ readonly fills?: 'none' | 'forwards' | 'backwards' | 'both'
46
+
47
+ readonly stops: Readonly<Record<string, readonly AnimationSetting[]>>
48
+ }
49
+
50
+ export function keyframeName(id: string): string {
51
+ return `anm-${id}`
52
+ }
53
+
54
+ function resolve(
55
+ animation: Animation,
56
+ set: AnimationSetting,
57
+ value: string,
58
+ named: ReadonlySet<string>,
59
+ kinds: Readonly<Record<string, string>>,
60
+ ): string {
61
+ if (!named.has(value)) {
62
+ throw new Error(
63
+ `\`${animation.id}\` sets ${set.property} to "${value}", and the style guide holds no row by ` +
64
+ 'that name. An animation composes what the style guide holds, or it composes nothing.',
65
+ )
66
+ }
67
+ const kind = kinds[value]
68
+ const accepted: readonly string[] = BOUND[set.property]
69
+ if (kind !== undefined && !accepted.includes(kind)) {
70
+ throw new Error(
71
+ `\`${animation.id}\` sets ${set.property} to \`${value}\`, and that row is a \`${kind}\`. ` +
72
+ `${set.property} takes ${accepted.map((one) => `\`${one}\``).join(' or ')}.`,
73
+ )
74
+ }
75
+ return `var(--${value})`
76
+ }
77
+
78
+ function valued(
79
+ animation: Animation,
80
+ set: AnimationSetting,
81
+ named: ReadonlySet<string>,
82
+ tokens: Readonly<Record<string, Token>>,
83
+ kinds: Readonly<Record<string, string>>,
84
+ ): Holds {
85
+ if (set.token !== undefined) {
86
+ const token = tokens[set.token]
87
+ if (token?.makes === undefined) {
88
+ throw new Error(
89
+ `\`${animation.id}\` asks \`${set.token}\` for ${set.property}, and that token carries no rule ` +
90
+ 'to make a value from. Name a style guide row instead, or give the token a rule.',
91
+ )
92
+ }
93
+ return sheeted(set.token, token.makes(set.asks ?? {}))
94
+ }
95
+ if (set.is === undefined) {
96
+ throw new Error(
97
+ `\`${animation.id}\` states ${set.property} with neither a value nor a token to make one from.`,
98
+ )
99
+ }
100
+ return held(named, resolve(animation, set, set.is, named, kinds), animation.id)
101
+ }
102
+
103
+ export function stopsOf(
104
+ animation: Animation,
105
+ named: ReadonlySet<string>,
106
+ tokens: Readonly<Record<string, Token>>,
107
+ kinds: Readonly<Record<string, string>>,
108
+ ): readonly { readonly stop: string; readonly made: Readonly<Record<string, Holds>> }[] {
109
+ return Object.keys(animation.stops).map((stop) => {
110
+ const made: Record<string, Holds> = {}
111
+ for (const set of animation.stops[stop] ?? []) made[set.property] = valued(animation, set, named, tokens, kinds)
112
+ return { stop, made }
113
+ })
114
+ }
115
+
116
+ export function shorthandOf(animation: Animation, named: ReadonlySet<string>): string {
117
+ for (const [what, row] of [
118
+ ['runs', animation.runs],
119
+ ['eases', animation.eases],
120
+ ] as const) {
121
+ if (named.has(row)) continue
122
+ throw new Error(
123
+ `\`${animation.id}\` ${what} \`${row}\`, and the style guide holds no row by that name. ` +
124
+ 'How long an animation takes and how it is paced are style guide values, named here rather than written.',
125
+ )
126
+ }
127
+ return [
128
+ keyframeName(animation.id),
129
+ `var(--${animation.runs})`,
130
+ `var(--${animation.eases})`,
131
+ ...(animation.repeats === undefined ? [] : [String(animation.repeats)]),
132
+ ...(animation.fills === undefined ? [] : [animation.fills]),
133
+ ].join(' ')
134
+ }
135
+
136
+ const one = (id: string, describes: string): Threaded => ({
137
+ id,
138
+ at: `thread/ui/animations/anm_${id.replace(/-/g, '_')}.ts`,
139
+ describes,
140
+ version: 1,
141
+ })
142
+
143
+ export const declares: readonly Threaded[] = [
144
+ one('press', 'A thing shrinking very slightly and coming back, which is what makes a press feel like it landed rather than like a value changed.'),
145
+ one('appear', 'Something arriving in place — faded up from nothing, without moving, for a thing that was always going to be there.'),
146
+ one('dismiss', 'Something leaving — faded away and shrunk a little, so the eye is not asked to follow it out.'),
147
+ one('rise', 'Something arriving from below, faded up as it comes. What a sheet, a toast or a menu does on its way in.'),
148
+ one('fall', 'Something leaving downward, the exit that answers a rise.'),
149
+ one('pulse', 'A thing breathing in place, repeated without end, for something waiting rather than working.'),
150
+ one('spin', 'A full turn, repeated without end. What says a thing is working and cannot say how far along it is.'),
151
+ one('drop', 'Something arriving from above, faded in as it comes. The entrance that answers a rise.'),
152
+ one('slide-in-left', 'Something coming in from the left, for a panel or a step arriving from off-stage rather than growing in place.'),
153
+ one('slide-in-right', 'Something coming in from the right.'),
154
+ one('slide-out-left', 'Something leaving to the left.'),
155
+ one('slide-out-right', 'Something leaving to the right.'),
156
+ one('zoom-in', 'Something arriving from nearer the reader and settling back to its true size.'),
157
+ one('zoom-out', 'Something leaving toward the reader as it fades.'),
158
+ one('pop-in', 'Something arriving past its own size and coming back — the overshoot is what reads as physical rather than as a value changing.'),
159
+ one('pop-out', 'Something swelling once before it goes, the exit that answers a pop.'),
160
+ one('blur-in', 'Something sharpening into place out of softness, without moving at all.'),
161
+ one('blur-out', 'Something dissolving rather than leaving.'),
162
+ one('lift', 'A thing raised a little off the page while it is pointed at. The plainest way a surface says it is reachable.'),
163
+ one('swell', 'A thing grown very slightly while pointed at, for where lifting it would move its neighbours.'),
164
+ one('nudge', 'A marker leaning the way it would take you, played while the wording beside it is pointed at.'),
165
+ one('wobble', 'A thing turned one way and back to be noticed. Says look at me without saying something is wrong.'),
166
+ one('flash', 'A thing dipping and returning, for a value that changed underneath the reader.'),
167
+ one('glow', 'The light around a thing swelling and settling, for something lit rather than framed.'),
168
+ one('shake', 'A short refusal — a thing pushed off centre and back twice, for an answer that was not accepted.'),
169
+ ]
170
+
171
+ export const ANIMATIONS = {
172
+ id: 'animation',
173
+ at: here,
174
+ files: 'ui/anm_*.ts',
175
+ describes:
176
+ 'Every animation an app may play, and the only place one may be brought into existence. A row names a file prefixed `anm_`, and that file is the animation: the stops it moves through, how long it takes and how it is paced — all of it naming style guide rows rather than holding values, so an animation moves when the rows move. Naming one is the whole of using one: a variant that plays `press` says `press`, and states no duration, no easing and no keyframes. The style guide picks which animation is the default for a thing, the way it picks every other value. A file under animations that no row names is not an animation but a file somebody wrote.',
177
+ version: 1,
178
+ cites: [
179
+ {
180
+ on: '2026-08-28',
181
+ said: 'Animations.ts, declares rows (through threaded) that names animations files at a path, with a prefix anm_ or ani_ or we do motions.ts with mot_, don\'t know. These define a bunch of animations that style guide will pick for a bunch of things as defaults. Like these template animations we see in video editors, we just name them and it\'s done.',
182
+ },
183
+ ],
184
+ declares,
185
+ } satisfies Threaded & { readonly files: string; readonly declares: readonly Threaded[] }
186
+
187
+ export function nameOf(id: string): string {
188
+ return id.replace(/-/g, '_').toUpperCase()
189
+ }
190
+
191
+ function starter(row: Threaded): string {
192
+ return `import type { Animation } from './animations.ts'
193
+
194
+ export const ${nameOf(row.id)} = {
195
+ id: '${row.id}',
196
+ runs: 'motion-normal',
197
+ eases: 'ease-out',
198
+ stops: {},
199
+ } as const satisfies Animation
200
+ `
201
+ }
202
+
203
+ export function seeds(
204
+ rows: readonly Threaded[],
205
+ has: (at: string) => boolean,
206
+ ): readonly { readonly path: string; readonly content: string }[] {
207
+ return rows.filter((row) => !has(row.at)).map((row) => ({ path: row.at, content: starter(row) }))
208
+ }
209
+
210
+ export async function mismatched(
211
+ rows: readonly Threaded[],
212
+ loader: (row: Threaded) => Promise<{ readonly id?: string } | undefined>,
213
+ ): Promise<readonly string[]> {
214
+ const found: string[] = []
215
+ for (const row of rows) {
216
+ const animation = await loader(row)
217
+ if (animation === undefined) {
218
+ found.push(
219
+ `\`${row.id}\` is declared at \`${row.at}\`, and that file exports no \`${nameOf(row.id)}\`. ` +
220
+ 'A row names a file, and the file is the thing.',
221
+ )
222
+ continue
223
+ }
224
+ if (animation.id !== row.id) {
225
+ found.push(
226
+ `\`${row.at}\` declares itself \`${String(animation.id)}\`, and the row that names it calls it \`${row.id}\`. ` +
227
+ "An animation file and the row that names it have to be the same animation — make the file's `id` match the row, or fix the row.",
228
+ )
229
+ }
230
+ }
231
+ return found
232
+ }
233
+
234
+ function extensionOf(path: string): string {
235
+ const name = basename(path)
236
+ const at = name.lastIndexOf('.')
237
+ return at === -1 ? '' : name.slice(at)
238
+ }
239
+
240
+ const ANM_PREFIX = /^anm_/
241
+
242
+ export function orphaned(rows: readonly Threaded[], strays: readonly string[]): readonly string[] {
243
+ const named = new Set(rows.map((row) => row.at))
244
+ return strays
245
+ .filter((one) => !named.has(one))
246
+ .map(
247
+ (one) =>
248
+ `\`${one}\` is under animations and no row names it — not a animation, a file somebody wrote. ` +
249
+ 'Add a row for it, or delete the file.',
250
+ )
251
+ }
252
+
253
+ async function run(): Promise<number> {
254
+ const tree = findTree()
255
+ const at = inTree(tree, 'animations.ts')
256
+ if (!existsSync(at)) {
257
+ stdout.write(`thread: this tree has no ${tree.name}/${basename(at)}, so it declares no animation and there is nothing to seed or check.\n`)
258
+ return 0
259
+ }
260
+ const sheet = (await import(pathToFileURL(at).href)) as {
261
+ ANIMATION?: { readonly declares?: readonly Threaded[] }
262
+ }
263
+
264
+ if (sheet.ANIMATION === undefined) {
265
+ stdout.write(`thread: ${tree.name}/animations.ts exports no \`ANIMATION\`, so nothing declares an animation here.\n`)
266
+ return 1
267
+ }
268
+
269
+ const rows = sheet.ANIMATION.declares ?? []
270
+ const checking = argv.includes('--check')
271
+ const exists = (where: string): boolean => existsSync(join(tree.at, where))
272
+ const made = seeds(rows, exists)
273
+
274
+ if (checking) {
275
+ const unseeded = rows.find((row) => !exists(row.at))
276
+ if (unseeded !== undefined) {
277
+ stdout.write(
278
+ `thread: \`${unseeded.id}\` is declared at \`${unseeded.at}\`, and the file is not there yet. ` +
279
+ 'A row names a file, and `--check` never writes one — run `thread animations` and commit what it seeds.\n',
280
+ )
281
+ return 1
282
+ }
283
+ } else {
284
+ for (const seed of made) {
285
+ mkdirSync(dirname(join(tree.at, seed.path)), { recursive: true })
286
+ writeFileSync(join(tree.at, seed.path), seed.content, 'utf8')
287
+ stdout.write(`thread: seeded ${seed.path}\n`)
288
+ }
289
+ }
290
+
291
+ const wrong = (
292
+ await mismatched(rows, async (row) => {
293
+ const module = (await import(pathToFileURL(join(tree.at, row.at)).href)) as Record<string, unknown>
294
+ return module[nameOf(row.id)] as { readonly id?: string } | undefined
295
+ })
296
+ )[0]
297
+ if (wrong !== undefined) {
298
+ stdout.write(`thread: ${wrong}\n`)
299
+ return 1
300
+ }
301
+
302
+ const stray = orphaned(rows, strayFiles(tree.at, 'anm_', '.' + 'ts'))[0]
303
+ if (stray !== undefined) {
304
+ stdout.write(`thread: ${stray}\n`)
305
+ return 1
306
+ }
307
+
308
+ stdout.write(
309
+ checking
310
+ ? `thread: ${String(rows.length)} animation(s) declared, all seeded\n`
311
+ : `thread: ${String(rows.length)} animation(s) declared, ${String(made.length)} seeded, ` +
312
+ `${String(rows.length - made.length)} already written\n`,
313
+ )
314
+ return 0
315
+ }
316
+
317
+ if (invoked(import.meta.url)) exit(await run())
package/dist/ui/attach.js CHANGED
@@ -102,7 +102,7 @@ const SHIPPED = [
102
102
  into: 'ui.ts',
103
103
  list: 'GENERATORS',
104
104
  id: 'compositions-generator',
105
- source: " {\n id: 'compositions-generator',\n at: 'node_modules/@prestack/thread-ui/dist/ui/compositions.js',\n describes: 'The seeder of composition files — it reads the compositions an app declares and writes the `cmp_` file each row names when that file is not there yet, so a composition begins by being declared rather than by somebody remembering to add a file beside the row. Run with `--check`, the mode the gate runs it in, it writes nothing and refuses instead: a row whose file is absent, a `cmp_` file no row names, and an import that is not a declared element, a declared composition or the door — so a composition that did not come from the CLI, or that reaches something nothing declares, stops the build rather than rendering.',\n version: 1,\n by: 'thread compositions',\n runs: 'thread compositions --check',\n },",
105
+ source: " {\n id: 'compositions-generator',\n at: 'node_modules/@prestack/thread-ui/dist/ui/compositions.js',\n describes: 'The seeder of composition files — it reads the compositions an app declares and writes the `cmp_` file each row names when that file is not there yet, so a composition begins by being declared rather than by somebody remembering to add a file beside the row. Run with `--check`, the mode the gate runs it in, it writes nothing and refuses instead: a row whose file is absent, a `cmp_` file no row names, and an import that is not a declared element, a declared composition or the door — so a composition that did not come from the CLI, or that reaches something nothing declares, stops the build rather than rendering. It also writes each of them compiled, into `app/_generated/compiled` — a variant worn as a tag becomes the class it resolves to and a named place becomes the class its arrangement declares, so a page imports a file this framework wrote rather than one a bundler plugin rewrites on the way past.',\n version: 1,\n by: 'thread compositions',\n runs: 'thread compositions --check',\n derives: [\n {\n id: 'compiled',\n at: 'app/_generated/compiled',\n describes: 'Every composition and screen as React runs them, written from the files a person wrote: a variant worn as a tag has become the class it resolves to, and a named place has become the class its arrangement declares. Nobody edits anything here, and nothing reads the sources at run time — this is what a page imports, which is why no compiler of this framework has to be installed for the app to build.',\n version: 1,\n by: 'thread compositions',\n runs: 'thread compositions --check',\n },\n ],\n },",
106
106
  },
107
107
  {
108
108
  into: 'ui.ts',
@@ -222,7 +222,7 @@ function quoted(value) {
222
222
  return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
223
223
  }
224
224
  function installed(at) {
225
- return at.replace(/^thread\/ui\/(.*)\.tsx?$/, `${ATTACH_HOME}/$1.js`);
225
+ return at.replace(/^thread\/(.*)\.tsx?$/, `${ATTACH_HOME}/$1.js`);
226
226
  }
227
227
  function literal(value) {
228
228
  return typeof value === 'number' ? String(value) : quoted(value);
@@ -246,7 +246,7 @@ function themed(held) {
246
246
  return `{ ${each.join(', ')} }`;
247
247
  }
248
248
  const WRITTEN = new Set([
249
- 'id', 'at', 'describes', 'version', 'derives', 'holds',
249
+ 'id', 'at', 'describes', 'version', 'derives', 'holds', 'runs',
250
250
  'section', 'token', 'is', 'asks', 'sets', 'modes', 'places', 'themes', 'wears', 'selector',
251
251
  'arranges', 'plays',
252
252
  ]);
@@ -309,6 +309,7 @@ function row(one, file, sheet) {
309
309
  ' ],',
310
310
  ]),
311
311
  ...(held ? [] : [` at: ${quoted(installed(one.at ?? ''))},`]),
312
+ ...(one.runs === undefined ? [] : [` runs: ${quoted(one.runs.split(' --own').join(''))},`]),
312
313
  ` describes: ${quoted(one.describes)},`,
313
314
  ' version: 1,',
314
315
  ' },',
@@ -320,8 +321,13 @@ const ARRANGES = {
320
321
  styleguide: ['themes'],
321
322
  element: ['tag', 'raises', 'carries', 'always', 'places', 'works', 'measures', 'reads', 'gives', 'keeps', 'animates', 'shows', 'called', 'wires', 'dresses'],
322
323
  };
324
+ const TEACHING = {
325
+ composition: `export { TEACHES, catalogue } from '${named('ui')}/compositions'`,
326
+ screen: `export { TEACHES, screening as catalogue } from '${named('ui')}/compositions'`,
327
+ };
323
328
  function source(node, file, rows) {
324
329
  const extra = stated(rows, ARRANGES[node.id] ?? []).map((key) => SHAPES[key] ?? '');
330
+ const taught = TEACHING[node.id];
325
331
  const authors = node.declares !== undefined || rows.some((one) => one.at !== undefined && one.at !== node.at);
326
332
  const values = node.holds !== undefined || rows.some((one) => one.at === undefined || one.at === node.at);
327
333
  const own = node.id
@@ -356,7 +362,7 @@ export const ${node.id.toUpperCase()}: ${bespoke ? own : 'Threaded'} = {
356
362
  version: 1,
357
363
  ${held.join('\n')}
358
364
  }
359
- `;
365
+ ${taught === undefined ? '' : `\n${taught}\n`}`;
360
366
  }
361
367
  const LISTS = [
362
368
  { node: TOKENS, file: 'tokens.ts' },
@@ -463,11 +469,14 @@ export const ATTACH = {
463
469
  attaches: SEEDED.attaches,
464
470
  id: SEEDED.id,
465
471
  needs: {
472
+ '@floating-ui/dom': '^1.7.0',
473
+ '@floating-ui/react-dom': '^2.1.0',
466
474
  '@types/react': '^19.0.0',
467
475
  '@types/react-dom': '^19.0.0',
468
476
  next: '^16.0.0',
469
477
  react: '^19.0.0',
470
478
  'react-dom': '^19.0.0',
479
+ zod: '^4.5.4',
471
480
  },
472
481
  rows: [
473
482
  ...CONCEPTS.map((one) => ({