@adea-ai/ui 0.94.0 → 0.95.1
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/README.md +43 -1
- package/dist/NOTICE +10 -0
- package/dist/components/composites/action-button/action-button.d.ts.map +1 -1
- package/dist/components/composites/action-button/action-button.js +2 -0
- package/dist/components/composites/action-button/action-button.js.map +1 -1
- package/dist/components/composites/scene-controls/index.d.ts +3 -0
- package/dist/components/composites/scene-controls/index.d.ts.map +1 -0
- package/dist/components/composites/scene-controls/index.js +2 -0
- package/dist/components/composites/scene-controls/scene-controls.d.ts +41 -0
- package/dist/components/composites/scene-controls/scene-controls.d.ts.map +1 -0
- package/dist/components/composites/scene-controls/scene-controls.js +291 -0
- package/dist/components/composites/scene-controls/scene-controls.js.map +1 -0
- package/dist/components/ui/button/button.d.ts +2 -1
- package/dist/components/ui/button/button.d.ts.map +1 -1
- package/dist/components/ui/button/button.js +9 -3
- package/dist/components/ui/button/button.js.map +1 -1
- package/dist/components/ui/dialog/dialog.d.ts.map +1 -1
- package/dist/components/ui/dialog/dialog.js +5 -1
- package/dist/components/ui/dialog/dialog.js.map +1 -1
- package/dist/components/ui/modal-dialog/modal-dialog.d.ts +6 -0
- package/dist/components/ui/modal-dialog/modal-dialog.d.ts.map +1 -1
- package/dist/components/ui/modal-dialog/modal-dialog.js +4 -0
- package/dist/components/ui/modal-dialog/modal-dialog.js.map +1 -1
- package/dist/components/ui/sheet/sheet.d.ts +8 -1
- package/dist/components/ui/sheet/sheet.d.ts.map +1 -1
- package/dist/components/ui/sheet/sheet.js +43 -19
- package/dist/components/ui/sheet/sheet.js.map +1 -1
- package/dist/components/ui/toggle/toggle.d.ts +1 -1
- package/dist/components/ui/toggle/toggle.d.ts.map +1 -1
- package/dist/components/ui/toggle/toggle.js +3 -1
- package/dist/components/ui/toggle/toggle.js.map +1 -1
- package/dist/components/ui/toggle-group/toggle-group.d.ts.map +1 -1
- package/dist/components/ui/toggle-group/toggle-group.js +20 -7
- package/dist/components/ui/toggle-group/toggle-group.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/lib/tokens.d.ts.map +1 -1
- package/dist/lib/tokens.js +10 -0
- package/dist/lib/tokens.js.map +1 -1
- package/dist/lib/utils.d.ts.map +1 -1
- package/dist/lib/utils.js +2 -0
- package/dist/lib/utils.js.map +1 -1
- package/dist/lib/variants.d.ts +5 -2
- package/dist/lib/variants.d.ts.map +1 -1
- package/dist/lib/variants.js +7 -3
- package/dist/lib/variants.js.map +1 -1
- package/dist/lint/index.d.ts.map +1 -1
- package/dist/lint.js +2 -1
- package/dist/lint.js.map +1 -1
- package/dist/r/registry.json +24 -0
- package/dist/r/scene-controls.json +24 -0
- package/dist/r/src/components/composites/action-button/action-button.tsx +2 -0
- package/dist/r/src/components/composites/scene-controls/index.ts +7 -0
- package/dist/r/src/components/composites/scene-controls/scene-controls.tsx +282 -0
- package/dist/r/src/components/ui/button/button.tsx +20 -2
- package/dist/r/src/components/ui/dialog/dialog.tsx +6 -1
- package/dist/r/src/components/ui/modal-dialog/modal-dialog.tsx +20 -0
- package/dist/r/src/components/ui/sheet/sheet.tsx +37 -4
- package/dist/r/src/components/ui/toggle/toggle.tsx +2 -0
- package/dist/r/src/components/ui/toggle-group/toggle-group.tsx +18 -1
- package/dist/r/src/lib/tokens.ts +10 -0
- package/dist/r/src/lib/utils.ts +2 -0
- package/dist/r/src/lib/variants.ts +5 -2
- package/dist/r/src/styles/base.css +29 -9
- package/dist/r/src/styles/theme.css +8 -2
- package/package.json +2 -1
- package/registry.json +24 -0
- package/src/components/composites/action-button/action-button.tsx +2 -0
- package/src/components/composites/scene-controls/index.ts +7 -0
- package/src/components/composites/scene-controls/scene-controls.tsx +282 -0
- package/src/components/ui/button/button.tsx +20 -2
- package/src/components/ui/dialog/dialog.tsx +6 -1
- package/src/components/ui/modal-dialog/modal-dialog.tsx +20 -0
- package/src/components/ui/sheet/sheet.tsx +37 -4
- package/src/components/ui/toggle/toggle.tsx +2 -0
- package/src/components/ui/toggle-group/toggle-group.tsx +18 -1
- package/src/index.ts +1 -0
- package/src/lib/tokens.ts +10 -0
- package/src/lib/utils.ts +2 -0
- package/src/lib/variants.ts +5 -2
- package/src/lint/index.ts +1 -0
- package/src/styles/base.css +29 -9
- package/src/styles/theme.css +8 -2
|
@@ -250,6 +250,18 @@
|
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Opt into the stable 2xl control rung only when a coarse pointer is available.
|
|
255
|
+
* `any-pointer` intentionally includes hybrid devices without changing the
|
|
256
|
+
* glyph or desktop density of the selected Button size.
|
|
257
|
+
*/
|
|
258
|
+
@utility touch-target-comfortable {
|
|
259
|
+
@media (any-pointer: coarse) {
|
|
260
|
+
min-inline-size: var(--control-height-2xl);
|
|
261
|
+
min-block-size: var(--control-height-2xl);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
253
265
|
/**
|
|
254
266
|
* The drag region of a frameless desktop window.
|
|
255
267
|
*
|
|
@@ -422,9 +434,9 @@
|
|
|
422
434
|
}
|
|
423
435
|
}
|
|
424
436
|
|
|
425
|
-
/*
|
|
426
|
-
|
|
427
|
-
|
|
437
|
+
/* Glass treatments used by the top bar, rail, scene-control groups and floating
|
|
438
|
+
overlays. Consumers use the owning component; shared composites use the
|
|
439
|
+
named utility instead of restyling a primitive. */
|
|
428
440
|
@utility glass-panel {
|
|
429
441
|
background: color-mix(in oklch, var(--chrome) calc(var(--chrome-alpha) * 100%), transparent);
|
|
430
442
|
backdrop-filter: blur(18px) saturate(1.6);
|
|
@@ -438,6 +450,12 @@
|
|
|
438
450
|
-webkit-backdrop-filter: blur(12px) saturate(1.4);
|
|
439
451
|
}
|
|
440
452
|
|
|
453
|
+
@utility glass-scrim {
|
|
454
|
+
background: color-mix(in oklch, var(--scrim) 20%, transparent);
|
|
455
|
+
backdrop-filter: blur(2px);
|
|
456
|
+
-webkit-backdrop-filter: blur(2px);
|
|
457
|
+
}
|
|
458
|
+
|
|
441
459
|
/* The focus ring. Applied by components that cannot use the global outline
|
|
442
460
|
because they need the ring to follow their own radius. */
|
|
443
461
|
@utility focus-ring {
|
|
@@ -657,7 +675,8 @@
|
|
|
657
675
|
|
|
658
676
|
@media (prefers-reduced-transparency: reduce) {
|
|
659
677
|
.glass-panel,
|
|
660
|
-
.glass-surface
|
|
678
|
+
.glass-surface,
|
|
679
|
+
.glass-scrim {
|
|
661
680
|
backdrop-filter: none !important;
|
|
662
681
|
-webkit-backdrop-filter: none !important;
|
|
663
682
|
background: var(--background) !important;
|
|
@@ -670,18 +689,19 @@
|
|
|
670
689
|
}
|
|
671
690
|
|
|
672
691
|
.glass-panel,
|
|
673
|
-
.glass-surface
|
|
692
|
+
.glass-surface,
|
|
693
|
+
.glass-scrim {
|
|
674
694
|
background: var(--background) !important;
|
|
675
695
|
border: 1px solid var(--foreground) !important;
|
|
676
696
|
}
|
|
677
697
|
}
|
|
678
698
|
|
|
679
|
-
/*
|
|
680
|
-
|
|
681
|
-
surface instead, keeping borders and shadows so panels stay delimited. */
|
|
699
|
+
/* Without backdrop-filter there is no blurred context behind a glass surface.
|
|
700
|
+
Fall back to a solid surface so glass panels and scrim groups stay legible. */
|
|
682
701
|
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
|
|
683
702
|
.glass-panel,
|
|
684
|
-
.glass-surface
|
|
703
|
+
.glass-surface,
|
|
704
|
+
.glass-scrim {
|
|
685
705
|
background: var(--background) !important;
|
|
686
706
|
}
|
|
687
707
|
}
|
|
@@ -582,6 +582,7 @@
|
|
|
582
582
|
--control-height-md: 2rem; /* 32px — form default */
|
|
583
583
|
--control-height-lg: 2.25rem; /* 36px */
|
|
584
584
|
--control-height-xl: 2.5rem; /* 40px */
|
|
585
|
+
--control-height-2xl: 3rem; /* 48px — stable touch target */
|
|
585
586
|
|
|
586
587
|
--control-padding-2xs: 0.375rem;
|
|
587
588
|
--control-padding-xs: 0.5rem;
|
|
@@ -589,6 +590,7 @@
|
|
|
589
590
|
--control-padding-md: 0.75rem;
|
|
590
591
|
--control-padding-lg: 0.875rem;
|
|
591
592
|
--control-padding-xl: 1rem;
|
|
593
|
+
--control-padding-2xl: 1.125rem;
|
|
592
594
|
|
|
593
595
|
--row-height-sm: 1.75rem;
|
|
594
596
|
--row-height-md: 2rem;
|
|
@@ -646,14 +648,15 @@
|
|
|
646
648
|
--z-toast: 180;
|
|
647
649
|
}
|
|
648
650
|
|
|
649
|
-
/* Compact density: the
|
|
651
|
+
/* Compact density: the standard control ladder one rung tighter.
|
|
650
652
|
*
|
|
651
653
|
* It is a *shift* rather than a second set of numbers chosen by eye, so the
|
|
652
654
|
* relationship between the rungs is identical at either density and a control keeps
|
|
653
655
|
* its place in the ladder. Height moves by 4px per rung and padding by 2px, which is
|
|
654
656
|
* the step each already uses, and the row heights move by the same 4px. A `sm`
|
|
655
657
|
* control goes from 28px to 24px — the `xs` control's comfortable height. That is the
|
|
656
|
-
* intended meaning, not an oversight.
|
|
658
|
+
* intended meaning, not an oversight. The `2xl` touch target is intentionally
|
|
659
|
+
* omitted here so it stays at least 48px at either density.
|
|
657
660
|
*
|
|
658
661
|
* 16px at `2xs` is the floor. One rung below it would be 12px, under the minimum
|
|
659
662
|
* comfortable touch target, so the ladder compresses from the bottom rather than
|
|
@@ -833,6 +836,7 @@
|
|
|
833
836
|
--height-control-md: var(--control-height-md);
|
|
834
837
|
--height-control-lg: var(--control-height-lg);
|
|
835
838
|
--height-control-xl: var(--control-height-xl);
|
|
839
|
+
--height-control-2xl: var(--control-height-2xl);
|
|
836
840
|
/* Tailwind `size-*` prefers --size-* and then --spacing-*. Without this
|
|
837
841
|
bridge, square icon controls use the padding ladder below for both axes.
|
|
838
842
|
Height, square size and inline padding remain distinct semantic contracts. */
|
|
@@ -842,6 +846,7 @@
|
|
|
842
846
|
--size-control-md: var(--control-height-md);
|
|
843
847
|
--size-control-lg: var(--control-height-lg);
|
|
844
848
|
--size-control-xl: var(--control-height-xl);
|
|
849
|
+
--size-control-2xl: var(--control-height-2xl);
|
|
845
850
|
--height-row-sm: var(--row-height-sm);
|
|
846
851
|
--height-row-md: var(--row-height-md);
|
|
847
852
|
--height-row-lg: var(--row-height-lg);
|
|
@@ -855,6 +860,7 @@
|
|
|
855
860
|
--spacing-control-md: var(--control-padding-md);
|
|
856
861
|
--spacing-control-lg: var(--control-padding-lg);
|
|
857
862
|
--spacing-control-xl: var(--control-padding-xl);
|
|
863
|
+
--spacing-control-2xl: var(--control-padding-2xl);
|
|
858
864
|
|
|
859
865
|
--width-rail: var(--rail-width);
|
|
860
866
|
--width-rail-expanded: var(--rail-width-expanded);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adea-ai/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.95.1",
|
|
4
4
|
"description": "The Adea design system: Kobalte-backed Solid components, semantic OKLCH tokens, and the app-shell layout primitives shared by Adea and Cortana.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adea",
|
|
@@ -103,6 +103,7 @@
|
|
|
103
103
|
"check:packed-layout": "node --test scripts/check-packed-layout-process.test.mjs && node scripts/check-packed-layout.mjs",
|
|
104
104
|
"check:packed-consumer": "bun scripts/check-packed-consumer.ts",
|
|
105
105
|
"check:packed-list-row": "bun scripts/check-packed-list-row.ts",
|
|
106
|
+
"check:packed-scene-controls": "bun scripts/check-packed-scene-controls.ts",
|
|
106
107
|
"check:packed-conversation": "bun scripts/check-packed-conversation.ts",
|
|
107
108
|
"check:packed-transcript": "bun scripts/check-packed-transcript.ts",
|
|
108
109
|
"check:packed-layout-renderer": "bun scripts/check-packed-layout-renderer.ts",
|
package/registry.json
CHANGED
|
@@ -1962,6 +1962,30 @@
|
|
|
1962
1962
|
}
|
|
1963
1963
|
]
|
|
1964
1964
|
},
|
|
1965
|
+
{
|
|
1966
|
+
"name": "scene-controls",
|
|
1967
|
+
"type": "registry:ui",
|
|
1968
|
+
"title": "Scene Controls",
|
|
1969
|
+
"description": "Shared host-neutral scene movement, jump and zoom controls.",
|
|
1970
|
+
"dependencies": [
|
|
1971
|
+
"lucide-solid@^1.48.0"
|
|
1972
|
+
],
|
|
1973
|
+
"registryDependencies": [
|
|
1974
|
+
"@adea-ai/ui/action-button"
|
|
1975
|
+
],
|
|
1976
|
+
"files": [
|
|
1977
|
+
{
|
|
1978
|
+
"path": "src/components/composites/scene-controls/index.ts",
|
|
1979
|
+
"type": "registry:ui",
|
|
1980
|
+
"target": "components/composites/scene-controls/index.ts"
|
|
1981
|
+
},
|
|
1982
|
+
{
|
|
1983
|
+
"path": "src/components/composites/scene-controls/scene-controls.tsx",
|
|
1984
|
+
"type": "registry:ui",
|
|
1985
|
+
"target": "components/composites/scene-controls/scene-controls.tsx"
|
|
1986
|
+
}
|
|
1987
|
+
]
|
|
1988
|
+
},
|
|
1965
1989
|
{
|
|
1966
1990
|
"name": "settings",
|
|
1967
1991
|
"type": "registry:ui",
|
|
@@ -84,6 +84,7 @@ export function ActionButton<T extends ValidComponent = 'button'>(props: ActionB
|
|
|
84
84
|
'as',
|
|
85
85
|
'variant',
|
|
86
86
|
'size',
|
|
87
|
+
'touchTarget',
|
|
87
88
|
'class',
|
|
88
89
|
'children',
|
|
89
90
|
'disabled',
|
|
@@ -106,6 +107,7 @@ export function ActionButton<T extends ValidComponent = 'button'>(props: ActionB
|
|
|
106
107
|
...rest,
|
|
107
108
|
variant: local.variant,
|
|
108
109
|
size: local.size,
|
|
110
|
+
touchTarget: local.touchTarget,
|
|
109
111
|
class: [local.class, local.tooltip && disabled() && 'aria-disabled:pointer-events-auto']
|
|
110
112
|
.filter(Boolean)
|
|
111
113
|
.join(' '),
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, ZoomIn, ZoomOut } from 'lucide-solid'
|
|
2
|
+
import { createSignal, onCleanup, Show, type JSX } from 'solid-js'
|
|
3
|
+
import { ActionButton } from '../action-button'
|
|
4
|
+
|
|
5
|
+
/** A host-neutral direction reported while a movement control is held. */
|
|
6
|
+
export type SceneMovementDirection = 'forward' | 'left' | 'backward' | 'right'
|
|
7
|
+
|
|
8
|
+
/** Accessible names and tooltip text for the shared scene controls. */
|
|
9
|
+
export type SceneControlLabels = {
|
|
10
|
+
movementGroup: string
|
|
11
|
+
forward: string
|
|
12
|
+
forwardTooltip: string
|
|
13
|
+
left: string
|
|
14
|
+
leftTooltip: string
|
|
15
|
+
backward: string
|
|
16
|
+
backwardTooltip: string
|
|
17
|
+
right: string
|
|
18
|
+
rightTooltip: string
|
|
19
|
+
jump: string
|
|
20
|
+
jumpTooltip: string
|
|
21
|
+
zoomGroup: string
|
|
22
|
+
zoomOut: string
|
|
23
|
+
zoomIn: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULT_LABELS: SceneControlLabels = {
|
|
27
|
+
movementGroup: 'Scene movement controls',
|
|
28
|
+
forward: 'Move forward',
|
|
29
|
+
forwardTooltip: 'Hold to move forward',
|
|
30
|
+
left: 'Move left',
|
|
31
|
+
leftTooltip: 'Hold to move left',
|
|
32
|
+
backward: 'Move backward',
|
|
33
|
+
backwardTooltip: 'Hold to move backward',
|
|
34
|
+
right: 'Move right',
|
|
35
|
+
rightTooltip: 'Hold to move right',
|
|
36
|
+
jump: 'Jump',
|
|
37
|
+
jumpTooltip: 'Hold to jump',
|
|
38
|
+
zoomGroup: 'Camera zoom',
|
|
39
|
+
zoomOut: 'Zoom out',
|
|
40
|
+
zoomIn: 'Zoom in',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const pointerSource = (pointerId: number) => `pointer:${pointerId}`
|
|
44
|
+
const keySource = (key: string) => `key:${key}`
|
|
45
|
+
|
|
46
|
+
export type SceneControlsProps = {
|
|
47
|
+
/** Called on the transition into and out of each held movement direction. */
|
|
48
|
+
onMovementChange?: (direction: SceneMovementDirection, pressed: boolean) => void
|
|
49
|
+
/** Called when the jump action becomes pressed or released. */
|
|
50
|
+
onJumpChange?: (pressed: boolean) => void
|
|
51
|
+
/** Called once when zoom in is activated. */
|
|
52
|
+
onZoomIn?: () => void
|
|
53
|
+
/** Called once when zoom out is activated. */
|
|
54
|
+
onZoomOut?: () => void
|
|
55
|
+
/** Override the default English accessible names and tooltip copy. */
|
|
56
|
+
labels?: Partial<SceneControlLabels>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type HoldActionProps = {
|
|
60
|
+
label: string
|
|
61
|
+
tooltip: string
|
|
62
|
+
createChangeCallback: () => (pressed: boolean) => void
|
|
63
|
+
children: JSX.Element
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A held action that reports one down/up pair across pointer and keyboard input.
|
|
68
|
+
*
|
|
69
|
+
* The arrangement, rounded scrim group surfaces and release policy translate
|
|
70
|
+
* Adea's on-screen scene controls; host key synthesis and scene APIs are replaced
|
|
71
|
+
* by callbacks.
|
|
72
|
+
*/
|
|
73
|
+
function HoldAction(props: HoldActionProps) {
|
|
74
|
+
const [pressed, setPressed] = createSignal(false)
|
|
75
|
+
const sources = new Set<string>()
|
|
76
|
+
let activeChangeCallback: ((pressed: boolean) => void) | undefined
|
|
77
|
+
|
|
78
|
+
const press = (source: string) => {
|
|
79
|
+
if (sources.has(source)) return
|
|
80
|
+
const wasPressed = sources.size > 0
|
|
81
|
+
sources.add(source)
|
|
82
|
+
if (!wasPressed) {
|
|
83
|
+
setPressed(true)
|
|
84
|
+
activeChangeCallback = props.createChangeCallback()
|
|
85
|
+
activeChangeCallback(true)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const release = (source: string) => {
|
|
90
|
+
if (!sources.delete(source) || sources.size > 0) return
|
|
91
|
+
setPressed(false)
|
|
92
|
+
const callback = activeChangeCallback
|
|
93
|
+
activeChangeCallback = undefined
|
|
94
|
+
callback?.(false)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const releaseAll = () => {
|
|
98
|
+
if (sources.size === 0) return
|
|
99
|
+
sources.clear()
|
|
100
|
+
setPressed(false)
|
|
101
|
+
const callback = activeChangeCallback
|
|
102
|
+
activeChangeCallback = undefined
|
|
103
|
+
callback?.(false)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const onWindowPointerUp = (event: PointerEvent) => release(pointerSource(event.pointerId))
|
|
107
|
+
const onWindowBlur = () => releaseAll()
|
|
108
|
+
|
|
109
|
+
if (typeof window !== 'undefined') {
|
|
110
|
+
window.addEventListener('pointerup', onWindowPointerUp)
|
|
111
|
+
window.addEventListener('blur', onWindowBlur)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
onCleanup(() => {
|
|
115
|
+
releaseAll()
|
|
116
|
+
if (typeof window !== 'undefined') {
|
|
117
|
+
window.removeEventListener('pointerup', onWindowPointerUp)
|
|
118
|
+
window.removeEventListener('blur', onWindowBlur)
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<ActionButton
|
|
124
|
+
type="button"
|
|
125
|
+
variant="secondary"
|
|
126
|
+
size="icon-2xl"
|
|
127
|
+
aria-label={props.label}
|
|
128
|
+
aria-pressed={pressed()}
|
|
129
|
+
tooltip={props.tooltip}
|
|
130
|
+
class="touch-none select-none"
|
|
131
|
+
onContextMenu={(event) => event.preventDefault()}
|
|
132
|
+
onPointerDown={(event) => {
|
|
133
|
+
if (event.button !== 0) return
|
|
134
|
+
event.preventDefault()
|
|
135
|
+
const source = pointerSource(event.pointerId)
|
|
136
|
+
if (sources.has(source)) return
|
|
137
|
+
try {
|
|
138
|
+
event.currentTarget.setPointerCapture(event.pointerId)
|
|
139
|
+
} catch {
|
|
140
|
+
// Pointer capture is not available in every embedded browser. The
|
|
141
|
+
// window pointerup fallback still closes this held action.
|
|
142
|
+
}
|
|
143
|
+
press(source)
|
|
144
|
+
}}
|
|
145
|
+
onPointerUp={(event) => release(pointerSource(event.pointerId))}
|
|
146
|
+
onPointerCancel={(event) => release(pointerSource(event.pointerId))}
|
|
147
|
+
onLostPointerCapture={(event) => release(pointerSource(event.pointerId))}
|
|
148
|
+
onKeyDown={(event) => {
|
|
149
|
+
if (event.key !== ' ' && event.key !== 'Enter') return
|
|
150
|
+
event.preventDefault()
|
|
151
|
+
press(keySource(event.key))
|
|
152
|
+
}}
|
|
153
|
+
onKeyUp={(event) => {
|
|
154
|
+
if (event.key !== ' ' && event.key !== 'Enter') return
|
|
155
|
+
event.preventDefault()
|
|
156
|
+
release(keySource(event.key))
|
|
157
|
+
}}
|
|
158
|
+
onBlur={releaseAll}
|
|
159
|
+
onClick={(event) => {
|
|
160
|
+
// Assistive technology can activate a button without pointer or keyboard
|
|
161
|
+
// down/up events. Treat that activation as one short held action.
|
|
162
|
+
if (event.detail !== 0 || sources.size > 0) return
|
|
163
|
+
press('activation')
|
|
164
|
+
release('activation')
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
{props.children}
|
|
168
|
+
</ActionButton>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Shared movement, jump and zoom controls for a host-rendered scene.
|
|
174
|
+
*
|
|
175
|
+
* Movement and jump callbacks receive pressed/released state from pointer and
|
|
176
|
+
* keyboard input. Zoom callbacks fire on activation. The host owns positioning,
|
|
177
|
+
* scene state and camera behavior.
|
|
178
|
+
*/
|
|
179
|
+
export function SceneControls(props: SceneControlsProps) {
|
|
180
|
+
const labels = () => ({ ...DEFAULT_LABELS, ...props.labels })
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div class="flex min-w-0 max-w-full flex-wrap items-end gap-3">
|
|
184
|
+
<Show when={props.onMovementChange}>
|
|
185
|
+
<div
|
|
186
|
+
role="group"
|
|
187
|
+
aria-label={labels().movementGroup}
|
|
188
|
+
class="glass-scrim grid min-w-0 max-w-full grid-cols-3 gap-1.5 rounded-3xl p-2"
|
|
189
|
+
>
|
|
190
|
+
<span aria-hidden="true" />
|
|
191
|
+
<HoldAction
|
|
192
|
+
label={labels().forward}
|
|
193
|
+
tooltip={labels().forwardTooltip}
|
|
194
|
+
createChangeCallback={() => {
|
|
195
|
+
const callback = props.onMovementChange
|
|
196
|
+
return (pressed) => callback?.('forward', pressed)
|
|
197
|
+
}}
|
|
198
|
+
>
|
|
199
|
+
<ArrowUp aria-hidden="true" />
|
|
200
|
+
</HoldAction>
|
|
201
|
+
<span aria-hidden="true" />
|
|
202
|
+
<HoldAction
|
|
203
|
+
label={labels().left}
|
|
204
|
+
tooltip={labels().leftTooltip}
|
|
205
|
+
createChangeCallback={() => {
|
|
206
|
+
const callback = props.onMovementChange
|
|
207
|
+
return (pressed) => callback?.('left', pressed)
|
|
208
|
+
}}
|
|
209
|
+
>
|
|
210
|
+
<ArrowLeft aria-hidden="true" />
|
|
211
|
+
</HoldAction>
|
|
212
|
+
<HoldAction
|
|
213
|
+
label={labels().backward}
|
|
214
|
+
tooltip={labels().backwardTooltip}
|
|
215
|
+
createChangeCallback={() => {
|
|
216
|
+
const callback = props.onMovementChange
|
|
217
|
+
return (pressed) => callback?.('backward', pressed)
|
|
218
|
+
}}
|
|
219
|
+
>
|
|
220
|
+
<ArrowDown aria-hidden="true" />
|
|
221
|
+
</HoldAction>
|
|
222
|
+
<HoldAction
|
|
223
|
+
label={labels().right}
|
|
224
|
+
tooltip={labels().rightTooltip}
|
|
225
|
+
createChangeCallback={() => {
|
|
226
|
+
const callback = props.onMovementChange
|
|
227
|
+
return (pressed) => callback?.('right', pressed)
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
<ArrowRight aria-hidden="true" />
|
|
231
|
+
</HoldAction>
|
|
232
|
+
</div>
|
|
233
|
+
</Show>
|
|
234
|
+
<div class="ml-auto flex min-w-0 max-w-full flex-col items-end gap-2">
|
|
235
|
+
<Show when={props.onZoomIn || props.onZoomOut}>
|
|
236
|
+
<div
|
|
237
|
+
role="group"
|
|
238
|
+
aria-label={labels().zoomGroup}
|
|
239
|
+
class="glass-scrim flex min-w-0 max-w-full flex-wrap gap-1.5 rounded-2xl p-1.5"
|
|
240
|
+
>
|
|
241
|
+
<Show when={props.onZoomOut}>
|
|
242
|
+
<ActionButton
|
|
243
|
+
type="button"
|
|
244
|
+
variant="secondary"
|
|
245
|
+
size="icon-2xl"
|
|
246
|
+
aria-label={labels().zoomOut}
|
|
247
|
+
tooltip={labels().zoomOut}
|
|
248
|
+
onClick={() => props.onZoomOut?.()}
|
|
249
|
+
>
|
|
250
|
+
<ZoomOut aria-hidden="true" />
|
|
251
|
+
</ActionButton>
|
|
252
|
+
</Show>
|
|
253
|
+
<Show when={props.onZoomIn}>
|
|
254
|
+
<ActionButton
|
|
255
|
+
type="button"
|
|
256
|
+
variant="secondary"
|
|
257
|
+
size="icon-2xl"
|
|
258
|
+
aria-label={labels().zoomIn}
|
|
259
|
+
tooltip={labels().zoomIn}
|
|
260
|
+
onClick={() => props.onZoomIn?.()}
|
|
261
|
+
>
|
|
262
|
+
<ZoomIn aria-hidden="true" />
|
|
263
|
+
</ActionButton>
|
|
264
|
+
</Show>
|
|
265
|
+
</div>
|
|
266
|
+
</Show>
|
|
267
|
+
<Show when={props.onJumpChange}>
|
|
268
|
+
<HoldAction
|
|
269
|
+
label={labels().jump}
|
|
270
|
+
tooltip={labels().jumpTooltip}
|
|
271
|
+
createChangeCallback={() => {
|
|
272
|
+
const callback = props.onJumpChange
|
|
273
|
+
return (pressed) => callback?.(pressed)
|
|
274
|
+
}}
|
|
275
|
+
>
|
|
276
|
+
<ArrowUp aria-hidden="true" />
|
|
277
|
+
</HoldAction>
|
|
278
|
+
</Show>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
)
|
|
282
|
+
}
|
|
@@ -47,6 +47,7 @@ export const buttonVariants = cva(
|
|
|
47
47
|
md: `${controlSize.md} [&_svg]:size-4`,
|
|
48
48
|
lg: `${controlSize.lg} [&_svg]:size-4`,
|
|
49
49
|
xl: `${controlSize.xl} [&_svg]:size-5`,
|
|
50
|
+
'2xl': `${controlSize['2xl']} [&_svg]:size-6`,
|
|
50
51
|
/* Square, for an icon with no label. The accessible name then has to
|
|
51
52
|
come from `aria-label` or a visually hidden label — an icon-only
|
|
52
53
|
button with neither is the accessibility finding the Storybook lane
|
|
@@ -57,6 +58,11 @@ export const buttonVariants = cva(
|
|
|
57
58
|
'icon-md': 'size-control-md p-0 [&_svg]:size-4',
|
|
58
59
|
'icon-lg': 'size-control-lg p-0 [&_svg]:size-4',
|
|
59
60
|
'icon-xl': 'size-control-xl p-0 [&_svg]:size-5',
|
|
61
|
+
'icon-2xl': 'size-control-2xl p-0 [&_svg]:size-6',
|
|
62
|
+
},
|
|
63
|
+
/** Opt in to a stable touch target on devices with any coarse pointer. */
|
|
64
|
+
touchTarget: {
|
|
65
|
+
comfortable: 'touch-target-comfortable',
|
|
60
66
|
},
|
|
61
67
|
},
|
|
62
68
|
defaultVariants: {
|
|
@@ -76,13 +82,25 @@ export type ButtonProps<T extends ValidComponent = 'button'> = PolymorphicProps<
|
|
|
76
82
|
>
|
|
77
83
|
|
|
78
84
|
export function Button<T extends ValidComponent = 'button'>(props: ButtonProps<T>) {
|
|
79
|
-
const [local, rest] = splitProps(props as ButtonProps, [
|
|
85
|
+
const [local, rest] = splitProps(props as ButtonProps, [
|
|
86
|
+
'variant',
|
|
87
|
+
'size',
|
|
88
|
+
'touchTarget',
|
|
89
|
+
'class',
|
|
90
|
+
])
|
|
80
91
|
|
|
81
92
|
return (
|
|
82
93
|
<Polymorphic
|
|
83
94
|
as="button"
|
|
84
95
|
type="button"
|
|
85
|
-
class={cn(
|
|
96
|
+
class={cn(
|
|
97
|
+
buttonVariants({
|
|
98
|
+
variant: local.variant,
|
|
99
|
+
size: local.size,
|
|
100
|
+
touchTarget: local.touchTarget,
|
|
101
|
+
}),
|
|
102
|
+
local.class
|
|
103
|
+
)}
|
|
86
104
|
{...(rest as ComponentProps<'button'>)}
|
|
87
105
|
/>
|
|
88
106
|
)
|
|
@@ -147,11 +147,16 @@ export function createDialogFocusRestoration({
|
|
|
147
147
|
if (
|
|
148
148
|
!(target instanceof HTMLElement) ||
|
|
149
149
|
target === document.body ||
|
|
150
|
-
target === document.documentElement
|
|
150
|
+
target === document.documentElement
|
|
151
|
+
) {
|
|
152
|
+
return
|
|
153
|
+
}
|
|
154
|
+
if (
|
|
151
155
|
target === element ||
|
|
152
156
|
element.contains(target) ||
|
|
153
157
|
target === (opener instanceof HTMLElement ? opener : null)
|
|
154
158
|
) {
|
|
159
|
+
if (!cycle.closed) cycle.focusMovedOutside = false
|
|
155
160
|
return
|
|
156
161
|
}
|
|
157
162
|
cycle.focusMovedOutside = true
|
|
@@ -33,6 +33,12 @@ import {
|
|
|
33
33
|
* handler takes control or focus has moved to another layer. For controlled
|
|
34
34
|
* dialogs without a composed trigger, pass `restoreFocusRef` when the opener
|
|
35
35
|
* cannot be inferred from the focused element.
|
|
36
|
+
* 5. **It keeps its own portal out of an enclosing layer's `aria-hidden`
|
|
37
|
+
* bookkeeping.** A non-modal dialog opened while a modal `Sheet` is alive would
|
|
38
|
+
* otherwise be aria-hidden one frame after it mounts — the enclosing layer's
|
|
39
|
+
* hide-outside observer walks late-arriving body children as background. The
|
|
40
|
+
* portal carries the marker that observer exempts; a modal layer opened above
|
|
41
|
+
* still hides it, because that path matches a different attribute.
|
|
36
42
|
*
|
|
37
43
|
* Use `Dialog` directly when you need its composition; use this when you want a
|
|
38
44
|
* dialog that is correct by default. `size="settings"` supplies a wide, bounded
|
|
@@ -111,6 +117,20 @@ export function ModalDialog(props: ModalDialogProps) {
|
|
|
111
117
|
if (!wasInert[index]) node.removeAttribute('inert')
|
|
112
118
|
})
|
|
113
119
|
})
|
|
120
|
+
|
|
121
|
+
// Kobalte's hide-outside observer walks body children added after its layer
|
|
122
|
+
// opened and aria-hides them one frame later, so a non-modal dialog portaled
|
|
123
|
+
// in during that window would land outside the accessibility tree — role
|
|
124
|
+
// queries never resolve it again while the enclosing layer stays open. The
|
|
125
|
+
// observer exempts added nodes carrying its top-layer marker (it checks the
|
|
126
|
+
// React Aria spelling it ported; `data-kb-top-layer` is only matched by a
|
|
127
|
+
// newer layer's initial walk, which must still hide this dialog).
|
|
128
|
+
let portal = element
|
|
129
|
+
while (portal.parentElement !== null && portal.parentElement !== document.body) {
|
|
130
|
+
portal = portal.parentElement
|
|
131
|
+
}
|
|
132
|
+
portal.setAttribute('data-react-aria-top-layer', 'true')
|
|
133
|
+
onCleanup(() => portal.removeAttribute('data-react-aria-top-layer'))
|
|
114
134
|
})
|
|
115
135
|
|
|
116
136
|
return (
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Dialog as KobalteDialog,
|
|
3
|
+
Content as KobalteDialogContent,
|
|
4
|
+
useDialogContext,
|
|
5
|
+
} from '@kobalte/core/dialog'
|
|
2
6
|
// AlertDialog shares DialogRoot.Content; Sheet uses the stable named primitive.
|
|
3
7
|
import { X } from 'lucide-solid'
|
|
4
|
-
import type { ComponentProps, JSX } from 'solid-js'
|
|
8
|
+
import type { Accessor, ComponentProps, JSX } from 'solid-js'
|
|
5
9
|
import { Show, splitProps } from 'solid-js'
|
|
6
10
|
import { cva, type VariantProps } from '#lib/variants'
|
|
7
11
|
import { cn } from '#lib/utils'
|
|
8
|
-
import { DialogOverlay } from '../dialog/dialog'
|
|
12
|
+
import { createDialogFocusRestoration, DialogOverlay } from '../dialog/dialog'
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* Sheet.
|
|
@@ -18,6 +22,11 @@ import { DialogOverlay } from '../dialog/dialog'
|
|
|
18
22
|
* `side` decides which edge; the panel is a full-height column for `start` and
|
|
19
23
|
* `end`, and a full-width row for `top` and `bottom`. The slide-in direction
|
|
20
24
|
* follows from the side, so a caller never pairs them by hand.
|
|
25
|
+
*
|
|
26
|
+
* Controlled sheets opened from outside a `SheetTrigger` restore focus to the
|
|
27
|
+
* element focused before opening. Pass `restoreFocusRef` when a stable external
|
|
28
|
+
* opener must be used instead, such as when another layer closes before the
|
|
29
|
+
* sheet opens.
|
|
21
30
|
*/
|
|
22
31
|
const sheetVariants = cva(
|
|
23
32
|
'bg-popover text-popover-foreground fixed z-(--z-dialog) flex flex-col gap-4 border-border shadow-lg',
|
|
@@ -39,10 +48,27 @@ const sheetVariants = cva(
|
|
|
39
48
|
export type SheetContentProps = ComponentProps<typeof KobalteDialogContent> &
|
|
40
49
|
VariantProps<typeof sheetVariants> & {
|
|
41
50
|
closeButton?: JSX.Element | false
|
|
51
|
+
/** Supplies the stable external element to focus when the controlled sheet closes. */
|
|
52
|
+
restoreFocusRef?: Accessor<HTMLElement | undefined>
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
export function SheetContent(props: SheetContentProps) {
|
|
45
|
-
const [local, rest] = splitProps(props, [
|
|
56
|
+
const [local, rest] = splitProps(props, [
|
|
57
|
+
'class',
|
|
58
|
+
'side',
|
|
59
|
+
'children',
|
|
60
|
+
'closeButton',
|
|
61
|
+
'restoreFocusRef',
|
|
62
|
+
'onOpenAutoFocus',
|
|
63
|
+
'onCloseAutoFocus',
|
|
64
|
+
])
|
|
65
|
+
const dialog = useDialogContext()
|
|
66
|
+
const focusRestoration = createDialogFocusRestoration({
|
|
67
|
+
open: dialog.isOpen,
|
|
68
|
+
restoreFocusRef: local.restoreFocusRef,
|
|
69
|
+
onOpenAutoFocus: local.onOpenAutoFocus,
|
|
70
|
+
onCloseAutoFocus: local.onCloseAutoFocus,
|
|
71
|
+
})
|
|
46
72
|
|
|
47
73
|
return (
|
|
48
74
|
<KobalteDialog.Portal>
|
|
@@ -55,11 +81,18 @@ export function SheetContent(props: SheetContentProps) {
|
|
|
55
81
|
local.class
|
|
56
82
|
)}
|
|
57
83
|
{...rest}
|
|
84
|
+
onOpenAutoFocus={focusRestoration.onOpenAutoFocus}
|
|
85
|
+
onCloseAutoFocus={focusRestoration.onCloseAutoFocus}
|
|
58
86
|
>
|
|
59
87
|
{local.children}
|
|
60
88
|
<Show when={local.closeButton !== false}>
|
|
89
|
+
{/* The close action opts into the tab order explicitly. Engines that
|
|
90
|
+
derive tabbability from the DOM instead of the host (Playwright's
|
|
91
|
+
WebKit walks only explicit tabindex and form controls) would skip
|
|
92
|
+
an implicitly-tabbable button and jump past the sheet's end. */}
|
|
61
93
|
<KobalteDialog.CloseButton
|
|
62
94
|
aria-label="Close"
|
|
95
|
+
tabindex="0"
|
|
63
96
|
class="absolute top-3.5 end-3.5 rounded-md p-1 text-muted-foreground transition-colors ease-out outline-none hover:bg-surface-hover hover:text-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle"
|
|
64
97
|
>
|
|
65
98
|
{local.closeButton ?? <X class="size-4" />}
|