@adecore/ui 0.25.9 → 0.26.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.
@@ -1 +1 @@
1
- export declare const shownTabs: (widths: readonly number[], room: number, gap: number, more: number, picked: number) => number[];
1
+ export declare const shownTabs: (widths: readonly number[], room: number, gap: number, more: number, kept: readonly number[]) => number[];
@@ -1,10 +1,10 @@
1
1
  /*
2
2
  * The tabs a strip shows, by index, given the width of every tab, the room the strip has, the gap
3
3
  * between two tabs and the width of the button that opens the rest. Everything fits, or the tabs fit
4
- * from the start up to the button. The picked tab is never left to the menu: it takes the place of
5
- * the last ones that no longer fit beside it.
4
+ * from the start up to the button. The kept tabs (the picked one, the pinned ones) are never left to
5
+ * the menu: they take the place of the last ones that no longer fit beside them.
6
6
  */
7
- export const shownTabs = (widths, room, gap, more, picked) => {
7
+ export const shownTabs = (widths, room, gap, more, kept) => {
8
8
  const span = (indices) => indices.reduce((sum, index) => sum + widths[index], 0) + gap * Math.max(0, indices.length - 1);
9
9
  const all = widths.map((_, index) => index);
10
10
  // Half a pixel of slack, since a measured width is fractional and the room is not.
@@ -12,18 +12,16 @@ export const shownTabs = (widths, room, gap, more, picked) => {
12
12
  return all;
13
13
  }
14
14
  const limit = room - more - gap + 0.5;
15
+ const held = all.filter((index) => kept.includes(index));
15
16
  const shown = [];
16
17
  for (const index of all) {
17
- if (span([...shown, index]) > limit) {
18
+ if (held.includes(index)) {
19
+ continue;
20
+ }
21
+ if (span([...held, ...shown, index]) > limit) {
18
22
  break;
19
23
  }
20
24
  shown.push(index);
21
25
  }
22
- if (picked < 0 || picked >= widths.length || shown.includes(picked)) {
23
- return shown;
24
- }
25
- while (shown.length > 0 && span([...shown, picked]) > limit) {
26
- shown.pop();
27
- }
28
- return [...shown, picked];
26
+ return [...held, ...shown].sort((left, right) => left - right);
29
27
  };
@@ -19,8 +19,9 @@ export declare function TabsList({ className, children, start, end, after, varia
19
19
  type TabProps = ComponentProps<typeof BaseTabs.Tab>;
20
20
  export type TabsTabProps = TabProps & {
21
21
  onClose?(): void;
22
+ pinned?: boolean;
22
23
  };
23
- export declare function TabsTab({ className, onClose, children, ...props }: TabsTabProps): import("react").JSX.Element;
24
+ export declare function TabsTab({ className, onClose, pinned, children, ...props }: TabsTabProps): import("react").JSX.Element;
24
25
  export declare const TabsPanel: import("react").ForwardRefExoticComponent<Omit<import("@base-ui/react").TabsPanelProps, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
25
26
  export type TabsCountProps = Omit<useRender.ComponentProps<'span'>, 'children'> & {
26
27
  value: number;
@@ -70,7 +70,7 @@ export function TabsList({ className, children, start, end, after, variant = 'li
70
70
  // Null until the first measure, which runs before the first paint.
71
71
  const [shown, setShown] = useState(null);
72
72
  const tabs = Children.toArray(children).filter((child) => isValidElement(child) && child.type === TabsTab);
73
- const picked = tabs.findIndex((tab) => tab.props.value === value);
73
+ const kept = tabs.flatMap((tab, index) => (tab.props.value === value || tab.props.pinned === true ? [index] : []));
74
74
  useLayoutEffect(() => {
75
75
  const host = strip.current;
76
76
  const row = measure.current;
@@ -84,7 +84,7 @@ export function TabsList({ className, children, start, end, after, variant = 'li
84
84
  const room = host.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight) - taken;
85
85
  const cells = [...row.children].map((cell) => cell.getBoundingClientRect().width);
86
86
  const more = cells.pop() ?? 0;
87
- const next = shownTabs(cells, room, parseFloat(getComputedStyle(row).columnGap) || 0, more, picked);
87
+ const next = shownTabs(cells, room, parseFloat(getComputedStyle(row).columnGap) || 0, more, kept);
88
88
  setShown((current) => (sameIndices(current, next) ? current : next));
89
89
  };
90
90
  fit();
@@ -143,12 +143,15 @@ export function TabsList({ className, children, start, end, after, variant = 'li
143
143
  const visible = shown === null ? tabs : shown.map((index) => tabs[index]);
144
144
  const hidden = shown === null ? [] : tabs.filter((_, index) => !shown.includes(index));
145
145
  const more = (_jsxs(_Fragment, { children: [t('tabs.more'), _jsx(Icon, { icon: ChevronDown, size: 14 })] }));
146
- return (_jsx(VariantContext, { value: variant, children: _jsxs("div", { ref: strip, className: clsx('relative flex items-center', style.strip, className), children: [start !== undefined && start !== null && (_jsx("div", { ref: head, className: "flex shrink-0 items-center gap-1", children: start })), _jsx(BaseTabs.List, { ref: list, className: clsx('flex min-w-0', style.list, scrolls && 'tabs-scroll scroll-fade-x'), "data-fade-start": scrolls && edges.start ? '' : undefined, "data-fade-end": scrolls && edges.end ? '' : undefined, ...props, children: visible }), hidden.length > 0 && (_jsxs(MenuRoot, { children: [_jsx(MenuTrigger, { className: style.more, children: more }), _jsx(MenuPopup, { align: "end", className: "min-w-48", children: _jsx(InMenuContext, { value: true, children: hidden.map((tab) => (_jsx(MenuItem, { disabled: tab.props.disabled, onClick: () => select(tab.props.value), children: tab.props.children }, tab.key))) }) })] })), after !== undefined && after !== null && (_jsx("div", { ref: beside, className: "flex shrink-0 items-center gap-1", children: after })), end !== undefined && end !== null && (_jsx("div", { ref: tail, className: clsx('flex shrink-0 items-center gap-1', hidden.length === 0 && 'ml-auto'), children: end })), !scrolls && (_jsxs("div", { ref: measure, "aria-hidden": true, inert: true, className: clsx('pointer-events-none invisible absolute top-0 left-0 flex', style.row), children: [tabs.map((tab) => (_jsxs("span", { className: style.tab, children: [tab.props.children, tab.props.onClose !== undefined && _jsx("span", { className: "size-5 shrink-0" })] }, tab.key))), _jsx("span", { className: style.more, children: more })] }))] }) }));
146
+ return (_jsx(VariantContext, { value: variant, children: _jsxs("div", { ref: strip, className: clsx('relative flex items-center', style.strip, className), children: [start !== undefined && start !== null && (_jsx("div", { ref: head, className: "flex shrink-0 items-center gap-1", children: start })), _jsx(BaseTabs.List, { ref: list, className: clsx('flex min-w-0', style.list, scrolls && 'tabs-scroll scroll-fade-x'), "data-fade-start": scrolls && edges.start ? '' : undefined, "data-fade-end": scrolls && edges.end ? '' : undefined, ...props, children: visible }), hidden.length > 0 && (_jsxs(MenuRoot, { children: [_jsx(MenuTrigger, { className: style.more, children: more }), _jsx(MenuPopup, { align: "end", className: "min-w-48", children: _jsx(InMenuContext, { value: true, children: hidden.map((tab) => (_jsx(MenuItem, { disabled: tab.props.disabled, onClick: () => select(tab.props.value), children: tab.props.children }, tab.key))) }) })] })), after !== undefined && after !== null && (_jsx("div", { ref: beside, className: "flex shrink-0 items-center gap-1", children: after })), end !== undefined && end !== null && (_jsx("div", { ref: tail, className: clsx('flex shrink-0 items-center gap-1', hidden.length === 0 && 'ml-auto'), children: end })), !scrolls && (_jsxs("div", { ref: measure, "aria-hidden": true, inert: true, className: clsx('pointer-events-none invisible absolute top-0 left-0 flex', style.row), children: [tabs.map((tab) => (_jsxs("span", { className: style.tab, children: [tab.props.children, tab.props.onClose !== undefined && tab.props.pinned !== true && _jsx("span", { className: "size-5 shrink-0" })] }, tab.key))), _jsx("span", { className: style.more, children: more })] }))] }) }));
147
147
  }
148
148
  /* The close button stays out of the tab order, so a closable tab is still one stop; the keys close it from the tab itself. */
149
- export function TabsTab({ className, onClose, children, ...props }) {
149
+ export function TabsTab({ className, onClose, pinned = false, children, ...props }) {
150
150
  const { t } = useTranslation('ui');
151
151
  const tab = STYLES[useContext(VariantContext)].tab;
152
+ if (pinned) {
153
+ return (_jsx(BaseTabs.Tab, { className: withClass(tab, className), "data-pinned": "", ...props, children: children }));
154
+ }
152
155
  if (onClose === undefined) {
153
156
  return (_jsx(BaseTabs.Tab, { className: withClass(tab, className), ...props, children: children }));
154
157
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/ui",
3
- "version": "0.25.9",
3
+ "version": "0.26.1",
4
4
  "description": "React components, a theme, formatters and a settings dialog for desktop-grade apps on React 19, Base UI and Tailwind 4.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "author": "Bas Milius",
@@ -1,10 +1,10 @@
1
1
  /*
2
2
  * The tabs a strip shows, by index, given the width of every tab, the room the strip has, the gap
3
3
  * between two tabs and the width of the button that opens the rest. Everything fits, or the tabs fit
4
- * from the start up to the button. The picked tab is never left to the menu: it takes the place of
5
- * the last ones that no longer fit beside it.
4
+ * from the start up to the button. The kept tabs (the picked one, the pinned ones) are never left to
5
+ * the menu: they take the place of the last ones that no longer fit beside them.
6
6
  */
7
- export const shownTabs = (widths: readonly number[], room: number, gap: number, more: number, picked: number): number[] => {
7
+ export const shownTabs = (widths: readonly number[], room: number, gap: number, more: number, kept: readonly number[]): number[] => {
8
8
  const span = (indices: readonly number[]): number => indices.reduce((sum, index) => sum + widths[index]!, 0) + gap * Math.max(0, indices.length - 1);
9
9
  const all = widths.map((_, index) => index);
10
10
  // Half a pixel of slack, since a measured width is fractional and the room is not.
@@ -12,18 +12,16 @@ export const shownTabs = (widths: readonly number[], room: number, gap: number,
12
12
  return all;
13
13
  }
14
14
  const limit = room - more - gap + 0.5;
15
+ const held = all.filter((index) => kept.includes(index));
15
16
  const shown: number[] = [];
16
17
  for (const index of all) {
17
- if (span([...shown, index]) > limit) {
18
+ if (held.includes(index)) {
19
+ continue;
20
+ }
21
+ if (span([...held, ...shown, index]) > limit) {
18
22
  break;
19
23
  }
20
24
  shown.push(index);
21
25
  }
22
- if (picked < 0 || picked >= widths.length || shown.includes(picked)) {
23
- return shown;
24
- }
25
- while (shown.length > 0 && span([...shown, picked]) > limit) {
26
- shown.pop();
27
- }
28
- return [...shown, picked];
26
+ return [...held, ...shown].sort((left, right) => left - right);
29
27
  };
@@ -130,7 +130,7 @@ export function TabsList({ className, children, start, end, after, variant = 'li
130
130
  const tabs = Children.toArray(children).filter(
131
131
  (child): child is ReactElement<ComponentProps<typeof TabsTab>> => isValidElement(child) && child.type === TabsTab
132
132
  );
133
- const picked = tabs.findIndex((tab) => tab.props.value === value);
133
+ const kept = tabs.flatMap((tab, index) => (tab.props.value === value || tab.props.pinned === true ? [index] : []));
134
134
 
135
135
  useLayoutEffect(() => {
136
136
  const host = strip.current;
@@ -148,7 +148,7 @@ export function TabsList({ className, children, start, end, after, variant = 'li
148
148
  const room = host.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight) - taken;
149
149
  const cells = [...row.children].map((cell) => cell.getBoundingClientRect().width);
150
150
  const more = cells.pop() ?? 0;
151
- const next = shownTabs(cells, room, parseFloat(getComputedStyle(row).columnGap) || 0, more, picked);
151
+ const next = shownTabs(cells, room, parseFloat(getComputedStyle(row).columnGap) || 0, more, kept);
152
152
  setShown((current) => (sameIndices(current, next) ? current : next));
153
153
  };
154
154
  fit();
@@ -263,7 +263,7 @@ export function TabsList({ className, children, start, end, after, variant = 'li
263
263
  {tabs.map((tab) => (
264
264
  <span key={tab.key} className={style.tab}>
265
265
  {tab.props.children}
266
- {tab.props.onClose !== undefined && <span className="size-5 shrink-0" />}
266
+ {tab.props.onClose !== undefined && tab.props.pinned !== true && <span className="size-5 shrink-0" />}
267
267
  </span>
268
268
  ))}
269
269
  <span className={style.more}>{more}</span>
@@ -279,13 +279,23 @@ type TabProps = ComponentProps<typeof BaseTabs.Tab>;
279
279
  export type TabsTabProps = TabProps & {
280
280
  /* Makes the tab closable: a close button in it, a middle click, and Delete or Backspace while the tab has focus. */
281
281
  onClose?(): void;
282
+ /* Keeps the tab in the strip, never in the menu, and leaves it unclosable even with `onClose`. */
283
+ pinned?: boolean;
282
284
  };
283
285
 
284
286
  /* The close button stays out of the tab order, so a closable tab is still one stop; the keys close it from the tab itself. */
285
- export function TabsTab({ className, onClose, children, ...props }: TabsTabProps) {
287
+ export function TabsTab({ className, onClose, pinned = false, children, ...props }: TabsTabProps) {
286
288
  const { t } = useTranslation('ui');
287
289
  const tab = STYLES[useContext(VariantContext)].tab;
288
290
 
291
+ if (pinned) {
292
+ return (
293
+ <BaseTabs.Tab className={withClass(tab, className)} data-pinned="" {...props}>
294
+ {children}
295
+ </BaseTabs.Tab>
296
+ );
297
+ }
298
+
289
299
  if (onClose === undefined) {
290
300
  return (
291
301
  <BaseTabs.Tab className={withClass(tab, className)} {...props}>