ajo-ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +126 -0
- package/dist/accordion.js +130 -0
- package/dist/avatar.js +54 -0
- package/dist/calendar.js +2 -0
- package/dist/carousel.js +239 -0
- package/dist/chart.js +790 -0
- package/dist/checkbox-group.js +70 -0
- package/dist/checkbox.js +77 -0
- package/dist/chunks/bar-CVafh6C1.js +99 -0
- package/dist/chunks/calendar-q8jZ8Cxr.js +1923 -0
- package/dist/chunks/collection-DtRB63U4.js +111 -0
- package/dist/chunks/data-table-DpkWv4y4.js +1039 -0
- package/dist/chunks/menu-B45IyHHC.js +704 -0
- package/dist/chunks/native-BJdhd9XJ.js +20 -0
- package/dist/chunks/popup-C8Bb3l_g.js +459 -0
- package/dist/chunks/popup-surface-BDCgtVU0.js +18 -0
- package/dist/chunks/position-D6_i_SRn.js +434 -0
- package/dist/chunks/virtual-list-B7hjGkjk.js +413 -0
- package/dist/collapsible.js +106 -0
- package/dist/command.js +263 -0
- package/dist/context-menu.js +112 -0
- package/dist/data-table.js +5 -0
- package/dist/dialog.js +207 -0
- package/dist/direction.js +22 -0
- package/dist/drawer.js +139 -0
- package/dist/field.js +26 -0
- package/dist/index.js +38 -0
- package/dist/input-date.js +994 -0
- package/dist/input-group.js +52 -0
- package/dist/input-otp.js +179 -0
- package/dist/menu.js +2 -0
- package/dist/menubar.js +236 -0
- package/dist/message-scroller.js +446 -0
- package/dist/navigation-menu.js +330 -0
- package/dist/popover.js +307 -0
- package/dist/progress.js +39 -0
- package/dist/radio-group.js +107 -0
- package/dist/resizable.js +172 -0
- package/dist/select.js +961 -0
- package/dist/sidebar.js +343 -0
- package/dist/slider.js +259 -0
- package/dist/switch.js +53 -0
- package/dist/tabs.js +182 -0
- package/dist/toast.js +492 -0
- package/dist/toggle-group.js +111 -0
- package/dist/toggle.js +52 -0
- package/dist/toolbar.js +127 -0
- package/dist/tooltip.js +196 -0
- package/dist/utils.js +104 -0
- package/dist/virtual-list.js +2 -0
- package/package.json +250 -0
- package/src/accordion.tsx +261 -0
- package/src/availability.ts +261 -0
- package/src/avatar.tsx +99 -0
- package/src/bar.ts +156 -0
- package/src/calendar.tsx +1441 -0
- package/src/carousel.tsx +424 -0
- package/src/chart.tsx +1194 -0
- package/src/checkbox-group.tsx +132 -0
- package/src/checkbox.tsx +130 -0
- package/src/collapsible.tsx +188 -0
- package/src/collection.ts +154 -0
- package/src/command.tsx +511 -0
- package/src/context-menu.tsx +233 -0
- package/src/data-table-contract.ts +143 -0
- package/src/data-table-model.ts +760 -0
- package/src/data-table.tsx +475 -0
- package/src/dialog.tsx +393 -0
- package/src/direction.tsx +45 -0
- package/src/drawer.tsx +251 -0
- package/src/field.tsx +61 -0
- package/src/index.ts +37 -0
- package/src/input-date.tsx +1539 -0
- package/src/input-group.tsx +142 -0
- package/src/input-otp.tsx +324 -0
- package/src/menu-cluster.ts +124 -0
- package/src/menu.tsx +1095 -0
- package/src/menubar.tsx +459 -0
- package/src/message-scroller.tsx +732 -0
- package/src/native.ts +26 -0
- package/src/navigation-menu.tsx +578 -0
- package/src/popover.tsx +519 -0
- package/src/popup-surface.tsx +31 -0
- package/src/popup.ts +569 -0
- package/src/position.ts +523 -0
- package/src/progress.tsx +70 -0
- package/src/radio-group.tsx +186 -0
- package/src/resizable.tsx +310 -0
- package/src/segments.ts +922 -0
- package/src/select.tsx +1501 -0
- package/src/sidebar.tsx +683 -0
- package/src/slider.tsx +424 -0
- package/src/switch.tsx +104 -0
- package/src/tabs.tsx +314 -0
- package/src/toast.tsx +923 -0
- package/src/toggle-group.tsx +249 -0
- package/src/toggle.tsx +91 -0
- package/src/toolbar.tsx +212 -0
- package/src/tooltip.tsx +359 -0
- package/src/utils.ts +204 -0
- package/src/virtual-list.tsx +205 -0
- package/src/virtual.ts +385 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { IntrinsicElements, Stateful, Stateless, WithChildren } from 'ajo'
|
|
2
|
+
import { callHandler, callRef, dom, listen, statefulRootAttrs as rootAttrs } from 'ajo-cloves'
|
|
3
|
+
import { context } from 'ajo/context'
|
|
4
|
+
import { ariaChecked, bool, flag, syncCheckedState } from './utils'
|
|
5
|
+
|
|
6
|
+
/** Layout and keyboard-navigation axis of a radio group. */
|
|
7
|
+
export type RadioGroupOrientation = 'horizontal' | 'vertical'
|
|
8
|
+
|
|
9
|
+
/** Props for a radio-group fieldset and its controlled selection. */
|
|
10
|
+
export type RadioGroupArgs = WithChildren<IntrinsicElements['fieldset'] & {
|
|
11
|
+
/** Shared radio input name. */
|
|
12
|
+
name?: string
|
|
13
|
+
/** Controlled selected value. */
|
|
14
|
+
value?: string
|
|
15
|
+
/** Initial selected value for uncontrolled groups. */
|
|
16
|
+
defaultValue?: string
|
|
17
|
+
/** Layout orientation. */
|
|
18
|
+
orientation?: RadioGroupOrientation
|
|
19
|
+
/** Called when a radio item becomes checked. */
|
|
20
|
+
onValueChange?: (value: string, event: Event) => void
|
|
21
|
+
}>
|
|
22
|
+
|
|
23
|
+
/** Props for a radio item and its rendered indicator. */
|
|
24
|
+
export type RadioGroupItemArgs = IntrinsicElements['input'] & {
|
|
25
|
+
/** Classes for the indicator element. */
|
|
26
|
+
indicatorClass?: string
|
|
27
|
+
/** Classes for the native input element. */
|
|
28
|
+
inputClass?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type RadioGroupContextValue = {
|
|
32
|
+
defaultValue?: string
|
|
33
|
+
disabled?: boolean
|
|
34
|
+
name?: string
|
|
35
|
+
onValueChange?: (value: string, event: Event) => void
|
|
36
|
+
required?: boolean
|
|
37
|
+
value?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type RadioGroupRootArgs = WithChildren<RadioGroupContextValue>
|
|
41
|
+
|
|
42
|
+
const RadioGroupContext = context<RadioGroupContextValue | null>(null)
|
|
43
|
+
|
|
44
|
+
const syncRadioInput = (input: HTMLInputElement) =>
|
|
45
|
+
syncCheckedState(input, input.closest<HTMLElement>('[data-slot="radio-group-item"]'))
|
|
46
|
+
|
|
47
|
+
const syncRadioGroup = (root: HTMLElement) => {
|
|
48
|
+
for (const input of root.querySelectorAll<HTMLInputElement>('input[data-slot="radio-group-input"]')) {
|
|
49
|
+
syncRadioInput(input)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const itemState = (active: boolean | undefined, attrs: IntrinsicElements['input']) =>
|
|
54
|
+
bool(active ?? attrs['set:checked'] ?? attrs.checked ?? attrs.defaultChecked) ? 'checked' : 'unchecked'
|
|
55
|
+
|
|
56
|
+
const change = (
|
|
57
|
+
value: string | undefined,
|
|
58
|
+
previous: unknown,
|
|
59
|
+
onValueChange: RadioGroupContextValue['onValueChange'],
|
|
60
|
+
) => (event: Event) => {
|
|
61
|
+
callHandler(previous, event)
|
|
62
|
+
|
|
63
|
+
const input = event.currentTarget as HTMLInputElement
|
|
64
|
+
if (input.checked && value != null) onValueChange?.(value, event)
|
|
65
|
+
queueMicrotask(() => {
|
|
66
|
+
const root = input.closest<HTMLElement>('[data-slot="radio-group"]')
|
|
67
|
+
if (root) syncRadioGroup(root)
|
|
68
|
+
else syncRadioInput(input)
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const RadioGroupRoot: Stateful<RadioGroupRootArgs, 'fieldset'> = function* () {
|
|
73
|
+
listen(this, 'change', () => {
|
|
74
|
+
queueMicrotask(() => syncRadioGroup(this))
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
for (const { children, defaultValue, disabled, name, onValueChange, required, value } of this) {
|
|
78
|
+
RadioGroupContext({ defaultValue, disabled, name, onValueChange, required, value })
|
|
79
|
+
if (dom(this)) queueMicrotask(() => syncRadioGroup(this))
|
|
80
|
+
yield <>{children}</>
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
RadioGroupRoot.is = 'fieldset'
|
|
85
|
+
|
|
86
|
+
/** Unstyled radio group with native fieldset semantics. */
|
|
87
|
+
const RadioGroup: Stateless<RadioGroupArgs> = ({
|
|
88
|
+
children,
|
|
89
|
+
defaultValue,
|
|
90
|
+
disabled,
|
|
91
|
+
name,
|
|
92
|
+
onValueChange,
|
|
93
|
+
orientation = 'vertical',
|
|
94
|
+
required,
|
|
95
|
+
value,
|
|
96
|
+
...attrs
|
|
97
|
+
}) => {
|
|
98
|
+
const disabledFlag = disabled ? true : undefined
|
|
99
|
+
const requiredFlag = required ? true : undefined
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<RadioGroupRoot
|
|
103
|
+
{...rootAttrs(attrs)}
|
|
104
|
+
defaultValue={defaultValue}
|
|
105
|
+
disabled={disabledFlag}
|
|
106
|
+
name={name}
|
|
107
|
+
onValueChange={onValueChange}
|
|
108
|
+
required={requiredFlag}
|
|
109
|
+
value={value}
|
|
110
|
+
attr:aria-orientation={orientation}
|
|
111
|
+
attr:data-disabled={flag(disabled)}
|
|
112
|
+
attr:data-orientation={orientation}
|
|
113
|
+
attr:data-slot="radio-group"
|
|
114
|
+
attr:disabled={disabledFlag}
|
|
115
|
+
>
|
|
116
|
+
{children}
|
|
117
|
+
</RadioGroupRoot>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Unstyled native radio item with indicator slot. */
|
|
122
|
+
const RadioGroupItem: Stateless<RadioGroupItemArgs> = ({
|
|
123
|
+
checked,
|
|
124
|
+
class: classes,
|
|
125
|
+
disabled,
|
|
126
|
+
indicatorClass,
|
|
127
|
+
inputClass,
|
|
128
|
+
name,
|
|
129
|
+
ref,
|
|
130
|
+
required,
|
|
131
|
+
'set:checked': setChecked,
|
|
132
|
+
'set:onchange': setOnChange,
|
|
133
|
+
type: _type,
|
|
134
|
+
value,
|
|
135
|
+
...attrs
|
|
136
|
+
}) => {
|
|
137
|
+
const group = RadioGroupContext()
|
|
138
|
+
const itemValue = value == null ? undefined : String(value)
|
|
139
|
+
const ownChecked = checked == null ? undefined : Boolean(checked)
|
|
140
|
+
const groupChecked = group?.value != null
|
|
141
|
+
? itemValue === group.value
|
|
142
|
+
: group?.defaultValue != null
|
|
143
|
+
? itemValue === group.defaultValue
|
|
144
|
+
: undefined
|
|
145
|
+
const active = ownChecked ?? groupChecked
|
|
146
|
+
const syncChecked = active ?? setChecked as boolean | undefined
|
|
147
|
+
const state = itemState(active, { ...attrs, checked, defaultChecked: attrs.defaultChecked, 'set:checked': setChecked })
|
|
148
|
+
const disabledValue = disabled ?? group?.disabled
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<span
|
|
152
|
+
class={classes}
|
|
153
|
+
data-disabled={flag(disabledValue)}
|
|
154
|
+
data-slot="radio-group-item"
|
|
155
|
+
data-state={state}
|
|
156
|
+
>
|
|
157
|
+
<input
|
|
158
|
+
{...attrs}
|
|
159
|
+
aria-checked={ariaChecked(state)}
|
|
160
|
+
checked={active}
|
|
161
|
+
class={inputClass}
|
|
162
|
+
data-slot="radio-group-input"
|
|
163
|
+
data-state={state}
|
|
164
|
+
disabled={disabledValue}
|
|
165
|
+
name={name ?? group?.name}
|
|
166
|
+
ref={element => {
|
|
167
|
+
callRef(ref, element)
|
|
168
|
+
if (element) queueMicrotask(() => syncRadioInput(element))
|
|
169
|
+
}}
|
|
170
|
+
required={required ?? group?.required}
|
|
171
|
+
set:checked={syncChecked}
|
|
172
|
+
set:onchange={change(itemValue, setOnChange, group?.onValueChange)}
|
|
173
|
+
type="radio"
|
|
174
|
+
value={value}
|
|
175
|
+
/>
|
|
176
|
+
<span
|
|
177
|
+
aria-hidden="true"
|
|
178
|
+
class={indicatorClass}
|
|
179
|
+
data-slot="radio-group-indicator"
|
|
180
|
+
data-state={state}
|
|
181
|
+
/>
|
|
182
|
+
</span>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export { RadioGroup, RadioGroupItem }
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import type { IntrinsicElements, Stateful, Stateless, WithChildren } from 'ajo'
|
|
2
|
+
import { clamp, listen, move, statefulRootAttrs as rootAttrs } from 'ajo-cloves'
|
|
3
|
+
import { context } from 'ajo/context'
|
|
4
|
+
import { stlx } from './utils'
|
|
5
|
+
|
|
6
|
+
/** Axis along which adjacent panels are resized. */
|
|
7
|
+
export type ResizableOrientation = 'horizontal' | 'vertical'
|
|
8
|
+
/** Panel size expressed as a percentage number or CSS-like string. */
|
|
9
|
+
export type ResizableSize = number | string
|
|
10
|
+
|
|
11
|
+
/** Props for a group of adjacent resizable panels. */
|
|
12
|
+
export type ResizablePanelGroupArgs = WithChildren<IntrinsicElements['div'] & {
|
|
13
|
+
/** Resize direction for adjacent panels. */
|
|
14
|
+
orientation?: ResizableOrientation
|
|
15
|
+
}>
|
|
16
|
+
|
|
17
|
+
/** Props for a panel with initial and bounded sizes. */
|
|
18
|
+
export type ResizablePanelArgs = WithChildren<IntrinsicElements['div'] & {
|
|
19
|
+
/** Initial panel size as a percentage number or string such as `50%`. */
|
|
20
|
+
defaultSize?: ResizableSize
|
|
21
|
+
/** Minimum panel size as a percentage. */
|
|
22
|
+
minSize?: ResizableSize
|
|
23
|
+
/** Maximum panel size as a percentage. */
|
|
24
|
+
maxSize?: ResizableSize
|
|
25
|
+
}>
|
|
26
|
+
|
|
27
|
+
/** Props for the pointer- and keyboard-operated resize handle. */
|
|
28
|
+
export type ResizableHandleArgs = WithChildren<IntrinsicElements['div'] & {
|
|
29
|
+
/** Disable pointer and keyboard resizing for this handle. */
|
|
30
|
+
disabled?: boolean
|
|
31
|
+
}>
|
|
32
|
+
|
|
33
|
+
/** Layout direction inherited by Resizable parts. */
|
|
34
|
+
export type ResizableContextValue = {
|
|
35
|
+
orientation: ResizableOrientation
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type ResizeSession = {
|
|
39
|
+
groupSize: number
|
|
40
|
+
handle: HTMLElement
|
|
41
|
+
next: HTMLElement
|
|
42
|
+
nextMax: number
|
|
43
|
+
nextMin: number
|
|
44
|
+
prev: HTMLElement
|
|
45
|
+
prevMax: number
|
|
46
|
+
prevMin: number
|
|
47
|
+
prevStart: number
|
|
48
|
+
total: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Layout direction shared by a ResizablePanelGroup with its parts. */
|
|
52
|
+
export const ResizableContext = context<ResizableContextValue>({ orientation: 'horizontal' })
|
|
53
|
+
|
|
54
|
+
const percent = (size: ResizableSize | undefined, fallback: number) => {
|
|
55
|
+
if (typeof size === 'number' && Number.isFinite(size)) return size
|
|
56
|
+
if (typeof size === 'string') {
|
|
57
|
+
const parsed = Number.parseFloat(size)
|
|
58
|
+
if (Number.isFinite(parsed)) return parsed
|
|
59
|
+
}
|
|
60
|
+
return fallback
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const limit = (panel: HTMLElement, name: 'maxSize' | 'minSize', fallback: number) =>
|
|
64
|
+
percent(panel.dataset[name], fallback)
|
|
65
|
+
|
|
66
|
+
const style = (
|
|
67
|
+
orientation: ResizableOrientation,
|
|
68
|
+
defaultSize: ResizableSize | undefined,
|
|
69
|
+
minSize: ResizableSize | undefined,
|
|
70
|
+
maxSize: ResizableSize | undefined,
|
|
71
|
+
styles: unknown,
|
|
72
|
+
) => {
|
|
73
|
+
const basis = percent(defaultSize, 0)
|
|
74
|
+
|
|
75
|
+
return stlx(
|
|
76
|
+
typeof styles === 'string' ? styles : undefined,
|
|
77
|
+
{ flex: defaultSize == null ? '1 1 0' : `0 0 ${basis}%` },
|
|
78
|
+
minSize == null ? undefined : {
|
|
79
|
+
[orientation === 'horizontal' ? 'minWidth' : 'minHeight']: `${percent(minSize, 0)}%`,
|
|
80
|
+
},
|
|
81
|
+
maxSize == null ? undefined : {
|
|
82
|
+
[orientation === 'horizontal' ? 'maxWidth' : 'maxHeight']: `${percent(maxSize, 100)}%`,
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const mainSize = (element: HTMLElement, orientation: ResizableOrientation) => {
|
|
88
|
+
const box = element.getBoundingClientRect()
|
|
89
|
+
return orientation === 'horizontal' ? box.width : box.height
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const previousPanel = (handle: HTMLElement) => {
|
|
93
|
+
let node = handle.previousElementSibling
|
|
94
|
+
while (node && !(node instanceof HTMLElement && node.matches('[data-slot="resizable-panel"]'))) {
|
|
95
|
+
node = node.previousElementSibling
|
|
96
|
+
}
|
|
97
|
+
return node instanceof HTMLElement ? node : null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const nextPanel = (handle: HTMLElement) => {
|
|
101
|
+
let node = handle.nextElementSibling
|
|
102
|
+
while (node && !(node instanceof HTMLElement && node.matches('[data-slot="resizable-panel"]'))) {
|
|
103
|
+
node = node.nextElementSibling
|
|
104
|
+
}
|
|
105
|
+
return node instanceof HTMLElement ? node : null
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const owningGroup = (handle: HTMLElement) =>
|
|
109
|
+
handle.closest<HTMLElement>('[data-slot="resizable-panel-group"]')
|
|
110
|
+
|
|
111
|
+
const setPanelSize = (panel: HTMLElement, px: number, groupSize: number) => {
|
|
112
|
+
const pct = groupSize > 0 ? px / groupSize * 100 : 0
|
|
113
|
+
panel.dataset.size = String(pct)
|
|
114
|
+
panel.style.flex = `0 0 ${px}px`
|
|
115
|
+
panel.dispatchEvent(new CustomEvent('resize', { detail: { size: pct } }))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const setHandleValue = (session: ResizeSession, prev: number) => {
|
|
119
|
+
const pct = session.groupSize > 0 ? prev / session.groupSize * 100 : 0
|
|
120
|
+
session.handle.setAttribute('aria-valuenow', String(Math.round(clamp(pct, 0, 100))))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const applyResize = (session: ResizeSession, delta: number) => {
|
|
124
|
+
// Conflicting panel constraints can invert the bounds; the lower bound wins.
|
|
125
|
+
const min = Math.max(session.prevMin, session.total - session.nextMax)
|
|
126
|
+
const max = Math.min(session.prevMax, session.total - session.nextMin)
|
|
127
|
+
const prev = clamp(session.prevStart + delta, min, Math.max(min, max))
|
|
128
|
+
const next = session.total - prev
|
|
129
|
+
|
|
130
|
+
setPanelSize(session.prev, prev, session.groupSize)
|
|
131
|
+
setPanelSize(session.next, next, session.groupSize)
|
|
132
|
+
setHandleValue(session, prev)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const session = (
|
|
136
|
+
group: HTMLElement,
|
|
137
|
+
handle: HTMLElement,
|
|
138
|
+
orientation: ResizableOrientation,
|
|
139
|
+
): ResizeSession | null => {
|
|
140
|
+
const prev = previousPanel(handle)
|
|
141
|
+
const next = nextPanel(handle)
|
|
142
|
+
const groupSize = mainSize(group, orientation)
|
|
143
|
+
if (!prev || !next || groupSize <= 0) return null
|
|
144
|
+
|
|
145
|
+
const prevStart = mainSize(prev, orientation)
|
|
146
|
+
const nextStart = mainSize(next, orientation)
|
|
147
|
+
const total = prevStart + nextStart
|
|
148
|
+
|
|
149
|
+
const active = {
|
|
150
|
+
groupSize,
|
|
151
|
+
handle,
|
|
152
|
+
next,
|
|
153
|
+
nextMax: groupSize * limit(next, 'maxSize', 100) / 100,
|
|
154
|
+
nextMin: groupSize * limit(next, 'minSize', 0) / 100,
|
|
155
|
+
prev,
|
|
156
|
+
prevMax: groupSize * limit(prev, 'maxSize', 100) / 100,
|
|
157
|
+
prevMin: groupSize * limit(prev, 'minSize', 0) / 100,
|
|
158
|
+
prevStart,
|
|
159
|
+
total,
|
|
160
|
+
}
|
|
161
|
+
setHandleValue(active, prevStart)
|
|
162
|
+
return active
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const ResizablePanelGroupRoot: Stateful<ResizablePanelGroupArgs> = function* () {
|
|
166
|
+
let orientation: ResizableOrientation = 'horizontal'
|
|
167
|
+
let active: ResizeSession | null = null
|
|
168
|
+
let cursor = ''
|
|
169
|
+
let select = ''
|
|
170
|
+
|
|
171
|
+
const restore = () => {
|
|
172
|
+
if (!active) return
|
|
173
|
+
document.body.style.cursor = cursor
|
|
174
|
+
document.body.style.userSelect = select
|
|
175
|
+
active = null
|
|
176
|
+
}
|
|
177
|
+
this.signal.addEventListener('abort', restore, { once: true })
|
|
178
|
+
|
|
179
|
+
const drag = move(this, {
|
|
180
|
+
onMove: data => {
|
|
181
|
+
if (active) applyResize(active, orientation === 'horizontal' ? data.dx : data.dy)
|
|
182
|
+
},
|
|
183
|
+
onEnd: () => restore(),
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const startPointer = (event: PointerEvent) => {
|
|
187
|
+
if (active || event.button !== 0) return
|
|
188
|
+
|
|
189
|
+
const handle = (event.target as HTMLElement | null)?.closest<HTMLElement>('[data-slot="resizable-handle"]')
|
|
190
|
+
if (!handle || owningGroup(handle) !== this || handle.getAttribute('aria-disabled') === 'true') return
|
|
191
|
+
|
|
192
|
+
const next = session(this, handle, orientation)
|
|
193
|
+
if (!next) return
|
|
194
|
+
|
|
195
|
+
event.preventDefault()
|
|
196
|
+
event.stopPropagation()
|
|
197
|
+
|
|
198
|
+
cursor = document.body.style.cursor
|
|
199
|
+
select = document.body.style.userSelect
|
|
200
|
+
document.body.style.cursor = orientation === 'horizontal' ? 'col-resize' : 'row-resize'
|
|
201
|
+
document.body.style.userSelect = 'none'
|
|
202
|
+
|
|
203
|
+
active = next
|
|
204
|
+
if (!drag.start(event)) restore()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const keyResize = (event: KeyboardEvent) => {
|
|
208
|
+
const handle = (event.target as HTMLElement | null)?.closest<HTMLElement>('[data-slot="resizable-handle"]')
|
|
209
|
+
if (!handle || owningGroup(handle) !== this || handle.getAttribute('aria-disabled') === 'true') return
|
|
210
|
+
|
|
211
|
+
const step = event.shiftKey ? 50 : 10
|
|
212
|
+
const delta = orientation === 'horizontal'
|
|
213
|
+
? event.key === 'ArrowLeft' ? -step : event.key === 'ArrowRight' ? step : 0
|
|
214
|
+
: event.key === 'ArrowUp' ? -step : event.key === 'ArrowDown' ? step : 0
|
|
215
|
+
|
|
216
|
+
if (!delta) return
|
|
217
|
+
|
|
218
|
+
const active = session(this, handle, orientation)
|
|
219
|
+
if (!active) return
|
|
220
|
+
|
|
221
|
+
event.preventDefault()
|
|
222
|
+
applyResize(active, delta)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
listen(this, 'pointerdown', startPointer)
|
|
226
|
+
listen(this, 'keydown', keyResize)
|
|
227
|
+
|
|
228
|
+
for (const args of this) {
|
|
229
|
+
orientation = args.orientation ?? 'horizontal'
|
|
230
|
+
ResizableContext({ orientation })
|
|
231
|
+
yield <>{args.children}</>
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Unstyled resizable panel group for split layouts. */
|
|
236
|
+
const ResizablePanelGroup: Stateless<ResizablePanelGroupArgs> = ({
|
|
237
|
+
children,
|
|
238
|
+
orientation = 'horizontal',
|
|
239
|
+
...attrs
|
|
240
|
+
}) => (
|
|
241
|
+
<ResizablePanelGroupRoot
|
|
242
|
+
{...rootAttrs(attrs)}
|
|
243
|
+
orientation={orientation}
|
|
244
|
+
attr:aria-orientation={orientation}
|
|
245
|
+
attr:data-orientation={orientation}
|
|
246
|
+
attr:data-panel-group-direction={orientation}
|
|
247
|
+
attr:data-slot="resizable-panel-group"
|
|
248
|
+
>
|
|
249
|
+
{children}
|
|
250
|
+
</ResizablePanelGroupRoot>
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
/** Unstyled resizable flex panel. */
|
|
254
|
+
const ResizablePanel: Stateless<ResizablePanelArgs> = ({
|
|
255
|
+
children,
|
|
256
|
+
defaultSize,
|
|
257
|
+
maxSize,
|
|
258
|
+
minSize,
|
|
259
|
+
style: styles,
|
|
260
|
+
...attrs
|
|
261
|
+
}) => {
|
|
262
|
+
const { orientation } = ResizableContext()
|
|
263
|
+
const initial = percent(defaultSize, 0)
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<div
|
|
267
|
+
{...attrs}
|
|
268
|
+
data-max-size={maxSize == null ? undefined : percent(maxSize, 100)}
|
|
269
|
+
data-min-size={minSize == null ? undefined : percent(minSize, 0)}
|
|
270
|
+
data-size={defaultSize == null ? undefined : initial}
|
|
271
|
+
data-slot="resizable-panel"
|
|
272
|
+
style={style(orientation, defaultSize, minSize, maxSize, styles)}
|
|
273
|
+
>
|
|
274
|
+
{children}
|
|
275
|
+
</div>
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Unstyled resize separator between adjacent panels. */
|
|
280
|
+
const ResizableHandle: Stateless<ResizableHandleArgs> = ({
|
|
281
|
+
children,
|
|
282
|
+
disabled,
|
|
283
|
+
...attrs
|
|
284
|
+
}) => {
|
|
285
|
+
const { orientation } = ResizableContext()
|
|
286
|
+
const separator = orientation === 'vertical' ? 'horizontal' : 'vertical'
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
<div
|
|
290
|
+
{...attrs}
|
|
291
|
+
aria-disabled={disabled ? 'true' : undefined}
|
|
292
|
+
aria-orientation={separator}
|
|
293
|
+
aria-valuemax="100"
|
|
294
|
+
aria-valuemin="0"
|
|
295
|
+
data-orientation={separator}
|
|
296
|
+
data-panel-group-direction={orientation}
|
|
297
|
+
data-slot="resizable-handle"
|
|
298
|
+
role="separator"
|
|
299
|
+
tabindex={disabled ? -1 : 0}
|
|
300
|
+
>
|
|
301
|
+
{children}
|
|
302
|
+
</div>
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export {
|
|
307
|
+
ResizableHandle,
|
|
308
|
+
ResizablePanel,
|
|
309
|
+
ResizablePanelGroup,
|
|
310
|
+
}
|