@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
@@ -1,19 +1,12 @@
1
- var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
- if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
- return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
- return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
- });
6
- }
7
- return path;
8
- };
9
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
- import { basename, dirname, join } from 'node:path';
11
- import { stdout } from 'node:process';
12
- import { pathToFileURL } from 'node:url';
13
- import { wrapped } from "@prestack/thread/core/asking";
1
+ import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from "@prestack/thread/core/files";
2
+ import { basename, dirname, join, relative } from 'node:path';
3
+ import { argv, stdout } from 'node:process';
4
+ import { TYPED, wrapped } from "@prestack/thread/core/answers";
14
5
  import { exit, invoked } from "@prestack/thread/core/invoked";
6
+ import { valuesIn } from "@prestack/thread/core/rows";
15
7
  import { named as packageOf } from "@prestack/thread/core/scope";
16
8
  import { findTree, inTree } from "@prestack/thread/core/tree";
9
+ import { compiled } from "./generators/react.js";
17
10
  export const TEACHES = [
18
11
  ' what the body may reach, and how a premium state is written:',
19
12
  '',
@@ -21,23 +14,27 @@ export const TEACHES = [
21
14
  ' the row that declared itself the caller — and `translate`, `clock` and `dispatch`. Name',
22
15
  ' it in the body and the import is written for you.',
23
16
  '',
24
- ' A premium state is DATA, not a permission. The row says what somebody IS; a grant says',
25
- ' what they may RUN; a variant says how a thing LOOKS. So a locked look is a signal set',
26
- ' from a value you already have, so the hook is read where it is used:',
17
+ ' A premium look is not decided at the call site. A grant already says who may run an',
18
+ ' operation, in words your own entity declares, and a signal that names that operation',
19
+ ' holds for exactly the callers the grant refuses. The rule is written once, on the',
20
+ ' grant, and every screen follows it:',
27
21
  '',
28
- ' <FeatureBox.paywalled locked={useUi().actor?.roles.includes(\'role_paid\') !== true}>',
22
+ ' <FeatureBox.paywalled>',
29
23
  ' <Text k=\'feature.title\' />',
30
24
  ' </FeatureBox.paywalled>',
31
25
  '',
32
26
  ' Read it in three parts, and each is its own command:',
33
27
  '',
34
- ' `locked` is a boolean prop because the ELEMENT declares it raises that signal —',
35
- ' `thread create element` with `raises: locked`. A signal this app declares cannot be',
36
- ' raised on an element that never said it raises one, so `Box` will not take it.',
28
+ ' `locked` is a signal the ELEMENT declares it raises — `thread create element` with',
29
+ ' `raises: locked`. A signal this app declares cannot be raised on an element that',
30
+ ' never said it raises one, so `Box` will not take it. Nothing passes it here: it is',
31
+ ' set for you wherever the gate refuses the operation the signal names.',
37
32
  ' `.paywalled` is a variant worn as the TAG, never passed as a prop, and it is what',
38
33
  ' carries the look — `thread create variant` with a `state: locked` and what it sets.',
39
- ' `useUi().actor` is who is asking, with the roles read off the row that declared',
40
- ' itself the caller in `thread create entity`.',
34
+ ' Declare that state on a MATERIAL instead and every variant made of it locks the same',
35
+ ' way, which is how one overlay dresses a whole app from one row.',
36
+ ' The rule itself is `thread grant`, and the words in it are options on the value your',
37
+ ' entity names as saying what a caller may do — `thread create entity`.',
41
38
  '',
42
39
  ' THE MEMBER IS THE VARIANT\'S WHOLE ID, CAMELCASED — never the part after the element',
43
40
  ' name. A variant `button-fill` is worn `<Button.buttonFill>`, and `<Button.fill>` is not',
@@ -78,6 +75,22 @@ export const TEACHES = [
78
75
  ' A name no screen has and a destination not written out as a name are each refused there.',
79
76
  ' Never an `href` and never',
80
77
  ' a router: a move the map cannot see is a screen the map reads as unreachable.',
78
+ '',
79
+ ' how a screen shows data:',
80
+ '',
81
+ ' A screen NAMES the reads it binds, and never fetches. `views` takes the ids of declared',
82
+ ' views, the framework runs each one, and `useView` hands back what came out:',
83
+ '',
84
+ ' export const Orders = screen(\'orders\', { as: \'page\', views: [\'vw_orders\'] }, () => (',
85
+ ' <Each rows={useView<readonly Row[]>(\'vw_orders\') ?? []} by="id">',
86
+ ' {(row) => <Text k="orders.line" with={{ name: row.name }} />}',
87
+ ' </Each>',
88
+ ' ))',
89
+ '',
90
+ ' `useView` comes through the same door as `useUi`, and it refuses a name the screen did not',
91
+ ' bind. `thread routes` refuses a view this app declares no operation for, and writes what each',
92
+ ' screen shows into the map. A fetch of your own, or a hook you wrote, is how a screen ends up',
93
+ ' showing something nothing declared, and there is no second way in.',
81
94
  ];
82
95
  export const REACHES = /\buseUi\s*\(/;
83
96
  const here = 'thread/ui/compositions.ts';
@@ -113,29 +126,25 @@ export function nameOf(at) {
113
126
  const file = at.split('\\').join('/').split('/').at(-1) ?? at;
114
127
  return PASCAL(file.replace(/^cmp_/, '').replace(/\.[^.]+$/, ''));
115
128
  }
116
- async function sheetOf(tree, file, ...named) {
117
- try {
118
- const held = (await import(__rewriteRelativeImportExtension(pathToFileURL(inTree(tree, file)).href)));
119
- for (const one of named) {
120
- const found = held[one]?.declares;
121
- if (found !== undefined)
122
- return found;
123
- }
124
- return [];
125
- }
126
- catch {
129
+ function sheetOf(tree, file, ...named) {
130
+ const at = inTree(tree, file);
131
+ if (!existsSync(at))
127
132
  return [];
133
+ const held = valuesIn(readFileSync(at));
134
+ for (const one of named) {
135
+ const found = held[one]?.declares;
136
+ if (found !== undefined)
137
+ return found;
128
138
  }
139
+ return [];
129
140
  }
130
141
  const camelOf = (id) => id.replace(/-([a-z0-9])/g, (_, one) => one.toUpperCase());
131
142
  const sentence = (one, pad) => String(one.describes ?? '').trim() === '' ? [] : wrapped(String(one.describes), pad);
132
- export async function catalogue(tree, making) {
133
- const [elements, variants, compositions, screens] = await Promise.all([
134
- sheetOf(tree, `elements.${'ts'}`, 'ELEMENT', 'ELEMENTS'),
135
- sheetOf(tree, `variants.${'ts'}`, 'VARIANT', 'VARIANTS'),
136
- sheetOf(tree, `compositions.${'ts'}`, 'COMPOSITION', 'COMPOSITIONS'),
137
- making === 'screen' ? sheetOf(tree, `screens.${'ts'}`, 'SCREEN', 'SCREENS') : Promise.resolve([]),
138
- ]);
143
+ function listed(tree, making) {
144
+ const elements = sheetOf(tree, `elements.${'ts'}`, 'ELEMENT', 'ELEMENTS');
145
+ const variants = sheetOf(tree, `variants.${'ts'}`, 'VARIANT', 'VARIANTS');
146
+ const compositions = sheetOf(tree, `compositions.${'ts'}`, 'COMPOSITION', 'COMPOSITIONS');
147
+ const screens = making === 'screen' ? sheetOf(tree, `screens.${'ts'}`, 'SCREEN', 'SCREENS') : [];
139
148
  const worn = new Map();
140
149
  for (const one of variants) {
141
150
  for (const each of one.wears ?? []) {
@@ -180,16 +189,18 @@ export async function catalogue(tree, making) {
180
189
  '',
181
190
  ]),
182
191
  ' Materials, style guide values, fonts and animations are chosen inside a VARIANT, never inside',
183
- ' a body — `npx thread create variant` prints every one of them at the moment one is needed.',
192
+ ` a body — \`${TYPED} create variant\` prints every one of them at the moment one is needed.`,
184
193
  '',
185
- ' Every element and the tags it is written as: `npx thread create element`. Every variant and',
186
- ' the tag it is worn as: `npx thread create variant`. Then open the one nearest to what you are',
187
- ' about to build with `npx thread read <id>` — it already answers how a body is arranged, how a',
194
+ ` Every element and the tags it is written as: \`${TYPED} create element\`. Every variant and`,
195
+ ` the tag it is worn as: \`${TYPED} create variant\`. Then open the one nearest to what you are`,
196
+ ` about to build with \`${TYPED} read <id>\` — it already answers how a body is arranged, how a`,
188
197
  ' variant is worn and how a control reaches an operation, and a file cannot be wrong in the way',
189
198
  ' a remembered example can.',
190
199
  '',
191
200
  ];
192
201
  }
202
+ export const catalogue = async () => listed(findTree(), 'composition');
203
+ export const screening = async () => listed(findTree(), 'screen');
193
204
  const TAG = /<([A-Z][A-Za-z0-9]*)/g;
194
205
  export function composed(body, rows, from, except) {
195
206
  const named = new Map();
@@ -210,8 +221,10 @@ export function composed(body, rows, from, except) {
210
221
  });
211
222
  }
212
223
  const IMPORTS = /^import\s[\s\S]*?from\s*['"]([^'"]+)['"]/gm;
213
- export const DOORS = new Set(['react', `${packageOf('ui')}/wrappers`, `${packageOf('ui')}/drag`]);
214
- const SHIPPED = new RegExp(`^${packageOf('ui')}/elements/([a-z0-9-]+)$`);
224
+ const FRAMEWORK = 'thread/';
225
+ export const HOME = `${packageOf('ui')}/${here.slice(FRAMEWORK.length).split('/').slice(0, -1).join('/')}`;
226
+ export const DOORS = new Set(['react', `${HOME}/wrappers`, `${HOME}/drag`]);
227
+ const SHIPPED = new RegExp(`^${HOME}/elements/([a-z0-9-]+)$`);
215
228
  const slashed = (path) => path.split('\\').join('/');
216
229
  export function reaching(at, text, elements, declared) {
217
230
  const here = slashed(at).split('/').slice(0, -1).join('/');
@@ -229,7 +242,192 @@ export function reaching(at, text, elements, declared) {
229
242
  }
230
243
  return [...found].sort();
231
244
  }
232
- export async function undeclared(tree, rows) {
245
+ const CLAUSE = /^import\s+(?!type\s)([\s\S]*?)\s*from\s*['"][^'"]+['"]/gm;
246
+ const WORD = /(?:^|[,{]|\s)(?:[A-Za-z_$][\w$]*\s+as\s+)?([A-Za-z_$][\w$]*)/g;
247
+ const NOT_A_NAME = new Set(['type', 'as', 'from', 'import']);
248
+ const KEPT = 'a composition keeps no state of its own. State belongs to the element that raises the signal, in the works file beside it';
249
+ const RUNS = 'nothing here runs on its own. A read is bound by the screen in its own views list, and a press is answered by dispatch';
250
+ const REFUSED = {
251
+ useState: KEPT,
252
+ useReducer: KEPT,
253
+ useSyncExternalStore: KEPT,
254
+ useEffect: RUNS,
255
+ useLayoutEffect: RUNS,
256
+ element: 'only the compiler writes an element, and a file that calls it turns off the checks that keep this one declared',
257
+ };
258
+ export const COMPILED = 'app/_generated/compiled';
259
+ export function compiledAt(at) {
260
+ return `${COMPILED}/${slashed(at)}`;
261
+ }
262
+ const ENDINGS = ['.tsx', '.ts'];
263
+ function beside(root, where, said) {
264
+ if (!said.startsWith(`${HOME}/`))
265
+ return undefined;
266
+ const tail = said.slice(HOME.length + 1);
267
+ for (const ending of ENDINGS) {
268
+ const at = `${COMPILED}/ui/${tail}${ending}`;
269
+ if (!existsSync(join(root, at)))
270
+ continue;
271
+ const out = slashed(relative(dirname(where), at));
272
+ return out.startsWith('.') ? out : `./${out}`;
273
+ }
274
+ return undefined;
275
+ }
276
+ const NAMED_IMPORT = /(['"])(@[^'"]+)\1/g;
277
+ export function settled(root, rows, checking, by, kept) {
278
+ const said = [];
279
+ for (const row of rows) {
280
+ const file = join(root, row.at);
281
+ if (!existsSync(file))
282
+ continue;
283
+ const held = compiled(readFileSync(file, 'utf8'), row.at);
284
+ if (held.because !== undefined) {
285
+ said.push(held.because);
286
+ continue;
287
+ }
288
+ const where = compiledAt(row.at);
289
+ kept.add(where);
290
+ const text = (held.text ?? '').replace(NAMED_IMPORT, (whole, quote, said) => {
291
+ const found = beside(root, where, said);
292
+ return found === undefined ? whole : `${quote}${found}${quote}`;
293
+ });
294
+ const at = join(root, where);
295
+ if (!checking) {
296
+ writeFileSync(at, text, 'utf8');
297
+ continue;
298
+ }
299
+ if (existsSync(at) && readFileSync(at, 'utf8') === text)
300
+ continue;
301
+ said.push(`${where} is not what the compiler writes from ${row.at}. A compiled file that differs from its ` +
302
+ `source is a hand edit wearing a generated name. Run \`${by}\` and commit what it writes.`);
303
+ }
304
+ return said;
305
+ }
306
+ const ORG = packageOf('ui').split('/')[0] ?? '';
307
+ const REACHED = /^(?:import|export)\s+(type\s+)?(?:(?:[^\n]|\n(?=[ \t}]))*?\s)?from\s*['"]([^'"]+)['"]/gm;
308
+ export const RUNTIME = ['wrappers.tsx', 'drag.ts', 'provider.tsx', 'islands/map.ts'];
309
+ const NODE = /^node:/;
310
+ function installs(root) {
311
+ try {
312
+ const held = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
313
+ return new Set([...Object.keys(held.dependencies ?? {}), ...Object.keys(held.devDependencies ?? {})]);
314
+ }
315
+ catch {
316
+ return new Set();
317
+ }
318
+ }
319
+ function packaging(said) {
320
+ return said.startsWith('@') ? said.split('/').slice(0, 2).join('/') : (said.split('/')[0] ?? said);
321
+ }
322
+ export function emitting(root, entries, checking, kept) {
323
+ const has = installs(root);
324
+ const from = join(root, 'node_modules', packageOf('ui'), 'dist', 'ui');
325
+ const said = [];
326
+ if (!existsSync(from)) {
327
+ return [
328
+ `this app declares an interface and ${packageOf('ui')} is not installed, so there is nothing to emit it from. ` +
329
+ `Install it and run \`thread attach ${packageOf('ui')}\` again.`,
330
+ ];
331
+ }
332
+ const seen = new Set();
333
+ const walk = (at) => {
334
+ if (seen.has(at))
335
+ return;
336
+ seen.add(at);
337
+ const file = join(from, at);
338
+ if (!existsSync(file)) {
339
+ said.push(`${at} is part of the interface this app emits, and the installed ${packageOf('ui')} does not hold it. ` +
340
+ 'Upgrade the framework packages and run this again.');
341
+ return;
342
+ }
343
+ const text = readFileSync(file, 'utf8');
344
+ const where = `${COMPILED}/ui/${at}`;
345
+ kept.add(where);
346
+ const to = join(root, where);
347
+ if (checking) {
348
+ if (!existsSync(to) || readFileSync(to, 'utf8') !== text) {
349
+ said.push(`${where} is not what this app emits from ${packageOf('ui')}. An emitted file that differs from ` +
350
+ 'its source is a hand edit wearing a generated name. Run `thread compositions` and commit what it writes.');
351
+ }
352
+ }
353
+ else {
354
+ writeFileSync(to, text, 'utf8');
355
+ }
356
+ for (const one of text.matchAll(REACHED)) {
357
+ const named = one[2] ?? '';
358
+ if (named.startsWith('.')) {
359
+ const next = slashed(join(dirname(at), named));
360
+ if (next.startsWith('..')) {
361
+ said.push(`${at} reaches \`${named}\`, which is outside the interface this app emits.`);
362
+ continue;
363
+ }
364
+ walk(next);
365
+ continue;
366
+ }
367
+ if (one[1] !== undefined)
368
+ continue;
369
+ if (!named.startsWith(`${ORG}/`)) {
370
+ const held = packaging(named);
371
+ if (NODE.test(named) || has.has(held))
372
+ continue;
373
+ said.push(`${at} reaches ${held}, and this app installs nothing by that name. An emitted interface brings ` +
374
+ `what it reaches with it, so ${held} has to arrive when ` +
375
+ `thread attach ${packageOf('ui')} runs.`);
376
+ continue;
377
+ }
378
+ said.push(`${at} reaches \`${named}\`, so emitting it would leave this app importing the framework while it runs. ` +
379
+ 'It reaches what is emitted beside it, or nothing of this framework at all.');
380
+ }
381
+ };
382
+ for (const one of entries)
383
+ walk(one);
384
+ return said;
385
+ }
386
+ export function stale(root, kept, checking) {
387
+ const said = [];
388
+ const walk = (at) => {
389
+ const where = join(root, at);
390
+ if (!existsSync(where))
391
+ return;
392
+ for (const each of readdirSync(where, { withFileTypes: true })) {
393
+ const path = `${at}/${each.name}`;
394
+ if (each.isDirectory()) {
395
+ walk(path);
396
+ continue;
397
+ }
398
+ if (kept.has(path))
399
+ continue;
400
+ if (checking) {
401
+ said.push(`${path} sits under ${COMPILED} and nothing declares what it was written from any more. ` +
402
+ 'Run `thread compositions` and it goes.');
403
+ continue;
404
+ }
405
+ rmSync(join(root, path), { force: true });
406
+ }
407
+ };
408
+ walk(COMPILED);
409
+ return said;
410
+ }
411
+ export function handing(at, text) {
412
+ const found = [];
413
+ for (const one of text.matchAll(CLAUSE)) {
414
+ for (const name of (one[1] ?? '').matchAll(WORD)) {
415
+ const held = name[1];
416
+ if (held === undefined || NOT_A_NAME.has(held))
417
+ continue;
418
+ const why = REFUSED[held];
419
+ if (why === undefined)
420
+ continue;
421
+ found.push(`${at} imports \`${held}\`, and ${why}.`);
422
+ }
423
+ }
424
+ if (/\bfetch\s*\(/.test(text)) {
425
+ found.push(`${at} calls \`fetch\`, and nothing here reaches the network by hand. A screen names the reads it ` +
426
+ 'binds in its own views list, and the framework runs each one.');
427
+ }
428
+ return found;
429
+ }
430
+ export function undeclared(tree, rows) {
233
431
  const elements = new Set();
234
432
  const declared = new Set();
235
433
  for (const [file, exported] of [
@@ -240,7 +438,7 @@ export async function undeclared(tree, rows) {
240
438
  const where = inTree(tree, file);
241
439
  if (!existsSync(where))
242
440
  continue;
243
- const sheet = (await import(__rewriteRelativeImportExtension(pathToFileURL(where).href)));
441
+ const sheet = valuesIn(readFileSync(where));
244
442
  for (const row of sheet[exported]?.declares ?? []) {
245
443
  if (exported === 'ELEMENT')
246
444
  elements.add(row.id);
@@ -253,7 +451,9 @@ export async function undeclared(tree, rows) {
253
451
  const file = join(tree.at, row.at);
254
452
  if (!existsSync(file))
255
453
  continue;
256
- for (const one of reaching(row.at, readFileSync(file, 'utf8'), elements, declared)) {
454
+ const text = readFileSync(file, 'utf8');
455
+ found.push(...handing(row.at, text));
456
+ for (const one of reaching(row.at, text, elements, declared)) {
257
457
  found.push(`${row.at} imports \`${one}\`, which this app declares nothing by. A composition reaches ` +
258
458
  'declared elements, other declared compositions, and the door — everything else is a thing ' +
259
459
  'nothing wrote down. Declare it, or take it out and write the body through `thread create`.');
@@ -265,8 +465,8 @@ function starter(row) {
265
465
  const name = nameOf(row.at);
266
466
  return `'use client'
267
467
 
268
- import { Box } from './elements/box.tsx'
269
- import { composition } from './wrappers.tsx'
468
+ import { Box } from '${HOME}/elements/box'
469
+ import { composition } from '${HOME}/wrappers'
270
470
 
271
471
  export const ${name} = composition('${name}', () => <Box.card />)
272
472
  `;
@@ -274,26 +474,50 @@ export const ${name} = composition('${name}', () => <Box.card />)
274
474
  export function seeds(rows, held) {
275
475
  return rows.filter((row) => !held(row.at)).map((row) => ({ path: row.at, content: starter(row) }));
276
476
  }
277
- async function run() {
477
+ function run() {
278
478
  const tree = findTree();
279
479
  const at = inTree(tree, 'compositions.ts');
280
480
  if (!existsSync(at)) {
281
481
  stdout.write(`thread: this tree has no ${tree.name}/${basename(at)}, so it declares no composition and there is nothing to seed or check.\n`);
282
482
  return 0;
283
483
  }
284
- const held = (await import(__rewriteRelativeImportExtension(pathToFileURL(at).href)));
484
+ const held = valuesIn(readFileSync(at));
285
485
  if (held.COMPOSITION === undefined) {
286
486
  stdout.write(`thread: ${tree.name}/compositions.ts exports no \`COMPOSITION\`, so nothing declares a composition here.\n`);
287
487
  return 1;
288
488
  }
289
489
  const rows = held.COMPOSITION.declares ?? [];
490
+ const checking = argv.includes('--check');
290
491
  const made = seeds(rows, (where) => existsSync(join(tree.at, where)));
291
- for (const one of made) {
292
- mkdirSync(dirname(join(tree.at, one.path)), { recursive: true });
293
- writeFileSync(join(tree.at, one.path), one.content, 'utf8');
294
- stdout.write(`thread: seeded ${one.path}\n`);
492
+ const unseeded = made[0];
493
+ if (checking && unseeded !== undefined) {
494
+ stdout.write(`thread: ${unseeded.path} is declared and the file is not there yet. A row names a file, and ` +
495
+ '`--check` never writes one — run `thread compositions` and commit what it seeds.\n');
496
+ return 1;
497
+ }
498
+ if (!checking) {
499
+ for (const one of made) {
500
+ writeFileSync(join(tree.at, one.path), one.content, 'utf8');
501
+ stdout.write(`thread: seeded ${one.path}\n`);
502
+ }
295
503
  }
296
- const loose = await undeclared(tree, rows);
504
+ const parts = sheetOf(tree, 'elements.ts', 'ELEMENT', 'ELEMENTS');
505
+ const shipped = `node_modules/${packageOf('ui')}/dist/ui/`;
506
+ const entries = [
507
+ ...RUNTIME,
508
+ ...parts.flatMap((one) => (one.at ?? '').startsWith(shipped) ? [(one.at ?? '').slice(shipped.length).replace(/\.js$/, '.tsx')] : []),
509
+ ];
510
+ const kept = new Set();
511
+ for (const one of sheetOf(tree, 'screens.ts', 'SCREEN', 'SCREENS')) {
512
+ if (one.at !== undefined && one.at !== '')
513
+ kept.add(compiledAt(one.at));
514
+ }
515
+ const found = [
516
+ ...undeclared(tree, rows),
517
+ ...emitting(tree.at, entries, checking, kept),
518
+ ...settled(tree.at, rows, checking, 'thread compositions', kept),
519
+ ];
520
+ const loose = [...found, ...(found.length === 0 ? stale(tree.at, kept, checking) : [])];
297
521
  for (const one of loose)
298
522
  stdout.write(`thread: ${one}\n`);
299
523
  stdout.write(`thread: ${String(rows.length)} composition(s) declared, ${String(made.length)} seeded, ` +
@@ -302,4 +526,4 @@ async function run() {
302
526
  return loose.length === 0 ? 0 : 1;
303
527
  }
304
528
  if (invoked(import.meta.url))
305
- exit(await run());
529
+ exit(run());