@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,64 @@
1
+ import type { Threaded } from '@prestack/thread/core/thread'
2
+
3
+ interface Worn extends Threaded {
4
+ readonly section: string
5
+ readonly wears: readonly string[]
6
+ }
7
+
8
+ interface Catalogue extends Threaded {
9
+ readonly files: string
10
+ readonly declares: readonly Worn[]
11
+ }
12
+
13
+ const at = (id: string): string => `thread/ui/variants/var_${id}.ts`
14
+
15
+ export const VARIANTS = {
16
+ id: 'variant',
17
+ at: 'thread/ui/variants.ts',
18
+ files: 'ui/var_*.ts',
19
+ describes:
20
+ 'How a thing looks: a material plus the style guide values it names directly, never raw ones, and how that look changes under a state. It says nothing about type, because typography belongs to the role the wording states; what is left here is the look of the box and the colour of what sits in it. Each row names a file prefixed `var_`, and that file is the variant: what it looks like, what it looks like under a state, and which states it reacts to. The row states which elements accept it, and it is worn as the tag, so a variant `button-fill` is written `<Button.buttonFill>` and an element nothing declares is refused. A state is the set of signals holding at once, and two states that can both hold and both set one property must have their combination declared, or which one wins is an accident of file order. An app adds its own to this same list, and a row that goes missing breaks the build, because what the framework offers is not an app’s to withdraw.',
21
+ version: 1,
22
+ cites: [
23
+ {
24
+ on: '2026-08-24',
25
+ said: 'Variants define lots of things. Including how they look in different states. Including how they look in multiple states at once. (hover, pressed) is one variant that covers both hover and pressed, so they look the same when they are simultaneous. (hover) (pressed) (hover, pressed) however have different states for the three situations.',
26
+ },
27
+ {
28
+ on: '2026-08-25',
29
+ said: 'Variants basically say: This is how I look. These are the elements that accept me. These are my behaviours under different states. These are the states I can react to.',
30
+ },
31
+ {
32
+ on: '2026-08-28',
33
+ said: 'fonts.ts as SSOT for everything related to fonts and easy to change fonts throughout the app like tokens, like....the exact same reason for existing that tokens+styleguide have is the same reason we have fonts.ts',
34
+ },
35
+ ],
36
+ declares: [
37
+ { id: 'card', section: 'surface', at: at('card'), wears: ['Accordion', 'Bars', 'Box', 'Donut', 'Empty', 'Funnel', 'Heatmap', 'LineChart', 'Media', 'Menu', 'Scatter', 'Sparkline', 'Surface', 'Table', 'Tooltip'], describes: 'A surface holding other content, and the shape every unregistered variant falls back to.' },
38
+ { id: 'list-row', section: 'surface', at: at('list-row'), wears: ['Box'], describes: 'One row of a list — square, quiet, and separated by its neighbours rather than by a shadow.' },
39
+ { id: 'framed', section: 'surface', at: at('framed'), wears: ['Image', 'Media', 'AspectRatio'], describes: 'A picture given the same corners as a panel, on a sunken ground so the gap before it loads is not a white hole.' },
40
+ { id: 'bubble-them', section: 'surface', at: at('bubble-them'), wears: ['Box'], describes: 'A message somebody else sent. The plain half of a conversation.' },
41
+ { id: 'bubble-me', section: 'surface', at: at('bubble-me'), wears: ['Box'], describes: 'A message the current person sent. Takes the accent, which is how a conversation reads as two voices without a second colour role.' },
42
+ { id: 'button-fill', section: 'button', at: at('button-fill'), wears: ['Button'], describes: 'The one action a screen is asking for. The accent behind it, and the only button on a screen that should carry it.' },
43
+ { id: 'button-normal', section: 'button', at: at('button-normal'), wears: ['Button'], describes: 'The ordinary button. A surface with a hairline round it — pressable, and asking for nothing in particular.' },
44
+ { id: 'button-ghost', section: 'button', at: at('button-ghost'), wears: ['Button'], describes: 'Wording that acts. No ground and no line until a pointer is over it, which is what keeps a row of them from reading as a wall of boxes.' },
45
+ { id: 'button-border', section: 'button', at: at('button-border'), wears: ['Button'], describes: 'An action offered beside a filled one. The accent as an edge rather than a ground, so the shape says pressable and the colour says not this one.' },
46
+ { id: 'button-destructive', section: 'button', at: at('button-destructive'), wears: ['Button'], describes: 'Something that cannot be undone. The same shape as the asking button, in the colour reserved for consequence.' },
47
+ { id: 'button-destructive-ghost', section: 'button', at: at('button-destructive-ghost'), wears: ['Button'], describes: 'A destructive action that is not the point of the screen — delete, sitting quietly at the end of a menu. Carries the negative as ink and gains a ground only under a pointer.' },
48
+ { id: 'button-cta', section: 'button', at: at('button-cta'), wears: ['Button'], describes: 'The one thing a landing screen exists to get pressed. Taller than every other button and set in running type rather than label type, which is what makes it read from across the page.' },
49
+ { id: 'button-quiet', section: 'button', at: at('button-quiet'), wears: ['Button'], describes: 'An action that is available and is not being suggested — cancel, skip, dismiss. Ink that supports rather than states, and a smaller symbol beside it.' },
50
+ { id: 'button-glass', section: 'button', at: at('button-glass'), wears: ['Button'], describes: 'A button over a picture or a video, where a solid fill would hide what it is sitting on. What is behind shows through, softened.' },
51
+ { id: 'input', section: 'entry', at: at('input'), wears: ['Field', 'Select', 'Slider', 'Textarea', 'Toggle'], describes: 'Anything a person types into. Tighter vertical room than a surface, so the text baseline sits where a label expects it.' },
52
+ { id: 'choice', section: 'entry', at: at('choice'), wears: ['RadioGroup'], describes: 'A set of options laid out to be chosen between. A group is not a control — it cannot be focused or invalid itself, only the radios inside it can — so it takes arrangement and a ground and states nothing it cannot be in.' },
53
+ { id: 'tab-bar', section: 'navigation', at: at('tab-bar'), wears: ['Breadcrumb', 'Pagination', 'Tabs'], describes: 'The strip of destinations along one edge. Rounder and less padded than a surface, because it holds pressable things rather than content.' },
54
+ { id: 'link-inline', section: 'navigation', at: at('link-inline'), wears: ['Link'], describes: 'Wording that takes you somewhere. Carries the accent so it reads as reachable without an underline, and draws a real ring when a keyboard reaches it.' },
55
+ { id: 'chip', section: 'marker', at: at('chip'), wears: ['Badge'], describes: 'A small token standing for a value. The tightest room in the set, and pill-shaped like the buttons.' },
56
+ { id: 'glyph-plain', section: 'marker', at: at('glyph-plain'), wears: ['Glyph'], describes: 'A pictogram at the size most controls use, drawn in the ink of whatever it sits beside.' },
57
+ { id: 'avatar-round', section: 'marker', at: at('avatar-round'), wears: ['Avatar'], describes: 'A person, standing for themselves. Fully round, sunken so it reads as a well rather than a card, and sized to be recognised at a glance.' },
58
+ { id: 'rule', section: 'marker', at: at('rule'), wears: ['Separator'], describes: 'The line between two things. The thinnest a screen can draw, in the colour that separates without asking to be seen.' },
59
+ { id: 'skeleton-block', section: 'marker', at: at('skeleton-block'), wears: ['Skeleton'], describes: 'The shape of something that has not arrived. Sunken and breathing, which is what says waiting rather than empty.' },
60
+ { id: 'spinner-turning', section: 'marker', at: at('spinner-turning'), wears: ['Spinner'], describes: 'Work with no measurable end. Turns without stopping, in the accent, so it reads as the app doing something rather than the app stuck.' },
61
+ { id: 'stat-tile', section: 'figure', at: at('stat-tile'), wears: ['Gauge', 'Progress', 'Stat'], describes: 'A single figure given room to be read at a glance, so its type role is the display one rather than body.' },
62
+ { id: 'tap-target', section: 'gesture', at: at('tap-target'), wears: ['HitBox'], describes: 'Anything a finger acts on directly rather than through a control — the one variant a hit box wears. It is where a tap, a press and a carry are answered, because a hit box is the only thing that raises those and nothing accepted it before.' },
63
+ ],
64
+ } as const satisfies Catalogue
@@ -42,6 +42,7 @@ export interface UiConfig {
42
42
  readonly id: string;
43
43
  readonly selector: string;
44
44
  }[];
45
+ readonly holding?: readonly string[];
45
46
  }
46
47
  export declare const UiContext: import("react").Context<UiConfig | null>;
47
48
  export declare function UiProvider({ config, children }: {
@@ -57,6 +58,7 @@ export interface SurfaceRequest {
57
58
  readonly on?: 'press' | 'shown';
58
59
  };
59
60
  readonly holds?: readonly ((typeof declared)[number]['id'] | (string & {}))[];
61
+ readonly raises?: readonly ((typeof declared)[number]['id'] | (string & {}))[];
60
62
  readonly animates?: boolean;
61
63
  readonly measures?: Readonly<Record<string, string | number>>;
62
64
  }
@@ -113,7 +115,7 @@ export type Element<N extends string, P> = [keyof Members<N, P>] extends [never]
113
115
  }> : N extends ArrangementElement ? Component<P & {
114
116
  readonly at?: string;
115
117
  }> & Members<N, P> : Members<N, P>;
116
- export declare function element<N extends string, P>(name: N, fn: ElementBody<P>, own?: string): Element<N, P>;
118
+ export declare function element<N extends string, P>(name: N, fn: ElementBody<P>, own: string, wears: Readonly<Record<string, string>>): Element<N, P>;
117
119
  export declare function composition<P>(own: string, fn: (props: P) => ReactNode): Component<P & {
118
120
  readonly at?: string;
119
121
  readonly className?: string;
@@ -123,6 +125,7 @@ export interface ScreenControls {
123
125
  readonly onDismiss?: () => void;
124
126
  readonly at?: string;
125
127
  }
128
+ export declare function useView<T>(id: string): T | undefined;
126
129
  export declare function screen<P>(own: string, options: ScreenOptions, fn: Component<P>): Component<P & ScreenControls>;
127
130
  export declare function behaviourOf(presentation: Presentation): (typeof PRESENTATION_BEHAVIOUR)[Presentation];
128
131
  export {};
@@ -1,12 +1,8 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
4
- import { idOf } from "./checked.js";
5
4
  import { PRESENTATION_BEHAVIOUR } from "./door.js";
6
5
  import { selectorOf } from "./signals.js";
7
- import { classOf, classOfElement } from "./variant.js";
8
- import { ELEMENTS } from "./elements.js";
9
- import { VARIANTS } from "./variants.js";
10
6
  import { Presented } from "./presented.js";
11
7
  import { useRenderLoopDetector } from "./render-loop.js";
12
8
  export const UiContext = createContext(null);
@@ -23,7 +19,7 @@ export function useUi() {
23
19
  return config;
24
20
  }
25
21
  export function useSurface(request = {}) {
26
- const { translate, appearance, clock, asking, dispatch: run } = useUi();
22
+ const { translate, appearance, clock, asking, holding, dispatch: run } = useUi();
27
23
  const [t, setT] = useState(0);
28
24
  const animates = request.animates === true && !appearance.reduceMotion;
29
25
  useEffect(() => {
@@ -31,7 +27,8 @@ export function useSurface(request = {}) {
31
27
  return undefined;
32
28
  return clock.subscribe(setT);
33
29
  }, [animates, clock]);
34
- const holds = request.holds ?? [];
30
+ const gated = (request.raises ?? []).filter((one) => (holding ?? []).includes(one));
31
+ const holds = gated.length === 0 ? (request.holds ?? []) : [...new Set([...(request.holds ?? []), ...gated])];
35
32
  const held = new Set(holds);
36
33
  const raised = (signal) => held.has(signal);
37
34
  const a11y = {};
@@ -178,21 +175,14 @@ function wearing(held, added) {
178
175
  return held;
179
176
  return held === undefined || held === '' ? added : `${held} ${added}`;
180
177
  }
181
- function camel(id) {
182
- return id.replace(/-([a-z0-9])/g, (_, one) => one.toUpperCase());
183
- }
184
- export function element(name, fn, own) {
185
- const arranged = ELEMENTS.declares.find((row) => row.id === idOf(name) && 'sets' in row);
186
- const laid = own ?? (arranged === undefined ? undefined : classOfElement(arranged.id));
187
- const Made = (props) => fn(props, wearing(laid, props.className));
178
+ export function element(name, fn, own, wears) {
179
+ const Made = (props) => fn(props, wearing(own, props.className));
188
180
  named(Made, name);
189
- for (const row of VARIANTS.declares) {
190
- if (!row.wears.includes(name))
191
- continue;
192
- const worn = wearing(laid, classOf(row.id));
181
+ for (const [member, dressed] of Object.entries(wears)) {
182
+ const worn = wearing(own, dressed);
193
183
  const Worn = (props) => fn(props, wearing(worn, props.className));
194
- named(Worn, `${name}.${camel(row.id)}`);
195
- Object.defineProperty(Made, camel(row.id), { value: Worn, enumerable: false, configurable: true });
184
+ named(Worn, `${name}.${member}`);
185
+ Object.defineProperty(Made, member, { value: Worn, enumerable: false, configurable: true });
196
186
  }
197
187
  return Made;
198
188
  }
@@ -204,10 +194,60 @@ export function composition(own, fn) {
204
194
  named(Made, own);
205
195
  return Made;
206
196
  }
197
+ const ViewsContext = createContext(null);
198
+ const NO_SCREEN = 'No screen. A view is read for the screen that binds it, so anything reading one renders inside a screen, ' +
199
+ 'and a body outside every screen has nothing to read from.';
200
+ export function useView(id) {
201
+ const held = useContext(ViewsContext);
202
+ if (held === null)
203
+ throw new Error(NO_SCREEN);
204
+ const named = Object.keys(held);
205
+ if (!named.includes(id)) {
206
+ throw new Error(`"${id}" is not a view this screen shows. A screen states every read it binds in its own views list, ` +
207
+ `and this one states ${named.length === 0 ? 'none' : named.join(', ')}. Name it there — nothing fetches on its own.`);
208
+ }
209
+ return held[id];
210
+ }
211
+ function useViews(views) {
212
+ const { dispatch } = useUi();
213
+ const named = views.join(' ');
214
+ const [held, setHeld] = useState({});
215
+ const [wrong, setWrong] = useState(undefined);
216
+ useEffect(() => {
217
+ const wanted = named === '' ? [] : named.split(' ');
218
+ if (wanted.length === 0)
219
+ return undefined;
220
+ if (dispatch === undefined)
221
+ throw new Error(NO_DISPATCH);
222
+ let live = true;
223
+ void Promise.all(wanted.map(async (one) => [one, await dispatch(one)]))
224
+ .then((all) => {
225
+ if (live)
226
+ setHeld(Object.fromEntries(all));
227
+ })
228
+ .catch((thrown) => {
229
+ if (live)
230
+ setWrong(thrown instanceof Error ? thrown : new Error(String(thrown)));
231
+ });
232
+ return () => {
233
+ live = false;
234
+ };
235
+ }, [named, dispatch]);
236
+ const all = useMemo(() => {
237
+ const found = {};
238
+ for (const one of named === '' ? [] : named.split(' '))
239
+ found[one] = held[one];
240
+ return found;
241
+ }, [named, held]);
242
+ if (wrong !== undefined)
243
+ throw wrong;
244
+ return all;
245
+ }
207
246
  export function screen(own, options, fn) {
208
247
  const Screen = (props) => {
209
248
  useRenderLoopDetector(own, props);
210
- const laid = _jsx("div", { className: own, children: fn(props) });
249
+ const shown = useViews(options.views ?? []);
250
+ const laid = (_jsx(ViewsContext.Provider, { value: shown, children: _jsx("div", { className: own, children: fn(props) }) }));
211
251
  if (options.as === undefined)
212
252
  return laid;
213
253
  return (_jsx(Presented, { presentation: options.as, ...(props.open === undefined ? {} : { open: props.open }), ...(props.onDismiss === undefined ? {} : { onDismiss: props.onDismiss }), children: laid }));
@@ -0,0 +1,426 @@
1
+ 'use client'
2
+
3
+ import { createContext, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
4
+
5
+ import type { Collector } from './collector.ts'
6
+ import type { ArrangementElement, VariantsFor, Wearing } from './checked.ts'
7
+
8
+ import { PRESENTATION_BEHAVIOUR, type Clock, type Presentation, type Surface, type Ticker } from './door.ts'
9
+ import { holds as declared, selectorOf } from './signals.ts'
10
+
11
+ declare module 'react' {
12
+ interface HTMLAttributes<T> {
13
+ readonly at?: string
14
+ }
15
+ interface SVGAttributes<T> {
16
+ readonly at?: string
17
+ }
18
+ }
19
+ import { Presented } from './presented.tsx'
20
+ import { useRenderLoopDetector } from './render-loop.ts'
21
+
22
+ export interface Appearance {
23
+ readonly mode: 'light' | 'dark'
24
+
25
+ readonly reduceMotion: boolean
26
+ }
27
+
28
+ export interface UiConfig {
29
+
30
+ readonly palette: { readonly accent: string; readonly surface: string }
31
+
32
+ readonly translate: (key: string, params?: Readonly<Record<string, unknown>>) => string
33
+ readonly appearance: Appearance
34
+ readonly clock: Clock
35
+
36
+ readonly ticker?: Ticker
37
+
38
+ readonly now?: () => number
39
+
40
+ readonly edgeScroll?: { readonly band?: number; readonly speed?: number }
41
+
42
+ readonly collector?: Collector
43
+
44
+ readonly platform?: string
45
+
46
+ readonly dispatch?: (name: string, input?: Readonly<Record<string, unknown>>) => Promise<unknown>
47
+
48
+ readonly navigateTo?: (screen: string) => void
49
+
50
+ readonly actor?: { readonly id: string | null; readonly roles: readonly string[]; readonly entitlements: readonly string[] }
51
+
52
+ readonly asking?: readonly { readonly id: string; readonly selector: string }[]
53
+
54
+ readonly holding?: readonly string[]
55
+ }
56
+
57
+ export const UiContext = createContext<UiConfig | null>(null)
58
+
59
+ const MISSING =
60
+ 'No UI context. Every element resolves its look, its text and its clock through the door, so it ' +
61
+ 'must render inside <UiProvider>. An element rendering without one would silently have no theme ' +
62
+ 'and no locale — which looks like a styling bug and is a wiring bug.'
63
+
64
+ export function UiProvider({ config, children }: { config: UiConfig; children: ReactNode }): ReactNode {
65
+ return <UiContext.Provider value={config}>{children}</UiContext.Provider>
66
+ }
67
+
68
+ export function useUi(): UiConfig {
69
+ const config = useContext(UiContext)
70
+ if (config === null) throw new Error(MISSING)
71
+ return config
72
+ }
73
+
74
+ export interface SurfaceRequest {
75
+
76
+ readonly className?: string
77
+
78
+ readonly invokes?: { readonly dispatch?: string; readonly input?: Readonly<Record<string, unknown>>; readonly on?: 'press' | 'shown' }
79
+
80
+ readonly holds?: readonly ((typeof declared)[number]['id'] | (string & {}))[]
81
+
82
+ readonly raises?: readonly ((typeof declared)[number]['id'] | (string & {}))[]
83
+
84
+ readonly animates?: boolean
85
+
86
+ readonly measures?: Readonly<Record<string, string | number>>
87
+ }
88
+
89
+ export function useSurface(request: SurfaceRequest = {}): Surface {
90
+ const { translate, appearance, clock, asking, holding, dispatch: run } = useUi()
91
+ const [t, setT] = useState(0)
92
+
93
+ const animates = request.animates === true && !appearance.reduceMotion
94
+
95
+ useEffect(() => {
96
+ if (!animates) return undefined
97
+ return clock.subscribe(setT)
98
+ }, [animates, clock])
99
+
100
+ const gated = (request.raises ?? []).filter((one) => (holding ?? []).includes(one))
101
+ const holds = gated.length === 0 ? (request.holds ?? []) : [...new Set([...(request.holds ?? []), ...gated])]
102
+
103
+ const held = new Set<string>(holds)
104
+ const raised = (signal: string): boolean => held.has(signal)
105
+
106
+ const a11y: Record<string, string | boolean | number> = {}
107
+ if (raised('loading') || raised('pending')) a11y['aria-busy'] = true
108
+
109
+ const marks: Record<string, boolean> = {}
110
+ for (const one of holds) {
111
+ const selector = selectorOf(one) ?? (asking ?? []).find((row) => row.id === one)?.selector
112
+ if (selector === undefined || !selector.startsWith('[data-')) continue
113
+ marks[selector.slice(1, -1)] = true
114
+ }
115
+
116
+ const [arriving, setArriving] = useState(true)
117
+ if (arriving) marks['data-arriving'] = true
118
+
119
+ const began = useRef(false)
120
+ useEffect(() => {
121
+ if (!arriving) return undefined
122
+ let live = true
123
+ const outer = requestAnimationFrame(() => {
124
+ requestAnimationFrame(() => {
125
+ if (live && !began.current) setArriving(false)
126
+ })
127
+ })
128
+ return () => {
129
+ live = false
130
+ cancelAnimationFrame(outer)
131
+ }
132
+ }, [arriving])
133
+
134
+ const measures: Record<string, string | number> = {}
135
+ for (const [name, value] of Object.entries(request.measures ?? {})) measures[`--${name}`] = value
136
+
137
+ const asked = request.invokes
138
+ const named = asked?.dispatch
139
+ const carried = asked?.input
140
+ const when = asked?.on ?? 'press'
141
+
142
+ useEffect(() => {
143
+ if (named === undefined || when !== 'shown') return
144
+ if (asking === undefined) return
145
+ void run?.(named, carried)
146
+ }, [named, when, carried, run, asking])
147
+
148
+ const invokes: Record<string, unknown> = {
149
+
150
+ onAnimationStart: (event: { readonly target: unknown; readonly currentTarget: unknown }) => {
151
+ if (event.target !== event.currentTarget) return
152
+ began.current = true
153
+ },
154
+ onAnimationEnd: (event: { readonly target: unknown; readonly currentTarget: unknown }) => {
155
+ if (event.target !== event.currentTarget) return
156
+ setArriving(false)
157
+ },
158
+ ...(named === undefined || when === 'shown'
159
+ ? {}
160
+ : {
161
+ onClick: () => {
162
+ if (run === undefined) throw new Error(`\`${named}\` cannot be invoked from here — no dispatch is wired`)
163
+ void run(named, carried)
164
+ },
165
+ }),
166
+ }
167
+
168
+ return {
169
+ ...(request.className === undefined ? {} : { className: request.className }),
170
+ holds,
171
+ marks,
172
+ measures,
173
+ invokes,
174
+
175
+ marked: raised,
176
+ text: translate,
177
+ a11y,
178
+ t: animates ? t : 0,
179
+ }
180
+ }
181
+
182
+ export type Press =
183
+ | { readonly dispatch: string; readonly input?: Readonly<Record<string, unknown>>; readonly navigateTo?: never; readonly onPress?: never }
184
+ | { readonly navigateTo: string; readonly dispatch?: never; readonly input?: never; readonly onPress?: never }
185
+ | { readonly onPress: () => void; readonly dispatch?: never; readonly navigateTo?: never; readonly input?: never }
186
+ | { readonly dispatch?: never; readonly navigateTo?: never; readonly input?: never; readonly onPress?: never }
187
+
188
+ export function pressOf(props: {
189
+ readonly dispatch?: string
190
+ readonly input?: Readonly<Record<string, unknown>>
191
+ readonly navigateTo?: string
192
+ readonly onPress?: () => void
193
+ }): Press {
194
+ if (props.onPress !== undefined) return { onPress: props.onPress }
195
+ if (props.navigateTo !== undefined) return { navigateTo: props.navigateTo }
196
+ if (props.dispatch === undefined) return {}
197
+ return props.input === undefined ? { dispatch: props.dispatch } : { dispatch: props.dispatch, input: props.input }
198
+ }
199
+
200
+ export interface PressOrigin {
201
+
202
+ readonly element: string
203
+
204
+ readonly name?: string
205
+ }
206
+
207
+ const NO_DISPATCH =
208
+ 'No dispatch. An operation runs one way here and that way is `dispatch`, handed the name, so a thing that names ' +
209
+ 'one must render inside a <UiProvider> whose config supplies it. Without it the press has nowhere to go, which ' +
210
+ 'reads as a dead control and is a wiring gap.'
211
+
212
+ const NO_NAVIGATE =
213
+ 'No navigateTo. Moving between screens is its own thing, supplied beside dispatch rather than through it, so a ' +
214
+ 'thing that names a screen must render inside a <UiProvider> whose config supplies it.'
215
+
216
+ export function usePress(press: Press, origin?: PressOrigin): (() => void) | undefined {
217
+ const { collector, dispatch, navigateTo } = useUi()
218
+
219
+ const reported = (run: () => void): (() => void) => {
220
+ if (collector === undefined || origin === undefined) return run
221
+ return () => {
222
+ collector.record({
223
+ property: 'interaction',
224
+ element: origin.element,
225
+ ...(origin.name === undefined ? {} : { name: origin.name }),
226
+ action: 'pressed',
227
+ })
228
+ run()
229
+ }
230
+ }
231
+
232
+ if (press.onPress !== undefined) return reported(press.onPress)
233
+
234
+ if (press.navigateTo !== undefined) {
235
+ const where = press.navigateTo
236
+ return reported(() => {
237
+ if (navigateTo === undefined) throw new Error(NO_NAVIGATE)
238
+ navigateTo(where)
239
+ })
240
+ }
241
+
242
+ if (press.dispatch !== undefined) {
243
+ const named = press.dispatch
244
+ const input = press.input
245
+ return reported(() => {
246
+ if (dispatch === undefined) throw new Error(NO_DISPATCH)
247
+ void dispatch(named, input)
248
+ })
249
+ }
250
+
251
+ return undefined
252
+ }
253
+
254
+ export function useVisibility(active: boolean): (node: HTMLElement | null) => void {
255
+ const { clock } = useUi()
256
+ return useMemo(() => {
257
+ return (node: HTMLElement | null): void => {
258
+ if (!active || node === null || typeof IntersectionObserver !== 'function') return
259
+
260
+ const observer = new IntersectionObserver(([entry]) => {
261
+ clock.setVisible(entry?.isIntersecting ?? true)
262
+ })
263
+ observer.observe(node)
264
+ }
265
+ }, [active, clock])
266
+ }
267
+
268
+ export interface ScreenOptions {
269
+ readonly as?: Presentation
270
+ readonly views?: readonly string[]
271
+
272
+ readonly unreachable?: boolean
273
+ }
274
+
275
+ type Component<P> = (props: P) => ReactNode
276
+
277
+ export type ElementBody<P> = (props: P, className?: string) => ReactNode
278
+
279
+ function wearing(held: string | undefined, added: string | undefined): string | undefined {
280
+ if (added === undefined) return held
281
+ return held === undefined || held === '' ? added : `${held} ${added}`
282
+ }
283
+
284
+ type Members<N extends string, P> = {
285
+ readonly [K in
286
+ | VariantsFor<N>
287
+ | { [W in keyof Wearing]: N extends Wearing[W] ? W : never }[keyof Wearing]]: Component<
288
+ P & { readonly at?: string }
289
+ >
290
+ }
291
+
292
+ export type Element<N extends string, P> = [keyof Members<N, P>] extends [never]
293
+ ? Component<P & { readonly at?: string }>
294
+ : N extends ArrangementElement
295
+ ? Component<P & { readonly at?: string }> & Members<N, P>
296
+ : Members<N, P>
297
+
298
+ export function element<N extends string, P>(
299
+ name: N,
300
+ fn: ElementBody<P>,
301
+ own: string,
302
+ wears: Readonly<Record<string, string>>,
303
+ ): Element<N, P> {
304
+
305
+ const Made = (props: P & { readonly className?: string }): ReactNode => fn(props, wearing(own, props.className))
306
+ named(Made, name)
307
+
308
+ for (const [member, dressed] of Object.entries(wears)) {
309
+ const worn = wearing(own, dressed)
310
+ const Worn = (props: P & { readonly className?: string }): ReactNode =>
311
+ fn(props, wearing(worn, props.className))
312
+ named(Worn, `${name}.${member}`)
313
+ Object.defineProperty(Made, member, { value: Worn, enumerable: false, configurable: true })
314
+ }
315
+
316
+ return Made as unknown as Element<N, P>
317
+ }
318
+
319
+ function named(component: unknown, value: string): void {
320
+ Object.defineProperty(component, 'displayName', { value, enumerable: false, configurable: true })
321
+ }
322
+
323
+ export function composition<P>(
324
+ own: string,
325
+ fn: (props: P) => ReactNode,
326
+ ): Component<P & { readonly at?: string; readonly className?: string }> {
327
+
328
+ const Made = (props: P & { readonly className?: string }): ReactNode => (
329
+ <div className={wearing(own, props.className)}>{fn(props)}</div>
330
+ )
331
+ named(Made, own)
332
+ return Made
333
+ }
334
+
335
+ export interface ScreenControls {
336
+
337
+ readonly open?: boolean
338
+ readonly onDismiss?: () => void
339
+
340
+ readonly at?: string
341
+ }
342
+
343
+ const ViewsContext = createContext<Readonly<Record<string, unknown>> | null>(null)
344
+
345
+ const NO_SCREEN =
346
+ 'No screen. A view is read for the screen that binds it, so anything reading one renders inside a screen, ' +
347
+ 'and a body outside every screen has nothing to read from.'
348
+
349
+ export function useView<T>(id: string): T | undefined {
350
+ const held = useContext(ViewsContext)
351
+ if (held === null) throw new Error(NO_SCREEN)
352
+ const named = Object.keys(held)
353
+ if (!named.includes(id)) {
354
+ throw new Error(
355
+ `"${id}" is not a view this screen shows. A screen states every read it binds in its own views list, ` +
356
+ `and this one states ${named.length === 0 ? 'none' : named.join(', ')}. Name it there — nothing fetches on its own.`,
357
+ )
358
+ }
359
+ return held[id] as T | undefined
360
+ }
361
+
362
+ function useViews(views: readonly string[]): Readonly<Record<string, unknown>> {
363
+ const { dispatch } = useUi()
364
+ const named = views.join(' ')
365
+ const [held, setHeld] = useState<Readonly<Record<string, unknown>>>({})
366
+ const [wrong, setWrong] = useState<Error | undefined>(undefined)
367
+
368
+ useEffect(() => {
369
+ const wanted = named === '' ? [] : named.split(' ')
370
+ if (wanted.length === 0) return undefined
371
+ if (dispatch === undefined) throw new Error(NO_DISPATCH)
372
+ let live = true
373
+ void Promise.all(wanted.map(async (one) => [one, await dispatch(one)] as const))
374
+ .then((all) => {
375
+ if (live) setHeld(Object.fromEntries(all))
376
+ })
377
+ .catch((thrown: unknown) => {
378
+ if (live) setWrong(thrown instanceof Error ? thrown : new Error(String(thrown)))
379
+ })
380
+ return () => {
381
+ live = false
382
+ }
383
+ }, [named, dispatch])
384
+
385
+ const all = useMemo(() => {
386
+ const found: Record<string, unknown> = {}
387
+ for (const one of named === '' ? [] : named.split(' ')) found[one] = held[one]
388
+ return found
389
+ }, [named, held])
390
+
391
+ if (wrong !== undefined) throw wrong
392
+ return all
393
+ }
394
+
395
+ export function screen<P>(own: string, options: ScreenOptions, fn: Component<P>): Component<P & ScreenControls> {
396
+ const Screen = (props: P & ScreenControls): ReactNode => {
397
+
398
+ useRenderLoopDetector(own, props as Record<string, unknown>)
399
+
400
+ const shown = useViews(options.views ?? [])
401
+
402
+ const laid = (
403
+ <ViewsContext.Provider value={shown}>
404
+ <div className={own}>{fn(props)}</div>
405
+ </ViewsContext.Provider>
406
+ )
407
+
408
+ if (options.as === undefined) return laid
409
+
410
+ return (
411
+ <Presented
412
+ presentation={options.as}
413
+ {...(props.open === undefined ? {} : { open: props.open })}
414
+ {...(props.onDismiss === undefined ? {} : { onDismiss: props.onDismiss })}
415
+ >
416
+ {laid}
417
+ </Presented>
418
+ )
419
+ }
420
+ Screen.displayName = own
421
+ return Screen
422
+ }
423
+
424
+ export function behaviourOf(presentation: Presentation): (typeof PRESENTATION_BEHAVIOUR)[Presentation] {
425
+ return PRESENTATION_BEHAVIOUR[presentation]
426
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prestack/thread-ui",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -11,6 +11,12 @@
11
11
  "./*": "./dist/*.js"
12
12
  },
13
13
  "dependencies": {
14
- "@prestack/thread": "0.0.1"
14
+ "@floating-ui/dom": "^1.7.0",
15
+ "@floating-ui/react-dom": "^2.1.0",
16
+ "react": "^19.0.0",
17
+ "react-dom": "^19.0.0",
18
+ "typescript": "^6.0.0",
19
+ "zod": "^4.5.4",
20
+ "@prestack/thread": "^0.1.0"
15
21
  }
16
22
  }
@@ -1 +0,0 @@
1
- export {};