@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.
- 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.ts +317 -0
- package/dist/ui/attach.js +13 -4
- package/dist/ui/attach.ts +613 -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 +10 -1
- package/dist/ui/compositions.js +262 -23
- package/dist/ui/compositions.ts +640 -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.ts +79 -0
- package/dist/ui/generators/elements.d.ts +1 -11
- package/dist/ui/generators/elements.js +43 -13
- package/dist/ui/generators/elements.ts +794 -0
- package/dist/ui/generators/i18n.ts +301 -0
- package/dist/ui/generators/icons.ts +214 -0
- package/dist/ui/generators/react.js +10 -3
- package/dist/ui/generators/react.ts +268 -0
- package/dist/ui/generators/routes.d.ts +1 -0
- package/dist/ui/generators/routes.js +89 -69
- package/dist/ui/generators/routes.ts +739 -0
- package/dist/ui/generators/show.ts +259 -0
- package/dist/ui/generators/styles.js +1 -1
- package/dist/ui/generators/styles.ts +670 -0
- package/dist/ui/generators/variants.ts +205 -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/makers/message.ts +292 -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.ts +206 -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 +17 -8
- package/dist/ui/screens.ts +142 -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.ts +235 -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
|
@@ -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
|
+
}
|
package/dist/ui/door.ts
ADDED
|
@@ -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
|
+
}
|