@tristan2828/ui-foundation 4.49.0 → 4.50.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.
- package/conventions/docs/cell-patterns.md +12 -10
- package/dist/components/app/board.js +2 -2
- package/dist/components/app/milkdown-editor.js +11 -3
- package/dist/components/app/rich-text-toolbar.d.ts +1 -0
- package/dist/components/app/rich-text-toolbar.js +2 -0
- package/dist/components/app/rich-text-tools-bar.d.ts +18 -0
- package/dist/components/app/rich-text-tools-bar.js +17 -0
- package/package.json +1 -1
|
@@ -26,15 +26,15 @@
|
|
|
26
26
|
- [A variant: a pressed icon inside another value's cell](#a-variant-a-pressed-icon-inside-another-values-cell) · lines 565–648
|
|
27
27
|
- [16. Title linking to the record's view](#16-title-linking-to-the-records-view) · lines 650–685
|
|
28
28
|
- [17. Quick action on the view](#17-quick-action-on-the-view) · lines 687–760
|
|
29
|
-
- [18. Editable value](#18-editable-value) · lines 762–
|
|
30
|
-
- [A list of sub-items](#a-list-of-sub-items) · lines
|
|
31
|
-
- [In a table's rows](#in-a-tables-rows) · lines
|
|
32
|
-
- [A table of a few short columns: `width="content"`](#a-table-of-a-few-short-columns-widthcontent) · lines
|
|
33
|
-
- [19. Stage circle for an ordered status](#19-stage-circle-for-an-ordered-status) · lines
|
|
34
|
-
- [20. Count linking to the related records](#20-count-linking-to-the-related-records) · lines
|
|
35
|
-
- [21. Rows grouped under headings, and a pill filter](#21-rows-grouped-under-headings-and-a-pill-filter) · lines
|
|
36
|
-
- [22. A highlighted row, and rows that are done](#22-a-highlighted-row-and-rows-that-are-done) · lines
|
|
37
|
-
- [23. A Group menu, and a record read beside the list](#23-a-group-menu-and-a-record-read-beside-the-list) · lines
|
|
29
|
+
- [18. Editable value](#18-editable-value) · lines 762–1218
|
|
30
|
+
- [A list of sub-items](#a-list-of-sub-items) · lines 1034–1119
|
|
31
|
+
- [In a table's rows](#in-a-tables-rows) · lines 1121–1206
|
|
32
|
+
- [A table of a few short columns: `width="content"`](#a-table-of-a-few-short-columns-widthcontent) · lines 1208–1218
|
|
33
|
+
- [19. Stage circle for an ordered status](#19-stage-circle-for-an-ordered-status) · lines 1220–1286
|
|
34
|
+
- [20. Count linking to the related records](#20-count-linking-to-the-related-records) · lines 1288–1352
|
|
35
|
+
- [21. Rows grouped under headings, and a pill filter](#21-rows-grouped-under-headings-and-a-pill-filter) · lines 1354–1422
|
|
36
|
+
- [22. A highlighted row, and rows that are done](#22-a-highlighted-row-and-rows-that-are-done) · lines 1424–1452
|
|
37
|
+
- [23. A Group menu, and a record read beside the list](#23-a-group-menu-and-a-record-read-beside-the-list) · lines 1454–1528
|
|
38
38
|
|
|
39
39
|
<!-- /index -->
|
|
40
40
|
|
|
@@ -1010,7 +1010,9 @@ const price = editInPlace({
|
|
|
1010
1010
|
Markdown, and `<Markdown>` shows them. The fixed toolbar's Full screen
|
|
1011
1011
|
button fills the window with the field (Esc leaves; nothing is lost or
|
|
1012
1012
|
saved by it), and on a touch screen or under 768px it carries Indent
|
|
1013
|
-
and Outdent for list items, where there is no Tab. A
|
|
1013
|
+
and Outdent for list items, where there is no Tab. A field with only the
|
|
1014
|
+
floating toolbar (`inline` notes) gets the same two in a small "Editor
|
|
1015
|
+
tools" strip over its corner while it has focus. A new checklist item
|
|
1014
1016
|
shows a "To-do" placeholder and the caret until you type.
|
|
1015
1017
|
`toolbar={false}` leaves out the toolbars and the slash menu where a
|
|
1016
1018
|
value is short and the shortcuts are enough; `toolbar="floating"` keeps
|
|
@@ -76,7 +76,7 @@ export function Board({ 'aria-label': ariaLabel, columns, items, getId, getColum
|
|
|
76
76
|
return (_jsx("div", { "data-slot": "board", "data-state": "error", children: _jsx(ErrorState, { error: error, onRetry: onRetry }) }));
|
|
77
77
|
}
|
|
78
78
|
if (isLoading) {
|
|
79
|
-
return (_jsx("div", { "data-slot": "board", "data-state": "loading", role: "group", "aria-busy": "true", "aria-label": ariaLabel, className: "flex gap-3 overflow-x-auto pb-2", children: columns.map((column) => (_jsxs("div", { className: cn('flex flex-col gap-2 rounded-lg bg-muted p-2', COLUMN_WIDTH), children: [_jsx(Skeleton, { className: "h-5 w-24" }), _jsx(Skeleton, { className: "h-20 w-full" }), _jsx(Skeleton, { className: "h-20 w-full" })] }, column.value))) }));
|
|
79
|
+
return (_jsx("div", { "data-slot": "board", "data-state": "loading", role: "group", "aria-busy": "true", "aria-label": ariaLabel, className: "flex gap-3 overflow-x-auto pb-2", children: columns.map((column) => (_jsxs("div", { className: cn('flex flex-col gap-2 rounded-lg bg-muted p-2 [--muted-foreground:var(--table-highlight-muted-foreground)]', COLUMN_WIDTH), children: [_jsx(Skeleton, { className: "h-5 w-24" }), _jsx(Skeleton, { className: "h-20 w-full" }), _jsx(Skeleton, { className: "h-20 w-full" })] }, column.value))) }));
|
|
80
80
|
}
|
|
81
81
|
const shown = items.filter((item) => columns.some((column) => column.value === getColumn(item)));
|
|
82
82
|
if (shown.length === 0) {
|
|
@@ -109,7 +109,7 @@ function BoardColumnView({ column, count, headingLevel, onDrop, children, }) {
|
|
|
109
109
|
const headingId = useId();
|
|
110
110
|
const Heading = `h${headingLevel}`;
|
|
111
111
|
const accepts = (event) => onDrop !== undefined && event.dataTransfer.types.includes(DRAG_TYPE);
|
|
112
|
-
return (_jsxs("section", { "aria-labelledby": headingId, "data-slot": "board-column", "data-over": over || undefined, className: cn('flex snap-start flex-col gap-2 rounded-lg bg-muted p-2 outline-2 outline-offset-2 outline-transparent data-over:outline-ring', COLUMN_WIDTH), onDragOver: (event) => {
|
|
112
|
+
return (_jsxs("section", { "aria-labelledby": headingId, "data-slot": "board-column", "data-over": over || undefined, className: cn('flex snap-start flex-col gap-2 rounded-lg bg-muted p-2 [--muted-foreground:var(--table-highlight-muted-foreground)] outline-2 outline-offset-2 outline-transparent data-over:outline-ring', COLUMN_WIDTH), onDragOver: (event) => {
|
|
113
113
|
if (!accepts(event))
|
|
114
114
|
return;
|
|
115
115
|
event.preventDefault();
|
|
@@ -82,6 +82,7 @@ import { addRowAtEnd, handleTableKey, runTableAction } from './rich-text-table.j
|
|
|
82
82
|
import { applySlash, closeSlash, slashItems, slashOptionId, slashPlugin, slashQuery, } from './rich-text-slash.js';
|
|
83
83
|
import { RichTextSlashMenu } from './rich-text-slash-menu.js';
|
|
84
84
|
import { RichTextToolbar } from './rich-text-toolbar.js';
|
|
85
|
+
import { RichTextToolsBar } from './rich-text-tools-bar.js';
|
|
85
86
|
import { CalloutKindMenu, TableBar } from './rich-text-widgets.js';
|
|
86
87
|
// The commonmark preset, minus the plugin that writes `<br />`. A $remark
|
|
87
88
|
// plugin is a pair (its options and the plugin), spread into `plugins`.
|
|
@@ -311,6 +312,7 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
|
|
|
311
312
|
const [dismissed, setDismissed] = useState(null);
|
|
312
313
|
const toolbarRef = useRef(null);
|
|
313
314
|
const floatingRef = useRef(null);
|
|
315
|
+
const toolsRef = useRef(null);
|
|
314
316
|
// The field's element, which the floating toolbar is placed against.
|
|
315
317
|
const [field, setField] = useState(null);
|
|
316
318
|
// Whether the floating toolbar shows, for the editor's keys (set up once).
|
|
@@ -332,7 +334,7 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
|
|
|
332
334
|
});
|
|
333
335
|
// Whether focus is on the floating toolbar, or in a menu opened from it
|
|
334
336
|
// (the Highlight menu's popup is in a portal).
|
|
335
|
-
const inFloating = (node) => Boolean(floatingRef.current?.contains(node) || (node instanceof Element && node.closest('[data-rich-text-menu]')));
|
|
337
|
+
const inFloating = (node) => Boolean(floatingRef.current?.contains(node) || toolsRef.current?.contains(node) || (node instanceof Element && node.closest('[data-rich-text-menu]')));
|
|
336
338
|
// Full screen: the field fills the window, in place (the editor stays
|
|
337
339
|
// mounted, so nothing is lost).
|
|
338
340
|
const [fullScreen, setFullScreen] = useState(false);
|
|
@@ -580,7 +582,7 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
|
|
|
580
582
|
// Alt+F10: to the floating toolbar when it shows, else the
|
|
581
583
|
// fixed one (the convention editors share).
|
|
582
584
|
if (event.altKey && event.key === 'F10') {
|
|
583
|
-
const bar = floatingOpenRef.current ? floatingRef.current : toolbarRef.current;
|
|
585
|
+
const bar = floatingOpenRef.current ? floatingRef.current : (toolbarRef.current ?? toolsRef.current);
|
|
584
586
|
const button = bar?.querySelector('button[tabindex="0"]');
|
|
585
587
|
if (!button)
|
|
586
588
|
return false;
|
|
@@ -842,6 +844,8 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
|
|
|
842
844
|
floatingOpenRef.current = floatingOpen;
|
|
843
845
|
});
|
|
844
846
|
const returnToText = () => editorView?.focus();
|
|
847
|
+
// The tools strip of a field with only the floating toolbar.
|
|
848
|
+
const toolsOpen = Boolean(toolbar === 'floating' && editorView && (focused || fullScreen));
|
|
845
849
|
const toggleFullScreen = () => {
|
|
846
850
|
setFullScreen((on) => !on);
|
|
847
851
|
editorView?.focus();
|
|
@@ -913,7 +917,11 @@ export default function MilkdownEditor({ id, defaultValue, onChange, onProblem,
|
|
|
913
917
|
createPortal(_jsx(TableBar, { side: "row", active: active, readOnly: readOnly, onAdd: () => addToTable(widget.getPos, 'row') }), widget.slot, `${widget.key}-row`),
|
|
914
918
|
createPortal(_jsx(TableBar, { side: "column", active: active, readOnly: readOnly, onAdd: () => addToTable(widget.getPos, 'column') }), widget.column, `${widget.key}-column`),
|
|
915
919
|
];
|
|
916
|
-
}), slashOpen && slash && editorView && (_jsx(RichTextSlashMenu, { id: slashListId, view: editorView, at: slash.to, field: field, items: slashMatches, active: slashIndex, onHighlight: (index) => setSlashActive({ key: slashMenuKey, index }), onPick: (format) => applySlash(editorView, slash, format) })),
|
|
920
|
+
}), slashOpen && slash && editorView && (_jsx(RichTextSlashMenu, { id: slashListId, view: editorView, at: slash.to, field: field, items: slashMatches, active: slashIndex, onHighlight: (index) => setSlashActive({ key: slashMenuKey, index }), onPick: (format) => applySlash(editorView, slash, format) })), toolsOpen && (_jsx(RichTextToolsBar, { ref: toolsRef, state: editorState, readOnly: readOnly, fullScreen: fullScreen, controls: editableId, onFormat: runFormat, onFullScreen: toggleFullScreen, onReturn: returnToText, onBlur: (event) => {
|
|
921
|
+
const next = event.relatedTarget;
|
|
922
|
+
if (editorView && !editorView.dom.contains(next) && !inFloating(next))
|
|
923
|
+
setFocused(false);
|
|
924
|
+
} })), floatingOpen && editorView && editorState && (_jsx(RichTextFloatingToolbar, { ref: floatingRef, view: editorView, state: editorState, field: field, controls: editableId, onFormat: runFormat, onDismiss: () => setDismissed({ from: editorState.selection.from, to: editorState.selection.to }), onReturn: returnToText, onBlur: (event) => {
|
|
917
925
|
const next = event.relatedTarget;
|
|
918
926
|
if (!editorView.dom.contains(next) && !inFloating(next))
|
|
919
927
|
setFocused(false);
|
|
@@ -30,6 +30,8 @@ import { RichTextTableMenu } from './rich-text-table-menu.js';
|
|
|
30
30
|
const SELECTION = ['bold', 'italic', 'strike', 'code', 'highlight', 'link'];
|
|
31
31
|
const SETS = {
|
|
32
32
|
all: FORMAT_GROUPS,
|
|
33
|
+
// Outdent and Indent, for a field with no bar above the text (rich-text-tools-bar.tsx).
|
|
34
|
+
tools: [[FORMAT_ITEMS.get('outdent'), FORMAT_ITEMS.get('indent')]],
|
|
33
35
|
selection: [SELECTION.slice(0, 5), SELECTION.slice(5)].map((ids) => ids.map((id) => FORMAT_ITEMS.get(id))),
|
|
34
36
|
};
|
|
35
37
|
// A touch screen or a narrow one: where the keyboard has no Tab, so the
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FocusEvent, Ref } from 'react';
|
|
2
|
+
import type { EditorState } from '@milkdown/kit/prose/state';
|
|
3
|
+
import type { FormatId } from './rich-text-formats.js';
|
|
4
|
+
export interface RichTextToolsBarProps {
|
|
5
|
+
state: EditorState | null;
|
|
6
|
+
readOnly: boolean;
|
|
7
|
+
fullScreen: boolean;
|
|
8
|
+
/** The editable element's id, for `aria-controls`. */
|
|
9
|
+
controls: string;
|
|
10
|
+
onFormat: (id: FormatId) => void;
|
|
11
|
+
onFullScreen: () => void;
|
|
12
|
+
/** Esc: focus back to the text. */
|
|
13
|
+
onReturn: () => void;
|
|
14
|
+
/** Focus leaving one of its buttons, for the editor to tell whether it left the field. */
|
|
15
|
+
onBlur: (event: FocusEvent) => void;
|
|
16
|
+
ref?: Ref<HTMLDivElement>;
|
|
17
|
+
}
|
|
18
|
+
export declare function RichTextToolsBar({ state, readOnly, fullScreen, controls, onFormat, onFullScreen, onReturn, onBlur, ref }: RichTextToolsBarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from 'cn';
|
|
3
|
+
import { FormatButtons } from './rich-text-toolbar.js';
|
|
4
|
+
export function RichTextToolsBar({ state, readOnly, fullScreen, controls, onFormat, onFullScreen, onReturn, onBlur, ref }) {
|
|
5
|
+
const onKeyDown = (event) => {
|
|
6
|
+
// Full screen's Esc is the field's (milkdown-editor.tsx); otherwise it
|
|
7
|
+
// gives focus back to the text.
|
|
8
|
+
if (event.key === 'Escape' && !fullScreen) {
|
|
9
|
+
event.preventDefault();
|
|
10
|
+
event.stopPropagation();
|
|
11
|
+
onReturn();
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
return (_jsx("div", { ref: ref, role: "toolbar", "aria-label": "Editor tools", "aria-controls": controls, "data-slot": "rich-text-tools-bar", className: cn('absolute right-0 z-40 flex items-center gap-0.5 rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10',
|
|
15
|
+
// Above the field, or inside its corner once it fills the window.
|
|
16
|
+
fullScreen ? 'top-2 right-2' : 'bottom-full mb-1'), onKeyDown: onKeyDown, onBlur: onBlur, children: _jsx(FormatButtons, { formats: "tools", state: state, readOnly: readOnly, onFormat: onFormat, onFullScreen: onFullScreen, fullScreen: fullScreen }) }));
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tristan2828/ui-foundation",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.50.1",
|
|
4
4
|
"description": "Shared UI foundation for contract-first, database-backed React apps: shadcn primitives, DataTable/EntityForm composites, auth, the gateway error seam, design tokens, lint rules and the AI playbook.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|