@adecore/ui 0.21.1 → 0.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +4 -4
  2. package/dist/DisabledReason.js +4 -0
  3. package/dist/Field.js +3 -2
  4. package/dist/Input.js +3 -8
  5. package/dist/SegmentBar.js +2 -1
  6. package/dist/Separator.js +2 -3
  7. package/dist/SlidingColumn.js +1 -1
  8. package/dist/ZoomControls.js +4 -1
  9. package/dist/field-context.d.ts +5 -0
  10. package/dist/field-context.js +6 -0
  11. package/dist/file-icon.js +8 -10
  12. package/dist/file-tree/parts.js +8 -15
  13. package/dist/file-tree/use-file-tree-shift.js +4 -5
  14. package/dist/file-tree/use-slots.js +31 -31
  15. package/dist/format/datetime.js +6 -5
  16. package/dist/format/locale.js +6 -1
  17. package/dist/format/number.js +15 -18
  18. package/dist/menu/parts.d.ts +3 -2
  19. package/dist/menu/parts.js +4 -2
  20. package/dist/modality.js +4 -5
  21. package/dist/placement.d.ts +4 -0
  22. package/dist/placement.js +23 -0
  23. package/dist/popover/parts.d.ts +4 -3
  24. package/dist/popover/parts.js +7 -4
  25. package/dist/shortcut.js +9 -4
  26. package/dist/tabs/parts.js +3 -2
  27. package/dist/tree/parts.js +10 -20
  28. package/dist/tree/use-tree-shift.js +1 -1
  29. package/dist/workspace/SplitView.js +20 -14
  30. package/dist/workspace/TabStrip.js +19 -11
  31. package/dist/workspace/Workspace.js +4 -3
  32. package/dist/workspace/model.js +123 -122
  33. package/package.json +1 -1
  34. package/src/DisabledReason.tsx +4 -5
  35. package/src/Field.tsx +4 -2
  36. package/src/IconButton.tsx +0 -1
  37. package/src/Input.tsx +3 -10
  38. package/src/Pill.tsx +8 -1
  39. package/src/SegmentBar.tsx +3 -10
  40. package/src/Separator.tsx +2 -3
  41. package/src/SlidingColumn.tsx +1 -1
  42. package/src/ZoomControls.tsx +11 -7
  43. package/src/field-context.ts +7 -0
  44. package/src/file-icon.ts +8 -10
  45. package/src/file-tree/parts.tsx +17 -22
  46. package/src/file-tree/use-file-tree-shift.tsx +4 -7
  47. package/src/file-tree/use-slots.ts +31 -31
  48. package/src/format/datetime.ts +7 -5
  49. package/src/format/locale.ts +7 -7
  50. package/src/format/number.ts +17 -20
  51. package/src/menu/parts.tsx +10 -29
  52. package/src/modality.ts +4 -5
  53. package/src/placement.ts +26 -0
  54. package/src/popover/parts.tsx +10 -63
  55. package/src/settings/SettingsDialog.tsx +1 -1
  56. package/src/shortcut.ts +9 -4
  57. package/src/tabs/parts.tsx +8 -8
  58. package/src/tree/parts.tsx +14 -21
  59. package/src/tree/use-tree-shift.tsx +1 -1
  60. package/src/workspace/SplitView.tsx +23 -19
  61. package/src/workspace/TabStrip.tsx +20 -12
  62. package/src/workspace/Workspace.tsx +5 -3
  63. package/src/workspace/model.ts +141 -138
  64. /package/dist/{file-tree → tree}/shift.d.ts +0 -0
  65. /package/dist/{file-tree → tree}/shift.js +0 -0
  66. /package/src/{file-tree → tree}/shift.ts +0 -0
@@ -2,6 +2,7 @@ import type { ComponentProps } from 'react';
2
2
  import { Popover as BasePopover } from '@base-ui-components/react/popover';
3
3
  import { PreviewCard as BasePreviewCard } from '@base-ui-components/react/preview-card';
4
4
  import { withClass } from '../class-name.ts';
5
+ import { splitPlacement, type PlacementKey } from '../placement.ts';
5
6
  import { renderPositioner } from '../popup-layer.ts';
6
7
 
7
8
  /*
@@ -16,10 +17,7 @@ const POPUP_VARIANT = {
16
17
 
17
18
  export type PopupVariant = keyof typeof POPUP_VARIANT;
18
19
 
19
- type Placement = Pick<
20
- ComponentProps<typeof BasePopover.Positioner>,
21
- 'side' | 'align' | 'sideOffset' | 'alignOffset' | 'collisionPadding' | 'collisionBoundary' | 'collisionAvoidance' | 'anchor' | 'sticky' | 'positionMethod'
22
- >;
20
+ type Placement = Pick<ComponentProps<typeof BasePopover.Positioner>, PlacementKey>;
23
21
 
24
22
  export const PopoverRoot = BasePopover.Root;
25
23
 
@@ -37,38 +35,12 @@ export type PopoverPopupProps = ComponentProps<typeof BasePopover.Popup> &
37
35
  };
38
36
 
39
37
  /* The portal, the positioner and the popup of a popover: under its trigger and lined up with its start unless it says otherwise. */
40
- export function PopoverPopup({
41
- variant = 'menu',
42
- side = 'bottom',
43
- align = 'start',
44
- sideOffset = 8,
45
- alignOffset,
46
- collisionPadding,
47
- collisionBoundary,
48
- collisionAvoidance,
49
- anchor,
50
- sticky,
51
- positionMethod,
52
- className,
53
- ...props
54
- }: PopoverPopupProps) {
38
+ export function PopoverPopup({ variant = 'menu', side = 'bottom', align = 'start', sideOffset = 8, className, ...props }: PopoverPopupProps) {
39
+ const [placement, popupProps] = splitPlacement(props);
55
40
  return (
56
41
  <BasePopover.Portal>
57
- <BasePopover.Positioner
58
- className="popup-positioner"
59
- render={renderPositioner}
60
- side={side}
61
- align={align}
62
- sideOffset={sideOffset}
63
- alignOffset={alignOffset}
64
- collisionPadding={collisionPadding}
65
- collisionBoundary={collisionBoundary}
66
- collisionAvoidance={collisionAvoidance}
67
- anchor={anchor}
68
- sticky={sticky}
69
- positionMethod={positionMethod}
70
- >
71
- <BasePopover.Popup className={withClass(POPUP_VARIANT[variant], className)} {...props} />
42
+ <BasePopover.Positioner className="popup-positioner" render={renderPositioner} {...placement} side={side} align={align} sideOffset={sideOffset}>
43
+ <BasePopover.Popup className={withClass(POPUP_VARIANT[variant], className)} {...popupProps} />
72
44
  </BasePopover.Positioner>
73
45
  </BasePopover.Portal>
74
46
  );
@@ -84,37 +56,12 @@ export type PreviewCardPopupProps = ComponentProps<typeof BasePreviewCard.Popup>
84
56
  };
85
57
 
86
58
  /* A card that opens while the pointer rests on its trigger, drawn like a popover. */
87
- export function PreviewCardPopup({
88
- variant = 'menu',
89
- side = 'bottom',
90
- align = 'start',
91
- sideOffset = 8,
92
- alignOffset,
93
- collisionPadding,
94
- collisionBoundary,
95
- collisionAvoidance,
96
- anchor,
97
- sticky,
98
- positionMethod,
99
- className,
100
- ...props
101
- }: PreviewCardPopupProps) {
59
+ export function PreviewCardPopup({ variant = 'menu', side = 'bottom', align = 'start', sideOffset = 8, className, ...props }: PreviewCardPopupProps) {
60
+ const [placement, popupProps] = splitPlacement(props);
102
61
  return (
103
62
  <BasePreviewCard.Portal>
104
- <BasePreviewCard.Positioner
105
- className="z-(--z-popup)"
106
- side={side}
107
- align={align}
108
- sideOffset={sideOffset}
109
- alignOffset={alignOffset}
110
- collisionPadding={collisionPadding}
111
- collisionBoundary={collisionBoundary}
112
- collisionAvoidance={collisionAvoidance}
113
- anchor={anchor}
114
- sticky={sticky}
115
- positionMethod={positionMethod}
116
- >
117
- <BasePreviewCard.Popup className={withClass(POPUP_VARIANT[variant], className)} {...props} />
63
+ <BasePreviewCard.Positioner className="z-(--z-popup)" {...placement} side={side} align={align} sideOffset={sideOffset}>
64
+ <BasePreviewCard.Popup className={withClass(POPUP_VARIANT[variant], className)} {...popupProps} />
118
65
  </BasePreviewCard.Positioner>
119
66
  </BasePreviewCard.Portal>
120
67
  );
@@ -277,7 +277,7 @@ export function SettingsDialog({
277
277
  </div>
278
278
  </div>
279
279
  {/* The header sits outside the panels: inside one it remounts on every section
280
- change, which throws the keyboard's focus away mid-arrow-key. */}
280
+ change, which throws the keyboard's focus away mid-arrow-key. */}
281
281
  <div className="relative flex min-h-0 min-w-0 grow flex-col">
282
282
  <div
283
283
  className={clsx('flex min-w-0 items-start gap-4 px-8 pt-5.5 pb-4.5 max-[960px]:px-4', floating && 'absolute inset-x-0 top-0 z-10')}
package/src/shortcut.ts CHANGED
@@ -37,7 +37,13 @@ const NAMED_KEYS = new Set(['Enter', 'Escape', 'Backspace', 'Delete', 'Tab', 'Sp
37
37
 
38
38
  const FUNCTION_KEY = /^F([1-9]|1[0-2])$/;
39
39
 
40
- const MODIFIER_WORDS = ['Mod', 'Ctrl', 'Meta', 'Alt', 'Shift'] as const;
40
+ const MODIFIER_FIELDS: Readonly<Record<string, 'mod' | 'ctrl' | 'meta' | 'alt' | 'shift'>> = {
41
+ Mod: 'mod',
42
+ Ctrl: 'ctrl',
43
+ Meta: 'meta',
44
+ Alt: 'alt',
45
+ Shift: 'shift'
46
+ };
41
47
 
42
48
  const isKey = (word: string): boolean =>
43
49
  /^[A-Za-z0-9]$/.test(word) || word in PUNCTUATION_CODES || CHARACTER_KEYS.has(word) || NAMED_KEYS.has(word) || FUNCTION_KEY.test(word);
@@ -51,9 +57,8 @@ export const shortcut = (text: string): Shortcut => {
51
57
  const words = text === '+' ? ['+'] : text.endsWith('++') ? [...text.slice(0, -2).split('+'), '+'] : text.split('+');
52
58
  const result: Shortcut = { mod: false, ctrl: false, meta: false, alt: false, shift: false, key: '' };
53
59
  for (const word of words) {
54
- const modifier = MODIFIER_WORDS.find((candidate) => candidate === word);
55
- if (modifier) {
56
- result[modifier === 'Mod' ? 'mod' : modifier === 'Ctrl' ? 'ctrl' : modifier === 'Meta' ? 'meta' : modifier === 'Alt' ? 'alt' : 'shift'] = true;
60
+ if (Object.hasOwn(MODIFIER_FIELDS, word)) {
61
+ result[MODIFIER_FIELDS[word]!] = true;
57
62
  continue;
58
63
  }
59
64
  if (!isKey(word)) {
@@ -124,6 +124,12 @@ export function TabsList({ className, children, start, end, ...props }: TabsList
124
124
 
125
125
  const visible = shown === null ? tabs : shown.map((index) => tabs[index]!);
126
126
  const hidden = shown === null ? [] : tabs.filter((_, index) => !shown.includes(index));
127
+ const more = (
128
+ <>
129
+ {t('tabs.more')}
130
+ <Icon icon={ChevronDown} size={14} />
131
+ </>
132
+ );
127
133
 
128
134
  return (
129
135
  <div ref={strip} className={clsx('relative flex items-center gap-4 border-b border-border', className)}>
@@ -137,10 +143,7 @@ export function TabsList({ className, children, start, end, ...props }: TabsList
137
143
  </BaseTabs.List>
138
144
  {hidden.length > 0 && (
139
145
  <MenuRoot>
140
- <MenuTrigger className={MORE}>
141
- {t('tabs.more')}
142
- <Icon icon={ChevronDown} size={14} />
143
- </MenuTrigger>
146
+ <MenuTrigger className={MORE}>{more}</MenuTrigger>
144
147
  <MenuPopup align="end" className="min-w-48">
145
148
  <InMenuContext value>
146
149
  {hidden.map((tab) => (
@@ -164,10 +167,7 @@ export function TabsList({ className, children, start, end, ...props }: TabsList
164
167
  {tab.props.onClose !== undefined && <span className="size-5 shrink-0" />}
165
168
  </span>
166
169
  ))}
167
- <span className={MORE}>
168
- {t('tabs.more')}
169
- <Icon icon={ChevronDown} size={14} />
170
- </span>
170
+ <span className={MORE}>{more}</span>
171
171
  </div>
172
172
  </div>
173
173
  );
@@ -1,4 +1,4 @@
1
- import { useCallback, type MouseEvent, type KeyboardEvent, type PointerEvent } from 'react';
1
+ import { useCallback, type MouseEvent } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { useRender } from '@base-ui-components/react/use-render';
4
4
  import { ChevronRight } from 'lucide-react';
@@ -9,6 +9,14 @@ import { mergeRefs } from '../merge-refs.ts';
9
9
  import { treeRowStyle } from './style.ts';
10
10
  import { TREE_ROWS, useTreeShift } from './use-tree-shift.tsx';
11
11
 
12
+ /* A handler that keeps the event to the control, so the row under it never sees the press. */
13
+ const stopped =
14
+ <Event extends { stopPropagation(): void }>(handler?: (event: Event) => void) =>
15
+ (event: Event): void => {
16
+ event.stopPropagation();
17
+ handler?.(event);
18
+ };
19
+
12
20
  export type TreeRootProps = useRender.ComponentProps<'div'> & {
13
21
  /* What a row wider than the tree does: cut its name off at the end, or keep the row whole and let every row slide sideways together, as a file tree does. */
14
22
  overflow?: 'truncate' | 'scroll';
@@ -127,10 +135,7 @@ export function TreeChevron({ expanded, onExpandedChange, render, className, ref
127
135
  onExpandedChange?.(!expanded);
128
136
  }
129
137
  },
130
- onDoubleClick: (event: MouseEvent<HTMLButtonElement>) => {
131
- event.stopPropagation();
132
- onDoubleClick?.(event);
133
- },
138
+ onDoubleClick: stopped(onDoubleClick),
134
139
  children: <Icon icon={ChevronRight} size={12} className={expanded ? 'rotate-90' : undefined} />
135
140
  }
136
141
  });
@@ -156,22 +161,10 @@ export function TreeControl({ render, className, ref, onClick, onDoubleClick, on
156
161
  ...props,
157
162
  'data-tree-control': true,
158
163
  className: clsx('adecore-tree-control', className),
159
- onClick: (event: MouseEvent<HTMLSpanElement>) => {
160
- event.stopPropagation();
161
- onClick?.(event);
162
- },
163
- onDoubleClick: (event: MouseEvent<HTMLSpanElement>) => {
164
- event.stopPropagation();
165
- onDoubleClick?.(event);
166
- },
167
- onKeyDown: (event: KeyboardEvent<HTMLSpanElement>) => {
168
- event.stopPropagation();
169
- onKeyDown?.(event);
170
- },
171
- onPointerDown: (event: PointerEvent<HTMLSpanElement>) => {
172
- event.stopPropagation();
173
- onPointerDown?.(event);
174
- }
164
+ onClick: stopped(onClick),
165
+ onDoubleClick: stopped(onDoubleClick),
166
+ onKeyDown: stopped(onKeyDown),
167
+ onPointerDown: stopped(onPointerDown)
175
168
  }
176
169
  });
177
170
  }
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useState, type ReactElement } from 'react';
2
- import { clampShift, maxShift, SHIFT_PROPERTY, shiftNeed, shiftThumb, sidewaysDelta } from '../file-tree/shift.ts';
2
+ import { clampShift, maxShift, SHIFT_PROPERTY, shiftNeed, shiftThumb, sidewaysDelta } from './shift.ts';
3
3
 
4
4
  const HIDE_AFTER = 900;
5
5
 
@@ -8,6 +8,7 @@ import {
8
8
  useState,
9
9
  type CSSProperties,
10
10
  type DragEvent,
11
+ type FocusEvent,
11
12
  type KeyboardEvent,
12
13
  type ReactNode
13
14
  } from 'react';
@@ -90,6 +91,12 @@ interface Measurement {
90
91
  const ZERO: Measurement = { width: 0, height: 0, left: 0, top: 0, scaleX: 1, scaleY: 1 };
91
92
  const MINIMUM: SplitMinimum = { width: 120, height: 80 };
92
93
 
94
+ /* The pane's section or header under `container`, found by its data attribute rather than a selector, so no id needs escaping. */
95
+ function paneElement(container: ParentNode | null | undefined, attribute: 'splitPane' | 'splitHeader', paneId: string): HTMLElement | undefined {
96
+ const selector = attribute === 'splitPane' ? '[data-split-pane]' : '[data-split-header]';
97
+ return Array.from(container?.querySelectorAll<HTMLElement>(selector) ?? []).find((element) => element.dataset[attribute] === paneId);
98
+ }
99
+
93
100
  function Divider({
94
101
  divider,
95
102
  minimumResizeShare,
@@ -227,7 +234,7 @@ export function SplitView({
227
234
  (!previous.isConnected || previous.closest('[hidden]')) &&
228
235
  (!active || active === element.ownerDocument.body || active === previous)
229
236
  ) {
230
- const pane = Array.from(element.querySelectorAll<HTMLElement>('[data-split-pane]')).find((item) => item.dataset.splitPane === value.focused);
237
+ const pane = paneElement(element, 'splitPane', value.focused);
231
238
  const target = pane?.querySelector<HTMLElement>('[role="tab"][aria-selected="true"]') ?? pane;
232
239
  target?.focus({ preventScroll: true });
233
240
  }
@@ -312,6 +319,14 @@ export function SplitView({
312
319
  .sort((left, right) => (left.viewId < right.viewId ? -1 : left.viewId > right.viewId ? 1 : 0));
313
320
  const visible = new Map(geometry.panes.map((pane) => [pane.pane.id, pane]));
314
321
  const radius = (rect: SplitRect): string => paneRadius(rect, measurement.width, measurement.height, edges, layout === 'roomy' ? 8 : 0);
322
+ // A header squares off the top corners of the body under it.
323
+ const bodyRadius = (rect: SplitRect, header: number): string => {
324
+ const corners = radius(rect).split(' ');
325
+ if (header > 0) {
326
+ corners[0] = corners[1] = '0px';
327
+ }
328
+ return corners.join(' ');
329
+ };
315
330
  const bounds: SplitViewBounds[] = panes.flatMap((pane) => {
316
331
  const rect = visible.get(pane.id);
317
332
  const header = Math.min(barHeight, rect?.height ?? 0);
@@ -323,11 +338,8 @@ export function SplitView({
323
338
  height: Math.max(0, rect.height - header) * measurement.scaleY
324
339
  }
325
340
  : { x: 0, y: 0, width: 0, height: 0 };
326
- const corners = rect ? radius(rect).split(' ') : ['0px', '0px', '0px', '0px'];
327
- if (header > 0) {
328
- corners[0] = corners[1] = '0px';
329
- }
330
- return pane.views.map((viewId) => ({ viewId, paneId: pane.id, active: !!rect && pane.active === viewId, rect: body, borderRadius: corners.join(' ') }));
341
+ const borderRadius = rect ? bodyRadius(rect, header) : '0px 0px 0px 0px';
342
+ return pane.views.map((viewId) => ({ viewId, paneId: pane.id, active: !!rect && pane.active === viewId, rect: body, borderRadius }));
331
343
  });
332
344
  const boundsKey = JSON.stringify(bounds);
333
345
  const reportedBounds = useRef('');
@@ -379,9 +391,7 @@ export function SplitView({
379
391
  return null;
380
392
  }
381
393
  const header = Math.min(barHeight, rect.height);
382
- const strip = Array.from(root.current?.querySelectorAll<HTMLElement>('[data-split-header]') ?? []).find(
383
- (element) => element.dataset.splitHeader === rect.pane.id
384
- );
394
+ const strip = paneElement(root.current, 'splitHeader', rect.pane.id);
385
395
  const tabRects = Array.from(strip?.querySelectorAll('[data-document-tab]') ?? [], (element) => element.getBoundingClientRect());
386
396
  const inHeader = y < rect.y + header;
387
397
  const side = inHeader ? undefined : (splitSideAt(rect, x, y) ?? 'center');
@@ -446,9 +456,7 @@ export function SplitView({
446
456
  const next = options[0];
447
457
  if (next) {
448
458
  dispatch({ type: 'focus', paneId: next.pane.id });
449
- const element = Array.from(root.current?.querySelectorAll<HTMLElement>('[data-split-pane]') ?? []).find(
450
- (pane) => pane.dataset.splitPane === next.pane.id
451
- );
459
+ const element = paneElement(root.current, 'splitPane', next.pane.id);
452
460
  element?.querySelector<HTMLElement>('[role="tab"][aria-selected="true"]')?.focus();
453
461
  if (!showTabs) {
454
462
  element?.focus();
@@ -465,7 +473,7 @@ export function SplitView({
465
473
  style,
466
474
  'data-layout': layout,
467
475
  'data-dragging': dragging || undefined,
468
- onFocusCapture: (event: React.FocusEvent<HTMLDivElement>) => {
476
+ onFocusCapture: (event: FocusEvent<HTMLDivElement>) => {
469
477
  props.onFocusCapture?.(event);
470
478
  lastFocus.current = event.target;
471
479
  },
@@ -591,17 +599,13 @@ export function SplitView({
591
599
  const rect = visible.get(pane.id);
592
600
  const active = !!rect && pane.active === viewId;
593
601
  const header = Math.min(barHeight, rect?.height ?? 0);
594
- const corners = rect ? radius(rect).split(' ') : [];
595
- if (header > 0) {
596
- corners[0] = corners[1] = '0px';
597
- }
598
602
  const position: CSSProperties = rect
599
603
  ? {
600
604
  left: rect.x,
601
605
  top: rect.y + header,
602
606
  width: rect.width,
603
607
  height: Math.max(0, rect.height - header),
604
- borderRadius: corners.join(' ')
608
+ borderRadius: bodyRadius(rect, header)
605
609
  }
606
610
  : {};
607
611
  return (
@@ -637,7 +641,7 @@ export function SplitView({
637
641
  type: 'equalizeAxis',
638
642
  axis: divider.axis,
639
643
  length: divider.axis === 'horizontal' ? measurement.width : measurement.height,
640
- gap: layout === 'roomy' ? 8 : 1
644
+ gap
641
645
  }
642
646
  : { type: 'equalize', splitId: divider.splitId, index: divider.index }
643
647
  )
@@ -21,6 +21,22 @@ import { IconButton, type IconButtonProps } from '../IconButton.tsx';
21
21
  import { Tooltip } from '../Tooltip.tsx';
22
22
  import { tabGapAt } from './geometry.ts';
23
23
 
24
+ /* Where an arrow, Home or End moves the selection from `index`, wrapping at either end; null for any other key. */
25
+ function tabIndexForKey(key: string, index: number, count: number): number | null {
26
+ switch (key) {
27
+ case 'ArrowRight':
28
+ return (index + 1) % count;
29
+ case 'ArrowLeft':
30
+ return (index + count - 1) % count;
31
+ case 'Home':
32
+ return 0;
33
+ case 'End':
34
+ return count - 1;
35
+ default:
36
+ return null;
37
+ }
38
+ }
39
+
24
40
  export interface TabStripItem {
25
41
  id: string;
26
42
  label: string;
@@ -213,6 +229,7 @@ export function DocumentTab({ item, onClose, closeShortcut, onDoubleClick, onDra
213
229
  const { items, value, onValueChange } = context;
214
230
  const index = items.findIndex((candidate) => candidate.id === item.id);
215
231
  const close = (): void => context.close(item.id, onClose);
232
+ const closable = item.closable !== false && (onClose ?? context.onClose) !== undefined;
216
233
  return useRender({
217
234
  render,
218
235
  ref,
@@ -247,21 +264,12 @@ export function DocumentTab({ item, onClose, closeShortcut, onDoubleClick, onDra
247
264
  }}
248
265
  onDragEnd={onDragEnd ?? context.onDragEnd}
249
266
  onKeyDown={(event) => {
250
- const next =
251
- event.key === 'ArrowRight'
252
- ? (index + 1) % items.length
253
- : event.key === 'ArrowLeft'
254
- ? (index + items.length - 1) % items.length
255
- : event.key === 'Home'
256
- ? 0
257
- : event.key === 'End'
258
- ? items.length - 1
259
- : null;
267
+ const next = tabIndexForKey(event.key, index, items.length);
260
268
  if (next !== null) {
261
269
  event.preventDefault();
262
270
  onValueChange(items[next]!.id);
263
271
  context.focus(next);
264
- } else if (event.key === 'Delete' && item.closable !== false && (onClose || context.onClose)) {
272
+ } else if (event.key === 'Delete' && closable) {
265
273
  event.preventDefault();
266
274
  close();
267
275
  }
@@ -279,7 +287,7 @@ export function DocumentTab({ item, onClose, closeShortcut, onDoubleClick, onDra
279
287
  </button>
280
288
  </Tooltip>
281
289
  {item.pinned && <Icon icon={Pin} size={12} className="shrink-0 text-text-muted" />}
282
- {item.closable !== false && (onClose || context.onClose) && (
290
+ {closable && (
283
291
  <IconButton
284
292
  icon={X}
285
293
  size="2xs"
@@ -15,6 +15,8 @@ export interface WorkspaceProps extends useRender.ComponentProps<'div'> {
15
15
  sidePanelOpen?: boolean;
16
16
  }
17
17
 
18
+ const isPresent = (node: ReactNode): boolean => node !== undefined && node !== null && node !== false;
19
+
18
20
  export function Workspace({
19
21
  layout = 'standard',
20
22
  toolbar,
@@ -31,11 +33,11 @@ export function Workspace({
31
33
  style,
32
34
  ...props
33
35
  }: WorkspaceProps) {
34
- const hasSidebar = sidebar !== undefined && sidebar !== null && sidebar !== false;
35
- const hasPanel = sidePanel !== undefined && sidePanel !== null && sidePanel !== false;
36
+ const hasSidebar = isPresent(sidebar);
37
+ const hasPanel = isPresent(sidePanel);
36
38
  const leftOpen = hasSidebar && (sidebarOpen ?? true);
37
39
  const rightOpen = hasPanel && (sidePanelOpen ?? true);
38
- const hasToolbar = toolbar !== undefined && toolbar !== null && toolbar !== false;
40
+ const hasToolbar = isPresent(toolbar);
39
41
  const element = useRender({
40
42
  render,
41
43
  ref,