@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
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, type RefObject } from 'react'
|
|
4
|
+
|
|
5
|
+
export const ITEM_ATTR = 'data-roving-item'
|
|
6
|
+
|
|
7
|
+
export interface RovingOptions {
|
|
8
|
+
readonly orientation: 'vertical' | 'horizontal'
|
|
9
|
+
|
|
10
|
+
readonly wrap?: boolean
|
|
11
|
+
readonly active?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface Item {
|
|
15
|
+
readonly element: HTMLElement
|
|
16
|
+
readonly disabled: boolean
|
|
17
|
+
readonly text: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function items(root: HTMLElement | null): readonly Item[] {
|
|
21
|
+
if (root === null) return []
|
|
22
|
+
return [...root.querySelectorAll<HTMLElement>(`[${ITEM_ATTR}]`)].map((element) => ({
|
|
23
|
+
element,
|
|
24
|
+
|
|
25
|
+
disabled: element.hasAttribute('disabled') || element.getAttribute('aria-disabled') === 'true',
|
|
26
|
+
text: (element.textContent ?? '').trim().toLowerCase(),
|
|
27
|
+
}))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function step(list: readonly Item[], from: number, direction: 1 | -1, wrap: boolean): number {
|
|
31
|
+
const count = list.length
|
|
32
|
+
if (count === 0) return -1
|
|
33
|
+
let index = from
|
|
34
|
+
for (let moved = 0; moved < count; moved += 1) {
|
|
35
|
+
const next = index + direction
|
|
36
|
+
if (next < 0 || next >= count) {
|
|
37
|
+
if (!wrap) return from
|
|
38
|
+
index = direction === 1 ? 0 : count - 1
|
|
39
|
+
} else {
|
|
40
|
+
index = next
|
|
41
|
+
}
|
|
42
|
+
if (!(list[index] as Item).disabled) return index
|
|
43
|
+
}
|
|
44
|
+
return from
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function firstEnabled(list: readonly Item[], from = 0): number {
|
|
48
|
+
for (let i = from; i < list.length; i += 1) if (!(list[i] as Item).disabled) return i
|
|
49
|
+
return -1
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function lastEnabled(list: readonly Item[]): number {
|
|
53
|
+
for (let i = list.length - 1; i >= 0; i -= 1) if (!(list[i] as Item).disabled) return i
|
|
54
|
+
return -1
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isRtl(element: HTMLElement): boolean {
|
|
58
|
+
const declared = element.closest('[dir]')?.getAttribute('dir')?.toLowerCase()
|
|
59
|
+
if (declared === 'rtl') return true
|
|
60
|
+
if (declared === 'ltr') return false
|
|
61
|
+
return getComputedStyle(element).direction === 'rtl'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const TYPEAHEAD_MS = 1000
|
|
65
|
+
|
|
66
|
+
export function typeaheadMatch(list: readonly Item[], buffer: string, current: number): number {
|
|
67
|
+
const enabled = list.map((item, index) => ({ item, index })).filter(({ item }) => !item.disabled)
|
|
68
|
+
if (enabled.length === 0 || buffer.length === 0) return -1
|
|
69
|
+
|
|
70
|
+
const repeated = buffer.length > 1 && [...buffer].every((c) => c === buffer[0])
|
|
71
|
+
const prefix = repeated ? (buffer[0] as string) : buffer
|
|
72
|
+
|
|
73
|
+
const startAt = enabled.findIndex(({ index }) => index === current)
|
|
74
|
+
for (let i = 0; i < enabled.length; i += 1) {
|
|
75
|
+
const candidate = enabled[(startAt + 1 + i) % enabled.length] as { item: Item; index: number }
|
|
76
|
+
if (candidate.item.text.startsWith(prefix)) return candidate.index
|
|
77
|
+
}
|
|
78
|
+
return -1
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function useRovingFocus(ref: RefObject<HTMLElement | null>, options: RovingOptions): void {
|
|
82
|
+
const { orientation, wrap = false, active = true } = options
|
|
83
|
+
const indexRef = useRef(0)
|
|
84
|
+
const bufferRef = useRef('')
|
|
85
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
|
86
|
+
|
|
87
|
+
const applyTabIndex = useCallback(
|
|
88
|
+
(list: readonly Item[], index: number): void => {
|
|
89
|
+
list.forEach((item, i) => {
|
|
90
|
+
item.element.setAttribute('tabindex', i === index ? '0' : '-1')
|
|
91
|
+
})
|
|
92
|
+
},
|
|
93
|
+
[],
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!active) return
|
|
98
|
+
const root = ref.current
|
|
99
|
+
if (root === null) return
|
|
100
|
+
|
|
101
|
+
const list = items(root)
|
|
102
|
+
const start = firstEnabled(list)
|
|
103
|
+
indexRef.current = start === -1 ? 0 : start
|
|
104
|
+
applyTabIndex(list, indexRef.current)
|
|
105
|
+
|
|
106
|
+
const move = (to: number): void => {
|
|
107
|
+
const current = items(root)
|
|
108
|
+
if (to < 0 || to >= current.length) return
|
|
109
|
+
indexRef.current = to
|
|
110
|
+
applyTabIndex(current, to)
|
|
111
|
+
;(current[to] as Item).element.focus()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const onKeyDown = (event: KeyboardEvent): void => {
|
|
115
|
+
const current = items(root)
|
|
116
|
+
if (current.length === 0) return
|
|
117
|
+
|
|
118
|
+
const rtl = isRtl(root)
|
|
119
|
+
const forward = orientation === 'horizontal' ? (rtl ? 'ArrowLeft' : 'ArrowRight') : 'ArrowDown'
|
|
120
|
+
const backward = orientation === 'horizontal' ? (rtl ? 'ArrowRight' : 'ArrowLeft') : 'ArrowUp'
|
|
121
|
+
|
|
122
|
+
if (event.key === forward) {
|
|
123
|
+
event.preventDefault()
|
|
124
|
+
move(step(current, indexRef.current, 1, wrap))
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
if (event.key === backward) {
|
|
128
|
+
event.preventDefault()
|
|
129
|
+
move(step(current, indexRef.current, -1, wrap))
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
if (event.key === 'Home') {
|
|
133
|
+
event.preventDefault()
|
|
134
|
+
move(firstEnabled(current))
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
if (event.key === 'End') {
|
|
138
|
+
event.preventDefault()
|
|
139
|
+
move(lastEnabled(current))
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (event.key.length !== 1 || event.ctrlKey || event.metaKey || event.altKey) return
|
|
144
|
+
event.preventDefault()
|
|
145
|
+
bufferRef.current += event.key.toLowerCase()
|
|
146
|
+
if (timerRef.current !== undefined) clearTimeout(timerRef.current)
|
|
147
|
+
timerRef.current = setTimeout(() => {
|
|
148
|
+
bufferRef.current = ''
|
|
149
|
+
}, TYPEAHEAD_MS)
|
|
150
|
+
|
|
151
|
+
const found = typeaheadMatch(current, bufferRef.current, indexRef.current)
|
|
152
|
+
if (found !== -1) move(found)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
root.addEventListener('keydown', onKeyDown)
|
|
156
|
+
return () => {
|
|
157
|
+
root.removeEventListener('keydown', onKeyDown)
|
|
158
|
+
if (timerRef.current !== undefined) clearTimeout(timerRef.current)
|
|
159
|
+
}
|
|
160
|
+
}, [active, orientation, wrap, ref, applyTabIndex])
|
|
161
|
+
}
|
package/dist/ui/screens.js
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
10
|
-
import { basename, dirname, join } from 'node:path';
|
|
11
|
-
import { stdout } from 'node:process';
|
|
12
|
-
import { pathToFileURL } from 'node:url';
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "@prestack/thread/core/files";
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { argv, stdout } from 'node:process';
|
|
13
4
|
import { exit, invoked } from "@prestack/thread/core/invoked";
|
|
5
|
+
import { valuesIn } from "@prestack/thread/core/rows";
|
|
14
6
|
import { findTree, inTree } from "@prestack/thread/core/tree";
|
|
15
|
-
import { undeclared } from "./compositions.js";
|
|
7
|
+
import { HOME, settled, undeclared } from "./compositions.js";
|
|
16
8
|
import { PRESENTATIONS } from "./door.js";
|
|
17
9
|
const here = 'thread/ui/screens.ts';
|
|
18
10
|
const said = [
|
|
@@ -70,7 +62,7 @@ function starter(row) {
|
|
|
70
62
|
const name = nameOf(row.at);
|
|
71
63
|
return `'use client'
|
|
72
64
|
|
|
73
|
-
import { screen } from '
|
|
65
|
+
import { screen } from '${HOME}/wrappers'
|
|
74
66
|
|
|
75
67
|
export const ${name} = screen('${row.id}', {}, () => (
|
|
76
68
|
<>
|
|
@@ -81,26 +73,34 @@ export const ${name} = screen('${row.id}', {}, () => (
|
|
|
81
73
|
export function seeds(rows, held) {
|
|
82
74
|
return rows.filter((row) => !held(row.at)).map((row) => ({ path: row.at, content: starter(row) }));
|
|
83
75
|
}
|
|
84
|
-
|
|
76
|
+
function run() {
|
|
85
77
|
const tree = findTree();
|
|
86
78
|
const at = inTree(tree, 'screens.ts');
|
|
87
79
|
if (!existsSync(at)) {
|
|
88
80
|
stdout.write(`thread: this tree has no ${tree.name}/${basename(at)}, so it declares no screen and there is nothing to seed or check.\n`);
|
|
89
81
|
return 0;
|
|
90
82
|
}
|
|
91
|
-
const held = (
|
|
83
|
+
const held = valuesIn(readFileSync(at));
|
|
92
84
|
if (held.SCREEN === undefined) {
|
|
93
85
|
stdout.write(`thread: ${tree.name}/screens.ts exports no \`SCREEN\`, so nothing declares a screen here.\n`);
|
|
94
86
|
return 1;
|
|
95
87
|
}
|
|
96
88
|
const rows = held.SCREEN.declares ?? [];
|
|
89
|
+
const checking = argv.includes('--check');
|
|
97
90
|
const made = seeds(rows, (where) => existsSync(join(tree.at, where)));
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
91
|
+
const unseeded = made[0];
|
|
92
|
+
if (checking && unseeded !== undefined) {
|
|
93
|
+
stdout.write(`thread: ${unseeded.path} is declared and the file is not there yet. A row names a file, and ` +
|
|
94
|
+
'`--check` never writes one — run `thread screens` and commit what it seeds.\n');
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
if (!checking) {
|
|
98
|
+
for (const one of made) {
|
|
99
|
+
writeFileSync(join(tree.at, one.path), one.content, 'utf8');
|
|
100
|
+
stdout.write(`thread: seeded ${one.path}\n`);
|
|
101
|
+
}
|
|
102
102
|
}
|
|
103
|
-
const loose =
|
|
103
|
+
const loose = [...undeclared(tree, rows), ...settled(tree.at, rows, checking, 'thread screens', new Set())];
|
|
104
104
|
for (const one of loose)
|
|
105
105
|
stdout.write(`thread: ${one}\n`);
|
|
106
106
|
stdout.write(`thread: ${String(rows.length)} screen(s) declared, ${String(made.length)} seeded, ` +
|
|
@@ -109,4 +109,4 @@ async function run() {
|
|
|
109
109
|
return loose.length === 0 ? 0 : 1;
|
|
110
110
|
}
|
|
111
111
|
if (invoked(import.meta.url))
|
|
112
|
-
exit(
|
|
112
|
+
exit(run());
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from '@prestack/thread/core/files'
|
|
2
|
+
import { basename, dirname, join } from 'node:path'
|
|
3
|
+
import { argv, stdout } from 'node:process'
|
|
4
|
+
|
|
5
|
+
import { exit, invoked } from '@prestack/thread/core/invoked'
|
|
6
|
+
import { valuesIn } from '@prestack/thread/core/rows'
|
|
7
|
+
import type { Threaded } from '@prestack/thread/core/thread'
|
|
8
|
+
import { findTree, inTree } from '@prestack/thread/core/tree'
|
|
9
|
+
|
|
10
|
+
import { HOME, settled, undeclared } from './compositions.ts'
|
|
11
|
+
import { PRESENTATIONS } from './door.ts'
|
|
12
|
+
|
|
13
|
+
const here = 'thread/ui/screens.ts'
|
|
14
|
+
|
|
15
|
+
const said = [
|
|
16
|
+
{
|
|
17
|
+
on: '2026-08-25',
|
|
18
|
+
said: 'Screens are basically compositions arranged in a screen. And this is where we have breakpoint, etc. Compositions maybe also have breakpoints.',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
on: '2026-08-25',
|
|
22
|
+
said: 'Arrangement is a real fucking thing. IT IS HOW THE STUFF LOOKS IN A SCREEN',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
on: '2026-08-25',
|
|
26
|
+
said: 'Both',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
on: '2026-09-09',
|
|
30
|
+
said: 'We have one screen or more. A button navigates to other screen. From THAT the routes are generated.',
|
|
31
|
+
},
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
export const declares: readonly Threaded[] = []
|
|
35
|
+
|
|
36
|
+
export const SCREENS: Threaded & { readonly files: string } = {
|
|
37
|
+
id: 'screen',
|
|
38
|
+
at: here,
|
|
39
|
+
files: 'screens/scr_*.tsx',
|
|
40
|
+
describes:
|
|
41
|
+
'Compositions arranged, and the way it is presented: page, sheet, popover, overlay or toast. Where it lives is never typed. The first page an app declares answers `/`, every other page answers its own name, and a sheet, popover, overlay or toast has no route, because a press opens it where somebody already is. The arrangement is the whole of what a screen adds, because how things sit on a screen is declared rather than markup written twice. Each row names a file prefixed `scr_`, sets the properties that lay its compositions out, and may set them differently under a mode, which is where a breakpoint lives: a width is a media query the same way dark is. A composition may set its own arrangement too. The framework ships none, because what an app is made of is the app’s, and this row says what a screen is, never what is inside it.',
|
|
42
|
+
version: 1,
|
|
43
|
+
cites: said,
|
|
44
|
+
declares,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const PASCAL = (id: string): string =>
|
|
48
|
+
id
|
|
49
|
+
.split(/[-_]/)
|
|
50
|
+
.map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
|
|
51
|
+
.join('')
|
|
52
|
+
|
|
53
|
+
export function nameOf(at: string): string {
|
|
54
|
+
const file = at.split('\\').join('/').split('/').at(-1) ?? at
|
|
55
|
+
return PASCAL(file.replace(/^scr_/, '').replace(/\.[^.]+$/, ''))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const PAGE = PRESENTATIONS[0]
|
|
59
|
+
|
|
60
|
+
const PRESENTED = /screen\(\s*'[^']*'\s*,\s*\{[^}]*as:\s*'([^']*)'/
|
|
61
|
+
|
|
62
|
+
export function presentedIn(source: string): string {
|
|
63
|
+
return PRESENTED.exec(source)?.[1] ?? PAGE
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function routeFor(id: string, entry: boolean): string {
|
|
67
|
+
return entry ? '/' : `/${id}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function pageAt(route: string): string {
|
|
71
|
+
const trimmed = route.replace(/^\/+|\/+$/g, '')
|
|
72
|
+
return trimmed === '' ? `app/page.${'tsx'}` : `app/${trimmed}/page.${'tsx'}`
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function entryAmong(rows: readonly { readonly id: string }[], shownAs: (id: string) => string): string | undefined {
|
|
76
|
+
return rows.find((row) => shownAs(row.id) === PAGE)?.id
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function starter(row: Threaded): string {
|
|
80
|
+
const name = nameOf(row.at)
|
|
81
|
+
return `'use client'
|
|
82
|
+
|
|
83
|
+
import { screen } from '${HOME}/wrappers'
|
|
84
|
+
|
|
85
|
+
export const ${name} = screen('${row.id}', {}, () => (
|
|
86
|
+
<>
|
|
87
|
+
</>
|
|
88
|
+
))
|
|
89
|
+
`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function seeds(rows: readonly Threaded[], held: (at: string) => boolean): readonly { readonly path: string; readonly content: string }[] {
|
|
93
|
+
return rows.filter((row) => !held(row.at)).map((row) => ({ path: row.at, content: starter(row) }))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function run(): number {
|
|
97
|
+
const tree = findTree()
|
|
98
|
+
const at = inTree(tree, 'screens.ts')
|
|
99
|
+
if (!existsSync(at)) {
|
|
100
|
+
stdout.write(`thread: this tree has no ${tree.name}/${basename(at)}, so it declares no screen and there is nothing to seed or check.\n`)
|
|
101
|
+
return 0
|
|
102
|
+
}
|
|
103
|
+
const held = valuesIn(readFileSync(at)) as { SCREEN?: { readonly declares?: readonly Threaded[] } }
|
|
104
|
+
|
|
105
|
+
if (held.SCREEN === undefined) {
|
|
106
|
+
stdout.write(`thread: ${tree.name}/screens.ts exports no \`SCREEN\`, so nothing declares a screen here.\n`)
|
|
107
|
+
return 1
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const rows = held.SCREEN.declares ?? []
|
|
111
|
+
const checking = argv.includes('--check')
|
|
112
|
+
const made = seeds(rows, (where) => existsSync(join(tree.at, where)))
|
|
113
|
+
|
|
114
|
+
const unseeded = made[0]
|
|
115
|
+
if (checking && unseeded !== undefined) {
|
|
116
|
+
stdout.write(
|
|
117
|
+
`thread: ${unseeded.path} is declared and the file is not there yet. A row names a file, and ` +
|
|
118
|
+
'`--check` never writes one — run `thread screens` and commit what it seeds.\n',
|
|
119
|
+
)
|
|
120
|
+
return 1
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!checking) {
|
|
124
|
+
for (const one of made) {
|
|
125
|
+
writeFileSync(join(tree.at, one.path), one.content, 'utf8')
|
|
126
|
+
stdout.write(`thread: seeded ${one.path}\n`)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const loose = [...undeclared(tree, rows), ...settled(tree.at, rows, checking, 'thread screens', new Set<string>())]
|
|
131
|
+
for (const one of loose) stdout.write(`thread: ${one}\n`)
|
|
132
|
+
|
|
133
|
+
stdout.write(
|
|
134
|
+
`thread: ${String(rows.length)} screen(s) declared, ${String(made.length)} seeded, ` +
|
|
135
|
+
`${String(rows.length - made.length)} already written, ` +
|
|
136
|
+
`${loose.length === 0 ? 'none reaching anything undeclared' : `${String(loose.length)} reaching something undeclared`}\n`,
|
|
137
|
+
)
|
|
138
|
+
return loose.length === 0 ? 0 : 1
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (invoked(import.meta.url)) exit(run())
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { nameOf } from './token.ts'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
let held = 0
|
|
6
|
+
let previous: string | null = null
|
|
7
|
+
|
|
8
|
+
export function lockScroll(): () => void {
|
|
9
|
+
if (held === 0) {
|
|
10
|
+
previous = document.body.style.overflow
|
|
11
|
+
document.body.style.overflow = `var(${nameOf('overflow-hidden')})`
|
|
12
|
+
}
|
|
13
|
+
held += 1
|
|
14
|
+
|
|
15
|
+
let released = false
|
|
16
|
+
return () => {
|
|
17
|
+
if (released) return
|
|
18
|
+
released = true
|
|
19
|
+
held -= 1
|
|
20
|
+
if (held > 0) return
|
|
21
|
+
document.body.style.overflow = previous ?? ''
|
|
22
|
+
previous = null
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function scrollLocks(): number {
|
|
27
|
+
return held
|
|
28
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { contrast, CONTRAST_LARGE } from './contrast.ts'
|
|
2
|
+
|
|
3
|
+
const GOLDEN_ANGLE = 137.508
|
|
4
|
+
|
|
5
|
+
const toLinear = (c: number): number => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)
|
|
6
|
+
const toSrgb = (c: number): number => (c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055)
|
|
7
|
+
|
|
8
|
+
function parseHex(hex: string): [number, number, number] {
|
|
9
|
+
const clean = hex.replace('#', '')
|
|
10
|
+
const full = clean.length === 3 ? clean.split('').map((c) => c + c).join('') : clean.slice(0, 6)
|
|
11
|
+
const n = Number.parseInt(full, 16)
|
|
12
|
+
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function toHex(r: number, g: number, b: number): string {
|
|
16
|
+
const channel = (v: number): string =>
|
|
17
|
+
Math.round(Math.min(1, Math.max(0, v)) * 255)
|
|
18
|
+
.toString(16)
|
|
19
|
+
.padStart(2, '0')
|
|
20
|
+
return `#${channel(r)}${channel(g)}${channel(b)}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Oklch {
|
|
24
|
+
|
|
25
|
+
readonly l: number
|
|
26
|
+
|
|
27
|
+
readonly c: number
|
|
28
|
+
|
|
29
|
+
readonly h: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function hexToOklch(hex: string): Oklch {
|
|
33
|
+
const [r8, g8, b8] = parseHex(hex)
|
|
34
|
+
const r = toLinear(r8)
|
|
35
|
+
const g = toLinear(g8)
|
|
36
|
+
const b = toLinear(b8)
|
|
37
|
+
|
|
38
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b)
|
|
39
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b)
|
|
40
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b)
|
|
41
|
+
|
|
42
|
+
const lab = {
|
|
43
|
+
L: 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
|
|
44
|
+
a: 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
|
|
45
|
+
b: 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const hue = (Math.atan2(lab.b, lab.a) * 180) / Math.PI
|
|
49
|
+
return { l: lab.L, c: Math.hypot(lab.a, lab.b), h: hue < 0 ? hue + 360 : hue }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function oklchToHex({ l: L, c: C, h }: Oklch): string {
|
|
53
|
+
const rad = (h * Math.PI) / 180
|
|
54
|
+
const a = C * Math.cos(rad)
|
|
55
|
+
const b = C * Math.sin(rad)
|
|
56
|
+
|
|
57
|
+
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3
|
|
58
|
+
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3
|
|
59
|
+
const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3
|
|
60
|
+
|
|
61
|
+
return toHex(
|
|
62
|
+
toSrgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
|
|
63
|
+
toSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
|
|
64
|
+
toSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s),
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function seriesRamp(accent: string, surface: string, count: number): readonly string[] {
|
|
69
|
+
const base = hexToOklch(accent)
|
|
70
|
+
const out: string[] = []
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < count; i += 1) {
|
|
73
|
+
const hue = (base.h + GOLDEN_ANGLE * i) % 360
|
|
74
|
+
out.push(oklchToHex(readable({ ...base, h: hue }, surface)))
|
|
75
|
+
}
|
|
76
|
+
return out
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readable(colour: Oklch, surface: string): Oklch {
|
|
80
|
+
if (contrast(oklchToHex(colour), surface) >= CONTRAST_LARGE) return colour
|
|
81
|
+
|
|
82
|
+
const surfaceL = hexToOklch(surface).l
|
|
83
|
+
|
|
84
|
+
const direction = surfaceL > 0.5 ? -1 : 1
|
|
85
|
+
|
|
86
|
+
let best = colour
|
|
87
|
+
for (let step = 1; step <= 24; step += 1) {
|
|
88
|
+
const candidate = { ...colour, l: Math.min(0.98, Math.max(0.02, colour.l + direction * step * 0.02)) }
|
|
89
|
+
best = candidate
|
|
90
|
+
if (contrast(oklchToHex(candidate), surface) >= CONTRAST_LARGE) return candidate
|
|
91
|
+
}
|
|
92
|
+
return best
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function seriesColour(ramp: readonly string[], index: number): string {
|
|
96
|
+
return ramp[index % ramp.length] ?? ramp[0] ?? '#888888'
|
|
97
|
+
}
|