@tristan2828/ui-foundation 3.20.1 → 3.22.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 (41) hide show
  1. package/conventions/docs/add-an-entity.md +9 -4
  2. package/conventions/docs/cell-patterns.md +13 -4
  3. package/conventions/docs/design-language.md +22 -0
  4. package/dist/components/app/data-table.js +21 -13
  5. package/dist/components/app/markdown.js +38 -20
  6. package/dist/components/app/milkdown-editor.js +189 -31
  7. package/dist/components/app/prose-look.d.ts +32 -0
  8. package/dist/components/app/prose-look.js +48 -0
  9. package/dist/components/app/rich-text-format-items.d.ts +13 -1
  10. package/dist/components/app/rich-text-format-items.js +43 -10
  11. package/dist/components/app/rich-text-formats.d.ts +15 -2
  12. package/dist/components/app/rich-text-formats.js +99 -10
  13. package/dist/components/app/rich-text-node-views.d.ts +31 -0
  14. package/dist/components/app/rich-text-node-views.js +79 -0
  15. package/dist/components/app/rich-text-shortcuts.d.ts +12 -0
  16. package/dist/components/app/rich-text-shortcuts.js +52 -0
  17. package/dist/components/app/rich-text-slash-menu.js +4 -1
  18. package/dist/components/app/rich-text-slash.js +26 -10
  19. package/dist/components/app/rich-text-table-menu.d.ts +20 -0
  20. package/dist/components/app/rich-text-table-menu.js +53 -0
  21. package/dist/components/app/rich-text-table.d.ts +19 -0
  22. package/dist/components/app/rich-text-table.js +162 -0
  23. package/dist/components/app/rich-text-toolbar.d.ts +11 -2
  24. package/dist/components/app/rich-text-toolbar.js +14 -23
  25. package/dist/components/app/rich-text-widgets.d.ts +19 -0
  26. package/dist/components/app/rich-text-widgets.js +34 -0
  27. package/dist/components/app/secondary-filters.d.ts +18 -0
  28. package/dist/components/app/secondary-filters.js +32 -0
  29. package/dist/components/ui/badge.d.ts +1 -1
  30. package/dist/components/ui/combobox.js +7 -1
  31. package/dist/components/ui/dropdown-menu.d.ts +29 -0
  32. package/dist/components/ui/dropdown-menu.js +51 -0
  33. package/dist/hooks/use-table-url-state.d.ts +1 -1
  34. package/dist/hooks/use-table-url-state.js +6 -5
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +1 -0
  37. package/dist/lib/markdown-callout.d.ts +38 -0
  38. package/dist/lib/markdown-callout.js +103 -0
  39. package/package.json +1 -1
  40. package/styles/index.css +17 -0
  41. package/styles/theme.css +30 -0
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // The fixed toolbar's Table menu: a table inserted where the caret is, or,
3
+ // in a table, its rows, columns and alignment (rich-text-table.ts). A menu
4
+ // button in the toolbar's row (one of its tab stops' places, moved to with
5
+ // the arrow keys like the rest); the menu itself is Base UI's (ARIA's menu
6
+ // pattern). Picking an item gives focus back to the text, the caret where
7
+ // it was (the editor keeps its selection while the menu has focus); Esc
8
+ // gives it back to the button, as a menu button's does.
9
+ import { useRef } from 'react';
10
+ import { AlignCenterIcon, AlignLeftIcon, AlignRightIcon, BetweenHorizontalEndIcon, BetweenHorizontalStartIcon, BetweenVerticalEndIcon, BetweenVerticalStartIcon, ChevronDownIcon, TableIcon, Trash2Icon, } from 'lucide-react';
11
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '../ui/dropdown-menu.js';
12
+ import { toggleVariants } from '../ui/toggle.js';
13
+ import { tableAt } from './rich-text-formats.js';
14
+ import { columnAlignment, tableActionEnabled } from './rich-text-table.js';
15
+ const ROWS = [
16
+ { action: 'rowAbove', label: 'Insert row above', icon: BetweenHorizontalStartIcon },
17
+ { action: 'rowBelow', label: 'Insert row below', icon: BetweenHorizontalEndIcon },
18
+ { action: 'columnLeft', label: 'Insert column left', icon: BetweenVerticalStartIcon },
19
+ { action: 'columnRight', label: 'Insert column right', icon: BetweenVerticalEndIcon },
20
+ ];
21
+ const ALIGNMENTS = [
22
+ { value: 'left', action: 'alignLeft', label: 'Left', icon: AlignLeftIcon },
23
+ { value: 'center', action: 'alignCenter', label: 'Center', icon: AlignCenterIcon },
24
+ { value: 'right', action: 'alignRight', label: 'Right', icon: AlignRightIcon },
25
+ ];
26
+ const DELETES = [
27
+ { action: 'deleteRow', label: 'Delete row', icon: Trash2Icon },
28
+ { action: 'deleteColumn', label: 'Delete column', icon: Trash2Icon },
29
+ { action: 'deleteTable', label: 'Delete table', icon: Trash2Icon },
30
+ ];
31
+ export function RichTextTableMenu({ state, readOnly, tabIndex, onFocus, onKeyDown, onInsert, onAction, text, ref, }) {
32
+ const inTable = state ? tableAt(state) !== null : false;
33
+ const enabled = (action) => (state ? tableActionEnabled(state, action) : false);
34
+ // Whether the menu is closing because an item was picked.
35
+ const picked = useRef(false);
36
+ const run = (action) => {
37
+ picked.current = true;
38
+ action();
39
+ };
40
+ const finalFocus = () => {
41
+ const toText = picked.current;
42
+ picked.current = false;
43
+ return toText ? (text() ?? true) : true;
44
+ };
45
+ const item = ({ action, label, icon: Icon }) => (_jsxs(DropdownMenuItem, { disabled: !enabled(action), onClick: () => run(() => onAction(action)), children: [_jsx(Icon, { "aria-hidden": "true" }), label] }, action));
46
+ return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { ref: ref, "aria-label": "Table", disabled: readOnly, tabIndex: tabIndex, "data-active": inTable || undefined,
47
+ // Shaped like the toolbar's toggles: on while the caret is in a table.
48
+ className: toggleVariants({ size: 'sm', className: 'gap-0.5 px-1.5 data-active:bg-muted data-active:text-foreground' }), onFocus: onFocus, onKeyDown: onKeyDown, children: [_jsx(TableIcon, { "aria-hidden": "true" }), _jsx(ChevronDownIcon, { "aria-hidden": "true", className: "size-3" })] }), _jsxs(DropdownMenuContent, { className: "w-56", finalFocus: finalFocus, children: [_jsxs(DropdownMenuItem, { disabled: inTable, onClick: () => run(onInsert), children: [_jsx(TableIcon, { "aria-hidden": "true" }), "Insert table"] }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuGroup, { children: ROWS.map(item) }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuGroup, { children: [_jsx(DropdownMenuLabel, { children: "Align column" }), _jsx(DropdownMenuRadioGroup, { value: state && inTable ? columnAlignment(state) : null, onValueChange: (value) => {
49
+ const alignment = ALIGNMENTS.find((one) => one.value === value);
50
+ if (alignment)
51
+ run(() => onAction(alignment.action));
52
+ }, children: ALIGNMENTS.map(({ value, label, icon: Icon }) => (_jsxs(DropdownMenuRadioItem, { value: value, disabled: !inTable, closeOnClick: true, children: [_jsx(Icon, { "aria-hidden": "true" }), label] }, value))) })] }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuGroup, { children: DELETES.map(item) })] })] }));
53
+ }
@@ -0,0 +1,19 @@
1
+ import { type EditorState, type Transaction } from '@milkdown/kit/prose/state';
2
+ type Dispatch = (tr: Transaction) => void;
3
+ export type TableAction = 'rowAbove' | 'rowBelow' | 'columnLeft' | 'columnRight' | 'alignLeft' | 'alignCenter' | 'alignRight' | 'deleteRow' | 'deleteColumn' | 'deleteTable';
4
+ export type ColumnAlignment = 'left' | 'center' | 'right';
5
+ /** The caret's column's alignment, as the header cell has it (GFM's default is left). */
6
+ export declare function columnAlignment(state: EditorState): ColumnAlignment;
7
+ /** Whether an action can apply where the caret is. */
8
+ export declare function tableActionEnabled(state: EditorState, action: TableAction): boolean;
9
+ /** Runs an action where the caret is. False when it can't apply there. */
10
+ export declare function runTableAction(state: EditorState, dispatch: Dispatch, action: TableAction): boolean;
11
+ /**
12
+ * The editor's table keys, before Milkdown's own (which go to the next
13
+ * cell on Tab, and out of the table on any Enter). True when it handled
14
+ * the key.
15
+ */
16
+ export declare function handleTableKey(state: EditorState, dispatch: Dispatch, event: KeyboardEvent): boolean;
17
+ /** A row added at the table's end, the caret into its first cell (the "Add a row" bar). */
18
+ export declare function addRowAtEnd(state: EditorState, dispatch: Dispatch): boolean;
19
+ export {};
@@ -0,0 +1,162 @@
1
+ import { TextSelection } from '@milkdown/kit/prose/state';
2
+ import { addColumnAfter, addColumnBefore, deleteColumn, deleteRow, deleteTable, isInTable, selectedRect, TableMap, } from '@milkdown/kit/prose/tables';
3
+ const ALIGN = {
4
+ alignLeft: 'left',
5
+ alignCenter: 'center',
6
+ alignRight: 'right',
7
+ };
8
+ /** The caret's column's alignment, as the header cell has it (GFM's default is left). */
9
+ export function columnAlignment(state) {
10
+ if (!isInTable(state))
11
+ return 'left';
12
+ const { map, table, left } = selectedRect(state);
13
+ const value = table.nodeAt(map.map[left])?.attrs.alignment;
14
+ return value ?? 'left';
15
+ }
16
+ /** Whether an action can apply where the caret is. */
17
+ export function tableActionEnabled(state, action) {
18
+ if (!isInTable(state))
19
+ return false;
20
+ const { map, top, bottom, left, right } = selectedRect(state);
21
+ switch (action) {
22
+ case 'rowAbove':
23
+ return top > 0;
24
+ case 'deleteRow':
25
+ // Not the header row, and never every row under it.
26
+ return top > 0 && bottom - top < map.height - 1;
27
+ case 'deleteColumn':
28
+ return right - left < map.width;
29
+ default:
30
+ return true;
31
+ }
32
+ }
33
+ /** Runs an action where the caret is. False when it can't apply there. */
34
+ export function runTableAction(state, dispatch, action) {
35
+ if (!tableActionEnabled(state, action))
36
+ return false;
37
+ const alignment = ALIGN[action];
38
+ if (alignment)
39
+ return alignColumn(state, dispatch, alignment);
40
+ switch (action) {
41
+ case 'rowAbove':
42
+ return addRow(state, dispatch, 'above');
43
+ case 'rowBelow':
44
+ return addRow(state, dispatch, 'below');
45
+ case 'columnLeft':
46
+ return addColumn(state, dispatch, 'left');
47
+ case 'columnRight':
48
+ return addColumn(state, dispatch, 'right');
49
+ case 'deleteRow':
50
+ return deleteRow(state, dispatch);
51
+ case 'deleteColumn':
52
+ return deleteColumn(state, dispatch);
53
+ case 'deleteTable':
54
+ return deleteTable(state, dispatch);
55
+ default:
56
+ return false;
57
+ }
58
+ }
59
+ // A row of empty cells, each aligned as its column is, above or below the
60
+ // caret's rows (or at the end: `'end'`), and the caret into it in the
61
+ // same column (the first, at the end).
62
+ function addRow(state, dispatch, where) {
63
+ const { map, table, tableStart, top, bottom, left } = selectedRect(state);
64
+ const index = where === 'above' ? top : where === 'below' ? bottom : map.height;
65
+ if (index === 0)
66
+ return false;
67
+ const { schema } = state;
68
+ const cells = Array.from({ length: map.width }, (_value, column) => schema.nodes.table_cell.createAndFill({ alignment: table.nodeAt(map.map[column])?.attrs.alignment ?? null }));
69
+ let pos = tableStart;
70
+ for (let row = 0; row < index; row++)
71
+ pos += table.child(row).nodeSize;
72
+ const tr = state.tr.insert(pos, schema.nodes.table_row.create(null, cells));
73
+ const updated = tr.doc.nodeAt(tableStart - 1);
74
+ const column = where === 'end' ? 0 : left;
75
+ tr.setSelection(TextSelection.near(tr.doc.resolve(tableStart + TableMap.get(updated).map[index * map.width + column] + 1)));
76
+ dispatch(tr.scrollIntoView());
77
+ return true;
78
+ }
79
+ // A column of empty cells left or right of the caret's columns, unaligned
80
+ // (prosemirror-tables makes its cells with the schema's default, left,
81
+ // which would write `:---` for a column nobody aligned). The caret stays.
82
+ function addColumn(state, dispatch, side) {
83
+ const { tableStart, left, right } = selectedRect(state);
84
+ let added = null;
85
+ const add = side === 'left' ? addColumnBefore : addColumnAfter;
86
+ if (!add(state, (tr) => (added = tr)))
87
+ return false;
88
+ const tr = added;
89
+ const table = tr.doc.nodeAt(tableStart - 1);
90
+ const map = TableMap.get(table);
91
+ const column = side === 'left' ? left : right;
92
+ for (let row = 0; row < map.height; row++) {
93
+ const pos = map.map[row * map.width + column];
94
+ const cell = table.nodeAt(pos);
95
+ tr.setNodeMarkup(tableStart + pos, undefined, { ...cell.attrs, alignment: null });
96
+ }
97
+ dispatch(tr);
98
+ return true;
99
+ }
100
+ // Every cell in the caret's columns aligned; left is GFM's default, so it
101
+ // clears the alignment rather than writing `:---`.
102
+ function alignColumn(state, dispatch, alignment) {
103
+ const { map, table, tableStart, left, right } = selectedRect(state);
104
+ const tr = state.tr;
105
+ const value = alignment === 'left' ? null : alignment;
106
+ for (let row = 0; row < map.height; row++) {
107
+ for (let column = left; column < right; column++) {
108
+ const pos = map.map[row * map.width + column];
109
+ const cell = table.nodeAt(pos);
110
+ if (cell.attrs.alignment !== value)
111
+ tr.setNodeMarkup(tableStart + pos, undefined, { ...cell.attrs, alignment: value });
112
+ }
113
+ }
114
+ if (tr.docChanged)
115
+ dispatch(tr);
116
+ return true;
117
+ }
118
+ // The caret's cell: its row and column, and the table's size.
119
+ function cellAt(state) {
120
+ const rect = selectedRect(state);
121
+ return { ...rect, row: rect.top, column: rect.left };
122
+ }
123
+ // The caret out of the table: to the start of the block after it, or a
124
+ // new line there when nothing follows.
125
+ function exitTable(state, dispatch) {
126
+ const { tableStart, table } = selectedRect(state);
127
+ const after = tableStart - 1 + table.nodeSize;
128
+ const tr = state.tr;
129
+ const next = tr.doc.resolve(after).nodeAfter;
130
+ if (!next || !next.isTextblock)
131
+ tr.insert(after, state.schema.nodes.paragraph.create());
132
+ tr.setSelection(TextSelection.near(tr.doc.resolve(after + 1), 1));
133
+ dispatch(tr.scrollIntoView());
134
+ return true;
135
+ }
136
+ /**
137
+ * The editor's table keys, before Milkdown's own (which go to the next
138
+ * cell on Tab, and out of the table on any Enter). True when it handled
139
+ * the key.
140
+ */
141
+ export function handleTableKey(state, dispatch, event) {
142
+ if (event.metaKey || event.ctrlKey || event.altKey || !isInTable(state) || !state.selection.empty)
143
+ return false;
144
+ const { map, row, column } = cellAt(state);
145
+ const lastRow = row === map.height - 1;
146
+ if (event.key === 'Tab' && !event.shiftKey && lastRow && column === map.width - 1)
147
+ return addRow(state, dispatch, 'end');
148
+ if (event.key !== 'Enter')
149
+ return false;
150
+ if (event.shiftKey)
151
+ return true;
152
+ if (lastRow)
153
+ return exitTable(state, dispatch);
154
+ const { tableStart } = selectedRect(state);
155
+ const below = map.map[(row + 1) * map.width + column];
156
+ dispatch(state.tr.setSelection(TextSelection.near(state.doc.resolve(tableStart + below + 1))).scrollIntoView());
157
+ return true;
158
+ }
159
+ /** A row added at the table's end, the caret into its first cell (the "Add a row" bar). */
160
+ export function addRowAtEnd(state, dispatch) {
161
+ return isInTable(state) && addRow(state, dispatch, 'end');
162
+ }
@@ -2,6 +2,7 @@ import { type Ref } from 'react';
2
2
  import type { EditorState } from '@milkdown/kit/prose/state';
3
3
  import { type FormatItem } from './rich-text-format-items.js';
4
4
  import { type FormatId } from './rich-text-formats.js';
5
+ import type { TableAction } from './rich-text-table.js';
5
6
  declare const SETS: {
6
7
  all: FormatItem[][];
7
8
  selection: FormatItem[][];
@@ -14,8 +15,12 @@ export type FormatButtonsProps = {
14
15
  readOnly: boolean;
15
16
  /** A button pressed: run the format on the editor and give it focus back. */
16
17
  onFormat: (id: FormatId) => void;
18
+ /** A Table menu item picked (only the fixed toolbar has the menu). */
19
+ onTable?: (action: TableAction) => void;
20
+ /** The editable element, for the Table menu to give focus back to. */
21
+ text?: () => HTMLElement | null;
17
22
  };
18
- export declare function FormatButtons({ formats, state, readOnly, onFormat }: FormatButtonsProps): import("react").JSX.Element;
23
+ export declare function FormatButtons({ formats, state, readOnly, onFormat, onTable, text }: FormatButtonsProps): import("react").JSX.Element;
19
24
  export interface RichTextToolbarProps {
20
25
  /** The editor's state now, to show which formats are on. Null until it's made. */
21
26
  state: EditorState | null;
@@ -24,8 +29,12 @@ export interface RichTextToolbarProps {
24
29
  controls?: string;
25
30
  /** A button pressed: run the format on the editor and give it focus back. */
26
31
  onFormat: (id: FormatId) => void;
32
+ /** A Table menu item picked: run it on the editor and give it focus back. */
33
+ onTable: (action: TableAction) => void;
34
+ /** The editable element, for the Table menu to give focus back to. */
35
+ text: () => HTMLElement | null;
27
36
  /** The toolbar's element, for Alt+F10 to reach it from the text. */
28
37
  ref?: Ref<HTMLDivElement>;
29
38
  }
30
- export declare function RichTextToolbar({ state, readOnly, controls, onFormat, ref }: RichTextToolbarProps): import("react").JSX.Element;
39
+ export declare function RichTextToolbar({ state, readOnly, controls, onFormat, onTable, text, ref }: RichTextToolbarProps): import("react").JSX.Element;
31
40
  export {};
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // The rich-text editor's fixed toolbar: one row of pressed/not-pressed
3
- // buttons above the text, each a format and its keyboard shortcut.
3
+ // buttons above the text, each a format and its keyboard shortcut, and a
4
+ // Table menu (rich-text-table-menu.tsx).
4
5
  //
5
6
  // - One tab stop (ARIA's toolbar pattern): Tab reaches the last button
6
7
  // used, the arrow keys, Home and End move along the row.
@@ -16,35 +17,21 @@ import { Fragment, useRef, useState } from 'react';
16
17
  import { Separator } from '../ui/separator.js';
17
18
  import { Toggle } from '../ui/toggle.js';
18
19
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip.js';
19
- import { FORMAT_GROUPS } from './rich-text-format-items.js';
20
+ import { FORMAT_GROUPS, FORMAT_ITEMS } from './rich-text-format-items.js';
20
21
  import { formatOn } from './rich-text-formats.js';
21
- function isApple() {
22
- return typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
23
- }
24
- // `aria-keyshortcuts` names real keys: Meta on Apple, Control elsewhere.
25
- function ariaKeys(keys, apple) {
26
- return keys.replace('Mod', apple ? 'Meta' : 'Control');
27
- }
28
- // What a person reads: ⌘⌥X on Apple, Ctrl+Alt+X elsewhere.
29
- function shownKeys(keys, apple) {
30
- if (!apple)
31
- return keys.replace('Mod', 'Ctrl');
32
- const symbols = { Mod: '⌘', Alt: '⌥', Shift: '⇧', Enter: '↵' };
33
- return keys
34
- .split('+')
35
- .map((part) => symbols[part] ?? part)
36
- .join('');
37
- }
22
+ import { ariaKeys, isApple, shownKeys } from './rich-text-shortcuts.js';
23
+ import { RichTextTableMenu } from './rich-text-table-menu.js';
38
24
  // Which buttons a toolbar has: every format (the fixed one), or the ones
39
25
  // that apply to selected words (the floating one).
26
+ const SELECTION = ['bold', 'italic', 'strike', 'code', 'link'];
40
27
  const SETS = {
41
28
  all: FORMAT_GROUPS,
42
- selection: [FORMAT_GROUPS[0], FORMAT_GROUPS[4]],
29
+ selection: [SELECTION.slice(0, 4), SELECTION.slice(4)].map((ids) => ids.map((id) => FORMAT_ITEMS.get(id))),
43
30
  };
44
31
  // A toolbar's row of buttons: one tab stop, the last one used (ARIA's
45
32
  // toolbar pattern); the arrow keys, Home and End move along the row. The
46
33
  // toolbar's element (`role="toolbar"`, its name) is the caller's.
47
- export function FormatButtons({ formats, state, readOnly, onFormat }) {
34
+ export function FormatButtons({ formats, state, readOnly, onFormat, onTable, text = () => null }) {
48
35
  const apple = isApple();
49
36
  const groups = SETS[formats];
50
37
  const items = groups.flat();
@@ -71,6 +58,10 @@ export function FormatButtons({ formats, state, readOnly, onFormat }) {
71
58
  };
72
59
  return (_jsx(TooltipProvider, { delay: 400, children: groups.map((group, groupIndex) => (_jsxs(Fragment, { children: [groupIndex > 0 && _jsx(Separator, { orientation: "vertical", className: "mx-1 h-5 self-center" }), group.map((item) => {
73
60
  const index = items.indexOf(item);
61
+ if (item.id === 'table')
62
+ return (_jsx(RichTextTableMenu, { ref: (element) => {
63
+ buttons.current[index] = element;
64
+ }, state: state, readOnly: readOnly, tabIndex: index === current ? 0 : -1, onFocus: () => setCurrent(index), onKeyDown: onKeyDown, onInsert: () => onFormat('table'), onAction: (action) => onTable?.(action), text: text }, item.id));
74
65
  const Icon = item.icon;
75
66
  const tip = [
76
67
  item.label,
@@ -86,6 +77,6 @@ export function FormatButtons({ formats, state, readOnly, onFormat }) {
86
77
  onMouseDown: (event) => event.preventDefault(), onFocus: () => setCurrent(index), onKeyDown: onKeyDown, onPressedChange: () => onFormat(item.id) }), children: _jsx(Icon, {}) }), _jsx(TooltipContent, { children: tip })] }, item.id));
87
78
  })] }, groupIndex))) }));
88
79
  }
89
- export function RichTextToolbar({ state, readOnly, controls, onFormat, ref }) {
90
- return (_jsx("div", { ref: ref, role: "toolbar", "aria-label": "Formatting", "aria-controls": controls, "data-slot": "rich-text-toolbar", className: "flex flex-wrap items-center gap-0.5 border-b border-input px-1 py-1", children: _jsx(FormatButtons, { formats: "all", state: state, readOnly: readOnly, onFormat: onFormat }) }));
80
+ export function RichTextToolbar({ state, readOnly, controls, onFormat, onTable, text, ref }) {
81
+ return (_jsx("div", { ref: ref, role: "toolbar", "aria-label": "Formatting", "aria-controls": controls, "data-slot": "rich-text-toolbar", className: "flex flex-wrap items-center gap-0.5 border-b border-input px-1 py-1", children: _jsx(FormatButtons, { formats: "all", state: state, readOnly: readOnly, onFormat: onFormat, onTable: onTable, text: text }) }));
91
82
  }
@@ -0,0 +1,19 @@
1
+ import { type CalloutKind } from '../../lib/markdown-callout.js';
2
+ export interface CalloutKindMenuProps {
3
+ kind: CalloutKind;
4
+ readOnly: boolean;
5
+ onPick: (kind: CalloutKind) => void;
6
+ onRemove: () => void;
7
+ /** The editable element, where focus goes when the menu closes. */
8
+ text: () => HTMLElement | null;
9
+ }
10
+ export declare function CalloutKindMenu({ kind, readOnly, onPick, onRemove, text }: CalloutKindMenuProps): import("react").JSX.Element;
11
+ export interface TableBarProps {
12
+ /** Under the table (a row) or along its right (a column). */
13
+ side: 'row' | 'column';
14
+ /** The caret is in this table: shown without the pointer over it. */
15
+ active: boolean;
16
+ readOnly: boolean;
17
+ onAdd: () => void;
18
+ }
19
+ export declare function TableBar({ side, active, readOnly, onAdd }: TableBarProps): import("react").JSX.Element | null;
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // The React parts of the editor's node views (rich-text-node-views.ts),
3
+ // rendered into their slots through portals:
4
+ //
5
+ // - A callout's icon is a menu of its kinds (and Remove callout). From the
6
+ // keyboard, "/" at the start of a line in the callout lists the kinds
7
+ // too (rich-text-slash.ts), so the icon is a mouse shortcut and out of
8
+ // the tab order.
9
+ // - A table's bars add a column at its right and a row at its end, shown
10
+ // while the pointer is over the table or the caret is in it. Mouse
11
+ // shortcuts as well: Tab in the last cell adds a row, and the toolbar's
12
+ // Table menu does both.
13
+ //
14
+ // Each gives focus back to the text when it's done.
15
+ import { PlusIcon } from 'lucide-react';
16
+ import { cn } from 'cn';
17
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '../ui/dropdown-menu.js';
18
+ import { CALLOUT_KINDS } from '../../lib/markdown-callout.js';
19
+ import { CALLOUT_LOOK } from './prose-look.js';
20
+ export function CalloutKindMenu({ kind, readOnly, onPick, onRemove, text }) {
21
+ const look = CALLOUT_LOOK[kind];
22
+ const Icon = look.icon;
23
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { tabIndex: -1, disabled: readOnly, "aria-label": `${look.label} callout: change its kind`, "data-slot": "rich-text-callout-kind", className: "flex size-5 items-center justify-center rounded-md outline-none hover:bg-foreground/10 focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none", children: _jsx(Icon, { "aria-hidden": "true", className: cn('size-4', look.tint) }) }), _jsxs(DropdownMenuContent, { className: "w-44", finalFocus: () => text() ?? true, children: [_jsx(DropdownMenuRadioGroup, { value: kind, onValueChange: (value) => onPick(value), children: CALLOUT_KINDS.map((one) => {
24
+ const OneIcon = CALLOUT_LOOK[one].icon;
25
+ return (_jsxs(DropdownMenuRadioItem, { value: one, closeOnClick: true, children: [_jsx(OneIcon, { "aria-hidden": "true", className: CALLOUT_LOOK[one].tint }), CALLOUT_LOOK[one].label] }, one));
26
+ }) }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { onClick: onRemove, children: "Remove callout" })] })] }));
27
+ }
28
+ export function TableBar({ side, active, readOnly, onAdd }) {
29
+ if (readOnly)
30
+ return null;
31
+ return (_jsx("button", { type: "button", tabIndex: -1, "aria-label": side === 'row' ? 'Add a row' : 'Add a column', "data-slot": `rich-text-table-add-${side}`, className: cn('flex flex-1 items-center justify-center rounded-md text-muted-foreground transition-opacity hover:bg-muted hover:text-foreground', 'opacity-0 group-hover/table:opacity-100', active && 'opacity-100'),
32
+ // A press keeps focus in the text: the caret moves into the new cell.
33
+ onMouseDown: (event) => event.preventDefault(), onClick: onAdd, children: _jsx(PlusIcon, { "aria-hidden": "true", className: "size-3.5" }) }));
34
+ }
@@ -0,0 +1,18 @@
1
+ import { type ReactNode } from 'react';
2
+ /** A filter that's on: its chip's text ("In stock: No") and how to take it off. */
3
+ export type ActiveFilter = {
4
+ id: string;
5
+ label: string;
6
+ onRemove: () => void;
7
+ };
8
+ export type SecondaryFiltersProps = {
9
+ /** The filter controls the button opens, each with its own label. */
10
+ children: ReactNode;
11
+ /** The filters among them that are on now, in the order their chips show. */
12
+ active: readonly ActiveFilter[];
13
+ /** Takes every one of them off at once. */
14
+ onClear: () => void;
15
+ /** The button's text. */
16
+ label?: string;
17
+ };
18
+ export declare function SecondaryFilters({ children, active, onClear, label }: SecondaryFiltersProps): import("react").JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // A table toolbar's less-used filters behind one "Filters" button, so a
3
+ // table with many filters still starts near the top of the page (issue
4
+ // #93). The primary filters (search, status, whatever most visits use)
5
+ // stay in the toolbar; this goes beside them, holding the rest:
6
+ //
7
+ // - The button says how many of its filters are on ("Filters 2", named
8
+ // "Filters, 2 active"), and opens them in a popover, or a sheet from the
9
+ // bottom on a phone. Base UI gives focus back to the button on close.
10
+ // - Each filter that's on shows beside the button as a chip with a remove
11
+ // button, so a filtered list never looks unfiltered while they're
12
+ // hidden; "Clear filters" takes them all off. Both are here too, in the
13
+ // panel's footer.
14
+ // - It holds no state of its own: the app passes what's on (`active`, from
15
+ // the URL like every filter, useTableUrlState), so the count and chips
16
+ // follow back/forward and saved views. Collapsing is display only.
17
+ // - Esc in a filter's open list closes the list only (Base UI's lists keep
18
+ // their Esc); the next one closes the panel.
19
+ import { useState } from 'react';
20
+ import { SlidersHorizontalIcon, XIcon } from 'lucide-react';
21
+ import { Button } from '../ui/button.js';
22
+ import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from '../ui/popover.js';
23
+ import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from '../ui/sheet.js';
24
+ import { useIsMobile } from '../../hooks/use-mobile.js';
25
+ export function SecondaryFilters({ children, active, onClear, label = 'Filters' }) {
26
+ const isMobile = useIsMobile();
27
+ const [open, setOpen] = useState(false);
28
+ const count = active.length;
29
+ const trigger = (_jsxs(Button, { variant: "outline", "aria-label": count > 0 ? `${label}, ${count} active` : label, children: [_jsx(SlidersHorizontalIcon, { "aria-hidden": "true" }), label, count > 0 && (_jsx("span", { "aria-hidden": "true", className: "rounded-full bg-primary px-1.5 type-caption text-primary-foreground tabular-nums", children: count }))] }));
30
+ const footer = (_jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { variant: "ghost", size: "sm", disabled: count === 0, onClick: onClear, children: "Clear filters" }), _jsx(Button, { size: "sm", onClick: () => setOpen(false), children: "Done" })] }));
31
+ return (_jsxs("div", { "data-slot": "secondary-filters", className: "flex flex-wrap items-center gap-2", children: [isMobile ? (_jsxs(Sheet, { open: open, onOpenChange: setOpen, children: [_jsx(SheetTrigger, { render: trigger }), _jsxs(SheetContent, { side: "bottom", className: "max-h-[85svh] overflow-y-auto", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: label }) }), _jsx("div", { className: "flex flex-col gap-3 px-4 [&>*]:w-full", children: children }), _jsx(SheetFooter, { children: footer })] })] })) : (_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { render: trigger }), _jsxs(PopoverContent, { align: "start", className: "w-auto min-w-64 gap-3 p-3", children: [_jsx(PopoverTitle, { children: label }), _jsx("div", { className: "flex flex-col gap-3", children: children }), footer] })] })), active.map((filter) => (_jsxs("span", { "data-slot": "secondary-filter-chip", className: "inline-flex h-7 items-center gap-0.5 rounded-md bg-muted pr-0.5 pl-2 type-caption text-foreground", children: [filter.label, _jsx(Button, { variant: "ghost", size: "icon-xs", "aria-label": `Remove filter: ${filter.label}`, onClick: filter.onRemove, children: _jsx(XIcon, { "aria-hidden": "true" }) })] }, filter.id))), count > 1 && (_jsx(Button, { variant: "ghost", size: "sm", onClick: onClear, children: "Clear filters" }))] }));
32
+ }
@@ -1,7 +1,7 @@
1
1
  import { useRender } from "@base-ui/react/use-render";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  declare const badgeVariants: (props?: ({
4
- variant?: "link" | "success" | "default" | "outline" | "secondary" | "ghost" | "destructive" | "info" | "warning" | "outline-success" | "outline-warning" | "outline-info" | "outline-destructive" | "tinted-success" | "tinted-warning" | "tinted-info" | "tinted-destructive" | null | undefined;
4
+ variant?: "link" | "success" | "default" | "outline" | "secondary" | "ghost" | "destructive" | "warning" | "info" | "outline-success" | "outline-warning" | "outline-info" | "outline-destructive" | "tinted-success" | "tinted-warning" | "tinted-info" | "tinted-destructive" | null | undefined;
5
5
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
6
6
  declare function Badge({ className, variant, render, ...props }: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
7
7
  export { Badge, badgeVariants };
@@ -53,8 +53,14 @@ function ComboboxChip({ className, children, showRemove = true, ...props }) {
53
53
  // it after the chip ("Remove fragile") when the chip is plain text.
54
54
  , { "aria-label": typeof children === "string" ? `Remove ${children}` : "Remove", render: _jsx(Button, { variant: "ghost", size: "icon-xs" }), className: "-ml-1 opacity-50 hover:opacity-100", "data-slot": "combobox-chip-remove", children: _jsx(XIcon, { className: "pointer-events-none" }) }))] }));
55
55
  }
56
+ // Patched (ui-foundation, issue #94): upstream's input always keeps 4rem,
57
+ // so chips that fit on one line still pushed it onto an empty second one
58
+ // and the control stood twice as tall as a Select beside it. With chips
59
+ // in and no focus it takes only what's left of the chips' line (none, if
60
+ // they fill it); focused, it gets its 4rem back to type in, wrapping then
61
+ // if the line is full.
56
62
  function ComboboxChipsInput({ className, ...props }) {
57
- return (_jsx(ComboboxPrimitive.Input, { "data-slot": "combobox-chip-input", className: cn("min-w-16 flex-1 outline-none", className), ...props }));
63
+ return (_jsx(ComboboxPrimitive.Input, { "data-slot": "combobox-chip-input", className: cn("min-w-16 flex-1 outline-none [[data-slot=combobox-chips]:has([data-slot=combobox-chip])_&:not(:focus)]:min-w-0", className), ...props }));
58
64
  }
59
65
  function useComboboxAnchor() {
60
66
  return React.useRef(null);
@@ -0,0 +1,29 @@
1
+ import * as React from "react";
2
+ import { Menu as MenuPrimitive } from "@base-ui/react/menu";
3
+ declare function DropdownMenu({ ...props }: MenuPrimitive.Root.Props): React.JSX.Element;
4
+ declare function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props): React.JSX.Element;
5
+ declare function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props): React.JSX.Element;
6
+ declare function DropdownMenuContent({ align, alignOffset, side, sideOffset, className, ...props }: MenuPrimitive.Popup.Props & Pick<MenuPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">): React.JSX.Element;
7
+ declare function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props): React.JSX.Element;
8
+ declare function DropdownMenuLabel({ className, inset, ...props }: MenuPrimitive.GroupLabel.Props & {
9
+ inset?: boolean;
10
+ }): React.JSX.Element;
11
+ declare function DropdownMenuItem({ className, inset, variant, ...props }: MenuPrimitive.Item.Props & {
12
+ inset?: boolean;
13
+ variant?: "default" | "destructive";
14
+ }): React.JSX.Element;
15
+ declare function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props): React.JSX.Element;
16
+ declare function DropdownMenuSubTrigger({ className, inset, children, ...props }: MenuPrimitive.SubmenuTrigger.Props & {
17
+ inset?: boolean;
18
+ }): React.JSX.Element;
19
+ declare function DropdownMenuSubContent({ align, alignOffset, side, sideOffset, className, ...props }: React.ComponentProps<typeof DropdownMenuContent>): React.JSX.Element;
20
+ declare function DropdownMenuCheckboxItem({ className, children, checked, inset, ...props }: MenuPrimitive.CheckboxItem.Props & {
21
+ inset?: boolean;
22
+ }): React.JSX.Element;
23
+ declare function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props): React.JSX.Element;
24
+ declare function DropdownMenuRadioItem({ className, children, inset, ...props }: MenuPrimitive.RadioItem.Props & {
25
+ inset?: boolean;
26
+ }): React.JSX.Element;
27
+ declare function DropdownMenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props): React.JSX.Element;
28
+ declare function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">): React.JSX.Element;
29
+ export { DropdownMenu, DropdownMenuPortal, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, };
@@ -0,0 +1,51 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { Menu as MenuPrimitive } from "@base-ui/react/menu";
4
+ import { cn } from "cn";
5
+ import { ChevronRightIcon, CheckIcon } from "lucide-react";
6
+ function DropdownMenu({ ...props }) {
7
+ return _jsx(MenuPrimitive.Root, { "data-slot": "dropdown-menu", ...props });
8
+ }
9
+ function DropdownMenuPortal({ ...props }) {
10
+ return _jsx(MenuPrimitive.Portal, { "data-slot": "dropdown-menu-portal", ...props });
11
+ }
12
+ function DropdownMenuTrigger({ ...props }) {
13
+ return _jsx(MenuPrimitive.Trigger, { "data-slot": "dropdown-menu-trigger", ...props });
14
+ }
15
+ function DropdownMenuContent({ align = "start", alignOffset = 0, side = "bottom", sideOffset = 4, className, ...props }) {
16
+ return (_jsx(MenuPrimitive.Portal, { children: _jsx(MenuPrimitive.Positioner, { className: "isolate z-50 outline-none", align: align, alignOffset: alignOffset, side: side, sideOffset: sideOffset, children: _jsx(MenuPrimitive.Popup, { "data-slot": "dropdown-menu-content", className: cn("z-50 max-h-(--available-height) w-(--anchor-width) min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:overflow-hidden data-closed:fade-out-0 data-closed:zoom-out-95", className), ...props }) }) }));
17
+ }
18
+ function DropdownMenuGroup({ ...props }) {
19
+ return _jsx(MenuPrimitive.Group, { "data-slot": "dropdown-menu-group", ...props });
20
+ }
21
+ function DropdownMenuLabel({ className, inset, ...props }) {
22
+ return (_jsx(MenuPrimitive.GroupLabel, { "data-slot": "dropdown-menu-label", "data-inset": inset, className: cn("px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7", className), ...props }));
23
+ }
24
+ function DropdownMenuItem({ className, inset, variant = "default", ...props }) {
25
+ return (_jsx(MenuPrimitive.Item, { "data-slot": "dropdown-menu-item", "data-inset": inset, "data-variant": variant, className: cn("group/dropdown-menu-item relative flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive", className), ...props }));
26
+ }
27
+ function DropdownMenuSub({ ...props }) {
28
+ return _jsx(MenuPrimitive.SubmenuRoot, { "data-slot": "dropdown-menu-sub", ...props });
29
+ }
30
+ function DropdownMenuSubTrigger({ className, inset, children, ...props }) {
31
+ return (_jsxs(MenuPrimitive.SubmenuTrigger, { "data-slot": "dropdown-menu-sub-trigger", "data-inset": inset, className: cn("flex cursor-default items-center gap-1.5 rounded-md px-1.5 py-1 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-popup-open:bg-accent data-popup-open:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props, children: [children, _jsx(ChevronRightIcon, { className: "ml-auto" })] }));
32
+ }
33
+ function DropdownMenuSubContent({ align = "start", alignOffset = -3, side = "right", sideOffset = 0, className, ...props }) {
34
+ return (_jsx(DropdownMenuContent, { "data-slot": "dropdown-menu-sub-content", className: cn("w-auto min-w-[96px] rounded-lg bg-popover p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className), align: align, alignOffset: alignOffset, side: side, sideOffset: sideOffset, ...props }));
35
+ }
36
+ function DropdownMenuCheckboxItem({ className, children, checked, inset, ...props }) {
37
+ return (_jsxs(MenuPrimitive.CheckboxItem, { "data-slot": "dropdown-menu-checkbox-item", "data-inset": inset, className: cn("relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), checked: checked, ...props, children: [_jsx("span", { className: "pointer-events-none absolute right-2 flex items-center justify-center", "data-slot": "dropdown-menu-checkbox-item-indicator", children: _jsx(MenuPrimitive.CheckboxItemIndicator, { children: _jsx(CheckIcon, {}) }) }), children] }));
38
+ }
39
+ function DropdownMenuRadioGroup({ ...props }) {
40
+ return (_jsx(MenuPrimitive.RadioGroup, { "data-slot": "dropdown-menu-radio-group", ...props }));
41
+ }
42
+ function DropdownMenuRadioItem({ className, children, inset, ...props }) {
43
+ return (_jsxs(MenuPrimitive.RadioItem, { "data-slot": "dropdown-menu-radio-item", "data-inset": inset, className: cn("relative flex cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props, children: [_jsx("span", { className: "pointer-events-none absolute right-2 flex items-center justify-center", "data-slot": "dropdown-menu-radio-item-indicator", children: _jsx(MenuPrimitive.RadioItemIndicator, { children: _jsx(CheckIcon, {}) }) }), children] }));
44
+ }
45
+ function DropdownMenuSeparator({ className, ...props }) {
46
+ return (_jsx(MenuPrimitive.Separator, { "data-slot": "dropdown-menu-separator", className: cn("-mx-1 my-1 h-px bg-border", className), ...props }));
47
+ }
48
+ function DropdownMenuShortcut({ className, ...props }) {
49
+ return (_jsx("span", { "data-slot": "dropdown-menu-shortcut", className: cn("ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground", className), ...props }));
50
+ }
51
+ export { DropdownMenu, DropdownMenuPortal, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, };
@@ -9,7 +9,7 @@ export declare function useTableUrlState<F extends string, M extends string = ne
9
9
  setPage: (next: number) => void;
10
10
  setSorting: (next: SortingState) => void;
11
11
  setFilter: (name: F, value: string) => void;
12
- setFilters: (changes: Partial<Record<F, string>>) => void;
12
+ setFilters: (changes: Partial<Record<F, string>> & Partial<Record<M, string[]>>) => void;
13
13
  setMultiFilter: (name: M, values: string[]) => void;
14
14
  applyView: (view: TableView<F, M>) => void;
15
15
  };
@@ -36,11 +36,12 @@ export function useTableUrlState(filterNames, multiFilterNames = [], views = {})
36
36
  setPage: (next) => update({ page: next > 1 ? String(next) : null }),
37
37
  setSorting: (next) => update({ sort: serializeSort(next), page: null }),
38
38
  setFilter: (name, value) => update({ [name]: value, page: null }),
39
- // Several single-value filters in one call — for any user action that
40
- // changes more than one (both ends of a date range, "clear all"). Two
41
- // setFilter()s in the same tick lose all but the last: react-router
42
- // hands each setSearchParams updater the params from the last render,
43
- // not the ones the previous updater produced.
39
+ // Several filters in one call — for any user action that changes more
40
+ // than one (both ends of a date range, "clear filters"). Two setters in
41
+ // the same tick lose all but the last: react-router hands each
42
+ // setSearchParams updater the params from the last render, not the
43
+ // ones the previous updater produced. Single-value filters take a
44
+ // string ('' clears), multi-value ones a list ([] clears).
44
45
  setFilters: (changes) => update({ ...changes, page: null }),
45
46
  setMultiFilter: (name, values) => update({ [name]: values, page: null }),
46
47
  // Replaces the whole filter + sort state at once — a saved view. See
package/dist/index.d.ts CHANGED
@@ -15,6 +15,7 @@ export { ListEditor } from './components/app/list-editor.js';
15
15
  export { PasswordInput } from './components/app/password-input.js';
16
16
  export { RichTextEditor, type RichTextEditorProps } from './components/app/rich-text-editor.js';
17
17
  export { RouteErrorBoundary } from './components/app/route-error-boundary.js';
18
+ export { SecondaryFilters, type ActiveFilter, type SecondaryFiltersProps } from './components/app/secondary-filters.js';
18
19
  export { StageCircle, type StageCircleProps, type StageCircleStage, type StageCircleTone, } from './components/app/stage-circle.js';
19
20
  export { ThemeProvider } from './components/theme-provider.js';
20
21
  export { LoginRoute, type LoginRouteProps } from './routes/login.js';
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ export { ListEditor } from './components/app/list-editor.js';
20
20
  export { PasswordInput } from './components/app/password-input.js';
21
21
  export { RichTextEditor } from './components/app/rich-text-editor.js';
22
22
  export { RouteErrorBoundary } from './components/app/route-error-boundary.js';
23
+ export { SecondaryFilters } from './components/app/secondary-filters.js';
23
24
  export { StageCircle, } from './components/app/stage-circle.js';
24
25
  export { ThemeProvider } from './components/theme-provider.js';
25
26
  // Screens outside the app shell