@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.
@@ -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–1216
30
- - [A list of sub-items](#a-list-of-sub-items) · lines 1032–1117
31
- - [In a table's rows](#in-a-tables-rows) · lines 1119–1204
32
- - [A table of a few short columns: `width="content"`](#a-table-of-a-few-short-columns-widthcontent) · lines 1206–1216
33
- - [19. Stage circle for an ordered status](#19-stage-circle-for-an-ordered-status) · lines 1218–1284
34
- - [20. Count linking to the related records](#20-count-linking-to-the-related-records) · lines 1286–1350
35
- - [21. Rows grouped under headings, and a pill filter](#21-rows-grouped-under-headings-and-a-pill-filter) · lines 1352–1420
36
- - [22. A highlighted row, and rows that are done](#22-a-highlighted-row-and-rows-that-are-done) · lines 1422–1450
37
- - [23. A Group menu, and a record read beside the list](#23-a-group-menu-and-a-record-read-beside-the-list) · lines 1452–1526
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 new checklist item
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) })), 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) => {
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);
@@ -5,6 +5,7 @@ import { type FormatId } from './rich-text-formats.js';
5
5
  import type { TableAction } from './rich-text-table.js';
6
6
  declare const SETS: {
7
7
  all: FormatItem[][];
8
+ tools: FormatItem[][];
8
9
  selection: FormatItem[][];
9
10
  };
10
11
  export type FormatButtonsProps = {
@@ -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.49.0",
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": {