@cosmicdrift/kumiko-renderer 0.334.0 → 0.336.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.
Files changed (43) hide show
  1. package/dist/app/content-preview.js +7 -9
  2. package/dist/app/extension-sections.d.ts +6 -0
  3. package/dist/app/form-schema.d.ts +2 -1
  4. package/dist/app/form-schema.js +11 -2
  5. package/dist/app/kumiko-screen.d.ts +5 -0
  6. package/dist/app/kumiko-screen.js +196 -65
  7. package/dist/app/nav.d.ts +5 -0
  8. package/dist/app/nav.js +17 -4
  9. package/dist/app/reference-facet-bridge.js +8 -1
  10. package/dist/app/row-actions.d.ts +12 -0
  11. package/dist/app/row-actions.js +24 -7
  12. package/dist/app/screen-access.js +13 -9
  13. package/dist/app/secret-mint-body.js +40 -6
  14. package/dist/app/secrets-edit-body.js +71 -44
  15. package/dist/components/embedded-list-field.js +2 -4
  16. package/dist/components/multi-select-checkboxes.js +4 -1
  17. package/dist/components/reference-create-dialog.js +9 -4
  18. package/dist/components/related-list-section.js +7 -5
  19. package/dist/components/render-edit-action-button.d.ts +1 -1
  20. package/dist/components/render-edit-action-button.js +5 -1
  21. package/dist/components/render-edit-logic.d.ts +2 -1
  22. package/dist/components/render-edit-logic.js +8 -5
  23. package/dist/components/render-edit-types.d.ts +10 -1
  24. package/dist/components/render-edit.js +189 -54
  25. package/dist/components/render-field.js +10 -13
  26. package/dist/components/render-list.js +6 -6
  27. package/dist/components/write-form-section.js +12 -20
  28. package/dist/hooks/use-reference-lookup.d.ts +7 -0
  29. package/dist/hooks/use-reference-lookup.js +10 -0
  30. package/dist/hooks/use-report-step-complete.d.ts +2 -0
  31. package/dist/hooks/use-report-step-complete.js +13 -0
  32. package/dist/i18n.js +8 -4
  33. package/dist/index.d.ts +6 -2
  34. package/dist/index.js +4 -1
  35. package/dist/primitives.d.ts +30 -8
  36. package/dist/primitives.js +5 -0
  37. package/dist/session/session-ended.d.ts +16 -0
  38. package/dist/session/session-ended.js +73 -0
  39. package/dist/sort-by-accessor.d.ts +6 -2
  40. package/dist/sort-by-accessor.js +13 -3
  41. package/dist/sse/live-events.d.ts +1 -1
  42. package/package.json +5 -5
  43. package/src/changes.json +54 -0
@@ -6,6 +6,13 @@ export declare function referenceLookupSource(refFeature: string, refEntity: str
6
6
  readonly queryQn: string;
7
7
  readonly labelKey: string;
8
8
  };
9
+ export declare function referenceOptionSource(field: {
10
+ readonly refLabelField?: string;
11
+ readonly refOptionsQuery?: string;
12
+ }, refFeature: string, refEntity: string): {
13
+ readonly queryQn: string;
14
+ readonly labelKey: string;
15
+ };
9
16
  /** Empty while the query loads (or fails), so callers fall back to the raw id.
10
17
  * `featureName` is the reference's target feature, not the screen's. */
11
18
  export declare function useReferenceLookup(featureName: string, refEntity: string, labelField: string): {
@@ -34,6 +34,16 @@ export function referenceLookupSource(refFeature, refEntity, labelField) {
34
34
  labelKey: labelField,
35
35
  };
36
36
  }
37
+ // Where a reference field's option rows come from, and which of their keys
38
+ // holds the display text — the entity's own list handler plus `refLabelField`
39
+ // by default, or an author-supplied handler whose rows carry `label`
40
+ // (fw#2780), for targets with no readable column of their own.
41
+ export function referenceOptionSource(field, refFeature, refEntity) {
42
+ if (field.refOptionsQuery !== undefined) {
43
+ return { queryQn: field.refOptionsQuery, labelKey: "label" };
44
+ }
45
+ return referenceLookupSource(refFeature, refEntity, field.refLabelField ?? "id");
46
+ }
37
47
  /** Empty while the query loads (or fails), so callers fall back to the raw id.
38
48
  * `featureName` is the reference's target feature, not the screen's. */
39
49
  export function useReferenceLookup(featureName, refEntity, labelField) {
@@ -0,0 +1,2 @@
1
+ import type { ExtensionSectionProps } from "../app/extension-sections.js";
2
+ export declare function useReportStepComplete(reportStepComplete: ExtensionSectionProps["reportStepComplete"], complete: boolean | null): void;
@@ -0,0 +1,13 @@
1
+ import { useEffect } from "react";
2
+ // Lets an extension wizard step report whether it already holds its data
3
+ // without a raw useEffect in app code (the no-raw-hooks guard forbids those).
4
+ // `complete === null` means the step's data is still loading: nothing is
5
+ // reported, so a loading step never flashes "not done" over a passed-via-Next
6
+ // state. Outside update-mode wizards `reportStepComplete` is undefined and the
7
+ // hook does nothing.
8
+ export function useReportStepComplete(reportStepComplete, complete) {
9
+ useEffect(() => {
10
+ if (reportStepComplete !== undefined && complete !== null)
11
+ reportStepComplete(complete);
12
+ }, [reportStepComplete, complete]);
13
+ }
package/dist/i18n.js CHANGED
@@ -65,7 +65,8 @@ export function useLocale() {
65
65
  }
66
66
  // Subscribe + current locale-snapshot. Wir brauchen den Rückgabewert
67
67
  // selbst nicht — wichtig ist nur der re-render-trigger.
68
- useSyncExternalStore(ctx.resolver.subscribe, () => ctx.resolver.locale(), () => ctx.resolver.locale());
68
+ const getLocaleSnapshot = () => ctx.resolver.locale();
69
+ useSyncExternalStore(ctx.resolver.subscribe, getLocaleSnapshot, getLocaleSnapshot);
69
70
  return ctx.resolver;
70
71
  }
71
72
  /** `t("key", params)` resolves in this order:
@@ -83,7 +84,8 @@ export function useTranslation() {
83
84
  // Re-Render bei Sprach-Wechsel. `ctx.resolver.subscribe` ist bereits
84
85
  // eine stable-reference aus dem Resolver, daher hier keine eigene
85
86
  // Memoization der Subscribe-Callback nötig.
86
- const locale = useSyncExternalStore(ctx.resolver.subscribe, () => ctx.resolver.locale(), () => ctx.resolver.locale());
87
+ const getLocaleSnapshot = () => ctx.resolver.locale();
88
+ const locale = useSyncExternalStore(ctx.resolver.subscribe, getLocaleSnapshot, getLocaleSnapshot);
87
89
  // `t` MUSS referenz-stabil sein solange sich Resolver/Bundles/Locale
88
90
  // nicht ändern — Consumer nutzen `t` regelmäßig in useEffect-Deps
89
91
  // (z.B. um Queries neu zu laden wenn sich die Sprache ändert). Ein neu
@@ -132,7 +134,8 @@ function bundleLocaleTiers(locale, fallbackLocale, formality) {
132
134
  * Used by DataTable FormatCell so plain tables without a provider do not crash. */
133
135
  export function useOptionalLocale() {
134
136
  const ctx = useContext(LocaleContext);
135
- useSyncExternalStore((onStoreChange) => (ctx ? ctx.resolver.subscribe(onStoreChange) : () => { }), () => (ctx ? ctx.resolver.locale() : "en"), () => (ctx ? ctx.resolver.locale() : "en"));
137
+ const getLocaleSnapshot = () => (ctx ? ctx.resolver.locale() : "en");
138
+ useSyncExternalStore((onStoreChange) => (ctx ? ctx.resolver.subscribe(onStoreChange) : () => { }), getLocaleSnapshot, getLocaleSnapshot);
136
139
  return ctx === undefined ? undefined : ctx.resolver.locale();
137
140
  }
138
141
  /** Non-throwing time-zone read — "UTC" outside LocaleProvider, matching
@@ -144,7 +147,8 @@ export function useOptionalTimeZone() {
144
147
  /** Non-throwing translate — undefined outside LocaleProvider. */
145
148
  export function useOptionalTranslation() {
146
149
  const ctx = useContext(LocaleContext);
147
- const locale = useSyncExternalStore((onStoreChange) => (ctx ? ctx.resolver.subscribe(onStoreChange) : () => { }), () => (ctx ? ctx.resolver.locale() : "en"), () => (ctx ? ctx.resolver.locale() : "en"));
150
+ const getLocaleSnapshot = () => (ctx ? ctx.resolver.locale() : "en");
151
+ const locale = useSyncExternalStore((onStoreChange) => (ctx ? ctx.resolver.subscribe(onStoreChange) : () => { }), getLocaleSnapshot, getLocaleSnapshot);
148
152
  const formality = useContext(FormalityContext);
149
153
  const t = useCallback((key, params) => ctx === undefined ? key : translateWithFallbacks(ctx, locale, formality, key, params), [ctx, locale, formality]);
150
154
  return ctx === undefined ? undefined : t;
package/dist/index.d.ts CHANGED
@@ -35,6 +35,7 @@ export { dispatcherErrorText, WriteFailedError } from "./app/write-failed-error.
35
35
  export { RelatedListSection } from "./components/related-list-section.js";
36
36
  export type { RenderEditAction, RenderEditChangeState, RenderEditControls, RenderEditProps, } from "./components/render-edit.js";
37
37
  export { RenderEdit } from "./components/render-edit.js";
38
+ export { needsActionConfirm } from "./components/render-edit-action-button.js";
38
39
  export type { RenderFieldProps } from "./components/render-field.js";
39
40
  export { RenderField } from "./components/render-field.js";
40
41
  export type { RenderListProps } from "./components/render-list.js";
@@ -60,6 +61,7 @@ export type { UseMutationResult } from "./hooks/use-mutation.js";
60
61
  export { useMutation } from "./hooks/use-mutation.js";
61
62
  export type { UseQueryOptions, UseQueryResult } from "./hooks/use-query.js";
62
63
  export { entityFromQueryType, useQuery } from "./hooks/use-query.js";
64
+ export { useReportStepComplete } from "./hooks/use-report-step-complete.js";
63
65
  export { useStore, useStoreSelector } from "./hooks/use-store.js";
64
66
  export type { StreamStatus, UseStreamHandlerOptions, UseStreamHandlerResult, } from "./hooks/use-stream-handler.js";
65
67
  export { useStreamHandler } from "./hooks/use-stream-handler.js";
@@ -68,8 +70,10 @@ export { createStaticLocaleResolver, FormalityProvider, LocaleProvider, mergeTra
68
70
  export { kumikoDefaultTranslations } from "./i18n-defaults.js";
69
71
  export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.js";
70
72
  export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.js";
71
- export type { ActionMenuItemSpec, ActionOverflowMenuProps, AppPrimitives, BannerProps, ButtonProps, CardOptions, CardProps, CardSlots, CopyButtonProps, CorePrimitives, DataTableDateRangeFacet, DataTableFacet, DataTableFacetChip, DataTableProps, DataTableRowAction, DataTableRowActionMode, DataTableRowGrouping, DataTableSort, DataTableSortDir, DialogProps, DrawerProps, EmbeddedListCellType, EmbeddedListColumn, EmbeddedListInputProps, EmbeddedListTotal, FieldCellWidth, FieldProps, FillContainerProps, FormProps, FormSectionNavItem, FormWidth, GridCellProps, GridProps, HeadingProps, InputProps, JsonViewProps, LightboxProps, LinkProps, MetricBandProps, MetricProps, ModalProps, PageHeaderProps, PrimitivesProviderProps, PrimitivesRegistry, ProgressProps, ProgressTone, PromoPanelAction, PromoPanelProps, RuntimeRenderer, SecretRevealProps, SecretRevealValue, SectionProps, ShareButtonProps, StatusBadgeProps, StatusTone, StepBarProps, StickyActionBarProps, StickyPrimaryActionMarker, TabsProps, TextProps, TrailingInputAction, WizardStepGroupProps, } from "./primitives.js";
72
- export { PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, usePrimitives, } from "./primitives.js";
73
+ export type { ActionMenuItemSpec, ActionOverflowMenuProps, AppPrimitives, BannerProps, ButtonProps, CardOptions, CardProps, CardSlots, CopyButtonProps, CorePrimitives, DataTableDateRangeFacet, DataTableFacet, DataTableFacetChip, DataTableProps, DataTableRowAction, DataTableRowActionMode, DataTableRowGrouping, DataTableSort, DataTableSortDir, DialogProps, DrawerProps, EmbeddedListCellType, EmbeddedListColumn, EmbeddedListInputProps, EmbeddedListTotal, FieldCellWidth, FieldProps, FillContainerProps, FooterActionMarker, FooterActionRole, FormProps, FormSectionNavItem, FormWidth, GridCellProps, GridProps, HeadingProps, InputProps, JsonViewProps, LightboxProps, LinkProps, MetricBandProps, MetricProps, ModalProps, PageHeaderProps, PrimitivesProviderProps, PrimitivesRegistry, ProgressProps, ProgressTone, PromoPanelAction, PromoPanelProps, RuntimeRenderer, SecretRevealProps, SecretRevealValue, SectionProps, ShareButtonProps, StatusBadgeProps, StatusTone, StepBarProps, StickyActionBarProps, StickyPrimaryActionMarker, TabsProps, TextProps, TrailingInputAction, WizardStepGroupProps, } from "./primitives.js";
74
+ export { FOOTER_ACTION_ROLE_PROP, NARROW_LABEL_PROP, PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, usePrimitives, } from "./primitives.js";
75
+ export type { SessionEndedSignal, SessionEndedSignalProviderProps, } from "./session/session-ended.js";
76
+ export { createSessionEndedSignal, isSessionEndedError, SESSION_ENDED_ERROR_CODES, SessionEndedSignalProvider, useSessionEndedSignal, withSessionEndedDetection, } from "./session/session-ended.js";
73
77
  export { buildWhatsAppShareUrl } from "./share-links.js";
74
78
  export { sortByAccessor } from "./sort-by-accessor.js";
75
79
  export type { LiveEvent, LiveEventSubscriber, LiveEventsProviderProps } from "./sse/live-events.js";
package/dist/index.js CHANGED
@@ -29,6 +29,7 @@ export { VariableChips } from "./app/variable-chips.js";
29
29
  export { dispatcherErrorText, WriteFailedError } from "./app/write-failed-error.js";
30
30
  export { RelatedListSection } from "./components/related-list-section.js";
31
31
  export { RenderEdit } from "./components/render-edit.js";
32
+ export { needsActionConfirm } from "./components/render-edit-action-button.js";
32
33
  export { RenderField } from "./components/render-field.js";
33
34
  export { RenderList } from "./components/render-list.js";
34
35
  export { DispatcherProvider, useDispatcher, useDispatcherStatus, useOptionalDispatcher, } from "./context/dispatcher-context.js";
@@ -43,13 +44,15 @@ export { useForm } from "./hooks/use-form.js";
43
44
  export { useListUrlState } from "./hooks/use-list-url-state.js";
44
45
  export { useMutation } from "./hooks/use-mutation.js";
45
46
  export { entityFromQueryType, useQuery } from "./hooks/use-query.js";
47
+ export { useReportStepComplete } from "./hooks/use-report-step-complete.js";
46
48
  export { useStore, useStoreSelector } from "./hooks/use-store.js";
47
49
  export { useStreamHandler } from "./hooks/use-stream-handler.js";
48
50
  export { createStaticLocaleResolver, FormalityProvider, LocaleProvider, mergeTranslations, translationsByLocaleFromKeys, useFormality, useLocale, useOptionalLocale, useOptionalTranslation, useTranslation, } from "./i18n.js";
49
51
  export { kumikoDefaultTranslations } from "./i18n-defaults.js";
50
52
  export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.js";
51
53
  export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.js";
52
- export { PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, usePrimitives, } from "./primitives.js";
54
+ export { FOOTER_ACTION_ROLE_PROP, NARROW_LABEL_PROP, PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, usePrimitives, } from "./primitives.js";
55
+ export { createSessionEndedSignal, isSessionEndedError, SESSION_ENDED_ERROR_CODES, SessionEndedSignalProvider, useSessionEndedSignal, withSessionEndedDetection, } from "./session/session-ended.js";
53
56
  export { buildWhatsAppShareUrl } from "./share-links.js";
54
57
  export { sortByAccessor } from "./sort-by-accessor.js";
55
58
  export { LiveEventsProvider, useLiveEvents } from "./sse/live-events.js";
@@ -14,6 +14,16 @@ export type DataAttributes = Readonly<Record<`data-${string}`, string>>;
14
14
  * split can't inspect (see `ScreenSlots.footerPrimary`). */
15
15
  export declare const STICKY_PRIMARY_ACTION_PROP: "kumikoStickyPrimaryAction";
16
16
  export type StickyPrimaryActionMarker = Partial<Record<typeof STICKY_PRIMARY_ACTION_PROP, boolean>>;
17
+ export type FooterActionRole = "back" | "primary";
18
+ /** Tells the pinned phone footer which node is Back and which is the primary
19
+ * action, for buttons that are not `type="submit"` (e.g. the wizard Back). */
20
+ export declare const FOOTER_ACTION_ROLE_PROP: "kumikoFooterActionRole";
21
+ /** Short label a footer action uses instead of its children below `sm`. */
22
+ export declare const NARROW_LABEL_PROP: "kumikoNarrowLabel";
23
+ export type FooterActionMarker = {
24
+ readonly [FOOTER_ACTION_ROLE_PROP]?: FooterActionRole;
25
+ readonly [NARROW_LABEL_PROP]?: string;
26
+ };
17
27
  /** Standard-Button. `loading` zeigt einen Spinner statt der Children
18
28
  * und sollte mit `disabled` kombiniert werden, wenn die Action wirklich
19
29
  * blockiert bis das Loading durch ist (z.B. async submit). Native-
@@ -957,6 +967,10 @@ export type GridProps = {
957
967
  /** One full-width row per child, separated by hairlines — the field rows of
958
968
  * a `settings-list` section. Wins over `columns`/`flow`. */
959
969
  readonly list?: boolean;
970
+ /** Id of the element naming this grid (a Field label). Turns the grid into
971
+ * a `role="group"` so a Field whose control is a set of inputs (checkbox
972
+ * group) still gives screen readers a group name. */
973
+ readonly ariaLabelledBy?: string;
960
974
  };
961
975
  /** Width class of a field cell inside a `flow` grid: sized by what the field holds. */
962
976
  export type FieldCellWidth = "text" | "number" | "money" | "date" | "timestamp" | "select" | "full" | "toggle";
@@ -1095,9 +1109,9 @@ export type CardSlots = {
1095
1109
  readonly header?: ReactNode;
1096
1110
  readonly title?: ReactNode;
1097
1111
  readonly subtitle?: ReactNode;
1098
- /** Free-form block content left of `headerActions`, below title/subtitle —
1099
- * for a host-supplied header (projectionDetail `slots.header`) that must
1100
- * share the action row instead of pushing actions into a row of their own. */
1112
+ /** Free-form block content in its own full-width row below the
1113
+ * title/subtitle/`headerActions` row, within the same header block — for a
1114
+ * host-supplied header (projectionDetail `slots.header`). */
1101
1115
  readonly headerContent?: ReactNode;
1102
1116
  /** Rechtsbündiger Header-Slot (Range-Umschalter, Filter, Badge …). */
1103
1117
  readonly headerActions?: ReactNode;
@@ -1149,12 +1163,13 @@ export type ProgressProps = {
1149
1163
  readonly testId?: string;
1150
1164
  };
1151
1165
  /** Wizard step overview — numbered chips, one per step label, with the
1152
- * active step highlighted. With `onStepSelect`, completed steps (index
1153
- * below `currentIndex`) become buttons for going back; forward jumps stay
1154
- * impossible because step-jump validation is a separate concern
1155
- * (kumiko-framework#1966). Implementations render a
1166
+ * active step highlighted. With `onStepSelect`, completed steps become
1167
+ * buttons (every non-current step with `selectableSteps: "all"`); the caller
1168
+ * owns the validation gate for forward jumps. Implementations render a
1156
1169
  * narrow-viewport fallback showing `compactLabel` instead (caller
1157
- * supplies it pre-translated, e.g. "Step 2 of 5 · Industry") — which of
1170
+ * supplies it pre-translated, e.g. "Step 2 of 5 · Industry"). With
1171
+ * `onStepSelect` and `selectableSteps: "all"` that fallback is a dropdown
1172
+ * listing all steps so narrow viewports can jump too — which of
1158
1173
  * the two is visible is a responsive layout choice owned by the
1159
1174
  * implementation, not this contract. */
1160
1175
  export type StepBarProps = {
@@ -1162,6 +1177,13 @@ export type StepBarProps = {
1162
1177
  readonly currentIndex: number;
1163
1178
  readonly compactLabel: string;
1164
1179
  readonly onStepSelect?: (index: number) => void;
1180
+ /** Explicit done state per step index. Absent: a step is done when its
1181
+ * index is below `currentIndex`. The current step is never done. */
1182
+ readonly doneSteps?: readonly boolean[];
1183
+ /** "done" (default): only done steps are jump targets. "all": every
1184
+ * non-current step is, when `onStepSelect` is set; the compact label then
1185
+ * becomes a step picker. */
1186
+ readonly selectableSteps?: "done" | "all";
1165
1187
  /** "label" (default) swaps the step row for `compactLabel` on narrow
1166
1188
  * viewports; "steps" keeps the row there too — for short wizards whose
1167
1189
  * labels fit a phone and whose done steps must stay tappable. */
@@ -6,6 +6,11 @@ import { createContext, useContext, } from "react";
6
6
  * footer-slot mount, whose actual rendered button is opaque app code the
7
7
  * split can't inspect (see `ScreenSlots.footerPrimary`). */
8
8
  export const STICKY_PRIMARY_ACTION_PROP = "kumikoStickyPrimaryAction";
9
+ /** Tells the pinned phone footer which node is Back and which is the primary
10
+ * action, for buttons that are not `type="submit"` (e.g. the wizard Back). */
11
+ export const FOOTER_ACTION_ROLE_PROP = "kumikoFooterActionRole";
12
+ /** Short label a footer action uses instead of its children below `sm`. */
13
+ export const NARROW_LABEL_PROP = "kumikoNarrowLabel";
9
14
  /** Teil-C action-icon collapse rule: a group of more than two actions where
10
15
  * every member carries an icon renders icon-only instead of wall-to-wall
11
16
  * text buttons; any icon-less member keeps the whole group on text so it
@@ -0,0 +1,16 @@
1
+ import type { Dispatcher, DispatcherError } from "@cosmicdrift/kumiko-headless";
2
+ import { type ReactNode } from "react";
3
+ export declare const SESSION_ENDED_ERROR_CODES: readonly ["session_invalid", "invalid_token", "missing_token", "unauthenticated"];
4
+ export declare function isSessionEndedError(error: Pick<DispatcherError, "code" | "httpStatus">): boolean;
5
+ export type SessionEndedSignal = {
6
+ notify(): void;
7
+ subscribe(listener: () => void): () => void;
8
+ };
9
+ export declare function createSessionEndedSignal(): SessionEndedSignal;
10
+ export declare function withSessionEndedDetection(dispatcher: Dispatcher, signal: SessionEndedSignal): Dispatcher;
11
+ export type SessionEndedSignalProviderProps = {
12
+ readonly signal: SessionEndedSignal;
13
+ readonly children: ReactNode;
14
+ };
15
+ export declare function SessionEndedSignalProvider({ signal, children, }: SessionEndedSignalProviderProps): ReactNode;
16
+ export declare function useSessionEndedSignal(): SessionEndedSignal | undefined;
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext } from "react";
3
+ // Only auth-layer 401s end a session. Handlers never throw 401 otherwise and
4
+ // 403 means "authenticated but not allowed", which must not log the user out.
5
+ export const SESSION_ENDED_ERROR_CODES = [
6
+ "session_invalid",
7
+ "invalid_token",
8
+ "missing_token",
9
+ "unauthenticated",
10
+ ];
11
+ export function isSessionEndedError(error) {
12
+ return (error.httpStatus === 401 &&
13
+ SESSION_ENDED_ERROR_CODES.some((sessionEndedCode) => sessionEndedCode === error.code));
14
+ }
15
+ function isSessionEndedThrow(thrown) {
16
+ if (typeof thrown !== "object" || thrown === null)
17
+ return false;
18
+ if (!("code" in thrown) || !("httpStatus" in thrown))
19
+ return false;
20
+ const { code, httpStatus } = thrown;
21
+ return typeof code === "string" && typeof httpStatus === "number"
22
+ ? isSessionEndedError({ code, httpStatus })
23
+ : false;
24
+ }
25
+ export function createSessionEndedSignal() {
26
+ const listeners = new Set();
27
+ return {
28
+ notify() {
29
+ for (const listener of [...listeners])
30
+ listener();
31
+ },
32
+ subscribe(listener) {
33
+ listeners.add(listener);
34
+ return () => {
35
+ listeners.delete(listener);
36
+ };
37
+ },
38
+ };
39
+ }
40
+ export function withSessionEndedDetection(dispatcher, signal) {
41
+ async function detect(pending) {
42
+ const result = await pending;
43
+ if (!result.isSuccess && result.error !== undefined && isSessionEndedError(result.error)) {
44
+ signal.notify();
45
+ }
46
+ return result;
47
+ }
48
+ return {
49
+ write: (type, payload, opts) => detect(dispatcher.write(type, payload, opts)),
50
+ query: (type, payload, opts) => detect(dispatcher.query(type, payload, opts)),
51
+ batch: (commands, opts) => detect(dispatcher.batch(commands, opts)),
52
+ async *stream(type, payload, opts) {
53
+ try {
54
+ yield* dispatcher.stream(type, payload, opts);
55
+ }
56
+ catch (thrown) {
57
+ if (isSessionEndedThrow(thrown))
58
+ signal.notify();
59
+ throw thrown;
60
+ }
61
+ },
62
+ statusStore: dispatcher.statusStore,
63
+ pendingWrites: () => dispatcher.pendingWrites(),
64
+ pendingFiles: () => dispatcher.pendingFiles(),
65
+ };
66
+ }
67
+ const SessionEndedSignalContext = createContext(undefined);
68
+ export function SessionEndedSignalProvider({ signal, children, }) {
69
+ return _jsx(SessionEndedSignalContext, { value: signal, children: children });
70
+ }
71
+ export function useSessionEndedSignal() {
72
+ return useContext(SessionEndedSignalContext);
73
+ }
@@ -1,5 +1,9 @@
1
1
  import type { DataTableSort } from "./primitives.js";
2
+ type SortValue = string | number | null;
2
3
  /** Sorts `rows` by a `DataTableSort` against a field->accessor map. Unknown
3
4
  * field or `sort === null` returns `rows` unchanged (no-op, not an error —
4
- * callers pass whatever the DataTable reports). */
5
- export declare function sortByAccessor<TRow>(rows: readonly TRow[], sort: DataTableSort | null, accessors: Readonly<Record<string, (row: TRow) => string | number>>): readonly TRow[];
5
+ * callers pass whatever the DataTable reports). `null` values sort last in
6
+ * both directions; strings compare locale-aware with numeric collation, so
7
+ * numeric strings (Postgres numeric/bigint) order by value. */
8
+ export declare function sortByAccessor<TRow>(rows: readonly TRow[], sort: DataTableSort | null, accessors: Readonly<Record<string, (row: TRow) => SortValue>>, locale?: string): readonly TRow[];
9
+ export {};
@@ -1,16 +1,26 @@
1
+ function compareNonNull(a, b, collator) {
2
+ if (typeof a === "number" && typeof b === "number")
3
+ return a - b;
4
+ return collator.compare(String(a), String(b));
5
+ }
1
6
  /** Sorts `rows` by a `DataTableSort` against a field->accessor map. Unknown
2
7
  * field or `sort === null` returns `rows` unchanged (no-op, not an error —
3
- * callers pass whatever the DataTable reports). */
4
- export function sortByAccessor(rows, sort, accessors) {
8
+ * callers pass whatever the DataTable reports). `null` values sort last in
9
+ * both directions; strings compare locale-aware with numeric collation, so
10
+ * numeric strings (Postgres numeric/bigint) order by value. */
11
+ export function sortByAccessor(rows, sort, accessors, locale) {
5
12
  if (sort === null)
6
13
  return rows;
7
14
  const accessor = accessors[sort.field];
8
15
  if (accessor === undefined)
9
16
  return rows;
10
17
  const factor = sort.dir === "asc" ? 1 : -1;
18
+ const collator = new Intl.Collator(locale, { numeric: true, sensitivity: "base" });
11
19
  return [...rows].sort((a, b) => {
12
20
  const av = accessor(a);
13
21
  const bv = accessor(b);
14
- return av < bv ? -factor : av > bv ? factor : 0;
22
+ if (av === null || bv === null)
23
+ return av === bv ? 0 : av === null ? 1 : -1;
24
+ return factor * compareNonNull(av, bv, collator);
15
25
  });
16
26
  }
@@ -4,8 +4,8 @@ export type LiveEvent = {
4
4
  readonly data: {
5
5
  readonly id: string;
6
6
  readonly aggregateType: string;
7
+ readonly eventType: string;
7
8
  readonly version: number;
8
- readonly payload: unknown;
9
9
  readonly createdAt: string;
10
10
  };
11
11
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.334.0",
3
+ "version": "0.336.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,9 +15,9 @@
15
15
  }
16
16
  },
17
17
  "dependencies": {
18
- "@cosmicdrift/kumiko-framework": "0.334.0",
19
- "@cosmicdrift/kumiko-headless": "0.334.0",
20
- "@cosmicdrift/kumiko-types": "0.334.0",
18
+ "@cosmicdrift/kumiko-framework": "0.336.0",
19
+ "@cosmicdrift/kumiko-headless": "0.336.0",
20
+ "@cosmicdrift/kumiko-types": "0.336.0",
21
21
  "react": "^19.2.6",
22
22
  "temporal-polyfill": "^0.3.2",
23
23
  "zod": "^4.4.3"
@@ -28,7 +28,7 @@
28
28
  "@types/react-dom": "^19.2.3",
29
29
  "jsdom": "^29.1.1",
30
30
  "react-dom": "^19.2.6",
31
- "@cosmicdrift/kumiko-locale-de": "0.334.0"
31
+ "@cosmicdrift/kumiko-locale-de": "0.336.0"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",
package/src/changes.json CHANGED
@@ -1,4 +1,58 @@
1
1
  [
2
+ {
3
+ "version": "0.336.0",
4
+ "type": "breaking",
5
+ "title": "LiveEvent data has no payload, it carries eventType",
6
+ "detail": "`LiveEvent.data` is now `{ id, aggregateType, eventType, version, createdAt }`. The server sends signals only, so consumers refetch through a query.",
7
+ "migration": "Replace reads of `event.data.payload` with a query refetch. `useQuery({ live: true })` already does this and needs no change."
8
+ },
9
+ {
10
+ "version": "0.336.0",
11
+ "type": "improvement",
12
+ "title": "useReportStepComplete reports an extension wizard step's completeness without a raw useEffect",
13
+ "detail": "`useReportStepComplete(props.reportStepComplete, complete)` with `complete: boolean | null`. `null` reports nothing (data loading), a boolean is reported whenever it changes. Outside update-mode wizards `reportStepComplete` is undefined and the hook does nothing.",
14
+ "migration": "Extension steps that call `reportStepComplete` from a `useEffect` should switch to `useReportStepComplete(reportStepComplete, complete)`; app repos need this to pass the no-raw-hooks guard."
15
+ },
16
+ {
17
+ "version": "0.335.0",
18
+ "type": "fix",
19
+ "title": "secretMint reveal and confirm render as one screen form with the translated screen title; new RenderEdit props leadContent and i18nScreenId"
20
+ },
21
+ {
22
+ "version": "0.335.0",
23
+ "type": "fix",
24
+ "title": "Router paths encode and decode each segment, so entity ids with slashes or spaces round-trip"
25
+ },
26
+ {
27
+ "version": "0.335.0",
28
+ "type": "fix",
29
+ "title": "Icon-only list and edit action buttons set a hover title"
30
+ },
31
+ {
32
+ "version": "0.335.0",
33
+ "type": "fix",
34
+ "title": "Secret delete confirmation, grouped-field conditions, scoped valid flag, tenant currency in reference-create dialog",
35
+ "detail": "Renderer review fixes. Deleting a secret on a secretsEdit screen now asks for confirmation first (new `config.secrets.deleteConfirm` key) and the delete button is disabled while a save or delete is running. The multiSelect checkbox group is exposed as a labelled group (`GridProps.ariaLabelledBy`). The inline reference-create dialog seeds `currency: { kind: \"tenant\" }` money fields from the tenant currency. Fields declared only inside a section `groups` entry now get their `visible`/`readOnly`/`required` conditions registered. `onChange`'s `valid` ignores issues on hidden fields and outside the `fields` scope, like submit does (shared `relevantFieldIssues` helper). Copy-link in the form footer keeps a 44px touch target on narrow viewports. A free-text sibling-field number unit longer than 8 characters is no longer rendered as a suffix."
36
+ },
37
+ {
38
+ "version": "0.335.0",
39
+ "type": "fix",
40
+ "title": "Scoped validation matches exact paths, root refine issues are visible, tabs keep the active tab by id, update redirects use the saved FK, related list sorting handles numeric strings, nulls and locales",
41
+ "detail": "Form and list fixes. A scoped `validate()` entry such as `address.city` no longer reports sibling issues like `address.zip`. Root-level `.refine()` issues now show as a banner in `RenderEdit` instead of blocking submit silently. In tabs mode the active tab follows its id when a field change hides an earlier tab, and an explicit subtitle override is kept. An entityEdit update redirect with `idFrom` reads the saved projection, so a changed parent FK redirects to the new parent. A row-extractor key missing from the row no longer wipes the target field's default. `relatedList` client-side sorting orders numeric strings by value, puts empty values last and compares text locale-aware. A reference list facet warns in dev when the lookup hits its row cap."
42
+ },
43
+ {
44
+ "version": "0.335.0",
45
+ "type": "fix",
46
+ "title": "writeForm section shows the error banner for issues no rendered field can display",
47
+ "detail": "A writeForm section keeps its error banner when the server reports an issue no rendered field can show (`version`, `id`, a root-level refine, a hidden field), instead of failing the save without any visible message."
48
+ },
49
+ {
50
+ "version": "0.335.0",
51
+ "type": "breaking",
52
+ "title": "Wizard editing an existing record shows data-based done state and allows jumping to any step; delete record actions leave the screen",
53
+ "detail": "Update-mode wizards compute done per step from the record (valid, non-empty fields, or passed via Next) and make every non-current step a jump target; forward jumps run the current step's validate gate. `ExtensionSectionProps.reportStepComplete` lets extension steps report completeness. `StepBar` gets `doneSteps` and `selectableSteps`. A writeHandler record action with `redirect`, or a delete of the shown record, navigates away (returnTo, redirect, listScreenId, entity list) instead of refetching.",
54
+ "migration": "Extension wizard steps in an entityEdit that edits an existing record should call `reportStepComplete(true)` (usually from an effect) once they hold their data, otherwise the step bar shows them as not done until the user passes them with Next. This also applies to singleton wizards (e.g. a company-setup wizard), whose steps now show done by data and are all jump targets; tests that assumed back-only chips must be updated. A delete writeHandler action on a projectionDetail or entityEdit now navigates away after success (returnTo, else listScreenId or the entity's list) instead of refetching into \"record not found\"; set `redirect` to choose another target. List row actions are unchanged."
55
+ },
2
56
  {
3
57
  "version": "0.333.0",
4
58
  "type": "improvement",