@prestack/thread-ui 0.0.1 → 0.0.2
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.
- package/dist/ui/animations/anm_appear.ts +13 -0
- package/dist/ui/animations/anm_blur_in.ts +19 -0
- package/dist/ui/animations/anm_blur_out.ts +18 -0
- package/dist/ui/animations/anm_dismiss.ts +18 -0
- package/dist/ui/animations/anm_drop.ts +19 -0
- package/dist/ui/animations/anm_fall.ts +18 -0
- package/dist/ui/animations/anm_flash.ts +18 -0
- package/dist/ui/animations/anm_glow.ts +16 -0
- package/dist/ui/animations/anm_lift.ts +16 -0
- package/dist/ui/animations/anm_nudge.ts +16 -0
- package/dist/ui/animations/anm_pop_in.ts +23 -0
- package/dist/ui/animations/anm_pop_out.ts +22 -0
- package/dist/ui/animations/anm_press.ts +12 -0
- package/dist/ui/animations/anm_pulse.ts +13 -0
- package/dist/ui/animations/anm_rise.ts +19 -0
- package/dist/ui/animations/anm_shake.ts +13 -0
- package/dist/ui/animations/anm_slide_in_left.ts +19 -0
- package/dist/ui/animations/anm_slide_in_right.ts +19 -0
- package/dist/ui/animations/anm_slide_out_left.ts +18 -0
- package/dist/ui/animations/anm_slide_out_right.ts +18 -0
- package/dist/ui/animations/anm_spin.ts +12 -0
- package/dist/ui/animations/anm_swell.ts +16 -0
- package/dist/ui/animations/anm_wobble.ts +24 -0
- package/dist/ui/animations/anm_zoom_in.ts +19 -0
- package/dist/ui/animations/anm_zoom_out.ts +18 -0
- package/dist/ui/animations.ts +317 -0
- package/dist/ui/attach.js +13 -4
- package/dist/ui/attach.ts +613 -0
- package/dist/ui/charts.tsx +74 -0
- package/dist/ui/checked.d.ts +2 -2
- package/dist/ui/checked.ts +32 -0
- package/dist/ui/collector.ts +140 -0
- package/dist/ui/compositions.d.ts +10 -1
- package/dist/ui/compositions.js +262 -23
- package/dist/ui/compositions.ts +640 -0
- package/dist/ui/consent.ts +55 -0
- package/dist/ui/content.tsx +39 -0
- package/dist/ui/contrast.ts +27 -0
- package/dist/ui/controls.tsx +30 -0
- package/dist/ui/disclosure.tsx +22 -0
- package/dist/ui/dismiss-layer.ts +87 -0
- package/dist/ui/door.ts +140 -0
- package/dist/ui/drag.ts +316 -0
- package/dist/ui/elements/accordion.js +2 -2
- package/dist/ui/elements/accordion.tsx +47 -0
- package/dist/ui/elements/accordion.works.ts +66 -0
- package/dist/ui/elements/aspect-ratio.js +1 -1
- package/dist/ui/elements/aspect-ratio.tsx +30 -0
- package/dist/ui/elements/aspect-ratio.works.ts +5 -0
- package/dist/ui/elements/avatar.js +1 -1
- package/dist/ui/elements/avatar.tsx +33 -0
- package/dist/ui/elements/avatar.works.ts +16 -0
- package/dist/ui/elements/badge.js +1 -1
- package/dist/ui/elements/badge.tsx +32 -0
- package/dist/ui/elements/badge.works.ts +9 -0
- package/dist/ui/elements/bars.js +2 -2
- package/dist/ui/elements/bars.tsx +49 -0
- package/dist/ui/elements/bars.works.ts +29 -0
- package/dist/ui/elements/box.js +3 -2
- package/dist/ui/elements/box.tsx +35 -0
- package/dist/ui/elements/breadcrumb.js +2 -2
- package/dist/ui/elements/breadcrumb.tsx +37 -0
- package/dist/ui/elements/breadcrumb.works.ts +24 -0
- package/dist/ui/elements/button.js +1 -1
- package/dist/ui/elements/button.tsx +45 -0
- package/dist/ui/elements/button.works.tsx +52 -0
- package/dist/ui/elements/column.js +1 -1
- package/dist/ui/elements/column.tsx +28 -0
- package/dist/ui/elements/consent.js +2 -2
- package/dist/ui/elements/consent.tsx +70 -0
- package/dist/ui/elements/consent.works.tsx +132 -0
- package/dist/ui/elements/donut.js +2 -2
- package/dist/ui/elements/donut.tsx +44 -0
- package/dist/ui/elements/donut.works.ts +42 -0
- package/dist/ui/elements/each.js +1 -1
- package/dist/ui/elements/each.tsx +33 -0
- package/dist/ui/elements/each.works.tsx +82 -0
- package/dist/ui/elements/empty.js +1 -1
- package/dist/ui/elements/empty.tsx +32 -0
- package/dist/ui/elements/field.js +1 -1
- package/dist/ui/elements/field.tsx +44 -0
- package/dist/ui/elements/field.works.ts +65 -0
- package/dist/ui/elements/funnel.js +2 -2
- package/dist/ui/elements/funnel.tsx +49 -0
- package/dist/ui/elements/funnel.works.ts +37 -0
- package/dist/ui/elements/gauge.js +1 -1
- package/dist/ui/elements/gauge.tsx +39 -0
- package/dist/ui/elements/gauge.works.ts +42 -0
- package/dist/ui/elements/glyph.js +1 -1
- package/dist/ui/elements/glyph.tsx +29 -0
- package/dist/ui/elements/glyph.works.tsx +42 -0
- package/dist/ui/elements/grid.js +1 -1
- package/dist/ui/elements/grid.tsx +28 -0
- package/dist/ui/elements/heatmap.js +2 -2
- package/dist/ui/elements/heatmap.tsx +47 -0
- package/dist/ui/elements/heatmap.works.ts +24 -0
- package/dist/ui/elements/hit-box.js +1 -1
- package/dist/ui/elements/hit-box.tsx +46 -0
- package/dist/ui/elements/hit-box.works.tsx +576 -0
- package/dist/ui/elements/image.js +1 -1
- package/dist/ui/elements/image.tsx +28 -0
- package/dist/ui/elements/line-chart.js +2 -2
- package/dist/ui/elements/line-chart.tsx +44 -0
- package/dist/ui/elements/line-chart.works.ts +41 -0
- package/dist/ui/elements/link.js +1 -1
- package/dist/ui/elements/link.tsx +37 -0
- package/dist/ui/elements/link.works.ts +25 -0
- package/dist/ui/elements/media.js +1 -1
- package/dist/ui/elements/media.tsx +56 -0
- package/dist/ui/elements/menu.js +2 -2
- package/dist/ui/elements/menu.tsx +50 -0
- package/dist/ui/elements/menu.works.tsx +153 -0
- package/dist/ui/elements/money.js +1 -1
- package/dist/ui/elements/money.tsx +28 -0
- package/dist/ui/elements/money.works.ts +9 -0
- package/dist/ui/elements/pagination.js +2 -2
- package/dist/ui/elements/pagination.tsx +38 -0
- package/dist/ui/elements/pagination.works.ts +20 -0
- package/dist/ui/elements/progress.js +1 -1
- package/dist/ui/elements/progress.tsx +33 -0
- package/dist/ui/elements/progress.works.ts +8 -0
- package/dist/ui/elements/radio-group.js +2 -2
- package/dist/ui/elements/radio-group.tsx +49 -0
- package/dist/ui/elements/radio-group.works.ts +33 -0
- package/dist/ui/elements/row.js +1 -1
- package/dist/ui/elements/row.tsx +28 -0
- package/dist/ui/elements/scatter.js +1 -1
- package/dist/ui/elements/scatter.tsx +41 -0
- package/dist/ui/elements/scatter.works.ts +38 -0
- package/dist/ui/elements/select.js +2 -2
- package/dist/ui/elements/select.tsx +42 -0
- package/dist/ui/elements/select.works.ts +5 -0
- package/dist/ui/elements/separator.js +1 -1
- package/dist/ui/elements/separator.tsx +30 -0
- package/dist/ui/elements/skeleton.js +1 -1
- package/dist/ui/elements/skeleton.tsx +30 -0
- package/dist/ui/elements/slider.js +1 -1
- package/dist/ui/elements/slider.tsx +33 -0
- package/dist/ui/elements/slider.works.ts +5 -0
- package/dist/ui/elements/sparkline.js +1 -1
- package/dist/ui/elements/sparkline.tsx +36 -0
- package/dist/ui/elements/sparkline.works.ts +32 -0
- package/dist/ui/elements/spinner.js +1 -1
- package/dist/ui/elements/spinner.tsx +32 -0
- package/dist/ui/elements/stack.js +1 -1
- package/dist/ui/elements/stack.tsx +28 -0
- package/dist/ui/elements/stat.js +1 -1
- package/dist/ui/elements/stat.tsx +35 -0
- package/dist/ui/elements/stat.works.ts +14 -0
- package/dist/ui/elements/surface.js +1 -1
- package/dist/ui/elements/surface.tsx +35 -0
- package/dist/ui/elements/surface.works.ts +9 -0
- package/dist/ui/elements/table.js +2 -2
- package/dist/ui/elements/table.tsx +61 -0
- package/dist/ui/elements/table.works.ts +30 -0
- package/dist/ui/elements/tabs.js +2 -2
- package/dist/ui/elements/tabs.tsx +47 -0
- package/dist/ui/elements/tabs.works.ts +84 -0
- package/dist/ui/elements/text.js +1 -1
- package/dist/ui/elements/text.tsx +32 -0
- package/dist/ui/elements/textarea.js +1 -1
- package/dist/ui/elements/textarea.tsx +37 -0
- package/dist/ui/elements/textarea.works.ts +5 -0
- package/dist/ui/elements/time.js +1 -1
- package/dist/ui/elements/time.tsx +34 -0
- package/dist/ui/elements/time.works.ts +25 -0
- package/dist/ui/elements/toggle.js +1 -1
- package/dist/ui/elements/toggle.tsx +33 -0
- package/dist/ui/elements/toggle.works.ts +5 -0
- package/dist/ui/elements/tooltip.js +1 -1
- package/dist/ui/elements/tooltip.tsx +39 -0
- package/dist/ui/elements/tooltip.works.ts +77 -0
- package/dist/ui/elements/value.js +1 -1
- package/dist/ui/elements/value.tsx +30 -0
- package/dist/ui/elements/value.works.ts +9 -0
- package/dist/ui/elements.ts +127 -0
- package/dist/ui/focus-scope.ts +121 -0
- package/dist/ui/fonts.ts +355 -0
- package/dist/ui/generators/dev.ts +79 -0
- package/dist/ui/generators/elements.d.ts +1 -11
- package/dist/ui/generators/elements.js +43 -13
- package/dist/ui/generators/elements.ts +794 -0
- package/dist/ui/generators/i18n.ts +301 -0
- package/dist/ui/generators/icons.ts +214 -0
- package/dist/ui/generators/react.js +10 -3
- package/dist/ui/generators/react.ts +268 -0
- package/dist/ui/generators/routes.d.ts +1 -0
- package/dist/ui/generators/routes.js +89 -69
- package/dist/ui/generators/routes.ts +739 -0
- package/dist/ui/generators/show.ts +259 -0
- package/dist/ui/generators/styles.js +1 -1
- package/dist/ui/generators/styles.ts +670 -0
- package/dist/ui/generators/variants.ts +205 -0
- package/dist/ui/i18n/catalogs/en.ts +191 -0
- package/dist/ui/i18n/format.ts +221 -0
- package/dist/ui/i18n/locales.ts +33 -0
- package/dist/ui/i18n/parse.ts +315 -0
- package/dist/ui/i18n/translator.ts +31 -0
- package/dist/ui/i18n/types.ts +96 -0
- package/dist/ui/icons.ts +331 -0
- package/dist/ui/islands/map.ts +58 -0
- package/dist/ui/localizations.ts +75 -0
- package/dist/ui/makers/message.ts +292 -0
- package/dist/ui/material.ts +236 -0
- package/dist/ui/materials/mat_bordered.ts +13 -0
- package/dist/ui/materials/mat_ceramic.ts +13 -0
- package/dist/ui/materials/mat_clay.ts +13 -0
- package/dist/ui/materials/mat_elevated.ts +24 -0
- package/dist/ui/materials/mat_flat.ts +34 -0
- package/dist/ui/materials/mat_glass.ts +20 -0
- package/dist/ui/materials/mat_lit.ts +13 -0
- package/dist/ui/materials/mat_moulded.ts +11 -0
- package/dist/ui/materials.ts +206 -0
- package/dist/ui/modes.ts +44 -0
- package/dist/ui/navigation.tsx +32 -0
- package/dist/ui/positioning.ts +90 -0
- package/dist/ui/presented.tsx +119 -0
- package/dist/ui/provider.d.ts +2 -0
- package/dist/ui/provider.js +7 -1
- package/dist/ui/provider.tsx +182 -0
- package/dist/ui/render-loop.ts +133 -0
- package/dist/ui/roving-focus.ts +161 -0
- package/dist/ui/screens.js +17 -8
- package/dist/ui/screens.ts +142 -0
- package/dist/ui/scroll-lock.ts +28 -0
- package/dist/ui/series.ts +97 -0
- package/dist/ui/signals.ts +386 -0
- package/dist/ui/styleguide.ts +231 -0
- package/dist/ui/styleguides.ts +374 -0
- package/dist/ui/themes.ts +22 -0
- package/dist/ui/token.js +1 -1
- package/dist/ui/token.ts +78 -0
- package/dist/ui/tokens/shadow.ts +25 -0
- package/dist/ui/tokens.ts +235 -0
- package/dist/ui/validators.ts +232 -0
- package/dist/ui/variant.d.ts +1 -0
- package/dist/ui/variant.js +3 -0
- package/dist/ui/variant.ts +352 -0
- package/dist/ui/variants/var_avatar-round.ts +14 -0
- package/dist/ui/variants/var_bubble-me.ts +14 -0
- package/dist/ui/variants/var_bubble-them.ts +13 -0
- package/dist/ui/variants/var_button-border.ts +76 -0
- package/dist/ui/variants/var_button-cta.ts +76 -0
- package/dist/ui/variants/var_button-destructive-ghost.ts +72 -0
- package/dist/ui/variants/var_button-destructive.ts +76 -0
- package/dist/ui/variants/var_button-fill.ts +76 -0
- package/dist/ui/variants/var_button-ghost.ts +72 -0
- package/dist/ui/variants/var_button-glass.ts +55 -0
- package/dist/ui/variants/var_button-normal.ts +76 -0
- package/dist/ui/variants/var_button-quiet.ts +74 -0
- package/dist/ui/variants/var_card.ts +17 -0
- package/dist/ui/variants/var_chip.ts +13 -0
- package/dist/ui/variants/var_choice.ts +15 -0
- package/dist/ui/variants/var_framed.ts +10 -0
- package/dist/ui/variants/var_glyph-plain.ts +11 -0
- package/dist/ui/variants/var_input.ts +43 -0
- package/dist/ui/variants/var_link-inline.ts +29 -0
- package/dist/ui/variants/var_list-row.ts +34 -0
- package/dist/ui/variants/var_rule.ts +10 -0
- package/dist/ui/variants/var_skeleton-block.ts +12 -0
- package/dist/ui/variants/var_spinner-turning.ts +12 -0
- package/dist/ui/variants/var_stat-tile.ts +13 -0
- package/dist/ui/variants/var_tab-bar.ts +15 -0
- package/dist/ui/variants/var_tap-target.ts +25 -0
- package/dist/ui/variants.ts +64 -0
- package/dist/ui/wrappers.d.ts +4 -1
- package/dist/ui/wrappers.js +60 -20
- package/dist/ui/wrappers.tsx +426 -0
- package/package.json +8 -2
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
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
|
+
mkdirSync(dirname(join(at, one.path)), { recursive: true })
|
|
284
|
+
writeFileSync(join(at, one.path), one.made, 'utf8')
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
stdout.write(
|
|
288
|
+
`i18n: ${String(everything.length)} key(s) across ${String(languages.length)} language(s) — ` +
|
|
289
|
+
`${String(used.size)} named by this interface, ${String(everything.length - used.size)} shipped with the framework\n`,
|
|
290
|
+
)
|
|
291
|
+
if (blank.length > 0) {
|
|
292
|
+
stdout.write(
|
|
293
|
+
`i18n: ${String(blank.length)} still to write — the gate refuses until they are filled:\n` +
|
|
294
|
+
`${blank.slice(0, 20).map((one) => ` ${one}`).join('\n')}\n` +
|
|
295
|
+
(blank.length > 20 ? ` … and ${String(blank.length - 20)} more\n` : ''),
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
return 0
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (invoked(import.meta.url)) exit(await run())
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import { argv, stderr, stdout } from 'node:process'
|
|
6
|
+
import { pathToFileURL } from 'node:url'
|
|
7
|
+
|
|
8
|
+
import { exit } from '@prestack/thread/core/invoked'
|
|
9
|
+
import { findTree, inTree } from '@prestack/thread/core/tree'
|
|
10
|
+
|
|
11
|
+
import { brandExport, drawnAs, setNamed, sizeRows, SET_ROW, type IconSet } from '../icons.ts'
|
|
12
|
+
|
|
13
|
+
interface Row {
|
|
14
|
+
readonly id: string
|
|
15
|
+
readonly is?: string
|
|
16
|
+
readonly sets?: Readonly<Record<string, string>>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Node {
|
|
20
|
+
readonly holds?: readonly Row[]
|
|
21
|
+
readonly declares?: readonly Row[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function inside(node: Node): readonly Row[] {
|
|
25
|
+
return [...(node.holds ?? []), ...(node.declares ?? [])]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const compare = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0)
|
|
29
|
+
|
|
30
|
+
function module(
|
|
31
|
+
set: IconSet,
|
|
32
|
+
drawn: readonly { readonly id: string; readonly as: string }[],
|
|
33
|
+
sizes: readonly { readonly id: string; readonly is: string }[],
|
|
34
|
+
): string {
|
|
35
|
+
const names = [...new Set(drawn.map((one) => one.as))].sort(compare)
|
|
36
|
+
|
|
37
|
+
return [
|
|
38
|
+
|
|
39
|
+
"'use client'",
|
|
40
|
+
'',
|
|
41
|
+
'/* GENERATED by thread — do not edit. Change the icon rows and regenerate. */',
|
|
42
|
+
'',
|
|
43
|
+
`import { ${names.join(', ')} } from '${set.module}'`,
|
|
44
|
+
"import { installIcons } from '../icons.ts'",
|
|
45
|
+
'',
|
|
46
|
+
'installIcons(',
|
|
47
|
+
` { name: '${set.name}', module: '${set.module}', size: ${String(set.size)}, viewBox: ${String(set.viewBox)}` +
|
|
48
|
+
`${set.nativeStroke === undefined ? '' : `, nativeStroke: ${String(set.nativeStroke)}`} },`,
|
|
49
|
+
' {',
|
|
50
|
+
...drawn.map((one) => ` '${one.id}': ${one.as},`),
|
|
51
|
+
' },',
|
|
52
|
+
' {',
|
|
53
|
+
...sizes.map((one) => ` '${one.id}': '${one.is}',`),
|
|
54
|
+
' },',
|
|
55
|
+
')',
|
|
56
|
+
'',
|
|
57
|
+
'/*',
|
|
58
|
+
' * Rendered once by the layout, and that is the point of it.',
|
|
59
|
+
' *',
|
|
60
|
+
' * A bare side-effect import of a client module from a server layout is a client REFERENCE, not',
|
|
61
|
+
' * an evaluation: the install never ran, and every icon refused on a page whose layout plainly',
|
|
62
|
+
' * imported it. Rendering this is what evaluates the module, in the graph the elements run in.',
|
|
63
|
+
' */',
|
|
64
|
+
'export function ThreadIcons(): null {',
|
|
65
|
+
' return null',
|
|
66
|
+
'}',
|
|
67
|
+
'',
|
|
68
|
+
].join('\n')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function exported(root: string, module: string): ReadonlySet<string> | undefined {
|
|
72
|
+
let entry: string
|
|
73
|
+
try {
|
|
74
|
+
entry = createRequire(join(root, 'package.json')).resolve(module)
|
|
75
|
+
} catch {
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
const suffix = `.d.${'ts'}`
|
|
79
|
+
const found = new Set<string>()
|
|
80
|
+
const seen: string[] = []
|
|
81
|
+
let dir = dirname(entry)
|
|
82
|
+
for (let up = 0; up < 5; up += 1) {
|
|
83
|
+
let names: readonly string[] = []
|
|
84
|
+
try {
|
|
85
|
+
names = readdirSync(dir).filter((each) => each.endsWith(suffix))
|
|
86
|
+
} catch {
|
|
87
|
+
names = []
|
|
88
|
+
}
|
|
89
|
+
for (const one of names) seen.push(join(dir, one))
|
|
90
|
+
if (existsSync(join(dir, 'package.json')) && seen.length > 0) break
|
|
91
|
+
const next = dirname(dir)
|
|
92
|
+
if (next === dir) break
|
|
93
|
+
dir = next
|
|
94
|
+
}
|
|
95
|
+
for (const one of seen) {
|
|
96
|
+
let text = ''
|
|
97
|
+
try {
|
|
98
|
+
text = readFileSync(one, 'utf8')
|
|
99
|
+
} catch {
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
for (const each of text.matchAll(/declare const (\w+)/g)) found.add(each[1] ?? '')
|
|
103
|
+
for (const each of text.matchAll(/(\w+) as (\w+)/g)) found.add(each[2] ?? '')
|
|
104
|
+
for (const each of text.matchAll(/export declare const (\w+)/g)) found.add(each[1] ?? '')
|
|
105
|
+
}
|
|
106
|
+
return found.size === 0 ? undefined : found
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function node(file: string, name: string): Promise<Node> {
|
|
110
|
+
const at = inTree(findTree(), file)
|
|
111
|
+
if (!existsSync(at)) {
|
|
112
|
+
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`)
|
|
113
|
+
exit(0)
|
|
114
|
+
}
|
|
115
|
+
const held = (await import(pathToFileURL(at).href)) as Record<string, unknown>
|
|
116
|
+
const one = held[name]
|
|
117
|
+
if (one === undefined) throw new Error(`_threads/${file} exports no \`${name}\`.`)
|
|
118
|
+
return one as Node
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function run(): Promise<number> {
|
|
122
|
+
const tree = findTree()
|
|
123
|
+
const rows = inside(await node('icons.ts', 'ICON'))
|
|
124
|
+
|
|
125
|
+
const picked = rows.find((row) => row.id === SET_ROW)
|
|
126
|
+
if (picked?.is === undefined) {
|
|
127
|
+
stderr.write(
|
|
128
|
+
`thread: the icon sheet declares no \`${SET_ROW}\` row, so nothing says which library the app ` +
|
|
129
|
+
'draws from. One row, naming a registered library.\n',
|
|
130
|
+
)
|
|
131
|
+
return 1
|
|
132
|
+
}
|
|
133
|
+
const set = setNamed(picked.is)
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
createRequire(join(tree.at, 'package.json')).resolve(set.module)
|
|
137
|
+
} catch {
|
|
138
|
+
stderr.write(
|
|
139
|
+
`thread: the icon rows draw from \`${set.name}\`, which is the package \`${set.module}\`, and this ` +
|
|
140
|
+
`app does not have it installed. Add it, or name a library the app already has.\n`,
|
|
141
|
+
)
|
|
142
|
+
return 1
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const sizes = [...sizeRows(rows)].sort((one, two) => compare(one.id, two.id))
|
|
146
|
+
const held = new Set(sizes.map((one) => one.id))
|
|
147
|
+
|
|
148
|
+
if (sizes.length === 0) {
|
|
149
|
+
stderr.write(
|
|
150
|
+
'thread: the icon sheet declares no size row, so nothing says how large a symbol is drawn. ' +
|
|
151
|
+
'One row per size, holding a length.\n',
|
|
152
|
+
)
|
|
153
|
+
return 1
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const drawn = rows
|
|
157
|
+
.filter((row) => row.id !== SET_ROW && !held.has(row.id))
|
|
158
|
+
.map((row) => ({ id: row.id, as: row.sets?.[set.name] ?? drawnAs(set.name, row.id, row.id) }))
|
|
159
|
+
.sort((one, two) => compare(one.id, two.id))
|
|
160
|
+
|
|
161
|
+
const missing = drawn.find((one) => one.as === '')
|
|
162
|
+
if (missing !== undefined) {
|
|
163
|
+
stderr.write(`thread: \`${missing.id}\` names no symbol in \`${set.name}\`.\n`)
|
|
164
|
+
return 1
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const known = exported(tree.at, set.module)
|
|
168
|
+
if (known === undefined) {
|
|
169
|
+
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`)
|
|
170
|
+
} else {
|
|
171
|
+
const unknown = drawn.find((one) => !one.as.startsWith('si') && !known.has(one.as))
|
|
172
|
+
if (unknown !== undefined) {
|
|
173
|
+
stderr.write(
|
|
174
|
+
`thread: \`${unknown.id}\` is drawn as \`${unknown.as}\` in \`${set.name}\`, and that library does not export it — its ` +
|
|
175
|
+
`declarations list ${String(known.size)} names and none of them is that one. Check the library's own naming; the module ` +
|
|
176
|
+
'would fail to build with it.\n',
|
|
177
|
+
)
|
|
178
|
+
return 1
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const brand = drawn.filter((one) => one.as.startsWith('si')).map((one) => brandExport(one.id))
|
|
183
|
+
if (brand.length > 0) stdout.write(`thread: ${String(brand.length)} brand icon(s) drawn from their own set\n`)
|
|
184
|
+
|
|
185
|
+
const at = join(tree.at, 'app/_generated/icons.ts')
|
|
186
|
+
const made = module(set, drawn, sizes)
|
|
187
|
+
|
|
188
|
+
if (argv.includes('--check')) {
|
|
189
|
+
let current = ''
|
|
190
|
+
try {
|
|
191
|
+
current = readFileSync(at, 'utf8')
|
|
192
|
+
} catch {
|
|
193
|
+
current = ''
|
|
194
|
+
}
|
|
195
|
+
if (current === made) {
|
|
196
|
+
stdout.write('icons: app/_generated/icons.ts is what the rows produce\n')
|
|
197
|
+
return 0
|
|
198
|
+
}
|
|
199
|
+
stderr.write(
|
|
200
|
+
'icons: app/_generated/icons.ts is not what the rows produce. A derived file that differs from ' +
|
|
201
|
+
'its generator is a hand edit wearing a generated name. Run `thread icons` and commit what it writes.\n',
|
|
202
|
+
)
|
|
203
|
+
return 1
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
mkdirSync(dirname(at), { recursive: true })
|
|
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())
|
|
@@ -9,7 +9,7 @@ const OWNED = /\.(t|j)sx$/;
|
|
|
9
9
|
const VARIANT_MEMBER = /^[a-z]/;
|
|
10
10
|
const ARRANGERS = new Set(['composition', 'screen']);
|
|
11
11
|
const HOST = /^[a-z]/;
|
|
12
|
-
const
|
|
12
|
+
const MAKES = 'element';
|
|
13
13
|
const ONE_WORD = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
14
14
|
function prefixed(called) {
|
|
15
15
|
return called === 'screen' ? 's-' : 'c-';
|
|
@@ -30,6 +30,9 @@ function arranging(held, at) {
|
|
|
30
30
|
}
|
|
31
31
|
return found?.own;
|
|
32
32
|
}
|
|
33
|
+
function within(held, at) {
|
|
34
|
+
return held.some((one) => at >= one.from && at < one.to);
|
|
35
|
+
}
|
|
33
36
|
function tagOf(node) {
|
|
34
37
|
return node.tagName;
|
|
35
38
|
}
|
|
@@ -66,7 +69,11 @@ export function compiled(text, at) {
|
|
|
66
69
|
const arranges = [];
|
|
67
70
|
const edits = [];
|
|
68
71
|
let because;
|
|
72
|
+
const writing = [];
|
|
69
73
|
const calls = (node) => {
|
|
74
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === MAKES) {
|
|
75
|
+
writing.push({ from: node.getStart(), to: node.getEnd() });
|
|
76
|
+
}
|
|
70
77
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && ARRANGERS.has(node.expression.text)) {
|
|
71
78
|
const first = node.arguments[0];
|
|
72
79
|
const said = first === undefined ? undefined : quoted(first);
|
|
@@ -96,13 +103,13 @@ export function compiled(text, at) {
|
|
|
96
103
|
const tag = tagOf(node);
|
|
97
104
|
const held = named(attributes(node));
|
|
98
105
|
const worn = [];
|
|
99
|
-
if (ts.isIdentifier(tag) && HOST.test(tag.text) && !
|
|
106
|
+
if (ts.isIdentifier(tag) && HOST.test(tag.text) && !within(writing, node.getStart())) {
|
|
100
107
|
because =
|
|
101
108
|
`${at} writes <${tag.text}>, which is a host element: it wears no declared style, sits in no ` +
|
|
102
109
|
'catalogue, and the style guide, a rebrand and every overlay read past it. Declare the element ' +
|
|
103
110
|
'and wear its look, or use the declared groupings where a host element would have gone';
|
|
104
111
|
}
|
|
105
|
-
if (held.className !== undefined && !
|
|
112
|
+
if (held.className !== undefined && !within(writing, node.getStart())) {
|
|
106
113
|
because =
|
|
107
114
|
`${at} writes a \`className\` by hand, which is CSS written in place: it names no style guide ` +
|
|
108
115
|
'row, wears no declared variant, and the style guide, a rebrand and every overlay read past ' +
|