@kushagradhawan/kookie-ui 0.1.47 → 0.1.49

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 (149) hide show
  1. package/components.css +858 -30
  2. package/dist/cjs/components/_internal/shell-bottom.d.ts +31 -0
  3. package/dist/cjs/components/_internal/shell-bottom.d.ts.map +1 -0
  4. package/dist/cjs/components/_internal/shell-bottom.js +2 -0
  5. package/dist/cjs/components/_internal/shell-bottom.js.map +7 -0
  6. package/dist/cjs/components/_internal/shell-handles.d.ts +7 -0
  7. package/dist/cjs/components/_internal/shell-handles.d.ts.map +1 -0
  8. package/dist/cjs/components/_internal/shell-handles.js +2 -0
  9. package/dist/cjs/components/_internal/shell-handles.js.map +7 -0
  10. package/dist/cjs/components/_internal/shell-inspector.d.ts +31 -0
  11. package/dist/cjs/components/_internal/shell-inspector.d.ts.map +1 -0
  12. package/dist/cjs/components/_internal/shell-inspector.js +2 -0
  13. package/dist/cjs/components/_internal/shell-inspector.js.map +7 -0
  14. package/dist/cjs/components/_internal/shell-resize.d.ts +24 -0
  15. package/dist/cjs/components/_internal/shell-resize.d.ts.map +1 -0
  16. package/dist/cjs/components/_internal/shell-resize.js +2 -0
  17. package/dist/cjs/components/_internal/shell-resize.js.map +7 -0
  18. package/dist/cjs/components/_internal/shell-sidebar.d.ts +37 -0
  19. package/dist/cjs/components/_internal/shell-sidebar.d.ts.map +1 -0
  20. package/dist/cjs/components/_internal/shell-sidebar.js +2 -0
  21. package/dist/cjs/components/_internal/shell-sidebar.js.map +7 -0
  22. package/dist/cjs/components/alert-dialog.d.ts.map +1 -1
  23. package/dist/cjs/components/alert-dialog.js +1 -1
  24. package/dist/cjs/components/alert-dialog.js.map +2 -2
  25. package/dist/cjs/components/dialog.d.ts.map +1 -1
  26. package/dist/cjs/components/dialog.js +1 -1
  27. package/dist/cjs/components/dialog.js.map +2 -2
  28. package/dist/cjs/components/schemas/index.d.ts +2 -0
  29. package/dist/cjs/components/schemas/index.d.ts.map +1 -1
  30. package/dist/cjs/components/schemas/index.js +1 -1
  31. package/dist/cjs/components/schemas/index.js.map +3 -3
  32. package/dist/cjs/components/schemas/shell.schema.d.ts +1025 -0
  33. package/dist/cjs/components/schemas/shell.schema.d.ts.map +1 -0
  34. package/dist/cjs/components/schemas/shell.schema.js +2 -0
  35. package/dist/cjs/components/schemas/shell.schema.js.map +7 -0
  36. package/dist/cjs/components/shell.context.d.ts +37 -0
  37. package/dist/cjs/components/shell.context.d.ts.map +1 -0
  38. package/dist/cjs/components/shell.context.js +2 -0
  39. package/dist/cjs/components/shell.context.js.map +7 -0
  40. package/dist/cjs/components/shell.d.ts +6 -68
  41. package/dist/cjs/components/shell.d.ts.map +1 -1
  42. package/dist/cjs/components/shell.hooks.d.ts +3 -0
  43. package/dist/cjs/components/shell.hooks.d.ts.map +1 -0
  44. package/dist/cjs/components/shell.hooks.js +2 -0
  45. package/dist/cjs/components/shell.hooks.js.map +7 -0
  46. package/dist/cjs/components/shell.js +1 -1
  47. package/dist/cjs/components/shell.js.map +3 -3
  48. package/dist/cjs/components/shell.types.d.ts +20 -0
  49. package/dist/cjs/components/shell.types.d.ts.map +1 -0
  50. package/dist/cjs/components/shell.types.js +2 -0
  51. package/dist/cjs/components/shell.types.js.map +7 -0
  52. package/dist/cjs/components/sidebar.d.ts +1 -1
  53. package/dist/cjs/components/sidebar.d.ts.map +1 -1
  54. package/dist/cjs/components/sidebar.js +1 -1
  55. package/dist/cjs/components/sidebar.js.map +3 -3
  56. package/dist/esm/components/_internal/shell-bottom.d.ts +31 -0
  57. package/dist/esm/components/_internal/shell-bottom.d.ts.map +1 -0
  58. package/dist/esm/components/_internal/shell-bottom.js +2 -0
  59. package/dist/esm/components/_internal/shell-bottom.js.map +7 -0
  60. package/dist/esm/components/_internal/shell-handles.d.ts +7 -0
  61. package/dist/esm/components/_internal/shell-handles.d.ts.map +1 -0
  62. package/dist/esm/components/_internal/shell-handles.js +2 -0
  63. package/dist/esm/components/_internal/shell-handles.js.map +7 -0
  64. package/dist/esm/components/_internal/shell-inspector.d.ts +31 -0
  65. package/dist/esm/components/_internal/shell-inspector.d.ts.map +1 -0
  66. package/dist/esm/components/_internal/shell-inspector.js +2 -0
  67. package/dist/esm/components/_internal/shell-inspector.js.map +7 -0
  68. package/dist/esm/components/_internal/shell-resize.d.ts +24 -0
  69. package/dist/esm/components/_internal/shell-resize.d.ts.map +1 -0
  70. package/dist/esm/components/_internal/shell-resize.js +2 -0
  71. package/dist/esm/components/_internal/shell-resize.js.map +7 -0
  72. package/dist/esm/components/_internal/shell-sidebar.d.ts +37 -0
  73. package/dist/esm/components/_internal/shell-sidebar.d.ts.map +1 -0
  74. package/dist/esm/components/_internal/shell-sidebar.js +2 -0
  75. package/dist/esm/components/_internal/shell-sidebar.js.map +7 -0
  76. package/dist/esm/components/alert-dialog.d.ts.map +1 -1
  77. package/dist/esm/components/alert-dialog.js +1 -1
  78. package/dist/esm/components/alert-dialog.js.map +2 -2
  79. package/dist/esm/components/dialog.d.ts.map +1 -1
  80. package/dist/esm/components/dialog.js +1 -1
  81. package/dist/esm/components/dialog.js.map +2 -2
  82. package/dist/esm/components/schemas/index.d.ts +2 -0
  83. package/dist/esm/components/schemas/index.d.ts.map +1 -1
  84. package/dist/esm/components/schemas/index.js +1 -1
  85. package/dist/esm/components/schemas/index.js.map +3 -3
  86. package/dist/esm/components/schemas/shell.schema.d.ts +1025 -0
  87. package/dist/esm/components/schemas/shell.schema.d.ts.map +1 -0
  88. package/dist/esm/components/schemas/shell.schema.js +2 -0
  89. package/dist/esm/components/schemas/shell.schema.js.map +7 -0
  90. package/dist/esm/components/shell.context.d.ts +37 -0
  91. package/dist/esm/components/shell.context.d.ts.map +1 -0
  92. package/dist/esm/components/shell.context.js +2 -0
  93. package/dist/esm/components/shell.context.js.map +7 -0
  94. package/dist/esm/components/shell.d.ts +6 -68
  95. package/dist/esm/components/shell.d.ts.map +1 -1
  96. package/dist/esm/components/shell.hooks.d.ts +3 -0
  97. package/dist/esm/components/shell.hooks.d.ts.map +1 -0
  98. package/dist/esm/components/shell.hooks.js +2 -0
  99. package/dist/esm/components/shell.hooks.js.map +7 -0
  100. package/dist/esm/components/shell.js +1 -1
  101. package/dist/esm/components/shell.js.map +3 -3
  102. package/dist/esm/components/shell.types.d.ts +20 -0
  103. package/dist/esm/components/shell.types.d.ts.map +1 -0
  104. package/dist/esm/components/shell.types.js +2 -0
  105. package/dist/esm/components/shell.types.js.map +7 -0
  106. package/dist/esm/components/sidebar.d.ts +1 -1
  107. package/dist/esm/components/sidebar.d.ts.map +1 -1
  108. package/dist/esm/components/sidebar.js +1 -1
  109. package/dist/esm/components/sidebar.js.map +2 -2
  110. package/layout/utilities.css +168 -84
  111. package/layout.css +168 -84
  112. package/package.json +2 -1
  113. package/schemas/base-button.json +1 -1
  114. package/schemas/button.json +1 -1
  115. package/schemas/icon-button.json +1 -1
  116. package/schemas/index.json +6 -6
  117. package/schemas/shell-bottom.json +168 -0
  118. package/schemas/shell-content.json +34 -0
  119. package/schemas/shell-handle.json +34 -0
  120. package/schemas/shell-header.json +42 -0
  121. package/schemas/shell-inspector.json +171 -0
  122. package/schemas/shell-panel.json +167 -0
  123. package/schemas/shell-rail.json +132 -0
  124. package/schemas/shell-root.json +54 -0
  125. package/schemas/shell-sidebar.json +182 -0
  126. package/schemas/shell-trigger.json +76 -0
  127. package/schemas/toggle-button.json +1 -1
  128. package/schemas/toggle-icon-button.json +1 -1
  129. package/src/components/_internal/shell-bottom.tsx +251 -0
  130. package/src/components/_internal/shell-handles.tsx +193 -0
  131. package/src/components/_internal/shell-inspector.tsx +242 -0
  132. package/src/components/_internal/shell-resize.tsx +30 -0
  133. package/src/components/_internal/shell-sidebar.tsx +347 -0
  134. package/src/components/alert-dialog.tsx +6 -0
  135. package/src/components/dialog.tsx +6 -0
  136. package/src/components/schemas/index.ts +46 -0
  137. package/src/components/schemas/shell.schema.ts +403 -0
  138. package/src/components/shell.context.tsx +56 -0
  139. package/src/components/shell.css +5 -17
  140. package/src/components/shell.hooks.ts +31 -0
  141. package/src/components/shell.tsx +368 -1684
  142. package/src/components/shell.types.ts +27 -0
  143. package/src/components/sidebar.tsx +1 -1
  144. package/src/styles/tokens/blur.css +2 -2
  145. package/src/styles/tokens/color.css +2 -2
  146. package/styles.css +1031 -116
  147. package/tokens/base.css +5 -2
  148. package/tokens.css +5 -2
  149. package/utilities.css +168 -84
@@ -0,0 +1,251 @@
1
+ import * as React from 'react';
2
+ import classNames from 'classnames';
3
+ import * as Sheet from '../sheet.js';
4
+ import { VisuallyHidden } from '../visually-hidden.js';
5
+ import { useShell } from '../shell.context.js';
6
+ import { useResponsivePresentation } from '../shell.hooks.js';
7
+ import { PaneResizeContext } from './shell-resize.js';
8
+ import { BottomHandle, PaneHandle } from './shell-handles.js';
9
+ import { BREAKPOINTS } from '../shell.types.js';
10
+ import type { Breakpoint, PaneMode, PaneSizePersistence, ResponsivePresentation } from '../shell.types.js';
11
+
12
+ interface PaneProps extends React.ComponentPropsWithoutRef<'div'> {
13
+ presentation?: ResponsivePresentation;
14
+ mode?: PaneMode;
15
+ defaultMode?: any;
16
+ onModeChange?: (mode: PaneMode) => void;
17
+ expandedSize?: number;
18
+ minSize?: number;
19
+ maxSize?: number;
20
+ resizable?: boolean;
21
+ collapsible?: boolean;
22
+ onExpand?: () => void;
23
+ onCollapse?: () => void;
24
+ onResize?: (size: number) => void;
25
+ resizer?: React.ReactNode;
26
+ onResizeStart?: (size: number) => void;
27
+ onResizeEnd?: (size: number) => void;
28
+ snapPoints?: number[];
29
+ snapTolerance?: number;
30
+ collapseThreshold?: number;
31
+ paneId?: string;
32
+ persistence?: PaneSizePersistence;
33
+ }
34
+
35
+ type BottomComponent = React.ForwardRefExoticComponent<PaneProps & React.RefAttributes<HTMLDivElement>> & { Handle: typeof BottomHandle };
36
+
37
+ export const Bottom = React.forwardRef<HTMLDivElement, PaneProps>(
38
+ (
39
+ {
40
+ className,
41
+ presentation = 'fixed',
42
+ mode,
43
+ defaultMode = 'collapsed',
44
+ onModeChange,
45
+ expandedSize = 200,
46
+ minSize = 100,
47
+ maxSize = 400,
48
+ resizable = false,
49
+ collapsible = true,
50
+ onExpand,
51
+ onCollapse,
52
+ onResize,
53
+ onResizeStart,
54
+ onResizeEnd,
55
+ snapPoints,
56
+ snapTolerance,
57
+ collapseThreshold,
58
+ paneId,
59
+ persistence,
60
+ children,
61
+ style,
62
+ ...props
63
+ },
64
+ ref,
65
+ ) => {
66
+ const shell = useShell();
67
+ const resolvedPresentation = useResponsivePresentation(presentation);
68
+ const isOverlay = resolvedPresentation === 'overlay';
69
+ const isStacked = resolvedPresentation === 'stacked';
70
+ const localRef = React.useRef<HTMLDivElement | null>(null);
71
+ const setRef = React.useCallback(
72
+ (node: HTMLDivElement | null) => {
73
+ localRef.current = node;
74
+ if (typeof ref === 'function') ref(node);
75
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
76
+ },
77
+ [ref],
78
+ );
79
+ const childArray = React.Children.toArray(children) as React.ReactElement[];
80
+ const handleChildren = childArray.filter((el: React.ReactElement) => React.isValidElement(el) && el.type === BottomHandle);
81
+ const contentChildren = childArray.filter((el: React.ReactElement) => !(React.isValidElement(el) && el.type === BottomHandle));
82
+
83
+ const resolveResponsiveMode = React.useCallback((): PaneMode => {
84
+ if (typeof defaultMode === 'string') return defaultMode as PaneMode;
85
+ const dm = defaultMode as Partial<Record<Breakpoint, PaneMode>> | undefined;
86
+ if (dm && dm[shell.currentBreakpoint as Breakpoint]) {
87
+ return dm[shell.currentBreakpoint as Breakpoint] as PaneMode;
88
+ }
89
+ const bpKeys = Object.keys(BREAKPOINTS) as Array<keyof typeof BREAKPOINTS>;
90
+ const order: Breakpoint[] = ([...bpKeys].reverse() as Breakpoint[]).concat('initial' as Breakpoint);
91
+ const startIdx = order.indexOf(shell.currentBreakpoint as Breakpoint);
92
+ for (let i = startIdx + 1; i < order.length; i++) {
93
+ const bp = order[i];
94
+ if (dm && dm[bp]) {
95
+ return dm[bp] as PaneMode;
96
+ }
97
+ }
98
+ return 'collapsed';
99
+ }, [defaultMode, shell.currentBreakpoint]);
100
+
101
+ const didInitRef = React.useRef(false);
102
+ React.useEffect(() => {
103
+ if (didInitRef.current) return;
104
+ didInitRef.current = true;
105
+ if (mode === undefined) {
106
+ const initial = resolveResponsiveMode();
107
+ if (shell.bottomMode !== initial) shell.setBottomMode(initial);
108
+ }
109
+ }, []);
110
+
111
+ const lastBottomBpRef = React.useRef<Breakpoint | null>(null);
112
+ const lastResolvedBottomRef = React.useRef<PaneMode | null>(null);
113
+ React.useEffect(() => {
114
+ if (mode !== undefined) return;
115
+ if (!shell.currentBreakpointReady) return;
116
+ if (lastBottomBpRef.current === shell.currentBreakpoint) return;
117
+ lastBottomBpRef.current = shell.currentBreakpoint as Breakpoint;
118
+ const next = resolveResponsiveMode();
119
+ if (lastResolvedBottomRef.current === next) return;
120
+ lastResolvedBottomRef.current = next;
121
+ if (next !== shell.bottomMode) shell.setBottomMode(next);
122
+ }, [mode, shell.currentBreakpoint, shell.currentBreakpointReady, resolveResponsiveMode, shell.bottomMode, shell.setBottomMode]);
123
+
124
+ React.useEffect(() => {
125
+ if (mode !== undefined && shell.bottomMode !== mode) {
126
+ shell.setBottomMode(mode);
127
+ }
128
+ }, [mode, shell]);
129
+
130
+ React.useEffect(() => {
131
+ if (mode === undefined) {
132
+ onModeChange?.(shell.bottomMode);
133
+ }
134
+ }, [shell.bottomMode, mode, onModeChange]);
135
+
136
+ React.useEffect(() => {
137
+ if (shell.bottomMode === 'expanded') {
138
+ onExpand?.();
139
+ } else {
140
+ onCollapse?.();
141
+ }
142
+ }, [shell.bottomMode, onExpand, onCollapse]);
143
+
144
+ const isExpanded = shell.bottomMode === 'expanded';
145
+
146
+ const persistenceAdapter = React.useMemo(() => {
147
+ if (!paneId || persistence) return persistence;
148
+ const key = `kookie-ui:shell:bottom:${paneId}`;
149
+ const adapter: PaneSizePersistence = {
150
+ load: () => {
151
+ if (typeof window === 'undefined') return undefined;
152
+ const v = window.localStorage.getItem(key);
153
+ return v ? Number(v) : undefined;
154
+ },
155
+ save: (size: number) => {
156
+ if (typeof window === 'undefined') return;
157
+ window.localStorage.setItem(key, String(size));
158
+ },
159
+ };
160
+ return adapter;
161
+ }, [paneId, persistence]);
162
+
163
+ React.useEffect(() => {
164
+ let mounted = true;
165
+ (async () => {
166
+ if (!resizable || !persistenceAdapter?.load || isOverlay) return;
167
+ const loaded = await persistenceAdapter.load();
168
+ if (mounted && typeof loaded === 'number' && localRef.current) {
169
+ localRef.current.style.setProperty('--bottom-size', `${loaded}px`);
170
+ onResize?.(loaded);
171
+ }
172
+ })();
173
+ return () => {
174
+ mounted = false;
175
+ };
176
+ }, [resizable, persistenceAdapter, onResize, isOverlay]);
177
+
178
+ const handleEl =
179
+ resizable && !isOverlay && isExpanded ? (
180
+ <PaneResizeContext.Provider
181
+ value={{
182
+ containerRef: localRef,
183
+ cssVarName: '--bottom-size',
184
+ minSize,
185
+ maxSize,
186
+ defaultSize: expandedSize,
187
+ orientation: 'horizontal',
188
+ edge: 'start',
189
+ computeNext: (client, startClient, startSize) => {
190
+ const delta = client - startClient;
191
+ return startSize - delta;
192
+ },
193
+ onResize,
194
+ onResizeStart,
195
+ onResizeEnd: (size) => {
196
+ onResizeEnd?.(size);
197
+ persistenceAdapter?.save?.(size);
198
+ },
199
+ target: 'bottom',
200
+ collapsible,
201
+ snapPoints,
202
+ snapTolerance: snapTolerance ?? 8,
203
+ collapseThreshold,
204
+ requestCollapse: () => shell.setBottomMode('collapsed'),
205
+ requestToggle: () => shell.togglePane('bottom'),
206
+ }}
207
+ >
208
+ {handleChildren.length > 0 ? handleChildren.map((el, i) => React.cloneElement(el, { key: el.key ?? i })) : <PaneHandle />}
209
+ </PaneResizeContext.Provider>
210
+ ) : null;
211
+
212
+ if (isOverlay) {
213
+ const open = shell.bottomMode === 'expanded';
214
+ return (
215
+ <Sheet.Root open={open} onOpenChange={(o) => shell.setBottomMode(o ? 'expanded' : 'collapsed')}>
216
+ <Sheet.Content side="bottom" style={{ padding: 0 }} height={{ initial: `${expandedSize}px` }}>
217
+ <VisuallyHidden>
218
+ <Sheet.Title>Bottom panel</Sheet.Title>
219
+ </VisuallyHidden>
220
+ {contentChildren}
221
+ </Sheet.Content>
222
+ </Sheet.Root>
223
+ );
224
+ }
225
+
226
+ return (
227
+ <div
228
+ {...props}
229
+ ref={setRef}
230
+ className={classNames('rt-ShellBottom', className)}
231
+ data-mode={shell.bottomMode}
232
+ data-peek={shell.peekTarget === 'bottom' || undefined}
233
+ data-presentation={resolvedPresentation}
234
+ data-open={(isStacked && isExpanded) || undefined}
235
+ style={{
236
+ ...style,
237
+ ['--bottom-size' as any]: `${expandedSize}px`,
238
+ ['--bottom-min-size' as any]: `${minSize}px`,
239
+ ['--bottom-max-size' as any]: `${maxSize}px`,
240
+ }}
241
+ >
242
+ <div className="rt-ShellBottomContent" data-visible={isExpanded || undefined}>
243
+ {contentChildren}
244
+ </div>
245
+ {handleEl}
246
+ </div>
247
+ );
248
+ },
249
+ ) as BottomComponent;
250
+ Bottom.displayName = 'Shell.Bottom';
251
+ Bottom.Handle = BottomHandle;
@@ -0,0 +1,193 @@
1
+ import * as React from 'react';
2
+ import classNames from 'classnames';
3
+ import { usePaneResize } from './shell-resize.js';
4
+
5
+ export const PaneHandle = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(({ className, children, ...props }, ref) => {
6
+ const {
7
+ containerRef,
8
+ cssVarName,
9
+ minSize,
10
+ maxSize,
11
+ defaultSize,
12
+ orientation,
13
+ edge,
14
+ computeNext,
15
+ onResize,
16
+ onResizeStart,
17
+ onResizeEnd,
18
+ snapPoints,
19
+ snapTolerance,
20
+ collapseThreshold,
21
+ collapsible,
22
+ target,
23
+ requestCollapse,
24
+ requestToggle,
25
+ } = usePaneResize();
26
+
27
+ const activeCleanupRef = React.useRef<(() => void) | null>(null);
28
+ React.useEffect(
29
+ () => () => {
30
+ try {
31
+ activeCleanupRef.current?.();
32
+ } catch {}
33
+ activeCleanupRef.current = null;
34
+ },
35
+ [],
36
+ );
37
+
38
+ const ariaOrientation = orientation;
39
+
40
+ return (
41
+ <div
42
+ {...props}
43
+ ref={ref}
44
+ className={classNames('rt-ShellResizer', className)}
45
+ data-orientation={orientation}
46
+ data-edge={edge}
47
+ role="slider"
48
+ aria-orientation={ariaOrientation}
49
+ aria-valuemin={minSize}
50
+ aria-valuemax={maxSize}
51
+ aria-valuenow={defaultSize}
52
+ tabIndex={0}
53
+ onPointerDown={(e) => {
54
+ if (!containerRef.current) return;
55
+ e.preventDefault();
56
+ const container = containerRef.current;
57
+ const handleEl = e.currentTarget as HTMLElement;
58
+ const pointerId = e.pointerId;
59
+ try {
60
+ activeCleanupRef.current?.();
61
+ } catch {}
62
+ container.setAttribute('data-resizing', '');
63
+ try {
64
+ handleEl.setPointerCapture(pointerId);
65
+ } catch {}
66
+ const startClient = orientation === 'vertical' ? e.clientX : e.clientY;
67
+ const startSize = parseFloat(getComputedStyle(container).getPropertyValue(cssVarName) || `${defaultSize}`);
68
+ const clamp = (v: number) => Math.min(Math.max(v, minSize), maxSize);
69
+ const body = document.body;
70
+ const prevCursor = body.style.cursor;
71
+ const prevUserSelect = body.style.userSelect;
72
+ body.style.cursor = orientation === 'vertical' ? 'col-resize' : 'row-resize';
73
+ body.style.userSelect = 'none';
74
+ onResizeStart?.(startSize);
75
+ const handleMove = (ev: PointerEvent) => {
76
+ const client = orientation === 'vertical' ? ev.clientX : ev.clientY;
77
+ const next = clamp(computeNext(client, startClient, startSize));
78
+ container.style.setProperty(cssVarName, `${next}px`);
79
+ handleEl.setAttribute('aria-valuenow', String(next));
80
+ onResize?.(next);
81
+ };
82
+ const cleanup = () => {
83
+ try {
84
+ handleEl.releasePointerCapture(pointerId);
85
+ } catch {}
86
+ window.removeEventListener('pointermove', handleMove as any);
87
+ window.removeEventListener('pointerup', handleUp as any);
88
+ window.removeEventListener('pointercancel', handleUp as any);
89
+ window.removeEventListener('keydown', handleKey as any);
90
+ handleEl.removeEventListener('lostpointercapture', handleUp as any);
91
+ container.removeAttribute('data-resizing');
92
+ body.style.cursor = prevCursor;
93
+ body.style.userSelect = prevUserSelect;
94
+ activeCleanupRef.current = null;
95
+ };
96
+ const handleUp = () => {
97
+ const finalSize = parseFloat(getComputedStyle(container).getPropertyValue(cssVarName) || `${defaultSize}`);
98
+ let snapped = finalSize;
99
+ if (snapPoints && snapPoints.length) {
100
+ const nearest = snapPoints.reduce((acc, p) => (Math.abs(p - finalSize) < Math.abs(acc - finalSize) ? p : acc), snapPoints[0]);
101
+ if (Math.abs(nearest - finalSize) <= (snapTolerance ?? 8)) {
102
+ snapped = nearest;
103
+ container.style.setProperty(cssVarName, `${snapped}px`);
104
+ handleEl.setAttribute('aria-valuenow', String(snapped));
105
+ onResize?.(snapped);
106
+ }
107
+ }
108
+ if (collapsible && typeof collapseThreshold === 'number' && finalSize <= collapseThreshold) {
109
+ requestCollapse?.();
110
+ }
111
+ onResizeEnd?.(snapped);
112
+ cleanup();
113
+ };
114
+ const handleKey = (kev: KeyboardEvent) => {
115
+ if (kev.key === 'Escape') {
116
+ container.style.setProperty(cssVarName, `${startSize}px`);
117
+ handleEl.setAttribute('aria-valuenow', String(startSize));
118
+ onResizeEnd?.(startSize);
119
+ cleanup();
120
+ }
121
+ };
122
+ window.addEventListener('pointermove', handleMove as any);
123
+ window.addEventListener('pointerup', handleUp as any);
124
+ window.addEventListener('pointercancel', handleUp as any);
125
+ window.addEventListener('keydown', handleKey as any);
126
+ handleEl.addEventListener('lostpointercapture', handleUp as any);
127
+ activeCleanupRef.current = cleanup;
128
+ }}
129
+ onDoubleClick={() => {
130
+ if (collapsible) requestToggle?.();
131
+ }}
132
+ onKeyDown={(e) => {
133
+ if (!containerRef.current) return;
134
+ const container = containerRef.current;
135
+ const current = parseFloat(getComputedStyle(container).getPropertyValue(cssVarName) || `${defaultSize}`);
136
+ const clamp = (v: number) => Math.min(Math.max(v, minSize), maxSize);
137
+ const step = e.shiftKey ? 32 : 8;
138
+ let delta = 0;
139
+ if (orientation === 'vertical') {
140
+ if (e.key === 'ArrowRight') delta = step;
141
+ else if (e.key === 'ArrowLeft') delta = -step;
142
+ } else {
143
+ if (e.key === 'ArrowDown') delta = step;
144
+ else if (e.key === 'ArrowUp') delta = -step;
145
+ }
146
+ if (e.key === 'Home') {
147
+ e.preventDefault();
148
+ onResizeStart?.(current);
149
+ const next = clamp(minSize);
150
+ container.style.setProperty(cssVarName, `${next}px`);
151
+ (e.currentTarget as HTMLElement).setAttribute('aria-valuenow', String(next));
152
+ onResize?.(next);
153
+ onResizeEnd?.(next);
154
+ return;
155
+ }
156
+ if (e.key === 'End') {
157
+ e.preventDefault();
158
+ onResizeStart?.(current);
159
+ const next = clamp(maxSize);
160
+ container.style.setProperty(cssVarName, `${next}px`);
161
+ (e.currentTarget as HTMLElement).setAttribute('aria-valuenow', String(next));
162
+ onResize?.(next);
163
+ onResizeEnd?.(next);
164
+ return;
165
+ }
166
+ if (delta !== 0) {
167
+ e.preventDefault();
168
+ onResizeStart?.(current);
169
+ const next = clamp(current + (edge === 'start' && orientation === 'vertical' ? -delta : delta));
170
+ container.style.setProperty(cssVarName, `${next}px`);
171
+ (e.currentTarget as HTMLElement).setAttribute('aria-valuenow', String(next));
172
+ onResize?.(next);
173
+ onResizeEnd?.(next);
174
+ }
175
+ }}
176
+ >
177
+ {children}
178
+ </div>
179
+ );
180
+ });
181
+ PaneHandle.displayName = 'Shell.Handle';
182
+
183
+ export const PanelHandle = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>((props, ref) => <PaneHandle {...props} ref={ref} />);
184
+ PanelHandle.displayName = 'Shell.Panel.Handle';
185
+
186
+ export const SidebarHandle = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>((props, ref) => <PaneHandle {...props} ref={ref} />);
187
+ SidebarHandle.displayName = 'Shell.Sidebar.Handle';
188
+
189
+ export const InspectorHandle = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>((props, ref) => <PaneHandle {...props} ref={ref} />);
190
+ InspectorHandle.displayName = 'Shell.Inspector.Handle';
191
+
192
+ export const BottomHandle = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>((props, ref) => <PaneHandle {...props} ref={ref} />);
193
+ BottomHandle.displayName = 'Shell.Bottom.Handle';
@@ -0,0 +1,242 @@
1
+ import * as React from 'react';
2
+ import classNames from 'classnames';
3
+ import * as Sheet from '../sheet.js';
4
+ import { VisuallyHidden } from '../visually-hidden.js';
5
+ import { useShell } from '../shell.context.js';
6
+ import { useResponsivePresentation } from '../shell.hooks.js';
7
+ import { PaneResizeContext } from './shell-resize.js';
8
+ import { InspectorHandle, PaneHandle } from './shell-handles.js';
9
+ import { BREAKPOINTS } from '../shell.types.js';
10
+ import type { Breakpoint, PaneMode, PaneSizePersistence, ResponsivePresentation } from '../shell.types.js';
11
+
12
+ interface PaneProps extends React.ComponentPropsWithoutRef<'div'> {
13
+ presentation?: ResponsivePresentation;
14
+ mode?: PaneMode;
15
+ defaultMode?: any;
16
+ onModeChange?: (mode: PaneMode) => void;
17
+ expandedSize?: number;
18
+ minSize?: number;
19
+ maxSize?: number;
20
+ resizable?: boolean;
21
+ collapsible?: boolean;
22
+ onExpand?: () => void;
23
+ onCollapse?: () => void;
24
+ onResize?: (size: number) => void;
25
+ resizer?: React.ReactNode;
26
+ onResizeStart?: (size: number) => void;
27
+ onResizeEnd?: (size: number) => void;
28
+ snapPoints?: number[];
29
+ snapTolerance?: number;
30
+ collapseThreshold?: number;
31
+ paneId?: string;
32
+ persistence?: PaneSizePersistence;
33
+ }
34
+
35
+ type InspectorComponent = React.ForwardRefExoticComponent<PaneProps & React.RefAttributes<HTMLDivElement>> & { Handle: typeof InspectorHandle };
36
+
37
+ export const Inspector = React.forwardRef<HTMLDivElement, PaneProps>(
38
+ (
39
+ {
40
+ className,
41
+ presentation = { initial: 'overlay', lg: 'fixed' },
42
+ mode,
43
+ defaultMode = 'collapsed',
44
+ onModeChange,
45
+ expandedSize = 320,
46
+ minSize = 200,
47
+ maxSize = 500,
48
+ resizable = false,
49
+ collapsible = true,
50
+ onExpand,
51
+ onCollapse,
52
+ onResize,
53
+ onResizeStart,
54
+ onResizeEnd,
55
+ snapPoints,
56
+ snapTolerance,
57
+ collapseThreshold,
58
+ paneId,
59
+ persistence,
60
+ children,
61
+ style,
62
+ ...props
63
+ },
64
+ ref,
65
+ ) => {
66
+ const shell = useShell();
67
+ const resolvedPresentation = useResponsivePresentation(presentation);
68
+ const isOverlay = resolvedPresentation === 'overlay';
69
+ const isStacked = resolvedPresentation === 'stacked';
70
+ const localRef = React.useRef<HTMLDivElement | null>(null);
71
+ const setRef = React.useCallback(
72
+ (node: HTMLDivElement | null) => {
73
+ localRef.current = node;
74
+ if (typeof ref === 'function') ref(node);
75
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
76
+ },
77
+ [ref],
78
+ );
79
+ const childArray = React.Children.toArray(children) as React.ReactElement[];
80
+ const handleChildren = childArray.filter((el: React.ReactElement) => React.isValidElement(el) && el.type === InspectorHandle);
81
+ const contentChildren = childArray.filter((el: React.ReactElement) => !(React.isValidElement(el) && el.type === InspectorHandle));
82
+
83
+ const resolveResponsiveMode = React.useCallback((): PaneMode => {
84
+ if (typeof defaultMode === 'string') return defaultMode as PaneMode;
85
+ const dm = defaultMode as Partial<Record<Breakpoint, PaneMode>> | undefined;
86
+ if (dm && dm[shell.currentBreakpoint as Breakpoint]) {
87
+ return dm[shell.currentBreakpoint as Breakpoint] as PaneMode;
88
+ }
89
+ const bpKeys = Object.keys(BREAKPOINTS) as Array<keyof typeof BREAKPOINTS>;
90
+ const order: Breakpoint[] = ([...bpKeys].reverse() as Breakpoint[]).concat('initial' as Breakpoint);
91
+ const startIdx = order.indexOf(shell.currentBreakpoint as Breakpoint);
92
+ for (let i = startIdx + 1; i < order.length; i++) {
93
+ const bp = order[i];
94
+ if (dm && dm[bp]) {
95
+ return dm[bp] as PaneMode;
96
+ }
97
+ }
98
+ return 'collapsed';
99
+ }, [defaultMode, shell.currentBreakpoint]);
100
+
101
+ const lastInspectorBpRef = React.useRef<Breakpoint | null>(null);
102
+ React.useEffect(() => {
103
+ if (mode !== undefined) return;
104
+ if (!shell.currentBreakpointReady) return;
105
+ if (lastInspectorBpRef.current === shell.currentBreakpoint) return;
106
+ lastInspectorBpRef.current = shell.currentBreakpoint as Breakpoint;
107
+ const next = resolveResponsiveMode();
108
+ if (next !== shell.inspectorMode) {
109
+ shell.setInspectorMode(next);
110
+ }
111
+ }, [mode, shell.currentBreakpoint, shell.currentBreakpointReady, resolveResponsiveMode, shell.inspectorMode, shell.setInspectorMode]);
112
+
113
+ React.useEffect(() => {
114
+ if (mode !== undefined && shell.inspectorMode !== mode) {
115
+ shell.setInspectorMode(mode);
116
+ }
117
+ }, [mode, shell]);
118
+
119
+ React.useEffect(() => {
120
+ if (mode === undefined) {
121
+ onModeChange?.(shell.inspectorMode);
122
+ }
123
+ }, [shell.inspectorMode, mode, onModeChange]);
124
+
125
+ React.useEffect(() => {
126
+ if (shell.inspectorMode === 'expanded') {
127
+ onExpand?.();
128
+ } else {
129
+ onCollapse?.();
130
+ }
131
+ }, [shell.inspectorMode, onExpand, onCollapse]);
132
+
133
+ const isExpanded = shell.inspectorMode === 'expanded';
134
+
135
+ const persistenceAdapter = React.useMemo(() => {
136
+ if (!paneId || persistence) return persistence;
137
+ const key = `kookie-ui:shell:inspector:${paneId}`;
138
+ const adapter: PaneSizePersistence = {
139
+ load: () => {
140
+ if (typeof window === 'undefined') return undefined;
141
+ const v = window.localStorage.getItem(key);
142
+ return v ? Number(v) : undefined;
143
+ },
144
+ save: (size: number) => {
145
+ if (typeof window === 'undefined') return;
146
+ window.localStorage.setItem(key, String(size));
147
+ },
148
+ };
149
+ return adapter;
150
+ }, [paneId, persistence]);
151
+
152
+ React.useEffect(() => {
153
+ let mounted = true;
154
+ (async () => {
155
+ if (!resizable || !persistenceAdapter?.load || isOverlay) return;
156
+ const loaded = await persistenceAdapter.load();
157
+ if (mounted && typeof loaded === 'number' && localRef.current) {
158
+ localRef.current.style.setProperty('--inspector-size', `${loaded}px`);
159
+ onResize?.(loaded);
160
+ }
161
+ })();
162
+ return () => {
163
+ mounted = false;
164
+ };
165
+ }, [resizable, persistenceAdapter, onResize, isOverlay]);
166
+
167
+ const handleEl =
168
+ resizable && !isOverlay && isExpanded ? (
169
+ <PaneResizeContext.Provider
170
+ value={{
171
+ containerRef: localRef,
172
+ cssVarName: '--inspector-size',
173
+ minSize,
174
+ maxSize,
175
+ defaultSize: expandedSize,
176
+ orientation: 'vertical',
177
+ edge: 'start',
178
+ computeNext: (client, startClient, startSize) => {
179
+ const isRtl = getComputedStyle(localRef.current!).direction === 'rtl';
180
+ const delta = client - startClient;
181
+ return startSize + (isRtl ? delta : -delta);
182
+ },
183
+ onResize,
184
+ onResizeStart,
185
+ onResizeEnd: (size) => {
186
+ onResizeEnd?.(size);
187
+ persistenceAdapter?.save?.(size);
188
+ },
189
+ target: 'inspector',
190
+ collapsible,
191
+ snapPoints,
192
+ snapTolerance: snapTolerance ?? 8,
193
+ collapseThreshold,
194
+ requestCollapse: () => shell.setInspectorMode('collapsed'),
195
+ requestToggle: () => shell.togglePane('inspector'),
196
+ }}
197
+ >
198
+ {handleChildren.length > 0 ? handleChildren.map((el, i) => React.cloneElement(el, { key: el.key ?? i })) : <PaneHandle />}
199
+ </PaneResizeContext.Provider>
200
+ ) : null;
201
+
202
+ if (isOverlay) {
203
+ const open = shell.inspectorMode === 'expanded';
204
+ return (
205
+ <Sheet.Root open={open} onOpenChange={(o) => shell.setInspectorMode(o ? 'expanded' : 'collapsed')}>
206
+ <Sheet.Content side="end" style={{ padding: 0 }} width={{ initial: `${expandedSize}px` }}>
207
+ <VisuallyHidden>
208
+ <Sheet.Title>Inspector</Sheet.Title>
209
+ </VisuallyHidden>
210
+ {contentChildren}
211
+ </Sheet.Content>
212
+ </Sheet.Root>
213
+ );
214
+ }
215
+
216
+ return (
217
+ <div
218
+ {...props}
219
+ ref={setRef}
220
+ className={classNames('rt-ShellInspector', className)}
221
+ data-mode={shell.inspectorMode}
222
+ data-peek={shell.peekTarget === 'inspector' || undefined}
223
+ data-presentation={resolvedPresentation}
224
+ data-open={(isStacked && isExpanded) || undefined}
225
+ style={{
226
+ ...style,
227
+ ['--inspector-size' as any]: `${expandedSize}px`,
228
+ ['--inspector-min-size' as any]: `${minSize}px`,
229
+ ['--inspector-max-size' as any]: `${maxSize}px`,
230
+ }}
231
+ >
232
+ <div className="rt-ShellInspectorContent" data-visible={isExpanded || undefined}>
233
+ {contentChildren}
234
+ </div>
235
+ {handleEl}
236
+ </div>
237
+ );
238
+ },
239
+ ) as InspectorComponent;
240
+
241
+ Inspector.displayName = 'Shell.Inspector';
242
+ Inspector.Handle = InspectorHandle;