@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,84 +0,0 @@
1
- import { useId, useRef, useState } from 'react'
2
-
3
- import { ITEM_ATTR, useRovingFocus } from '../roving-focus.ts'
4
- import type { ReactNode, RefObject } from 'react'
5
-
6
- import type { Row } from '../charts.tsx'
7
- import type { Surface } from '../door.ts'
8
- import type { TabsProps } from './tabs.tsx'
9
-
10
- type Kept = {
11
- readonly list: RefObject<HTMLDivElement | null>
12
- readonly base: string
13
- readonly open: string | undefined
14
- readonly choose: (id: string) => void
15
- }
16
-
17
- export function uses(props: TabsProps): {
18
- readonly holds?: readonly string[]
19
- readonly animates?: boolean
20
- readonly on?: Readonly<Record<string, unknown>>
21
- readonly list: RefObject<HTMLDivElement | null>
22
- readonly base: string
23
- readonly open: string | undefined
24
- readonly choose: (id: string) => void
25
- } {
26
- const list = useRef<HTMLDivElement>(null)
27
- const base = useId()
28
- const first = props.items.find((one) => one.disabled !== true)?.id
29
- const [active, setActive] = useState<string | undefined>(props.initial ?? first)
30
- useRovingFocus(list, { orientation: props.orientation === 'vertical' ? 'vertical' : 'horizontal' })
31
-
32
- const open = props.items.find((one) => one.id === active) ?? props.items.find((one) => one.disabled !== true)
33
- return {
34
- list,
35
- base,
36
- open: open?.id,
37
- choose: (id: string): void => {
38
- setActive(id)
39
- props.onChange?.(id)
40
- },
41
- }
42
- }
43
-
44
- export function strip(props: TabsProps, kept: Kept): Readonly<Record<string, unknown>> {
45
- return { ref: kept.list, role: 'tablist', 'aria-orientation': props.orientation === 'vertical' ? 'vertical' : 'horizontal' }
46
- }
47
-
48
- export function tab(_props: TabsProps, kept: Kept, row: Row, _at: number): Readonly<Record<string, unknown>> {
49
- const id = String(row['id'])
50
- return {
51
- ...{ [ITEM_ATTR]: '' },
52
- type: 'button',
53
- role: 'tab',
54
- id: `${kept.base}-tab-${id}`,
55
- 'aria-controls': `${kept.base}-panel-${id}`,
56
- 'aria-selected': id === kept.open,
57
- disabled: row['disabled'] === true,
58
- onClick: (): void => kept.choose(id),
59
- }
60
- }
61
-
62
- export function opened(_props: TabsProps, kept: Kept): boolean {
63
- return kept.open !== undefined
64
- }
65
-
66
- export function panel(_props: TabsProps, kept: Kept): Readonly<Record<string, unknown>> {
67
- return {
68
- role: 'tabpanel',
69
- id: `${kept.base}-panel-${String(kept.open)}`,
70
- 'aria-labelledby': `${kept.base}-tab-${String(kept.open)}`,
71
- }
72
- }
73
-
74
- export function shown(
75
- props: TabsProps,
76
- ...rest: readonly [Surface, Kept] | readonly [Row, Surface, Kept]
77
- ): Readonly<Record<string, ReactNode>> {
78
- if (rest.length === 3) {
79
- const [row, surface] = rest
80
- return { label: surface.text(String(row['labelKey'])) }
81
- }
82
- const [, kept] = rest
83
- return { inside: props.items.find((one) => one.id === kept.open)?.content ?? null }
84
- }
@@ -1,33 +0,0 @@
1
- 'use client'
2
-
3
- import type { MessageKey } from '../content.tsx'
4
- import { roleClass, type FontRoleId } from '../fonts.ts'
5
-
6
- import type { Dispatching } from '../checked.ts'
7
- import { type Element, element, useSurface } from '../wrappers.tsx'
8
-
9
-
10
- export interface TextProps {
11
- readonly k: MessageKey
12
- readonly with?: Readonly<Record<string, unknown>>
13
- readonly is?: FontRoleId
14
- readonly announce?: 'alert' | 'status'
15
- readonly dispatch?: keyof Dispatching
16
- readonly input?: Readonly<Record<string, unknown>>
17
- readonly invokeOn?: 'press' | 'shown'
18
- }
19
-
20
- export const Text: Element<'Text', TextProps> = element(
21
- 'Text',
22
- (props: TextProps, className) => {
23
- const surface = useSurface({ className: roleClass(props.is, className),
24
- invokes: { ...(props.dispatch === undefined ? {} : { dispatch: props.dispatch }), ...(props.input === undefined ? {} : { input: props.input }), ...(props.invokeOn === undefined ? {} : { on: props.invokeOn }) }, })
25
- return (
26
- <span className={surface.className} {...surface.marks} {...surface.invokes} {...surface.a11y} role={props.announce}>
27
- {surface.text(props.k, props.with)}
28
- </span>
29
- )
30
- },
31
- 'e-text',
32
- {},
33
- )
@@ -1,38 +0,0 @@
1
- 'use client'
2
-
3
- import type { MessageKey } from '../content.tsx'
4
- import { roleClass } from '../fonts.ts'
5
-
6
- import type { Dispatching } from '../checked.ts'
7
- import { type Element, element, useSurface } from '../wrappers.tsx'
8
- import { limited } from '../validators.ts'
9
- import { does } from './textarea.works.ts'
10
-
11
-
12
- export interface TextareaProps {
13
- readonly labelKey: MessageKey
14
- readonly name?: string
15
- readonly value?: string
16
- readonly onValue?: (said: string) => void
17
- readonly validator?: string
18
- readonly rows?: number
19
- readonly dispatch?: keyof Dispatching
20
- readonly input?: Readonly<Record<string, unknown>>
21
- readonly invokeOn?: 'press' | 'shown'
22
- }
23
-
24
- export const Textarea: Element<'Textarea', TextareaProps> = element(
25
- 'Textarea',
26
- (props: TextareaProps, className) => {
27
- const surface = useSurface({ className: roleClass(undefined, className),
28
- invokes: { ...(props.dispatch === undefined ? {} : { dispatch: props.dispatch }), ...(props.input === undefined ? {} : { input: props.input }), ...(props.invokeOn === undefined ? {} : { on: props.invokeOn }) }, })
29
- return (
30
- <label className={surface.className} {...surface.marks} {...surface.invokes} {...surface.a11y}>
31
- <span>{surface.text(props.labelKey)}</span>
32
- {props.value === undefined ? null : <textarea name={props.name} rows={props.rows ?? 4} {...limited(props.validator)} disabled={surface.marked('disabled')} value={props.value ?? ''} onChange={(event) => does(props).wrote(event.target.value)}></textarea>}
33
- </label>
34
- )
35
- },
36
- 'e-textarea',
37
- {"input":"thread-input"},
38
- )
@@ -1,5 +0,0 @@
1
- import type { TextareaProps } from './textarea.tsx'
2
-
3
- export function does(props: TextareaProps): { readonly wrote: (said: string) => void } {
4
- return { wrote: (said: string) => props.onValue?.(said) }
5
- }
@@ -1,40 +0,0 @@
1
- 'use client'
2
-
3
- import { roleClass, type FontRoleId } from '../fonts.ts'
4
-
5
- import type { Dispatching } from '../checked.ts'
6
- import { type Element, element, useSurface } from '../wrappers.tsx'
7
- import { shows, uses } from './time.works.ts'
8
-
9
-
10
- export interface TimeProps {
11
- readonly value?: unknown
12
- readonly relative?: boolean
13
- readonly is?: FontRoleId
14
- readonly invalid?: boolean
15
- readonly dispatch?: keyof Dispatching
16
- readonly input?: Readonly<Record<string, unknown>>
17
- readonly invokeOn?: 'press' | 'shown'
18
- }
19
-
20
- export const Time: Element<'Time', TimeProps> = element(
21
- 'Time',
22
- (props: TimeProps, className) => {
23
- const kept = uses(props)
24
- const surface = useSurface({ className: roleClass(props.is, className),
25
- invokes: { ...(props.dispatch === undefined ? {} : { dispatch: props.dispatch }), ...(props.input === undefined ? {} : { input: props.input }), ...(props.invokeOn === undefined ? {} : { on: props.invokeOn }) },
26
- holds: [
27
- ...(props.invalid === true ? ['invalid' as const] : []),
28
- ...(kept.holds ?? []),
29
- ],
30
- raises: ['invalid' as const],
31
- ...(kept.animates === undefined ? {} : { animates: kept.animates }), })
32
- return (
33
- <span className={surface.className} {...surface.marks} {...surface.invokes} {...surface.a11y} {...(kept.on ?? {})}>
34
- {shows(props, surface, kept)}
35
- </span>
36
- )
37
- },
38
- 'e-time',
39
- {},
40
- )
@@ -1,29 +0,0 @@
1
- import { useCallback, useSyncExternalStore, type ReactNode } from 'react'
2
-
3
- import { renderValue } from '../content.tsx'
4
- import { runValidator } from '../validators.ts'
5
- import type { Surface } from '../door.ts'
6
- import { useUi } from '../wrappers.tsx'
7
- import type { TimeProps } from './time.tsx'
8
-
9
- const NO_CLOCK_YET = (): number | undefined => undefined
10
-
11
- export function uses(props: TimeProps): { readonly holds?: readonly string[]; readonly animates?: boolean; readonly on?: Readonly<Record<string, unknown>>; readonly at: number | undefined } {
12
- const { now, ticker } = useUi()
13
- const subscribe = useCallback(
14
- (onChange: () => void) => (props.relative === true && ticker !== undefined ? ticker.subscribe(onChange) : () => undefined),
15
- [props.relative, ticker],
16
- )
17
- const read = useCallback(() => now?.(), [now])
18
- const at = useSyncExternalStore(subscribe, read, NO_CLOCK_YET)
19
- const { relative, value } = props
20
- const { ok } = runValidator(relative === true ? 'relativeTime' : 'datetime', value, relative === true ? { now: at } : {})
21
- return ok ? { at } : { at, holds: ['invalid'] }
22
- }
23
-
24
- export function shows(props: TimeProps, surface: Surface, kept: { readonly at: number | undefined }): ReactNode {
25
- if (props.relative !== true || kept.at === undefined) {
26
- return renderValue({ validator: 'datetime', value: props.value }, surface)
27
- }
28
- return renderValue({ validator: 'relativeTime', value: props.value, context: { now: kept.at } }, surface)
29
- }
@@ -1,34 +0,0 @@
1
- 'use client'
2
-
3
- import type { MessageKey } from '../content.tsx'
4
- import { roleClass } from '../fonts.ts'
5
-
6
- import type { Dispatching } from '../checked.ts'
7
- import { type Element, element, useSurface } from '../wrappers.tsx'
8
- import { does } from './toggle.works.ts'
9
-
10
-
11
- export interface ToggleProps {
12
- readonly labelKey: MessageKey
13
- readonly on?: boolean
14
- readonly onToggle?: (said: boolean) => void
15
- readonly dispatch?: keyof Dispatching
16
- readonly input?: Readonly<Record<string, unknown>>
17
- readonly invokeOn?: 'press' | 'shown'
18
- }
19
-
20
- export const Toggle: Element<'Toggle', ToggleProps> = element(
21
- 'Toggle',
22
- (props: ToggleProps, className) => {
23
- const surface = useSurface({ className: roleClass(undefined, className),
24
- invokes: { ...(props.dispatch === undefined ? {} : { dispatch: props.dispatch }), ...(props.input === undefined ? {} : { input: props.input }), ...(props.invokeOn === undefined ? {} : { on: props.invokeOn }) }, })
25
- return (
26
- <label className={surface.className} {...surface.marks} {...surface.invokes} {...surface.a11y}>
27
- {props.on === undefined ? null : <input type="checkbox" disabled={surface.marked('disabled')} checked={props.on === true} onChange={(event) => does(props).changed(event.target.checked)} />}
28
- <span>{surface.text(props.labelKey)}</span>
29
- </label>
30
- )
31
- },
32
- 'e-toggle',
33
- {"input":"thread-input"},
34
- )
@@ -1,5 +0,0 @@
1
- import type { ToggleProps } from './toggle.tsx'
2
-
3
- export function does(props: ToggleProps): { readonly changed: (on: boolean) => void } {
4
- return { changed: (on: boolean) => props.onToggle?.(on) }
5
- }
@@ -1,40 +0,0 @@
1
- 'use client'
2
-
3
- import type { ReactNode } from 'react'
4
- import type { MessageKey } from '../content.tsx'
5
-
6
- import type { Dispatching } from '../checked.ts'
7
- import { type Element, element, useSurface } from '../wrappers.tsx'
8
- import { measured, bubble, showing, uses } from './tooltip.works.ts'
9
-
10
-
11
- export interface TooltipProps {
12
- readonly children: ReactNode
13
- readonly labelKey: MessageKey
14
- readonly side?: string
15
- readonly dispatch?: keyof Dispatching
16
- readonly input?: Readonly<Record<string, unknown>>
17
- readonly invokeOn?: 'press' | 'shown'
18
- }
19
-
20
- export const Tooltip: Element<'Tooltip', TooltipProps> = element(
21
- 'Tooltip',
22
- (props: TooltipProps, className) => {
23
- const kept = uses(props)
24
- const surface = useSurface({ className,
25
- invokes: { ...(props.dispatch === undefined ? {} : { dispatch: props.dispatch }), ...(props.input === undefined ? {} : { input: props.input }), ...(props.invokeOn === undefined ? {} : { on: props.invokeOn }) },
26
- holds: [
27
- ...(kept.holds ?? []),
28
- ],
29
- measures: measured(props, kept),
30
- ...(kept.animates === undefined ? {} : { animates: kept.animates }), })
31
- return (
32
- <span {...surface.marks} {...surface.invokes} {...surface.a11y} {...(kept.on ?? {})} style={surface.measures}>
33
- {props.children}
34
- {!showing(props, kept) ? null : <span className={`e-tooltip-bubble ${surface.className ?? ''}`} {...bubble(props, kept)}>{surface.text(props.labelKey)}</span>}
35
- </span>
36
- )
37
- },
38
- 'e-tooltip',
39
- {"card":"thread-card"},
40
- )
@@ -1,77 +0,0 @@
1
- import { useEffect, useRef, useState } from 'react'
2
-
3
- import { useDismissLayer } from '../dismiss-layer.ts'
4
- import { positionOf, type AnchorOptions } from '../positioning.ts'
5
- import type { TooltipProps } from './tooltip.tsx'
6
-
7
- type Kept = {
8
- readonly trigger: React.RefObject<HTMLSpanElement | null>
9
- readonly layer: React.RefObject<HTMLSpanElement | null>
10
- readonly open: boolean
11
- readonly at: { readonly x: number; readonly y: number } | null
12
- readonly show: (to: boolean) => void
13
- }
14
-
15
- export function uses(props: TooltipProps): {
16
- readonly holds?: readonly string[]
17
- readonly animates?: boolean
18
- readonly on: Readonly<Record<string, unknown>>
19
- readonly trigger: React.RefObject<HTMLSpanElement | null>
20
- readonly layer: React.RefObject<HTMLSpanElement | null>
21
- readonly open: boolean
22
- readonly at: { readonly x: number; readonly y: number } | null
23
- readonly show: (to: boolean) => void
24
- } {
25
- const trigger = useRef<HTMLSpanElement>(null)
26
- const layer = useRef<HTMLSpanElement>(null)
27
- const [open, setOpen] = useState(false)
28
- const [at, setAt] = useState<{ x: number; y: number } | null>(null)
29
-
30
- useDismissLayer(layer, {
31
- active: open,
32
- dismissOnEscape: true,
33
- dismissOnOutside: true,
34
- onDismiss: () => setOpen(false),
35
- })
36
-
37
- useEffect(() => {
38
- if (!open) return undefined
39
- const anchor = trigger.current
40
- const floating = layer.current
41
- if (anchor === null || floating === null) return undefined
42
- let live = true
43
- void positionOf(anchor, floating, { placement: (props.side ?? 'top') as NonNullable<AnchorOptions['placement']> }).then((position) => {
44
- if (live) setAt({ x: position.x, y: position.y })
45
- })
46
- return () => {
47
- live = false
48
- }
49
- }, [open, props.side])
50
-
51
- return {
52
- on: {
53
- ref: trigger,
54
- onPointerEnter: () => setOpen(true),
55
- onPointerLeave: () => setOpen(false),
56
- onFocus: () => setOpen(true),
57
- onBlur: () => setOpen(false),
58
- },
59
- trigger,
60
- layer,
61
- open,
62
- at,
63
- show: setOpen,
64
- }
65
- }
66
-
67
- export function showing(_props: TooltipProps, kept: Kept): boolean {
68
- return kept.open
69
- }
70
-
71
- export function bubble(props: TooltipProps, kept: Kept): Readonly<Record<string, unknown>> {
72
- return { ref: kept.layer, role: 'tooltip', 'data-side': props.side ?? 'top' }
73
- }
74
-
75
- export function measured(_props: TooltipProps, kept: Kept): Readonly<Record<string, string | number>> {
76
- return kept.at === null ? {} : { x: `${String(kept.at.x)}px`, y: `${String(kept.at.y)}px` }
77
- }
@@ -1,38 +0,0 @@
1
- 'use client'
2
-
3
- import { roleClass, type FontRoleId } from '../fonts.ts'
4
-
5
- import type { Dispatching } from '../checked.ts'
6
- import { type Element, element, useSurface } from '../wrappers.tsx'
7
- import { shows } from './value.works.ts'
8
-
9
-
10
- export interface ValueProps {
11
- readonly validator: string
12
- readonly value?: unknown
13
- readonly context?: Readonly<Record<string, unknown>>
14
- readonly is?: FontRoleId
15
- readonly invalid?: boolean
16
- readonly dispatch?: keyof Dispatching
17
- readonly input?: Readonly<Record<string, unknown>>
18
- readonly invokeOn?: 'press' | 'shown'
19
- }
20
-
21
- export const Value: Element<'Value', ValueProps> = element(
22
- 'Value',
23
- (props: ValueProps, className) => {
24
- const surface = useSurface({ className: roleClass(props.is, className),
25
- invokes: { ...(props.dispatch === undefined ? {} : { dispatch: props.dispatch }), ...(props.input === undefined ? {} : { input: props.input }), ...(props.invokeOn === undefined ? {} : { on: props.invokeOn }) },
26
- holds: [
27
- ...(props.invalid === true ? ['invalid' as const] : []),
28
- ],
29
- raises: ['invalid' as const], })
30
- return (
31
- <span className={surface.className} {...surface.marks} {...surface.invokes} {...surface.a11y}>
32
- {shows(props, surface)}
33
- </span>
34
- )
35
- },
36
- 'e-value',
37
- {},
38
- )
@@ -1,9 +0,0 @@
1
- import type { ReactNode } from 'react'
2
-
3
- import { renderValue } from '../content.tsx'
4
- import type { Surface } from '../door.ts'
5
- import type { ValueProps } from './value.tsx'
6
-
7
- export function shows(props: ValueProps, surface: Surface): ReactNode {
8
- return renderValue({ validator: props.validator, value: props.value, ...(props.context === undefined ? {} : { context: props.context }) }, surface)
9
- }
@@ -1,127 +0,0 @@
1
- import type { Threaded } from '@prestack/thread/core/thread'
2
-
3
- import type { Wearing } from './token.ts'
4
-
5
- interface Arranging extends Threaded {
6
- readonly arranges?: boolean
7
- readonly sets?: Readonly<Record<string, string>>
8
-
9
- readonly tag?: string | Readonly<Record<string, string>>
10
- readonly raises?: readonly string[]
11
-
12
- readonly carries?: Readonly<Record<string, string>>
13
-
14
- readonly always?: readonly string[]
15
-
16
- readonly places?: Readonly<Record<string, Readonly<Record<string, string>>>>
17
-
18
- readonly works?: string
19
-
20
- readonly measures?: readonly string[]
21
-
22
- readonly reads?: string
23
-
24
- readonly gives?: readonly string[]
25
-
26
- readonly keeps?: boolean
27
-
28
- readonly animates?: boolean
29
-
30
- readonly shows?: string
31
-
32
- readonly called?: string
33
-
34
- readonly wires?: string
35
-
36
- readonly dresses?: string
37
- }
38
-
39
- interface Catalogue extends Threaded {
40
- readonly files: string
41
- readonly derives?: readonly Arranging[]
42
- }
43
-
44
- const arranging = <const T extends readonly Arranging[]>(
45
- rows: T & {
46
- readonly [I in keyof T]: (T[I] extends { readonly sets: infer Said }
47
- ? { readonly sets: { readonly [P in keyof Said]: Wearing<Said[P] & string> } }
48
- : unknown) &
49
- (T[I] extends { readonly places: infer Inside }
50
- ? {
51
- readonly places: {
52
- readonly [R in keyof Inside]: {
53
- readonly [P in keyof Inside[R]]: Wearing<Inside[R][P] & string>
54
- }
55
- }
56
- }
57
- : unknown)
58
- },
59
- ): T => rows
60
-
61
- export const ELEMENTS = {
62
- id: 'element',
63
- at: 'thread/ui/elements.ts',
64
- files: 'ui/*.tsx',
65
- describes: 'An interface part, and function only: what it does, what it carries and which signals it raises, never how it looks and never markup a person writes. Its look is a variant it wears, its wording arrives as a message key rather than as words, and `thread elements` writes its file from this row, so the file is never typed. Every part this framework ships is listed here, an app adds its own to the same list, and a row that goes missing breaks the build, because what the framework offers is not an app’s to withdraw. A thing made of parts already listed here is a composition, not a new element.',
66
- version: 1,
67
- cites: [
68
- {
69
- on: '2026-08-31',
70
- said: 'elements does not hold anything that can render anything, its functionality only . only our compiler can translate things into proper css and html and only at build time do we produce a renderable result.',
71
- },
72
- {
73
- on: '2026-08-29',
74
- said: 'CLI actually generates the tsx file, and you MUST give the file its body, properly.',
75
- },
76
- ],
77
- derives: arranging([
78
- { id: 'accordion', at: 'thread/ui/elements/accordion.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'div', keeps: true, works: './accordion.works.ts', carries: { items: 'parting', initial: 'ordering', exclusive: 'flag', onChange: 'choosing' }, places: { section: { tag: 'div', each: 'items', by: 'id', gives: 'section', holds: 'header region' }, header: { tag: 'button', gives: 'header', shows: 'label' }, region: { tag: 'div', gives: 'region', shows: 'inside' } }, describes: 'Sections that open one at a time, for long material somebody reads part of.' },
79
- { id: 'aspect-ratio', at: 'thread/ui/elements/aspect-ratio.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'div', arranges: true, carries: { ratio: 'count' }, measures: ['ratio'], works: './aspect-ratio.works.ts', sets: { aspectRatio: 'ratio' }, describes: 'A slot that keeps its proportions whatever width it gets, so a loading picture reserves its room.' },
80
- { id: 'avatar', at: 'thread/ui/elements/avatar.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'span', reads: 'picture', carries: { labelKey: 'naming', src: 'picture', initials: 'words' }, works: './avatar.works.ts', places: { picture: { tag: 'img', shows: 'src' }, lettering: { shows: 'lettering' } }, describes: 'A person or an organisation as a small picture, falling back to initials when there is no image.' },
81
- { id: 'badge', at: 'thread/ui/elements/badge.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'span', carries: { labelKey: 'wording', count: 'count', max: 'count' }, works: './badge.works.ts', describes: 'A short status word attached to something else. For counts and states, never for an action.' },
82
- { id: 'bars', at: 'thread/ui/elements/bars.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'div', reads: 'picture', keeps: true, works: './bars.works.ts', carries: { rows: 'listing', by: 'ruling', value: 'ruling', format: 'words', titleKey: 'titling' }, places: { bar: { each: 'rows', by: 'by', marks: 'sized', holds: 'label amount' }, label: { shows: 'label' }, amount: { shows: 'amount' } }, describes: 'Quantities compared across categories. The default when the horizontal axis is not time.' },
83
- { id: 'box', at: 'thread/ui/elements/box.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'div', arranges: true, reads: 'grouping', raises: ['scrolling'], carries: { labelKey: 'titling' }, describes: 'The declared `div`. Groups children, optionally as a landmark region, and optionally scrolls. A box that scrolls raises `scrolling`, which a variant may dress; naming one makes it a group, and leaving it unnamed leaves it a box.' },
84
- { id: 'breadcrumb', at: 'thread/ui/elements/breadcrumb.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'nav', called: 'nav.breadcrumb', works: './breadcrumb.works.ts', carries: { map: 'mapping', current: 'ruling' }, places: { step: { tag: 'Link.linkInline', each: 'steps', by: 'id', gives: 'crumb' } }, describes: 'The trail back up from wherever somebody is, derived from the route rather than assembled by hand.' },
85
- { id: 'button', at: 'thread/ui/elements/button.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'button', keeps: true, works: './button.works.tsx', wires: 'wires', carries: { label: 'wording', with: 'holding', icon: 'words', shows: 'words', type: 'words', disabled: 'flag', loading: 'flag', input: 'holding', navigateTo: 'words', onPress: 'doing' }, places: { symbol: { when: 'iconed', shows: 'symbol', display: 'layout-flex', alignItems: 'align-centre' }, wording: { when: 'worded', shows: 'wording' } }, describes: 'A press that runs a declared workflow or goes to a declared screen. The only way to start either from a tap.' },
86
- { id: 'column', at: 'thread/ui/elements/column.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'div', describes: 'Children stacked top to bottom. The vertical arrangement, and the default shape of a form.', arranges: true, sets: { display: 'layout-flex', flexDirection: 'flow-down', gap: 'space-3' } },
87
- { id: 'consent', at: 'thread/ui/elements/consent.tsx', by: 'thread elements', runs: 'thread elements --own --check', keeps: true, works: './consent.works.tsx', carries: { decision: 'judging', onDecide: 'judged', policyKey: 'noting', preselected: 'ordering' }, places: { panel: { tag: 'Box.card', holds: 'body' }, body: { tag: 'Column', holds: 'title policy list actions' }, title: { tag: 'Text', gives: 'title' }, policy: { tag: 'Text', when: 'hasPolicy', gives: 'policy' }, list: { tag: 'Column', holds: 'category' }, category: { tag: 'Box', each: 'categories', by: 'id', holds: 'words switch fixed' }, words: { tag: 'Column', holds: 'label describe' }, label: { tag: 'Text', gives: 'label' }, describe: { tag: 'Text', gives: 'describe' }, switch: { tag: 'Toggle.input', when: 'optional', gives: 'chooser' }, fixed: { tag: 'Text', when: 'required', gives: 'fixed' }, actions: { tag: 'Box', holds: 'withdraw decline accept accept-all' }, withdraw: { tag: 'Button.buttonQuiet', when: 'withdrawable', gives: 'withdraw' }, decline: { tag: 'Button.buttonBorder', gives: 'decline' }, accept: { tag: 'Button.buttonBorder', gives: 'accept' }, 'accept-all': { tag: 'Button.buttonFill', gives: 'acceptAll' } }, describes: 'The complete permission-to-collect surface, with every declared category and its decision.' },
88
- { id: 'donut', at: 'thread/ui/elements/donut.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'svg', reads: 'picture', keeps: true, works: './donut.works.ts', carries: { rows: 'listing', by: 'ruling', value: 'ruling', titleKey: 'titling' }, places: { slice: { tag: 'circle', each: 'rows', by: 'by', gives: 'slice' } }, describes: 'Parts of one whole, for a handful of slices. Beyond that the reading is worse than a bar.' },
89
- { id: 'each', at: 'thread/ui/elements/each.tsx', by: 'thread elements', runs: 'thread elements --own --check', works: './each.works.tsx', carries: { rows: 'anything', by: 'ruling', children: 'drawing', settles: 'words', paced: 'words' }, describes: 'One rendered thing per row of data. The construct that replaces mapping over an array by hand.' },
90
- { id: 'empty', at: 'thread/ui/elements/empty.tsx', by: 'thread elements', runs: 'thread elements --own --check', always: ['empty'], tag: 'div', describes: 'What a surface says when its read answered with nothing. Never the same rendering as still loading.' },
91
- { id: 'field', at: 'thread/ui/elements/field.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'label', keeps: true, works: './field.works.ts', carries: { name: 'ruling', validator: 'ruling', labelKey: 'wording', value: 'words', onValue: 'picking', context: 'holding', autocomplete: 'words', disabled: 'flag' }, places: { caption: { shows: 'labelKey' }, entry: { tag: 'input', gives: 'entry' }, fault: { reads: 'alarm', shows: 'fault' } }, describes: 'One value being typed, governed entirely by the validator it names — keyboard, mask, limit and message.' },
92
- { id: 'funnel', at: 'thread/ui/elements/funnel.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'div', reads: 'picture', keeps: true, works: './funnel.works.ts', carries: { rows: 'listing', by: 'ruling', value: 'ruling', format: 'words', titleKey: 'titling' }, places: { stage: { each: 'rows', by: 'by', marks: 'sized', holds: 'label amount' }, label: { shows: 'label' }, amount: { shows: 'amount' } }, describes: 'Fall-off between ordered stages, with the drop at each step derived rather than supplied.' },
93
- { id: 'gauge', at: 'thread/ui/elements/gauge.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'svg', reads: 'picture', keeps: true, works: './gauge.works.ts', carries: { value: 'count', min: 'count', max: 'count', labelKey: 'naming' }, places: { track: { tag: 'path', gives: 'track' }, needle: { tag: 'path', gives: 'needle' } }, describes: 'A single quantity against a target, where the reading is how far along rather than how much.' },
94
- { id: 'grid', at: 'thread/ui/elements/grid.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'div', describes: 'Children arranged, and nothing more. Its tracks are not something a row can state — the style guide binds no track property, deliberately — so this sets nothing and a caller that wants a set of equal panels wraps a `Row` instead. Kept, and its sentence made true, because the one it carried promised tracks it has never laid out: “as many equal tracks as the container allows”, on a row with no `sets` at all.', arranges: true },
95
- { id: 'heatmap', at: 'thread/ui/elements/heatmap.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'div', reads: 'picture', keeps: true, works: './heatmap.works.ts', carries: { rows: 'listing', by: 'ruling', x: 'ruling', y: 'ruling', value: 'words', titleKey: 'titling' }, places: { cell: { each: 'rows', by: 'by', marks: 'sized' } }, describes: 'Density across two axes, for when the pattern matters more than any single cell.' },
96
- { id: 'hit-box', at: 'thread/ui/elements/hit-box.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'div', arranges: true, keeps: true, works: './hit-box.works.tsx', wires: 'wires', carries: { gesture: 'ruling', tuning: 'tuning', axis: 'words', grabs: 'subjecting', accepts: 'ordering', collects: 'subjecting', dwell: 'count', moves: 'flag', onDrop: 'doing', input: 'holding' }, sets: { display: 'layout-block', touchAction: 'touch', userSelect: 'pick', visibility: 'shown', position: 'flow' }, measures: ['touch', 'pick', 'shown', 'flow', 'width', 'height', 'carry', 'grip'], places: { carried: { tag: 'div', shows: 'carried', position: 'position-pinned', left: 'space-0', top: 'space-0', width: 'width', height: 'height', transform: 'carry', transformOrigin: 'grip', pointerEvents: 'reach-through', userSelect: 'picking-off', zIndex: 'layer-carried', willChange: 'readying-movement' } }, describes: 'A region that claims a gesture, composed with anything and declared by nothing. Put it where the grab should be, not on the element.' },
97
- { id: 'glyph', at: 'thread/ui/elements/glyph.tsx', by: 'thread elements', runs: 'thread elements --own --check', carries: { name: 'ruling', labelKey: 'noting' }, works: './glyph.works.tsx', describes: 'A semantic pictogram — `close`, `search` — resolved from whichever set the design chose at build time. It is laid out rather than left inline, because an `svg` that is left inline stands on a text baseline: the line it sits on keeps a descender\'s worth of room under the drawing, so the drawing rides above the middle of whatever holds it and the wording beside it reads as sitting lower. Measured 2026-09-03 in `thread-palette-lab`: a 24px symbol in a 28px box, its centre 2.00px above the row\'s and the wording\'s 0.36px below — 2.36px apart, the same amount everywhere, on every icon in the app.', sets: { display: 'layout-flex' } },
98
- { id: 'image', at: 'thread/ui/elements/image.tsx', by: 'thread elements', runs: 'thread elements --own --check', carries: { src: 'source', altKey: 'naming' }, tag: 'img', describes: 'A picture with alternative wording, for photographs and artwork rather than for interface symbols.' },
99
- { id: 'line-chart', at: 'thread/ui/elements/line-chart.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'svg', reads: 'picture', keeps: true, works: './line-chart.works.ts', carries: { rows: 'listing', by: 'words', value: 'ruling', titleKey: 'titling' }, places: { line: { tag: 'polyline', each: 'lines', by: 'group', gives: 'drawing' } }, describes: 'A quantity over time, with axes. The chart for a trend somebody will read values off.' },
100
- { id: 'link', at: 'thread/ui/elements/link.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'a', keeps: true, shows: 'labelKey', carries: { navigateTo: 'ruling', labelKey: 'wording', is: 'styling' }, works: './link.works.ts', describes: 'Wording that carries somebody to a declared screen. A press that navigates and does not act.' },
101
- { id: 'media', at: 'thread/ui/elements/media.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: { by: 'as', image: 'img', video: 'video', audio: 'audio', file: 'a' }, carries: { as: 'words', src: 'source', altKey: 'naming' }, describes: 'Whatever a media or file value turns out to be — a picture, a clip, a document — rendered by its kind.' },
102
- { id: 'menu', at: 'thread/ui/elements/menu.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'span', keeps: true, works: './menu.works.tsx', dresses: 'opener', measures: ['x', 'y'], carries: { labelKey: 'wording', items: 'listing', side: 'words' }, places: { opener: { tag: 'button', gives: 'opener', shows: 'labelKey' }, portal: { tag: 'Presented', gives: 'portal', holds: 'layer' }, layer: { tag: 'div', gives: 'listing', holds: 'row', position: 'position-lifted', left: 'x', top: 'y' }, row: { tag: 'MenuRow', each: 'items', by: 'labelKey', gives: 'row' } }, describes: 'Actions revealed on demand from a trigger, with keyboard traversal and dismissal already handled.' },
103
- { id: 'money', at: 'thread/ui/elements/money.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'span', raises: ['invalid'], carries: { value: 'amount', is: 'styling' }, works: './money.works.ts', describes: 'An amount with its currency, formatted for the locale. Never a number and a symbol concatenated.' },
104
- { id: 'pagination', at: 'thread/ui/elements/pagination.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'nav', called: 'nav.pagination', works: './pagination.works.ts', carries: { map: 'mapping', sequence: 'ordering', current: 'ruling' }, places: { step: { tag: 'Link.linkInline', each: 'steps', by: 'id', gives: 'step' } }, describes: 'Movement between pages of a long result, for when scrolling to the end is not reasonable.' },
105
- { id: 'progress', at: 'thread/ui/elements/progress.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'div', reads: 'progress', carries: { value: 'count', target: 'count', labelKey: 'naming' }, measures: ['fraction', 'span'], works: './progress.works.ts', places: { bar: { width: 'span' } }, describes: 'Work advancing toward a known end. The determinate half of what a spinner shows.' },
106
- { id: 'radio-group', at: 'thread/ui/elements/radio-group.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'div', reads: 'choices', keeps: true, works: './radio-group.works.ts', carries: { name: 'ruling', labelKey: 'naming', options: 'listing', chosen: 'words', onValue: 'picking', disabled: 'flag' }, places: { option: { tag: 'label', each: 'options', by: 'value', holds: 'control caption' }, control: { tag: 'input', reads: 'choosing', gives: 'option' }, caption: { shows: 'caption' } }, describes: 'One answer from a short set, all visible at once. The version of a picker with nothing hidden.' },
107
- { id: 'row', at: 'thread/ui/elements/row.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'div', describes: 'Children left to right at the width each one needs. The horizontal arrangement, and the one this catalogue did not have — measured 2026-08-31: a column stacks, a grid cuts equal tracks, a stack overlaps, and a box states what it means rather than which way it runs, so a label beside a button had no declared shape and every app drew it with a host element.', arranges: true, sets: { display: 'layout-flex', flexDirection: 'flow-across', alignItems: 'align-centre', gap: 'space-3' } },
108
- { id: 'scatter', at: 'thread/ui/elements/scatter.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'svg', reads: 'picture', keeps: true, works: './scatter.works.ts', carries: { rows: 'listing', x: 'ruling', y: 'ruling', titleKey: 'titling' }, places: { point: { tag: 'circle', each: 'rows', keyed: 'position', gives: 'point' } }, describes: 'Two measurements plotted against each other, to show whether they move together.' },
109
- { id: 'select', at: 'thread/ui/elements/select.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'label', carries: { labelKey: 'wording', options: 'listing', value: 'words', onValue: 'picking' }, works: './select.works.ts', places: { label: { shows: 'labelKey' }, control: { tag: 'select', reads: 'chooser', shows: 'value', holds: 'option' }, option: { tag: 'option', each: 'options', by: 'value', shows: 'labelKey' } }, describes: 'One answer picked from a closed list somebody scrolls. For a handful of options, prefer radios.' },
110
- { id: 'separator', at: 'thread/ui/elements/separator.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'div', reads: 'divider', carries: { labelKey: 'noting', orientation: 'words' }, places: { label: { shows: 'labelKey' } }, describes: 'A visible break between groups, announced as one rather than drawn as a border on a neighbour.' },
111
- { id: 'skeleton', at: 'thread/ui/elements/skeleton.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'span', reads: 'placeholder', carries: { lines: 'count' }, places: { line: { each: 'lines' } }, describes: 'The shape of content that has not arrived, sized like the real thing so nothing jumps when it does.' },
112
- { id: 'slider', at: 'thread/ui/elements/slider.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'input', reads: 'sliding', gives: ['min', 'max', 'step'], carries: { labelKey: 'naming', value: 'count', min: 'count', max: 'count', step: 'count', onValue: 'moving' }, works: './slider.works.ts', describes: 'A number chosen along a range, where the approximate value matters more than the exact one.' },
113
- { id: 'sparkline', at: 'thread/ui/elements/sparkline.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'svg', reads: 'placeholder', keeps: true, works: './sparkline.works.ts', carries: { rows: 'listing', value: 'ruling' }, places: { line: { tag: 'polyline', gives: 'drawing' } }, describes: 'A shape over time with no axes, small enough to sit inside a row of text.' },
114
- { id: 'spinner', at: 'thread/ui/elements/spinner.tsx', by: 'thread elements', runs: 'thread elements --own --check', carries: { labelKey: 'naming' }, always: ['spinner'], tag: 'span', describes: 'Work in progress with no measurable end. Where the end IS measurable, show progress instead.' },
115
- { id: 'stack', at: 'thread/ui/elements/stack.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'div', describes: 'Children on the z-axis, overlapping by a token offset — an avatar sitting over the top edge of a panel.', arranges: true },
116
- { id: 'stat', at: 'thread/ui/elements/stat.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'figure', carries: { labelKey: 'wording', value: 'amount', compare: 'count', format: 'words' }, works: './stat.works.ts', places: { figure: { shows: 'figure' }, caption: { tag: 'figcaption', shows: 'labelKey' }, delta: { shows: 'delta' } }, describes: 'One headline number with its movement, derived. Passing a precomputed delta is how a dashboard disagrees with itself.' },
117
- { id: 'surface', at: 'thread/ui/elements/surface.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'div', arranges: true, keeps: true, animates: true, works: './surface.works.ts', describes: 'A plain painted panel wearing a resolved look, for when the treatment IS the content.' },
118
- { id: 'table', at: 'thread/ui/elements/table.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'table', keeps: true, works: './table.works.ts', carries: { rows: 'listing', columns: 'listing', by: 'ruling', titleKey: 'titling' }, places: { head: { tag: 'thead', holds: 'heading' }, heading: { tag: 'tr', holds: 'headed' }, headed: { tag: 'th', each: 'columns', by: 'field', gives: 'heading', shows: 'heading' }, body: { tag: 'tbody', holds: 'record' }, record: { tag: 'tr', each: 'rows', by: 'by', holds: 'cell' }, cell: { tag: 'td', each: 'columns', by: 'field', shows: 'cell' } }, describes: 'Rows and columns somebody scans and compares. Reach for this when the exact values are the point.' },
119
- { id: 'tabs', at: 'thread/ui/elements/tabs.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'div', keeps: true, works: './tabs.works.ts', carries: { items: 'parting', initial: 'words', onChange: 'picking', orientation: 'words' }, dresses: 'strip', places: { strip: { tag: 'div', gives: 'strip', holds: 'tab', display: 'layout-flex', alignItems: 'align-centre' }, tab: { tag: 'button', each: 'items', by: 'id', gives: 'tab', shows: 'label', display: 'layout-flex', alignItems: 'align-centre', justifyContent: 'align-centre', height: 'control-md', padding: 'control-pad-y control-pad-x', borderRadius: 'control-radius', borderWidth: 'edge-hair', borderStyle: 'line-off', borderColor: 'colour-clear', background: 'colour-clear', color: 'colour-ink', cursor: 'cursor-press', transition: 'motion-quick ease-out' }, panel: { tag: 'div', when: 'opened', gives: 'panel', shows: 'inside' } }, describes: 'Sibling panels where only one is visible, switched without going anywhere. Not navigation between screens. It `dresses` its strip rather than itself, which is the difference between a row of tabs and a capsule: the element wraps the strip AND the panel, so a variant landing on the element put `radius-lg` and a filled material around the whole assembly — measured 2026-09-03, and it is what made tabs read as one pill with a page inside it. The tab itself states its own look here because every key it had was structural, so the generator emitted a `button` with no class at all and nothing any style guide row said could reach it.' },
120
- { id: 'text', at: 'thread/ui/elements/text.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'span', shows: 'k', sets: { display: 'layout-inline-block' }, carries: { k: 'wording', with: 'holding', is: 'styling', announce: 'announcing' }, describes: 'The only place a message key becomes visible words. Every sentence a person reads passes through here. It sets `inline-block` because that is what makes a font role\'s trim reach it: `text-box-trim` acts on a block container\'s line boxes and an inline box is not one, so a role carrying the trim governed nothing wherever wording rendered inline. Measured 2026-09-03 at a 1280 viewport: 62 of 90 text elements trimmed to their cap height exactly without this, 72 with it, and the number wrapping to more than one line was 4 either way — it costs no wrapping. Inline-block rather than block because wording still belongs in a line beside other things when an app puts it there.' },
121
- { id: 'textarea', at: 'thread/ui/elements/textarea.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'label', carries: { labelKey: 'wording', name: 'words', value: 'words', onValue: 'picking', validator: 'words', rows: 'count' }, works: './textarea.works.ts', places: { label: { shows: 'labelKey' }, control: { tag: 'textarea', reads: 'writing', shows: 'value', takes: 'name rows validator' } }, describes: 'Prose being typed across more than one line, where the shape of what is written matters.' },
122
- { id: 'time', at: 'thread/ui/elements/time.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'span', raises: ['invalid'], keeps: true, carries: { value: 'amount', relative: 'flag', is: 'styling' }, works: './time.works.ts', describes: 'A moment, formatted absolutely or as distance from now. Never a date somebody assembled from parts.' },
123
- { id: 'toggle', at: 'thread/ui/elements/toggle.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'label', carries: { labelKey: 'wording', on: 'flag', onToggle: 'telling' }, works: './toggle.works.ts', places: { control: { tag: 'input', reads: 'choice', shows: 'on' }, label: { shows: 'labelKey' } }, describes: 'A single yes-or-no answer, flipped in place. Reach for this rather than two mutually exclusive buttons.' },
124
- { id: 'tooltip', at: 'thread/ui/elements/tooltip.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'span', arranges: true, keeps: true, works: './tooltip.works.ts', dresses: 'bubble', measures: ['x', 'y'], carries: { labelKey: 'wording', side: 'words' }, places: { bubble: { tag: 'span', when: 'showing', gives: 'bubble', shows: 'labelKey', position: 'position-lifted', left: 'x', top: 'y' } }, describes: 'A short clarification revealed by hovering or focusing. Never the only place something is said.' },
125
- { id: 'value', at: 'thread/ui/elements/value.tsx', by: 'thread elements', runs: 'thread elements --own --check', tag: 'span', raises: ['invalid'], carries: { validator: 'ruling', value: 'amount', context: 'holding', is: 'styling' }, works: './value.works.ts', describes: 'A single formatted datum, punctuated by the validator that governs its kind rather than by the caller.' },
126
- ]),
127
- } as const satisfies Catalogue