@cosmicdrift/kumiko-renderer-web 0.349.0 → 0.350.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.
- package/dist/app/nav.js +19 -10
- package/dist/primitives/index.js +17 -9
- package/dist/primitives/lightbox.js +2 -2
- package/package.json +7 -7
- package/src/changes.json +6 -0
package/dist/app/nav.js
CHANGED
|
@@ -121,25 +121,34 @@ function applySearchParamUpdates(updates) {
|
|
|
121
121
|
for (const l of listeners)
|
|
122
122
|
l();
|
|
123
123
|
}
|
|
124
|
-
|
|
124
|
+
// undefined = the caller passed no search params; "" = an explicitly empty query.
|
|
125
|
+
function formatSearch(options) {
|
|
126
|
+
if (options?.searchParams === undefined)
|
|
127
|
+
return undefined;
|
|
128
|
+
const query = new URLSearchParams(options.searchParams).toString();
|
|
129
|
+
return query === "" ? "" : `?${query}`;
|
|
130
|
+
}
|
|
131
|
+
function pushPath(path, search) {
|
|
125
132
|
if (typeof window === "undefined")
|
|
126
133
|
return;
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
134
|
+
// Repeated navigate() calls to the same URL must not flood the history.
|
|
135
|
+
// Without explicit search params the current query counts as unchanged.
|
|
136
|
+
const unchanged = window.location.pathname === path &&
|
|
137
|
+
(search === undefined || window.location.search === search);
|
|
138
|
+
if (unchanged)
|
|
130
139
|
return;
|
|
131
|
-
window.history.pushState(null, "", path);
|
|
140
|
+
window.history.pushState(null, "", `${path}${search ?? ""}`);
|
|
132
141
|
for (const l of listeners)
|
|
133
142
|
l();
|
|
134
143
|
}
|
|
135
|
-
function replacePath(path) {
|
|
144
|
+
function replacePath(path, search) {
|
|
136
145
|
if (typeof window === "undefined")
|
|
137
146
|
return;
|
|
138
147
|
// No path-change short-circuit here: callers explicitly chose replace
|
|
139
148
|
// to avoid creating a history entry, even when the URL is identical.
|
|
140
149
|
// (pushPath skips no-op pushes; replacePath honors the call so the
|
|
141
150
|
// entry-stack semantics stay predictable.)
|
|
142
|
-
window.history.replaceState(null, "", path);
|
|
151
|
+
window.history.replaceState(null, "", `${path}${search ?? ""}`);
|
|
143
152
|
for (const l of listeners)
|
|
144
153
|
l();
|
|
145
154
|
}
|
|
@@ -197,9 +206,9 @@ export function useBrowserNavApi(options) {
|
|
|
197
206
|
const toPath = (target) => prependBasePath(formatPath(inCurrentWorkspace(resolveTarget(features, target))), basePath);
|
|
198
207
|
return {
|
|
199
208
|
route,
|
|
200
|
-
navigate: (target) => pushPath(toPath(target)),
|
|
201
|
-
replace: (target) => replacePath(toPath(target)),
|
|
202
|
-
hrefFor: (target) => toPath(target)
|
|
209
|
+
navigate: (target, navigateOptions) => pushPath(toPath(target), formatSearch(navigateOptions)),
|
|
210
|
+
replace: (target, navigateOptions) => replacePath(toPath(target), formatSearch(navigateOptions)),
|
|
211
|
+
hrefFor: (target, navigateOptions) => `${toPath(target)}${formatSearch(navigateOptions) ?? ""}`,
|
|
203
212
|
searchParams,
|
|
204
213
|
setSearchParams: applySearchParamUpdates,
|
|
205
214
|
};
|
package/dist/primitives/index.js
CHANGED
|
@@ -504,7 +504,7 @@ const EXPAND_TOGGLE_CELL_CLASS = "w-10 px-1! py-0!";
|
|
|
504
504
|
const EXPANSION_CELL_CLASS = "whitespace-normal bg-muted p-0! align-top md:[&_td:last-child]:bg-muted";
|
|
505
505
|
const EXPANSION_CONTENT_CLASS = "pt-1 pr-4 pb-3.5 pl-[52px]";
|
|
506
506
|
function RowExpandToggle({ expanded, controlsId, label, onToggle, testId, }) {
|
|
507
|
-
return (_jsx(UiButton, { type: "button", variant: "ghost", size: "icon", className: "size-8 shrink-0", "aria-expanded": expanded, ...(expanded && { "aria-controls": controlsId }), "aria-label": label, "data-testid": testId, onClick: (e) => {
|
|
507
|
+
return (_jsx(UiButton, { type: "button", variant: "ghost", size: "icon", className: "size-8 shrink-0 max-md:size-11", "aria-expanded": expanded, ...(expanded && { "aria-controls": controlsId }), "aria-label": label, "data-testid": testId, onClick: (e) => {
|
|
508
508
|
e.stopPropagation();
|
|
509
509
|
onToggle();
|
|
510
510
|
}, onKeyDown: (e) => e.stopPropagation(), children: _jsx(ChevronRight, { className: cn("size-4 transition-transform", expanded && "rotate-90"), "aria-hidden": "true" }) }));
|
|
@@ -670,7 +670,7 @@ function DefaultDataTable({ columns, rows, onRowClick, sort, onSortChange, empty
|
|
|
670
670
|
e.preventDefault();
|
|
671
671
|
e.stopPropagation();
|
|
672
672
|
onRowClick(row);
|
|
673
|
-
}, children: body })) : (body), hasMenu && (_jsx("div", { onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), children: _jsx(RowActionsCell, { row: row, actions: menuActions, mode: rowActionMode, rowIsLink: rowIsLink }) }))] }, row.id));
|
|
673
|
+
}, children: body })) : (body), hasMenu && (_jsx("div", { onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), children: _jsx(RowActionsCell, { row: row, actions: menuActions, mode: rowActionMode, rowIsLink: rowIsLink, compact: true }) }))] }, row.id));
|
|
674
674
|
if (!isRowExpandable)
|
|
675
675
|
return rowItem;
|
|
676
676
|
// The expansion is a sibling <li>, outside the row's click target, so
|
|
@@ -764,7 +764,7 @@ function primaryRowAction(actions) {
|
|
|
764
764
|
// The isVisible filter runs here; an action hidden for a given row never
|
|
765
765
|
// reaches render. If every action is hidden, the cell stays empty (no
|
|
766
766
|
// phantom column).
|
|
767
|
-
function RowActionsCell({ row, actions, mode = "adaptive", rowIsLink = false, }) {
|
|
767
|
+
function RowActionsCell({ row, actions, mode = "adaptive", rowIsLink = false, compact = false, }) {
|
|
768
768
|
const visible = actions.filter((a) => a.isVisible === undefined || a.isVisible(row));
|
|
769
769
|
if (visible.length === 0)
|
|
770
770
|
return null;
|
|
@@ -774,12 +774,12 @@ function RowActionsCell({ row, actions, mode = "adaptive", rowIsLink = false, })
|
|
|
774
774
|
// otherwise "inline" is exactly the wall-to-wall text-button problem
|
|
775
775
|
// this feature exists to fix. An author-set `display` takes that choice over.
|
|
776
776
|
const iconOnly = !hasExplicitDisplay && shouldRenderActionsIconOnly(visible);
|
|
777
|
-
return (_jsx("div", { className: "flex w-full items-center gap-1 justify-start", children: visible.map((a) => (_jsx(RowActionButton, { row: row, action: a, iconOnly: iconOnly }, a.id))) }));
|
|
777
|
+
return (_jsx("div", { className: "flex w-full items-center gap-1 justify-start", children: visible.map((a) => (_jsx(RowActionButton, { row: row, action: a, iconOnly: iconOnly, compact: compact }, a.id))) }));
|
|
778
778
|
}
|
|
779
779
|
if (hasExplicitDisplay) {
|
|
780
780
|
const inline = visible.filter((a) => a.display !== undefined);
|
|
781
781
|
const rest = visible.filter((a) => a.display === undefined);
|
|
782
|
-
return (_jsxs("div", { className: "inline-flex items-center gap-1 justify-end", children: [inline.map((a) => (_jsx(RowActionButton, { row: row, action: a }, a.id))), rest.length > 0 && _jsx(RowActionsKebab, { row: row, actions: rest })] }));
|
|
782
|
+
return (_jsxs("div", { className: "inline-flex items-center gap-1 justify-end", children: [inline.map((a) => (_jsx(RowActionButton, { row: row, action: a, compact: compact }, a.id))), rest.length > 0 && _jsx(RowActionsKebab, { row: row, actions: rest })] }));
|
|
783
783
|
}
|
|
784
784
|
if (rowIsLink) {
|
|
785
785
|
return (_jsx("div", { className: "inline-flex items-center justify-end", children: _jsx(RowActionsKebab, { row: row, actions: visible }) }));
|
|
@@ -890,12 +890,20 @@ function useRowActionTrigger(row) {
|
|
|
890
890
|
};
|
|
891
891
|
return { busy, triggerNow };
|
|
892
892
|
}
|
|
893
|
-
|
|
893
|
+
// An icon-only form without a resolvable icon would render an empty button.
|
|
894
|
+
// `compact` is the narrow card layout, where "responsive" collapses to its icon.
|
|
895
|
+
function effectiveRowActionDisplay(display, hasIcon, compact) {
|
|
896
|
+
if (display === "responsive")
|
|
897
|
+
return hasIcon && compact ? "icon" : "button";
|
|
898
|
+
if (display === "icon" && !hasIcon)
|
|
899
|
+
return "link";
|
|
900
|
+
return display;
|
|
901
|
+
}
|
|
902
|
+
function RowActionButton({ row, action, iconOnly = false, asLink = false, compact = false, }) {
|
|
894
903
|
const { busy, triggerNow } = useRowActionTrigger(row);
|
|
895
904
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
896
905
|
const resolvedIcon = actionIconFor(action.icon);
|
|
897
|
-
|
|
898
|
-
const display = action.display === "icon" && resolvedIcon === undefined ? "link" : action.display;
|
|
906
|
+
const display = effectiveRowActionDisplay(action.display, resolvedIcon !== undefined, compact);
|
|
899
907
|
const isBorderedButton = display === "button";
|
|
900
908
|
const showIconOnly = display === "icon" || (display === undefined && iconOnly && resolvedIcon !== undefined);
|
|
901
909
|
const linkLike = display === "link" || (display === undefined && asLink);
|
|
@@ -924,7 +932,7 @@ function RowActionButton({ row, action, iconOnly = false, asLink = false, }) {
|
|
|
924
932
|
? "h-8 rounded-md px-3 font-medium max-md:h-11"
|
|
925
933
|
: linkLike
|
|
926
934
|
? "h-7 rounded-md px-2.5 max-md:h-11"
|
|
927
|
-
: "h-8 rounded-sm", showIconOnly ? "w-8" : !linkLike && !isBorderedButton && "px-2", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", "disabled:opacity-50 disabled:pointer-events-none", variantClass), children: busy ? (_jsx(Loader2, { className: "size-3.5 animate-spin", "aria-hidden": "true" })) : resolvedIcon === undefined || (linkLike && !showIconOnly) ? (action.label) : showIconOnly ? (_jsx(Icon, { name: resolvedIcon, className: "size-4" })) : (_jsxs(_Fragment, { children: [_jsx(Icon, { name: resolvedIcon, className: "size-4" }), action.label] })) }), _jsx(DefaultDialog, { open: confirmOpen, onOpenChange: setConfirmOpen, title: action.label, ...(action.confirm !== undefined && { description: action.confirm }), confirmLabel: action.confirmLabel ?? action.label, ...(action.style === "danger" && { variant: "danger" }), onConfirm: () => triggerNow(action), testId: `row-${row.id}-action-${action.id}-dialog` })] }));
|
|
935
|
+
: "h-8 rounded-sm max-md:h-11", showIconOnly ? "w-8 max-md:w-11" : !linkLike && !isBorderedButton && "px-2", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", "disabled:opacity-50 disabled:pointer-events-none", variantClass), children: busy ? (_jsx(Loader2, { className: "size-3.5 animate-spin", "aria-hidden": "true" })) : resolvedIcon === undefined || (linkLike && !showIconOnly) ? (action.label) : showIconOnly ? (_jsx(Icon, { name: resolvedIcon, className: "size-4" })) : (_jsxs(_Fragment, { children: [_jsx(Icon, { name: resolvedIcon, className: "size-4" }), action.label] })) }), _jsx(DefaultDialog, { open: confirmOpen, onOpenChange: setConfirmOpen, title: action.label, ...(action.confirm !== undefined && { description: action.confirm }), confirmLabel: action.confirmLabel ?? action.label, ...(action.style === "danger" && { variant: "danger" }), onConfirm: () => triggerNow(action), testId: `row-${row.id}-action-${action.id}-dialog` })] }));
|
|
928
936
|
}
|
|
929
937
|
// Kebab dropdown for >2 actions, one inline confirm dialog per item like
|
|
930
938
|
// the inline-button path. Menu is controlled so it closes explicitly on
|
|
@@ -6,7 +6,7 @@ import { cn } from "../lib/cn.js";
|
|
|
6
6
|
import { ModalShell } from "./modal-shell.js";
|
|
7
7
|
const NAV_BUTTON_CLASS = "absolute top-1/2 -translate-y-1/2 flex size-9 items-center justify-center rounded-full border border-border bg-card text-foreground opacity-80 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring";
|
|
8
8
|
export function DefaultLightbox(props) {
|
|
9
|
-
const { open, onOpenChange, testId } = props;
|
|
9
|
+
const { open, onOpenChange, testId, actions, showPosition = true } = props;
|
|
10
10
|
const t = useTranslation();
|
|
11
11
|
const images = "images" in props ? props.images : [{ src: props.src, alt: props.alt }];
|
|
12
12
|
const onIndexChange = "images" in props ? props.onIndexChange : undefined;
|
|
@@ -31,5 +31,5 @@ export function DefaultLightbox(props) {
|
|
|
31
31
|
goTo(index - 1);
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
|
-
return (_jsxs(ModalShell, { open: open, onOpenChange: onOpenChange, testId: testId, closeLabel: t("kumiko.dialog.close"), noAriaDescription: true, ...(canNavigate && { onContentKeyDown: handleKeyDown }), contentClassName: cn("border-0 bg-transparent p-0 shadow-none", "max-w-[95vw] max-h-[90vh] w-auto"), children: [_jsx(DialogPrimitive.Title, { className: "sr-only", children: current.alt }), _jsx("img", { src: current.src, alt: current.alt, className: "block max-h-[85vh] max-w-[90vw] w-auto rounded-lg border border-border bg-card shadow-lg" }), canNavigate && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", "aria-label": t("kumiko.lightbox.previous"), onClick: () => goTo(index - 1), className: cn(NAV_BUTTON_CLASS, "left-2"), children: _jsx(ChevronLeft, { className: "size-5" }) }), _jsx("button", { type: "button", "aria-label": t("kumiko.lightbox.next"), onClick: () => goTo(index + 1), className: cn(NAV_BUTTON_CLASS, "right-2"), children: _jsx(ChevronRight, { className: "size-5" }) }), _jsx("div", { className: "absolute bottom-2 left-1/2 -translate-x-1/2 rounded-full bg-card px-3 py-1 text-xs text-foreground shadow-sm", children: t("kumiko.lightbox.position", { current: index + 1, total }) })] }))] }));
|
|
34
|
+
return (_jsxs(ModalShell, { open: open, onOpenChange: onOpenChange, testId: testId, closeLabel: t("kumiko.dialog.close"), noAriaDescription: true, ...(canNavigate && { onContentKeyDown: handleKeyDown }), contentClassName: cn("border-0 bg-transparent p-0 shadow-none", "max-w-[95vw] max-h-[90vh] w-auto"), children: [_jsx(DialogPrimitive.Title, { className: "sr-only", children: current.alt }), _jsx("img", { src: current.src, alt: current.alt, className: "block max-h-[85vh] max-w-[90vw] w-auto rounded-lg border border-border bg-card shadow-lg" }), canNavigate && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", "aria-label": t("kumiko.lightbox.previous"), onClick: () => goTo(index - 1), className: cn(NAV_BUTTON_CLASS, "left-2"), children: _jsx(ChevronLeft, { className: "size-5" }) }), _jsx("button", { type: "button", "aria-label": t("kumiko.lightbox.next"), onClick: () => goTo(index + 1), className: cn(NAV_BUTTON_CLASS, "right-2"), children: _jsx(ChevronRight, { className: "size-5" }) }), showPosition && (_jsx("div", { className: "absolute bottom-2 left-1/2 -translate-x-1/2 rounded-full bg-card px-3 py-1 text-xs text-foreground shadow-sm", children: t("kumiko.lightbox.position", { current: index + 1, total }) }))] })), actions !== undefined && (_jsx("div", { "data-testid": testId !== undefined ? `${testId}-actions` : undefined, className: "absolute left-2 top-2 flex items-center gap-2", children: actions }))] }));
|
|
35
35
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.350.0",
|
|
4
4
|
"description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
24
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
25
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
26
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
27
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
23
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.350.0",
|
|
24
|
+
"@cosmicdrift/kumiko-framework": "0.350.0",
|
|
25
|
+
"@cosmicdrift/kumiko-headless": "0.350.0",
|
|
26
|
+
"@cosmicdrift/kumiko-renderer": "0.350.0",
|
|
27
|
+
"@cosmicdrift/kumiko-types": "0.350.0",
|
|
28
28
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
29
29
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
30
30
|
"@radix-ui/react-label": "^2.1.8",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"@types/react-dom": "^19.2.3",
|
|
73
73
|
"jsdom": "^29.1.1",
|
|
74
74
|
"tailwindcss": "^4.3.0",
|
|
75
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
75
|
+
"@cosmicdrift/kumiko-locale-de": "0.350.0"
|
|
76
76
|
},
|
|
77
77
|
"repository": {
|
|
78
78
|
"type": "git",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.350.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Row actions with display \"responsive\", Lightbox actions and showPosition, navigate with searchParams, facet counts refresh after writes",
|
|
6
|
+
"detail": "Responsive row actions, Lightbox actions, navigate with search params, fresh facet counts\n`RowActionDisplay` gets `\"responsive\"`: a labelled button with icon in the table (768px and up), icon only in the narrow card layout with the label as accessible name. Without an icon it stays a button. Existing values keep their look. Below 768px, icon-only row actions and the row expand arrow have a 44px touch target.\n`Lightbox` takes `actions` (nodes in the top-left corner, e.g. a download button) and `showPosition` (default `true`; `false` hides the `{current} / {total}` counter).\n`NavApi.navigate`, `replace` and `hrefFor` take an optional `{ searchParams }` (`NavigateOptions`). Without it the query is dropped as before. `listFilterUrlKey(screenId, field)` builds the URL key a list reads a facet filter from.\nFacet chip counts refetch after a write from a row action, toolbar action, drawer or expanded row, and follow the entity's live events. Before, they kept the count from the first load."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.349.0",
|
|
4
10
|
"type": "improvement",
|