@prestack/thread-ui 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/dist/ui/animations/anm_appear.ts +13 -0
  2. package/dist/ui/animations/anm_blur_in.ts +19 -0
  3. package/dist/ui/animations/anm_blur_out.ts +18 -0
  4. package/dist/ui/animations/anm_dismiss.ts +18 -0
  5. package/dist/ui/animations/anm_drop.ts +19 -0
  6. package/dist/ui/animations/anm_fall.ts +18 -0
  7. package/dist/ui/animations/anm_flash.ts +18 -0
  8. package/dist/ui/animations/anm_glow.ts +16 -0
  9. package/dist/ui/animations/anm_lift.ts +16 -0
  10. package/dist/ui/animations/anm_nudge.ts +16 -0
  11. package/dist/ui/animations/anm_pop_in.ts +23 -0
  12. package/dist/ui/animations/anm_pop_out.ts +22 -0
  13. package/dist/ui/animations/anm_press.ts +12 -0
  14. package/dist/ui/animations/anm_pulse.ts +13 -0
  15. package/dist/ui/animations/anm_rise.ts +19 -0
  16. package/dist/ui/animations/anm_shake.ts +13 -0
  17. package/dist/ui/animations/anm_slide_in_left.ts +19 -0
  18. package/dist/ui/animations/anm_slide_in_right.ts +19 -0
  19. package/dist/ui/animations/anm_slide_out_left.ts +18 -0
  20. package/dist/ui/animations/anm_slide_out_right.ts +18 -0
  21. package/dist/ui/animations/anm_spin.ts +12 -0
  22. package/dist/ui/animations/anm_swell.ts +16 -0
  23. package/dist/ui/animations/anm_wobble.ts +24 -0
  24. package/dist/ui/animations/anm_zoom_in.ts +19 -0
  25. package/dist/ui/animations/anm_zoom_out.ts +18 -0
  26. package/dist/ui/animations.ts +317 -0
  27. package/dist/ui/attach.js +13 -4
  28. package/dist/ui/attach.ts +613 -0
  29. package/dist/ui/charts.tsx +74 -0
  30. package/dist/ui/checked.d.ts +2 -2
  31. package/dist/ui/checked.ts +32 -0
  32. package/dist/ui/collector.ts +140 -0
  33. package/dist/ui/compositions.d.ts +10 -1
  34. package/dist/ui/compositions.js +262 -23
  35. package/dist/ui/compositions.ts +640 -0
  36. package/dist/ui/consent.ts +55 -0
  37. package/dist/ui/content.tsx +39 -0
  38. package/dist/ui/contrast.ts +27 -0
  39. package/dist/ui/controls.tsx +30 -0
  40. package/dist/ui/disclosure.tsx +22 -0
  41. package/dist/ui/dismiss-layer.ts +87 -0
  42. package/dist/ui/door.ts +140 -0
  43. package/dist/ui/drag.ts +316 -0
  44. package/dist/ui/elements/accordion.js +2 -2
  45. package/dist/ui/elements/accordion.tsx +47 -0
  46. package/dist/ui/elements/accordion.works.ts +66 -0
  47. package/dist/ui/elements/aspect-ratio.js +1 -1
  48. package/dist/ui/elements/aspect-ratio.tsx +30 -0
  49. package/dist/ui/elements/aspect-ratio.works.ts +5 -0
  50. package/dist/ui/elements/avatar.js +1 -1
  51. package/dist/ui/elements/avatar.tsx +33 -0
  52. package/dist/ui/elements/avatar.works.ts +16 -0
  53. package/dist/ui/elements/badge.js +1 -1
  54. package/dist/ui/elements/badge.tsx +32 -0
  55. package/dist/ui/elements/badge.works.ts +9 -0
  56. package/dist/ui/elements/bars.js +2 -2
  57. package/dist/ui/elements/bars.tsx +49 -0
  58. package/dist/ui/elements/bars.works.ts +29 -0
  59. package/dist/ui/elements/box.js +3 -2
  60. package/dist/ui/elements/box.tsx +35 -0
  61. package/dist/ui/elements/breadcrumb.js +2 -2
  62. package/dist/ui/elements/breadcrumb.tsx +37 -0
  63. package/dist/ui/elements/breadcrumb.works.ts +24 -0
  64. package/dist/ui/elements/button.js +1 -1
  65. package/dist/ui/elements/button.tsx +45 -0
  66. package/dist/ui/elements/button.works.tsx +52 -0
  67. package/dist/ui/elements/column.js +1 -1
  68. package/dist/ui/elements/column.tsx +28 -0
  69. package/dist/ui/elements/consent.js +2 -2
  70. package/dist/ui/elements/consent.tsx +70 -0
  71. package/dist/ui/elements/consent.works.tsx +132 -0
  72. package/dist/ui/elements/donut.js +2 -2
  73. package/dist/ui/elements/donut.tsx +44 -0
  74. package/dist/ui/elements/donut.works.ts +42 -0
  75. package/dist/ui/elements/each.js +1 -1
  76. package/dist/ui/elements/each.tsx +33 -0
  77. package/dist/ui/elements/each.works.tsx +82 -0
  78. package/dist/ui/elements/empty.js +1 -1
  79. package/dist/ui/elements/empty.tsx +32 -0
  80. package/dist/ui/elements/field.js +1 -1
  81. package/dist/ui/elements/field.tsx +44 -0
  82. package/dist/ui/elements/field.works.ts +65 -0
  83. package/dist/ui/elements/funnel.js +2 -2
  84. package/dist/ui/elements/funnel.tsx +49 -0
  85. package/dist/ui/elements/funnel.works.ts +37 -0
  86. package/dist/ui/elements/gauge.js +1 -1
  87. package/dist/ui/elements/gauge.tsx +39 -0
  88. package/dist/ui/elements/gauge.works.ts +42 -0
  89. package/dist/ui/elements/glyph.js +1 -1
  90. package/dist/ui/elements/glyph.tsx +29 -0
  91. package/dist/ui/elements/glyph.works.tsx +42 -0
  92. package/dist/ui/elements/grid.js +1 -1
  93. package/dist/ui/elements/grid.tsx +28 -0
  94. package/dist/ui/elements/heatmap.js +2 -2
  95. package/dist/ui/elements/heatmap.tsx +47 -0
  96. package/dist/ui/elements/heatmap.works.ts +24 -0
  97. package/dist/ui/elements/hit-box.js +1 -1
  98. package/dist/ui/elements/hit-box.tsx +46 -0
  99. package/dist/ui/elements/hit-box.works.tsx +576 -0
  100. package/dist/ui/elements/image.js +1 -1
  101. package/dist/ui/elements/image.tsx +28 -0
  102. package/dist/ui/elements/line-chart.js +2 -2
  103. package/dist/ui/elements/line-chart.tsx +44 -0
  104. package/dist/ui/elements/line-chart.works.ts +41 -0
  105. package/dist/ui/elements/link.js +1 -1
  106. package/dist/ui/elements/link.tsx +37 -0
  107. package/dist/ui/elements/link.works.ts +25 -0
  108. package/dist/ui/elements/media.js +1 -1
  109. package/dist/ui/elements/media.tsx +56 -0
  110. package/dist/ui/elements/menu.js +2 -2
  111. package/dist/ui/elements/menu.tsx +50 -0
  112. package/dist/ui/elements/menu.works.tsx +153 -0
  113. package/dist/ui/elements/money.js +1 -1
  114. package/dist/ui/elements/money.tsx +28 -0
  115. package/dist/ui/elements/money.works.ts +9 -0
  116. package/dist/ui/elements/pagination.js +2 -2
  117. package/dist/ui/elements/pagination.tsx +38 -0
  118. package/dist/ui/elements/pagination.works.ts +20 -0
  119. package/dist/ui/elements/progress.js +1 -1
  120. package/dist/ui/elements/progress.tsx +33 -0
  121. package/dist/ui/elements/progress.works.ts +8 -0
  122. package/dist/ui/elements/radio-group.js +2 -2
  123. package/dist/ui/elements/radio-group.tsx +49 -0
  124. package/dist/ui/elements/radio-group.works.ts +33 -0
  125. package/dist/ui/elements/row.js +1 -1
  126. package/dist/ui/elements/row.tsx +28 -0
  127. package/dist/ui/elements/scatter.js +1 -1
  128. package/dist/ui/elements/scatter.tsx +41 -0
  129. package/dist/ui/elements/scatter.works.ts +38 -0
  130. package/dist/ui/elements/select.js +2 -2
  131. package/dist/ui/elements/select.tsx +42 -0
  132. package/dist/ui/elements/select.works.ts +5 -0
  133. package/dist/ui/elements/separator.js +1 -1
  134. package/dist/ui/elements/separator.tsx +30 -0
  135. package/dist/ui/elements/skeleton.js +1 -1
  136. package/dist/ui/elements/skeleton.tsx +30 -0
  137. package/dist/ui/elements/slider.js +1 -1
  138. package/dist/ui/elements/slider.tsx +33 -0
  139. package/dist/ui/elements/slider.works.ts +5 -0
  140. package/dist/ui/elements/sparkline.js +1 -1
  141. package/dist/ui/elements/sparkline.tsx +36 -0
  142. package/dist/ui/elements/sparkline.works.ts +32 -0
  143. package/dist/ui/elements/spinner.js +1 -1
  144. package/dist/ui/elements/spinner.tsx +32 -0
  145. package/dist/ui/elements/stack.js +1 -1
  146. package/dist/ui/elements/stack.tsx +28 -0
  147. package/dist/ui/elements/stat.js +1 -1
  148. package/dist/ui/elements/stat.tsx +35 -0
  149. package/dist/ui/elements/stat.works.ts +14 -0
  150. package/dist/ui/elements/surface.js +1 -1
  151. package/dist/ui/elements/surface.tsx +35 -0
  152. package/dist/ui/elements/surface.works.ts +9 -0
  153. package/dist/ui/elements/table.js +2 -2
  154. package/dist/ui/elements/table.tsx +61 -0
  155. package/dist/ui/elements/table.works.ts +30 -0
  156. package/dist/ui/elements/tabs.js +2 -2
  157. package/dist/ui/elements/tabs.tsx +47 -0
  158. package/dist/ui/elements/tabs.works.ts +84 -0
  159. package/dist/ui/elements/text.js +1 -1
  160. package/dist/ui/elements/text.tsx +32 -0
  161. package/dist/ui/elements/textarea.js +1 -1
  162. package/dist/ui/elements/textarea.tsx +37 -0
  163. package/dist/ui/elements/textarea.works.ts +5 -0
  164. package/dist/ui/elements/time.js +1 -1
  165. package/dist/ui/elements/time.tsx +34 -0
  166. package/dist/ui/elements/time.works.ts +25 -0
  167. package/dist/ui/elements/toggle.js +1 -1
  168. package/dist/ui/elements/toggle.tsx +33 -0
  169. package/dist/ui/elements/toggle.works.ts +5 -0
  170. package/dist/ui/elements/tooltip.js +1 -1
  171. package/dist/ui/elements/tooltip.tsx +39 -0
  172. package/dist/ui/elements/tooltip.works.ts +77 -0
  173. package/dist/ui/elements/value.js +1 -1
  174. package/dist/ui/elements/value.tsx +30 -0
  175. package/dist/ui/elements/value.works.ts +9 -0
  176. package/dist/ui/elements.ts +127 -0
  177. package/dist/ui/focus-scope.ts +121 -0
  178. package/dist/ui/fonts.ts +355 -0
  179. package/dist/ui/generators/dev.ts +79 -0
  180. package/dist/ui/generators/elements.d.ts +1 -11
  181. package/dist/ui/generators/elements.js +43 -13
  182. package/dist/ui/generators/elements.ts +794 -0
  183. package/dist/ui/generators/i18n.ts +301 -0
  184. package/dist/ui/generators/icons.ts +214 -0
  185. package/dist/ui/generators/react.js +10 -3
  186. package/dist/ui/generators/react.ts +268 -0
  187. package/dist/ui/generators/routes.d.ts +1 -0
  188. package/dist/ui/generators/routes.js +89 -69
  189. package/dist/ui/generators/routes.ts +739 -0
  190. package/dist/ui/generators/show.ts +259 -0
  191. package/dist/ui/generators/styles.js +1 -1
  192. package/dist/ui/generators/styles.ts +670 -0
  193. package/dist/ui/generators/variants.ts +205 -0
  194. package/dist/ui/i18n/catalogs/en.ts +191 -0
  195. package/dist/ui/i18n/format.ts +221 -0
  196. package/dist/ui/i18n/locales.ts +33 -0
  197. package/dist/ui/i18n/parse.ts +315 -0
  198. package/dist/ui/i18n/translator.ts +31 -0
  199. package/dist/ui/i18n/types.ts +96 -0
  200. package/dist/ui/icons.ts +331 -0
  201. package/dist/ui/islands/map.ts +58 -0
  202. package/dist/ui/localizations.ts +75 -0
  203. package/dist/ui/makers/message.ts +292 -0
  204. package/dist/ui/material.ts +236 -0
  205. package/dist/ui/materials/mat_bordered.ts +13 -0
  206. package/dist/ui/materials/mat_ceramic.ts +13 -0
  207. package/dist/ui/materials/mat_clay.ts +13 -0
  208. package/dist/ui/materials/mat_elevated.ts +24 -0
  209. package/dist/ui/materials/mat_flat.ts +34 -0
  210. package/dist/ui/materials/mat_glass.ts +20 -0
  211. package/dist/ui/materials/mat_lit.ts +13 -0
  212. package/dist/ui/materials/mat_moulded.ts +11 -0
  213. package/dist/ui/materials.ts +206 -0
  214. package/dist/ui/modes.ts +44 -0
  215. package/dist/ui/navigation.tsx +32 -0
  216. package/dist/ui/positioning.ts +90 -0
  217. package/dist/ui/presented.tsx +119 -0
  218. package/dist/ui/provider.d.ts +2 -0
  219. package/dist/ui/provider.js +7 -1
  220. package/dist/ui/provider.tsx +182 -0
  221. package/dist/ui/render-loop.ts +133 -0
  222. package/dist/ui/roving-focus.ts +161 -0
  223. package/dist/ui/screens.js +17 -8
  224. package/dist/ui/screens.ts +142 -0
  225. package/dist/ui/scroll-lock.ts +28 -0
  226. package/dist/ui/series.ts +97 -0
  227. package/dist/ui/signals.ts +386 -0
  228. package/dist/ui/styleguide.ts +231 -0
  229. package/dist/ui/styleguides.ts +374 -0
  230. package/dist/ui/themes.ts +22 -0
  231. package/dist/ui/token.js +1 -1
  232. package/dist/ui/token.ts +78 -0
  233. package/dist/ui/tokens/shadow.ts +25 -0
  234. package/dist/ui/tokens.ts +235 -0
  235. package/dist/ui/validators.ts +232 -0
  236. package/dist/ui/variant.d.ts +1 -0
  237. package/dist/ui/variant.js +3 -0
  238. package/dist/ui/variant.ts +352 -0
  239. package/dist/ui/variants/var_avatar-round.ts +14 -0
  240. package/dist/ui/variants/var_bubble-me.ts +14 -0
  241. package/dist/ui/variants/var_bubble-them.ts +13 -0
  242. package/dist/ui/variants/var_button-border.ts +76 -0
  243. package/dist/ui/variants/var_button-cta.ts +76 -0
  244. package/dist/ui/variants/var_button-destructive-ghost.ts +72 -0
  245. package/dist/ui/variants/var_button-destructive.ts +76 -0
  246. package/dist/ui/variants/var_button-fill.ts +76 -0
  247. package/dist/ui/variants/var_button-ghost.ts +72 -0
  248. package/dist/ui/variants/var_button-glass.ts +55 -0
  249. package/dist/ui/variants/var_button-normal.ts +76 -0
  250. package/dist/ui/variants/var_button-quiet.ts +74 -0
  251. package/dist/ui/variants/var_card.ts +17 -0
  252. package/dist/ui/variants/var_chip.ts +13 -0
  253. package/dist/ui/variants/var_choice.ts +15 -0
  254. package/dist/ui/variants/var_framed.ts +10 -0
  255. package/dist/ui/variants/var_glyph-plain.ts +11 -0
  256. package/dist/ui/variants/var_input.ts +43 -0
  257. package/dist/ui/variants/var_link-inline.ts +29 -0
  258. package/dist/ui/variants/var_list-row.ts +34 -0
  259. package/dist/ui/variants/var_rule.ts +10 -0
  260. package/dist/ui/variants/var_skeleton-block.ts +12 -0
  261. package/dist/ui/variants/var_spinner-turning.ts +12 -0
  262. package/dist/ui/variants/var_stat-tile.ts +13 -0
  263. package/dist/ui/variants/var_tab-bar.ts +15 -0
  264. package/dist/ui/variants/var_tap-target.ts +25 -0
  265. package/dist/ui/variants.ts +64 -0
  266. package/dist/ui/wrappers.d.ts +4 -1
  267. package/dist/ui/wrappers.js +60 -20
  268. package/dist/ui/wrappers.tsx +426 -0
  269. package/package.json +8 -2
@@ -0,0 +1,46 @@
1
+ 'use client'
2
+
3
+ import type { ReactNode } from 'react'
4
+ import type { Subject } from '../drag.ts'
5
+
6
+ import { type Element, element, useSurface } from '../wrappers.tsx'
7
+ import { shown, uses, wires } from './hit-box.works.tsx'
8
+
9
+
10
+ export interface HitBoxProps {
11
+ readonly children: ReactNode
12
+ readonly gesture: string
13
+ readonly tuning?: Readonly<Record<string, number>>
14
+ readonly axis?: string
15
+ readonly grabs?: Subject | readonly Subject[]
16
+ readonly accepts: readonly string[]
17
+ readonly collects?: Subject | readonly Subject[]
18
+ readonly dwell?: number
19
+ readonly moves?: boolean
20
+ readonly onDrop?: () => void
21
+ readonly dispatch?: string
22
+ readonly input?: Readonly<Record<string, unknown>>
23
+ readonly invokeOn?: 'press' | 'shown'
24
+ }
25
+
26
+ export const HitBox: Element<'HitBox', HitBoxProps> = element(
27
+ 'HitBox',
28
+ (props: HitBoxProps, className) => {
29
+ const kept = uses(props)
30
+ const surface = useSurface({ className,
31
+ invokes: { ...(props.dispatch === undefined ? {} : { dispatch: props.dispatch }), ...(props.input === undefined ? {} : { input: props.input }), ...(props.invokeOn === undefined ? {} : { on: props.invokeOn }) },
32
+ holds: [
33
+ ...(kept.holds ?? []),
34
+ ],
35
+ ...(kept.animates === undefined ? {} : { animates: kept.animates }), })
36
+ const wired = wires(props, kept, surface)
37
+ return (
38
+ <div className={surface.className} {...surface.marks} {...surface.invokes} {...surface.a11y} {...(kept.on ?? {})} {...(wired.on ?? {})}>
39
+ {props.children}
40
+ <div className="e-hit-box-carried">{shown(props, surface, kept, wired).carried}</div>
41
+ </div>
42
+ )
43
+ },
44
+ 'e-hit-box',
45
+ {"tapTarget":"thread-tap-target"},
46
+ )
@@ -0,0 +1,576 @@
1
+ 'use client'
2
+
3
+ import { gestureFor, gestureNames, holds as declared, recognise, type GesturePhase, type PointerSample } from '../signals.ts'
4
+ import { useCallback, useEffect, useId, useRef, useState, useSyncExternalStore, type PointerEvent, type ReactNode } from 'react'
5
+ import { createPortal } from 'react-dom'
6
+
7
+ import {
8
+ accepted,
9
+ beginDrag,
10
+ claimGesture,
11
+ collect,
12
+ currentLayout,
13
+ dragSession,
14
+ dragStatusOf,
15
+ dwell,
16
+ endDrag,
17
+ innermostTarget,
18
+ moveDrag,
19
+ pullEdges,
20
+ registerTarget,
21
+ releaseGesture,
22
+ subscribeDrag,
23
+ type DragSession,
24
+ type DragStatus,
25
+ type DropTarget,
26
+ type Subject,
27
+ } from '../drag.ts'
28
+ import { measured, placed } from '../charts.tsx'
29
+ import { nameOf } from '../token.ts'
30
+ import { useSurface, useUi } from '../wrappers.tsx'
31
+ import type { HitBoxProps } from './hit-box.tsx'
32
+
33
+ export type Axis = 'x' | 'y' | 'both'
34
+
35
+ const held = (row: string): string => `var(${nameOf(row)})`
36
+
37
+ const TOUCH_ACTION: Record<Axis, string> = {
38
+ x: 'touch-down',
39
+ y: 'touch-across',
40
+ both: 'touch-own',
41
+ }
42
+
43
+ const wallClock = (): number => (typeof performance === 'object' ? performance.now() : Date.now()) / 1000
44
+
45
+ const settleMs = (): number => {
46
+ if (typeof document !== 'object') return 0
47
+ const said = getComputedStyle(document.documentElement).getPropertyValue('--motion-normal').trim()
48
+ const ms = /^([\d.]+)ms$/.exec(said)
49
+ if (ms !== null) return Number(ms[1])
50
+ const seconds = /^([\d.]+)s$/.exec(said)
51
+ return seconds === null ? 0 : Number(seconds[1]) * 1000
52
+ }
53
+
54
+ const SWING_AT = 220
55
+ const SWING_MAX = 12
56
+
57
+ const STIFFNESS = 180
58
+ const DAMPING = 14
59
+ const SETTLED = 0.05
60
+
61
+ const EMPTY: Readonly<Record<string, number>> = {}
62
+
63
+ interface Tracked {
64
+ readonly id: number
65
+ x: number
66
+ y: number
67
+ readonly startX: number
68
+ readonly startY: number
69
+ readonly startT: number
70
+ down: boolean
71
+ }
72
+
73
+ export function uses(props: HitBoxProps) {
74
+ const id = useId()
75
+ const { edgeScroll: pull } = useUi()
76
+
77
+ const claimed = gestureFor(props.gesture)
78
+ if (claimed === undefined) {
79
+ throw new Error(
80
+ `<HitBox gesture="${props.gesture}"> — nothing registered that gesture, so this region would ` +
81
+ `take the pointer and do nothing. Registered: ${gestureNames().join(' · ')}. Contribute one with defineGesture.`,
82
+ )
83
+ }
84
+
85
+ const [down, setDown] = useState(false)
86
+ const [phase, setPhase] = useState<GesturePhase>('idle')
87
+ const [produced, setProduced] = useState<Readonly<Record<string, number>>>({})
88
+
89
+ const released = useRef(Number.NEGATIVE_INFINITY)
90
+
91
+ const took = useRef<{
92
+ readonly width: number
93
+ readonly height: number
94
+ readonly gripX: number
95
+ readonly gripY: number
96
+ } | null>(null)
97
+
98
+ const [pointer, setPointer] = useState<{ readonly x: number; readonly y: number } | null>(null)
99
+
100
+ const [settling, setSettling] = useState(false)
101
+
102
+ const speed = useRef(0)
103
+ const moved = useRef(0)
104
+
105
+ const [lean, setLean] = useState(0)
106
+ const leanSpeed = useRef(0)
107
+ const lastTick = useRef(0)
108
+
109
+ const grabbed = useRef(props.grabs)
110
+ grabbed.current = props.grabs
111
+ const grabsKey = props.grabs === undefined ? '' : JSON.stringify(props.grabs)
112
+
113
+ const tuned = useRef(props.tuning)
114
+ tuned.current = props.tuning
115
+ const tuningKey = props.tuning === undefined ? '' : JSON.stringify(props.tuning)
116
+
117
+ const node = useRef<HTMLDivElement | null>(null)
118
+
119
+ const clockNow = useRef(0)
120
+
121
+ const wants = props.gesture === 'pinch' || props.gesture === 'rotate'
122
+ const synthesised = useRef<number | null>(null)
123
+
124
+ useEffect(() => {
125
+ const surfaceNode = node.current
126
+ if (!wants || surfaceNode === null) return undefined
127
+
128
+ const SPAN = 100
129
+
130
+ const put = (scale: number, radians: number): void => {
131
+ const box = surfaceNode.getBoundingClientRect()
132
+ const cx = box.left + box.width / 2
133
+ const cy = box.top + box.height / 2
134
+ const reach = (SPAN / 2) * scale
135
+ const dxr = Math.cos(radians) * reach
136
+ const dyr = Math.sin(radians) * reach
137
+
138
+ tracked.current.set(-1, { id: -1, x: cx + dxr, y: cy + dyr, startX: cx + SPAN / 2, startY: cy, startT: clockNow.current, down: true })
139
+ tracked.current.set(-2, { id: -2, x: cx - dxr, y: cy - dyr, startX: cx - SPAN / 2, startY: cy, startT: clockNow.current, down: true })
140
+ at.current = { x: cx, y: cy }
141
+ setDown(true)
142
+
143
+ if (synthesised.current !== null) clearTimeout(synthesised.current)
144
+ synthesised.current = window.setTimeout(() => {
145
+ tracked.current.delete(-1)
146
+ tracked.current.delete(-2)
147
+ setDown(false)
148
+ releaseGesture(id)
149
+ setPhase('idle')
150
+ setProduced(EMPTY)
151
+ }, 140)
152
+ }
153
+
154
+ let scale = 1
155
+ let radians = 0
156
+
157
+ const onWheel = (e: WheelEvent): void => {
158
+ if (!e.ctrlKey) return
159
+ e.preventDefault()
160
+ scale = Math.min(4, Math.max(0.25, scale * Math.exp(-e.deltaY / 100)))
161
+ put(scale, radians)
162
+ }
163
+
164
+ const onGesture = (e: Event): void => {
165
+ const held = e as Event & { scale?: number; rotation?: number }
166
+ e.preventDefault()
167
+ scale = held.scale ?? scale
168
+ radians = ((held.rotation ?? 0) * Math.PI) / 180
169
+ put(scale, radians)
170
+ }
171
+
172
+ surfaceNode.addEventListener('wheel', onWheel, { passive: false })
173
+ surfaceNode.addEventListener('gesturechange', onGesture)
174
+ return () => {
175
+ surfaceNode.removeEventListener('wheel', onWheel)
176
+ surfaceNode.removeEventListener('gesturechange', onGesture)
177
+ if (synthesised.current !== null) clearTimeout(synthesised.current)
178
+ }
179
+ }, [wants, id])
180
+
181
+ const tracked = useRef(new Map<number, Tracked>())
182
+ const at = useRef({ x: 0, y: 0 })
183
+ const lastFrame = useRef(0)
184
+
185
+ const key = props.accepts?.join(',') ?? ''
186
+ const status = useSyncExternalStore(
187
+ subscribeDrag,
188
+ useCallback(() => dragStatusOf(id, key === '' ? undefined : key.split(',')), [id, key]),
189
+ useCallback((): DragStatus => 'idle', []),
190
+ )
191
+
192
+ const carrying = status === 'carrying'
193
+ const raises = claimed.raises
194
+
195
+ return {
196
+ on: {},
197
+ pull,
198
+ animates: down || carrying || settling,
199
+ holds: [
200
+
201
+ ...(phase === 'active' || phase === 'done'
202
+ ? declared.filter((one) => raises.some((s) => s.name === one.id)).map((one) => one.id)
203
+ : []),
204
+ ...(carrying ? (['carrying'] as const) : []),
205
+ ...(status === 'over' && dwell() >= (props.dwell ?? 0) ? (['draggedOver'] as const) : []),
206
+ ...(status === 'refusing' ? (['refusing'] as const) : []),
207
+ ],
208
+ id,
209
+ claimed,
210
+ down,
211
+ setDown,
212
+ phase,
213
+ setPhase,
214
+ produced,
215
+ setProduced,
216
+ released,
217
+ took,
218
+ pointer,
219
+ setPointer,
220
+ settling,
221
+ setSettling,
222
+ speed,
223
+ moved,
224
+ lean,
225
+ setLean,
226
+ leanSpeed,
227
+ lastTick,
228
+ grabbed,
229
+ grabsKey,
230
+ tuned,
231
+ tuningKey,
232
+ node,
233
+ clockNow,
234
+ wants,
235
+ synthesised,
236
+ tracked,
237
+ at,
238
+ lastFrame,
239
+ key,
240
+ status,
241
+ carrying,
242
+ raises,
243
+ }
244
+ }
245
+
246
+ export function wires(props: HitBoxProps, kept: ReturnType<typeof uses>, surface: ReturnType<typeof useSurface>) {
247
+ const { dispatch: invoke } = useUi()
248
+ const { pull, id, claimed, down, setDown, phase, setPhase, produced, setProduced, released, took, pointer, setPointer, settling, setSettling, speed, moved, lean, setLean, leanSpeed, lastTick, grabbed, grabsKey, tuned, tuningKey, node, clockNow, wants, synthesised, tracked, at, lastFrame, key, status, carrying, raises } = kept
249
+
250
+ useEffect(() => {
251
+ if (!down) return undefined
252
+ const give = (e: globalThis.PointerEvent): void => {
253
+
254
+ const session = dragSession()
255
+ if (session?.from === id) {
256
+ const target = innermostTarget(currentLayout().hitBoxesAt(e.clientX, e.clientY))
257
+ const finished = endDrag()
258
+ if (target?.onDrop !== undefined && finished !== undefined && accepted(target.accepts, finished)) {
259
+ target.onDrop(finished)
260
+ }
261
+ }
262
+ tracked.current.clear()
263
+ setDown(false)
264
+ setPointer(null)
265
+ took.current = null
266
+ releaseGesture(id)
267
+
268
+ setPhase('idle')
269
+ }
270
+ const follow = (e: globalThis.PointerEvent): void => {
271
+ advance(e.pointerId, e.clientX, e.clientY)
272
+ }
273
+ window.addEventListener('pointermove', follow)
274
+ window.addEventListener('pointerup', give)
275
+ window.addEventListener('pointercancel', give)
276
+ return () => {
277
+ window.removeEventListener('pointermove', follow)
278
+ window.removeEventListener('pointerup', give)
279
+ window.removeEventListener('pointercancel', give)
280
+ }
281
+ }, [down, id])
282
+
283
+ useEffect(
284
+ () => () => {
285
+ releaseGesture(id)
286
+ if (dragSession()?.from === id) endDrag()
287
+ },
288
+ [id],
289
+ )
290
+
291
+ const onDrop = props.onDrop
292
+ const named = props.dispatch
293
+ const carriedInput = props.input
294
+ const collects = props.collects
295
+ useEffect(() => {
296
+ if (props.accepts === undefined) return undefined
297
+ const target: DropTarget = {
298
+ id,
299
+ accepts: key.split(','),
300
+ ...(props.dwell === undefined ? {} : { dwell: props.dwell }),
301
+ ...(collects === undefined ? {} : { collects: () => collects }),
302
+ ...(onDrop === undefined && named === undefined
303
+ ? {}
304
+ : {
305
+ onDrop: (finished: DragSession) => {
306
+ if (onDrop !== undefined) onDrop()
307
+ if (named === undefined) return
308
+ if (invoke === undefined) throw new Error(`\`${named}\` cannot be invoked from here — no dispatch is wired`)
309
+ void invoke(named, { ...(carriedInput ?? {}), ...(finished.subject[0] ?? {}) })
310
+ },
311
+ }),
312
+ }
313
+ return registerTarget(target)
314
+ }, [id, key, props.accepts, props.dwell, collects, onDrop, named, carriedInput, invoke])
315
+
316
+ const t = surface.t
317
+ clockNow.current = t
318
+ useEffect(() => {
319
+ if (!down) return
320
+ const dt = Math.max(0, t - lastFrame.current)
321
+ lastFrame.current = t
322
+
323
+ const pointers: PointerSample[] = [...tracked.current.values()].map((p) => ({
324
+ id: p.id,
325
+ x: p.x,
326
+ y: p.y,
327
+ startX: p.startX,
328
+ startY: p.startY,
329
+ held: t - p.startT,
330
+ down: p.down,
331
+ }))
332
+
333
+ const result = recognise(props.gesture, {
334
+ pointers,
335
+ t,
336
+ params: tuned.current ?? {},
337
+
338
+ sinceRelease: wallClock() - released.current,
339
+ })
340
+
341
+ const takesPointer = grabbed.current !== undefined
342
+ const mine = result.phase === 'idle' || !takesPointer ? true : claimGesture(id)
343
+ setPhase(mine ? result.phase : 'idle')
344
+
345
+ setProduced(mine && result.values !== undefined ? result.values : EMPTY)
346
+ if (!mine) return
347
+
348
+ const session = dragSession()
349
+
350
+ if (result.phase !== 'idle' && grabbed.current !== undefined && session === undefined) {
351
+ beginDrag({ gesture: props.gesture, subject: grabbed.current, from: id, x: at.current.x, y: at.current.y, t })
352
+ return
353
+ }
354
+
355
+ if (session === undefined || session.from !== id) return
356
+
357
+ const target = innermostTarget(currentLayout().hitBoxesAt(at.current.x, at.current.y))
358
+ moveDrag({ x: at.current.x, y: at.current.y, ...(target === undefined ? {} : { over: target.id }), t })
359
+
360
+ if (target?.collects !== undefined && dwell() >= (target.dwell ?? 0)) collect(target.collects())
361
+
362
+ pullEdges(at.current.x, at.current.y, dt, pull ?? {})
363
+
364
+ }, [down, t, pointer, id, props.gesture, grabsKey, tuningKey, pull])
365
+
366
+ useEffect(() => {
367
+ if (phase !== 'done') return undefined
368
+ const frame = requestAnimationFrame(() => {
369
+ releaseGesture(id)
370
+ setPhase('idle')
371
+ setProduced(EMPTY)
372
+ })
373
+ return () => cancelAnimationFrame(frame)
374
+ }, [phase, id])
375
+
376
+ const onPointerDown = (e: PointerEvent<HTMLDivElement>): void => {
377
+
378
+ if (grabbed.current !== undefined) {
379
+ e.currentTarget.setPointerCapture?.(e.pointerId)
380
+
381
+ e.preventDefault()
382
+ }
383
+
384
+ const box = e.currentTarget.getBoundingClientRect()
385
+ setPointer({ x: e.clientX, y: e.clientY })
386
+ took.current =
387
+ box.width > 0 && box.height > 0
388
+ ? { width: box.width, height: box.height, gripX: e.clientX - box.left, gripY: e.clientY - box.top }
389
+ : null
390
+ speed.current = 0
391
+ moved.current = wallClock()
392
+ tracked.current.set(e.pointerId, { id: e.pointerId, x: e.clientX, y: e.clientY, startX: e.clientX, startY: e.clientY, startT: t, down: true })
393
+ at.current = { x: e.clientX, y: e.clientY }
394
+ lastFrame.current = t
395
+ setDown(true)
396
+ }
397
+
398
+ const advance = (pointerId: number, x: number, y: number): void => {
399
+ const p = tracked.current.get(pointerId)
400
+ if (p === undefined) return
401
+ if (p.x === x && p.y === y) return
402
+ const e = { pointerId, clientX: x, clientY: y }
403
+
404
+ const at2 = wallClock()
405
+ const dt = Math.max(1 / 240, at2 - moved.current)
406
+ const raw = (e.clientX - p.x) / dt
407
+ speed.current = speed.current * 0.7 + raw * 0.3
408
+ moved.current = at2
409
+
410
+ p.x = e.clientX
411
+ p.y = e.clientY
412
+ at.current = { x: e.clientX, y: e.clientY }
413
+ setPointer({ x: e.clientX, y: e.clientY })
414
+ }
415
+
416
+ const onPointerMove = (e: PointerEvent<HTMLDivElement>): void => {
417
+ advance(e.pointerId, e.clientX, e.clientY)
418
+ }
419
+
420
+ const finish = (e: PointerEvent<HTMLDivElement>, cancelled: boolean): void => {
421
+ const p = tracked.current.get(e.pointerId)
422
+ if (p !== undefined) {
423
+
424
+ p.x = e.clientX
425
+ p.y = e.clientY
426
+ p.down = false
427
+ at.current = { x: e.clientX, y: e.clientY }
428
+ }
429
+
430
+ const session = dragSession()
431
+ if (session?.from === id) {
432
+
433
+ const target = cancelled ? undefined : innermostTarget(currentLayout().hitBoxesAt(e.clientX, e.clientY))
434
+ const finished = endDrag()
435
+ if (target?.onDrop !== undefined && finished !== undefined && accepted(target.accepts, finished)) {
436
+ target.onDrop(finished)
437
+ }
438
+
439
+ releaseGesture(id)
440
+
441
+ speed.current = 0
442
+ }
443
+
444
+ const last = p !== undefined && tracked.current.size === 1 && !cancelled ? p : undefined
445
+ let completed = false
446
+ if (last !== undefined) {
447
+ const result = recognise(props.gesture, {
448
+ pointers: [
449
+ { id: last.id, x: last.x, y: last.y, startX: last.startX, startY: last.startY, held: t - last.startT, down: false },
450
+ ],
451
+ t,
452
+ params: props.tuning ?? {},
453
+ sinceRelease: wallClock() - released.current,
454
+ })
455
+ if (result.phase !== 'idle' && claimGesture(id)) {
456
+ setPhase(result.phase)
457
+ setProduced(result.values ?? EMPTY)
458
+ completed = true
459
+ }
460
+ }
461
+
462
+ tracked.current.delete(e.pointerId)
463
+ if (tracked.current.size === 0) {
464
+ setDown(false)
465
+
466
+ releaseGesture(id)
467
+ setPointer(null)
468
+ took.current = null
469
+ if (!completed) setPhase('idle')
470
+
471
+ if (!cancelled) released.current = wallClock()
472
+ }
473
+ }
474
+
475
+ const session = dragSession()
476
+ const mine = carrying && session !== undefined && session.from === id
477
+
478
+ useEffect(() => {
479
+ if (!mine && !settling) {
480
+ lastTick.current = t
481
+ return
482
+ }
483
+ const step = Math.min(1 / 20, Math.max(0, t - lastTick.current))
484
+ lastTick.current = t
485
+ if (step === 0) return
486
+
487
+ const target = mine ? Math.max(-SWING_MAX, Math.min(SWING_MAX, speed.current / SWING_AT)) : 0
488
+ const force = -STIFFNESS * (lean - target) - DAMPING * leanSpeed.current
489
+ const next = lean + leanSpeed.current * step
490
+ leanSpeed.current += force * step
491
+
492
+ if (!mine && Math.abs(next) < SETTLED && Math.abs(leanSpeed.current) < SETTLED) {
493
+ leanSpeed.current = 0
494
+ if (lean !== 0) setLean(0)
495
+ return
496
+ }
497
+ setLean(next)
498
+ }, [t, mine, settling, lean])
499
+
500
+ useEffect(() => {
501
+ if (mine) {
502
+ setSettling(true)
503
+ return undefined
504
+ }
505
+ if (!settling) return undefined
506
+
507
+ if (Math.abs(lean) < SETTLED && Math.abs(leanSpeed.current) < SETTLED) {
508
+ const timer = setTimeout(() => setSettling(false), settleMs())
509
+ return () => clearTimeout(timer)
510
+ }
511
+ return undefined
512
+ }, [mine, settling, lean])
513
+
514
+ const swing = lean
515
+
516
+ useEffect(() => {
517
+ if (!mine) return undefined
518
+ const root = document.documentElement
519
+ const was = root.style.userSelect
520
+ root.style.userSelect = held('picking-off')
521
+ return () => {
522
+ root.style.userSelect = was
523
+ }
524
+ }, [mine])
525
+
526
+ const travels = props.moves !== false
527
+
528
+ const flying = mine && travels && took.current !== null && pointer !== null
529
+
530
+ const layer =
531
+ flying && took.current !== null && pointer !== null ? (
532
+
533
+ <div
534
+ aria-hidden
535
+ className={placed('hit-box', 'carried')}
536
+ style={measured({
537
+ width: `${String(took.current.width)}px`,
538
+ height: `${String(took.current.height)}px`,
539
+ carry:
540
+ `translate3d(${String(pointer.x - took.current.gripX)}px, ${String(pointer.y - took.current.gripY)}px, 0)` +
541
+ ' var(--transform-carried, scale(1)) var(--transform-carried-turn, rotate(0deg))' +
542
+ ` rotate(${swing.toFixed(2)}deg)`,
543
+ grip: `${took.current.gripX.toFixed(1)}px ${took.current.gripY.toFixed(1)}px`,
544
+ })}
545
+ >
546
+ {props.children}
547
+ </div>
548
+ ) : null
549
+
550
+ return {
551
+ on: {
552
+ ref: node,
553
+ 'data-hitbox': id,
554
+ style: measured({
555
+ touch: held(props.grabs === undefined ? 'touch-page' : (TOUCH_ACTION[(props.axis ?? 'both') as Axis] ?? 'touch-own')),
556
+ pick: held(props.grabs === undefined ? 'picking-open' : 'picking-off'),
557
+ shown: held(flying ? 'visibility-hidden' : 'visibility-shown'),
558
+ flow: held(flying ? 'position-lifted' : 'position-flow'),
559
+ }),
560
+ onPointerDown,
561
+ onPointerMove,
562
+ onPointerUp: (e: PointerEvent<HTMLDivElement>) => finish(e, false),
563
+ onPointerCancel: (e: PointerEvent<HTMLDivElement>) => finish(e, true),
564
+ },
565
+ layer,
566
+ }
567
+ }
568
+
569
+ export function shown(
570
+ _props: HitBoxProps,
571
+ _surface: ReturnType<typeof useSurface>,
572
+ _kept: ReturnType<typeof uses>,
573
+ wired: ReturnType<typeof wires>,
574
+ ): Readonly<Record<string, ReactNode>> {
575
+ return { carried: wired.layer === null ? null : createPortal(wired.layer, document.body) }
576
+ }
@@ -6,4 +6,4 @@ export const Image = element('Image', (props, className) => {
6
6
  const surface = useSurface({ className: roleClass(undefined, className),
7
7
  invokes: { ...(props.dispatch === undefined ? {} : { dispatch: props.dispatch }), ...(props.input === undefined ? {} : { input: props.input }), ...(props.invokeOn === undefined ? {} : { on: props.invokeOn }) }, });
8
8
  return (_jsx("img", { className: surface.className, ...surface.marks, ...surface.invokes, ...surface.a11y, src: props.src, alt: surface.text(props.altKey) }));
9
- }, 'e-image');
9
+ }, 'e-image', { "framed": "thread-framed" });
@@ -0,0 +1,28 @@
1
+ 'use client'
2
+
3
+ import type { MessageKey } from '../content.tsx'
4
+ import { roleClass } from '../fonts.ts'
5
+
6
+ import { type Element, element, useSurface } from '../wrappers.tsx'
7
+
8
+
9
+ export interface ImageProps {
10
+ readonly src: string
11
+ readonly altKey: MessageKey
12
+ readonly dispatch?: string
13
+ readonly input?: Readonly<Record<string, unknown>>
14
+ readonly invokeOn?: 'press' | 'shown'
15
+ }
16
+
17
+ export const Image: Element<'Image', ImageProps> = element(
18
+ 'Image',
19
+ (props: ImageProps, className) => {
20
+ const surface = useSurface({ className: roleClass(undefined, className),
21
+ invokes: { ...(props.dispatch === undefined ? {} : { dispatch: props.dispatch }), ...(props.input === undefined ? {} : { input: props.input }), ...(props.invokeOn === undefined ? {} : { on: props.invokeOn }) }, })
22
+ return (
23
+ <img className={surface.className} {...surface.marks} {...surface.invokes} {...surface.a11y} src={props.src} alt={surface.text(props.altKey)} />
24
+ )
25
+ },
26
+ 'e-image',
27
+ {"framed":"thread-framed"},
28
+ )
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { roleClass } from "../fonts.js";
4
4
  import { element, useSurface } from "../wrappers.js";
5
- import { Each } from "./each.js";
5
+ import { Each } from "../elements/each.js";
6
6
  import { drawing, lines, uses } from "./line-chart.works.js";
7
7
  export const LineChart = element('LineChart', (props, className) => {
8
8
  const kept = uses(props);
@@ -13,4 +13,4 @@ export const LineChart = element('LineChart', (props, className) => {
13
13
  ],
14
14
  ...(kept.animates === undefined ? {} : { animates: kept.animates }), });
15
15
  return (_jsx("svg", { className: surface.className, ...surface.marks, ...surface.invokes, ...surface.a11y, ...(kept.on ?? {}), role: "img", ...(props.titleKey === undefined ? {} : { 'aria-label': surface.text(props.titleKey) }), children: _jsx(Each, { rows: lines(props, kept), by: 'group', children: (each0, at) => (_jsx("polyline", { ...drawing(props, kept, each0, at) })) }) }));
16
- }, 'e-line-chart');
16
+ }, 'e-line-chart', { "card": "thread-card" });