@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,55 @@
1
+ import { z } from 'zod'
2
+
3
+ export const ConsentCategorySchema = z
4
+ .strictObject({
5
+ id: z
6
+ .string()
7
+ .regex(/^[a-z][a-z0-9_]*$/, 'a consent category id is lower_snake_case')
8
+ .describe('The id recorded in `thread.consent.categories`.'),
9
+ labelKey: z.string().min(1).describe('The category’s name, as a message key. Never a sentence.'),
10
+ describeKey: z.string().min(1).describe('What is collected under it, as a message key.'),
11
+
12
+ required: z.boolean().default(false).describe('True where the app cannot function without it.'),
13
+ })
14
+ .describe('One thing an app may ask permission to collect.')
15
+
16
+ export type ConsentCategory = z.infer<typeof ConsentCategorySchema>
17
+
18
+ const TELEMETRY: ConsentCategory = ConsentCategorySchema.parse({
19
+ id: 'telemetry',
20
+ labelKey: 'consent.telemetry.label',
21
+ describeKey: 'consent.telemetry.describe',
22
+ })
23
+
24
+ const REGISTRY = new Map<string, ConsentCategory>([[TELEMETRY.id, TELEMETRY]])
25
+
26
+ export function defineConsentCategory(category: ConsentCategory): void {
27
+ const parsed = ConsentCategorySchema.parse(category)
28
+ if (REGISTRY.has(parsed.id)) throw new Error(`consent category "${parsed.id}" is already registered`)
29
+ REGISTRY.set(parsed.id, parsed)
30
+ }
31
+
32
+ export function consentCategories(): readonly ConsentCategory[] {
33
+ return [...REGISTRY.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
34
+ }
35
+
36
+ export function consentCategory(id: string): ConsentCategory | undefined {
37
+ return REGISTRY.get(id)
38
+ }
39
+
40
+ export const ConsentDecisionSchema = z
41
+ .strictObject({
42
+ decision: z.enum(['accepted', 'declined', 'withdrawn']).describe('Withdrawal is a decision and is recorded like any other.'),
43
+
44
+ accepted: z.array(z.string()).describe('The category ids accepted.'),
45
+ offered: z.array(z.string()).describe('Every category shown at that moment, accepted or not.'),
46
+ })
47
+ .describe('One consent decision, with the choice set as it stood when it was made.')
48
+
49
+ export type ConsentDecision = z.infer<typeof ConsentDecisionSchema>
50
+
51
+ export function consentAllows(decision: ConsentDecision | undefined, categoryId: string): boolean {
52
+ if (consentCategory(categoryId)?.required === true) return true
53
+ if (decision === undefined || decision.decision !== 'accepted') return false
54
+ return decision.accepted.includes(categoryId)
55
+ }
@@ -0,0 +1,39 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useSyncExternalStore, type ReactNode } from 'react'
4
+ import { formatValue, runValidator, validatorFor } from './validators.ts'
5
+ import { ICON_TARGET, strokeFor } from './icons.ts'
6
+
7
+ import { element, useSurface, useUi, useVisibility } from './wrappers.tsx'
8
+
9
+ export type MessageKey = string
10
+
11
+ export interface ValueProps {
12
+ readonly validator: string
13
+ readonly value: unknown
14
+
15
+ readonly context?: Readonly<Record<string, unknown>>
16
+ }
17
+
18
+ export function renderValue(
19
+ { validator, value, context }: ValueProps,
20
+ surface: ReturnType<typeof useSurface>,
21
+ ): ReactNode {
22
+ const entry = validatorFor(validator)
23
+ const result = runValidator(validator, value, context ?? {})
24
+
25
+ if (!result.ok) {
26
+ return (
27
+ <span className={surface.className} {...surface.marks} data-invalid="true">
28
+ {surface.text(result.errorKey)}
29
+ </span>
30
+ )
31
+ }
32
+
33
+ return (
34
+ <span className={surface.className} {...surface.marks} data-validator={entry.name}>
35
+ {formatValue(validator, result.value, context ?? {})}
36
+ </span>
37
+ )
38
+ }
39
+
@@ -0,0 +1,27 @@
1
+ export const CONTRAST_LARGE = 3
2
+
3
+ function channel(value: number): number {
4
+ const c = value / 255
5
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
6
+ }
7
+
8
+ export function luminance(hex: string): number {
9
+ const clean = hex.replace('#', '')
10
+ const full =
11
+ clean.length === 3
12
+ ? clean
13
+ .split('')
14
+ .map((c) => c + c)
15
+ .join('')
16
+ : clean.slice(0, 6)
17
+ const num = Number.parseInt(full, 16)
18
+ const r = channel((num >> 16) & 255)
19
+ const g = channel((num >> 8) & 255)
20
+ const b = channel(num & 255)
21
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b
22
+ }
23
+
24
+ export function contrast(a: string, b: string): number {
25
+ const [light, dark] = [luminance(a), luminance(b)].sort((x, y) => y - x) as [number, number]
26
+ return (light + 0.05) / (dark + 0.05)
27
+ }
@@ -0,0 +1,30 @@
1
+ 'use client'
2
+
3
+ import { useRef, useState } from 'react'
4
+ import { acceptsKeystroke, applyMask, entryConstraints, evaluateMoney, runValidator } from './validators.ts'
5
+
6
+ import { ITEM_ATTR, useRovingFocus } from './roving-focus.ts'
7
+ import { element, pressOf, useSurface, usePress } from './wrappers.tsx'
8
+ import type { MessageKey } from './content.tsx'
9
+ import type { Dispatching } from './checked.ts'
10
+
11
+ export interface PressProps {
12
+
13
+ readonly dispatch?: [keyof Dispatching] extends [never] ? string : keyof Dispatching & string
14
+
15
+ readonly input?: Readonly<Record<string, unknown>>
16
+
17
+ readonly navigateTo?: string
18
+
19
+ readonly onPress?: () => void
20
+ }
21
+
22
+ export interface Option {
23
+ readonly value: string
24
+ readonly labelKey: MessageKey
25
+ }
26
+
27
+ export { Menu, type MenuProps } from './elements/menu.tsx'
28
+
29
+ export { type MenuItem } from './elements/menu.works.tsx'
30
+
@@ -0,0 +1,22 @@
1
+ 'use client'
2
+
3
+ import { useId, useRef, useState, type ReactNode } from 'react'
4
+
5
+ import { ITEM_ATTR, useRovingFocus } from './roving-focus.ts'
6
+ import { element, useSurface } from './wrappers.tsx'
7
+ import type { MessageKey } from './content.tsx'
8
+
9
+ export interface TabItem {
10
+ readonly id: string
11
+ readonly labelKey: MessageKey
12
+ readonly disabled?: boolean
13
+ readonly content: ReactNode
14
+ }
15
+
16
+ export interface AccordionItem {
17
+ readonly id: string
18
+ readonly labelKey: MessageKey
19
+ readonly disabled?: boolean
20
+ readonly content: ReactNode
21
+ }
22
+
@@ -0,0 +1,87 @@
1
+ 'use client'
2
+
3
+ import { useEffect, type RefObject } from 'react'
4
+
5
+ export interface Layer {
6
+ readonly ref: RefObject<HTMLElement | null>
7
+ readonly dismissOnEscape: boolean
8
+ readonly dismissOnOutside: boolean
9
+ readonly onDismiss: () => void
10
+ }
11
+
12
+ const layers: Layer[] = []
13
+
14
+ function ordered(): readonly Layer[] {
15
+ return layers
16
+ .filter((layer) => layer.ref.current !== null)
17
+ .sort((a, b) => {
18
+ const first = a.ref.current as HTMLElement
19
+ const second = b.ref.current as HTMLElement
20
+ if (first === second) return 0
21
+
22
+ return (first.compareDocumentPosition(second) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0 ? -1 : 1
23
+ })
24
+ }
25
+
26
+ function target(kind: 'escape' | 'outside'): Layer | undefined {
27
+ const open = ordered()
28
+ for (let i = open.length - 1; i >= 0; i -= 1) {
29
+ const layer = open[i] as Layer
30
+ if (kind === 'escape' ? layer.dismissOnEscape : layer.dismissOnOutside) return layer
31
+ }
32
+ return undefined
33
+ }
34
+
35
+ export function useDismissLayer(
36
+ ref: RefObject<HTMLElement | null>,
37
+ options: {
38
+ readonly active: boolean
39
+ readonly dismissOnEscape: boolean
40
+ readonly dismissOnOutside: boolean
41
+ readonly onDismiss: () => void
42
+ },
43
+ ): void {
44
+ const { active, dismissOnEscape, dismissOnOutside, onDismiss } = options
45
+
46
+ useEffect(() => {
47
+ if (!active) return
48
+ const layer: Layer = { ref, dismissOnEscape, dismissOnOutside, onDismiss }
49
+ layers.push(layer)
50
+
51
+ const onKeyDown = (event: KeyboardEvent): void => {
52
+ if (event.key !== 'Escape') return
53
+ if (target('escape') !== layer) return
54
+ event.preventDefault()
55
+
56
+ event.stopPropagation()
57
+ layer.onDismiss()
58
+ }
59
+
60
+ const onPointerDown = (event: Event): void => {
61
+ if (target('outside') !== layer) return
62
+ const root = ref.current
63
+ const node = event.target
64
+ if (root === null || !(node instanceof Node)) return
65
+ if (root.contains(node)) return
66
+
67
+ event.preventDefault()
68
+ event.stopPropagation()
69
+ layer.onDismiss()
70
+ }
71
+
72
+ document.addEventListener('keydown', onKeyDown, true)
73
+ document.addEventListener('pointerdown', onPointerDown, true)
74
+
75
+ return () => {
76
+ document.removeEventListener('keydown', onKeyDown, true)
77
+ document.removeEventListener('pointerdown', onPointerDown, true)
78
+ const at = layers.indexOf(layer)
79
+
80
+ if (at !== -1) layers.splice(at, 1)
81
+ }
82
+ }, [active, dismissOnEscape, dismissOnOutside, onDismiss, ref])
83
+ }
84
+
85
+ export function openLayers(): number {
86
+ return layers.length
87
+ }
@@ -0,0 +1,140 @@
1
+
2
+ export const PRESENTATIONS = ['page', 'sheet', 'popover', 'overlay', 'toast'] as const
3
+ export type Presentation = (typeof PRESENTATIONS)[number]
4
+
5
+ export const PRESENTATION_BEHAVIOUR: Readonly<Record<Presentation, {
6
+ readonly portal: boolean
7
+ readonly backdrop: boolean
8
+ readonly trapFocus: boolean
9
+ readonly returnFocus: boolean
10
+ readonly dismissOnEscape: boolean
11
+ readonly dismissOnOutside: boolean
12
+ }>> = {
13
+ page: { portal: false, backdrop: false, trapFocus: false, returnFocus: false, dismissOnEscape: false, dismissOnOutside: false },
14
+ sheet: { portal: true, backdrop: true, trapFocus: true, returnFocus: true, dismissOnEscape: true, dismissOnOutside: true },
15
+ overlay: { portal: true, backdrop: true, trapFocus: true, returnFocus: true, dismissOnEscape: true, dismissOnOutside: true },
16
+
17
+ popover: { portal: true, backdrop: false, trapFocus: false, returnFocus: true, dismissOnEscape: true, dismissOnOutside: true },
18
+
19
+ toast: { portal: true, backdrop: false, trapFocus: false, returnFocus: false, dismissOnEscape: false, dismissOnOutside: false },
20
+ }
21
+
22
+ export interface ClockSubscriber {
23
+
24
+ (t: number): void
25
+ }
26
+
27
+ export class Ticker {
28
+ #subscribers = new Set<() => void>()
29
+ #stop: (() => void) | undefined
30
+ readonly #every: (fn: () => void) => () => void
31
+
32
+ constructor(every?: (fn: () => void) => () => void) {
33
+ this.#every =
34
+ every ??
35
+ ((fn) => {
36
+ const id = setInterval(fn, 1000)
37
+ return () => clearInterval(id)
38
+ })
39
+ }
40
+
41
+ get running(): boolean {
42
+ return this.#stop !== undefined
43
+ }
44
+
45
+ subscribe(fn: () => void): () => void {
46
+ this.#subscribers.add(fn)
47
+ if (this.#stop === undefined) this.#stop = this.#every(() => this.tick())
48
+ return () => {
49
+ this.#subscribers.delete(fn)
50
+
51
+ if (this.#subscribers.size === 0) {
52
+ this.#stop?.()
53
+ this.#stop = undefined
54
+ }
55
+ }
56
+ }
57
+
58
+ tick(): void {
59
+ for (const fn of this.#subscribers) fn()
60
+ }
61
+ }
62
+
63
+ export class Clock {
64
+ #subscribers = new Set<ClockSubscriber>()
65
+ #visible = true
66
+ #running = false
67
+ #t = 0
68
+ #ticks = 0
69
+ readonly #schedule: (fn: () => void) => void
70
+
71
+ constructor(schedule?: (fn: () => void) => void) {
72
+ this.#schedule =
73
+ schedule ??
74
+ ((fn) => {
75
+ if (typeof requestAnimationFrame === 'function') requestAnimationFrame(() => fn())
76
+ else setTimeout(fn, 16)
77
+ })
78
+ }
79
+
80
+ get ticks(): number {
81
+ return this.#ticks
82
+ }
83
+
84
+ get running(): boolean {
85
+ return this.#running
86
+ }
87
+
88
+ subscribe(fn: ClockSubscriber): () => void {
89
+ this.#subscribers.add(fn)
90
+ this.#maybeStart()
91
+ return () => {
92
+ this.#subscribers.delete(fn)
93
+ if (this.#subscribers.size === 0) this.#running = false
94
+ }
95
+ }
96
+
97
+ setVisible(visible: boolean): void {
98
+ this.#visible = visible
99
+ if (visible) this.#maybeStart()
100
+ else this.#running = false
101
+ }
102
+
103
+ tick(dt: number): void {
104
+ if (!this.#visible) return
105
+ this.#t += dt
106
+ this.#ticks += 1
107
+ for (const fn of this.#subscribers) fn(this.#t)
108
+ }
109
+
110
+ #maybeStart(): void {
111
+ if (this.#running || !this.#visible || this.#subscribers.size === 0) return
112
+ this.#running = true
113
+ const loop = (): void => {
114
+ if (!this.#running) return
115
+ this.tick(1 / 60)
116
+ this.#schedule(loop)
117
+ }
118
+ this.#schedule(loop)
119
+ }
120
+ }
121
+
122
+ export interface Surface {
123
+ readonly className?: string
124
+
125
+ readonly holds: readonly string[]
126
+
127
+ readonly marks: Readonly<Record<string, boolean>>
128
+
129
+ readonly invokes: Readonly<Record<string, unknown>>
130
+
131
+ readonly marked: (signal: string) => boolean
132
+
133
+ readonly text: (key: string, params?: Readonly<Record<string, unknown>>) => string
134
+
135
+ readonly a11y: Readonly<Record<string, string | boolean | number>>
136
+
137
+ readonly measures: Readonly<Record<string, string | number>>
138
+
139
+ readonly t: number
140
+ }