@adecore/ui 0.23.1 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) 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 +27 -10
  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 +43 -8
  75. package/dist/workspace/Workspace.d.ts +2 -2
  76. package/dist/workspace/Workspace.js +4 -2
  77. package/dist/workspace/geometry.d.ts +11 -0
  78. package/dist/workspace/geometry.js +19 -0
  79. package/dist/workspace/preview.d.ts +4 -8
  80. package/dist/workspace/preview.js +17 -2
  81. package/package.json +2 -2
  82. package/src/ButtonGroup.tsx +1 -1
  83. package/src/Checkbox.tsx +1 -1
  84. package/src/ChoiceCards.tsx +2 -2
  85. package/src/CloseButton.tsx +1 -1
  86. package/src/ColorSwatch.tsx +1 -1
  87. package/src/ErrorBoundary.tsx +4 -4
  88. package/src/Field.tsx +4 -5
  89. package/src/IconButton.tsx +1 -1
  90. package/src/ListRow.tsx +1 -1
  91. package/src/MiddleTruncate.tsx +84 -0
  92. package/src/PanelHeader.tsx +1 -1
  93. package/src/SectionLabel.tsx +1 -1
  94. package/src/Select.tsx +1 -1
  95. package/src/SelectSearch.tsx +1 -1
  96. package/src/Slider.tsx +1 -1
  97. package/src/Surface.tsx +1 -1
  98. package/src/Switch.tsx +1 -1
  99. package/src/Toasts.tsx +5 -6
  100. package/src/Tooltip.tsx +5 -5
  101. package/src/clipboard.ts +11 -2
  102. package/src/dialog/parts.tsx +2 -2
  103. package/src/file-tree/helpers.ts +10 -0
  104. package/src/file-tree/parts.tsx +1 -1
  105. package/src/file-tree/use-slots.ts +68 -23
  106. package/src/format/datetime.ts +17 -0
  107. package/src/format/index.ts +1 -0
  108. package/src/index.ts +6 -3
  109. package/src/key-value-list/parts.tsx +1 -1
  110. package/src/locales/en.ts +82 -0
  111. package/src/locales/nl.ts +82 -0
  112. package/src/locales.ts +3 -2
  113. package/src/menu/parts.tsx +4 -4
  114. package/src/middle-truncate.ts +41 -0
  115. package/src/popover/parts.tsx +2 -2
  116. package/src/prefetch.ts +6 -1
  117. package/src/settings/ConfirmDialog.tsx +7 -1
  118. package/src/settings/SettingsDialog.tsx +1 -1
  119. package/src/tabs/parts.tsx +156 -54
  120. package/src/testing/dedupe.ts +87 -24
  121. package/src/theme.css +27 -10
  122. package/src/tree/parts.tsx +1 -1
  123. package/src/workspace/DropPreview.tsx +26 -0
  124. package/src/workspace/SplitView.tsx +58 -22
  125. package/src/workspace/TabStrip.tsx +44 -8
  126. package/src/workspace/Workspace.tsx +7 -3
  127. package/src/workspace/geometry.ts +26 -0
  128. package/src/workspace/preview.ts +22 -9
  129. package/dist/locales/en.json +0 -82
  130. package/dist/locales/nl.json +0 -82
  131. package/src/locales/en.json +0 -82
  132. package/src/locales/nl.json +0 -82
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import clsx from 'clsx';
3
- import { useRender } from '@base-ui-components/react/use-render';
3
+ import { useRender } from '@base-ui/react/use-render';
4
4
  import { SectionLabel } from './SectionLabel.tsx';
5
5
 
6
6
  export type PanelHeaderProps = useRender.ComponentProps<'header'> & {
@@ -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
  /* A label takes the line height of the row it sits in, which is why the size carries `/[inherit]`. */
5
5
  const SECTION_LABEL = 'text-xs/[inherit] font-medium text-text-faint';
package/src/Select.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Ref } from 'react';
2
- import { Select as BaseSelect } from '@base-ui-components/react/select';
2
+ import { Select as BaseSelect } from '@base-ui/react/select';
3
3
  import clsx from 'clsx';
4
4
  import { ChevronDown } from 'lucide-react';
5
5
  import { useTranslation } from 'react-i18next';
@@ -1,5 +1,5 @@
1
1
  import { useState } from 'react';
2
- import { Combobox } from '@base-ui-components/react/combobox';
2
+ import { Combobox } from '@base-ui/react/combobox';
3
3
  import clsx from 'clsx';
4
4
  import { ChevronDown, Search } from 'lucide-react';
5
5
  import { useTranslation } from 'react-i18next';
package/src/Slider.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import { useId, type Ref } from 'react';
2
2
  import clsx from 'clsx';
3
- import { Slider as BaseSlider } from '@base-ui-components/react/slider';
3
+ import { Slider as BaseSlider } from '@base-ui/react/slider';
4
4
  import { formatFixed } from './format/number.ts';
5
5
  import { placesOf } from './step-places.ts';
6
6
 
package/src/Surface.tsx CHANGED
@@ -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
  const FLOAT = 'border border-border bg-[color-mix(in_srgb,var(--surface-raised)_88%,transparent)] shadow-float backdrop-blur-[14px]';
5
5
 
package/src/Switch.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Ref } from 'react';
2
2
  import clsx from 'clsx';
3
- import { Switch as BaseSwitch } from '@base-ui-components/react/switch';
3
+ import { Switch as BaseSwitch } from '@base-ui/react/switch';
4
4
 
5
5
  export interface SwitchProps {
6
6
  checked: boolean;
package/src/Toasts.tsx CHANGED
@@ -2,6 +2,7 @@ import { useState, type ReactNode, type Ref } from 'react';
2
2
  import clsx from 'clsx';
3
3
  import { useTranslation } from 'react-i18next';
4
4
  import { CircleAlert, CircleCheck, Trash, X } from 'lucide-react';
5
+ import { Button } from './Button.tsx';
5
6
  import { Icon } from './Icon.tsx';
6
7
  import { IconButton } from './IconButton.tsx';
7
8
  import { Kbd } from './Kbd.tsx';
@@ -57,9 +58,9 @@ function TimerRing({ deadline, onDismiss }: { deadline: ToastDeadline; onDismiss
57
58
  function ActionButton({ action }: { action: ToastAction }) {
58
59
  return (
59
60
  <span className="flex items-center gap-2">
60
- <button className="rounded-sm text-sm font-semibold text-text hover:text-text-muted" onClick={action.run}>
61
+ <Button size="xs" onClick={action.run}>
61
62
  {action.label}
62
- </button>
63
+ </Button>
63
64
  {action.shortcut && <Kbd shortcut={action.shortcut} className="font-sans text-xs text-text-faint" />}
64
65
  </span>
65
66
  );
@@ -90,7 +91,7 @@ function ToastCard<T extends Toast>({ toast, footer, onDismiss }: { toast: T; fo
90
91
  )}
91
92
  {/* Two or more would squeeze the title beside them, so they get a line of their own. */}
92
93
  {actions.length > 1 && (
93
- <span className="mt-1 flex flex-wrap items-center gap-x-4 gap-y-1">
94
+ <span className="mt-1 -ml-2 flex flex-wrap items-center gap-x-2 gap-y-1">
94
95
  {actions.map((action) => (
95
96
  <ActionButton key={action.label} action={action} />
96
97
  ))}
@@ -133,9 +134,7 @@ export interface ToastsProps<T extends Toast> {
133
134
  */
134
135
  export function Toasts<T extends Toast>({ store, footer, className, ref }: ToastsProps<T>) {
135
136
  const toasts = store((state) => state.toasts);
136
- if (toasts.length === 0) {
137
- return null;
138
- }
137
+ // Stays mounted with nothing in it: a screen reader skips what a live region says the moment it is inserted.
139
138
  return (
140
139
  <div
141
140
  ref={ref}
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';