@adecore/ui 0.23.0 → 0.24.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.
Files changed (129) hide show
  1. package/dist/ButtonGroup.d.ts +1 -1
  2. package/dist/ButtonGroup.js +1 -1
  3. package/dist/Checkbox.js +1 -1
  4. package/dist/ChoiceCards.js +2 -2
  5. package/dist/CloseButton.js +1 -1
  6. package/dist/ColorSwatch.d.ts +1 -1
  7. package/dist/ColorSwatch.js +1 -1
  8. package/dist/Field.d.ts +1 -1
  9. package/dist/Field.js +4 -3
  10. package/dist/IconButton.d.ts +1 -1
  11. package/dist/IconButton.js +1 -1
  12. package/dist/ListRow.d.ts +1 -1
  13. package/dist/ListRow.js +1 -1
  14. package/dist/MiddleTruncate.d.ts +8 -0
  15. package/dist/MiddleTruncate.js +59 -0
  16. package/dist/PanelHeader.d.ts +1 -1
  17. package/dist/PanelHeader.js +1 -1
  18. package/dist/SectionLabel.d.ts +1 -1
  19. package/dist/SectionLabel.js +1 -1
  20. package/dist/Select.js +1 -1
  21. package/dist/SelectSearch.js +1 -1
  22. package/dist/Slider.js +1 -1
  23. package/dist/Surface.d.ts +1 -1
  24. package/dist/Surface.js +1 -1
  25. package/dist/Switch.js +1 -1
  26. package/dist/Toasts.d.ts +1 -1
  27. package/dist/Toasts.js +4 -5
  28. package/dist/Tooltip.d.ts +1 -1
  29. package/dist/Tooltip.js +2 -2
  30. package/dist/clipboard.d.ts +1 -1
  31. package/dist/clipboard.js +12 -2
  32. package/dist/dialog/parts.d.ts +3 -3
  33. package/dist/dialog/parts.js +2 -2
  34. package/dist/file-tree/helpers.d.ts +1 -0
  35. package/dist/file-tree/helpers.js +9 -0
  36. package/dist/file-tree/parts.d.ts +1 -1
  37. package/dist/file-tree/parts.js +1 -1
  38. package/dist/file-tree/use-slots.d.ts +8 -0
  39. package/dist/file-tree/use-slots.js +64 -22
  40. package/dist/format/datetime.d.ts +1 -0
  41. package/dist/format/datetime.js +15 -0
  42. package/dist/format/index.d.ts +1 -1
  43. package/dist/format/index.js +1 -1
  44. package/dist/index.d.ts +6 -3
  45. package/dist/index.js +5 -1
  46. package/dist/key-value-list/parts.d.ts +1 -1
  47. package/dist/key-value-list/parts.js +1 -1
  48. package/dist/locales/en.d.ts +83 -0
  49. package/dist/locales/en.js +82 -0
  50. package/dist/locales/nl.d.ts +83 -0
  51. package/dist/locales/nl.js +82 -0
  52. package/dist/locales.js +3 -2
  53. package/dist/menu/parts.d.ts +7 -7
  54. package/dist/menu/parts.js +4 -4
  55. package/dist/middle-truncate.d.ts +7 -0
  56. package/dist/middle-truncate.js +34 -0
  57. package/dist/popover/parts.d.ts +7 -7
  58. package/dist/popover/parts.js +2 -2
  59. package/dist/prefetch.js +6 -1
  60. package/dist/settings/ConfirmDialog.d.ts +4 -1
  61. package/dist/settings/ConfirmDialog.js +2 -2
  62. package/dist/settings/SettingsDialog.js +1 -1
  63. package/dist/tabs/parts.d.ts +8 -4
  64. package/dist/tabs/parts.js +84 -18
  65. package/dist/testing/dedupe.js +74 -23
  66. package/dist/theme.css +24 -8
  67. package/dist/tree/parts.d.ts +1 -1
  68. package/dist/tree/parts.js +1 -1
  69. package/dist/workspace/DropPreview.d.ts +8 -0
  70. package/dist/workspace/DropPreview.js +14 -0
  71. package/dist/workspace/SplitView.d.ts +1 -1
  72. package/dist/workspace/SplitView.js +59 -18
  73. package/dist/workspace/TabStrip.d.ts +1 -1
  74. package/dist/workspace/TabStrip.js +1 -1
  75. package/dist/workspace/Workspace.d.ts +2 -2
  76. package/dist/workspace/Workspace.js +4 -2
  77. package/dist/workspace/preview.d.ts +4 -8
  78. package/dist/workspace/preview.js +17 -2
  79. package/package.json +2 -2
  80. package/src/ButtonGroup.tsx +1 -1
  81. package/src/Checkbox.tsx +1 -1
  82. package/src/ChoiceCards.tsx +2 -2
  83. package/src/CloseButton.tsx +1 -1
  84. package/src/ColorSwatch.tsx +1 -1
  85. package/src/ErrorBoundary.tsx +4 -4
  86. package/src/Field.tsx +4 -5
  87. package/src/IconButton.tsx +1 -1
  88. package/src/ListRow.tsx +1 -1
  89. package/src/MiddleTruncate.tsx +84 -0
  90. package/src/PanelHeader.tsx +1 -1
  91. package/src/SectionLabel.tsx +1 -1
  92. package/src/Select.tsx +1 -1
  93. package/src/SelectSearch.tsx +1 -1
  94. package/src/Slider.tsx +1 -1
  95. package/src/Surface.tsx +1 -1
  96. package/src/Switch.tsx +1 -1
  97. package/src/Toasts.tsx +5 -6
  98. package/src/Tooltip.tsx +5 -5
  99. package/src/clipboard.ts +11 -2
  100. package/src/dialog/parts.tsx +2 -2
  101. package/src/file-tree/helpers.ts +10 -0
  102. package/src/file-tree/parts.tsx +1 -1
  103. package/src/file-tree/use-slots.ts +68 -23
  104. package/src/format/datetime.ts +17 -0
  105. package/src/format/index.ts +1 -0
  106. package/src/index.ts +6 -3
  107. package/src/key-value-list/parts.tsx +1 -1
  108. package/src/locales/en.ts +82 -0
  109. package/src/locales/nl.ts +82 -0
  110. package/src/locales.ts +3 -2
  111. package/src/menu/parts.tsx +4 -4
  112. package/src/middle-truncate.ts +41 -0
  113. package/src/popover/parts.tsx +2 -2
  114. package/src/prefetch.ts +6 -1
  115. package/src/settings/ConfirmDialog.tsx +7 -1
  116. package/src/settings/SettingsDialog.tsx +1 -1
  117. package/src/tabs/parts.tsx +156 -54
  118. package/src/testing/dedupe.ts +87 -24
  119. package/src/theme.css +24 -8
  120. package/src/tree/parts.tsx +1 -1
  121. package/src/workspace/DropPreview.tsx +26 -0
  122. package/src/workspace/SplitView.tsx +58 -22
  123. package/src/workspace/TabStrip.tsx +1 -1
  124. package/src/workspace/Workspace.tsx +7 -3
  125. package/src/workspace/preview.ts +22 -9
  126. package/dist/locales/en.json +0 -82
  127. package/dist/locales/nl.json +0 -82
  128. package/src/locales/en.json +0 -82
  129. package/src/locales/nl.json +0 -82
package/src/Tooltip.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useCallback, type ComponentProps, type ReactElement, type ReactNode } from 'react';
2
- import { Tooltip as BaseTooltip } from '@base-ui-components/react/tooltip';
2
+ import { Tooltip as BaseTooltip } from '@base-ui/react/tooltip';
3
3
  import { Kbd } from './Kbd.tsx';
4
4
  import type { Shortcut } from './shortcut.ts';
5
5
  import { registerShortcutHint } from './shortcut-hints.ts';
@@ -53,10 +53,10 @@ export function Tooltip({ label, open, onOpenChange, anchor, kbd, side = 'top',
53
53
  <BaseTooltip.Portal>
54
54
  <BaseTooltip.Positioner anchor={anchor} side={side} sideOffset={sideOffset} className="tooltip-positioner">
55
55
  <BaseTooltip.Popup role="tooltip" className="tooltip-popup">
56
- <BaseTooltip.Viewport className="max-w-72 px-[9px] py-[5px] text-pretty">
56
+ {/* Not Base UI's Viewport: that one morphs between the triggers of one popup and
57
+ measures and clones its content to do so, while every hint here has a root of its own. */}
58
+ <div className="max-w-72 px-[9px] py-[5px] text-pretty">
57
59
  <span className="wrap-anywhere">{label}</span>
58
- {/* The viewport wraps its children in a div of its own, so the 8px
59
- between the label and the shortcut has to sit on the shortcut itself. */}
60
60
  {kbd !== undefined &&
61
61
  (typeof kbd === 'string' ? (
62
62
  <Kbd variant="inline" className="ml-2">
@@ -65,7 +65,7 @@ export function Tooltip({ label, open, onOpenChange, anchor, kbd, side = 'top',
65
65
  ) : (
66
66
  <Kbd shortcut={kbd} variant="inline" className="ml-2" />
67
67
  ))}
68
- </BaseTooltip.Viewport>
68
+ </div>
69
69
  </BaseTooltip.Popup>
70
70
  </BaseTooltip.Positioner>
71
71
  </BaseTooltip.Portal>
package/src/clipboard.ts CHANGED
@@ -3,8 +3,17 @@
3
3
  * and allowed, and there is nothing a person can do about a refusal, so both sides stay quiet.
4
4
  */
5
5
 
6
- export const copyText = (text: string): void => {
7
- void navigator.clipboard?.writeText(text).catch(() => undefined);
6
+ /* Resolves to whether the text got there, for a caller that tells the person; it never rejects. */
7
+ export const copyText = async (text: string): Promise<boolean> => {
8
+ try {
9
+ if (!navigator.clipboard) {
10
+ return false;
11
+ }
12
+ await navigator.clipboard.writeText(text);
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
8
17
  };
9
18
 
10
19
  /* What is on the clipboard, or nothing at all when the browser will not say. */
@@ -1,7 +1,7 @@
1
1
  import { createContext, useContext, useState, type ComponentProps } from 'react';
2
2
  import clsx from 'clsx';
3
- import { Dialog as BaseDialog } from '@base-ui-components/react/dialog';
4
- import { useRender } from '@base-ui-components/react/use-render';
3
+ import { Dialog as BaseDialog } from '@base-ui/react/dialog';
4
+ import { useRender } from '@base-ui/react/use-render';
5
5
  import { withClass } from '../class-name.ts';
6
6
  import { useDialogLayer } from '../dialog-layer.ts';
7
7
 
@@ -21,6 +21,16 @@ export function visibleRows(model: FileTree): FileTreeVisibleRow[] {
21
21
  return model.getVisibleRows(0, model.getVisibleCount()).map((row) => ({ ...row, path: pathOfRow(row) }));
22
22
  }
23
23
 
24
+ /* The `count` rows from `start` and one on each side, for a neighbor's selection, without copying the whole list. */
25
+ export function visibleRowsAround(model: FileTree, start: number, count: number): FileTreeVisibleRow[] {
26
+ const first = Math.max(0, start - 1);
27
+ const last = Math.min(model.getVisibleCount() - 1, start + count);
28
+ if (last < first) {
29
+ return [];
30
+ }
31
+ return model.getVisibleRows(first, last).map((row) => ({ ...row, path: pathOfRow(row) }));
32
+ }
33
+
24
34
  export function applyExpansion(model: FileTree, collapsed: ReadonlySet<string>, keyOf?: FoldKeyOf): void {
25
35
  for (let pass = 0; pass < EXPANSION_PASSES; pass++) {
26
36
  const { collapse, expand } = expansionChanges(visibleRows(model), collapsed, keyOf);
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useEffect, useLayoutEffect, useRef, useState, type ReactNode } from 'react';
2
2
  import { createPortal } from 'react-dom';
3
- import { useRender } from '@base-ui-components/react/use-render';
3
+ import { useRender } from '@base-ui/react/use-render';
4
4
  import { FileTree as PierreFileTree, type FileTreeProps as PierreProps } from '@pierre/trees/react';
5
5
  import type { FileTree, FileTreeVisibleRow } from '@pierre/trees';
6
6
  import { useTranslation } from 'react-i18next';
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useId, useState } from 'react';
2
2
  import { flushSync } from 'react-dom';
3
3
  import type { FileTree, FileTreeVisibleRow } from '@pierre/trees';
4
- import { pathOfRow, visibleRows } from './helpers.ts';
4
+ import { pathOfRow, visibleRows, visibleRowsAround } from './helpers.ts';
5
5
  import { FILE_TREE_CSS } from './style.ts';
6
6
 
7
7
  export interface RowSlot {
@@ -10,7 +10,7 @@ export interface RowSlot {
10
10
  decoration: HTMLElement;
11
11
  }
12
12
 
13
- interface Port {
13
+ export interface Port {
14
14
  rowElement: HTMLElement;
15
15
  control: HTMLElement;
16
16
  decoration: HTMLElement;
@@ -18,6 +18,66 @@ interface Port {
18
18
  lightDecoration: HTMLElement;
19
19
  }
20
20
 
21
+ /*
22
+ * The rows of the mounted window, read from the spacer beta.6 puts above it (its first index times the
23
+ * row height), so a scroll frame copies those and not every visible row. The whole list stands in when
24
+ * the spacer is missing or the model already moved past what is mounted.
25
+ */
26
+ function mountedRows(model: FileTree, shadow: ShadowRoot, elements: readonly HTMLElement[]): FileTreeVisibleRow[] {
27
+ const spacer = shadow.querySelector<HTMLElement>('[data-file-tree-virtualized-sticky-offset]');
28
+ const offset = spacer === null ? Number.NaN : Number.parseFloat(spacer.style.height);
29
+ const itemHeight = model.getItemHeight();
30
+ if (!Number.isFinite(offset) || itemHeight <= 0) {
31
+ return visibleRows(model);
32
+ }
33
+ // A folder pinned to the top leaves the flow, but still takes its index in the window.
34
+ const pinned = shadow.querySelectorAll('[data-file-tree-sticky-row="true"]').length;
35
+ const rows = visibleRowsAround(model, Math.round(offset / itemHeight), elements.length + pinned);
36
+ const paths = new Set(rows.map((row) => row.path));
37
+ return elements.every((element) => element.dataset.itemPath === undefined || paths.has(element.dataset.itemPath)) ? rows : visibleRows(model);
38
+ }
39
+
40
+ /*
41
+ * Lines the ports up with their rows: every size first, then the spaces they reserve, then every offset,
42
+ * then the ports, so a pass forces two layouts at most however many rows are mounted.
43
+ */
44
+ export function positionPorts(ports: Iterable<Port>): void {
45
+ const spaced = [...ports].map((port) => ({
46
+ port,
47
+ control: port.lightControl.childNodes.length > 0 ? Math.ceil(port.lightControl.getBoundingClientRect().width) + 6 : 0,
48
+ decoration: port.lightDecoration.childNodes.length > 0 ? Math.ceil(port.lightDecoration.getBoundingClientRect().width) + 6 : 0
49
+ }));
50
+ for (const { port, control, decoration } of spaced) {
51
+ setSpace(port.rowElement, '--adecore-tree-control-space', control);
52
+ setSpace(port.rowElement, '--adecore-tree-decoration-space', decoration);
53
+ }
54
+ const placed = spaced.map(({ port, control, decoration }) => {
55
+ const row = port.rowElement;
56
+ const content = row.querySelector<HTMLElement>('[data-item-section="content"]');
57
+ const icon = row.querySelector<HTMLElement>('[data-item-section="icon"]');
58
+ return {
59
+ port,
60
+ top: `${row.offsetTop}px`,
61
+ controlLeft: `${row.offsetLeft + (icon?.offsetLeft ?? 4) - control}px`,
62
+ decorationLeft: `${row.offsetLeft + (content === null ? row.offsetWidth - decoration - 2 : content.offsetLeft + content.offsetWidth + 6)}px`
63
+ };
64
+ });
65
+ for (const { port, top, controlLeft, decorationLeft } of placed) {
66
+ port.control.style.top = top;
67
+ port.control.style.left = controlLeft;
68
+ port.decoration.style.top = top;
69
+ port.decoration.style.left = decorationLeft;
70
+ }
71
+ }
72
+
73
+ // An unchanged space is left alone, so a pass that only scrolled measures once.
74
+ function setSpace(row: HTMLElement, name: string, space: number): void {
75
+ const value = `${space}px`;
76
+ if (row.style.getPropertyValue(name) !== value) {
77
+ row.style.setProperty(name, value);
78
+ }
79
+ }
80
+
21
81
  // beta.6 owns button rows. Sibling slots keep real controls outside those buttons and in the app's stylesheet.
22
82
  export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, label: string) {
23
83
  const prefix = useId().replaceAll(':', '');
@@ -35,23 +95,7 @@ export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, lab
35
95
  let pending = false;
36
96
  let snapshot: string | null = null;
37
97
 
38
- const position = (): void => {
39
- for (const port of ports.values()) {
40
- const row = port.rowElement;
41
- const content = row.querySelector<HTMLElement>('[data-item-section="content"]');
42
- const icon = row.querySelector<HTMLElement>('[data-item-section="icon"]');
43
- const control = port.lightControl;
44
- const decoration = port.lightDecoration;
45
- const controlSpace = control.childNodes.length > 0 ? Math.ceil(control.getBoundingClientRect().width) + 6 : 0;
46
- const decorationSpace = decoration.childNodes.length > 0 ? Math.ceil(decoration.getBoundingClientRect().width) + 6 : 0;
47
- row.style.setProperty('--adecore-tree-control-space', `${controlSpace}px`);
48
- row.style.setProperty('--adecore-tree-decoration-space', `${decorationSpace}px`);
49
- port.control.style.top = `${row.offsetTop}px`;
50
- port.control.style.left = `${row.offsetLeft + (icon?.offsetLeft ?? 4) - controlSpace}px`;
51
- port.decoration.style.top = `${row.offsetTop}px`;
52
- port.decoration.style.left = `${row.offsetLeft + (content === null ? row.offsetWidth - decorationSpace - 2 : content.offsetLeft + content.offsetWidth + 6)}px`;
53
- }
54
- };
98
+ const position = (): void => positionPorts(ports.values());
55
99
 
56
100
  const resize = new ResizeObserver(position);
57
101
  const dropPort = (port: Port): void => {
@@ -106,11 +150,12 @@ export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, lab
106
150
  resize.observe(current);
107
151
  }
108
152
  shadow.querySelector('[role="tree"]')?.setAttribute('aria-label', label);
109
- const rows = visibleRows(model);
110
- const byPath = new Map(rows.map((row) => [pathOfRow(row), row]));
111
153
  const elements = [
112
154
  ...shadow.querySelectorAll<HTMLElement>('[data-type="item"]:not([data-item-parked="true"]):not([data-file-tree-sticky-row="true"])')
113
155
  ];
156
+ const rows = mountedRows(model, shadow, elements);
157
+ const byPath = new Map(rows.map((row) => [pathOfRow(row), row]));
158
+ const byIndex = new Map(rows.map((row) => [row.index, row]));
114
159
  const live = new Set(elements);
115
160
  for (const [element, port] of ports) {
116
161
  if (!live.has(element)) {
@@ -126,8 +171,8 @@ export function useFileTreeSlots(model: FileTree, frame: HTMLElement | null, lab
126
171
  continue;
127
172
  }
128
173
  element.dataset.interactive = 'true';
129
- element.dataset.joinedStart = String(row.isSelected && rows[row.index - 1]?.isSelected === true);
130
- element.dataset.joinedEnd = String(row.isSelected && rows[row.index + 1]?.isSelected === true);
174
+ element.dataset.joinedStart = String(row.isSelected && byIndex.get(row.index - 1)?.isSelected === true);
175
+ element.dataset.joinedEnd = String(row.isSelected && byIndex.get(row.index + 1)?.isSelected === true);
131
176
  let port = ports.get(element);
132
177
  if (port === undefined) {
133
178
  serial++;
@@ -1,3 +1,4 @@
1
+ import { formatAgo } from './duration.ts';
1
2
  import { dateFormatter, wordFormatter } from './locale.ts';
2
3
 
3
4
  /* The parts that are a word rather than a number, and so come from the language and not the region. */
@@ -63,3 +64,19 @@ export const isSameDay = (a: Date | number, b: Date | number): boolean => {
63
64
  };
64
65
 
65
66
  export const formatMoment = (at: Date | number, now: Date | number = Date.now()): string => (isSameDay(at, now) ? formatClock(at) : formatDayClock(at));
67
+
68
+ const WEEK = 7 * 24 * 60 * 60 * 1000;
69
+
70
+ /*
71
+ * How old a row is, for its right edge: `3d ago` for the first week, then the day, with the year once
72
+ * it is not this one. An `at` ahead of `now`, from a clock that runs ahead, reads as now.
73
+ */
74
+ export const formatAge = (at: Date | number, now: Date | number = Date.now()): string => {
75
+ const date = toDate(at);
76
+ const today = toDate(now);
77
+ const past = today.getTime() - date.getTime();
78
+ if (past < WEEK) {
79
+ return formatAgo(past);
80
+ }
81
+ return date.getFullYear() === today.getFullYear() ? formatDay(date) : formatDayWithYear(date);
82
+ };
@@ -1,6 +1,7 @@
1
1
  /* Every number, date, time and duration a person reads, written the way the language and the region they set ask for. */
2
2
 
3
3
  export {
4
+ formatAge,
4
5
  formatClock,
5
6
  formatDateTime,
6
7
  formatDay,
package/src/index.ts CHANGED
@@ -25,7 +25,7 @@ export type {
25
25
  } from './menu/parts.tsx';
26
26
  export type { KeyValueListItemProps, KeyValueListNameProps, KeyValueListRootProps, KeyValueListValueProps } from './key-value-list/parts.tsx';
27
27
  export type { PopoverPopupProps, PopupVariant, PreviewCardPopupProps } from './popover/parts.tsx';
28
- export type { TabsCountProps, TabsListProps, TabsRootProps, TabsTabProps, TabsValue } from './tabs/parts.tsx';
28
+ export type { TabsCountProps, TabsListProps, TabsRootProps, TabsTabProps, TabsValue, TabsVariant } from './tabs/parts.tsx';
29
29
 
30
30
  export { AccentSwatches, type AccentSwatchesProps } from './AccentSwatches.tsx';
31
31
  export { Banner, type BannerProps, type BannerTone } from './Banner.tsx';
@@ -49,11 +49,12 @@ export { Input, TextArea, type InputProps, type TextAreaProps } from './Input.ts
49
49
  export { Kbd, KeyCap, Keys, type KbdProps, type KeyCapProps, type KeysProps } from './Kbd.tsx';
50
50
  export { ListRow, type ListRowProps } from './ListRow.tsx';
51
51
  export { Meter, type MeterProps } from './Meter.tsx';
52
+ export { MiddleTruncate, type MiddleTruncateProps } from './MiddleTruncate.tsx';
52
53
  export { PanelEmpty, type PanelEmptyProps } from './PanelEmpty.tsx';
53
54
  export { PanelHeader, type PanelHeaderProps } from './PanelHeader.tsx';
54
55
  export { Pill, type PillProps } from './Pill.tsx';
55
56
  export { PromptDialog, type PromptDialogProps } from './PromptDialog.tsx';
56
- export { ProjectSwitcher, type ProjectSwitcherItem, type ProjectSwitcherProps } from './ProjectSwitcher.tsx';
57
+ export { ProjectSwitcher, ProjectSwitcherGroups, type ProjectSwitcherItem, type ProjectSwitcherProps } from './ProjectSwitcher.tsx';
57
58
  export { SectionLabel, type SectionLabelProps } from './SectionLabel.tsx';
58
59
  export { SegmentBar, type SegmentBarPart, type SegmentBarProps } from './SegmentBar.tsx';
59
60
  export { Segmented, type SegmentedOption, type SegmentedProps } from './Segmented.tsx';
@@ -128,6 +129,8 @@ export { Workspace, type WorkspaceProps } from './workspace/Workspace.tsx';
128
129
  export type { WorkspaceLayout } from './workspace/context.ts';
129
130
  export { DocumentTab, TabStrip, type DocumentTabProps, type TabStripProps, type TabStripItem } from './workspace/TabStrip.tsx';
130
131
  export { SplitView, type SplitViewProps, type SplitViewInfo, type SplitViewBounds, type SplitDropTarget, type SplitDrag } from './workspace/SplitView.tsx';
132
+ export { DropPreview, type DropPreviewProps } from './workspace/DropPreview.tsx';
133
+ export { dropPreviewPath, rectDropPreview, sameDropPreview, tabDropPreview, type DropPreviewShape, type TabDrop } from './workspace/preview.ts';
131
134
  export {
132
135
  createSplitLayout,
133
136
  normalizeSplitLayout,
@@ -141,4 +144,4 @@ export {
141
144
  type SplitLayout,
142
145
  type SplitCommand
143
146
  } from './workspace/model.ts';
144
- export type { SplitRect, SplitEdges, SplitMinimum } from './workspace/geometry.ts';
147
+ export { tabGapAt, type SplitRect, type SplitEdges, type SplitMinimum } from './workspace/geometry.ts';
@@ -1,5 +1,5 @@
1
1
  import clsx from 'clsx';
2
- import { useRender } from '@base-ui-components/react/use-render';
2
+ import { useRender } from '@base-ui/react/use-render';
3
3
 
4
4
  export type KeyValueListRootProps = useRender.ComponentProps<'dl'> & {
5
5
  /* A hairline between rows, for a long list such as the headers of a message. Without it the rows sit close, for a few facts in a card. */
@@ -0,0 +1,82 @@
1
+ export default {
2
+ action: {
3
+ cancel: 'Cancel',
4
+ copy: 'Copy',
5
+ copyError: 'Copy error',
6
+ dismiss: 'Dismiss',
7
+ reload: 'Reload',
8
+ retry: 'Try again',
9
+ select: 'Select',
10
+ selectAll: 'Select all'
11
+ },
12
+ accent: {
13
+ more: 'More colors'
14
+ },
15
+ settings: {
16
+ title: 'Settings',
17
+ section: 'Settings section',
18
+ sections: 'Settings sections',
19
+ close: 'Close settings',
20
+ failed: 'This settings page failed to render',
21
+ search: {
22
+ placeholder: 'Search settings',
23
+ clear: 'Clear search',
24
+ none: 'Nothing matches "{{query}}".',
25
+ results: 'Search results',
26
+ pane: 'Section'
27
+ }
28
+ },
29
+ controls: {
30
+ stepper: {
31
+ smaller: 'Smaller',
32
+ larger: 'Larger',
33
+ smallerFor: '{{label}}: smaller',
34
+ largerFor: '{{label}}: larger'
35
+ }
36
+ },
37
+ select: {
38
+ search: 'Search',
39
+ noMatch: 'No option matches “{{query}}”'
40
+ },
41
+ iconPicker: {
42
+ label: 'Icon',
43
+ none: 'No icon',
44
+ search: 'Find an icon',
45
+ clear: 'Clear search',
46
+ noMatch: 'No icon matches “{{query}}”'
47
+ },
48
+ zoom: {
49
+ out: 'Zoom out',
50
+ in: 'Zoom in',
51
+ presets: 'Zoom presets',
52
+ fit: 'Zoom to fit',
53
+ fitEverything: 'Fit everything'
54
+ },
55
+ waveform: {
56
+ position: '{{time}} of {{duration}}'
57
+ },
58
+ projectSwitcher: {
59
+ label: 'Projects',
60
+ recent: 'Recent projects',
61
+ actionsFor: 'Actions for {{name}}'
62
+ },
63
+ tabs: {
64
+ more: 'More',
65
+ close: 'Close tab'
66
+ },
67
+ tree: {
68
+ label: 'Files',
69
+ expand: 'Expand',
70
+ collapse: 'Collapse'
71
+ },
72
+ workspace: {
73
+ tabs: 'Open views',
74
+ unsaved: 'Unsaved changes',
75
+ closeTab: 'Close {{name}}',
76
+ resize: 'Resize panels',
77
+ pane: 'Panel',
78
+ movePane: 'Move panel',
79
+ maximize: 'Maximize panel',
80
+ restore: 'Restore panels'
81
+ }
82
+ };
@@ -0,0 +1,82 @@
1
+ export default {
2
+ action: {
3
+ cancel: 'Annuleren',
4
+ copy: 'Kopiëren',
5
+ copyError: 'Fout kopiëren',
6
+ dismiss: 'Sluiten',
7
+ reload: 'Herladen',
8
+ retry: 'Opnieuw proberen',
9
+ select: 'Selecteren',
10
+ selectAll: 'Alles selecteren'
11
+ },
12
+ accent: {
13
+ more: 'Meer kleuren'
14
+ },
15
+ settings: {
16
+ title: 'Instellingen',
17
+ section: 'Onderdeel van de instellingen',
18
+ sections: 'Onderdelen van de instellingen',
19
+ close: 'Instellingen sluiten',
20
+ failed: 'Deze instellingenpagina kon niet worden getekend',
21
+ search: {
22
+ placeholder: 'Instellingen zoeken',
23
+ clear: 'Zoekopdracht wissen',
24
+ none: 'Niets komt overeen met "{{query}}".',
25
+ results: 'Zoekresultaten',
26
+ pane: 'Onderdeel'
27
+ }
28
+ },
29
+ controls: {
30
+ stepper: {
31
+ smaller: 'Kleiner',
32
+ larger: 'Groter',
33
+ smallerFor: '{{label}}: kleiner',
34
+ largerFor: '{{label}}: groter'
35
+ }
36
+ },
37
+ select: {
38
+ search: 'Zoeken',
39
+ noMatch: 'Geen optie past bij “{{query}}”'
40
+ },
41
+ iconPicker: {
42
+ label: 'Icoon',
43
+ none: 'Geen icoon',
44
+ search: 'Zoek een icoon',
45
+ clear: 'Zoekopdracht wissen',
46
+ noMatch: 'Geen icoon past bij “{{query}}”'
47
+ },
48
+ zoom: {
49
+ out: 'Uitzoomen',
50
+ in: 'Inzoomen',
51
+ presets: 'Zoomstanden',
52
+ fit: 'Zoomen tot alles past',
53
+ fitEverything: 'Alles laten passen'
54
+ },
55
+ waveform: {
56
+ position: '{{time}} van {{duration}}'
57
+ },
58
+ projectSwitcher: {
59
+ label: 'Projecten',
60
+ recent: 'Recente projecten',
61
+ actionsFor: 'Acties voor {{name}}'
62
+ },
63
+ tabs: {
64
+ more: 'Meer',
65
+ close: 'Tabblad sluiten'
66
+ },
67
+ tree: {
68
+ label: 'Bestanden',
69
+ expand: 'Uitvouwen',
70
+ collapse: 'Invouwen'
71
+ },
72
+ workspace: {
73
+ tabs: 'Open views',
74
+ unsaved: 'Niet-opgeslagen wijzigingen',
75
+ closeTab: 'Sluit {{name}}',
76
+ resize: 'Panelen vergroten of verkleinen',
77
+ pane: 'Paneel',
78
+ movePane: 'Paneel verplaatsen',
79
+ maximize: 'Paneel maximaliseren',
80
+ restore: 'Panelen herstellen'
81
+ }
82
+ };
package/src/locales.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { i18n as I18n } from 'i18next';
2
- import en from './locales/en.json' with { type: 'json' };
3
- import nl from './locales/nl.json' with { type: 'json' };
2
+ /* The words are modules, not JSON: Node needs `with { type: 'json' }` on a JSON import, and a browser refuses it on the JavaScript Vite serves. */
3
+ import en from './locales/en.ts';
4
+ import nl from './locales/nl.ts';
4
5
 
5
6
  /* The words of this library live in a namespace of their own, which the app adds to its i18next beside its own namespaces. */
6
7
  export const UI_NAMESPACE = 'ui';
@@ -1,10 +1,10 @@
1
1
  import { createContext, useContext, type ComponentProps, type ReactNode, type Ref } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { Check, ChevronRight, type LucideIcon } from 'lucide-react';
4
- import { ContextMenu as BaseContextMenu } from '@base-ui-components/react/context-menu';
5
- import { Menu as BaseMenu } from '@base-ui-components/react/menu';
6
- import { Separator as BaseSeparator } from '@base-ui-components/react/separator';
7
- import { useRender } from '@base-ui-components/react/use-render';
4
+ import { ContextMenu as BaseContextMenu } from '@base-ui/react/context-menu';
5
+ import { Menu as BaseMenu } from '@base-ui/react/menu';
6
+ import { Separator as BaseSeparator } from '@base-ui/react/separator';
7
+ import { useRender } from '@base-ui/react/use-render';
8
8
  import { withClass } from '../class-name.ts';
9
9
  import { Icon } from '../Icon.tsx';
10
10
  import { Tooltip } from '../Tooltip.tsx';
@@ -0,0 +1,41 @@
1
+ export interface MiddleCut {
2
+ head: string;
3
+ /* What the ellipsis stands for; still in the text, so a copy or a find reads it. */
4
+ middle: string;
5
+ tail: string;
6
+ }
7
+
8
+ export const MIDDLE_ELLIPSIS = '…';
9
+
10
+ /*
11
+ * The head and tail of `text` that keep the most of it around an ellipsis within `width`, or null
12
+ * when the whole text fits. The tail gets the odd character, since the end of a path or a branch is
13
+ * what tells two of them apart. Code points, never UTF-16 units, so a cut never halves a character.
14
+ */
15
+ export function fitMiddle(text: string, width: number, measure: (text: string) => number): MiddleCut | null {
16
+ if (measure(text) <= width) {
17
+ return null;
18
+ }
19
+ const characters = Array.from(text);
20
+ const cutAt = (kept: number): MiddleCut => {
21
+ const headLength = Math.floor(kept / 2);
22
+ const tailStart = characters.length - (kept - headLength);
23
+ return {
24
+ head: characters.slice(0, headLength).join(''),
25
+ middle: characters.slice(headLength, tailStart).join(''),
26
+ tail: characters.slice(tailStart).join('')
27
+ };
28
+ };
29
+ const fits = (cut: MiddleCut): boolean => measure(cut.head + MIDDLE_ELLIPSIS + cut.tail) <= width;
30
+ let low = 0;
31
+ let high = characters.length - 1;
32
+ while (low < high) {
33
+ const kept = Math.ceil((low + high) / 2);
34
+ if (fits(cutAt(kept))) {
35
+ low = kept;
36
+ } else {
37
+ high = kept - 1;
38
+ }
39
+ }
40
+ return cutAt(low);
41
+ }
@@ -1,6 +1,6 @@
1
1
  import type { ComponentProps } from 'react';
2
- import { Popover as BasePopover } from '@base-ui-components/react/popover';
3
- import { PreviewCard as BasePreviewCard } from '@base-ui-components/react/preview-card';
2
+ import { Popover as BasePopover } from '@base-ui/react/popover';
3
+ import { PreviewCard as BasePreviewCard } from '@base-ui/react/preview-card';
4
4
  import { withClass } from '../class-name.ts';
5
5
  import { splitPlacement, type PlacementKey } from '../placement.ts';
6
6
  import { renderPositioner } from '../popup-layer.ts';
package/src/prefetch.ts CHANGED
@@ -16,7 +16,7 @@ export class Prefetcher {
16
16
  private running: Promise<void> | null = null;
17
17
  private loading = false;
18
18
  private readonly whenIdle: WhenIdle;
19
- /* Asked on every start, since a person can turn data saving on while the page runs. */
19
+ /* Asked on every ask and again before every load, since a person can turn data saving on while the page runs. */
20
20
  private readonly allowed: () => boolean;
21
21
 
22
22
  constructor(whenIdle: WhenIdle, allowed: () => boolean = () => true) {
@@ -78,6 +78,11 @@ export class Prefetcher {
78
78
  try {
79
79
  for (let load = this.queue.shift(); load; load = this.queue.shift()) {
80
80
  await new Promise<void>((resolve) => this.whenIdle(resolve));
81
+ if (!this.allowed()) {
82
+ // Data saving came on while it waited; a later ask may queue it again.
83
+ this.queued.delete(load);
84
+ continue;
85
+ }
81
86
  this.loading = true;
82
87
  try {
83
88
  await load();
@@ -1,3 +1,4 @@
1
+ import type { Ref } from 'react';
1
2
  import { PromptDialog } from '../PromptDialog.tsx';
2
3
 
3
4
  export interface ConfirmDialogProps {
@@ -8,12 +9,17 @@ export interface ConfirmDialogProps {
8
9
  confirmLabel: string;
9
10
  /* Closes the dialog once it resolves; a rejection stays on screen as the reason. */
10
11
  onConfirm(): Promise<void>;
12
+ /* Extra on the popup. */
13
+ className?: string;
14
+ ref?: Ref<HTMLDivElement>;
11
15
  }
12
16
 
13
17
  /* One destructive step, asked before it happens, over whichever dialog it was opened from. */
14
- export function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel, onConfirm }: ConfirmDialogProps) {
18
+ export function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel, onConfirm, className, ref }: ConfirmDialogProps) {
15
19
  return (
16
20
  <PromptDialog
21
+ ref={ref}
22
+ className={className}
17
23
  open={open}
18
24
  nested
19
25
  danger
@@ -1,7 +1,7 @@
1
1
  import { Suspense, useEffect, useMemo, useRef, useState, type ComponentType, type ReactNode, type Ref } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { useTranslation } from 'react-i18next';
4
- import { Tabs } from '@base-ui-components/react/tabs';
4
+ import { Tabs } from '@base-ui/react/tabs';
5
5
  import { Search, X, type LucideIcon } from 'lucide-react';
6
6
  import { CloseButton } from '../CloseButton.tsx';
7
7
  import { DialogDescription, DialogPopup, DialogRoot, DialogTitle } from '../dialog/parts.tsx';