@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.
- package/CHANGELOG.md +177 -0
- 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.js +2 -3
- package/dist/ui/animations.ts +316 -0
- package/dist/ui/attach.js +13 -10
- package/dist/ui/attach.ts +607 -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 +11 -2
- package/dist/ui/compositions.js +283 -59
- package/dist/ui/compositions.ts +633 -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.js +2 -1
- package/dist/ui/generators/dev.ts +80 -0
- package/dist/ui/generators/elements.d.ts +1 -11
- package/dist/ui/generators/elements.js +44 -14
- package/dist/ui/generators/elements.ts +794 -0
- package/dist/ui/generators/i18n.js +2 -3
- package/dist/ui/generators/i18n.ts +300 -0
- package/dist/ui/generators/icons.js +3 -12
- package/dist/ui/generators/icons.ts +214 -0
- package/dist/ui/generators/react.js +13 -5
- package/dist/ui/generators/react.ts +269 -0
- package/dist/ui/generators/routes.d.ts +1 -0
- package/dist/ui/generators/routes.js +99 -83
- package/dist/ui/generators/routes.ts +743 -0
- package/dist/ui/generators/show.js +4 -12
- package/dist/ui/generators/show.ts +260 -0
- package/dist/ui/generators/styles.js +3 -4
- package/dist/ui/generators/styles.ts +669 -0
- package/dist/ui/generators/variants.js +2 -3
- package/dist/ui/generators/variants.ts +204 -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/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.js +2 -3
- package/dist/ui/materials.ts +205 -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 +22 -22
- package/dist/ui/screens.ts +141 -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.js +1 -2
- package/dist/ui/tokens.ts +234 -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
- package/dist/ui/makers/message.d.ts +0 -1
- package/dist/ui/makers/message.js +0 -230
package/dist/ui/icons.ts
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import type { Threaded } from '@prestack/thread/core/thread'
|
|
2
|
+
|
|
3
|
+
export interface IconSet {
|
|
4
|
+
readonly name: string
|
|
5
|
+
readonly module: string
|
|
6
|
+
|
|
7
|
+
readonly size: number
|
|
8
|
+
|
|
9
|
+
readonly nativeStroke?: number
|
|
10
|
+
|
|
11
|
+
readonly viewBox: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const SETS: Readonly<Record<string, IconSet>> = {
|
|
15
|
+
lucide: { name: 'lucide', module: 'lucide-react', size: 24, nativeStroke: 2, viewBox: 24 },
|
|
16
|
+
phosphor: { name: 'phosphor', module: '@phosphor-icons/react', size: 24, nativeStroke: 1.5, viewBox: 256 },
|
|
17
|
+
tabler: { name: 'tabler', module: '@tabler/icons-react', size: 24, nativeStroke: 2, viewBox: 24 },
|
|
18
|
+
heroicons: { name: 'heroicons', module: '@heroicons/react/24/outline', size: 24, nativeStroke: 1.5, viewBox: 24 },
|
|
19
|
+
feather: { name: 'feather', module: 'react-icons/fi', size: 24, nativeStroke: 2, viewBox: 24 },
|
|
20
|
+
remix: { name: 'remix', module: 'react-icons/ri', size: 24, viewBox: 24 },
|
|
21
|
+
bootstrap: { name: 'bootstrap', module: 'react-icons/bs', size: 16, viewBox: 16 },
|
|
22
|
+
material: { name: 'material', module: 'react-icons/md', size: 24, viewBox: 24 },
|
|
23
|
+
ionicons: { name: 'ionicons', module: 'react-icons/io5', size: 24, viewBox: 512 },
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const ICON_TARGET = { size: 24, normalizedStroke: 1.75 } as const
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_ICON = 'md'
|
|
29
|
+
|
|
30
|
+
export function sizeRows(rows: readonly { readonly id: string; readonly is?: string }[]): readonly {
|
|
31
|
+
readonly id: string
|
|
32
|
+
readonly is: string
|
|
33
|
+
}[] {
|
|
34
|
+
return rows
|
|
35
|
+
.filter((row): row is { id: string; is: string } => row.id !== SET_ROW && typeof row.is === 'string')
|
|
36
|
+
.map((row) => ({ id: row.id, is: row.is }))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function sizeNamed(name: string): string {
|
|
40
|
+
return name.startsWith(SIZE_PREFIX) ? name : `${SIZE_PREFIX}${name}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function strokeFor(
|
|
44
|
+
|
|
45
|
+
set: { readonly viewBox: number; readonly nativeStroke?: number },
|
|
46
|
+
target: number = ICON_TARGET.normalizedStroke,
|
|
47
|
+
): number | undefined {
|
|
48
|
+
if (set.nativeStroke === undefined) return undefined
|
|
49
|
+
return Number(((target * set.viewBox) / ICON_TARGET.size).toFixed(3))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const BRAND_MODULE = 'simple-icons'
|
|
53
|
+
|
|
54
|
+
const DRAWN: Readonly<Record<string, Readonly<Record<string, string>>>> = {
|
|
55
|
+
|
|
56
|
+
bell: { lucide: 'Bell' },
|
|
57
|
+
star: { lucide: 'Star' },
|
|
58
|
+
heart: { lucide: 'Heart' },
|
|
59
|
+
share: { lucide: 'Share2' },
|
|
60
|
+
bookmark: { lucide: 'Bookmark' },
|
|
61
|
+
send: { lucide: 'Send' },
|
|
62
|
+
attach: { lucide: 'Paperclip' },
|
|
63
|
+
play: { lucide: 'Play' },
|
|
64
|
+
pause: { lucide: 'Pause' },
|
|
65
|
+
chevronLeft: { lucide: 'ChevronLeft' },
|
|
66
|
+
chevronUp: { lucide: 'ChevronUp' },
|
|
67
|
+
arrowUp: { lucide: 'ArrowUp' },
|
|
68
|
+
arrowDown: { lucide: 'ArrowDown' },
|
|
69
|
+
list: { lucide: 'List' },
|
|
70
|
+
signOut: { lucide: 'LogOut' },
|
|
71
|
+
signIn: { lucide: 'LogIn' },
|
|
72
|
+
secure: { lucide: 'ShieldCheck' },
|
|
73
|
+
card: { lucide: 'CreditCard' },
|
|
74
|
+
place: { lucide: 'MapPin' },
|
|
75
|
+
phone: { lucide: 'Phone' },
|
|
76
|
+
globe: { lucide: 'Globe' },
|
|
77
|
+
unlock: { lucide: 'Unlock' },
|
|
78
|
+
warning: { lucide: 'CircleAlert' },
|
|
79
|
+
success: { lucide: 'CircleCheck' },
|
|
80
|
+
failure: { lucide: 'CircleX' },
|
|
81
|
+
help: { lucide: 'CircleHelp' },
|
|
82
|
+
save: { lucide: 'Save' },
|
|
83
|
+
undo: { lucide: 'Undo2' },
|
|
84
|
+
redo: { lucide: 'Redo2' },
|
|
85
|
+
zoomIn: { lucide: 'ZoomIn' },
|
|
86
|
+
zoomOut: { lucide: 'ZoomOut' },
|
|
87
|
+
folder: { lucide: 'Folder' },
|
|
88
|
+
document: { lucide: 'FileText' },
|
|
89
|
+
people: { lucide: 'Users' },
|
|
90
|
+
addPerson: { lucide: 'UserPlus' },
|
|
91
|
+
tag: { lucide: 'Tag' },
|
|
92
|
+
flag: { lucide: 'Flag' },
|
|
93
|
+
archive: { lucide: 'Archive' },
|
|
94
|
+
inbox: { lucide: 'Inbox' },
|
|
95
|
+
light: { lucide: 'Sun' },
|
|
96
|
+
dark: { lucide: 'Moon' },
|
|
97
|
+
sound: { lucide: 'Volume2' },
|
|
98
|
+
mute: { lucide: 'VolumeX' },
|
|
99
|
+
microphone: { lucide: 'Mic' },
|
|
100
|
+
camera: { lucide: 'Camera' },
|
|
101
|
+
|
|
102
|
+
close: { lucide: 'X', phosphor: 'X', tabler: 'IconX', heroicons: 'XMarkIcon', feather: 'FiX', remix: 'RiCloseLine', bootstrap: 'BsX', material: 'MdClose', ionicons: 'IoClose' },
|
|
103
|
+
search: { lucide: 'Search', phosphor: 'MagnifyingGlass', tabler: 'IconSearch', heroicons: 'MagnifyingGlassIcon', feather: 'FiSearch', remix: 'RiSearchLine', bootstrap: 'BsSearch', material: 'MdSearch', ionicons: 'IoSearch' },
|
|
104
|
+
check: { lucide: 'Check', phosphor: 'Check', tabler: 'IconCheck', heroicons: 'CheckIcon', feather: 'FiCheck', remix: 'RiCheckLine', bootstrap: 'BsCheck', material: 'MdCheck', ionicons: 'IoCheckmark' },
|
|
105
|
+
chevronDown: { lucide: 'ChevronDown', phosphor: 'CaretDown', tabler: 'IconChevronDown', heroicons: 'ChevronDownIcon', feather: 'FiChevronDown', remix: 'RiArrowDownSLine', bootstrap: 'BsChevronDown', material: 'MdKeyboardArrowDown', ionicons: 'IoChevronDown' },
|
|
106
|
+
chevronRight: { lucide: 'ChevronRight', phosphor: 'CaretRight', tabler: 'IconChevronRight', heroicons: 'ChevronRightIcon', feather: 'FiChevronRight', remix: 'RiArrowRightSLine', bootstrap: 'BsChevronRight', material: 'MdKeyboardArrowRight', ionicons: 'IoChevronForward' },
|
|
107
|
+
plus: { lucide: 'Plus', phosphor: 'Plus', tabler: 'IconPlus', heroicons: 'PlusIcon', feather: 'FiPlus', remix: 'RiAddLine', bootstrap: 'BsPlus', material: 'MdAdd', ionicons: 'IoAdd' },
|
|
108
|
+
minus: { lucide: 'Minus', phosphor: 'Minus', tabler: 'IconMinus', heroicons: 'MinusIcon', feather: 'FiMinus', remix: 'RiSubtractLine', bootstrap: 'BsDash', material: 'MdRemove', ionicons: 'IoRemove' },
|
|
109
|
+
menu: { lucide: 'Menu', phosphor: 'List', tabler: 'IconMenu2', heroicons: 'Bars3Icon', feather: 'FiMenu', remix: 'RiMenuLine', bootstrap: 'BsList', material: 'MdMenu', ionicons: 'IoMenu' },
|
|
110
|
+
more: { lucide: 'MoreHorizontal', phosphor: 'DotsThree', tabler: 'IconDots', heroicons: 'EllipsisHorizontalIcon', feather: 'FiMoreHorizontal', remix: 'RiMoreLine', bootstrap: 'BsThreeDots', material: 'MdMoreHoriz', ionicons: 'IoEllipsisHorizontal' },
|
|
111
|
+
user: { lucide: 'User', phosphor: 'User', tabler: 'IconUser', heroicons: 'UserIcon', feather: 'FiUser', remix: 'RiUserLine', bootstrap: 'BsPerson', material: 'MdPerson', ionicons: 'IoPerson' },
|
|
112
|
+
settings: { lucide: 'Settings', phosphor: 'Gear', tabler: 'IconSettings', heroicons: 'Cog6ToothIcon', feather: 'FiSettings', remix: 'RiSettingsLine', bootstrap: 'BsGear', material: 'MdSettings', ionicons: 'IoSettings' },
|
|
113
|
+
trash: { lucide: 'Trash2', phosphor: 'Trash', tabler: 'IconTrash', heroicons: 'TrashIcon', feather: 'FiTrash2', remix: 'RiDeleteBinLine', bootstrap: 'BsTrash', material: 'MdDelete', ionicons: 'IoTrash' },
|
|
114
|
+
edit: { lucide: 'Pencil', phosphor: 'PencilSimple', tabler: 'IconPencil', heroicons: 'PencilIcon', feather: 'FiEdit2', remix: 'RiPencilLine', bootstrap: 'BsPencil', material: 'MdEdit', ionicons: 'IoPencil' },
|
|
115
|
+
calendar: { lucide: 'Calendar', phosphor: 'Calendar', tabler: 'IconCalendar', heroicons: 'CalendarIcon', feather: 'FiCalendar', remix: 'RiCalendarLine', bootstrap: 'BsCalendar', material: 'MdCalendarToday', ionicons: 'IoCalendar' },
|
|
116
|
+
clock: { lucide: 'Clock', phosphor: 'Clock', tabler: 'IconClock', heroicons: 'ClockIcon', feather: 'FiClock', remix: 'RiTimeLine', bootstrap: 'BsClock', material: 'MdAccessTime', ionicons: 'IoTime' },
|
|
117
|
+
eye: { lucide: 'Eye', phosphor: 'Eye', tabler: 'IconEye', heroicons: 'EyeIcon', feather: 'FiEye', remix: 'RiEyeLine', bootstrap: 'BsEye', material: 'MdVisibility', ionicons: 'IoEye' },
|
|
118
|
+
eyeOff: { lucide: 'EyeOff', phosphor: 'EyeSlash', tabler: 'IconEyeOff', heroicons: 'EyeSlashIcon', feather: 'FiEyeOff', remix: 'RiEyeOffLine', bootstrap: 'BsEyeSlash', material: 'MdVisibilityOff', ionicons: 'IoEyeOff' },
|
|
119
|
+
alert: { lucide: 'TriangleAlert', phosphor: 'Warning', tabler: 'IconAlertTriangle', heroicons: 'ExclamationTriangleIcon', feather: 'FiAlertTriangle', remix: 'RiErrorWarningLine', bootstrap: 'BsExclamationTriangle', material: 'MdWarning', ionicons: 'IoWarning' },
|
|
120
|
+
info: { lucide: 'Info', phosphor: 'Info', tabler: 'IconInfoCircle', heroicons: 'InformationCircleIcon', feather: 'FiInfo', remix: 'RiInformationLine', bootstrap: 'BsInfoCircle', material: 'MdInfo', ionicons: 'IoInformationCircle' },
|
|
121
|
+
externalLink: { lucide: 'ExternalLink', phosphor: 'ArrowSquareOut', tabler: 'IconExternalLink', heroicons: 'ArrowTopRightOnSquareIcon', feather: 'FiExternalLink', remix: 'RiExternalLinkLine', bootstrap: 'BsBoxArrowUpRight', material: 'MdOpenInNew', ionicons: 'IoOpenOutline' },
|
|
122
|
+
copy: { lucide: 'Copy', phosphor: 'Copy', tabler: 'IconCopy', heroicons: 'DocumentDuplicateIcon', feather: 'FiCopy', remix: 'RiFileCopyLine', bootstrap: 'BsClipboard', material: 'MdContentCopy', ionicons: 'IoCopy' },
|
|
123
|
+
download: { lucide: 'Download', phosphor: 'DownloadSimple', tabler: 'IconDownload', heroicons: 'ArrowDownTrayIcon', feather: 'FiDownload', remix: 'RiDownloadLine', bootstrap: 'BsDownload', material: 'MdDownload', ionicons: 'IoDownload' },
|
|
124
|
+
upload: { lucide: 'Upload', phosphor: 'UploadSimple', tabler: 'IconUpload', heroicons: 'ArrowUpTrayIcon', feather: 'FiUpload', remix: 'RiUploadLine', bootstrap: 'BsUpload', material: 'MdUpload', ionicons: 'IoCloudUpload' },
|
|
125
|
+
filter: { lucide: 'Filter', phosphor: 'Funnel', tabler: 'IconFilter', heroicons: 'FunnelIcon', feather: 'FiFilter', remix: 'RiFilterLine', bootstrap: 'BsFunnel', material: 'MdFilterList', ionicons: 'IoFilter' },
|
|
126
|
+
home: { lucide: 'House', phosphor: 'House', tabler: 'IconHome', heroicons: 'HomeIcon', feather: 'FiHome', remix: 'RiHomeLine', bootstrap: 'BsHouse', material: 'MdHome', ionicons: 'IoHome' },
|
|
127
|
+
mail: { lucide: 'Mail', phosphor: 'Envelope', tabler: 'IconMail', heroicons: 'EnvelopeIcon', feather: 'FiMail', remix: 'RiMailLine', bootstrap: 'BsEnvelope', material: 'MdMail', ionicons: 'IoMail' },
|
|
128
|
+
lock: { lucide: 'Lock', phosphor: 'Lock', tabler: 'IconLock', heroicons: 'LockClosedIcon', feather: 'FiLock', remix: 'RiLockLine', bootstrap: 'BsLock', material: 'MdLock', ionicons: 'IoLockClosed' },
|
|
129
|
+
refresh: { lucide: 'RefreshCw', phosphor: 'ArrowsClockwise', tabler: 'IconRefresh', heroicons: 'ArrowPathIcon', feather: 'FiRefreshCw', remix: 'RiRefreshLine', bootstrap: 'BsArrowRepeat', material: 'MdRefresh', ionicons: 'IoRefresh' },
|
|
130
|
+
arrowLeft: { lucide: 'ArrowLeft', phosphor: 'ArrowLeft', tabler: 'IconArrowLeft', heroicons: 'ArrowLeftIcon', feather: 'FiArrowLeft', remix: 'RiArrowLeftLine', bootstrap: 'BsArrowLeft', material: 'MdArrowBack', ionicons: 'IoArrowBack' },
|
|
131
|
+
arrowRight: { lucide: 'ArrowRight', phosphor: 'ArrowRight', tabler: 'IconArrowRight', heroicons: 'ArrowRightIcon', feather: 'FiArrowRight', remix: 'RiArrowRightLine', bootstrap: 'BsArrowRight', material: 'MdArrowForward', ionicons: 'IoArrowForward' },
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const REGISTRY = new Map<string, Readonly<Record<string, string>>>(Object.entries(DRAWN))
|
|
135
|
+
const SET_REGISTRY = new Map<string, IconSet>(Object.entries(SETS))
|
|
136
|
+
|
|
137
|
+
export function defineIconSet(set: IconSet): void {
|
|
138
|
+
if (SET_REGISTRY.has(set.name)) throw new Error(`icon set "${set.name}" is already registered`)
|
|
139
|
+
SET_REGISTRY.set(set.name, set)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function defineIcon(semantic: string, names: Readonly<Record<string, string>>): void {
|
|
143
|
+
REGISTRY.set(semantic, { ...REGISTRY.get(semantic), ...names })
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function iconSets(): readonly IconSet[] {
|
|
147
|
+
return [...SET_REGISTRY.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function iconSet(name: string): IconSet | undefined {
|
|
151
|
+
return SET_REGISTRY.get(name)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function iconNames(): readonly string[] {
|
|
155
|
+
return [...REGISTRY.keys()].sort()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function iconIn(set: string, semantic: string): string | undefined {
|
|
159
|
+
return REGISTRY.get(semantic)?.[set]
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function brandExport(brand: string): string {
|
|
163
|
+
return `si${brand.charAt(0).toUpperCase()}${brand.slice(1)}`
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
type Row = Omit<Threaded, 'at'> & {
|
|
167
|
+
readonly section?: string
|
|
168
|
+
readonly is?: string
|
|
169
|
+
readonly sets?: Readonly<Record<string, string>>
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const GROUPED: Readonly<Record<string, readonly string[]>> = {
|
|
173
|
+
navigation: ['chevronUp', 'chevronDown', 'chevronLeft', 'chevronRight', 'arrowUp', 'arrowDown', 'arrowLeft', 'arrowRight', 'home', 'menu', 'more', 'externalLink', 'list'],
|
|
174
|
+
action: ['close', 'check', 'plus', 'minus', 'search', 'filter', 'edit', 'trash', 'save', 'undo', 'redo', 'copy', 'download', 'upload', 'share', 'send', 'attach', 'refresh', 'archive', 'zoomIn', 'zoomOut'],
|
|
175
|
+
status: ['alert', 'info', 'warning', 'success', 'failure', 'help', 'bell', 'flag', 'tag'],
|
|
176
|
+
account: ['user', 'people', 'addPerson', 'signIn', 'signOut', 'settings', 'lock', 'unlock', 'secure', 'eye', 'eyeOff'],
|
|
177
|
+
content: ['document', 'folder', 'inbox', 'mail', 'calendar', 'clock', 'bookmark', 'star', 'heart', 'card', 'place', 'phone', 'globe'],
|
|
178
|
+
media: ['play', 'pause', 'sound', 'mute', 'microphone', 'camera', 'light', 'dark'],
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const sectionOf = (id: string): string => {
|
|
182
|
+
for (const [name, held] of Object.entries(GROUPED)) if (held.includes(id)) return name
|
|
183
|
+
return 'other'
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const SET_ROW = 'library'
|
|
187
|
+
|
|
188
|
+
export const SIZE_PREFIX = 'icon-'
|
|
189
|
+
|
|
190
|
+
export const holds: readonly Row[] = [
|
|
191
|
+
{
|
|
192
|
+
id: SET_ROW,
|
|
193
|
+
section: 'the library',
|
|
194
|
+
is: 'lucide',
|
|
195
|
+
describes:
|
|
196
|
+
'Which library every icon is drawn from. One name, because a set answers for the whole app — swapping it redraws every icon at once, and an icon a set does not carry is refused rather than quietly missing.',
|
|
197
|
+
version: 1,
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: `${SIZE_PREFIX}sm`,
|
|
201
|
+
section: 'size',
|
|
202
|
+
is: '16px',
|
|
203
|
+
describes:
|
|
204
|
+
'A symbol drawn inside a line of text, at the size that sits on the same baseline as the words around it.',
|
|
205
|
+
version: 1,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: `${SIZE_PREFIX}md`,
|
|
209
|
+
section: 'size',
|
|
210
|
+
is: '24px',
|
|
211
|
+
describes:
|
|
212
|
+
'A symbol beside wording, at the size most controls use. What anything asking for an icon is drawn at unless it says otherwise.',
|
|
213
|
+
version: 1,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: `${SIZE_PREFIX}lg`,
|
|
217
|
+
section: 'size',
|
|
218
|
+
is: '32px',
|
|
219
|
+
describes:
|
|
220
|
+
'A symbol standing on its own, where the drawing is the content rather than a marker beside it.',
|
|
221
|
+
version: 1,
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
...Object.keys(DRAWN)
|
|
225
|
+
.sort((one, two) => {
|
|
226
|
+
const group = sectionOf(one) < sectionOf(two) ? -1 : sectionOf(one) > sectionOf(two) ? 1 : 0
|
|
227
|
+
return group !== 0 ? group : one < two ? -1 : one > two ? 1 : 0
|
|
228
|
+
})
|
|
229
|
+
.map((semantic): Row => ({
|
|
230
|
+
id: semantic,
|
|
231
|
+
section: sectionOf(semantic),
|
|
232
|
+
sets: DRAWN[semantic] ?? {},
|
|
233
|
+
describes: `The \`${semantic}\` icon, and what it is called in each library — so wording asks for the meaning and the set decides the drawing.`,
|
|
234
|
+
version: 1,
|
|
235
|
+
})),
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
export const ICONS = {
|
|
239
|
+
id: 'icon',
|
|
240
|
+
at: 'thread/ui/icons.ts',
|
|
241
|
+
describes:
|
|
242
|
+
'Every icon an app may draw and which library it is drawn from. A row is a meaning — close, search, the chevron that opens a thing — and holds what that meaning is called in each library, so an element asks for the meaning and never for a library’s own name for it. One row says which library answers, and changing it redraws the whole app. An icon nothing declares is refused rather than rendered as a gap, and an app adds its own rows to this same list.',
|
|
243
|
+
version: 1,
|
|
244
|
+
cites: [
|
|
245
|
+
{
|
|
246
|
+
on: '2026-08-28',
|
|
247
|
+
said: 'Same for icons. We define icons available in icons.ts AND we make UI read from there and icons.ts pick what icon from what library.',
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
holds,
|
|
251
|
+
} satisfies Threaded & { readonly holds: readonly Row[] }
|
|
252
|
+
|
|
253
|
+
export function drawnAs(set: string, semantic: string, by: string): string {
|
|
254
|
+
const held = REGISTRY.get(semantic)
|
|
255
|
+
if (held === undefined) {
|
|
256
|
+
const has = [...REGISTRY.keys()].sort().map((one) => `\`${one}\``)
|
|
257
|
+
throw new Error(
|
|
258
|
+
`\`${by}\` asks for the \`${semantic}\` icon, and no row declares one by that name. ` +
|
|
259
|
+
`The declared icons are ${has.join(', ')}. Add a row for it, or name one that exists.`,
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
const drawn = held[set]
|
|
263
|
+
if (drawn !== undefined) return drawn
|
|
264
|
+
const has = Object.keys(held).sort().map((one) => `\`${one}\``)
|
|
265
|
+
throw new Error(
|
|
266
|
+
`\`${by}\` asks for the \`${semantic}\` icon and the app draws from \`${set}\`, which does not carry it. ` +
|
|
267
|
+
`It is carried by ${has.join(', ')}. Add \`${set}\` to that row, or draw from a library that has it.`,
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function setNamed(name: string): IconSet {
|
|
272
|
+
const found = SET_REGISTRY.get(name)
|
|
273
|
+
if (found !== undefined) return found
|
|
274
|
+
const has = [...SET_REGISTRY.keys()].sort().map((one) => `\`${one}\``)
|
|
275
|
+
throw new Error(
|
|
276
|
+
`\`${name}\` is named as the icon library, and no set by that name is registered. ` +
|
|
277
|
+
`The registered libraries are ${has.join(', ')}.`,
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export type IconComponent = (props: {
|
|
282
|
+
readonly size?: number
|
|
283
|
+
readonly strokeWidth?: number
|
|
284
|
+
readonly className?: string
|
|
285
|
+
readonly 'aria-hidden'?: boolean
|
|
286
|
+
readonly 'aria-label'?: string
|
|
287
|
+
readonly role?: string
|
|
288
|
+
}) => unknown
|
|
289
|
+
|
|
290
|
+
let installed:
|
|
291
|
+
| {
|
|
292
|
+
readonly set: IconSet
|
|
293
|
+
readonly drawn: Readonly<Record<string, IconComponent>>
|
|
294
|
+
readonly sizes?: Readonly<Record<string, string>>
|
|
295
|
+
}
|
|
296
|
+
| undefined
|
|
297
|
+
|
|
298
|
+
export function installIcons(
|
|
299
|
+
set: IconSet,
|
|
300
|
+
drawn: Readonly<Record<string, IconComponent>>,
|
|
301
|
+
sizes?: Readonly<Record<string, string>>,
|
|
302
|
+
): void {
|
|
303
|
+
installed = { set, drawn, ...(sizes === undefined ? {} : { sizes }) }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function installedSet(): IconSet | undefined {
|
|
307
|
+
return installed?.set
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function iconSize(name: string = DEFAULT_ICON): number {
|
|
311
|
+
const held = installed?.sizes?.[sizeNamed(name)]
|
|
312
|
+
if (held === undefined) return ICON_TARGET.size
|
|
313
|
+
const measured = Number.parseFloat(held)
|
|
314
|
+
return Number.isFinite(measured) ? measured : ICON_TARGET.size
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const NOT_INSTALLED =
|
|
318
|
+
'No icons are installed. The library an app draws from and the symbols it draws are generated ' +
|
|
319
|
+
'from the icon rows — run `thread icons` and import what it writes, beside the stylesheet. ' +
|
|
320
|
+
'Without it every icon renders as an empty box, which looks like a missing glyph and is a wiring gap.'
|
|
321
|
+
|
|
322
|
+
export function drawnFor(semantic: string): IconComponent {
|
|
323
|
+
if (installed === undefined) throw new Error(NOT_INSTALLED)
|
|
324
|
+
const component = installed.drawn[semantic]
|
|
325
|
+
if (component !== undefined) return component
|
|
326
|
+
const has = Object.keys(installed.drawn).sort().map((one) => `\`${one}\``)
|
|
327
|
+
throw new Error(
|
|
328
|
+
`\`${semantic}\` is asked for as an icon, and no row declares one by that name. ` +
|
|
329
|
+
`The declared icons are ${has.join(', ')}. Add a row for it, or name one that exists.`,
|
|
330
|
+
)
|
|
331
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
const ElementName = z
|
|
4
|
+
.string()
|
|
5
|
+
.regex(/^[A-Z][A-Za-z0-9]*$/, 'must be a component name, e.g. "Card"')
|
|
6
|
+
.describe('The export name. A screen is reached by importing it, so this is its address.')
|
|
7
|
+
|
|
8
|
+
export const ViewId = z
|
|
9
|
+
.string()
|
|
10
|
+
.regex(/^vw_[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/, 'Expected a view id ("vw_" + snake_case)')
|
|
11
|
+
.describe('The id of a declared read. A screen names one; it never states what the read does.')
|
|
12
|
+
|
|
13
|
+
export const ScreenName = ElementName.describe(
|
|
14
|
+
'The screen’s export name — `Home`, `Pricing`, `SignIn`. What `navigateTo` names, and its whole address.',
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
export const ScreenSchema = z
|
|
18
|
+
.strictObject({
|
|
19
|
+
id: ScreenName,
|
|
20
|
+
nameKey: z
|
|
21
|
+
.string()
|
|
22
|
+
.min(1)
|
|
23
|
+
.describe('The i18n key naming this screen — in a breadcrumb, a tab, a title. Never prose (rule: every user-facing string is a key).'),
|
|
24
|
+
|
|
25
|
+
path: z
|
|
26
|
+
.string()
|
|
27
|
+
.regex(/^\/(?:[a-z0-9-]+|\[[a-z][a-zA-Z0-9]*\])(?:\/(?:[a-z0-9-]+|\[[a-z][a-zA-Z0-9]*\]))*$|^\/$/, 'must be an App Router path, e.g. "/campaign/[id]"')
|
|
28
|
+
.optional()
|
|
29
|
+
.describe('Where this screen lives. `/`, `/settings`, `/campaign/[id]`. Absent for a sheet, popover, overlay or toast, which a press opens where somebody already is, so it has no address of its own.'),
|
|
30
|
+
|
|
31
|
+
entry: z.boolean().default(false).describe('True when somebody can arrive here without following a link from inside the app.'),
|
|
32
|
+
|
|
33
|
+
shows: z.array(ViewId).default([]).describe('Declared views this screen binds to. A screen binds; it never fetches.'),
|
|
34
|
+
})
|
|
35
|
+
.describe('One screen: where it lives, what it rests on, and whether somebody can arrive at it directly.')
|
|
36
|
+
export type Screen = z.infer<typeof ScreenSchema>
|
|
37
|
+
|
|
38
|
+
export const LinkSchema = z
|
|
39
|
+
.strictObject({
|
|
40
|
+
from: ScreenName.describe('The screen somebody is on.'),
|
|
41
|
+
to: ScreenName.describe('The screen they arrive at.'),
|
|
42
|
+
labelKey: z
|
|
43
|
+
.string()
|
|
44
|
+
.min(1)
|
|
45
|
+
.optional()
|
|
46
|
+
.describe('The i18n key of the button or link somebody presses to get there. Read off the press itself, and absent only when the press carries no wording.'),
|
|
47
|
+
})
|
|
48
|
+
.describe('One edge: somebody on `from` can reach `to`.')
|
|
49
|
+
export type Link = z.infer<typeof LinkSchema>
|
|
50
|
+
|
|
51
|
+
export const MapSchema = z
|
|
52
|
+
.strictObject({
|
|
53
|
+
specVersion: z.literal(1),
|
|
54
|
+
screens: z.array(ScreenSchema).default([]),
|
|
55
|
+
links: z.array(LinkSchema).default([]),
|
|
56
|
+
})
|
|
57
|
+
.describe('The map: the screens an app has and how somebody moves between them.')
|
|
58
|
+
export type MapIsland = z.infer<typeof MapSchema>
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Threaded } from '@prestack/thread/core/thread'
|
|
2
|
+
|
|
3
|
+
type Row = Omit<Threaded, 'at'> & {
|
|
4
|
+
readonly section?: string
|
|
5
|
+
readonly is?: string
|
|
6
|
+
readonly sets?: Readonly<Record<string, string>>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function fileFor(language: string): string {
|
|
10
|
+
return `i18n/${language}.json`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const SOURCE_ROW = 'source'
|
|
14
|
+
|
|
15
|
+
export const holds: readonly Row[] = [
|
|
16
|
+
{
|
|
17
|
+
id: SOURCE_ROW,
|
|
18
|
+
section: 'the source',
|
|
19
|
+
is: 'en',
|
|
20
|
+
describes:
|
|
21
|
+
'Which language this app is written in. One name, and it has to be one of the languages declared below — everything else is a translation of it, and the wording commands ask for those and never for this one twice.',
|
|
22
|
+
version: 1,
|
|
23
|
+
},
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
export const declares: readonly (Row & { readonly at: string })[] = [
|
|
27
|
+
{
|
|
28
|
+
id: 'en',
|
|
29
|
+
at: fileFor('en'),
|
|
30
|
+
section: 'languages',
|
|
31
|
+
is: 'English',
|
|
32
|
+
sets: { flag: '🇬🇧' },
|
|
33
|
+
describes:
|
|
34
|
+
'English, and everything this app says in it. Named in its own language, because the list this feeds is read by somebody who cannot yet read the language the app is currently in.',
|
|
35
|
+
version: 1,
|
|
36
|
+
},
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
export const LOCALIZATIONS: Threaded & {
|
|
40
|
+
readonly files: string
|
|
41
|
+
readonly holds: readonly Row[]
|
|
42
|
+
readonly declares: readonly (Row & { readonly at: string })[]
|
|
43
|
+
} = {
|
|
44
|
+
id: 'localization',
|
|
45
|
+
at: 'thread/ui/localizations.ts',
|
|
46
|
+
files: fileFor('*'),
|
|
47
|
+
describes:
|
|
48
|
+
'Every language an app is in, and which one it is written in. A row is a language tag, the language’s name in itself and a flag to show beside it, so a settings screen offering a choice is built from declarations rather than from a list somebody keeps in a component. Adding a row is what brings a wording file for that language into existence, and every key the interface names has to be answered in every language declared here — an unanswered one is a build error rather than a word that silently comes out in the wrong language.',
|
|
49
|
+
version: 1,
|
|
50
|
+
cites: [
|
|
51
|
+
{
|
|
52
|
+
on: '2026-08-29',
|
|
53
|
+
said: 'i18n ships by default, nothing hardcoded in the UI at all. i18n is REQUIRED. We define the languages the app will have, this generates automatically the i18n files so that we can add the stuff there.',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
on: '2026-08-29',
|
|
57
|
+
said: 'This means that we need a new Localization.ts file under UI, where we declare the languages the app is in, must be threaded on attach. This might maybe include an emoji for the country flag? And name of the language so we can reuse this in settings later',
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
holds,
|
|
61
|
+
declares,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function languagesIn(
|
|
65
|
+
rows: readonly { readonly id: string; readonly is?: string; readonly sets?: Readonly<Record<string, string>> }[],
|
|
66
|
+
): readonly { readonly id: string; readonly name: string; readonly flag: string }[] {
|
|
67
|
+
return rows
|
|
68
|
+
.filter((row) => row.id !== SOURCE_ROW)
|
|
69
|
+
.map((row) => ({ id: row.id, name: row.is ?? row.id, flag: row.sets?.flag ?? '' }))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function sourceIn(rows: readonly { readonly id: string; readonly is?: string }[]): string | undefined {
|
|
73
|
+
return rows.find((row) => row.id === SOURCE_ROW)?.is
|
|
74
|
+
}
|
|
75
|
+
|