@prestack/thread-ui 0.1.9 → 0.1.11

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 (210) hide show
  1. package/CHANGELOG.md +366 -0
  2. package/dist/ui/attach.js +6 -7
  3. package/dist/ui/compositions.d.ts +0 -2
  4. package/dist/ui/compositions.js +2 -112
  5. package/dist/ui/generators/routes.js +1 -0
  6. package/dist/ui/variant.d.ts +1 -3
  7. package/package.json +2 -2
  8. package/dist/ui/animations/anm_appear.ts +0 -13
  9. package/dist/ui/animations/anm_blur_in.ts +0 -19
  10. package/dist/ui/animations/anm_blur_out.ts +0 -18
  11. package/dist/ui/animations/anm_dismiss.ts +0 -18
  12. package/dist/ui/animations/anm_drop.ts +0 -19
  13. package/dist/ui/animations/anm_fall.ts +0 -18
  14. package/dist/ui/animations/anm_flash.ts +0 -18
  15. package/dist/ui/animations/anm_glow.ts +0 -16
  16. package/dist/ui/animations/anm_lift.ts +0 -16
  17. package/dist/ui/animations/anm_nudge.ts +0 -16
  18. package/dist/ui/animations/anm_pop_in.ts +0 -23
  19. package/dist/ui/animations/anm_pop_out.ts +0 -22
  20. package/dist/ui/animations/anm_press.ts +0 -12
  21. package/dist/ui/animations/anm_pulse.ts +0 -13
  22. package/dist/ui/animations/anm_rise.ts +0 -19
  23. package/dist/ui/animations/anm_shake.ts +0 -13
  24. package/dist/ui/animations/anm_slide_in_left.ts +0 -19
  25. package/dist/ui/animations/anm_slide_in_right.ts +0 -19
  26. package/dist/ui/animations/anm_slide_out_left.ts +0 -18
  27. package/dist/ui/animations/anm_slide_out_right.ts +0 -18
  28. package/dist/ui/animations/anm_spin.ts +0 -12
  29. package/dist/ui/animations/anm_swell.ts +0 -16
  30. package/dist/ui/animations/anm_wobble.ts +0 -24
  31. package/dist/ui/animations/anm_zoom_in.ts +0 -19
  32. package/dist/ui/animations/anm_zoom_out.ts +0 -18
  33. package/dist/ui/animations.ts +0 -308
  34. package/dist/ui/attach.ts +0 -553
  35. package/dist/ui/charts.tsx +0 -69
  36. package/dist/ui/checked.ts +0 -32
  37. package/dist/ui/collector.ts +0 -140
  38. package/dist/ui/compositions.ts +0 -555
  39. package/dist/ui/consent.ts +0 -55
  40. package/dist/ui/content.tsx +0 -27
  41. package/dist/ui/contrast.ts +0 -27
  42. package/dist/ui/controls.tsx +0 -25
  43. package/dist/ui/disclosure.tsx +0 -20
  44. package/dist/ui/dismiss-layer.ts +0 -87
  45. package/dist/ui/door.ts +0 -140
  46. package/dist/ui/drag.ts +0 -316
  47. package/dist/ui/elements/accordion.tsx +0 -47
  48. package/dist/ui/elements/accordion.works.ts +0 -66
  49. package/dist/ui/elements/aspect-ratio.tsx +0 -31
  50. package/dist/ui/elements/aspect-ratio.works.ts +0 -5
  51. package/dist/ui/elements/avatar.tsx +0 -34
  52. package/dist/ui/elements/avatar.works.ts +0 -16
  53. package/dist/ui/elements/badge.tsx +0 -33
  54. package/dist/ui/elements/badge.works.ts +0 -9
  55. package/dist/ui/elements/bars.tsx +0 -50
  56. package/dist/ui/elements/bars.works.ts +0 -29
  57. package/dist/ui/elements/box.tsx +0 -36
  58. package/dist/ui/elements/breadcrumb.tsx +0 -38
  59. package/dist/ui/elements/breadcrumb.works.ts +0 -24
  60. package/dist/ui/elements/button.tsx +0 -46
  61. package/dist/ui/elements/button.works.tsx +0 -52
  62. package/dist/ui/elements/column.tsx +0 -29
  63. package/dist/ui/elements/consent.tsx +0 -71
  64. package/dist/ui/elements/consent.works.tsx +0 -132
  65. package/dist/ui/elements/donut.tsx +0 -45
  66. package/dist/ui/elements/donut.works.ts +0 -42
  67. package/dist/ui/elements/each.tsx +0 -34
  68. package/dist/ui/elements/each.works.tsx +0 -82
  69. package/dist/ui/elements/empty.tsx +0 -33
  70. package/dist/ui/elements/field.tsx +0 -45
  71. package/dist/ui/elements/field.works.ts +0 -65
  72. package/dist/ui/elements/funnel.tsx +0 -50
  73. package/dist/ui/elements/funnel.works.ts +0 -37
  74. package/dist/ui/elements/gauge.tsx +0 -40
  75. package/dist/ui/elements/gauge.works.ts +0 -42
  76. package/dist/ui/elements/glyph.tsx +0 -30
  77. package/dist/ui/elements/glyph.works.tsx +0 -42
  78. package/dist/ui/elements/grid.tsx +0 -29
  79. package/dist/ui/elements/heatmap.tsx +0 -48
  80. package/dist/ui/elements/heatmap.works.ts +0 -24
  81. package/dist/ui/elements/hit-box.tsx +0 -47
  82. package/dist/ui/elements/hit-box.works.tsx +0 -576
  83. package/dist/ui/elements/image.tsx +0 -29
  84. package/dist/ui/elements/line-chart.tsx +0 -45
  85. package/dist/ui/elements/line-chart.works.ts +0 -41
  86. package/dist/ui/elements/link.tsx +0 -38
  87. package/dist/ui/elements/link.works.ts +0 -25
  88. package/dist/ui/elements/media.tsx +0 -57
  89. package/dist/ui/elements/menu.tsx +0 -51
  90. package/dist/ui/elements/menu.works.tsx +0 -154
  91. package/dist/ui/elements/money.tsx +0 -36
  92. package/dist/ui/elements/money.works.ts +0 -9
  93. package/dist/ui/elements/pagination.tsx +0 -39
  94. package/dist/ui/elements/pagination.works.ts +0 -20
  95. package/dist/ui/elements/progress.tsx +0 -34
  96. package/dist/ui/elements/progress.works.ts +0 -8
  97. package/dist/ui/elements/radio-group.tsx +0 -50
  98. package/dist/ui/elements/radio-group.works.ts +0 -33
  99. package/dist/ui/elements/row.tsx +0 -29
  100. package/dist/ui/elements/scatter.tsx +0 -42
  101. package/dist/ui/elements/scatter.works.ts +0 -38
  102. package/dist/ui/elements/select.tsx +0 -43
  103. package/dist/ui/elements/select.works.ts +0 -5
  104. package/dist/ui/elements/separator.tsx +0 -31
  105. package/dist/ui/elements/skeleton.tsx +0 -31
  106. package/dist/ui/elements/slider.tsx +0 -34
  107. package/dist/ui/elements/slider.works.ts +0 -5
  108. package/dist/ui/elements/sparkline.tsx +0 -37
  109. package/dist/ui/elements/sparkline.works.ts +0 -32
  110. package/dist/ui/elements/spinner.tsx +0 -33
  111. package/dist/ui/elements/stack.tsx +0 -29
  112. package/dist/ui/elements/stat.tsx +0 -36
  113. package/dist/ui/elements/stat.works.ts +0 -14
  114. package/dist/ui/elements/surface.tsx +0 -36
  115. package/dist/ui/elements/surface.works.ts +0 -9
  116. package/dist/ui/elements/table.tsx +0 -62
  117. package/dist/ui/elements/table.works.ts +0 -30
  118. package/dist/ui/elements/tabs.tsx +0 -47
  119. package/dist/ui/elements/tabs.works.ts +0 -84
  120. package/dist/ui/elements/text.tsx +0 -33
  121. package/dist/ui/elements/textarea.tsx +0 -38
  122. package/dist/ui/elements/textarea.works.ts +0 -5
  123. package/dist/ui/elements/time.tsx +0 -40
  124. package/dist/ui/elements/time.works.ts +0 -29
  125. package/dist/ui/elements/toggle.tsx +0 -34
  126. package/dist/ui/elements/toggle.works.ts +0 -5
  127. package/dist/ui/elements/tooltip.tsx +0 -40
  128. package/dist/ui/elements/tooltip.works.ts +0 -77
  129. package/dist/ui/elements/value.tsx +0 -38
  130. package/dist/ui/elements/value.works.ts +0 -9
  131. package/dist/ui/elements.ts +0 -127
  132. package/dist/ui/focus-scope.ts +0 -121
  133. package/dist/ui/fonts.ts +0 -355
  134. package/dist/ui/generators/dev.d.ts +0 -1
  135. package/dist/ui/generators/dev.js +0 -61
  136. package/dist/ui/generators/dev.ts +0 -74
  137. package/dist/ui/generators/elements.ts +0 -796
  138. package/dist/ui/generators/i18n.ts +0 -283
  139. package/dist/ui/generators/icons.ts +0 -207
  140. package/dist/ui/generators/react.ts +0 -267
  141. package/dist/ui/generators/routes.ts +0 -847
  142. package/dist/ui/generators/show.ts +0 -261
  143. package/dist/ui/generators/styles.ts +0 -677
  144. package/dist/ui/generators/variants.ts +0 -196
  145. package/dist/ui/i18n/catalogs/en.ts +0 -191
  146. package/dist/ui/i18n/format.ts +0 -221
  147. package/dist/ui/i18n/locales.ts +0 -33
  148. package/dist/ui/i18n/parse.ts +0 -315
  149. package/dist/ui/i18n/translator.ts +0 -31
  150. package/dist/ui/i18n/types.ts +0 -96
  151. package/dist/ui/icons.ts +0 -331
  152. package/dist/ui/islands/map.ts +0 -58
  153. package/dist/ui/localizations.ts +0 -75
  154. package/dist/ui/material.ts +0 -236
  155. package/dist/ui/materials/mat_bordered.ts +0 -13
  156. package/dist/ui/materials/mat_ceramic.ts +0 -13
  157. package/dist/ui/materials/mat_clay.ts +0 -13
  158. package/dist/ui/materials/mat_elevated.ts +0 -24
  159. package/dist/ui/materials/mat_flat.ts +0 -34
  160. package/dist/ui/materials/mat_glass.ts +0 -20
  161. package/dist/ui/materials/mat_lit.ts +0 -13
  162. package/dist/ui/materials/mat_moulded.ts +0 -11
  163. package/dist/ui/materials.ts +0 -197
  164. package/dist/ui/modes.ts +0 -44
  165. package/dist/ui/navigation.tsx +0 -29
  166. package/dist/ui/positioning.ts +0 -90
  167. package/dist/ui/presented.tsx +0 -119
  168. package/dist/ui/provider.tsx +0 -184
  169. package/dist/ui/render-loop.ts +0 -133
  170. package/dist/ui/roving-focus.ts +0 -161
  171. package/dist/ui/screens.ts +0 -141
  172. package/dist/ui/scroll-lock.ts +0 -28
  173. package/dist/ui/series.ts +0 -97
  174. package/dist/ui/signals.ts +0 -386
  175. package/dist/ui/styleguide.ts +0 -231
  176. package/dist/ui/styleguides.ts +0 -374
  177. package/dist/ui/themes.ts +0 -22
  178. package/dist/ui/token.ts +0 -78
  179. package/dist/ui/tokens/shadow.ts +0 -25
  180. package/dist/ui/tokens.ts +0 -234
  181. package/dist/ui/validators.ts +0 -232
  182. package/dist/ui/variant.ts +0 -352
  183. package/dist/ui/variants/var_avatar-round.ts +0 -14
  184. package/dist/ui/variants/var_bubble-me.ts +0 -14
  185. package/dist/ui/variants/var_bubble-them.ts +0 -13
  186. package/dist/ui/variants/var_button-border.ts +0 -76
  187. package/dist/ui/variants/var_button-cta.ts +0 -76
  188. package/dist/ui/variants/var_button-destructive-ghost.ts +0 -72
  189. package/dist/ui/variants/var_button-destructive.ts +0 -76
  190. package/dist/ui/variants/var_button-fill.ts +0 -76
  191. package/dist/ui/variants/var_button-ghost.ts +0 -72
  192. package/dist/ui/variants/var_button-glass.ts +0 -55
  193. package/dist/ui/variants/var_button-normal.ts +0 -76
  194. package/dist/ui/variants/var_button-quiet.ts +0 -74
  195. package/dist/ui/variants/var_card.ts +0 -17
  196. package/dist/ui/variants/var_chip.ts +0 -13
  197. package/dist/ui/variants/var_choice.ts +0 -15
  198. package/dist/ui/variants/var_framed.ts +0 -10
  199. package/dist/ui/variants/var_glyph-plain.ts +0 -11
  200. package/dist/ui/variants/var_input.ts +0 -43
  201. package/dist/ui/variants/var_link-inline.ts +0 -29
  202. package/dist/ui/variants/var_list-row.ts +0 -34
  203. package/dist/ui/variants/var_rule.ts +0 -10
  204. package/dist/ui/variants/var_skeleton-block.ts +0 -12
  205. package/dist/ui/variants/var_spinner-turning.ts +0 -12
  206. package/dist/ui/variants/var_stat-tile.ts +0 -13
  207. package/dist/ui/variants/var_tab-bar.ts +0 -15
  208. package/dist/ui/variants/var_tap-target.ts +0 -25
  209. package/dist/ui/variants.ts +0 -64
  210. package/dist/ui/wrappers.tsx +0 -429
@@ -1,283 +0,0 @@
1
- import { existsSync, readFileSync, readdirSync, writeFileSync } from '@prestack/thread/integrations/disk'
2
- import { 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 walk(join(home, 'dist'))) {
49
- if (!file.endsWith('.d.ts')) continue
50
- for (const one of readFileSync(file, '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
- 'export type Language =',
114
- ...languages.map((one) => ` | ${asJson(one.id)}`),
115
- '',
116
- `export const SOURCE: Language = ${asJson(source)}`,
117
- '',
118
- 'export const LANGUAGES: readonly { readonly id: Language; readonly name: string; readonly flag: string }[] = [',
119
- ...languages.map((one) => ` { id: ${asJson(one.id)}, name: ${asJson(one.name)}, flag: ${asJson(one.flag)} },`),
120
- ']',
121
- '',
122
- 'export type Used =',
123
- ...(used.length === 0 ? [' | never'] : used.map((one) => ` | ${asJson(one)}`)),
124
- '',
125
- 'const held = {',
126
- ...languages.map((one) => catalogue(one.id)),
127
- '} as const',
128
- '',
129
- "type Filled<S> = S extends '' ? never : S",
130
- '',
131
- 'type Answered<L extends Language> = {',
132
- ' readonly [K in Used]: K extends keyof (typeof held)[L] ? Filled<(typeof held)[L][K]> : never',
133
- '}',
134
- '',
135
- 'export const WORDING: { readonly [L in Language]: Answered<L> } = held',
136
- '',
137
- ].join('\n')
138
- }
139
-
140
- interface Node {
141
- readonly holds?: readonly { readonly id: string; readonly is?: string; readonly sets?: Readonly<Record<string, string>> }[]
142
- readonly declares?: readonly { readonly id: string; readonly is?: string; readonly sets?: Readonly<Record<string, string>> }[]
143
- }
144
-
145
- async function rows(at: string): Promise<NonNullable<Node['holds']>> {
146
- const path = inTree(findTree(), `localizations.${'ts'}`)
147
- if (!existsSync(path)) {
148
- 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`)
149
- exit(0)
150
- }
151
- const held = (await import(pathToFileURL(path).href)) as { LOCALIZATION?: Node }
152
- if (held.LOCALIZATION === undefined) {
153
- throw new Error(
154
- `${relative(at, path)} exports no \`LOCALIZATION\`, so nothing says what languages this app is ` +
155
- `in. Run \`thread attach ${packageOf('ui')}\` — the sheet is the package's to write, not yours.`,
156
- )
157
- }
158
- return [...(held.LOCALIZATION.holds ?? []), ...(held.LOCALIZATION.declares ?? [])]
159
- }
160
-
161
- async function run(): Promise<number> {
162
- const tree = findTree()
163
- const at = tree.at
164
- const held = (await rows(at)) ?? []
165
-
166
- const languages = languagesIn(held)
167
- const source = sourceIn(held)
168
-
169
- if (languages.length === 0) {
170
- stderr.write('i18n: no language is declared, so this app is in no language at all. Add one with `thread create language`.\n')
171
- return 1
172
- }
173
- if (source === undefined || !languages.some((one) => one.id === source)) {
174
- stderr.write(
175
- `i18n: the source row names \`${String(source)}\`, and that is not one of the declared languages ` +
176
- `(${languages.map((one) => one.id).join(', ')}). An app is written in one of the languages it is in.\n`,
177
- )
178
- return 1
179
- }
180
-
181
- const props = keyProps(at)
182
- if (props.length === 0) {
183
- stderr.write(
184
- `i18n: nothing in the installed \`${packageOf('ui')}\` declares a prop of type \`MessageKey\`, so no key could ` +
185
- 'be recognised in any file. That is a broken install, not an app with no wording.\n',
186
- )
187
- return 1
188
- }
189
-
190
- const used = new Set<string>()
191
- const mine = leftOut(at)
192
- for (const file of walk(at, [], mine)) for (const key of keysIn(readFileSync(file, 'utf8'), props)) used.add(key)
193
-
194
- const seeded = await shipped(at)
195
-
196
- const answered = new Set<string>()
197
- for (const language of languages) {
198
- for (const key of Object.keys(readWording(at, language.id))) answered.add(key)
199
- }
200
-
201
- const everything = [...new Set([...used, ...Object.keys(seeded), ...answered])].sort()
202
-
203
- const wording = new Map<string, Readonly<Record<string, string>>>()
204
- for (const language of languages) {
205
- const already = readWording(at, language.id)
206
- const made: Record<string, string> = {}
207
- for (const key of everything) made[key] = already[key] ?? (language.id === source ? (seeded[key] ?? '') : '')
208
- wording.set(language.id, made)
209
- }
210
-
211
- const files = [
212
- ...languages.map((one) => ({ path: fileFor(one.id), made: wordingFile(wording.get(one.id) ?? {}) })),
213
- { path: GENERATED, made: moduleFor(languages, source, [...used].sort(), wording) },
214
- ]
215
-
216
- const named = new Set(languages.map((one) => fileFor(one.id)))
217
- const stray: string[] = []
218
- try {
219
- for (const entry of readdirSync(join(at, 'i18n'), { withFileTypes: true })) {
220
- const path = `i18n/${entry.name}`
221
- if (entry.isDirectory() || named.has(path)) continue
222
- stray.push(path)
223
- }
224
- } catch { }
225
-
226
- const blank = languages.flatMap((one) =>
227
- everything.filter((key) => (wording.get(one.id) ?? {})[key] === '').map((key) => `${one.id}: ${key}`),
228
- )
229
-
230
- if (argv.includes('--check')) {
231
- const drifted = files.find((one) => {
232
- let current = ''
233
- try {
234
- current = readFileSync(join(at, one.path), 'utf8')
235
- } catch {
236
- current = ''
237
- }
238
- return current !== one.made
239
- })
240
- if (drifted !== undefined) {
241
- stderr.write(
242
- `i18n: ${drifted.path} is not what the declarations produce. Run \`thread i18n\` and fill in what it adds.\n`,
243
- )
244
- return 1
245
- }
246
- if (stray.length > 0) {
247
- stderr.write(
248
- `i18n: ${stray.join(', ')} — no declared language accounts for ${stray.length === 1 ? 'it' : 'them'}, ` +
249
- 'so it is not wording, it is a file somebody left there. Declare the language with ' +
250
- '`thread create language`, or delete the file.\n',
251
- )
252
- return 1
253
- }
254
- if (blank.length > 0) {
255
- stderr.write(
256
- `i18n: ${String(blank.length)} key(s) have no wording, and an untranslated key is an error rather ` +
257
- `than a word that comes out in the wrong language:\n${blank.map((one) => ` ${one}`).join('\n')}\n`,
258
- )
259
- return 1
260
- }
261
- stdout.write(`i18n: ${String(everything.length)} key(s) answered in ${String(languages.length)} language(s)\n`)
262
- return 0
263
- }
264
-
265
- for (const one of files) {
266
- writeFileSync(join(at, one.path), one.made, 'utf8')
267
- }
268
-
269
- stdout.write(
270
- `i18n: ${String(everything.length)} key(s) across ${String(languages.length)} language(s) — ` +
271
- `${String(used.size)} named by this interface, ${String(everything.length - used.size)} shipped with the framework\n`,
272
- )
273
- if (blank.length > 0) {
274
- stdout.write(
275
- `i18n: ${String(blank.length)} still to write — the gate refuses until they are filled:\n` +
276
- `${blank.slice(0, 20).map((one) => ` ${one}`).join('\n')}\n` +
277
- (blank.length > 20 ? ` … and ${String(blank.length - 20)} more\n` : ''),
278
- )
279
- }
280
- return 0
281
- }
282
-
283
- if (invoked(import.meta.url)) exit(await run())
@@ -1,207 +0,0 @@
1
- import { createRequire } from 'node:module'
2
-
3
- import { existsSync, readdirSync, readFileSync, writeFileSync } from '@prestack/thread/integrations/disk'
4
- import { valuesIn } from '@prestack/thread/core/rows'
5
- import { dirname, join, relative } from 'node:path'
6
- import { argv, stderr, stdout } from 'node:process'
7
-
8
- import { exit } from '@prestack/thread/core/invoked'
9
- import { findTree, inTree } from '@prestack/thread/core/tree'
10
- import { COMPILED } from '@prestack/thread/operations/capabilities'
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
- const SYMBOLS = 'app/_generated/icons.ts'
32
-
33
- function module(
34
- set: IconSet,
35
- drawn: readonly { readonly id: string; readonly as: string }[],
36
- sizes: readonly { readonly id: string; readonly is: string }[],
37
- ): string {
38
- const names = [...new Set(drawn.map((one) => one.as))].sort(compare)
39
-
40
- return [
41
-
42
- "'use client'",
43
- '',
44
- `import { ${names.join(', ')} } from '${set.module}'`,
45
- `import { installIcons } from './${relative(dirname(SYMBOLS), `${COMPILED}/ui/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
- 'export function ThreadIcons(): null {',
59
- ' return null',
60
- '}',
61
- '',
62
- ].join('\n')
63
- }
64
-
65
- function exported(root: string, module: string): ReadonlySet<string> | undefined {
66
- let entry: string
67
- try {
68
- entry = createRequire(join(root, 'package.json')).resolve(module)
69
- } catch {
70
- return undefined
71
- }
72
- const suffix = `.d.${'ts'}`
73
- const found = new Set<string>()
74
- const seen: string[] = []
75
- let dir = dirname(entry)
76
- for (let up = 0; up < 5; up += 1) {
77
- let names: readonly string[] = []
78
- try {
79
- names = readdirSync(dir).filter((each) => each.endsWith(suffix))
80
- } catch {
81
- names = []
82
- }
83
- for (const one of names) seen.push(join(dir, one))
84
- if (existsSync(join(dir, 'package.json')) && seen.length > 0) break
85
- const next = dirname(dir)
86
- if (next === dir) break
87
- dir = next
88
- }
89
- for (const one of seen) {
90
- let text = ''
91
- try {
92
- text = readFileSync(one, 'utf8')
93
- } catch {
94
- continue
95
- }
96
- for (const each of text.matchAll(/declare const (\w+)/g)) found.add(each[1] ?? '')
97
- for (const each of text.matchAll(/(\w+) as (\w+)/g)) found.add(each[2] ?? '')
98
- for (const each of text.matchAll(/export declare const (\w+)/g)) found.add(each[1] ?? '')
99
- }
100
- return found.size === 0 ? undefined : found
101
- }
102
-
103
- async function node(file: string, name: string): Promise<Node> {
104
- const at = inTree(findTree(), file)
105
- if (!existsSync(at)) {
106
- 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`)
107
- exit(0)
108
- }
109
- const held = valuesIn(readFileSync(at)) as Record<string, unknown>
110
- const one = held[name]
111
- if (one === undefined) throw new Error(`_threads/${file} exports no \`${name}\`.`)
112
- return one as Node
113
- }
114
-
115
- async function run(): Promise<number> {
116
- const tree = findTree()
117
- const rows = inside(await node('icons.ts', 'ICON'))
118
-
119
- const picked = rows.find((row) => row.id === SET_ROW)
120
- if (picked?.is === undefined) {
121
- stderr.write(
122
- `thread: the icon sheet declares no \`${SET_ROW}\` row, so nothing says which library the app ` +
123
- 'draws from. One row, naming a registered library.\n',
124
- )
125
- return 1
126
- }
127
- const set = setNamed(picked.is)
128
-
129
- try {
130
- createRequire(join(tree.at, 'package.json')).resolve(set.module)
131
- } catch {
132
- stderr.write(
133
- `thread: the icon rows draw from \`${set.name}\`, which is the package \`${set.module}\`, and this ` +
134
- `app does not have it installed. Add it, or name a library the app already has.\n`,
135
- )
136
- return 1
137
- }
138
-
139
- const sizes = [...sizeRows(rows)].sort((one, two) => compare(one.id, two.id))
140
- const held = new Set(sizes.map((one) => one.id))
141
-
142
- if (sizes.length === 0) {
143
- stderr.write(
144
- 'thread: the icon sheet declares no size row, so nothing says how large a symbol is drawn. ' +
145
- 'One row per size, holding a length.\n',
146
- )
147
- return 1
148
- }
149
-
150
- const drawn = rows
151
- .filter((row) => row.id !== SET_ROW && !held.has(row.id))
152
- .map((row) => ({ id: row.id, as: row.sets?.[set.name] ?? drawnAs(set.name, row.id, row.id) }))
153
- .sort((one, two) => compare(one.id, two.id))
154
-
155
- const missing = drawn.find((one) => one.as === '')
156
- if (missing !== undefined) {
157
- stderr.write(`thread: \`${missing.id}\` names no symbol in \`${set.name}\`.\n`)
158
- return 1
159
- }
160
-
161
- const known = exported(tree.at, set.module)
162
- if (known === undefined) {
163
- 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`)
164
- } else {
165
- const unknown = drawn.find((one) => !one.as.startsWith('si') && !known.has(one.as))
166
- if (unknown !== undefined) {
167
- stderr.write(
168
- `thread: \`${unknown.id}\` is drawn as \`${unknown.as}\` in \`${set.name}\`, and that library does not export it — its ` +
169
- `declarations list ${String(known.size)} names and none of them is that one. Check the library's own naming; the module ` +
170
- 'would fail to build with it.\n',
171
- )
172
- return 1
173
- }
174
- }
175
-
176
- const brand = drawn.filter((one) => one.as.startsWith('si')).map((one) => brandExport(one.id))
177
- if (brand.length > 0) stdout.write(`thread: ${String(brand.length)} brand icon(s) drawn from their own set\n`)
178
-
179
- const at = join(tree.at, SYMBOLS)
180
- const made = module(set, drawn, sizes)
181
-
182
- if (argv.includes('--check')) {
183
- let current = ''
184
- try {
185
- current = readFileSync(at, 'utf8')
186
- } catch {
187
- current = ''
188
- }
189
- if (current === made) {
190
- stdout.write('icons: app/_generated/icons.ts is what the rows produce\n')
191
- return 0
192
- }
193
- stderr.write(
194
- 'icons: app/_generated/icons.ts is not what the rows produce. A derived file that differs from ' +
195
- 'its generator is a hand edit wearing a generated name. Run `thread icons` and commit what it writes.\n',
196
- )
197
- return 1
198
- }
199
-
200
- writeFileSync(at, made, 'utf8')
201
- stdout.write(
202
- `thread: wrote app/_generated/icons.ts — ${String(drawn.length)} icons drawn from \`${set.name}\`\n`,
203
- )
204
- return 0
205
- }
206
-
207
- if (argv[1] !== undefined) exit(await run())