@prestack/thread-ui 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/CHANGELOG.md +177 -0
  2. package/dist/ui/animations/anm_appear.ts +13 -0
  3. package/dist/ui/animations/anm_blur_in.ts +19 -0
  4. package/dist/ui/animations/anm_blur_out.ts +18 -0
  5. package/dist/ui/animations/anm_dismiss.ts +18 -0
  6. package/dist/ui/animations/anm_drop.ts +19 -0
  7. package/dist/ui/animations/anm_fall.ts +18 -0
  8. package/dist/ui/animations/anm_flash.ts +18 -0
  9. package/dist/ui/animations/anm_glow.ts +16 -0
  10. package/dist/ui/animations/anm_lift.ts +16 -0
  11. package/dist/ui/animations/anm_nudge.ts +16 -0
  12. package/dist/ui/animations/anm_pop_in.ts +23 -0
  13. package/dist/ui/animations/anm_pop_out.ts +22 -0
  14. package/dist/ui/animations/anm_press.ts +12 -0
  15. package/dist/ui/animations/anm_pulse.ts +13 -0
  16. package/dist/ui/animations/anm_rise.ts +19 -0
  17. package/dist/ui/animations/anm_shake.ts +13 -0
  18. package/dist/ui/animations/anm_slide_in_left.ts +19 -0
  19. package/dist/ui/animations/anm_slide_in_right.ts +19 -0
  20. package/dist/ui/animations/anm_slide_out_left.ts +18 -0
  21. package/dist/ui/animations/anm_slide_out_right.ts +18 -0
  22. package/dist/ui/animations/anm_spin.ts +12 -0
  23. package/dist/ui/animations/anm_swell.ts +16 -0
  24. package/dist/ui/animations/anm_wobble.ts +24 -0
  25. package/dist/ui/animations/anm_zoom_in.ts +19 -0
  26. package/dist/ui/animations/anm_zoom_out.ts +18 -0
  27. package/dist/ui/animations.js +2 -3
  28. package/dist/ui/animations.ts +316 -0
  29. package/dist/ui/attach.js +13 -10
  30. package/dist/ui/attach.ts +607 -0
  31. package/dist/ui/charts.tsx +74 -0
  32. package/dist/ui/checked.d.ts +2 -2
  33. package/dist/ui/checked.ts +32 -0
  34. package/dist/ui/collector.ts +140 -0
  35. package/dist/ui/compositions.d.ts +11 -2
  36. package/dist/ui/compositions.js +283 -59
  37. package/dist/ui/compositions.ts +633 -0
  38. package/dist/ui/consent.ts +55 -0
  39. package/dist/ui/content.tsx +39 -0
  40. package/dist/ui/contrast.ts +27 -0
  41. package/dist/ui/controls.tsx +30 -0
  42. package/dist/ui/disclosure.tsx +22 -0
  43. package/dist/ui/dismiss-layer.ts +87 -0
  44. package/dist/ui/door.ts +140 -0
  45. package/dist/ui/drag.ts +316 -0
  46. package/dist/ui/elements/accordion.js +2 -2
  47. package/dist/ui/elements/accordion.tsx +47 -0
  48. package/dist/ui/elements/accordion.works.ts +66 -0
  49. package/dist/ui/elements/aspect-ratio.js +1 -1
  50. package/dist/ui/elements/aspect-ratio.tsx +30 -0
  51. package/dist/ui/elements/aspect-ratio.works.ts +5 -0
  52. package/dist/ui/elements/avatar.js +1 -1
  53. package/dist/ui/elements/avatar.tsx +33 -0
  54. package/dist/ui/elements/avatar.works.ts +16 -0
  55. package/dist/ui/elements/badge.js +1 -1
  56. package/dist/ui/elements/badge.tsx +32 -0
  57. package/dist/ui/elements/badge.works.ts +9 -0
  58. package/dist/ui/elements/bars.js +2 -2
  59. package/dist/ui/elements/bars.tsx +49 -0
  60. package/dist/ui/elements/bars.works.ts +29 -0
  61. package/dist/ui/elements/box.js +3 -2
  62. package/dist/ui/elements/box.tsx +35 -0
  63. package/dist/ui/elements/breadcrumb.js +2 -2
  64. package/dist/ui/elements/breadcrumb.tsx +37 -0
  65. package/dist/ui/elements/breadcrumb.works.ts +24 -0
  66. package/dist/ui/elements/button.js +1 -1
  67. package/dist/ui/elements/button.tsx +45 -0
  68. package/dist/ui/elements/button.works.tsx +52 -0
  69. package/dist/ui/elements/column.js +1 -1
  70. package/dist/ui/elements/column.tsx +28 -0
  71. package/dist/ui/elements/consent.js +2 -2
  72. package/dist/ui/elements/consent.tsx +70 -0
  73. package/dist/ui/elements/consent.works.tsx +132 -0
  74. package/dist/ui/elements/donut.js +2 -2
  75. package/dist/ui/elements/donut.tsx +44 -0
  76. package/dist/ui/elements/donut.works.ts +42 -0
  77. package/dist/ui/elements/each.js +1 -1
  78. package/dist/ui/elements/each.tsx +33 -0
  79. package/dist/ui/elements/each.works.tsx +82 -0
  80. package/dist/ui/elements/empty.js +1 -1
  81. package/dist/ui/elements/empty.tsx +32 -0
  82. package/dist/ui/elements/field.js +1 -1
  83. package/dist/ui/elements/field.tsx +44 -0
  84. package/dist/ui/elements/field.works.ts +65 -0
  85. package/dist/ui/elements/funnel.js +2 -2
  86. package/dist/ui/elements/funnel.tsx +49 -0
  87. package/dist/ui/elements/funnel.works.ts +37 -0
  88. package/dist/ui/elements/gauge.js +1 -1
  89. package/dist/ui/elements/gauge.tsx +39 -0
  90. package/dist/ui/elements/gauge.works.ts +42 -0
  91. package/dist/ui/elements/glyph.js +1 -1
  92. package/dist/ui/elements/glyph.tsx +29 -0
  93. package/dist/ui/elements/glyph.works.tsx +42 -0
  94. package/dist/ui/elements/grid.js +1 -1
  95. package/dist/ui/elements/grid.tsx +28 -0
  96. package/dist/ui/elements/heatmap.js +2 -2
  97. package/dist/ui/elements/heatmap.tsx +47 -0
  98. package/dist/ui/elements/heatmap.works.ts +24 -0
  99. package/dist/ui/elements/hit-box.js +1 -1
  100. package/dist/ui/elements/hit-box.tsx +46 -0
  101. package/dist/ui/elements/hit-box.works.tsx +576 -0
  102. package/dist/ui/elements/image.js +1 -1
  103. package/dist/ui/elements/image.tsx +28 -0
  104. package/dist/ui/elements/line-chart.js +2 -2
  105. package/dist/ui/elements/line-chart.tsx +44 -0
  106. package/dist/ui/elements/line-chart.works.ts +41 -0
  107. package/dist/ui/elements/link.js +1 -1
  108. package/dist/ui/elements/link.tsx +37 -0
  109. package/dist/ui/elements/link.works.ts +25 -0
  110. package/dist/ui/elements/media.js +1 -1
  111. package/dist/ui/elements/media.tsx +56 -0
  112. package/dist/ui/elements/menu.js +2 -2
  113. package/dist/ui/elements/menu.tsx +50 -0
  114. package/dist/ui/elements/menu.works.tsx +153 -0
  115. package/dist/ui/elements/money.js +1 -1
  116. package/dist/ui/elements/money.tsx +28 -0
  117. package/dist/ui/elements/money.works.ts +9 -0
  118. package/dist/ui/elements/pagination.js +2 -2
  119. package/dist/ui/elements/pagination.tsx +38 -0
  120. package/dist/ui/elements/pagination.works.ts +20 -0
  121. package/dist/ui/elements/progress.js +1 -1
  122. package/dist/ui/elements/progress.tsx +33 -0
  123. package/dist/ui/elements/progress.works.ts +8 -0
  124. package/dist/ui/elements/radio-group.js +2 -2
  125. package/dist/ui/elements/radio-group.tsx +49 -0
  126. package/dist/ui/elements/radio-group.works.ts +33 -0
  127. package/dist/ui/elements/row.js +1 -1
  128. package/dist/ui/elements/row.tsx +28 -0
  129. package/dist/ui/elements/scatter.js +1 -1
  130. package/dist/ui/elements/scatter.tsx +41 -0
  131. package/dist/ui/elements/scatter.works.ts +38 -0
  132. package/dist/ui/elements/select.js +2 -2
  133. package/dist/ui/elements/select.tsx +42 -0
  134. package/dist/ui/elements/select.works.ts +5 -0
  135. package/dist/ui/elements/separator.js +1 -1
  136. package/dist/ui/elements/separator.tsx +30 -0
  137. package/dist/ui/elements/skeleton.js +1 -1
  138. package/dist/ui/elements/skeleton.tsx +30 -0
  139. package/dist/ui/elements/slider.js +1 -1
  140. package/dist/ui/elements/slider.tsx +33 -0
  141. package/dist/ui/elements/slider.works.ts +5 -0
  142. package/dist/ui/elements/sparkline.js +1 -1
  143. package/dist/ui/elements/sparkline.tsx +36 -0
  144. package/dist/ui/elements/sparkline.works.ts +32 -0
  145. package/dist/ui/elements/spinner.js +1 -1
  146. package/dist/ui/elements/spinner.tsx +32 -0
  147. package/dist/ui/elements/stack.js +1 -1
  148. package/dist/ui/elements/stack.tsx +28 -0
  149. package/dist/ui/elements/stat.js +1 -1
  150. package/dist/ui/elements/stat.tsx +35 -0
  151. package/dist/ui/elements/stat.works.ts +14 -0
  152. package/dist/ui/elements/surface.js +1 -1
  153. package/dist/ui/elements/surface.tsx +35 -0
  154. package/dist/ui/elements/surface.works.ts +9 -0
  155. package/dist/ui/elements/table.js +2 -2
  156. package/dist/ui/elements/table.tsx +61 -0
  157. package/dist/ui/elements/table.works.ts +30 -0
  158. package/dist/ui/elements/tabs.js +2 -2
  159. package/dist/ui/elements/tabs.tsx +47 -0
  160. package/dist/ui/elements/tabs.works.ts +84 -0
  161. package/dist/ui/elements/text.js +1 -1
  162. package/dist/ui/elements/text.tsx +32 -0
  163. package/dist/ui/elements/textarea.js +1 -1
  164. package/dist/ui/elements/textarea.tsx +37 -0
  165. package/dist/ui/elements/textarea.works.ts +5 -0
  166. package/dist/ui/elements/time.js +1 -1
  167. package/dist/ui/elements/time.tsx +34 -0
  168. package/dist/ui/elements/time.works.ts +25 -0
  169. package/dist/ui/elements/toggle.js +1 -1
  170. package/dist/ui/elements/toggle.tsx +33 -0
  171. package/dist/ui/elements/toggle.works.ts +5 -0
  172. package/dist/ui/elements/tooltip.js +1 -1
  173. package/dist/ui/elements/tooltip.tsx +39 -0
  174. package/dist/ui/elements/tooltip.works.ts +77 -0
  175. package/dist/ui/elements/value.js +1 -1
  176. package/dist/ui/elements/value.tsx +30 -0
  177. package/dist/ui/elements/value.works.ts +9 -0
  178. package/dist/ui/elements.ts +127 -0
  179. package/dist/ui/focus-scope.ts +121 -0
  180. package/dist/ui/fonts.ts +355 -0
  181. package/dist/ui/generators/dev.js +2 -1
  182. package/dist/ui/generators/dev.ts +80 -0
  183. package/dist/ui/generators/elements.d.ts +1 -11
  184. package/dist/ui/generators/elements.js +44 -14
  185. package/dist/ui/generators/elements.ts +794 -0
  186. package/dist/ui/generators/i18n.js +2 -3
  187. package/dist/ui/generators/i18n.ts +300 -0
  188. package/dist/ui/generators/icons.js +3 -12
  189. package/dist/ui/generators/icons.ts +214 -0
  190. package/dist/ui/generators/react.js +13 -5
  191. package/dist/ui/generators/react.ts +269 -0
  192. package/dist/ui/generators/routes.d.ts +1 -0
  193. package/dist/ui/generators/routes.js +99 -83
  194. package/dist/ui/generators/routes.ts +743 -0
  195. package/dist/ui/generators/show.js +4 -12
  196. package/dist/ui/generators/show.ts +260 -0
  197. package/dist/ui/generators/styles.js +3 -4
  198. package/dist/ui/generators/styles.ts +669 -0
  199. package/dist/ui/generators/variants.js +2 -3
  200. package/dist/ui/generators/variants.ts +204 -0
  201. package/dist/ui/i18n/catalogs/en.ts +191 -0
  202. package/dist/ui/i18n/format.ts +221 -0
  203. package/dist/ui/i18n/locales.ts +33 -0
  204. package/dist/ui/i18n/parse.ts +315 -0
  205. package/dist/ui/i18n/translator.ts +31 -0
  206. package/dist/ui/i18n/types.ts +96 -0
  207. package/dist/ui/icons.ts +331 -0
  208. package/dist/ui/islands/map.ts +58 -0
  209. package/dist/ui/localizations.ts +75 -0
  210. package/dist/ui/material.ts +236 -0
  211. package/dist/ui/materials/mat_bordered.ts +13 -0
  212. package/dist/ui/materials/mat_ceramic.ts +13 -0
  213. package/dist/ui/materials/mat_clay.ts +13 -0
  214. package/dist/ui/materials/mat_elevated.ts +24 -0
  215. package/dist/ui/materials/mat_flat.ts +34 -0
  216. package/dist/ui/materials/mat_glass.ts +20 -0
  217. package/dist/ui/materials/mat_lit.ts +13 -0
  218. package/dist/ui/materials/mat_moulded.ts +11 -0
  219. package/dist/ui/materials.js +2 -3
  220. package/dist/ui/materials.ts +205 -0
  221. package/dist/ui/modes.ts +44 -0
  222. package/dist/ui/navigation.tsx +32 -0
  223. package/dist/ui/positioning.ts +90 -0
  224. package/dist/ui/presented.tsx +119 -0
  225. package/dist/ui/provider.d.ts +2 -0
  226. package/dist/ui/provider.js +7 -1
  227. package/dist/ui/provider.tsx +182 -0
  228. package/dist/ui/render-loop.ts +133 -0
  229. package/dist/ui/roving-focus.ts +161 -0
  230. package/dist/ui/screens.js +22 -22
  231. package/dist/ui/screens.ts +141 -0
  232. package/dist/ui/scroll-lock.ts +28 -0
  233. package/dist/ui/series.ts +97 -0
  234. package/dist/ui/signals.ts +386 -0
  235. package/dist/ui/styleguide.ts +231 -0
  236. package/dist/ui/styleguides.ts +374 -0
  237. package/dist/ui/themes.ts +22 -0
  238. package/dist/ui/token.js +1 -1
  239. package/dist/ui/token.ts +78 -0
  240. package/dist/ui/tokens/shadow.ts +25 -0
  241. package/dist/ui/tokens.js +1 -2
  242. package/dist/ui/tokens.ts +234 -0
  243. package/dist/ui/validators.ts +232 -0
  244. package/dist/ui/variant.d.ts +1 -0
  245. package/dist/ui/variant.js +3 -0
  246. package/dist/ui/variant.ts +352 -0
  247. package/dist/ui/variants/var_avatar-round.ts +14 -0
  248. package/dist/ui/variants/var_bubble-me.ts +14 -0
  249. package/dist/ui/variants/var_bubble-them.ts +13 -0
  250. package/dist/ui/variants/var_button-border.ts +76 -0
  251. package/dist/ui/variants/var_button-cta.ts +76 -0
  252. package/dist/ui/variants/var_button-destructive-ghost.ts +72 -0
  253. package/dist/ui/variants/var_button-destructive.ts +76 -0
  254. package/dist/ui/variants/var_button-fill.ts +76 -0
  255. package/dist/ui/variants/var_button-ghost.ts +72 -0
  256. package/dist/ui/variants/var_button-glass.ts +55 -0
  257. package/dist/ui/variants/var_button-normal.ts +76 -0
  258. package/dist/ui/variants/var_button-quiet.ts +74 -0
  259. package/dist/ui/variants/var_card.ts +17 -0
  260. package/dist/ui/variants/var_chip.ts +13 -0
  261. package/dist/ui/variants/var_choice.ts +15 -0
  262. package/dist/ui/variants/var_framed.ts +10 -0
  263. package/dist/ui/variants/var_glyph-plain.ts +11 -0
  264. package/dist/ui/variants/var_input.ts +43 -0
  265. package/dist/ui/variants/var_link-inline.ts +29 -0
  266. package/dist/ui/variants/var_list-row.ts +34 -0
  267. package/dist/ui/variants/var_rule.ts +10 -0
  268. package/dist/ui/variants/var_skeleton-block.ts +12 -0
  269. package/dist/ui/variants/var_spinner-turning.ts +12 -0
  270. package/dist/ui/variants/var_stat-tile.ts +13 -0
  271. package/dist/ui/variants/var_tab-bar.ts +15 -0
  272. package/dist/ui/variants/var_tap-target.ts +25 -0
  273. package/dist/ui/variants.ts +64 -0
  274. package/dist/ui/wrappers.d.ts +4 -1
  275. package/dist/ui/wrappers.js +60 -20
  276. package/dist/ui/wrappers.tsx +426 -0
  277. package/package.json +8 -2
  278. package/dist/ui/makers/message.d.ts +0 -1
  279. package/dist/ui/makers/message.js +0 -230
@@ -0,0 +1,234 @@
1
+ import { existsSync, readdirSync, writeFileSync } from '@prestack/thread/core/files'
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 } from '@prestack/thread/core/tree'
9
+
10
+ const here = 'thread/ui/tokens.ts'
11
+
12
+ const said = [
13
+ {
14
+ on: '2026-08-24',
15
+ said: 'What I wanted is tokens be a list of variables. Not that tokens are the list of variables with values. The list of variables with default values is the style guide. So, tokens declare colour is a variable that can be used. A token is a row.',
16
+ },
17
+ {
18
+ on: '2026-08-24',
19
+ said: 'We want to do something more we declare a new variable row (which should also be able to be a function, I don\'t want this to be a limiting factor).',
20
+ },
21
+ {
22
+ on: '2026-08-28',
23
+ said: 'Fonts now are SSOT for fonts. So font is not a token anymore.',
24
+ },
25
+ ]
26
+
27
+ const one = <Id extends string>(id: Id, describes: string): Omit<Threaded, 'at'> & { readonly id: Id } => ({
28
+ id,
29
+ describes,
30
+ version: 1,
31
+ })
32
+
33
+ const written = <Id extends string>(
34
+ id: Id,
35
+ describes: string,
36
+ at: string,
37
+ ): Threaded & { readonly id: Id } => ({ id, at, describes, version: 1 })
38
+
39
+ export const holds = [
40
+ one('colour', 'Something that can be painted — a surface, a word, a line, a veil. The style guide says which colours an app has and what each one is; this only says an app may have them.'),
41
+ one('gradient', 'A fill that changes across the thing it fills. One value however many colours and stops it is made from, so a surface can be lit or shaded without a material writing the ramp itself.'),
42
+ one('texture', 'A pattern that repeats across a surface — stripes, dots, a weave, a grid. Structured and deliberate, which is what tells it from noise.'),
43
+ one('noise', 'Grain scattered across a surface with no pattern to it, which is what keeps a large flat area from reading as dead. Made by a filter rather than by a ramp.'),
44
+ one('img', 'A picture used as a surface rather than as content — a backdrop, a scan, anything too irregular to be generated.'),
45
+ one('space', 'Room, wherever room is measured: inside a thing, between two things, around a region. One scale answers all three so a gap and a padding cannot drift apart.'),
46
+ one('radius', 'How far a corner is turned. Ranges from square, which is a decision rather than an omission, to fully round.'),
47
+ one('ratio', 'The proportion a box keeps whatever its width, so a picture or a video holds its shape before it has loaded.'),
48
+ one('edge', 'How thick a boundary line is drawn, from the thinnest a screen can show to one doing deliberate work.'),
49
+ one('line', 'How a boundary line is drawn — unbroken, broken, or not drawn at all. Held apart from how thick it is, because a border can change manner without changing weight.'),
50
+ one('type', 'How large type is set. The one scale a reader judges every other size against.'),
51
+ one('weight', 'How heavy type is drawn, from unemphasised running text to the one line that must be read first.'),
52
+ one('leading', 'How far apart lines of type sit. Large type needs less than small type, which is why it is its own scale.'),
53
+ one('tracking', 'How far apart letters sit. The one scale that reverses: large type is set tighter and small type wider.'),
54
+ one('case', 'Whether wording is drawn as it was written or forced to one case. A visual choice, so it sits with the type it changes rather than with the words themselves.'),
55
+ one('blur', 'How much what lies behind a surface is softened. Saturation rides along with it, because a blur without it reads grey.'),
56
+ one('filter', 'What is done to a thing’s own pixels once it is drawn — dimmed, drained of colour, brightened. The sibling of blur, which does the same to whatever is behind it.'),
57
+ one('transform', 'How a thing is moved, scaled or turned from where it would otherwise sit. What a press or a lift is made of.'),
58
+ one('motion', 'How long a change takes. Ranges from none, which is a decision, to long enough that the movement is what tells a reader what happened.'),
59
+ one('ease', 'How a change is paced across its duration — where it hurries, where it settles, whether it overshoots.'),
60
+ one('layer', 'What sits in front of what, where two things occupy the same place on a screen.'),
61
+ one('cursor', 'What the pointer becomes over a thing, which is how a screen says what pressing will do before anybody presses.'),
62
+ one('animation', 'Which movement a thing plays. A value of this kind names an animation the app declares, and the name is the whole of it — how long it takes and how it is paced belong to the animation rather than to the naming of one.'),
63
+ one('icon', 'How large a pictogram is drawn, from one sitting inside a line of text to one standing on its own as the content.'),
64
+ one('control', 'How tall something pressable is. The floor here is what makes a target reachable by a thumb rather than only by a cursor.'),
65
+ one('opacity', 'How present a thing is without being removed — faded because it is unavailable, or receded because it is not being attended to.'),
66
+ one('layout', 'How a thing arranges what is inside it. Founder, 2026-08-31: "Seems like they might be tokens right? Like...tokens of the layout kind? ANd then styleguide holds rows for them, and variant names the rows instead of values." Until this existed a variant wrote the CSS word itself, checked against a list of four properties and seven words that carried no reasoning and no citation — the last place in a variant where a value was not a row.'),
67
+ one('flow', 'Which way the children run — across, or down. Held apart from `layout` because an arrangement and its direction change independently: the same row of things becomes a column at a narrow width without ceasing to be an arrangement.'),
68
+ one('align', 'How the children line up across the flow — centred, gathered at the start or the end, or stretched to fill. The axis `flow` does not speak for.'),
69
+ one('wrap', 'Whether the children spill onto another line when they run out of room, or stay on one and let it overflow.'),
70
+
71
+ one('position', 'What a thing is placed against, and whether the page keeps its room once it moves. Nearly everything is laid where the page put it and takes its own space; the rest lift out of that in one of three ways, and lifting out is what makes covering something else possible at all.'),
72
+ one('visibility', 'Whether a thing is rendered, as against how present it looks. Held apart from `opacity` because they differ in what a person can still do: something faded to nothing is reachable and still read aloud, something hidden is neither, and both keep the room they were given.'),
73
+ one('reach', 'Whether a pointer can touch a thing at all — whether a press, a hover or a drag lands on it or falls through to what is behind. What a decoration drawn over a control needs, so the control underneath stays pressable.'),
74
+ one('picking', 'Whether the wording inside can be taken — selected, dragged across, copied. Prose wants this and a control does not: a label that selects itself mid-drag is how a button turns into a smear of highlight.'),
75
+ one('touch', 'What a finger dragged across a thing does — scrolls the page past it, scrolls along one axis only, or is the thing\'s own to interpret. What lets a thing be dragged on a phone without the page sliding away underneath it.'),
76
+ one('readying', 'What a thing warns the browser it is about to change, so the preparing happens before the movement rather than inside its first frame. A hint and nothing else: it changes no appearance, and a thing that names one and never changes has asked for work nobody uses.'),
77
+ one('origin', 'The point a thing turns, scales and skews about. Nothing at all until something is transformed, and then the difference between a menu growing out of the corner it was opened from and one growing out of its own middle.'),
78
+ one('overflow', 'What becomes of whatever does not fit — shown past the edge anyway, cut off at it, or reached by scrolling.'),
79
+ ] as const satisfies readonly Omit<Threaded, 'at'>[]
80
+
81
+ export const declares = [
82
+ written(
83
+ 'shadow',
84
+ 'Light a thing interrupts. Written as a rule rather than a list, so the style guide says how far a thing is lifted and how soft the light is, and the rule turns that into CSS.',
85
+ 'thread/ui/tokens/shadow.ts',
86
+ ),
87
+ ] as const satisfies readonly Threaded[]
88
+
89
+ export const TOKENS = {
90
+ id: 'token',
91
+ at: here,
92
+ describes:
93
+ 'A sort of variable an app may have, named and never valued — colour, space, shadow. It says such a variable can exist; the style guide says which ones there are and what each holds. Where the thing is better stated as a rule than as a list, the token carries that rule and a style guide row hands it arguments instead of a value. An app adds one to bring a new sort of variable into being, and may not remove what the framework put there.',
94
+ version: 1,
95
+ cites: said,
96
+ declares,
97
+ holds,
98
+ } as const satisfies Threaded & { readonly holds: readonly Omit<Threaded, 'at'>[] }
99
+
100
+ export function nameOf(id: string): string {
101
+ return id.replace(/-/g, '_').toUpperCase()
102
+ }
103
+
104
+ function starter(row: Threaded): string {
105
+ const name = nameOf(row.id)
106
+ return `import type { Token } from './token.ts'
107
+
108
+ export const ${name}: Token = {
109
+ id: '${row.id}',
110
+ }
111
+ `
112
+ }
113
+
114
+ export function seeds(rows: readonly Threaded[], held: (at: string) => boolean): readonly { readonly path: string; readonly content: string }[] {
115
+ return rows.filter((row) => !held(row.at)).map((row) => ({ path: row.at, content: starter(row) }))
116
+ }
117
+
118
+ export async function mismatched(
119
+ rows: readonly Threaded[],
120
+ loader: (row: Threaded) => Promise<{ readonly id?: string } | undefined>,
121
+ ): Promise<readonly string[]> {
122
+ const found: string[] = []
123
+ for (const row of rows) {
124
+ const token = await loader(row)
125
+ if (token === undefined) {
126
+ found.push(
127
+ `\`${row.id}\` is declared at \`${row.at}\`, and that file exports no \`${nameOf(row.id)}\`. ` +
128
+ 'A row names a file, and the file is the thing.',
129
+ )
130
+ continue
131
+ }
132
+ if (token.id !== row.id) {
133
+ found.push(
134
+ `\`${row.at}\` declares itself \`${String(token.id)}\`, and the row that names it calls it \`${row.id}\`. ` +
135
+ 'A token file and the row that names it have to be the same token — make the file’s `id` match the row, or fix the row.',
136
+ )
137
+ }
138
+ }
139
+ return found
140
+ }
141
+
142
+ function extensionOf(path: string): string {
143
+ const name = basename(path)
144
+ const at = name.lastIndexOf('.')
145
+ return at === -1 ? '' : name.slice(at)
146
+ }
147
+
148
+ export function orphaned(rows: readonly Threaded[], listing: (dir: string) => readonly string[]): readonly string[] {
149
+ const named = new Set(rows.map((row) => row.at))
150
+ const acceptedByDir = new Map<string, Set<string>>()
151
+ for (const row of rows) {
152
+ const dir = dirname(row.at)
153
+ const held = acceptedByDir.get(dir) ?? new Set<string>()
154
+ held.add(extensionOf(row.at))
155
+ acceptedByDir.set(dir, held)
156
+ }
157
+ const found: string[] = []
158
+ for (const [dir, accepted] of acceptedByDir) {
159
+ for (const file of listing(dir)) {
160
+ if (named.has(file)) continue
161
+ if (!accepted.has(extensionOf(file))) continue
162
+ found.push(
163
+ `\`${file}\` is under tokens and no row names it — not a token, a file somebody wrote. ` +
164
+ 'Add a row for it, or delete the file.',
165
+ )
166
+ }
167
+ }
168
+ return found
169
+ }
170
+
171
+ async function run(): Promise<number> {
172
+ const tree = findTree()
173
+ const at = inTree(tree, 'tokens.ts')
174
+ if (!existsSync(at)) {
175
+ stdout.write(`thread: this tree has no ${tree.name}/${basename(at)}, so it declares no token and there is nothing to seed or check.\n`)
176
+ return 0
177
+ }
178
+ const held = (await import(pathToFileURL(at).href)) as { TOKEN?: { readonly declares?: readonly Threaded[] } }
179
+
180
+ if (held.TOKEN === undefined) {
181
+ stdout.write(`thread: ${tree.name}/tokens.ts exports no \`TOKEN\`, so nothing declares a token here.\n`)
182
+ return 1
183
+ }
184
+
185
+ const rows = held.TOKEN.declares ?? []
186
+ const checking = argv.includes('--check')
187
+ const exists = (where: string): boolean => existsSync(join(tree.at, where))
188
+ const made = seeds(rows, exists)
189
+
190
+ if (checking) {
191
+ const unseeded = rows.find((row) => !exists(row.at))
192
+ if (unseeded !== undefined) {
193
+ stdout.write(
194
+ `thread: \`${unseeded.id}\` is declared at \`${unseeded.at}\`, and the file is not there yet. ` +
195
+ 'A row names a file, and `--check` never writes one — run `thread tokens` and commit what it seeds.\n',
196
+ )
197
+ return 1
198
+ }
199
+ } else {
200
+ for (const seed of made) {
201
+ writeFileSync(join(tree.at, seed.path), seed.content, 'utf8')
202
+ stdout.write(`thread: seeded ${seed.path}\n`)
203
+ }
204
+ }
205
+
206
+ const wrong = (
207
+ await mismatched(rows, async (row) => {
208
+ const module = (await import(pathToFileURL(join(tree.at, row.at)).href)) as Record<string, unknown>
209
+ const held = module[nameOf(row.id)]
210
+ if (held === undefined) return undefined
211
+ return held as { readonly id: string }
212
+ })
213
+ )[0]
214
+ if (wrong !== undefined) {
215
+ stdout.write(`thread: ${wrong}\n`)
216
+ return 1
217
+ }
218
+
219
+ const stray = orphaned(rows, (dir) => readdirSync(join(tree.at, dir)).map((name) => join(dir, name)))[0]
220
+ if (stray !== undefined) {
221
+ stdout.write(`thread: ${stray}\n`)
222
+ return 1
223
+ }
224
+
225
+ stdout.write(
226
+ checking
227
+ ? `thread: ${String(rows.length)} token(s) declared, all seeded\n`
228
+ : `thread: ${String(rows.length)} token(s) declared, ${String(made.length)} seeded, ` +
229
+ `${String(rows.length - made.length)} already written\n`,
230
+ )
231
+ return 0
232
+ }
233
+
234
+ if (invoked(import.meta.url)) exit(await run())
@@ -0,0 +1,232 @@
1
+ import {
2
+ ENTRY_MODES,
3
+ FORMATS,
4
+ KEYBOARDS,
5
+ formatNamed,
6
+ showValue,
7
+ type EntryMode,
8
+ type Facts,
9
+ type Format,
10
+ type Keyboard,
11
+ } from '@prestack/thread/core/types'
12
+
13
+ export { ENTRY_MODES, KEYBOARDS }
14
+ export type { EntryMode, Keyboard }
15
+
16
+ export type ValidateResult = { readonly ok: true; readonly value: unknown } | { readonly ok: false; readonly errorKey: string }
17
+
18
+ export const valid = (value: unknown): ValidateResult => ({ ok: true, value })
19
+ export const invalid = (errorKey: string): ValidateResult => ({ ok: false, errorKey })
20
+
21
+ export interface ValidatorContext {
22
+ readonly [key: string]: unknown
23
+ }
24
+
25
+ export function validators(): readonly Format[] {
26
+ return [...FORMATS].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
27
+ }
28
+
29
+ export function hasValidator(name: string): boolean {
30
+ return formatNamed(name) !== undefined
31
+ }
32
+
33
+ export function validatorFor(name: string): Format {
34
+ const one = formatNamed(name)
35
+ if (one === undefined) {
36
+ throw new Error(`unknown validator "${name}". Declared: ${FORMATS.map((entry) => entry.name).join(', ')}`)
37
+ }
38
+ return one
39
+ }
40
+
41
+ function factsFrom(context: ValidatorContext): Facts {
42
+ const text = (key: string): string | undefined => (typeof context[key] === 'string' ? (context[key] as string) : undefined)
43
+ const count = (key: string): number | undefined => (typeof context[key] === 'number' ? (context[key] as number) : undefined)
44
+ const locale = text('locale')
45
+ const currency = text('currency')
46
+ const max = count('max')
47
+ const now = count('now')
48
+ return {
49
+ ...(locale === undefined ? {} : { locale }),
50
+ ...(currency === undefined ? {} : { currency }),
51
+ ...(max === undefined ? {} : { max }),
52
+ ...(now === undefined ? {} : { now }),
53
+ }
54
+ }
55
+
56
+ export function runValidator(name: string, raw: unknown, context: ValidatorContext = {}): ValidateResult {
57
+ const one = formatNamed(name)
58
+ if (one === undefined) return invalid(`format.unknown.${name}`)
59
+ const outcome = one.check(raw, factsFrom(context))
60
+ return outcome.ok ? valid(outcome.value) : invalid(outcome.because)
61
+ }
62
+
63
+ export function formatValue(name: string, value: unknown, context: ValidatorContext = {}): string {
64
+ const one = formatNamed(name)
65
+ return one === undefined ? String(value) : showValue(one.type, name, value, factsFrom(context))
66
+ }
67
+
68
+ export interface EntryConstraints {
69
+ readonly keyboard: Keyboard
70
+ readonly accepts?: RegExp
71
+ readonly maxChars?: number
72
+ readonly entry: EntryMode
73
+ readonly mask?: string
74
+ readonly secret: boolean
75
+ readonly autocomplete?: string
76
+ }
77
+
78
+ export function entryConstraints(name: string, context: ValidatorContext = {}): EntryConstraints {
79
+ const one = validatorFor(name)
80
+ for (const key of one.requires ?? []) {
81
+ if (context[key] === undefined) {
82
+ throw new Error(`validator "${name}" requires "${key}" to render an entry field, and it was not provided`)
83
+ }
84
+ }
85
+ return {
86
+ keyboard: one.keyboard ?? 'text',
87
+ ...(one.accepts === undefined ? {} : { accepts: one.accepts }),
88
+ ...(one.maxChars === undefined ? {} : { maxChars: one.maxChars }),
89
+ entry: one.entry ?? 'plain',
90
+ ...(one.mask === undefined ? {} : { mask: one.mask }),
91
+ secret: one.secret === true,
92
+ ...(one.autocomplete === undefined ? {} : { autocomplete: one.autocomplete }),
93
+ }
94
+ }
95
+
96
+ export function acceptsKeystroke(name: string, current: string, key: string): boolean {
97
+ const one = validatorFor(name)
98
+ if (one.maxChars !== undefined && current.length >= one.maxChars) return false
99
+ if (one.accepts === undefined) return true
100
+ return new RegExp(one.accepts.source, one.accepts.flags.replace('g', '')).test(key)
101
+ }
102
+
103
+ export function applyMask(name: string, raw: string): string {
104
+ const one = validatorFor(name)
105
+ if (one.entry !== 'masked' || one.mask === undefined) return raw
106
+
107
+ const literals = new Set([...one.mask].filter((character) => character !== '0'))
108
+ const source = [...raw].filter((character) => !literals.has(character))
109
+
110
+ let out = ''
111
+ let taken = 0
112
+ for (const slot of one.mask) {
113
+ if (taken >= source.length) break
114
+ if (slot === '0') {
115
+ out += source[taken] as string
116
+ taken += 1
117
+ continue
118
+ }
119
+ out += slot
120
+ }
121
+ return out
122
+ }
123
+
124
+ interface Exact {
125
+ readonly n: bigint
126
+ readonly d: bigint
127
+ }
128
+
129
+ export function minorUnits(currency: string): number {
130
+
131
+ const { maximumFractionDigits } = new Intl.NumberFormat('en', { style: 'currency', currency }).resolvedOptions()
132
+
133
+ if (maximumFractionDigits === undefined) throw new Error(`no minor-unit exponent for currency "${currency}"`)
134
+ return maximumFractionDigits
135
+ }
136
+
137
+ const ZERO: Exact = { n: BigInt(0), d: BigInt(1) }
138
+
139
+ function add(a: Exact, b: Exact): Exact {
140
+ return { n: a.n * b.d + b.n * a.d, d: a.d * b.d }
141
+ }
142
+ function sub(a: Exact, b: Exact): Exact {
143
+ return { n: a.n * b.d - b.n * a.d, d: a.d * b.d }
144
+ }
145
+ function mul(a: Exact, b: Exact): Exact {
146
+ return { n: a.n * b.n, d: a.d * b.d }
147
+ }
148
+ function div(a: Exact, b: Exact): Exact | undefined {
149
+
150
+ return b.n === BigInt(0) ? undefined : { n: a.n * b.d, d: a.d * b.n }
151
+ }
152
+
153
+ function literal(text: string): Exact | undefined {
154
+ if (!/^\d+(\.\d+)?$/.test(text)) return undefined
155
+ const [whole, fraction = ''] = text.split('.')
156
+ return { n: BigInt(`${whole}${fraction}`), d: BigInt(10) ** BigInt(fraction.length) }
157
+ }
158
+
159
+ type Token = { readonly kind: 'number'; readonly value: Exact } | { readonly kind: 'op'; readonly op: string }
160
+
161
+ function tokenize(input: string): readonly Token[] | undefined {
162
+ const out: Token[] = []
163
+ for (const raw of input.replace(/\s+/g, '').split(/([+\-×÷*/])/).filter(Boolean)) {
164
+ if (/^[+\-×÷*/]$/.test(raw)) {
165
+ out.push({ kind: 'op', op: raw === '*' ? '×' : raw === '/' ? '÷' : raw })
166
+ continue
167
+ }
168
+ const value = literal(raw)
169
+ if (value === undefined) return undefined
170
+ out.push({ kind: 'number', value })
171
+ }
172
+ return out
173
+ }
174
+
175
+ export function evaluateMoney(input: string, currency: string): { readonly amount: number } | undefined {
176
+ const tokens = tokenize(input)
177
+ if (tokens === undefined || tokens.length === 0) return undefined
178
+ if (tokens[0]?.kind !== 'number' || tokens[tokens.length - 1]?.kind !== 'number') return undefined
179
+
180
+ const terms: Exact[] = []
181
+ const adders: string[] = []
182
+ let current = (tokens[0] as { value: Exact }).value
183
+
184
+ for (let i = 1; i < tokens.length; i += 2) {
185
+ const operator = tokens[i]
186
+ const operand = tokens[i + 1]
187
+ if (operator?.kind !== 'op' || operand?.kind !== 'number') return undefined
188
+ if (operator.op === '×') {
189
+ current = mul(current, operand.value)
190
+ continue
191
+ }
192
+ if (operator.op === '÷') {
193
+ const next = div(current, operand.value)
194
+ if (next === undefined) return undefined
195
+ current = next
196
+ continue
197
+ }
198
+ terms.push(current)
199
+ adders.push(operator.op)
200
+ current = operand.value
201
+ }
202
+ terms.push(current)
203
+
204
+ let total = terms[0] ?? ZERO
205
+ adders.forEach((op, i) => {
206
+ const term = terms[i + 1] ?? ZERO
207
+ total = op === '+' ? add(total, term) : sub(total, term)
208
+ })
209
+
210
+ const scale = BigInt(10) ** BigInt(minorUnits(currency))
211
+ const scaled = (total.n * scale) / total.d
212
+ const remainder = (total.n * scale) % total.d
213
+ const half = total.d / BigInt(2)
214
+ const away = remainder < BigInt(0) ? -BigInt(1) : BigInt(1)
215
+ const magnitude = remainder < BigInt(0) ? -remainder : remainder
216
+ const rounded = magnitude * BigInt(2) >= total.d && half >= BigInt(0) ? scaled + away : scaled
217
+
218
+ return { amount: Number(rounded) / Number(scale) }
219
+ }
220
+
221
+ export function limited(name: string | undefined): Readonly<Record<string, number | string>> {
222
+ if (name === undefined) return {}
223
+ try {
224
+ const held = entryConstraints(name, {})
225
+ return {
226
+ ...(held.maxChars === undefined ? {} : { maxLength: held.maxChars }),
227
+ ...(held.keyboard === undefined ? {} : { inputMode: held.keyboard }),
228
+ }
229
+ } catch {
230
+ return {}
231
+ }
232
+ }
@@ -107,6 +107,7 @@ export declare const variant: <const T extends Variant>(one: T & {
107
107
  } : unknown; };
108
108
  } : unknown)) => T;
109
109
  export declare function boundHeld(): Readonly<Record<string, readonly string[]>>;
110
+ export declare function camel(id: string): string;
110
111
  export declare function classOf(id: string): string;
111
112
  export declare function classOfElement(id: string): string;
112
113
  export declare function classOfComposition(id: string): string;
@@ -52,6 +52,9 @@ export const variant = (one) => one;
52
52
  export function boundHeld() {
53
53
  return BOUND;
54
54
  }
55
+ export function camel(id) {
56
+ return id.replace(/-([a-z0-9])/g, (_, one) => one.toUpperCase());
57
+ }
55
58
  export function classOf(id) {
56
59
  return `thread-${id}`;
57
60
  }