@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
@@ -6,8 +6,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
6
6
  }
7
7
  return path;
8
8
  };
9
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
10
- import { dirname, join, relative } from 'node:path';
9
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from "@prestack/thread/core/files";
10
+ import { join, relative } from 'node:path';
11
11
  import { argv, stderr, stdout } from 'node:process';
12
12
  import { pathToFileURL } from 'node:url';
13
13
  import { exit, invoked } from "@prestack/thread/core/invoked";
@@ -248,7 +248,6 @@ async function run() {
248
248
  return 0;
249
249
  }
250
250
  for (const one of files) {
251
- mkdirSync(dirname(join(at, one.path)), { recursive: true });
252
251
  writeFileSync(join(at, one.path), one.made, 'utf8');
253
252
  }
254
253
  stdout.write(`i18n: ${String(everything.length)} key(s) across ${String(languages.length)} language(s) — ` +
@@ -0,0 +1,300 @@
1
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from '@prestack/thread/core/files'
2
+ import { dirname, join, relative } from 'node:path'
3
+ import { argv, stderr, stdout } from 'node:process'
4
+ import { pathToFileURL } from 'node:url'
5
+
6
+ import { exit, invoked } from '@prestack/thread/core/invoked'
7
+ import { named as packageOf } from '@prestack/thread/core/scope'
8
+ import { findTree, inTree } from '@prestack/thread/core/tree'
9
+
10
+ import { fileFor, languagesIn, sourceIn } from '../localizations.ts'
11
+
12
+ const GENERATED = `app/_generated/wording.${'ts'}`
13
+
14
+ const SKIP = new Set(['node_modules', '.next', '.git', 'dist', '_generated'])
15
+
16
+ const CODE = /\.(tsx?|jsx?)$/
17
+
18
+ export function walk(at: string, found: string[] = [], skip: ReadonlySet<string> = new Set()): readonly string[] {
19
+ for (const entry of readdirSync(at, { withFileTypes: true })) {
20
+ if (SKIP.has(entry.name)) continue
21
+ const path = join(at, entry.name)
22
+ if (skip.has(path)) continue
23
+ if (entry.isDirectory()) walk(path, found, skip)
24
+ else if (CODE.test(entry.name)) found.push(path)
25
+ }
26
+ return found
27
+ }
28
+
29
+ function leftOut(at: string): ReadonlySet<string> {
30
+ try {
31
+ const held = JSON.parse(readFileSync(join(at, 'tsconfig.json'), 'utf8')) as { exclude?: readonly string[] }
32
+ return new Set((held.exclude ?? []).map((one) => join(at, one)))
33
+ } catch {
34
+
35
+ return new Set()
36
+ }
37
+ }
38
+
39
+ const TYPED = /readonly\s+([A-Za-z][A-Za-z0-9]*)\??\s*:\s*MessageKey\b/g
40
+
41
+ export function keyProps(at: string): readonly string[] {
42
+ const found = new Set<string>()
43
+ const home = join(at, 'node_modules', packageOf('ui'))
44
+ if (!existsSync(home)) return []
45
+ for (const file of walk(home)) {
46
+ for (const one of readFileSync(file, 'utf8').matchAll(TYPED)) found.add(one[1] ?? '')
47
+ }
48
+ for (const file of readdirSync(join(home, 'dist'), { withFileTypes: true })) {
49
+ if (!file.name.endsWith('.d.ts')) continue
50
+ for (const one of readFileSync(join(home, 'dist', file.name), 'utf8').matchAll(TYPED)) found.add(one[1] ?? '')
51
+ }
52
+ return [...found].sort()
53
+ }
54
+
55
+ const named = (props: readonly string[]): RegExp =>
56
+ new RegExp(`\\b(?:${props.join('|')})\\s*=\\s*(?:"([^"]+)"|'([^']+)')`, 'g')
57
+
58
+ export function keysIn(text: string, props: readonly string[]): readonly string[] {
59
+ if (props.length === 0) return []
60
+ return [...text.matchAll(named(props))].map((one) => one[1] ?? one[2] ?? '')
61
+ }
62
+
63
+ async function shipped(at: string): Promise<Readonly<Record<string, string>>> {
64
+ const held = (await import('../i18n/catalogs/en.ts')) as { en?: Readonly<Record<string, unknown>> }
65
+ const made: Record<string, string> = {}
66
+ for (const [key, entry] of Object.entries(held.en ?? {})) {
67
+ made[key] = typeof entry === 'string' ? entry : String((entry as { text?: string }).text ?? '')
68
+ }
69
+ void at
70
+ return made
71
+ }
72
+
73
+ export function readWording(at: string, language: string): Readonly<Record<string, string>> {
74
+ const path = join(at, fileFor(language))
75
+ if (!existsSync(path)) return {}
76
+ try {
77
+ const held = JSON.parse(readFileSync(path, 'utf8')) as Readonly<Record<string, unknown>>
78
+ const made: Record<string, string> = {}
79
+ for (const [key, value] of Object.entries(held)) made[key] = typeof value === 'string' ? value : ''
80
+ return made
81
+ } catch (wrong) {
82
+ throw new Error(
83
+ `${fileFor(language)} is there and will not parse, so what this app says in ${language} cannot ` +
84
+ `be known. Nothing was written — a reader that cannot read is not a reader that found nothing.\n ${String(wrong)}`,
85
+ )
86
+ }
87
+ }
88
+
89
+ const asJson = (value: string): string => JSON.stringify(value)
90
+
91
+ export function wordingFile(held: Readonly<Record<string, string>>): string {
92
+ const keys = Object.keys(held).sort()
93
+ return `{\n${keys.map((key) => ` ${asJson(key)}: ${asJson(held[key] ?? '')}`).join(',\n')}\n}\n`
94
+ }
95
+
96
+ function moduleFor(
97
+ languages: readonly { readonly id: string; readonly name: string; readonly flag: string }[],
98
+ source: string,
99
+ used: readonly string[],
100
+ wording: ReadonlyMap<string, Readonly<Record<string, string>>>,
101
+ ): string {
102
+ const catalogue = (language: string): string => {
103
+ const held = wording.get(language) ?? {}
104
+ const keys = Object.keys(held).sort()
105
+ return [
106
+ ` ${asJson(language)}: {`,
107
+ ...keys.map((key) => ` ${asJson(key)}: ${asJson(held[key] ?? '')},`),
108
+ ' },',
109
+ ].join('\n')
110
+ }
111
+
112
+ return [
113
+ '/* GENERATED by thread — do not edit. Change the wording in i18n/, or the rows, and regenerate. */',
114
+ '',
115
+ 'export type Language =',
116
+ ...languages.map((one) => ` | ${asJson(one.id)}`),
117
+ '',
118
+ `export const SOURCE: Language = ${asJson(source)}`,
119
+ '',
120
+ 'export const LANGUAGES: readonly { readonly id: Language; readonly name: string; readonly flag: string }[] = [',
121
+ ...languages.map((one) => ` { id: ${asJson(one.id)}, name: ${asJson(one.name)}, flag: ${asJson(one.flag)} },`),
122
+ ']',
123
+ '',
124
+ '/*',
125
+ ' * Every key this interface names.',
126
+ ' */',
127
+ 'export type Used =',
128
+ ...(used.length === 0 ? [' | never'] : used.map((one) => ` | ${asJson(one)}`)),
129
+ '',
130
+ 'const held = {',
131
+ ...languages.map((one) => catalogue(one.id)),
132
+ '} as const',
133
+ '',
134
+ '/*',
135
+ ' * AN UNANSWERED KEY IS A TYPE ERROR, NAMING THE LANGUAGE AND THE KEY.',
136
+ ' *',
137
+ ' * `Filled` is what makes the compiler one of the surfaces that refuses rather than a formality.',
138
+ ' * The catalogue above is `as const`, so every value is a literal type — an empty one is the',
139
+ ' * literal `""`, which `Filled` maps to `never`, which nothing is assignable to. So a key some',
140
+ ' * language has not answered fails HERE, at the line naming both, before anything is run.',
141
+ ' *',
142
+ ' * Written as an assignment rather than `satisfies`, and that is deliberate: `satisfies` on the',
143
+ ' * literal refuses the EXTRA keys too, and there are always extra ones — an app holds every',
144
+ ' * message it has, and `Used` is only the ones its markup names.',
145
+ ' */',
146
+ "type Filled<S> = S extends '' ? never : S",
147
+ '',
148
+ 'type Answered<L extends Language> = {',
149
+ ' readonly [K in Used]: K extends keyof (typeof held)[L] ? Filled<(typeof held)[L][K]> : never',
150
+ '}',
151
+ '',
152
+ 'export const WORDING: { readonly [L in Language]: Answered<L> } = held',
153
+ '',
154
+ ].join('\n')
155
+ }
156
+
157
+ interface Node {
158
+ readonly holds?: readonly { readonly id: string; readonly is?: string; readonly sets?: Readonly<Record<string, string>> }[]
159
+ readonly declares?: readonly { readonly id: string; readonly is?: string; readonly sets?: Readonly<Record<string, string>> }[]
160
+ }
161
+
162
+ async function rows(at: string): Promise<NonNullable<Node['holds']>> {
163
+ const path = inTree(findTree(), `localizations.${'ts'}`)
164
+ if (!existsSync(path)) {
165
+ stdout.write(`i18n: this tree has no ${relative(at, path)}, so it is in no language and there is no wording to write or check.\n`)
166
+ exit(0)
167
+ }
168
+ const held = (await import(pathToFileURL(path).href)) as { LOCALIZATION?: Node }
169
+ if (held.LOCALIZATION === undefined) {
170
+ throw new Error(
171
+ `${relative(at, path)} exports no \`LOCALIZATION\`, so nothing says what languages this app is ` +
172
+ `in. Run \`thread attach ${packageOf('ui')}\` — the sheet is the package's to write, not yours.`,
173
+ )
174
+ }
175
+ return [...(held.LOCALIZATION.holds ?? []), ...(held.LOCALIZATION.declares ?? [])]
176
+ }
177
+
178
+ async function run(): Promise<number> {
179
+ const tree = findTree()
180
+ const at = tree.at
181
+ const held = (await rows(at)) ?? []
182
+
183
+ const languages = languagesIn(held)
184
+ const source = sourceIn(held)
185
+
186
+ if (languages.length === 0) {
187
+ stderr.write('i18n: no language is declared, so this app is in no language at all. Add one with `thread create language`.\n')
188
+ return 1
189
+ }
190
+ if (source === undefined || !languages.some((one) => one.id === source)) {
191
+ stderr.write(
192
+ `i18n: the source row names \`${String(source)}\`, and that is not one of the declared languages ` +
193
+ `(${languages.map((one) => one.id).join(', ')}). An app is written in one of the languages it is in.\n`,
194
+ )
195
+ return 1
196
+ }
197
+
198
+ const props = keyProps(at)
199
+ if (props.length === 0) {
200
+ stderr.write(
201
+ `i18n: nothing in the installed \`${packageOf('ui')}\` declares a prop of type \`MessageKey\`, so no key could ` +
202
+ 'be recognised in any file. That is a broken install, not an app with no wording.\n',
203
+ )
204
+ return 1
205
+ }
206
+
207
+ const used = new Set<string>()
208
+ const mine = leftOut(at)
209
+ for (const file of walk(at, [], mine)) for (const key of keysIn(readFileSync(file, 'utf8'), props)) used.add(key)
210
+
211
+ const seeded = await shipped(at)
212
+
213
+ const answered = new Set<string>()
214
+ for (const language of languages) {
215
+ for (const key of Object.keys(readWording(at, language.id))) answered.add(key)
216
+ }
217
+
218
+ const everything = [...new Set([...used, ...Object.keys(seeded), ...answered])].sort()
219
+
220
+ const wording = new Map<string, Readonly<Record<string, string>>>()
221
+ for (const language of languages) {
222
+ const already = readWording(at, language.id)
223
+ const made: Record<string, string> = {}
224
+ for (const key of everything) made[key] = already[key] ?? (language.id === source ? (seeded[key] ?? '') : '')
225
+ wording.set(language.id, made)
226
+ }
227
+
228
+ const files = [
229
+ ...languages.map((one) => ({ path: fileFor(one.id), made: wordingFile(wording.get(one.id) ?? {}) })),
230
+ { path: GENERATED, made: moduleFor(languages, source, [...used].sort(), wording) },
231
+ ]
232
+
233
+ const named = new Set(languages.map((one) => fileFor(one.id)))
234
+ const stray: string[] = []
235
+ try {
236
+ for (const entry of readdirSync(join(at, 'i18n'), { withFileTypes: true })) {
237
+ const path = `i18n/${entry.name}`
238
+ if (entry.isDirectory() || named.has(path)) continue
239
+ stray.push(path)
240
+ }
241
+ } catch { }
242
+
243
+ const blank = languages.flatMap((one) =>
244
+ everything.filter((key) => (wording.get(one.id) ?? {})[key] === '').map((key) => `${one.id}: ${key}`),
245
+ )
246
+
247
+ if (argv.includes('--check')) {
248
+ const drifted = files.find((one) => {
249
+ let current = ''
250
+ try {
251
+ current = readFileSync(join(at, one.path), 'utf8')
252
+ } catch {
253
+ current = ''
254
+ }
255
+ return current !== one.made
256
+ })
257
+ if (drifted !== undefined) {
258
+ stderr.write(
259
+ `i18n: ${drifted.path} is not what the declarations produce. Run \`thread i18n\` and fill in what it adds.\n`,
260
+ )
261
+ return 1
262
+ }
263
+ if (stray.length > 0) {
264
+ stderr.write(
265
+ `i18n: ${stray.join(', ')} — no declared language accounts for ${stray.length === 1 ? 'it' : 'them'}, ` +
266
+ 'so it is not wording, it is a file somebody left there. Declare the language with ' +
267
+ '`thread create language`, or delete the file.\n',
268
+ )
269
+ return 1
270
+ }
271
+ if (blank.length > 0) {
272
+ stderr.write(
273
+ `i18n: ${String(blank.length)} key(s) have no wording, and an untranslated key is an error rather ` +
274
+ `than a word that comes out in the wrong language:\n${blank.map((one) => ` ${one}`).join('\n')}\n`,
275
+ )
276
+ return 1
277
+ }
278
+ stdout.write(`i18n: ${String(everything.length)} key(s) answered in ${String(languages.length)} language(s)\n`)
279
+ return 0
280
+ }
281
+
282
+ for (const one of files) {
283
+ writeFileSync(join(at, one.path), one.made, 'utf8')
284
+ }
285
+
286
+ stdout.write(
287
+ `i18n: ${String(everything.length)} key(s) across ${String(languages.length)} language(s) — ` +
288
+ `${String(used.size)} named by this interface, ${String(everything.length - used.size)} shipped with the framework\n`,
289
+ )
290
+ if (blank.length > 0) {
291
+ stdout.write(
292
+ `i18n: ${String(blank.length)} still to write — the gate refuses until they are filled:\n` +
293
+ `${blank.slice(0, 20).map((one) => ` ${one}`).join('\n')}\n` +
294
+ (blank.length > 20 ? ` … and ${String(blank.length - 20)} more\n` : ''),
295
+ )
296
+ }
297
+ return 0
298
+ }
299
+
300
+ if (invoked(import.meta.url)) exit(await run())
@@ -1,16 +1,8 @@
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
1
  import { createRequire } from 'node:module';
10
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from "@prestack/thread/core/files";
3
+ import { valuesIn } from "@prestack/thread/core/rows";
11
4
  import { dirname, join } from 'node:path';
12
5
  import { argv, stderr, stdout } from 'node:process';
13
- import { pathToFileURL } from 'node:url';
14
6
  import { exit } from "@prestack/thread/core/invoked";
15
7
  import { findTree, inTree } from "@prestack/thread/core/tree";
16
8
  import { brandExport, drawnAs, setNamed, sizeRows, SET_ROW } from "../icons.js";
@@ -104,7 +96,7 @@ async function node(file, name) {
104
96
  stdout.write(`icons: this tree has no ${findTree().name}/${file}, so it declares no icon and there is no module to write or check.\n`);
105
97
  exit(0);
106
98
  }
107
- const held = (await import(__rewriteRelativeImportExtension(pathToFileURL(at).href)));
99
+ const held = valuesIn(readFileSync(at));
108
100
  const one = held[name];
109
101
  if (one === undefined)
110
102
  throw new Error(`_threads/${file} exports no \`${name}\`.`);
@@ -178,7 +170,6 @@ async function run() {
178
170
  'its generator is a hand edit wearing a generated name. Run `thread icons` and commit what it writes.\n');
179
171
  return 1;
180
172
  }
181
- mkdirSync(dirname(at), { recursive: true });
182
173
  writeFileSync(at, made, 'utf8');
183
174
  stdout.write(`thread: wrote app/_generated/icons.ts — ${String(drawn.length)} icons drawn from \`${set.name}\`\n`);
184
175
  return 0;
@@ -0,0 +1,214 @@
1
+ import { createRequire } from 'node:module'
2
+
3
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from '@prestack/thread/core/files'
4
+ import { valuesIn } from '@prestack/thread/core/rows'
5
+ import { dirname, join } from 'node:path'
6
+ import { argv, stderr, stdout } from 'node:process'
7
+ import { pathToFileURL } from 'node:url'
8
+
9
+ import { exit } from '@prestack/thread/core/invoked'
10
+ import { findTree, inTree } from '@prestack/thread/core/tree'
11
+
12
+ import { brandExport, drawnAs, setNamed, sizeRows, SET_ROW, type IconSet } from '../icons.ts'
13
+
14
+ interface Row {
15
+ readonly id: string
16
+ readonly is?: string
17
+ readonly sets?: Readonly<Record<string, string>>
18
+ }
19
+
20
+ interface Node {
21
+ readonly holds?: readonly Row[]
22
+ readonly declares?: readonly Row[]
23
+ }
24
+
25
+ function inside(node: Node): readonly Row[] {
26
+ return [...(node.holds ?? []), ...(node.declares ?? [])]
27
+ }
28
+
29
+ const compare = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0)
30
+
31
+ function module(
32
+ set: IconSet,
33
+ drawn: readonly { readonly id: string; readonly as: string }[],
34
+ sizes: readonly { readonly id: string; readonly is: string }[],
35
+ ): string {
36
+ const names = [...new Set(drawn.map((one) => one.as))].sort(compare)
37
+
38
+ return [
39
+
40
+ "'use client'",
41
+ '',
42
+ '/* GENERATED by thread — do not edit. Change the icon rows and regenerate. */',
43
+ '',
44
+ `import { ${names.join(', ')} } from '${set.module}'`,
45
+ "import { installIcons } from '../icons.ts'",
46
+ '',
47
+ 'installIcons(',
48
+ ` { name: '${set.name}', module: '${set.module}', size: ${String(set.size)}, viewBox: ${String(set.viewBox)}` +
49
+ `${set.nativeStroke === undefined ? '' : `, nativeStroke: ${String(set.nativeStroke)}`} },`,
50
+ ' {',
51
+ ...drawn.map((one) => ` '${one.id}': ${one.as},`),
52
+ ' },',
53
+ ' {',
54
+ ...sizes.map((one) => ` '${one.id}': '${one.is}',`),
55
+ ' },',
56
+ ')',
57
+ '',
58
+ '/*',
59
+ ' * Rendered once by the layout, and that is the point of it.',
60
+ ' *',
61
+ ' * A bare side-effect import of a client module from a server layout is a client REFERENCE, not',
62
+ ' * an evaluation: the install never ran, and every icon refused on a page whose layout plainly',
63
+ ' * imported it. Rendering this is what evaluates the module, in the graph the elements run in.',
64
+ ' */',
65
+ 'export function ThreadIcons(): null {',
66
+ ' return null',
67
+ '}',
68
+ '',
69
+ ].join('\n')
70
+ }
71
+
72
+ function exported(root: string, module: string): ReadonlySet<string> | undefined {
73
+ let entry: string
74
+ try {
75
+ entry = createRequire(join(root, 'package.json')).resolve(module)
76
+ } catch {
77
+ return undefined
78
+ }
79
+ const suffix = `.d.${'ts'}`
80
+ const found = new Set<string>()
81
+ const seen: string[] = []
82
+ let dir = dirname(entry)
83
+ for (let up = 0; up < 5; up += 1) {
84
+ let names: readonly string[] = []
85
+ try {
86
+ names = readdirSync(dir).filter((each) => each.endsWith(suffix))
87
+ } catch {
88
+ names = []
89
+ }
90
+ for (const one of names) seen.push(join(dir, one))
91
+ if (existsSync(join(dir, 'package.json')) && seen.length > 0) break
92
+ const next = dirname(dir)
93
+ if (next === dir) break
94
+ dir = next
95
+ }
96
+ for (const one of seen) {
97
+ let text = ''
98
+ try {
99
+ text = readFileSync(one, 'utf8')
100
+ } catch {
101
+ continue
102
+ }
103
+ for (const each of text.matchAll(/declare const (\w+)/g)) found.add(each[1] ?? '')
104
+ for (const each of text.matchAll(/(\w+) as (\w+)/g)) found.add(each[2] ?? '')
105
+ for (const each of text.matchAll(/export declare const (\w+)/g)) found.add(each[1] ?? '')
106
+ }
107
+ return found.size === 0 ? undefined : found
108
+ }
109
+
110
+ async function node(file: string, name: string): Promise<Node> {
111
+ const at = inTree(findTree(), file)
112
+ if (!existsSync(at)) {
113
+ stdout.write(`icons: this tree has no ${findTree().name}/${file}, so it declares no icon and there is no module to write or check.\n`)
114
+ exit(0)
115
+ }
116
+ const held = valuesIn(readFileSync(at)) as Record<string, unknown>
117
+ const one = held[name]
118
+ if (one === undefined) throw new Error(`_threads/${file} exports no \`${name}\`.`)
119
+ return one as Node
120
+ }
121
+
122
+ async function run(): Promise<number> {
123
+ const tree = findTree()
124
+ const rows = inside(await node('icons.ts', 'ICON'))
125
+
126
+ const picked = rows.find((row) => row.id === SET_ROW)
127
+ if (picked?.is === undefined) {
128
+ stderr.write(
129
+ `thread: the icon sheet declares no \`${SET_ROW}\` row, so nothing says which library the app ` +
130
+ 'draws from. One row, naming a registered library.\n',
131
+ )
132
+ return 1
133
+ }
134
+ const set = setNamed(picked.is)
135
+
136
+ try {
137
+ createRequire(join(tree.at, 'package.json')).resolve(set.module)
138
+ } catch {
139
+ stderr.write(
140
+ `thread: the icon rows draw from \`${set.name}\`, which is the package \`${set.module}\`, and this ` +
141
+ `app does not have it installed. Add it, or name a library the app already has.\n`,
142
+ )
143
+ return 1
144
+ }
145
+
146
+ const sizes = [...sizeRows(rows)].sort((one, two) => compare(one.id, two.id))
147
+ const held = new Set(sizes.map((one) => one.id))
148
+
149
+ if (sizes.length === 0) {
150
+ stderr.write(
151
+ 'thread: the icon sheet declares no size row, so nothing says how large a symbol is drawn. ' +
152
+ 'One row per size, holding a length.\n',
153
+ )
154
+ return 1
155
+ }
156
+
157
+ const drawn = rows
158
+ .filter((row) => row.id !== SET_ROW && !held.has(row.id))
159
+ .map((row) => ({ id: row.id, as: row.sets?.[set.name] ?? drawnAs(set.name, row.id, row.id) }))
160
+ .sort((one, two) => compare(one.id, two.id))
161
+
162
+ const missing = drawn.find((one) => one.as === '')
163
+ if (missing !== undefined) {
164
+ stderr.write(`thread: \`${missing.id}\` names no symbol in \`${set.name}\`.\n`)
165
+ return 1
166
+ }
167
+
168
+ const known = exported(tree.at, set.module)
169
+ if (known === undefined) {
170
+ stdout.write(`thread: the declarations of \`${set.module}\` could not be read from this app, so the names the icon rows give it are unverified.\n`)
171
+ } else {
172
+ const unknown = drawn.find((one) => !one.as.startsWith('si') && !known.has(one.as))
173
+ if (unknown !== undefined) {
174
+ stderr.write(
175
+ `thread: \`${unknown.id}\` is drawn as \`${unknown.as}\` in \`${set.name}\`, and that library does not export it — its ` +
176
+ `declarations list ${String(known.size)} names and none of them is that one. Check the library's own naming; the module ` +
177
+ 'would fail to build with it.\n',
178
+ )
179
+ return 1
180
+ }
181
+ }
182
+
183
+ const brand = drawn.filter((one) => one.as.startsWith('si')).map((one) => brandExport(one.id))
184
+ if (brand.length > 0) stdout.write(`thread: ${String(brand.length)} brand icon(s) drawn from their own set\n`)
185
+
186
+ const at = join(tree.at, 'app/_generated/icons.ts')
187
+ const made = module(set, drawn, sizes)
188
+
189
+ if (argv.includes('--check')) {
190
+ let current = ''
191
+ try {
192
+ current = readFileSync(at, 'utf8')
193
+ } catch {
194
+ current = ''
195
+ }
196
+ if (current === made) {
197
+ stdout.write('icons: app/_generated/icons.ts is what the rows produce\n')
198
+ return 0
199
+ }
200
+ stderr.write(
201
+ 'icons: app/_generated/icons.ts is not what the rows produce. A derived file that differs from ' +
202
+ 'its generator is a hand edit wearing a generated name. Run `thread icons` and commit what it writes.\n',
203
+ )
204
+ return 1
205
+ }
206
+
207
+ writeFileSync(at, made, 'utf8')
208
+ stdout.write(
209
+ `thread: wrote app/_generated/icons.ts — ${String(drawn.length)} icons drawn from \`${set.name}\`\n`,
210
+ )
211
+ return 0
212
+ }
213
+
214
+ if (argv[1] !== undefined) exit(await run())
@@ -1,5 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { existsSync, readFileSync, statSync } from 'node:fs';
2
+ import { readFileSync as bytesAt } from 'node:fs';
3
+ import { existsSync, readFileSync, statSync } from "@prestack/thread/core/files";
3
4
  import { cwd } from 'node:process';
4
5
  import { dirname, join, resolve } from 'node:path';
5
6
  import ts from 'typescript';
@@ -9,7 +10,7 @@ const OWNED = /\.(t|j)sx$/;
9
10
  const VARIANT_MEMBER = /^[a-z]/;
10
11
  const ARRANGERS = new Set(['composition', 'screen']);
11
12
  const HOST = /^[a-z]/;
12
- const GENERATED = /^\s*\/\*\s*GENERATED by thread\b/;
13
+ const MAKES = 'element';
13
14
  const ONE_WORD = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
14
15
  function prefixed(called) {
15
16
  return called === 'screen' ? 's-' : 'c-';
@@ -30,6 +31,9 @@ function arranging(held, at) {
30
31
  }
31
32
  return found?.own;
32
33
  }
34
+ function within(held, at) {
35
+ return held.some((one) => at >= one.from && at < one.to);
36
+ }
33
37
  function tagOf(node) {
34
38
  return node.tagName;
35
39
  }
@@ -66,7 +70,11 @@ export function compiled(text, at) {
66
70
  const arranges = [];
67
71
  const edits = [];
68
72
  let because;
73
+ const writing = [];
69
74
  const calls = (node) => {
75
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === MAKES) {
76
+ writing.push({ from: node.getStart(), to: node.getEnd() });
77
+ }
70
78
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && ARRANGERS.has(node.expression.text)) {
71
79
  const first = node.arguments[0];
72
80
  const said = first === undefined ? undefined : quoted(first);
@@ -96,13 +104,13 @@ export function compiled(text, at) {
96
104
  const tag = tagOf(node);
97
105
  const held = named(attributes(node));
98
106
  const worn = [];
99
- if (ts.isIdentifier(tag) && HOST.test(tag.text) && !GENERATED.test(text)) {
107
+ if (ts.isIdentifier(tag) && HOST.test(tag.text) && !within(writing, node.getStart())) {
100
108
  because =
101
109
  `${at} writes <${tag.text}>, which is a host element: it wears no declared style, sits in no ` +
102
110
  'catalogue, and the style guide, a rebrand and every overlay read past it. Declare the element ' +
103
111
  'and wear its look, or use the declared groupings where a host element would have gone';
104
112
  }
105
- if (held.className !== undefined && !GENERATED.test(text)) {
113
+ if (held.className !== undefined && !within(writing, node.getStart())) {
106
114
  because =
107
115
  `${at} writes a \`className\` by hand, which is CSS written in place: it names no style guide ` +
108
116
  'row, wears no declared variant, and the style guide, a rebrand and every overlay read past ' +
@@ -185,7 +193,7 @@ export function ungated(from) {
185
193
  const held = JSON.parse(readFileSync(at, 'utf8'));
186
194
  for (const [named, was] of Object.entries(held.of ?? {})) {
187
195
  const now = existsSync(join(dirname(dirname(at)), named))
188
- ? createHash('sha256').update(readFileSync(join(dirname(dirname(at)), named))).digest('hex').slice(0, 16)
196
+ ? createHash('sha256').update(bytesAt(join(dirname(dirname(at)), named))).digest('hex').slice(0, 16)
189
197
  : 'gone';
190
198
  if (now === was)
191
199
  continue;