@cosmicdrift/kumiko-renderer 0.393.0 → 0.395.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/content-preview.js +1 -9
- package/dist/app/entity-list-screen.js +4 -2
- package/dist/app/kumiko-screen.js +4 -36
- package/dist/app/projection-list-body.js +1 -1
- package/dist/app/qn.d.ts +1 -6
- package/dist/app/qn.js +1 -12
- package/dist/app/screen-visibility-gate.d.ts +19 -0
- package/dist/app/screen-visibility-gate.js +60 -0
- package/dist/app/secrets-edit-body.js +5 -4
- package/dist/app/use-embedded-screen.d.ts +2 -0
- package/dist/app/use-embedded-screen.js +1 -1
- package/dist/app/use-gated-toolbar-actions.d.ts +7 -0
- package/dist/app/use-gated-toolbar-actions.js +51 -0
- package/dist/app/use-projection-list-actions.d.ts +1 -0
- package/dist/app/use-projection-list-actions.js +11 -2
- package/dist/app/visible-when.d.ts +5 -1
- package/dist/app/visible-when.js +11 -0
- package/dist/components/form-screen-layout.d.ts +2 -1
- package/dist/components/form-screen-layout.js +2 -2
- package/dist/components/render-edit-banners.js +1 -1
- package/dist/components/render-edit-layout-flags.d.ts +2 -1
- package/dist/components/render-edit-layout-flags.js +3 -2
- package/dist/components/render-edit-logic.d.ts +2 -1
- package/dist/components/render-edit-logic.js +14 -0
- package/dist/data-table-view-types.d.ts +4 -2
- package/dist/hooks/list-column-layout.d.ts +3 -1
- package/dist/hooks/list-column-layout.js +20 -7
- package/dist/hooks/use-list-view-state.d.ts +1 -1
- package/dist/hooks/use-list-view-state.js +3 -2
- package/dist/i18n-defaults.js +2 -0
- package/dist/web/app/client-plugin.d.ts +1 -1
- package/dist/web/app/dashboard-body.js +1 -1
- package/dist/web/app/dashboard-screen-tiles.d.ts +2 -1
- package/dist/web/app/dashboard-screen-tiles.js +9 -7
- package/dist/web/app/dashboard-shared.d.ts +2 -0
- package/dist/web/app/use-dashboard-screen-state.js +6 -1
- package/dist/web/index.d.ts +1 -1
- package/dist/web/index.js +1 -1
- package/dist/web/layout/shell-breadcrumb.d.ts +4 -1
- package/dist/web/layout/shell-breadcrumb.js +18 -3
- package/dist/web/layout/shell-header.js +4 -2
- package/dist/web/lib/upload-file-ref.d.ts +4 -1
- package/dist/web/lib/upload-file-ref.js +15 -3
- package/dist/web/primitives/action-menu.js +2 -2
- package/dist/web/primitives/calendar-popover.js +1 -1
- package/dist/web/primitives/combobox.js +1 -1
- package/dist/web/primitives/data-table/table-data-cell.js +27 -1
- package/dist/web/primitives/data-table/table-header-row.js +1 -1
- package/dist/web/primitives/data-table/table-view.js +2 -2
- package/dist/web/primitives/data-table/use-column-sizing.d.ts +4 -2
- package/dist/web/primitives/data-table/use-column-sizing.js +34 -11
- package/dist/web/primitives/data-table/view-menu.js +1 -1
- package/dist/web/primitives/data-table-cell.js +1 -1
- package/dist/web/primitives/dialog.js +1 -1
- package/dist/web/primitives/dropdown-menu.d.ts +1 -1
- package/dist/web/primitives/dropdown-menu.js +2 -2
- package/dist/web/primitives/file-upload.js +5 -2
- package/dist/web/primitives/files-dropzone.js +8 -2
- package/dist/web/primitives/lightbox.js +1 -1
- package/dist/web/primitives/modal-shell.js +1 -1
- package/dist/web/primitives/modal.js +1 -1
- package/dist/web/primitives/narrow-pinned-footer.js +1 -1
- package/dist/web/primitives/toast.js +2 -2
- package/dist/web/sse/live-events.js +0 -7
- package/dist/web/widgets/floating-panel-geometry.d.ts +32 -0
- package/dist/web/widgets/floating-panel-geometry.js +71 -0
- package/dist/web/widgets/floating-panel.d.ts +2 -6
- package/dist/web/widgets/floating-panel.js +11 -55
- package/dist/web/widgets/infinity-list.js +27 -14
- package/dist/web/widgets/plan-card.d.ts +2 -0
- package/dist/web/widgets/plan-card.js +5 -1
- package/package.json +6 -19
- package/src/changes.json +54 -0
- package/dist/web/app/rich-content-editor.d.ts +0 -3
- package/dist/web/app/rich-content-editor.js +0 -15
- package/dist/web/app/tiptap-editor.d.ts +0 -3
- package/dist/web/app/tiptap-editor.js +0 -120
|
@@ -16,20 +16,33 @@ export function minWidthsOf(schema) {
|
|
|
16
16
|
export function clampWidth(width, min) {
|
|
17
17
|
return Math.min(MAX_COLUMN_WIDTH_PX, Math.max(min, Math.round(width)));
|
|
18
18
|
}
|
|
19
|
+
/** Only columns with a schema `width`: the others are sized by their content until the user resizes. */
|
|
19
20
|
export function defaultWidths(schema, minWidths) {
|
|
20
|
-
return Object.fromEntries(schema.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
return Object.fromEntries(schema.flatMap((column) => column.width === undefined
|
|
22
|
+
? []
|
|
23
|
+
: [
|
|
24
|
+
[
|
|
25
|
+
column.field,
|
|
26
|
+
clampWidth(column.width, minWidths[column.field] ?? DEFAULT_MIN_COLUMN_WIDTH_PX),
|
|
27
|
+
],
|
|
28
|
+
]));
|
|
24
29
|
}
|
|
25
|
-
/** A saved width wins per field;
|
|
30
|
+
/** A saved width wins per field; fields the schema no longer has are dropped. */
|
|
26
31
|
export function mergeSavedWidths(defaults, saved, minWidths) {
|
|
27
|
-
return Object.fromEntries(Object.
|
|
32
|
+
return Object.fromEntries(Object.keys(minWidths).flatMap((field) => {
|
|
28
33
|
const savedWidth = saved[field];
|
|
29
34
|
const min = minWidths[field] ?? DEFAULT_MIN_COLUMN_WIDTH_PX;
|
|
30
|
-
|
|
35
|
+
if (savedWidth !== undefined)
|
|
36
|
+
return [[field, clampWidth(savedWidth, min)]];
|
|
37
|
+
const fallback = defaults[field];
|
|
38
|
+
return fallback === undefined ? [] : [[field, fallback]];
|
|
31
39
|
}));
|
|
32
40
|
}
|
|
41
|
+
export function clampWidths(widths, minWidths) {
|
|
42
|
+
return Object.fromEntries(Object.entries(widths).flatMap(([field, width]) => field in minWidths
|
|
43
|
+
? [[field, clampWidth(width, minWidths[field] ?? DEFAULT_MIN_COLUMN_WIDTH_PX)]]
|
|
44
|
+
: []));
|
|
45
|
+
}
|
|
33
46
|
export function sameRecord(a, b) {
|
|
34
47
|
const keys = Object.keys(a);
|
|
35
48
|
return keys.length === Object.keys(b).length && keys.every((key) => a[key] === b[key]);
|
|
@@ -21,7 +21,7 @@ export type ListViewState = {
|
|
|
21
21
|
/** Undefined unless `table.resizing` is on. */
|
|
22
22
|
readonly widths: DataTableColumnWidths | undefined;
|
|
23
23
|
readonly minWidths: DataTableColumnWidths | undefined;
|
|
24
|
-
readonly setWidth: (field: string, width: number) => void;
|
|
24
|
+
readonly setWidth: (field: string, width: number, others?: DataTableColumnWidths) => void;
|
|
25
25
|
/** Active group field, "" = not grouped. */
|
|
26
26
|
readonly groupBy: string;
|
|
27
27
|
/** Undefined unless `table.grouping.userChoice` is on. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { normalizeListColumn } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
import { useCallback, useMemo, useState } from "react";
|
|
3
|
-
import { clampWidth, DEFAULT_MIN_COLUMN_WIDTH_PX, defaultPins, defaultWidths, mergeSavedWidths, minWidthsOf, sameRecord, } from "./list-column-layout.js";
|
|
3
|
+
import { clampWidth, clampWidths, DEFAULT_MIN_COLUMN_WIDTH_PX, defaultPins, defaultWidths, mergeSavedWidths, minWidthsOf, sameRecord, } from "./list-column-layout.js";
|
|
4
4
|
import { sanitizeStoredListState } from "./list-state-schema.js";
|
|
5
5
|
export function listViewSchemaColumns(columns) {
|
|
6
6
|
return columns.map((column) => {
|
|
@@ -124,9 +124,10 @@ export function useListViewState(args) {
|
|
|
124
124
|
}, [commit, view]);
|
|
125
125
|
const setDensity = useCallback((next) => commit({ ...view, density: next }), [commit, view]);
|
|
126
126
|
const setPins = useCallback((next) => commit({ ...view, pins: next }), [commit, view]);
|
|
127
|
-
const setWidth = useCallback((field, width) => commit({
|
|
127
|
+
const setWidth = useCallback((field, width, others = {}) => commit({
|
|
128
128
|
...view,
|
|
129
129
|
widths: {
|
|
130
|
+
...clampWidths(others, minWidths),
|
|
130
131
|
...view.widths,
|
|
131
132
|
[field]: clampWidth(width, minWidths[field] ?? DEFAULT_MIN_COLUMN_WIDTH_PX),
|
|
132
133
|
},
|
package/dist/i18n-defaults.js
CHANGED
|
@@ -191,6 +191,8 @@ export const kumikoDefaultTranslations = {
|
|
|
191
191
|
"kumiko.dashboard.progress.unlimited": "unlimited",
|
|
192
192
|
"kumiko.dashboard.record.missing": "Open this page from a record to see its dashboard.",
|
|
193
193
|
"kumiko.dashboard.time-range": "Time range",
|
|
194
|
+
"kumiko.screen.forbidden": "You do not have access to this page.",
|
|
195
|
+
"kumiko.screen.notFound": "This page could not be found.",
|
|
194
196
|
"kumiko.screen.unavailable": "This page is not available right now.",
|
|
195
197
|
"kumiko.dashboard.brush-end": "Window end",
|
|
196
198
|
"kumiko.dashboard.brush-start": "Window start",
|
|
@@ -72,7 +72,7 @@ export type ClientFeatureDefinition = {
|
|
|
72
72
|
* `navProviders`; the static map wins on key collision (explicit beats
|
|
73
73
|
* derived). */
|
|
74
74
|
readonly navProvidersFromCollections?: (collections: readonly QualifiedContentCollection[]) => CollectionNavProviders;
|
|
75
|
-
/** Content editor components — map `contentFormat` ("plain" | "
|
|
75
|
+
/** Content editor components — map `contentFormat` ("plain" | "document") →
|
|
76
76
|
* React component. `r.contentCollection()` declares which format an
|
|
77
77
|
* editor edits; the collection's editor UI (template-resolver's
|
|
78
78
|
* TextBlockEditor et al.) looks the component up here and falls back to
|
|
@@ -48,7 +48,7 @@ function PanelBody({ panel, label, screenId, screenParams, translate, }) {
|
|
|
48
48
|
}
|
|
49
49
|
function renderDashboardPanel(panel, featureName, screenId, screenParams, translate) {
|
|
50
50
|
if (panel.kind === "screen") {
|
|
51
|
-
return (_jsx(ScreenPanelTile, { panel: panel, featureName: featureName, translate: translate }, panel.id));
|
|
51
|
+
return (_jsx(ScreenPanelTile, { panel: panel, featureName: featureName, recordId: screenParams.recordId, translate: translate }, panel.id));
|
|
52
52
|
}
|
|
53
53
|
const label = panel.kind === "custom" || panel.label === undefined ? undefined : translate(panel.label);
|
|
54
54
|
return (_jsx(GatedGridCell, { panel: panel, screenParams: screenParams, label: label ?? panel.id, className: panelSpanClassName(panel), children: _jsx(PanelBody, { panel: panel, label: label, screenId: screenId, screenParams: screenParams, translate: translate }) }, panel.id));
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { DashboardScreenPanel } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
import { type ReactNode } from "react";
|
|
3
3
|
import type { Translate } from "../../headless/index.ts";
|
|
4
|
-
export declare function ScreenPanelTile({ panel, featureName, translate, }: {
|
|
4
|
+
export declare function ScreenPanelTile({ panel, featureName, recordId, translate, }: {
|
|
5
5
|
readonly panel: DashboardScreenPanel;
|
|
6
6
|
readonly featureName: string;
|
|
7
|
+
readonly recordId: string | undefined;
|
|
7
8
|
readonly translate: Translate;
|
|
8
9
|
}): ReactNode;
|
|
@@ -6,19 +6,21 @@ import { EmbeddedFormProvider } from "../primitives/index.js";
|
|
|
6
6
|
import { SectionCard } from "../widgets/section-card.js";
|
|
7
7
|
import { PanelError, WIDE_PANEL } from "./dashboard-shared.js";
|
|
8
8
|
import { NO_GATE_PAYLOAD, VisibilityQueryContext, visibilityGate, } from "./dashboard-visibility.js";
|
|
9
|
-
function EmbeddedScreenPanelTile({ panel, target, translate, }) {
|
|
10
|
-
|
|
9
|
+
function EmbeddedScreenPanelTile({ recordId, panel, target, translate, }) {
|
|
10
|
+
// Only a record screen takes the dashboard's bound record; other screens have no id to read.
|
|
11
|
+
const entityId = target.screenType === "projectionDetail" ? recordId : undefined;
|
|
12
|
+
const screenBody = (_jsx(KumikoScreen, { schema: target.schema, qn: target.qn, translate: translate, ...(entityId !== undefined && { entityId }) }));
|
|
11
13
|
const embedded = panel.chromeless === true ? (_jsx(EmbeddedFormProvider, { children: _jsx(EmbeddedScreenProvider, { children: screenBody }) })) : (screenBody);
|
|
12
14
|
return (_jsx("div", { className: cn("min-w-0", WIDE_PANEL), "data-testid": `dashboard-panel-${panel.id}`, children: panel.label !== undefined && panel.chromeless !== true ? (_jsx(SectionCard, { title: translate(panel.label), children: embedded })) : (_jsxs(_Fragment, { children: [panel.label !== undefined && (_jsx("h3", { className: "mb-3 text-sm font-semibold text-foreground", children: translate(panel.label) })), embedded] })) }));
|
|
13
15
|
}
|
|
14
16
|
// Own component so the target lookup (a hook) only runs for panels that actually fall back.
|
|
15
|
-
function FallbackScreenPanelTile({ panel, fallback, featureName, translate, }) {
|
|
17
|
+
function FallbackScreenPanelTile({ panel, fallback, featureName, recordId, translate, }) {
|
|
16
18
|
const target = useEmbeddedScreen(featureName, fallback);
|
|
17
19
|
if (target === undefined)
|
|
18
20
|
return null;
|
|
19
|
-
return _jsx(EmbeddedScreenPanelTile, { panel: panel, target: target, translate: translate });
|
|
21
|
+
return (_jsx(EmbeddedScreenPanelTile, { panel: panel, target: target, recordId: recordId, translate: translate }));
|
|
20
22
|
}
|
|
21
|
-
export function ScreenPanelTile({ panel, featureName, translate, }) {
|
|
23
|
+
export function ScreenPanelTile({ panel, featureName, recordId, translate, }) {
|
|
22
24
|
const visibleWhen = panel.visibleWhen;
|
|
23
25
|
const visibility = useContext(VisibilityQueryContext).get(visibleWhen !== undefined ? visibilityGate(visibleWhen.query, NO_GATE_PAYLOAD).key : "");
|
|
24
26
|
const target = useEmbeddedScreen(featureName, panel.screen);
|
|
@@ -27,7 +29,7 @@ export function ScreenPanelTile({ panel, featureName, translate, }) {
|
|
|
27
29
|
if (verdict === "loading")
|
|
28
30
|
return null;
|
|
29
31
|
if (verdict !== "visible" && panel.fallback !== undefined) {
|
|
30
|
-
return (_jsx(FallbackScreenPanelTile, { panel: panel, fallback: panel.fallback, featureName: featureName, translate: translate }));
|
|
32
|
+
return (_jsx(FallbackScreenPanelTile, { panel: panel, fallback: panel.fallback, featureName: featureName, recordId: recordId, translate: translate }));
|
|
31
33
|
}
|
|
32
34
|
if (target === undefined)
|
|
33
35
|
return null;
|
|
@@ -38,5 +40,5 @@ export function ScreenPanelTile({ panel, featureName, translate, }) {
|
|
|
38
40
|
}
|
|
39
41
|
if (verdict !== "visible")
|
|
40
42
|
return null;
|
|
41
|
-
return _jsx(EmbeddedScreenPanelTile, { panel: panel, target: target, translate: translate });
|
|
43
|
+
return (_jsx(EmbeddedScreenPanelTile, { panel: panel, target: target, recordId: recordId, translate: translate }));
|
|
42
44
|
}
|
|
@@ -11,6 +11,8 @@ export type ScreenParams = {
|
|
|
11
11
|
readonly rangeParams: Readonly<Record<string, unknown>>;
|
|
12
12
|
/** `{ [payloadKey]: recordId }` of a record-bound dashboard, else empty. */
|
|
13
13
|
readonly recordParams: Readonly<Record<string, unknown>>;
|
|
14
|
+
/** Id read from `record.param`; set whenever the dashboard renders its panels. */
|
|
15
|
+
readonly recordId?: string;
|
|
14
16
|
};
|
|
15
17
|
export declare function panelPayload(panel: DashboardPanelQueryOptions, screenParams: ScreenParams): Readonly<Record<string, unknown>>;
|
|
16
18
|
type RefetchQuery = () => Promise<void>;
|
|
@@ -12,7 +12,12 @@ export function useDashboardScreenParams(screen, translate, nav) {
|
|
|
12
12
|
const recordQueryName = recordBinding?.query;
|
|
13
13
|
const { params: filterParams, picker } = useFilterParams(screen, !recordMissing);
|
|
14
14
|
const { params: rangeParams, control: rangeControl } = useTimeRange(screen, translate);
|
|
15
|
-
const screenParams = {
|
|
15
|
+
const screenParams = {
|
|
16
|
+
filterParams,
|
|
17
|
+
rangeParams,
|
|
18
|
+
recordParams,
|
|
19
|
+
...(recordId !== "" && { recordId }),
|
|
20
|
+
};
|
|
16
21
|
const { loadedAtMs, reportLoaded } = useLoadedAt(screenParams);
|
|
17
22
|
return {
|
|
18
23
|
recordBinding,
|
package/dist/web/index.d.ts
CHANGED
|
@@ -12,7 +12,6 @@ export type { KumikoLinkProps } from "./app/nav.tsx";
|
|
|
12
12
|
export { KumikoLink, useBrowserNavApi } from "./app/nav.tsx";
|
|
13
13
|
export { PlainContentEditor } from "./app/plain-content-editor.tsx";
|
|
14
14
|
export { useResolvers } from "./app/resolvers-context.tsx";
|
|
15
|
-
export { RichContentEditor } from "./app/rich-content-editor.tsx";
|
|
16
15
|
export { clearRememberedListState } from "./app/session-list-state-store.ts";
|
|
17
16
|
export type { NavIconKey } from "./icons.tsx";
|
|
18
17
|
export { Icon } from "./icons.tsx";
|
|
@@ -43,6 +42,7 @@ export { SidebarBrand } from "./layout/sidebar-brand.tsx";
|
|
|
43
42
|
export { SidebarPanel } from "./layout/sidebar-panel.tsx";
|
|
44
43
|
export type { SidebarUserProps } from "./layout/sidebar-user.tsx";
|
|
45
44
|
export { SidebarUser } from "./layout/sidebar-user.tsx";
|
|
45
|
+
export { useDispatchTarget } from "./layout/target-resolver-stub.tsx";
|
|
46
46
|
export { parseTargetFromSearchParams } from "./layout/target-url.ts";
|
|
47
47
|
export type { ThemeMenuItemProps } from "./layout/theme-menu-item.tsx";
|
|
48
48
|
export { ThemeMenuItem } from "./layout/theme-menu-item.tsx";
|
package/dist/web/index.js
CHANGED
|
@@ -11,7 +11,6 @@ export { createBrowserDraftStorage } from "./app/draft-storage.js";
|
|
|
11
11
|
export { KumikoLink, useBrowserNavApi } from "./app/nav.js";
|
|
12
12
|
export { PlainContentEditor } from "./app/plain-content-editor.js";
|
|
13
13
|
export { useResolvers } from "./app/resolvers-context.js";
|
|
14
|
-
export { RichContentEditor } from "./app/rich-content-editor.js";
|
|
15
14
|
export { clearRememberedListState } from "./app/session-list-state-store.js";
|
|
16
15
|
export { Icon } from "./icons.js";
|
|
17
16
|
export { AppLayout } from "./layout/app-layout.js";
|
|
@@ -28,6 +27,7 @@ export { Sidebar } from "./layout/sidebar.js";
|
|
|
28
27
|
export { SidebarBrand } from "./layout/sidebar-brand.js";
|
|
29
28
|
export { SidebarPanel } from "./layout/sidebar-panel.js";
|
|
30
29
|
export { SidebarUser } from "./layout/sidebar-user.js";
|
|
30
|
+
export { useDispatchTarget } from "./layout/target-resolver-stub.js";
|
|
31
31
|
export { parseTargetFromSearchParams } from "./layout/target-url.js";
|
|
32
32
|
export { ThemeMenuItem } from "./layout/theme-menu-item.js";
|
|
33
33
|
export { ThemeToggle } from "./layout/theme-toggle.js";
|
|
@@ -5,9 +5,12 @@ export type BreadcrumbCrumb = {
|
|
|
5
5
|
readonly screenId?: string;
|
|
6
6
|
};
|
|
7
7
|
export declare function screenTitleKey(screenShortId: string): string;
|
|
8
|
+
export declare function screenCreateTitleKey(screenShortId: string): string;
|
|
8
9
|
/** The short id of {screenId}'s parent list screen, resolved via the same
|
|
9
10
|
* logic as `resolveDetailBreadcrumb` — `undefined` when nothing resolves
|
|
10
11
|
* (unknown screen, or no explicit/heuristic parent). */
|
|
11
12
|
export declare function resolveParentScreenId(screens: readonly ScreenDefinition[], screenId: string): string | undefined;
|
|
12
|
-
export declare function resolveDetailBreadcrumb(screens: readonly ScreenDefinition[], detailScreenId: string, t: (key: string) => string
|
|
13
|
+
export declare function resolveDetailBreadcrumb(screens: readonly ScreenDefinition[], detailScreenId: string, t: (key: string) => string, options?: {
|
|
14
|
+
readonly creating?: boolean;
|
|
15
|
+
}): readonly BreadcrumbCrumb[] | undefined;
|
|
13
16
|
export declare function activeNavLabel(nodes: readonly NavNode[], screenId: string, t: (key: string) => string): string | undefined;
|
|
@@ -3,6 +3,20 @@ import { lastSegment } from "../../index.js";
|
|
|
3
3
|
export function screenTitleKey(screenShortId) {
|
|
4
4
|
return `screen:${screenShortId}.title`;
|
|
5
5
|
}
|
|
6
|
+
export function screenCreateTitleKey(screenShortId) {
|
|
7
|
+
return `screen:${screenShortId}.createTitle`;
|
|
8
|
+
}
|
|
9
|
+
// An edit screen opened without a record creates one; a feature names that state with
|
|
10
|
+
// `createTitle`, and without it the screen title stays (t returns the key when untranslated).
|
|
11
|
+
function detailTitle(screenShortId, creating, t) {
|
|
12
|
+
if (creating) {
|
|
13
|
+
const createKey = screenCreateTitleKey(screenShortId);
|
|
14
|
+
const createTitle = t(createKey);
|
|
15
|
+
if (createTitle !== createKey)
|
|
16
|
+
return createTitle;
|
|
17
|
+
}
|
|
18
|
+
return t(screenTitleKey(screenShortId));
|
|
19
|
+
}
|
|
6
20
|
// Delegates to resolveNavParentScreen, supplying the feature-qualified-id
|
|
7
21
|
// normalization the client-side schema needs.
|
|
8
22
|
function resolveParentScreen(screens, detail) {
|
|
@@ -18,18 +32,19 @@ export function resolveParentScreenId(screens, screenId) {
|
|
|
18
32
|
const parent = resolveParentScreen(screens, detail);
|
|
19
33
|
return parent !== undefined ? lastSegment(parent.id) : undefined;
|
|
20
34
|
}
|
|
21
|
-
export function resolveDetailBreadcrumb(screens, detailScreenId, t) {
|
|
35
|
+
export function resolveDetailBreadcrumb(screens, detailScreenId, t, options = {}) {
|
|
36
|
+
const creating = options.creating === true;
|
|
22
37
|
const detail = screens.find((s) => lastSegment(s.id) === detailScreenId);
|
|
23
38
|
if (detail === undefined)
|
|
24
39
|
return undefined;
|
|
25
40
|
const list = resolveParentScreen(screens, detail);
|
|
26
41
|
if (list === undefined) {
|
|
27
|
-
return [{ label:
|
|
42
|
+
return [{ label: detailTitle(detailScreenId, creating, t) }];
|
|
28
43
|
}
|
|
29
44
|
const listId = lastSegment(list.id);
|
|
30
45
|
return [
|
|
31
46
|
{ label: t(screenTitleKey(listId)), screenId: listId },
|
|
32
|
-
{ label:
|
|
47
|
+
{ label: detailTitle(detailScreenId, creating, t) },
|
|
33
48
|
];
|
|
34
49
|
}
|
|
35
50
|
export function activeNavLabel(nodes, screenId, t) {
|
|
@@ -53,8 +53,10 @@ export function ShellHeader({ schema, user, headerActions, allowedNavQns, }) {
|
|
|
53
53
|
activeNavLabel(tree, screenId, t);
|
|
54
54
|
if (navLabel !== undefined)
|
|
55
55
|
return [{ label: navLabel }];
|
|
56
|
-
return resolveDetailBreadcrumb(allScreens, screenId, t
|
|
57
|
-
|
|
56
|
+
return resolveDetailBreadcrumb(allScreens, screenId, t, {
|
|
57
|
+
creating: standInScreenId === undefined && entityId === undefined,
|
|
58
|
+
});
|
|
59
|
+
}, [allScreens, screenId, standInScreenId, entityId, t, tree, allowedTree]);
|
|
58
60
|
const setStatusElement = slot?.setStatusElement;
|
|
59
61
|
const setActionsElement = slot?.setActionsElement;
|
|
60
62
|
const titleOverride = slot?.title;
|
|
@@ -3,7 +3,10 @@ export type FileRefTarget = Pick<FileUploadFormDataInput, "entityType" | "entity
|
|
|
3
3
|
/** Carries the server's own `error` message when the upload endpoint sent one. */
|
|
4
4
|
export declare class FileUploadFailedError extends Error {
|
|
5
5
|
readonly fromServer: boolean;
|
|
6
|
-
|
|
6
|
+
readonly httpStatus?: number | undefined;
|
|
7
|
+
readonly code?: string | undefined;
|
|
8
|
+
constructor(message: string, fromServer: boolean, httpStatus?: number | undefined, code?: string | undefined);
|
|
7
9
|
}
|
|
10
|
+
export declare function isSessionEndedUploadError(cause: unknown): boolean;
|
|
8
11
|
/** Posts one file to the stack's file endpoint (double-submit CSRF) and returns the new FileRef id. */
|
|
9
12
|
export declare function uploadFileRef(file: Blob, target?: FileRefTarget): Promise<string>;
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
2
|
import { CSRF_HEADER_NAME, readCsrfToken } from "../../live/index.js";
|
|
3
|
+
import { isSessionEndedError } from "../../session/session-ended.js";
|
|
3
4
|
import { buildFileUploadFormData } from "./file-upload-form-data.js";
|
|
4
5
|
/** Carries the server's own `error` message when the upload endpoint sent one. */
|
|
5
6
|
export class FileUploadFailedError extends Error {
|
|
6
7
|
fromServer;
|
|
7
|
-
|
|
8
|
+
httpStatus;
|
|
9
|
+
code;
|
|
10
|
+
constructor(message, fromServer, httpStatus, code) {
|
|
8
11
|
super(message);
|
|
9
12
|
this.fromServer = fromServer;
|
|
13
|
+
this.httpStatus = httpStatus;
|
|
14
|
+
this.code = code;
|
|
10
15
|
}
|
|
11
16
|
}
|
|
17
|
+
export function isSessionEndedUploadError(cause) {
|
|
18
|
+
return (cause instanceof FileUploadFailedError &&
|
|
19
|
+
cause.code !== undefined &&
|
|
20
|
+
cause.httpStatus !== undefined &&
|
|
21
|
+
isSessionEndedError({ code: cause.code, httpStatus: cause.httpStatus }));
|
|
22
|
+
}
|
|
12
23
|
/** Posts one file to the stack's file endpoint (double-submit CSRF) and returns the new FileRef id. */
|
|
13
24
|
export async function uploadFileRef(file, target) {
|
|
14
25
|
const csrf = readCsrfToken();
|
|
@@ -21,9 +32,10 @@ export async function uploadFileRef(file, target) {
|
|
|
21
32
|
const id = propertyOf(json, "id");
|
|
22
33
|
if (!res.ok || typeof id !== "string") {
|
|
23
34
|
const message = propertyOf(json, "error");
|
|
35
|
+
const code = propertyOf(json, "code");
|
|
24
36
|
throw typeof message === "string"
|
|
25
|
-
? new FileUploadFailedError(message, true)
|
|
26
|
-
: new FileUploadFailedError(`upload failed (${res.status})`, false);
|
|
37
|
+
? new FileUploadFailedError(message, true, res.status, typeof code === "string" ? code : undefined)
|
|
38
|
+
: new FileUploadFailedError(`upload failed (${res.status})`, false, res.status);
|
|
27
39
|
}
|
|
28
40
|
return id;
|
|
29
41
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// Generic action menu built on
|
|
3
|
-
//
|
|
2
|
+
// Generic action menu built on the radix-ui DropdownMenu.
|
|
3
|
+
// Used for ProfileMenu (topbar avatar), Edit-
|
|
4
4
|
// Header-Menu (three dots), and list-row kebab menus; only the
|
|
5
5
|
// trigger differs between uses.
|
|
6
6
|
//
|
|
@@ -9,8 +9,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
9
9
|
// The text input (typing) intentionally lives in each Primitive, not here:
|
|
10
10
|
// date has one field, while timestamp has two (date + time). This lets both
|
|
11
11
|
// share the calendar mechanics without sharing their input layout.
|
|
12
|
-
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
13
12
|
import { Calendar as CalendarIcon } from "lucide-react";
|
|
13
|
+
import { Popover as PopoverPrimitive } from "radix-ui";
|
|
14
14
|
import { lazy, Suspense } from "react";
|
|
15
15
|
import { cn } from "../lib/cn.js";
|
|
16
16
|
const CalendarDayPicker = lazy(() => import("./calendar-day-picker.js"));
|
|
@@ -12,9 +12,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
// Multi-mode: `multiple: true` switches to tag display and multi-selection.
|
|
13
13
|
// Selected values render as small chips, while the search input remains open
|
|
14
14
|
// for additional selections.
|
|
15
|
-
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
16
15
|
import { Command } from "cmdk";
|
|
17
16
|
import { Check, ChevronDown, Loader2, Plus } from "lucide-react";
|
|
17
|
+
import { Popover as PopoverPrimitive } from "radix-ui";
|
|
18
18
|
import { useEffect, useState } from "react";
|
|
19
19
|
import { REFERENCE_SEARCH_DEBOUNCE_MS, useTranslation } from "../../index.js";
|
|
20
20
|
import { cn } from "../lib/cn.js";
|
|
@@ -8,9 +8,35 @@ import { columnCellPresentationProps } from "./data-cell-props.js";
|
|
|
8
8
|
// last column slid under the sticky actions column with
|
|
9
9
|
// nothing visibly cut off. max-w-xs bounds a single column;
|
|
10
10
|
// badges and dates stay nowrap by their own classes.
|
|
11
|
+
// A resizable table sizes by content until the user drags a column. Its
|
|
12
|
+
// first column names the row and gets the room it needs: it breaks anywhere
|
|
13
|
+
// only when the table runs out of width, so a long identifier stays on one
|
|
14
|
+
// line on a wide screen instead of wrapping at 20rem. Dates and timestamps
|
|
15
|
+
// keep one line there too; a narrow screen scrolls the table sideways.
|
|
16
|
+
const ONE_LINE_COLUMN_TYPES = new Set(["timestamp", "date"]);
|
|
17
|
+
// Projection lists synthesize every column as type "text"; their dates carry a format spec.
|
|
18
|
+
function isDateColumn(col) {
|
|
19
|
+
if (ONE_LINE_COLUMN_TYPES.has(col.type))
|
|
20
|
+
return true;
|
|
21
|
+
const renderer = col.renderer;
|
|
22
|
+
return (typeof renderer === "object" &&
|
|
23
|
+
renderer !== null &&
|
|
24
|
+
"format" in renderer &&
|
|
25
|
+
typeof renderer.format === "string" &&
|
|
26
|
+
ONE_LINE_COLUMN_TYPES.has(renderer.format));
|
|
27
|
+
}
|
|
28
|
+
function autoLayoutCellClassName(m, col, colIndex) {
|
|
29
|
+
if (m.sizing !== undefined && isDateColumn(col))
|
|
30
|
+
return "whitespace-nowrap";
|
|
31
|
+
return colIndex === 0 && m.sizing !== undefined
|
|
32
|
+
? "whitespace-normal [overflow-wrap:anywhere]"
|
|
33
|
+
: "max-w-xs whitespace-normal break-words";
|
|
34
|
+
}
|
|
11
35
|
function dataCellClassName(m, col, colIndex, pinClassName) {
|
|
12
36
|
return cn(col.fileDisplay === undefined
|
|
13
|
-
?
|
|
37
|
+
? m.sizing?.fixed === true
|
|
38
|
+
? "overflow-hidden whitespace-normal break-words"
|
|
39
|
+
: autoLayoutCellClassName(m, col, colIndex)
|
|
14
40
|
: FIT_CONTENT_CELL_CLASS, colIndex === 0 ? "font-medium text-foreground" : "text-foreground-secondary", NUMERIC_COLUMN_TYPES.has(col.type) && "text-right", TABULAR_COLUMN_TYPES.has(col.type) && "tabular-nums", col.highlighted === true && "bg-accent/40", m.density.cell, pinClassName);
|
|
15
41
|
}
|
|
16
42
|
export function renderDataCell(m, row, col, colIndex, opensOnClick) {
|
|
@@ -17,5 +17,5 @@ function renderExpandHeaderCell(m) {
|
|
|
17
17
|
return (_jsx(TableHead, { "data-testid": "column-expand", className: cn(EXPAND_TOGGLE_CELL_CLASS, expandHeaderPin.className), style: expandHeaderPin.style }));
|
|
18
18
|
}
|
|
19
19
|
export function renderTableHeader(m) {
|
|
20
|
-
return (_jsx(TableHeader, { className: "bg-muted", children: _jsxs(TableRow, { ref: m.headerRowRef, className: "hover:bg-transparent", children: [m.selection.headerCell(m.pins.headerCell(SELECT_COLUMN_KEY)), m.expansion !== undefined && renderExpandHeaderCell(m), m.columns.map((col) => renderColumnHeader(m, col)), m.hasTableActions && (_jsx(ActionsHeaderCell, { label: m.tableTranslate?.("kumiko.list.actions") ?? "Actions", style: m.pins.actionsHeaderStyle, absorbsLeftover: m.sizing
|
|
20
|
+
return (_jsx(TableHeader, { className: "bg-muted", children: _jsxs(TableRow, { ref: m.headerRowRef, className: "hover:bg-transparent", children: [m.selection.headerCell(m.pins.headerCell(SELECT_COLUMN_KEY)), m.expansion !== undefined && renderExpandHeaderCell(m), m.columns.map((col) => renderColumnHeader(m, col)), m.hasTableActions && (_jsx(ActionsHeaderCell, { label: m.tableTranslate?.("kumiko.list.actions") ?? "Actions", style: m.pins.actionsHeaderStyle, absorbsLeftover: m.sizing?.fixed === true }))] }) }));
|
|
21
21
|
}
|
|
@@ -8,7 +8,7 @@ import { renderTableHeader } from "./table-header-row.js";
|
|
|
8
8
|
import { TotalsFooterRow } from "./totals-footer.js";
|
|
9
9
|
function renderColumnWidths(m) {
|
|
10
10
|
const { sizing } = m;
|
|
11
|
-
if (sizing === undefined)
|
|
11
|
+
if (sizing === undefined || !sizing.fixed)
|
|
12
12
|
return null;
|
|
13
13
|
return (_jsx(ColumnWidthGroup, { fields: m.columns.map((col) => col.field), sizing: sizing, hasExpandColumn: m.expansion !== undefined, hasSelectColumn: m.selection.active, hasActionsColumn: m.hasTableActions }));
|
|
14
14
|
}
|
|
@@ -21,5 +21,5 @@ function renderTotalsRow(m) {
|
|
|
21
21
|
export function renderTableView(m) {
|
|
22
22
|
const { fillsHeight, chromeless, sizing, testId } = m;
|
|
23
23
|
const TableRoot = fillsHeight ? FlushTable : Table;
|
|
24
|
-
return (_jsx("div", { className: cn(!fillsHeight && "overflow-hidden", !fillsHeight && chromeless !== true && "rounded-lg border bg-card"), children: _jsxs(TableRoot, { "data-testid": testId, className: fillsHeight ? FILL_TABLE_CLASS : undefined, ...(sizing !== undefined && { style: sizing.tableStyle }), children: [renderColumnWidths(m), renderTableHeader(m), renderTableBody(m), renderTotalsRow(m)] }) }));
|
|
24
|
+
return (_jsx("div", { className: cn(!fillsHeight && "overflow-hidden", !fillsHeight && chromeless !== true && "rounded-lg border bg-card"), children: _jsxs(TableRoot, { "data-testid": testId, className: fillsHeight ? FILL_TABLE_CLASS : undefined, ...(sizing?.tableStyle !== undefined && { style: sizing.tableStyle }), children: [renderColumnWidths(m), renderTableHeader(m), renderTableBody(m), renderTotalsRow(m)] }) }));
|
|
25
25
|
}
|
|
@@ -2,14 +2,16 @@ import { type CSSProperties } from "react";
|
|
|
2
2
|
import type { DataTableViewOptions } from "../../../data-table-view-types.ts";
|
|
3
3
|
import { type MeasuredColumnWidths } from "./use-measured-column-widths.ts";
|
|
4
4
|
export type ColumnSizing = {
|
|
5
|
+
/** False until a column has a chosen width: the table keeps the auto layout and sizes columns by content. */
|
|
6
|
+
readonly fixed: boolean;
|
|
5
7
|
readonly widthOf: (field: string) => number;
|
|
6
8
|
readonly minWidthOf: (field: string) => number;
|
|
7
|
-
readonly tableStyle: CSSProperties;
|
|
9
|
+
readonly tableStyle: CSSProperties | undefined;
|
|
8
10
|
/** Live width while a drag is in progress; nothing reaches the view state until `commit`. */
|
|
9
11
|
readonly setLiveWidth: (field: string, width: number) => void;
|
|
10
12
|
readonly commit: (field: string, width: number) => void;
|
|
11
13
|
};
|
|
12
|
-
/**
|
|
14
|
+
/** Sizing for resizable columns; undefined when resizing is off. */
|
|
13
15
|
export declare function useColumnSizing({ columns, view, isNarrow, measured, hasExpandColumn, hasSelectColumn, }: {
|
|
14
16
|
readonly columns: readonly {
|
|
15
17
|
readonly field: string;
|
|
@@ -1,22 +1,36 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import { DEFAULT_COLUMN_WIDTH_PX } from "../../../hooks/list-column-layout.js";
|
|
2
3
|
import { isResizingActive } from "./column-layout-mode.js";
|
|
3
4
|
import { EXPAND_COLUMN_WIDTH_PX, SELECT_COLUMN_WIDTH_PX, } from "./use-measured-column-widths.js";
|
|
4
|
-
/**
|
|
5
|
+
/** Sizing for resizable columns; undefined when resizing is off. */
|
|
5
6
|
export function useColumnSizing({ columns, view, isNarrow, measured, hasExpandColumn, hasSelectColumn = false, }) {
|
|
6
7
|
const [live, setLive] = useState(null);
|
|
7
8
|
const active = isResizingActive(view, isNarrow);
|
|
8
9
|
const widths = view?.widths;
|
|
9
10
|
const minWidths = view?.minWidths;
|
|
10
11
|
const onWidthChange = view?.onWidthChange;
|
|
11
|
-
const commit = useCallback((field, width) => {
|
|
12
|
-
setLive(null);
|
|
13
|
-
onWidthChange?.(field, width);
|
|
14
|
-
}, [onWidthChange]);
|
|
15
12
|
return useMemo(() => {
|
|
16
13
|
if (!active || widths === undefined || minWidths === undefined)
|
|
17
14
|
return undefined;
|
|
18
15
|
const minWidthOf = (field) => minWidths[field] ?? 0;
|
|
19
|
-
const
|
|
16
|
+
const fixed = live !== null || columns.some((column) => widths[column.field] !== undefined);
|
|
17
|
+
// Columns without a chosen width keep the width the auto layout gave them, so the
|
|
18
|
+
// first drag moves one column instead of snapping every other one to a default.
|
|
19
|
+
const displayedWidths = () => live?.frozen ?? measured?.fields ?? {};
|
|
20
|
+
const widthOf = (field) => {
|
|
21
|
+
if (live?.field === field)
|
|
22
|
+
return live.width;
|
|
23
|
+
const chosen = widths[field];
|
|
24
|
+
if (chosen !== undefined)
|
|
25
|
+
return chosen;
|
|
26
|
+
const displayed = displayedWidths()[field];
|
|
27
|
+
if (displayed !== undefined && displayed > 0)
|
|
28
|
+
return displayed;
|
|
29
|
+
return fixed ? Math.max(DEFAULT_COLUMN_WIDTH_PX, minWidthOf(field)) : minWidthOf(field);
|
|
30
|
+
};
|
|
31
|
+
const unchosenDisplayedWidths = (dragged) => Object.fromEntries(columns
|
|
32
|
+
.filter((column) => column.field !== dragged && widths[column.field] === undefined)
|
|
33
|
+
.map((column) => [column.field, widthOf(column.field)]));
|
|
20
34
|
// The actions column keeps no width of its own: it absorbs the leftover space,
|
|
21
35
|
// so a dragged width holds exactly while the table is wider than its container.
|
|
22
36
|
const total = columns.reduce((sum, column) => sum + widthOf(column.field), 0) +
|
|
@@ -24,11 +38,20 @@ export function useColumnSizing({ columns, view, isNarrow, measured, hasExpandCo
|
|
|
24
38
|
(hasSelectColumn ? SELECT_COLUMN_WIDTH_PX : 0) +
|
|
25
39
|
(measured?.actions ?? 0);
|
|
26
40
|
return {
|
|
41
|
+
fixed,
|
|
27
42
|
widthOf,
|
|
28
43
|
minWidthOf,
|
|
29
|
-
tableStyle: { tableLayout: "fixed", width: `max(100%, ${total}px)` },
|
|
30
|
-
setLiveWidth: (field, width) => setLive(
|
|
31
|
-
|
|
44
|
+
tableStyle: fixed ? { tableLayout: "fixed", width: `max(100%, ${total}px)` } : undefined,
|
|
45
|
+
setLiveWidth: (field, width) => setLive((previous) => ({
|
|
46
|
+
field,
|
|
47
|
+
width,
|
|
48
|
+
frozen: previous?.frozen ?? { ...measured?.fields },
|
|
49
|
+
})),
|
|
50
|
+
commit: (field, width) => {
|
|
51
|
+
const others = unchosenDisplayedWidths(field);
|
|
52
|
+
setLive(null);
|
|
53
|
+
onWidthChange?.(field, width, others);
|
|
54
|
+
},
|
|
32
55
|
};
|
|
33
56
|
}, [
|
|
34
57
|
active,
|
|
@@ -39,6 +62,6 @@ export function useColumnSizing({ columns, view, isNarrow, measured, hasExpandCo
|
|
|
39
62
|
hasExpandColumn,
|
|
40
63
|
hasSelectColumn,
|
|
41
64
|
measured,
|
|
42
|
-
|
|
65
|
+
onWidthChange,
|
|
43
66
|
]);
|
|
44
67
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
|
3
2
|
import { Columns3 } from "lucide-react";
|
|
3
|
+
import { Popover as PopoverPrimitive } from "radix-ui";
|
|
4
4
|
import { useOptionalTranslation } from "../../../index.js";
|
|
5
5
|
import { Button as UiButton } from "../../ui/button.js";
|
|
6
6
|
import { SegmentedControl } from "../segmented-select.js";
|
|
@@ -76,7 +76,7 @@ export function isBadgeColumn(col) {
|
|
|
76
76
|
export function FirstCellLink({ enabled, empty, emptyLabel, onOpen, children, }) {
|
|
77
77
|
if (!enabled || (empty && emptyLabel === undefined))
|
|
78
78
|
return children;
|
|
79
|
-
return (_jsx("button", { type: "button", className: "max-w-full cursor-pointer
|
|
79
|
+
return (_jsx("button", { type: "button", className: "max-w-full cursor-pointer bg-transparent p-0 text-left font-medium text-foreground hover:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", onClick: (e) => {
|
|
80
80
|
e.stopPropagation();
|
|
81
81
|
onOpen();
|
|
82
82
|
}, onKeyDown: (e) => {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// Confirm dialog built on ModalShell. Cancel is always secondary; async
|
|
3
3
|
// onConfirm shows a spinner on the confirm button until the promise settles.
|
|
4
|
-
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
5
4
|
import { Loader2 } from "lucide-react";
|
|
5
|
+
import { Dialog as DialogPrimitive } from "radix-ui";
|
|
6
6
|
import { useRef, useState } from "react";
|
|
7
7
|
import { useTranslation } from "../../index.js";
|
|
8
8
|
import { cn } from "../lib/cn.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { DropdownMenu as Primitive } from "radix-ui";
|
|
2
2
|
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
3
3
|
export declare const DropdownMenu: import("react").FC<Primitive.DropdownMenuProps>;
|
|
4
4
|
export declare const DropdownMenuTrigger: import("react").ForwardRefExoticComponent<Primitive.DropdownMenuTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// shadcn-style DropdownMenu built on Radix. Wrappers around
|
|
3
|
-
//
|
|
3
|
+
// the radix-ui DropdownMenu use the same Tailwind token classes as
|
|
4
4
|
// the other primitives. The caller provides the trigger-button styling
|
|
5
5
|
// through the asChild pattern; this module supplies only Content, Item,
|
|
6
6
|
// Label, Separator, and CheckboxItem with consistent styling.
|
|
@@ -11,8 +11,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
// - Portaled content renders across stacking-context boundaries, which is
|
|
12
12
|
// useful in dialogs and popovers.
|
|
13
13
|
// - Keyboard navigation (↑↓ + Home/End) works by default.
|
|
14
|
-
import * as Primitive from "@radix-ui/react-dropdown-menu";
|
|
15
14
|
import { Check } from "lucide-react";
|
|
15
|
+
import { DropdownMenu as Primitive } from "radix-ui";
|
|
16
16
|
import { cn } from "../lib/cn.js";
|
|
17
17
|
export const DropdownMenu = Primitive.Root;
|
|
18
18
|
export const DropdownMenuTrigger = Primitive.Trigger;
|