@cosmicdrift/kumiko-renderer 0.130.1 → 0.131.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/package.json +3 -3
- package/src/app/dashboard-body.tsx +32 -0
- package/src/app/kumiko-screen.tsx +93 -16
- package/src/components/__tests__/render-field-app-locale.test.tsx +1 -0
- package/src/hooks/__tests__/use-disclosure.test.ts +25 -0
- package/src/hooks/__tests__/use-mutation.test.tsx +76 -0
- package/src/hooks/use-disclosure.ts +20 -0
- package/src/hooks/use-mutation.ts +51 -0
- package/src/i18n-defaults.ts +9 -0
- package/src/index.ts +7 -0
- package/src/primitives.tsx +23 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.131.0",
|
|
4
4
|
"description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.131.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.131.0",
|
|
20
20
|
"react": "^19.2.6"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Dashboard-Body-Injection: der KumikoScreen-Switch ist plattform-agnostisch,
|
|
2
|
+
// die Dashboard-Panels (StatCard, Charts) sind es nicht — die Implementierung
|
|
3
|
+
// kommt vom Platform-Package (renderer-web registriert seine Web-Variante in
|
|
4
|
+
// createKumikoApp). Gleiches Muster wie CustomScreensProvider.
|
|
5
|
+
|
|
6
|
+
import type { DashboardScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
7
|
+
import type { Translate } from "@cosmicdrift/kumiko-headless";
|
|
8
|
+
import { type ComponentType, createContext, type ReactNode, useContext } from "react";
|
|
9
|
+
|
|
10
|
+
export type DashboardBodyProps = {
|
|
11
|
+
readonly screen: DashboardScreenDefinition;
|
|
12
|
+
readonly translate?: Translate;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const DashboardBodyContext = createContext<ComponentType<DashboardBodyProps> | undefined>(
|
|
16
|
+
undefined,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export type DashboardBodyProviderProps = {
|
|
20
|
+
readonly children: ReactNode;
|
|
21
|
+
readonly value: ComponentType<DashboardBodyProps>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function DashboardBodyProvider({ children, value }: DashboardBodyProviderProps): ReactNode {
|
|
25
|
+
return <DashboardBodyContext.Provider value={value}>{children}</DashboardBodyContext.Provider>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Undefined wenn kein Platform-Package einen Dashboard-Body registriert
|
|
29
|
+
* hat — KumikoScreen zeigt dann seinen Placeholder-Banner. */
|
|
30
|
+
export function useDashboardBody(): ComponentType<DashboardBodyProps> | undefined {
|
|
31
|
+
return useContext(DashboardBodyContext);
|
|
32
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { ConfigCascade } from "@cosmicdrift/kumiko-framework/engine";
|
|
|
2
2
|
import type {
|
|
3
3
|
ActionFormScreenDefinition,
|
|
4
4
|
ConfigEditScreenDefinition,
|
|
5
|
+
DashboardScreenDefinition,
|
|
5
6
|
EntityDefinition,
|
|
6
7
|
EntityEditScreenDefinition,
|
|
7
8
|
EntityListScreenDefinition,
|
|
@@ -34,6 +35,7 @@ import { type DataTableFacet, type DataTableRowAction, usePrimitives } from "../
|
|
|
34
35
|
import { synthesizeActionFormEntity, synthesizeActionFormScreen } from "./action-form-shim";
|
|
35
36
|
import { synthesizeConfigEditEntity, synthesizeConfigEditScreen } from "./config-edit-shim";
|
|
36
37
|
import { useCustomScreenComponent } from "./custom-screens";
|
|
38
|
+
import { useDashboardBody } from "./dashboard-body";
|
|
37
39
|
import type { FeatureSchema } from "./feature-schema";
|
|
38
40
|
import { useNav } from "./nav";
|
|
39
41
|
import { synthesizeProjectionEntity, synthesizeProjectionScreen } from "./projection-list-shim";
|
|
@@ -146,6 +148,8 @@ export function KumikoScreen({
|
|
|
146
148
|
{...(onRowClick !== undefined && { onRowClick })}
|
|
147
149
|
/>
|
|
148
150
|
);
|
|
151
|
+
case "dashboard":
|
|
152
|
+
return <DashboardScreenBody screen={screen} translate={translate} />;
|
|
149
153
|
case "actionForm":
|
|
150
154
|
return <ActionFormBody schema={schema} screen={screen} translate={translate} />;
|
|
151
155
|
case "configEdit":
|
|
@@ -155,6 +159,29 @@ export function KumikoScreen({
|
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
161
|
|
|
162
|
+
// Injection-Body für dashboard-Screens: die Panel-Implementierung (StatCard,
|
|
163
|
+
// Charts) ist plattform-spezifisch und kommt aus dem DashboardBody-Context
|
|
164
|
+
// (renderer-web registriert die Web-Variante in createKumikoApp).
|
|
165
|
+
function DashboardScreenBody({
|
|
166
|
+
screen,
|
|
167
|
+
translate,
|
|
168
|
+
}: {
|
|
169
|
+
readonly screen: DashboardScreenDefinition;
|
|
170
|
+
readonly translate?: Translate;
|
|
171
|
+
}): ReactNode {
|
|
172
|
+
const { Banner, Text } = usePrimitives();
|
|
173
|
+
const Body = useDashboardBody();
|
|
174
|
+
if (Body === undefined) {
|
|
175
|
+
return (
|
|
176
|
+
<Banner padded variant="info" testId="kumiko-screen-dashboard-placeholder">
|
|
177
|
+
Dashboard screen <Text variant="code">{screen.id}</Text> — kein Dashboard-Body registriert
|
|
178
|
+
(createKumikoApp aus <Text variant="code">kumiko-renderer-web</Text> wired ihn automatisch).
|
|
179
|
+
</Banner>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return <Body screen={screen} {...(translate !== undefined && { translate })} />;
|
|
183
|
+
}
|
|
184
|
+
|
|
158
185
|
// Lookup-Body für custom-screens: schaut die Component aus dem
|
|
159
186
|
// CustomScreens-Context (gefüttert von clientFeatures.components in
|
|
160
187
|
// createKumikoApp). Wenn weder Provider gemounted noch screenId
|
|
@@ -1036,6 +1063,7 @@ function ProjectionListBody({
|
|
|
1036
1063
|
const { Banner } = usePrimitives();
|
|
1037
1064
|
const t = useTranslation();
|
|
1038
1065
|
const nav = useNav();
|
|
1066
|
+
const dispatcher = useOptionalDispatcher();
|
|
1039
1067
|
const effectiveTranslate = translate ?? t;
|
|
1040
1068
|
const entity = useMemo(() => synthesizeProjectionEntity(screen.columns), [screen.columns]);
|
|
1041
1069
|
const listScreen = useMemo(() => synthesizeProjectionScreen(screen), [screen]);
|
|
@@ -1066,39 +1094,88 @@ function ProjectionListBody({
|
|
|
1066
1094
|
if (screen.rowActions === undefined) return undefined;
|
|
1067
1095
|
const out: DataTableRowAction[] = [];
|
|
1068
1096
|
for (const action of screen.rowActions) {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1097
|
+
if (action.kind === "navigate") {
|
|
1098
|
+
const navigateAction = action;
|
|
1099
|
+
const visible = action.visible;
|
|
1100
|
+
out.push({
|
|
1101
|
+
id: action.id,
|
|
1102
|
+
label: effectiveTranslate(action.label),
|
|
1103
|
+
...(action.style !== undefined && { style: action.style }),
|
|
1104
|
+
onTrigger: (row: ListRowViewModel) => runNavigate(navigateAction, row),
|
|
1105
|
+
...(visible !== undefined && {
|
|
1106
|
+
isVisible: (row: ListRowViewModel) => evalFieldCondition(visible, row.values),
|
|
1107
|
+
}),
|
|
1108
|
+
});
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
// writeHandler (default-kind) — gleicher Dispatch-Pfad wie entityList:
|
|
1112
|
+
// Failure-Result MUSS zum Error werden (sonst schließt der Confirm-
|
|
1113
|
+
// Dialog kommentarlos).
|
|
1114
|
+
if (dispatcher === undefined) continue;
|
|
1115
|
+
const writeAction = action as RowActionWriteHandler;
|
|
1116
|
+
const writeVisible = writeAction.visible;
|
|
1073
1117
|
out.push({
|
|
1074
|
-
id:
|
|
1075
|
-
label: effectiveTranslate(
|
|
1076
|
-
...(
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1118
|
+
id: writeAction.id,
|
|
1119
|
+
label: effectiveTranslate(writeAction.label),
|
|
1120
|
+
...(writeAction.style !== undefined && { style: writeAction.style }),
|
|
1121
|
+
...(writeAction.confirm !== undefined && {
|
|
1122
|
+
confirm: effectiveTranslate(writeAction.confirm),
|
|
1123
|
+
}),
|
|
1124
|
+
...(writeAction.confirmLabel !== undefined && {
|
|
1125
|
+
confirmLabel: effectiveTranslate(writeAction.confirmLabel),
|
|
1126
|
+
}),
|
|
1127
|
+
onTrigger: async (row: ListRowViewModel) => {
|
|
1128
|
+
const payload =
|
|
1129
|
+
writeAction.payload !== undefined
|
|
1130
|
+
? evalRowExtractor(writeAction.payload, row.values)
|
|
1131
|
+
: { id: row.values["id"] };
|
|
1132
|
+
const result = await dispatcher.write(writeAction.handler, payload);
|
|
1133
|
+
if (!result.isSuccess) {
|
|
1134
|
+
throw new WriteFailedError(
|
|
1135
|
+
result.error,
|
|
1136
|
+
dispatcherErrorText(result.error, effectiveTranslate),
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
},
|
|
1140
|
+
...(writeVisible !== undefined && {
|
|
1141
|
+
isVisible: (row: ListRowViewModel) => evalFieldCondition(writeVisible, row.values),
|
|
1080
1142
|
}),
|
|
1081
1143
|
});
|
|
1082
1144
|
}
|
|
1083
1145
|
return out.length > 0 ? out : undefined;
|
|
1084
|
-
}, [screen.rowActions, effectiveTranslate, runNavigate]);
|
|
1146
|
+
}, [screen.rowActions, effectiveTranslate, runNavigate, dispatcher]);
|
|
1085
1147
|
|
|
1086
1148
|
const toolbarActions = useMemo((): readonly ToolbarActionButton[] | undefined => {
|
|
1087
1149
|
if (screen.toolbarActions === undefined) return undefined;
|
|
1088
1150
|
const out: ToolbarActionButton[] = [];
|
|
1089
1151
|
for (const action of screen.toolbarActions) {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1152
|
+
if (action.kind === "navigate") {
|
|
1153
|
+
const target = action.screen;
|
|
1154
|
+
out.push({
|
|
1155
|
+
id: action.id,
|
|
1156
|
+
label: effectiveTranslate(action.label),
|
|
1157
|
+
...(action.style !== undefined && { style: action.style }),
|
|
1158
|
+
onTrigger: () => nav.navigate({ screenId: target }),
|
|
1159
|
+
});
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
// writeHandler — analog entityList; ohne Dispatcher skippen statt crashen.
|
|
1163
|
+
if (dispatcher === undefined) continue;
|
|
1093
1164
|
out.push({
|
|
1094
1165
|
id: action.id,
|
|
1095
1166
|
label: effectiveTranslate(action.label),
|
|
1096
1167
|
...(action.style !== undefined && { style: action.style }),
|
|
1097
|
-
|
|
1168
|
+
...(action.confirm !== undefined && { confirm: effectiveTranslate(action.confirm) }),
|
|
1169
|
+
...(action.confirmLabel !== undefined && {
|
|
1170
|
+
confirmLabel: effectiveTranslate(action.confirmLabel),
|
|
1171
|
+
}),
|
|
1172
|
+
onTrigger: async () => {
|
|
1173
|
+
await dispatcher.write(action.handler, action.payload ?? {});
|
|
1174
|
+
},
|
|
1098
1175
|
});
|
|
1099
1176
|
}
|
|
1100
1177
|
return out.length > 0 ? out : undefined;
|
|
1101
|
-
}, [screen.toolbarActions, effectiveTranslate, nav]);
|
|
1178
|
+
}, [screen.toolbarActions, effectiveTranslate, nav, dispatcher]);
|
|
1102
1179
|
|
|
1103
1180
|
if (rowsQuery.loading && rowsQuery.data === null) {
|
|
1104
1181
|
return (
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { act, renderHook } from "@testing-library/react";
|
|
3
|
+
import { useDisclosure } from "../use-disclosure";
|
|
4
|
+
|
|
5
|
+
describe("useDisclosure", () => {
|
|
6
|
+
test("open/close/toggle steuern den Zustand", () => {
|
|
7
|
+
const { result } = renderHook(() => useDisclosure());
|
|
8
|
+
expect(result.current.open).toBe(false);
|
|
9
|
+
act(() => result.current.onOpen());
|
|
10
|
+
expect(result.current.open).toBe(true);
|
|
11
|
+
act(() => result.current.onClose());
|
|
12
|
+
expect(result.current.open).toBe(false);
|
|
13
|
+
act(() => result.current.onToggle());
|
|
14
|
+
expect(result.current.open).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("Callbacks sind referenz-stabil über Re-Renders", () => {
|
|
18
|
+
const { result, rerender } = renderHook(() => useDisclosure(true));
|
|
19
|
+
const first = result.current;
|
|
20
|
+
rerender();
|
|
21
|
+
expect(result.current.onOpen).toBe(first.onOpen);
|
|
22
|
+
expect(result.current.onClose).toBe(first.onClose);
|
|
23
|
+
expect(result.current.onToggle).toBe(first.onToggle);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
|
|
3
|
+
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
import { DispatcherProvider } from "../../context/dispatcher-context";
|
|
6
|
+
import { useMutation } from "../use-mutation";
|
|
7
|
+
|
|
8
|
+
function makeDispatcher(write: Dispatcher["write"]): Dispatcher {
|
|
9
|
+
return {
|
|
10
|
+
write,
|
|
11
|
+
query: (async () => ({ isSuccess: true, data: {} })) as unknown as Dispatcher["query"],
|
|
12
|
+
batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
|
|
13
|
+
statusStore: {
|
|
14
|
+
getState: () => "online",
|
|
15
|
+
subscribe: () => () => {},
|
|
16
|
+
} as unknown as Dispatcher["statusStore"],
|
|
17
|
+
pendingWrites: () => [],
|
|
18
|
+
pendingFiles: () => [],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function wrapperFor(dispatcher: Dispatcher) {
|
|
23
|
+
return ({ children }: { readonly children: ReactNode }) => (
|
|
24
|
+
<DispatcherProvider dispatcher={dispatcher}>{children}</DispatcherProvider>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("useMutation", () => {
|
|
29
|
+
test("Success setzt data, pending toggelt, Result wird durchgereicht", async () => {
|
|
30
|
+
let resolve: (() => void) | undefined;
|
|
31
|
+
const gate = new Promise<void>((r) => {
|
|
32
|
+
resolve = r;
|
|
33
|
+
});
|
|
34
|
+
const dispatcher = makeDispatcher((async (_type: string, payload: unknown) => {
|
|
35
|
+
await gate;
|
|
36
|
+
return { isSuccess: true, data: { echoed: payload } };
|
|
37
|
+
}) as unknown as Dispatcher["write"]);
|
|
38
|
+
|
|
39
|
+
const { result } = renderHook(() => useMutation<{ echoed: unknown }>("f:write:x:create"), {
|
|
40
|
+
wrapper: wrapperFor(dispatcher),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(result.current.pending).toBe(false);
|
|
44
|
+
let outcome: Awaited<ReturnType<typeof result.current.mutate>> | undefined;
|
|
45
|
+
act(() => {
|
|
46
|
+
void result.current.mutate({ name: "a" }).then((r) => {
|
|
47
|
+
outcome = r;
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
await waitFor(() => expect(result.current.pending).toBe(true));
|
|
51
|
+
act(() => resolve?.());
|
|
52
|
+
await waitFor(() => expect(result.current.pending).toBe(false));
|
|
53
|
+
expect(result.current.data).toEqual({ echoed: { name: "a" } });
|
|
54
|
+
expect(result.current.error).toBeNull();
|
|
55
|
+
expect(outcome?.isSuccess).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("Failure setzt error, reset räumt auf", async () => {
|
|
59
|
+
const dispatcher = makeDispatcher((async () => ({
|
|
60
|
+
isSuccess: false,
|
|
61
|
+
error: { code: "conflict", message: "boom", i18nKey: "errors.conflict" },
|
|
62
|
+
})) as unknown as Dispatcher["write"]);
|
|
63
|
+
|
|
64
|
+
const { result } = renderHook(() => useMutation("f:write:x:create"), {
|
|
65
|
+
wrapper: wrapperFor(dispatcher),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await act(async () => {
|
|
69
|
+
await result.current.mutate({});
|
|
70
|
+
});
|
|
71
|
+
expect(result.current.error?.code).toBe("conflict");
|
|
72
|
+
act(() => result.current.reset());
|
|
73
|
+
expect(result.current.error).toBeNull();
|
|
74
|
+
expect(result.current.data).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
|
|
3
|
+
// Open/close state for dialogs, sheets and collapsibles — the standard
|
|
4
|
+
// replacement for the hand-rolled `useState(false)` + toggle-callback
|
|
5
|
+
// trio in app screens. All callbacks are referentially stable.
|
|
6
|
+
|
|
7
|
+
export type UseDisclosureResult = {
|
|
8
|
+
readonly open: boolean;
|
|
9
|
+
readonly onOpen: () => void;
|
|
10
|
+
readonly onClose: () => void;
|
|
11
|
+
readonly onToggle: () => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function useDisclosure(initialOpen = false): UseDisclosureResult {
|
|
15
|
+
const [open, setOpen] = useState(initialOpen);
|
|
16
|
+
const onOpen = useCallback(() => setOpen(true), []);
|
|
17
|
+
const onClose = useCallback(() => setOpen(false), []);
|
|
18
|
+
const onToggle = useCallback(() => setOpen((prev) => !prev), []);
|
|
19
|
+
return { open, onOpen, onClose, onToggle };
|
|
20
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { DispatcherError, WriteResult } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
import { useCallback, useState } from "react";
|
|
3
|
+
import { useDispatcher } from "../context/dispatcher-context";
|
|
4
|
+
|
|
5
|
+
// React wrapper around dispatcher.write — the write-side sibling of
|
|
6
|
+
// useQuery. One hook instance per handler-type; `mutate` carries the
|
|
7
|
+
// payload so a single instance serves list-row actions with varying
|
|
8
|
+
// payloads.
|
|
9
|
+
//
|
|
10
|
+
// `mutate` resolves with the raw WriteResult so callers can branch
|
|
11
|
+
// (navigate on success, keep the form open on failure) without waiting
|
|
12
|
+
// for a re-render of `error`/`data`.
|
|
13
|
+
|
|
14
|
+
export type UseMutationResult<TData> = {
|
|
15
|
+
readonly mutate: (payload: unknown) => Promise<WriteResult<TData>>;
|
|
16
|
+
readonly pending: boolean;
|
|
17
|
+
readonly error: DispatcherError | null;
|
|
18
|
+
readonly data: TData | null;
|
|
19
|
+
readonly reset: () => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function useMutation<TData = unknown>(type: string): UseMutationResult<TData> {
|
|
23
|
+
const dispatcher = useDispatcher();
|
|
24
|
+
const [pending, setPending] = useState(false);
|
|
25
|
+
const [error, setError] = useState<DispatcherError | null>(null);
|
|
26
|
+
const [data, setData] = useState<TData | null>(null);
|
|
27
|
+
|
|
28
|
+
const mutate = useCallback(
|
|
29
|
+
async (payload: unknown): Promise<WriteResult<TData>> => {
|
|
30
|
+
setPending(true);
|
|
31
|
+
setError(null);
|
|
32
|
+
const result = await dispatcher.write<TData>(type, payload);
|
|
33
|
+
if (result.isSuccess) {
|
|
34
|
+
setData(result.data);
|
|
35
|
+
} else {
|
|
36
|
+
setError(result.error);
|
|
37
|
+
}
|
|
38
|
+
setPending(false);
|
|
39
|
+
return result;
|
|
40
|
+
},
|
|
41
|
+
[dispatcher, type],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const reset = useCallback(() => {
|
|
45
|
+
setPending(false);
|
|
46
|
+
setError(null);
|
|
47
|
+
setData(null);
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
return { mutate, pending, error, data, reset };
|
|
51
|
+
}
|
package/src/i18n-defaults.ts
CHANGED
|
@@ -45,9 +45,14 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
|
|
|
45
45
|
"kumiko.combobox.loading": "Lade…",
|
|
46
46
|
"kumiko.combobox.placeholder": "—",
|
|
47
47
|
|
|
48
|
+
// Widgets — Query-States (QueryTable, LoadingState, ErrorState).
|
|
49
|
+
"kumiko.widget.loading": "Lade…",
|
|
50
|
+
"kumiko.widget.error.title": "Konnte nicht geladen werden.",
|
|
51
|
+
|
|
48
52
|
// Nav — Sidebar Tree (Toggle-aria-Labels).
|
|
49
53
|
"kumiko.nav.expand": "Aufklappen",
|
|
50
54
|
"kumiko.nav.collapse": "Zuklappen",
|
|
55
|
+
"kumiko.nav.search": "Navigation durchsuchen…",
|
|
51
56
|
|
|
52
57
|
// Dialog — Confirm-Buttons + Close-aria-Label.
|
|
53
58
|
"kumiko.dialog.confirm": "Bestätigen",
|
|
@@ -149,8 +154,12 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
|
|
|
149
154
|
"kumiko.combobox.loading": "Loading…",
|
|
150
155
|
"kumiko.combobox.placeholder": "—",
|
|
151
156
|
|
|
157
|
+
"kumiko.widget.loading": "Loading…",
|
|
158
|
+
"kumiko.widget.error.title": "Couldn't load.",
|
|
159
|
+
|
|
152
160
|
"kumiko.nav.expand": "Expand",
|
|
153
161
|
"kumiko.nav.collapse": "Collapse",
|
|
162
|
+
"kumiko.nav.search": "Search navigation…",
|
|
154
163
|
|
|
155
164
|
"kumiko.dialog.confirm": "Confirm",
|
|
156
165
|
"kumiko.dialog.cancel": "Cancel",
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,8 @@ export type {
|
|
|
18
18
|
export { ColumnRenderersProvider, useColumnRenderer } from "./app/column-renderers";
|
|
19
19
|
export type { CustomScreensMap, CustomScreensProviderProps } from "./app/custom-screens";
|
|
20
20
|
export { CustomScreensProvider, useCustomScreenComponent } from "./app/custom-screens";
|
|
21
|
+
export type { DashboardBodyProps, DashboardBodyProviderProps } from "./app/dashboard-body";
|
|
22
|
+
export { DashboardBodyProvider, useDashboardBody } from "./app/dashboard-body";
|
|
21
23
|
export type {
|
|
22
24
|
ExtensionFormRegistry,
|
|
23
25
|
ExtensionFormSubmitHandler,
|
|
@@ -62,6 +64,8 @@ export {
|
|
|
62
64
|
REFERENCE_LIST_LOOKUP_LIMIT,
|
|
63
65
|
REFERENCE_SEARCH_DEBOUNCE_MS,
|
|
64
66
|
} from "./hooks/reference-limits";
|
|
67
|
+
export type { UseDisclosureResult } from "./hooks/use-disclosure";
|
|
68
|
+
export { useDisclosure } from "./hooks/use-disclosure";
|
|
65
69
|
export type { UseFormOptions, UseFormResult } from "./hooks/use-form";
|
|
66
70
|
export { useForm } from "./hooks/use-form";
|
|
67
71
|
export type {
|
|
@@ -71,6 +75,8 @@ export type {
|
|
|
71
75
|
ListUrlStateApi,
|
|
72
76
|
} from "./hooks/use-list-url-state";
|
|
73
77
|
export { useListUrlState } from "./hooks/use-list-url-state";
|
|
78
|
+
export type { UseMutationResult } from "./hooks/use-mutation";
|
|
79
|
+
export { useMutation } from "./hooks/use-mutation";
|
|
74
80
|
export type { UseQueryOptions, UseQueryResult } from "./hooks/use-query";
|
|
75
81
|
export { useQuery } from "./hooks/use-query";
|
|
76
82
|
export { useStore, useStoreSelector } from "./hooks/use-store";
|
|
@@ -111,6 +117,7 @@ export type {
|
|
|
111
117
|
HeadingProps,
|
|
112
118
|
InputProps,
|
|
113
119
|
LightboxProps,
|
|
120
|
+
LinkProps,
|
|
114
121
|
PrimitivesProviderProps,
|
|
115
122
|
PrimitivesRegistry,
|
|
116
123
|
RuntimeRenderer,
|
package/src/primitives.tsx
CHANGED
|
@@ -66,8 +66,25 @@ export type ButtonProps = {
|
|
|
66
66
|
readonly loading?: boolean;
|
|
67
67
|
/** Semantische Klasse — default="primary". Custom-Impls entscheiden
|
|
68
68
|
* was daraus visuell wird; die Renderer verwenden "primary" für
|
|
69
|
-
* Save, "danger" für Delete, "secondary" für Confirm-State
|
|
70
|
-
|
|
69
|
+
* Save, "danger" für Delete, "secondary" für Confirm-State,
|
|
70
|
+
* "link" für Inline-Aktionen im Fließtext (kein BG, underline). */
|
|
71
|
+
readonly variant?: "primary" | "secondary" | "danger" | "link";
|
|
72
|
+
readonly children: ReactNode;
|
|
73
|
+
readonly testId?: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** Navigations-Link. `variant="button"` rendert die Button-Optik auf einem
|
|
77
|
+
* semantischen Anchor (z.B. „Zum Login" nach Reset-Success), `muted` den
|
|
78
|
+
* dezenten Sekundär-Link. Interne SPA-Navigation läuft über den Browser-
|
|
79
|
+
* Default (History-Integration liegt beim Nav-Layer, nicht am Primitive). */
|
|
80
|
+
export type LinkProps = {
|
|
81
|
+
readonly href: string;
|
|
82
|
+
readonly variant?: "default" | "button" | "muted";
|
|
83
|
+
/** `_blank` setzt in der Web-Impl automatisch rel="noreferrer". */
|
|
84
|
+
readonly target?: "_blank";
|
|
85
|
+
/** Layout-Zusätze (self-center, text-xs) — Web merged via cn(),
|
|
86
|
+
* Native-Impls ignorieren es (Präzedenz: CardProps.className). */
|
|
87
|
+
readonly className?: string;
|
|
71
88
|
readonly children: ReactNode;
|
|
72
89
|
readonly testId?: string;
|
|
73
90
|
};
|
|
@@ -533,9 +550,10 @@ export type GridCellProps = {
|
|
|
533
550
|
/** Semantischer Text. Variants bilden Standard-Typografie-Rollen ab —
|
|
534
551
|
* `body` ist Default, `small` für sekundäre Labels, `code` für inline
|
|
535
552
|
* monospace (entityId, screen-id), `required-mark` für das Sternchen
|
|
536
|
-
* hinter Labels
|
|
553
|
+
* hinter Labels, `muted` für gedimmten Fließtext (text-sm muted-
|
|
554
|
+
* foreground). Custom-Impls mappen auf ihren TypeScale. */
|
|
537
555
|
export type TextProps = {
|
|
538
|
-
readonly variant?: "body" | "small" | "code" | "required-mark";
|
|
556
|
+
readonly variant?: "body" | "small" | "code" | "required-mark" | "muted";
|
|
539
557
|
readonly children: ReactNode;
|
|
540
558
|
readonly testId?: string;
|
|
541
559
|
};
|
|
@@ -674,6 +692,7 @@ export type CorePrimitives = {
|
|
|
674
692
|
readonly Lightbox: ComponentType<LightboxProps>;
|
|
675
693
|
readonly ConfigSourceBadge: ComponentType<ConfigSourceBadgeProps>;
|
|
676
694
|
readonly ConfigCascadeView: ComponentType<ConfigCascadeViewProps>;
|
|
695
|
+
readonly Link: ComponentType<LinkProps>;
|
|
677
696
|
};
|
|
678
697
|
|
|
679
698
|
/** Offene Extension-Zone für App-eigene Primitives. Devs erweitern
|