@cosmicdrift/kumiko-renderer 0.366.0 → 0.368.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.
@@ -19,6 +19,13 @@ export { qualifyScreenId };
19
19
  export declare function qualifyNavId(featureName: string, navId: string): string;
20
20
  export { screenAccessAllows };
21
21
  export declare function KumikoScreen({ schema, qn, translate, entityId, onRowClick, onCopyLink, }: KumikoScreenProps): ReactNode;
22
+ /** Empty state for a screen QN that is not in the schema (feature disabled for
23
+ * the tenant, stale link, miswired screen). The QN only goes to the console. */
24
+ export declare function ScreenMissingState({ screenQn, homeScreenId, testId, }: {
25
+ readonly screenQn: string;
26
+ readonly homeScreenId?: string;
27
+ readonly testId?: string;
28
+ }): ReactNode;
22
29
  export declare function buildInitialValues(fields: Readonly<Record<string, unknown>>, defaultCurrency?: string, moneyCurrencyOverrides?: Readonly<Record<string, string>>): FormValues;
23
30
  export declare function tenantCurrencyMoneyFieldNames(fields: Readonly<Record<string, unknown>>): readonly string[];
24
31
  export declare function literalCurrencyOverrides(fields: Readonly<Record<string, unknown>>): Readonly<Record<string, string>>;
@@ -61,7 +61,6 @@ export function qualifyNavId(featureName, navId) {
61
61
  }
62
62
  export { screenAccessAllows };
63
63
  export function KumikoScreen({ schema, qn, translate, entityId, onRowClick, onCopyLink, }) {
64
- const { Banner, Text } = usePrimitives();
65
64
  const userRoles = useUserRoles();
66
65
  const outerHost = useReturnHost();
67
66
  const screen = useMemo(() => schema.screens.find((s) => qualifyScreenId(schema.featureName, s.id) === qn), [schema.featureName, schema.screens, qn]);
@@ -71,10 +70,10 @@ export function KumikoScreen({ schema, qn, translate, entityId, onRowClick, onCo
71
70
  ...(entityId !== undefined && { entityId }),
72
71
  }), [screen, entityId]);
73
72
  if (!screen) {
74
- return (_jsxs(Banner, { padded: true, variant: "error", testId: "kumiko-screen-not-found", children: ["Screen not found: ", _jsx(Text, { variant: "code", children: qn })] }));
73
+ return _jsx(ScreenMissingState, { screenQn: qn });
75
74
  }
76
75
  if (!screenAccessAllows(screen.access, userRoles)) {
77
- return (_jsxs(Banner, { padded: true, variant: "error", testId: "kumiko-screen-access-denied", children: ["Access denied: ", _jsx(Text, { variant: "code", children: qn })] }));
76
+ return _jsx(ScreenAccessDeniedBanner, { screenQn: qn, testId: "kumiko-screen-access-denied" });
78
77
  }
79
78
  const screenBody = renderScreenBody({
80
79
  schema,
@@ -94,8 +93,7 @@ export function KumikoScreen({ schema, qn, translate, entityId, onRowClick, onCo
94
93
  return _jsx(ReturnHostProvider, { value: ownHost, children: body });
95
94
  }
96
95
  function ScreenVisibilityGate({ featureName, visibleWhen, fallback, translate, children, }) {
97
- const { Banner, PageHeader } = usePrimitives();
98
- const pageHeaderSlotAvailable = usePageHeaderSlotAvailable();
96
+ const { Banner } = usePrimitives();
99
97
  const result = useQuery(visibleWhen.query, {}, { live: true });
100
98
  const wasShown = useRef(false);
101
99
  const verdict = evalVisibleWhenAfterShown(visibleWhen, result, wasShown.current);
@@ -108,9 +106,43 @@ function ScreenVisibilityGate({ featureName, visibleWhen, fallback, translate, c
108
106
  if (verdict === "loading") {
109
107
  return (_jsx(Banner, { padded: true, variant: "loading", testId: "kumiko-screen-loading", children: "Loading\u2026" }));
110
108
  }
111
- // The route still names the locked screen; its breadcrumb must not label what stands in for it.
112
- const hideLockedBreadcrumb = PageHeader !== undefined && pageHeaderSlotAvailable ? _jsx(PageHeader, { hideBreadcrumb: true }) : null;
113
- return (_jsxs(_Fragment, { children: [hideLockedBreadcrumb, fallback !== undefined ? (_jsx(FallbackScreen, { featureName: featureName, fallback: fallback, translate: translate })) : (_jsx(ScreenUnavailableBanner, {}))] }));
109
+ if (fallback !== undefined) {
110
+ return _jsx(FallbackScreen, { featureName: featureName, fallback: fallback, translate: translate });
111
+ }
112
+ return (_jsxs(_Fragment, { children: [_jsx(LockedScreenHeader, {}), _jsx(ScreenUnavailableBanner, {})] }));
113
+ }
114
+ // The route still names the locked screen; its breadcrumb must not label what stands in for it.
115
+ // With a stand-in the header is labelled with that screen instead, none if it has no title.
116
+ function LockedScreenHeader({ standInScreenId }) {
117
+ const { PageHeader } = usePrimitives();
118
+ const pageHeaderSlotAvailable = usePageHeaderSlotAvailable();
119
+ if (PageHeader === undefined || !pageHeaderSlotAvailable)
120
+ return null;
121
+ return standInScreenId !== undefined ? (_jsx(PageHeader, { hideBreadcrumb: true, breadcrumbScreenId: standInScreenId })) : (_jsx(PageHeader, { hideBreadcrumb: true }));
122
+ }
123
+ function ScreenMissingHomeButton({ homeScreenId, testId, }) {
124
+ const { Button } = usePrimitives();
125
+ const t = useTranslation();
126
+ const nav = useNav();
127
+ return (_jsx(Button, { variant: "secondary", onClick: () => nav.navigate({ screenId: homeScreenId }), testId: testId, children: t("kumiko.screen-missing.home") }));
128
+ }
129
+ /** Empty state for a screen QN that is not in the schema (feature disabled for
130
+ * the tenant, stale link, miswired screen). The QN only goes to the console. */
131
+ export function ScreenMissingState({ screenQn, homeScreenId, testId = "kumiko-screen-not-found", }) {
132
+ const { Card, Text } = usePrimitives();
133
+ const t = useTranslation();
134
+ useEffect(() => {
135
+ console.warn(`[kumiko] screen "${screenQn}" is not in the schema (feature disabled, stale link or miswired screen).`);
136
+ }, [screenQn]);
137
+ return (_jsxs(Card, { options: { padded: true }, testId: testId, children: [_jsx(Text, { testId: `${testId}-title`, children: t("kumiko.screen-missing.title") }), _jsx(Text, { variant: "muted", children: t("kumiko.screen-missing.hint") }), homeScreenId !== undefined && (_jsx(ScreenMissingHomeButton, { homeScreenId: homeScreenId, testId: `${testId}-home` }))] }));
138
+ }
139
+ function ScreenAccessDeniedBanner({ screenQn, testId, }) {
140
+ const { Banner } = usePrimitives();
141
+ const t = useTranslation();
142
+ useEffect(() => {
143
+ console.warn(`[kumiko] access to screen "${screenQn}" denied for the current roles.`);
144
+ }, [screenQn]);
145
+ return (_jsx(Banner, { padded: true, variant: "error", testId: testId, children: t("kumiko.screen-access-denied.title") }));
114
146
  }
115
147
  function ScreenUnavailableBanner() {
116
148
  const { Banner } = usePrimitives();
@@ -121,9 +153,10 @@ function FallbackScreen({ featureName, fallback, translate, }) {
121
153
  const target = useEmbeddedScreen(featureName, fallback);
122
154
  // Also undefined when the user lacks access to the fallback screen, which is
123
155
  // not a framework error.
124
- if (target === undefined)
125
- return _jsx(ScreenUnavailableBanner, {});
126
- return _jsx(KumikoScreen, { schema: target.schema, qn: target.qn, translate: translate });
156
+ if (target === undefined) {
157
+ return (_jsxs(_Fragment, { children: [_jsx(LockedScreenHeader, {}), _jsx(ScreenUnavailableBanner, {})] }));
158
+ }
159
+ return (_jsxs(_Fragment, { children: [_jsx(LockedScreenHeader, { standInScreenId: lastSegment(target.qn) }), _jsx(KumikoScreen, { schema: target.schema, qn: target.qn, translate: translate })] }));
127
160
  }
128
161
  function renderScreenBody({ schema, screen, translate, entityId, onRowClick, onCopyLink, }) {
129
162
  switch (screen.type) {
@@ -1143,7 +1176,7 @@ function useDrawerAction(schema) {
1143
1176
  };
1144
1177
  }
1145
1178
  function DrawerHost({ schema, drawerAction, drawerScreen, drawerInitialValues, userRoles, translate, onClose, onSuccess, }) {
1146
- const { Drawer, Banner, Text, Dialog } = usePrimitives();
1179
+ const { Drawer, Dialog } = usePrimitives();
1147
1180
  const t = useTranslation();
1148
1181
  const effectiveTranslate = translate ?? t;
1149
1182
  const locale = useLocale().locale();
@@ -1185,7 +1218,7 @@ function DrawerHost({ schema, drawerAction, drawerScreen, drawerInitialValues, u
1185
1218
  requestClose();
1186
1219
  }, title: effectiveTranslate(drawerAction.title ?? drawerAction.label, titleParams), ...(drawerAction.subtitle !== undefined && {
1187
1220
  subtitle: effectiveTranslate(drawerAction.subtitle, titleParams),
1188
- }), testId: `toolbar-drawer-${drawerAction.id}`, children: _jsx(PageHeaderSlotAvailableProvider, { value: false, children: _jsx(InsideDrawerProvider, { value: true, children: drawerScreen === undefined ? (_jsxs(Banner, { padded: true, variant: "error", testId: "kumiko-toolbar-drawer-not-found", children: ["Screen not found: ", _jsx(Text, { variant: "code", children: drawerAction.screen })] })) : !allowed ? (_jsxs(Banner, { padded: true, variant: "error", testId: "kumiko-toolbar-drawer-access-denied", children: ["Access denied: ", _jsx(Text, { variant: "code", children: drawerAction.screen })] })) : (_jsx(ActionFormBody, { schema: schema, screen: drawerScreen, ...(translate !== undefined && { translate }), ...(drawerInitialValues !== undefined && {
1221
+ }), testId: `toolbar-drawer-${drawerAction.id}`, children: _jsx(PageHeaderSlotAvailableProvider, { value: false, children: _jsx(InsideDrawerProvider, { value: true, children: drawerScreen === undefined ? (_jsx(ScreenMissingState, { screenQn: drawerAction.screen, testId: "kumiko-toolbar-drawer-not-found" })) : !allowed ? (_jsx(ScreenAccessDeniedBanner, { screenQn: drawerAction.screen, testId: "kumiko-toolbar-drawer-access-denied" })) : (_jsx(ActionFormBody, { schema: schema, screen: drawerScreen, ...(translate !== undefined && { translate }), ...(drawerInitialValues !== undefined && {
1189
1222
  initialOverrides: drawerInitialValues,
1190
1223
  }), onSuccess: () => {
1191
1224
  setHasUnsavedInput(false);
@@ -63,6 +63,7 @@ export function synthesizeProjectionDetailScreen(screen) {
63
63
  allowCreate: false,
64
64
  allowDelete: false,
65
65
  ...(screen.description !== undefined && { description: screen.description }),
66
+ ...(screen.recordTitleField !== undefined && { recordTitleField: screen.recordTitleField }),
66
67
  ...(screen.fieldLabels !== undefined && { fieldLabels: screen.fieldLabels }),
67
68
  ...(screen.slots !== undefined && { slots: screen.slots }),
68
69
  ...(screen.access !== undefined && { access: screen.access }),
@@ -1240,6 +1240,12 @@ export function RenderEdit(props) {
1240
1240
  // floating as a heading above the band.
1241
1241
  const titleInSettingsHeader = isSettingsList && !titleInShell;
1242
1242
  const usesFieldFlow = isScreenForm || insideDrawer;
1243
+ // The caller's headerRegion already shows the record title in the shell page
1244
+ // header, so the type title ("Release") above the form would repeat it.
1245
+ const recordTitleInHeaderRegion = headerRegion !== undefined &&
1246
+ recordTitle !== undefined &&
1247
+ PageHeader !== undefined &&
1248
+ pageHeaderSlotAvailable;
1243
1249
  const allRequiredHint = (_jsx(Text, { variant: "muted", testId: "render-edit-all-required-hint", children: translate("kumiko.form.all-required") }));
1244
1250
  const stepRail = isWizard && isScreenForm && StepBar !== undefined;
1245
1251
  const upNextSection = screen.layout.wizard?.aside?.upNext === true ? filteredSections[currentStep + 1] : undefined;
@@ -1270,7 +1276,8 @@ export function RenderEdit(props) {
1270
1276
  actions: (_jsx(ActionOverflowMenu, { label: translate("kumiko.list.row-actions.more"), items: headerMenuItems, testId: "render-edit-header-menu" })),
1271
1277
  }) })), _jsx(Form, { onSubmit: () => void handleSubmit(), ...(hideSectionTitles !== true &&
1272
1278
  !titleInShell &&
1273
- !titleInSettingsHeader && { title: formTitle }), ...(formSubtitle !== undefined &&
1279
+ !titleInSettingsHeader &&
1280
+ !recordTitleInHeaderRegion && { title: formTitle }), ...(formSubtitle !== undefined &&
1274
1281
  !stepRail &&
1275
1282
  !titleInSettingsHeader && { subtitle: formSubtitle }), ...(isScreenForm && { screenForm: true }), ...(unsavedCount !== undefined &&
1276
1283
  (unsavedCount > 0 || hasDirtyFooter) && { unsavedCount }), ...(sectionNavItems.length > 0 && { sectionNav: sectionNavItems }), ...(stepRail && {
@@ -135,6 +135,10 @@ export const kumikoDefaultTranslations = {
135
135
  "kumiko.app-boot.no-open-screen": "This app has no screen that's accessible without a role restriction.",
136
136
  "kumiko.widget.loading": "Loading…",
137
137
  "kumiko.widget.error.title": "Couldn't load.",
138
+ "kumiko.screen-missing.title": "This page is not available",
139
+ "kumiko.screen-missing.hint": "It is not enabled for your account or no longer exists.",
140
+ "kumiko.screen-access-denied.title": "You do not have access to this page.",
141
+ "kumiko.screen-missing.home": "Go to start page",
138
142
  "kumiko.screen-error.title": "This screen failed to render.",
139
143
  "kumiko.screen-error.detail": "Screen: {screen}. {message}",
140
144
  "kumiko.screen-error.screen": "Screen: {screen}",
package/dist/index.d.ts CHANGED
@@ -19,7 +19,7 @@ export { ExtensionSectionsProvider, extensionSectionName, useExtensionSectionCom
19
19
  export type { AppSchema, FeatureSchema, QualifiedContentCollection, WorkspaceSchema, } from "./app/feature-schema.js";
20
20
  export { isAppSchema, toAppSchema } from "./app/feature-schema.js";
21
21
  export type { KumikoScreenProps } from "./app/kumiko-screen.js";
22
- export { KumikoScreen, qualifyNavId, qualifyScreenId } from "./app/kumiko-screen.js";
22
+ export { KumikoScreen, qualifyNavId, qualifyScreenId, ScreenMissingState, } from "./app/kumiko-screen.js";
23
23
  export type { DateRangeBound, DateRangeValue, ResolvedDateRangeFacet } from "./app/list-facets.js";
24
24
  export { buildDateRangePayload, clampDateRange, readDateRange, resolveDateRangeFacets, } from "./app/list-facets.js";
25
25
  export type { NavApi, NavigateOptions, NavProviderProps, NavRoute, NavTarget, ObjectTarget, ScreenTarget, } from "./app/nav.js";
@@ -75,7 +75,7 @@ export { createStaticLocaleResolver, FormalityProvider, LocaleProvider, mergeTra
75
75
  export { kumikoDefaultTranslations } from "./i18n-defaults.js";
76
76
  export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.js";
77
77
  export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.js";
78
- 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, LightboxImage, 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";
78
+ 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, LightboxImage, LightboxProps, LinkProps, MetricBandProps, MetricProps, ModalProps, PageHeaderProps, PickedFile, PrimitivesProviderProps, PrimitivesRegistry, ProgressProps, ProgressTone, PromoPanelAction, PromoPanelProps, RuntimeRenderer, SecretRevealProps, SecretRevealValue, SectionProps, ShareButtonProps, StatusBadgeProps, StatusTone, StepBarProps, StickyActionBarProps, StickyPrimaryActionMarker, TabsProps, TextProps, TrailingInputAction, WizardStepGroupProps, } from "./primitives.js";
79
79
  export { FOOTER_ACTION_ROLE_PROP, NARROW_LABEL_PROP, PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, useOptionalPrimitives, usePrimitives, } from "./primitives.js";
80
80
  export type { SessionEndedSignal, SessionEndedSignalProviderProps, } from "./session/session-ended.js";
81
81
  export { createSessionEndedSignal, isSessionEndedError, SESSION_ENDED_ERROR_CODES, SessionEndedSignalProvider, useSessionEndedSignal, withSessionEndedDetection, } from "./session/session-ended.js";
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ export { DashboardBodyProvider, useDashboardBody } from "./app/dashboard-body.js
19
19
  export { ExtensionFormRegistryProvider, useExtensionFormSubmit, } from "./app/extension-form-submit.js";
20
20
  export { ExtensionSectionsProvider, extensionSectionName, useExtensionSectionComponent, } from "./app/extension-sections.js";
21
21
  export { isAppSchema, toAppSchema } from "./app/feature-schema.js";
22
- export { KumikoScreen, qualifyNavId, qualifyScreenId } from "./app/kumiko-screen.js";
22
+ export { KumikoScreen, qualifyNavId, qualifyScreenId, ScreenMissingState, } from "./app/kumiko-screen.js";
23
23
  export { buildDateRangePayload, clampDateRange, readDateRange, resolveDateRangeFacets, } from "./app/list-facets.js";
24
24
  export { formatPath, hasDetailScreen, NavProvider, parsePath, resolveTarget, useNav, useNavigateWithInitialValues, } from "./app/nav.js";
25
25
  export { lastSegment } from "./app/qn.js";
@@ -180,6 +180,14 @@ export type TrailingInputAction = {
180
180
  readonly onPress: () => void;
181
181
  readonly testId?: string;
182
182
  };
183
+ /** Platform-neutral view of a picked file; the DOM `File` satisfies it. */
184
+ export type PickedFile = {
185
+ readonly name: string;
186
+ readonly size: number;
187
+ readonly type: string;
188
+ readonly webkitRelativePath: string;
189
+ readonly arrayBuffer: () => Promise<ArrayBuffer>;
190
+ };
183
191
  export type InputProps = {
184
192
  readonly kind: "text";
185
193
  readonly id: string;
@@ -318,6 +326,14 @@ export type InputProps = {
318
326
  /** Only for kind:"image" — forwarded to the file input's `capture`
319
327
  * attribute so a phone opens the camera instead of the file picker. */
320
328
  readonly capture?: "environment" | "user";
329
+ /** Web-only folder picker: the picked files (with `webkitRelativePath`)
330
+ * go to `onFilesPicked` and nothing is uploaded. Native impls ignore it. */
331
+ readonly directory?: boolean;
332
+ readonly onFilesPicked?: (files: readonly PickedFile[]) => void;
333
+ /** Translated label of the pick button; absent → the upload/change default. */
334
+ readonly pickLabel?: string;
335
+ /** Hidden file input gets `${testId}-input`, the pick button `${testId}-pick`. */
336
+ readonly testId?: string;
321
337
  } | {
322
338
  readonly kind: "date";
323
339
  readonly id: string;
@@ -1337,6 +1353,9 @@ export type PageHeaderProps = {
1337
1353
  /** Hide the shell header's breadcrumb while mounted, for a screen that stands in
1338
1354
  * for the one the route names (a locked screen's fallback). */
1339
1355
  readonly hideBreadcrumb?: boolean;
1356
+ /** Label the breadcrumb with this screen (id as in the route) instead of the one
1357
+ * the route names, for a locked screen's fallback. Wins over `hideBreadcrumb`. */
1358
+ readonly breadcrumbScreenId?: string;
1340
1359
  };
1341
1360
  /** One item in an `ActionOverflowMenu` (A7: header/row actions beyond the
1342
1361
  * primary one collapse into this menu instead of a wall of buttons). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.366.0",
3
+ "version": "0.368.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.366.0",
19
- "@cosmicdrift/kumiko-headless": "0.366.0",
20
- "@cosmicdrift/kumiko-types": "0.366.0",
18
+ "@cosmicdrift/kumiko-framework": "0.368.0",
19
+ "@cosmicdrift/kumiko-headless": "0.368.0",
20
+ "@cosmicdrift/kumiko-types": "0.368.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.366.0"
31
+ "@cosmicdrift/kumiko-locale-de": "0.368.0"
32
32
  },
33
33
  "repository": {
34
34
  "type": "git",
package/src/changes.json CHANGED
@@ -1,4 +1,29 @@
1
1
  [
2
+ {
3
+ "version": "0.368.0",
4
+ "type": "fix",
5
+ "title": "A screen that falls back via visibleWhen shows the fallback's title",
6
+ "detail": "When visibleWhen locks a screen and its fallback renders instead, the header no longer stays empty. It shows the title of the fallback screen (resolved like any other screen title, from its nav label), never the locked screen's. A fallback without a title still leaves the header empty. PageHeader takes a new breadcrumbScreenId prop for this."
7
+ },
8
+ {
9
+ "version": "0.367.0",
10
+ "type": "fix",
11
+ "title": "projectionDetail no longer shows an empty type-title block above the record",
12
+ "detail": "The projectionDetail shim dropped recordTitleField, so the duplicate-title fix never applied to real projectionDetail screens. The synthesized edit screen now carries it, and the page header shows the record title once."
13
+ },
14
+ {
15
+ "version": "0.367.0",
16
+ "type": "fix",
17
+ "title": "A route to a screen missing from the schema shows an empty state",
18
+ "detail": "A tenant-disabled feature, stale link or typo no longer falls back to the first feature or shows \"Screen not found: <qn>\". The app renders \"This page is not available\" with a button to the landing screen; the qualified name only goes to the console. New i18n keys kumiko.screen-missing.title, .hint and .home (en, de, es). The access-denied banner of KumikoScreen and the toolbar drawer is now translated too (kumiko.screen-access-denied.title) and no longer prints the qualified name.",
19
+ "migration": "e2e checks on the text \"Screen not found\" should use the testId kumiko-screen-not-found; the testIds are unchanged."
20
+ },
21
+ {
22
+ "version": "0.367.0",
23
+ "type": "fix",
24
+ "title": "projectionDetail drops the duplicate title block",
25
+ "detail": "A screen with recordTitleField no longer repeats the screen title above the form when the page header already shows the record title."
26
+ },
2
27
  {
3
28
  "version": "0.365.0",
4
29
  "type": "fix",