@cosmicdrift/kumiko-renderer 0.392.0 → 0.392.2

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.
@@ -2,5 +2,6 @@ import type { ReactNode } from "react";
2
2
  import type { DataTableModel } from "./use-data-table-state.ts";
3
3
  export declare function buildModelFacetCluster(m: DataTableModel): ReactNode;
4
4
  export declare function hasToolbarContent(m: DataTableModel, facetCluster: ReactNode): boolean;
5
+ export declare function toolbarMergesDescription(m: DataTableModel, facetCluster: ReactNode): boolean;
5
6
  export declare function renderToolbar(m: DataTableModel, facetCluster: ReactNode): ReactNode;
6
- export declare function renderToolbarDescription(m: DataTableModel, hasToolbar: boolean): ReactNode;
7
+ export declare function renderToolbarDescription(m: DataTableModel, hasToolbar: boolean, inToolbarRow?: boolean): ReactNode;
@@ -40,14 +40,27 @@ function renderFacetToggle(m, facetCluster) {
40
40
  const { testId, facetsOpenNarrow, setFacetsOpenNarrow } = m;
41
41
  return (_jsxs(_Fragment, { children: [_jsx(UiButton, { type: "button", variant: "outline", size: "icon", className: "size-11", "aria-label": m.tableTranslate?.("kumiko.list.filter.toggle") ?? "Filter", "aria-expanded": facetsOpenNarrow, "data-testid": testId !== undefined ? `${testId}-filter-toggle` : "filter-toggle", onClick: () => setFacetsOpenNarrow((open) => !open), children: _jsx(SlidersHorizontal, { className: "size-4", "aria-hidden": "true" }) }), facetsOpenNarrow && _jsx("div", { className: "w-full", children: facetCluster })] }));
42
42
  }
43
+ // With only end content (no search, no facets) the row has room for the description,
44
+ // so both share one line instead of stacking the description under the actions.
45
+ export function toolbarMergesDescription(m, facetCluster) {
46
+ return (m.toolbarDescription !== undefined &&
47
+ m.toolbarStart === undefined &&
48
+ facetCluster === undefined &&
49
+ toolbarEndHasContent(m.view, m.exportList, m.toolbarEnd));
50
+ }
43
51
  export function renderToolbar(m, facetCluster) {
44
52
  const { testId, toolbarStart } = m;
45
53
  const collapsesFacetsBehindButton = m.isNarrow && m.fillsHeight && facetCluster !== undefined;
46
- return (_jsxs("div", { "data-testid": testId !== undefined ? `${testId}-toolbar` : "render-list-toolbar", className: toolbarClassName(m), children: [toolbarStart !== undefined && renderToolbarStart(m, toolbarStart), collapsesFacetsBehindButton ? renderFacetToggle(m, facetCluster) : facetCluster, _jsx(ToolbarEndCluster, { view: m.view, columnLabels: new Map(m.schemaColumns.map((col) => [col.field, col.label])), exportList: m.exportList, toolbarEnd: m.toolbarEnd })] }));
54
+ const mergesDescription = toolbarMergesDescription(m, facetCluster);
55
+ return (_jsxs("div", { "data-testid": testId !== undefined ? `${testId}-toolbar` : "render-list-toolbar", className: cn(toolbarClassName(m), mergesDescription && "justify-between", mergesDescription && m.fillsHeight && !m.isNarrow && "md:py-2",
56
+ // A dashboard panel title above has no inset; the description must share its left edge.
57
+ mergesDescription && m.fillsHeight && m.insideEmbeddedScreen && "px-0"), children: [mergesDescription && renderToolbarDescription(m, true, true), toolbarStart !== undefined && renderToolbarStart(m, toolbarStart), collapsesFacetsBehindButton ? renderFacetToggle(m, facetCluster) : facetCluster, _jsx(ToolbarEndCluster, { view: m.view, columnLabels: new Map(m.schemaColumns.map((col) => [col.field, col.label])), exportList: m.exportList, toolbarEnd: m.toolbarEnd })] }));
47
58
  }
48
- export function renderToolbarDescription(m, hasToolbar) {
59
+ export function renderToolbarDescription(m, hasToolbar, inToolbarRow = false) {
49
60
  const { testId, toolbarDescription, fillsHeight, isNarrow } = m;
50
61
  if (toolbarDescription === undefined)
51
62
  return null;
52
- return (_jsx("p", { "data-testid": testId !== undefined ? `${testId}-description` : "render-list-description", className: cn("m-0 pb-2 text-sm text-muted-foreground", fillsHeight && (isNarrow ? "px-3" : "px-6"), fillsHeight && !hasToolbar && "pt-4"), children: toolbarDescription }));
63
+ return (_jsx("p", { "data-testid": testId !== undefined ? `${testId}-description` : "render-list-description", className: cn("m-0 text-sm text-muted-foreground",
64
+ // min-w-48 lets the actions wrap below the text instead of squeezing it.
65
+ inToolbarRow ? "min-w-48 flex-1" : "pb-2", !inToolbarRow && fillsHeight && (isNarrow ? "px-3" : "px-6"), !inToolbarRow && fillsHeight && !hasToolbar && "pt-4"), children: toolbarDescription }));
53
66
  }
@@ -30,6 +30,7 @@ export type DataTableModel = Omit<DataTableProps, "columns"> & {
30
30
  readonly rowTestId: (row: ListRowViewModel) => string;
31
31
  readonly expansion: RowExpansion | undefined;
32
32
  readonly fillsHeight: boolean;
33
+ readonly insideEmbeddedScreen: boolean;
33
34
  readonly selection: ReturnType<typeof useBulkSelection>;
34
35
  readonly headerRowRef: ReturnType<typeof useMeasuredColumnWidths>["headerRowRef"];
35
36
  readonly pins: ColumnPins<ListColumnViewModel>;
@@ -1,4 +1,5 @@
1
1
  import { useId, useState } from "react";
2
+ import { useIsEmbeddedScreen } from "../../../context/embedded-screen-context.js";
2
3
  import { useOptionalLocale, useOptionalTranslation } from "../../../index.js";
3
4
  import { useIsNarrowViewport } from "../use-narrow-viewport.js";
4
5
  import { isPinningActive, isResizingActive } from "./column-layout-mode.js";
@@ -55,6 +56,7 @@ export function useDataTableState(props) {
55
56
  const expansion = resolveRowExpansion(props);
56
57
  const isRowExpandable = expansion !== undefined;
57
58
  const fillsHeight = scrollBody === true;
59
+ const insideEmbeddedScreen = useIsEmbeddedScreen();
58
60
  const selection = useBulkSelection({
59
61
  bulk,
60
62
  rows,
@@ -110,6 +112,7 @@ export function useDataTableState(props) {
110
112
  rowTestId,
111
113
  expansion,
112
114
  fillsHeight,
115
+ insideEmbeddedScreen,
113
116
  selection,
114
117
  headerRowRef,
115
118
  pins,
@@ -1,7 +1,7 @@
1
1
  import { jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { cn } from "../lib/cn.js";
3
3
  import { renderTableContent } from "./data-table/table-content.js";
4
- import { buildModelFacetCluster, hasToolbarContent, renderToolbar, renderToolbarDescription, } from "./data-table/toolbar.js";
4
+ import { buildModelFacetCluster, hasToolbarContent, renderToolbar, renderToolbarDescription, toolbarMergesDescription, } from "./data-table/toolbar.js";
5
5
  import { useDataTableState } from "./data-table/use-data-table-state.js";
6
6
  import { screenPaddingClassName } from "./layout.js";
7
7
  // `screenPadding`: on a list screen this wrapper IS the screen container,
@@ -28,10 +28,11 @@ export function DefaultDataTable(props) {
28
28
  const model = useDataTableState(props);
29
29
  const facetCluster = buildModelFacetCluster(model);
30
30
  const hasToolbar = hasToolbarContent(model, facetCluster);
31
+ const mergesDescription = toolbarMergesDescription(model, facetCluster);
31
32
  // dashboard-01 pattern: the toolbar (search + facets + "+ New") sits ABOVE
32
33
  // the table in the same padding block; no separate bg bar, no screen title
33
34
  // (that lives in the shell's breadcrumb). The toolbar is NOT sticky: lists
34
35
  // typically scroll with the page container, not internally. Sticky would
35
36
  // compete with the topbar.
36
- return (_jsxs("div", { className: wrapperClassName(model), children: [!model.fillsHeight && renderToolbarDescription(model, hasToolbar), hasToolbar && renderToolbar(model, facetCluster), model.fillsHeight && renderToolbarDescription(model, hasToolbar), renderTableContent(model)] }));
37
+ return (_jsxs("div", { className: wrapperClassName(model), children: [!mergesDescription && !model.fillsHeight && renderToolbarDescription(model, hasToolbar), hasToolbar && renderToolbar(model, facetCluster), !mergesDescription && model.fillsHeight && renderToolbarDescription(model, hasToolbar), renderTableContent(model)] }));
37
38
  }
@@ -21,8 +21,8 @@ export function DefaultSection({ id, title, subtitle, children, actions, variant
21
21
  // footer — a title-less section still draws the row when actions are
22
22
  // present, so a hideTitle tabs-Section with `actions` isn't stranded.
23
23
  const TitleTag = insideScreenForm ? "h2" : "h3";
24
- const titleBlock = title !== undefined || subtitle !== undefined ? (_jsxs("div", { className: "flex flex-col gap-1", children: [title !== undefined && (_jsxs(TitleTag, { "data-testid": testId !== undefined ? `${testId}-title` : undefined, className: "flex items-center gap-2 text-base font-semibold leading-none tracking-tight", children: [icon !== undefined && _jsx(Icon, { name: icon, className: "size-4 text-muted-foreground" }), title] })), subtitle !== undefined && (_jsx("div", { "data-testid": testId !== undefined ? `${testId}-subtitle` : undefined, className: cn("text-sm text-muted-foreground", insideScreenForm && "text-[13px] text-foreground-secondary"), children: subtitle }))] })) : null;
25
- const header = titleBlock !== null || actions !== undefined ? (_jsxs("div", { className: "flex items-start justify-between gap-4", children: [titleBlock, actions !== undefined && (_jsx("div", { "data-testid": testId !== undefined ? `${testId}-actions` : undefined, className: "ml-auto flex shrink-0 items-center gap-2", children: actions }))] })) : null;
24
+ const titleBlock = title !== undefined || subtitle !== undefined ? (_jsxs("div", { className: "flex min-w-48 flex-1 flex-col gap-1", children: [title !== undefined && (_jsxs(TitleTag, { "data-testid": testId !== undefined ? `${testId}-title` : undefined, className: "flex items-center gap-2 text-base font-semibold leading-none tracking-tight", children: [icon !== undefined && _jsx(Icon, { name: icon, className: "size-4 text-muted-foreground" }), title] })), subtitle !== undefined && (_jsx("div", { "data-testid": testId !== undefined ? `${testId}-subtitle` : undefined, className: cn("text-sm text-muted-foreground", insideScreenForm && "text-[13px] text-foreground-secondary"), children: subtitle }))] })) : null;
25
+ const header = titleBlock !== null || actions !== undefined ? (_jsxs("div", { className: "flex flex-wrap items-start justify-between gap-x-4 gap-y-3", children: [titleBlock, actions !== undefined && (_jsx("div", { "data-testid": testId !== undefined ? `${testId}-actions` : undefined, className: "ml-auto flex shrink-0 items-center gap-2", children: actions }))] })) : null;
26
26
  // Inside a form: divider-less section WITHOUT a card surface of its own.
27
27
  // The separator lines BETWEEN sections come from the divide-y wrapper in
28
28
  // the form.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.392.0",
3
+ "version": "0.392.2",
4
4
  "description": "React renderer for Kumiko screens. The root export holds the platform-agnostic logic (primitives contract, hooks, KumikoScreen, navigation and SSE abstractions); ./headless the DOM-free UI logic, ./live the HTTP dispatcher, ./web the browser bindings (HTML primitives, history navigation, EventSource live events, createKumikoApp).",
5
5
  "license": "MIT",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -48,8 +48,8 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@cosmicdrift/kumiko-framework": "0.392.0",
52
- "@cosmicdrift/kumiko-types": "0.392.0",
51
+ "@cosmicdrift/kumiko-framework": "0.392.2",
52
+ "@cosmicdrift/kumiko-types": "0.392.2",
53
53
  "@radix-ui/react-dialog": "^1.1.15",
54
54
  "@radix-ui/react-dropdown-menu": "^2.1.16",
55
55
  "@radix-ui/react-label": "^2.1.8",
@@ -92,8 +92,8 @@
92
92
  }
93
93
  },
94
94
  "devDependencies": {
95
- "@cosmicdrift/kumiko-locale-de": "0.392.0",
96
- "@cosmicdrift/kumiko-testing": "0.392.0",
95
+ "@cosmicdrift/kumiko-locale-de": "0.392.2",
96
+ "@cosmicdrift/kumiko-testing": "0.392.2",
97
97
  "@tailwindcss/cli": "^4.3.0",
98
98
  "@testing-library/react": "^16.3.2",
99
99
  "@testing-library/user-event": "^14.6.1",
package/src/changes.json CHANGED
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.392.1",
4
+ "type": "fix",
5
+ "title": "List descriptions sit beside the toolbar actions",
6
+ "detail": "A description plus a toolbar with only end content (no search, no facets) renders as one row with the actions on the right, in embedded and full-height lists. Before, a full-height list put the description under the create button. With search or facets the layout is unchanged. Section header actions wrap below the title on narrow widths."
7
+ },
2
8
  {
3
9
  "version": "0.392.0",
4
10
  "type": "improvement",