@cosmicdrift/kumiko-renderer 0.377.0 → 0.379.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/action-form-shim.js +1 -2
- package/dist/app/action-form-support.js +16 -4
- package/dist/app/kumiko-screen.js +51 -38
- package/dist/app/list-empty-content.d.ts +1 -0
- package/dist/app/list-empty-content.js +5 -0
- package/dist/app/row-actions.d.ts +7 -1
- package/dist/app/row-actions.js +6 -1
- package/dist/app/secret-mint-body.js +3 -2
- package/dist/components/embedded-list-field.js +16 -7
- package/dist/components/file-list-field.js +4 -0
- package/dist/components/form-derivation-patch.d.ts +8 -0
- package/dist/components/form-derivation-patch.js +24 -0
- package/dist/components/form-screen-layout.d.ts +13 -0
- package/dist/components/form-screen-layout.js +13 -0
- package/dist/components/grid-cell-for-field.d.ts +4 -1
- package/dist/components/grid-cell-for-field.js +6 -2
- package/dist/components/list-empty-state.d.ts +4 -1
- package/dist/components/list-empty-state.js +4 -1
- package/dist/components/reference-create-dialog.js +1 -1
- package/dist/components/related-list-section.js +4 -1
- package/dist/components/render-edit-logic.d.ts +1 -1
- package/dist/components/render-edit-logic.js +8 -2
- package/dist/components/render-edit-types.d.ts +7 -1
- package/dist/components/render-edit.js +38 -6
- package/dist/components/render-field.d.ts +3 -1
- package/dist/components/render-field.js +27 -8
- package/dist/components/render-list.d.ts +3 -1
- package/dist/components/render-list.js +29 -8
- package/dist/components/write-form-section.js +4 -1
- package/dist/field-issue-text.d.ts +4 -0
- package/dist/field-issue-text.js +14 -0
- package/dist/file-ids-of.d.ts +1 -0
- package/dist/file-ids-of.js +7 -0
- package/dist/flow-diagram/icons.d.ts +3 -0
- package/dist/flow-diagram/icons.js +14 -0
- package/dist/flow-diagram/index.d.ts +2 -0
- package/dist/flow-diagram/index.js +1 -0
- package/dist/flow-diagram/render.d.ts +3 -0
- package/dist/flow-diagram/render.js +234 -0
- package/dist/flow-diagram/types.d.ts +18 -0
- package/dist/flow-diagram/types.js +1 -0
- package/dist/headless/dispatcher/index.d.ts +1 -0
- package/dist/headless/dispatcher/index.js +1 -0
- package/dist/headless/dispatcher/request-id.d.ts +1 -0
- package/dist/headless/dispatcher/request-id.js +15 -0
- package/dist/headless/form/form-controller.js +16 -1
- package/dist/headless/form/zod-bridge.js +7 -0
- package/dist/headless/index.d.ts +2 -1
- package/dist/headless/index.js +2 -1
- package/dist/headless/plural-key.d.ts +1 -0
- package/dist/headless/plural-key.js +5 -0
- package/dist/headless/view-model/edit.d.ts +8 -1
- package/dist/headless/view-model/edit.js +23 -7
- package/dist/headless/view-model/types.d.ts +11 -3
- package/dist/hooks/live-refetch-scheduler.d.ts +6 -0
- package/dist/hooks/live-refetch-scheduler.js +45 -0
- package/dist/hooks/use-query.js +8 -4
- package/dist/hooks/use-reference-lookup.d.ts +1 -0
- package/dist/hooks/use-reference-lookup.js +6 -3
- package/dist/i18n-defaults.js +28 -3
- package/dist/index.d.ts +6 -2
- package/dist/index.js +4 -1
- package/dist/leave-guard-dialog.d.ts +2 -0
- package/dist/leave-guard-dialog.js +28 -0
- package/dist/leave-guard.d.ts +15 -0
- package/dist/leave-guard.js +39 -0
- package/dist/live/dispatcher-live.js +1 -16
- package/dist/primitives.d.ts +39 -0
- package/dist/sse/live-events.d.ts +3 -0
- package/dist/sse/live-events.js +3 -0
- package/dist/web/app/create-app.js +2 -2
- package/dist/web/app/dashboard-body.js +13 -1
- package/dist/web/app/nav.js +51 -9
- package/dist/web/index.d.ts +2 -2
- package/dist/web/index.js +1 -1
- package/dist/web/layout/nav-tree.js +13 -3
- package/dist/web/layout/shell-header.js +3 -3
- package/dist/web/lib/file-upload-form-data.d.ts +2 -0
- package/dist/web/lib/file-upload-form-data.js +2 -0
- package/dist/web/lib/upload-error-key.d.ts +7 -0
- package/dist/web/lib/upload-error-key.js +23 -0
- package/dist/web/lib/upload-file-ref.d.ts +1 -1
- package/dist/web/primitives/embedded-list-input.js +5 -4
- package/dist/web/primitives/file-thumbnail.d.ts +6 -0
- package/dist/web/primitives/file-thumbnail.js +19 -19
- package/dist/web/primitives/file-upload.d.ts +2 -1
- package/dist/web/primitives/file-upload.js +11 -7
- package/dist/web/primitives/file-value-list.d.ts +3 -0
- package/dist/web/primitives/file-value-list.js +22 -0
- package/dist/web/primitives/files-dropzone.d.ts +16 -0
- package/dist/web/primitives/files-dropzone.js +52 -0
- package/dist/web/primitives/index.js +65 -30
- package/dist/web/primitives/segmented-select.js +8 -2
- package/dist/web/sse/live-events.d.ts +13 -0
- package/dist/web/sse/live-events.js +88 -23
- package/dist/web/widgets/bounded-load-cache.d.ts +1 -0
- package/dist/web/widgets/bounded-load-cache.js +25 -0
- package/dist/web/widgets/drawer.js +37 -2
- package/dist/web/widgets/index.d.ts +1 -0
- package/dist/web/widgets/index.js +1 -0
- package/dist/web/widgets/pdf-preview.d.ts +10 -0
- package/dist/web/widgets/pdf-preview.js +140 -0
- package/dist/web/widgets/upload-zone.d.ts +11 -2
- package/dist/web/widgets/upload-zone.js +30 -7
- package/package.json +14 -5
- package/src/changes.json +143 -0
- package/src/web/styles.css +13 -9
package/dist/i18n-defaults.js
CHANGED
|
@@ -40,6 +40,9 @@ export const kumikoDefaultTranslations = {
|
|
|
40
40
|
"kumiko.field.locatedTzHint": "Time local to the given location",
|
|
41
41
|
"kumiko.field.reference-created-no-id": "Record was created but could not be selected automatically. Please select it manually.",
|
|
42
42
|
"kumiko.field.unsupported": "This field type can't be edited here yet.",
|
|
43
|
+
"kumiko.field.file-list.download": "Download",
|
|
44
|
+
"kumiko.field.file-list.hidePreview": "Hide preview",
|
|
45
|
+
"kumiko.field.file-list.preview": "Preview",
|
|
43
46
|
"kumiko.field.fillAction.failed": "Could not fetch a suggestion.",
|
|
44
47
|
"kumiko.field.writeOnly.remove": "Remove stored value",
|
|
45
48
|
"kumiko.field.writeOnly.set": "Set",
|
|
@@ -55,7 +58,6 @@ export const kumikoDefaultTranslations = {
|
|
|
55
58
|
"kumiko.field.file-list.empty": "No files yet.",
|
|
56
59
|
"kumiko.field.file-upload.attached": "File attached",
|
|
57
60
|
"kumiko.field.file-upload.change": "Change",
|
|
58
|
-
"kumiko.field.file-upload.failed": "Upload failed",
|
|
59
61
|
"kumiko.field.file-upload.remove": "Remove",
|
|
60
62
|
"kumiko.field.file-upload.upload": "Upload",
|
|
61
63
|
"kumiko.field.embedded-list.add-row": "Add row",
|
|
@@ -65,13 +67,17 @@ export const kumikoDefaultTranslations = {
|
|
|
65
67
|
"kumiko.field.embedded-list.move-down": "Move down",
|
|
66
68
|
"kumiko.field.embedded-list.empty": "No rows yet.",
|
|
67
69
|
"kumiko.field.embedded-list.empty-cta": "Add first row",
|
|
68
|
-
"kumiko.field.embedded-list.paste-rows-truncated": "
|
|
69
|
-
"kumiko.field.embedded-list.paste-
|
|
70
|
+
"kumiko.field.embedded-list.paste-rows-truncated.one": "1 pasted row was dropped (max row count reached).",
|
|
71
|
+
"kumiko.field.embedded-list.paste-rows-truncated.other": "{count} pasted rows were dropped (max row count reached).",
|
|
72
|
+
"kumiko.field.embedded-list.paste-cells-unmatched.one": "1 cell had no matching option and was left unchanged.",
|
|
73
|
+
"kumiko.field.embedded-list.paste-cells-unmatched.other": "{count} cells had no matching option and were left unchanged.",
|
|
70
74
|
"kumiko.field.multiSelect.select-all": "Select all",
|
|
71
75
|
"kumiko.field.multiSelect.deselect-all": "Deselect all",
|
|
72
76
|
"kumiko.list.search-placeholder": "Search…",
|
|
73
77
|
"kumiko.list.empty.title": "No entries yet.",
|
|
74
78
|
"kumiko.list.empty.filtered": "No entries match the current search or filters.",
|
|
79
|
+
"kumiko.list.empty.defaultFiltered": "No entries match the default filter. Show all to include entries outside it.",
|
|
80
|
+
"kumiko.list.empty.showAll": "Show all",
|
|
75
81
|
"kumiko.list.empty.hint": "Create the first one to get started.",
|
|
76
82
|
"kumiko.list.no-entries": "No entries.",
|
|
77
83
|
"kumiko.record.not-found.title": "This record was not found.",
|
|
@@ -85,6 +91,7 @@ export const kumikoDefaultTranslations = {
|
|
|
85
91
|
"kumiko.list.related-list-truncated": "Showing the first {count} entries. More are available but not loaded — this list does not paginate.",
|
|
86
92
|
"kumiko.reference.system-tenant": "System",
|
|
87
93
|
"kumiko.reference.system-user": "System",
|
|
94
|
+
"kumiko.reference.unknown-user": "Unknown",
|
|
88
95
|
"kumiko.pager.status": "{from}–{to} of {total}",
|
|
89
96
|
"kumiko.pager.pageOf": "Page {page} of {pages}",
|
|
90
97
|
"kumiko.form.all-required": "All fields are required.",
|
|
@@ -155,6 +162,16 @@ export const kumikoDefaultTranslations = {
|
|
|
155
162
|
"kumiko.widget.upload.done": "Uploaded",
|
|
156
163
|
"kumiko.widget.upload.error": "Failed",
|
|
157
164
|
"kumiko.widget.upload.rejected-type": "File type not allowed",
|
|
165
|
+
"kumiko.widget.upload.too-large": "File is too large",
|
|
166
|
+
"kumiko.widget.upload.invalid-type": "File type or content not allowed",
|
|
167
|
+
"kumiko.widget.upload.max-count": "At most {count} files",
|
|
168
|
+
"kumiko.widget.upload.remove": "Remove file",
|
|
169
|
+
"kumiko.field.dropzone.title": "Drop files here or click to choose",
|
|
170
|
+
"kumiko.field.dropzone.limit-size": "up to {size} per file",
|
|
171
|
+
"kumiko.field.dropzone.limit-count": "up to {count} files",
|
|
172
|
+
"kumiko.widget.pdf-preview.loading": "Loading PDF…",
|
|
173
|
+
"kumiko.widget.pdf-preview.pageOf": "Page {page} of {pages}",
|
|
174
|
+
"kumiko.widget.pdf-preview.renderFailed": "The PDF could not be displayed.",
|
|
158
175
|
"kumiko.widget.step-bar.done": "Done",
|
|
159
176
|
"kumiko.widget.drawer.restore": "Restore drawer width",
|
|
160
177
|
"kumiko.widget.drawer.maximize": "Maximize drawer width",
|
|
@@ -252,10 +269,17 @@ export const kumikoDefaultTranslations = {
|
|
|
252
269
|
"kumiko.validation.out-of-range": "Value out of allowed range.",
|
|
253
270
|
"errors.validation.invalid_type": "Invalid value.",
|
|
254
271
|
"errors.validation.too_small": "Too small or too short (minimum: {minimum}).",
|
|
272
|
+
"errors.validation.list_min_rows.one": "At least 1 row required.",
|
|
273
|
+
"errors.validation.list_min_rows.other": "At least {minimum} rows required.",
|
|
255
274
|
"errors.validation.too_big": "Too big or too long (maximum: {maximum}).",
|
|
256
275
|
"kumiko.validation.number.min": "Must be at least {minimum}.",
|
|
257
276
|
"kumiko.validation.number.max": "Must be {maximum} or less.",
|
|
258
277
|
"errors.validation.invalid_format": "Invalid format.",
|
|
278
|
+
"errors.validation.invalid_format.email": "Invalid email address, e.g. name@example.com.",
|
|
279
|
+
"errors.validation.invalid_format.url": "Invalid web address, e.g. https://example.com.",
|
|
280
|
+
"errors.validation.invalid_format.date": "Invalid date, e.g. 2026-10-20.",
|
|
281
|
+
"errors.validation.invalid_format.datetime": "Invalid date and time, e.g. 2026-10-20T14:30.",
|
|
282
|
+
"errors.validation.invalid_format.uuid": "Invalid ID, e.g. 123e4567-e89b-12d3-a456-426614174000.",
|
|
259
283
|
"errors.validation.not_multiple_of": "Must be a multiple of {divisor}.",
|
|
260
284
|
"errors.validation.unrecognized_keys": "Unknown fields.",
|
|
261
285
|
"errors.validation.invalid_union": "Invalid value.",
|
|
@@ -277,6 +301,7 @@ export const kumikoDefaultTranslations = {
|
|
|
277
301
|
"errors.recordLocked": "This record is locked and can no longer be changed.",
|
|
278
302
|
"errors.uniqueViolation": "This entry already exists.",
|
|
279
303
|
"errors.unprocessable": "The request could not be processed.",
|
|
304
|
+
"errors.file.violatesBindTarget": "This file does not meet the requirements of the target field.",
|
|
280
305
|
"errors.unconfigured": "This feature isn't configured yet.",
|
|
281
306
|
"errors.internal": "Something went wrong. Please try again later.",
|
|
282
307
|
"errors.rate_limited": "Too many requests. Please try again shortly.",
|
package/dist/index.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export { evalVisibleWhen, type VisibleWhenVerdict } from "./app/visible-when.ts"
|
|
|
39
39
|
export { dispatcherErrorText, WriteFailedError } from "./app/write-failed-error.ts";
|
|
40
40
|
export type { ErrorBoundaryFallbackProps, ErrorBoundaryProps, } from "./components/error-boundary.tsx";
|
|
41
41
|
export { ErrorBoundary } from "./components/error-boundary.tsx";
|
|
42
|
+
export type { FormScreenLayoutProps } from "./components/form-screen-layout.tsx";
|
|
43
|
+
export { FormScreenLayout } from "./components/form-screen-layout.tsx";
|
|
42
44
|
export { RelatedListSection } from "./components/related-list-section.tsx";
|
|
43
45
|
export type { RenderEditAction, RenderEditChangeState, RenderEditControls, RenderEditProps, } from "./components/render-edit.tsx";
|
|
44
46
|
export { RenderEdit } from "./components/render-edit.tsx";
|
|
@@ -78,14 +80,16 @@ export type { LocaleProviderProps, TranslationBundle, TranslationsByKey, Transla
|
|
|
78
80
|
export { createStaticLocaleResolver, FormalityProvider, LocaleProvider, mergeTranslations, translationsByKeyFromLocales, translationsByLocaleFromKeys, useFormality, useLocale, useOptionalLocale, useOptionalTranslation, useTranslation, } from "./i18n.tsx";
|
|
79
81
|
export { kumikoDefaultTranslations } from "./i18n-defaults.ts";
|
|
80
82
|
export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.ts";
|
|
83
|
+
export { hasUnsavedInput, registerUnsavedInputSource, requestLeave, setLeaveConfirmer, useUnsavedInputSource, } from "./leave-guard.ts";
|
|
84
|
+
export { LeaveGuardDialog } from "./leave-guard-dialog.tsx";
|
|
81
85
|
export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.ts";
|
|
82
|
-
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, InputFillAction, 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.tsx";
|
|
86
|
+
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, FileValueListProps, FillContainerProps, FooterActionMarker, FooterActionRole, FormProps, FormSectionNavItem, FormWidth, GridCellProps, GridProps, HeadingProps, InputFillAction, 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.tsx";
|
|
83
87
|
export { FOOTER_ACTION_ROLE_PROP, NARROW_LABEL_PROP, PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, useOptionalPrimitives, usePrimitives, } from "./primitives.tsx";
|
|
84
88
|
export type { SessionEndedSignal, SessionEndedSignalProviderProps, } from "./session/session-ended.tsx";
|
|
85
89
|
export { createSessionEndedSignal, isSessionEndedError, SESSION_ENDED_ERROR_CODES, SessionEndedSignalProvider, useSessionEndedSignal, withSessionEndedDetection, } from "./session/session-ended.tsx";
|
|
86
90
|
export { buildWhatsAppShareUrl } from "./share-links.ts";
|
|
87
91
|
export { sortByAccessor } from "./sort-by-accessor.ts";
|
|
88
92
|
export type { LiveEvent, LiveEventSubscriber, LiveEventsProviderProps, } from "./sse/live-events.tsx";
|
|
89
|
-
export { LiveEventsProvider, useLiveEvents } from "./sse/live-events.tsx";
|
|
93
|
+
export { LIVE_RESYNC_EVENT_TYPE, LiveEventsProvider, useLiveEvents } from "./sse/live-events.tsx";
|
|
90
94
|
export type { AppTokens, ColorTokens, CoreTokens, FontTokens, RadiusTokens, ShadowTokens, SpacingTokens, ThemeMode, ThemePreference, Tokens, TokensApi, TokensProviderProps, } from "./tokens.tsx";
|
|
91
95
|
export { cssVarTokens, TokensProvider, useTokenController, useTokens } from "./tokens.tsx";
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ export { VariableChips } from "./app/variable-chips.js";
|
|
|
32
32
|
export { evalVisibleWhen } from "./app/visible-when.js";
|
|
33
33
|
export { dispatcherErrorText, WriteFailedError } from "./app/write-failed-error.js";
|
|
34
34
|
export { ErrorBoundary } from "./components/error-boundary.js";
|
|
35
|
+
export { FormScreenLayout } from "./components/form-screen-layout.js";
|
|
35
36
|
export { RelatedListSection } from "./components/related-list-section.js";
|
|
36
37
|
export { RenderEdit } from "./components/render-edit.js";
|
|
37
38
|
export { needsActionConfirm } from "./components/render-edit-action-button.js";
|
|
@@ -56,10 +57,12 @@ export { useStreamHandler } from "./hooks/use-stream-handler.js";
|
|
|
56
57
|
export { createStaticLocaleResolver, FormalityProvider, LocaleProvider, mergeTranslations, translationsByKeyFromLocales, translationsByLocaleFromKeys, useFormality, useLocale, useOptionalLocale, useOptionalTranslation, useTranslation, } from "./i18n.js";
|
|
57
58
|
export { kumikoDefaultTranslations } from "./i18n-defaults.js";
|
|
58
59
|
export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.js";
|
|
60
|
+
export { hasUnsavedInput, registerUnsavedInputSource, requestLeave, setLeaveConfirmer, useUnsavedInputSource, } from "./leave-guard.js";
|
|
61
|
+
export { LeaveGuardDialog } from "./leave-guard-dialog.js";
|
|
59
62
|
export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.js";
|
|
60
63
|
export { FOOTER_ACTION_ROLE_PROP, NARROW_LABEL_PROP, PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, useOptionalPrimitives, usePrimitives, } from "./primitives.js";
|
|
61
64
|
export { createSessionEndedSignal, isSessionEndedError, SESSION_ENDED_ERROR_CODES, SessionEndedSignalProvider, useSessionEndedSignal, withSessionEndedDetection, } from "./session/session-ended.js";
|
|
62
65
|
export { buildWhatsAppShareUrl } from "./share-links.js";
|
|
63
66
|
export { sortByAccessor } from "./sort-by-accessor.js";
|
|
64
|
-
export { LiveEventsProvider, useLiveEvents } from "./sse/live-events.js";
|
|
67
|
+
export { LIVE_RESYNC_EVENT_TYPE, LiveEventsProvider, useLiveEvents } from "./sse/live-events.js";
|
|
65
68
|
export { cssVarTokens, TokensProvider, useTokenController, useTokens } from "./tokens.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { useTranslation } from "./i18n.js";
|
|
4
|
+
import { setLeaveConfirmer } from "./leave-guard.js";
|
|
5
|
+
import { usePrimitives } from "./primitives.js";
|
|
6
|
+
export function LeaveGuardDialog() {
|
|
7
|
+
const { Dialog } = usePrimitives();
|
|
8
|
+
const t = useTranslation();
|
|
9
|
+
const [pending, setPending] = useState(null);
|
|
10
|
+
// Dialog fires onOpenChange(false) after onConfirm too; the ref lets that
|
|
11
|
+
// follow-up see the request is already settled instead of cancelling it.
|
|
12
|
+
const pendingRef = useRef(null);
|
|
13
|
+
useEffect(() => setLeaveConfirmer((request) => {
|
|
14
|
+
pendingRef.current = request;
|
|
15
|
+
setPending(request);
|
|
16
|
+
}), []);
|
|
17
|
+
const settle = useCallback((decide) => {
|
|
18
|
+
const request = pendingRef.current;
|
|
19
|
+
pendingRef.current = null;
|
|
20
|
+
setPending(null);
|
|
21
|
+
if (request !== null)
|
|
22
|
+
decide(request);
|
|
23
|
+
}, []);
|
|
24
|
+
return (_jsx(Dialog, { open: pending !== null, onOpenChange: (open) => {
|
|
25
|
+
if (!open)
|
|
26
|
+
settle((request) => request.cancel());
|
|
27
|
+
}, title: t("kumiko.drawer.discard.title"), description: t("kumiko.drawer.discard.body"), confirmLabel: t("kumiko.drawer.discard.confirm"), cancelLabel: t("kumiko.drawer.discard.cancel"), variant: "danger", initialFocus: "cancel", onConfirm: () => settle((request) => request.proceed()), testId: "leave-guard-dialog" }));
|
|
28
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type UnsavedInputSource = () => boolean;
|
|
2
|
+
type LeaveConfirmer = (decision: {
|
|
3
|
+
readonly proceed: () => void;
|
|
4
|
+
readonly cancel: () => void;
|
|
5
|
+
}) => void;
|
|
6
|
+
export declare function registerUnsavedInputSource(source: UnsavedInputSource): () => void;
|
|
7
|
+
export declare function hasUnsavedInput(): boolean;
|
|
8
|
+
export declare function setLeaveConfirmer(next: LeaveConfirmer): () => void;
|
|
9
|
+
/** Runs `proceed` right away when nothing is dirty (or no dialog host is
|
|
10
|
+
* mounted); otherwise asks the user first. */
|
|
11
|
+
export declare function requestLeave(proceed: () => void, cancel?: () => void): void;
|
|
12
|
+
/** `isDirty` is read lazily, at guard time, so a form can report "clean"
|
|
13
|
+
* synchronously after a successful save — before React re-renders. */
|
|
14
|
+
export declare function useUnsavedInputSource(isDirty: () => boolean): void;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
const sources = new Set();
|
|
3
|
+
let confirmer;
|
|
4
|
+
export function registerUnsavedInputSource(source) {
|
|
5
|
+
sources.add(source);
|
|
6
|
+
return () => {
|
|
7
|
+
sources.delete(source);
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function hasUnsavedInput() {
|
|
11
|
+
for (const source of sources)
|
|
12
|
+
if (source())
|
|
13
|
+
return true;
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
export function setLeaveConfirmer(next) {
|
|
17
|
+
confirmer = next;
|
|
18
|
+
return () => {
|
|
19
|
+
if (confirmer === next)
|
|
20
|
+
confirmer = undefined;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Runs `proceed` right away when nothing is dirty (or no dialog host is
|
|
24
|
+
* mounted); otherwise asks the user first. */
|
|
25
|
+
export function requestLeave(proceed, cancel = () => { }) {
|
|
26
|
+
if (confirmer === undefined || !hasUnsavedInput()) {
|
|
27
|
+
proceed();
|
|
28
|
+
// skip: nothing to lose, no question needed
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
confirmer({ proceed, cancel });
|
|
32
|
+
}
|
|
33
|
+
/** `isDirty` is read lazily, at guard time, so a form can report "clean"
|
|
34
|
+
* synchronously after a successful save — before React re-renders. */
|
|
35
|
+
export function useUnsavedInputSource(isDirty) {
|
|
36
|
+
const isDirtyRef = useRef(isDirty);
|
|
37
|
+
isDirtyRef.current = isDirty;
|
|
38
|
+
useEffect(() => registerUnsavedInputSource(() => isDirtyRef.current()), []);
|
|
39
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createStore, } from "../headless/index.js";
|
|
1
|
+
import { createStore, generateRequestId, } from "../headless/index.js";
|
|
3
2
|
import { CSRF_HEADER_NAME, readCsrfToken } from "./csrf.js";
|
|
4
3
|
import { batchEnvelopeSchema, errorOnlyEnvelopeSchema, queryEnvelopeSchema, writeEnvelopeSchema, } from "./envelopes.js";
|
|
5
4
|
import { buildAbortError, buildNetworkError, mapServerError } from "./error-mapping.js";
|
|
@@ -188,20 +187,6 @@ export function createLiveDispatcher(options = {}) {
|
|
|
188
187
|
}
|
|
189
188
|
const EMPTY_PENDING_WRITES = Object.freeze([]);
|
|
190
189
|
const EMPTY_PENDING_FILES = Object.freeze([]);
|
|
191
|
-
// crypto.randomUUID where available (browser, Bun, Node); Math.random
|
|
192
|
-
// fallback for React-Native runtimes without the WebCrypto polyfill.
|
|
193
|
-
// Uniqueness only needs to hold per user within the server's dedup window —
|
|
194
|
-
// this is an idempotency key, not a security token.
|
|
195
|
-
function generateRequestId() {
|
|
196
|
-
const webCrypto = propertyOf(globalThis, "crypto");
|
|
197
|
-
const randomUUID = propertyOf(webCrypto, "randomUUID");
|
|
198
|
-
if (typeof randomUUID === "function") {
|
|
199
|
-
const id = Reflect.apply(randomUUID, webCrypto, []);
|
|
200
|
-
if (typeof id === "string")
|
|
201
|
-
return id;
|
|
202
|
-
}
|
|
203
|
-
return `req-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
|
204
|
-
}
|
|
205
190
|
function buildUnexpectedShapeError(status) {
|
|
206
191
|
// The server did answer: keep its status so 401/5xx handling still applies.
|
|
207
192
|
return { ...buildNetworkError(`unexpected response shape (${status})`), httpStatus: status };
|
package/dist/primitives.d.ts
CHANGED
|
@@ -341,6 +341,9 @@ export type InputProps = {
|
|
|
341
341
|
/** Für den Upload-POST `/api/files` — der Endpoint prüft maxSize/accept
|
|
342
342
|
* gegen die Field-Def von (entityType, fieldName). */
|
|
343
343
|
readonly entityType?: string;
|
|
344
|
+
/** Screen QN of the actionForm the field belongs to; sent as `actionForm` instead of
|
|
345
|
+
* `entityType`, so the upload stays pending for that form's field. */
|
|
346
|
+
readonly uploadActionForm?: string;
|
|
344
347
|
/** Must be sent as the `entityId` part of the upload so the FileRef is
|
|
345
348
|
* attached to the record (qr overlays and access guards need it). */
|
|
346
349
|
readonly entityId?: string;
|
|
@@ -359,6 +362,26 @@ export type InputProps = {
|
|
|
359
362
|
readonly pickLabel?: string;
|
|
360
363
|
/** Hidden file input gets `${testId}-input`, the pick button `${testId}-pick`. */
|
|
361
364
|
readonly testId?: string;
|
|
365
|
+
} | {
|
|
366
|
+
/** Drop zone for a `files` / `images` field: the value is the list of uploaded FileRef ids,
|
|
367
|
+
* the implementation offers multi-pick, drag and drop and a remove control per file. */
|
|
368
|
+
readonly kind: "files-dropzone";
|
|
369
|
+
readonly id: string;
|
|
370
|
+
readonly name: string;
|
|
371
|
+
readonly value: readonly string[];
|
|
372
|
+
readonly onChange: (fileIds: readonly string[]) => void;
|
|
373
|
+
readonly disabled?: boolean;
|
|
374
|
+
readonly required?: boolean;
|
|
375
|
+
readonly hasError?: boolean;
|
|
376
|
+
readonly accept?: readonly string[];
|
|
377
|
+
readonly maxSize?: string;
|
|
378
|
+
readonly maxCount?: number;
|
|
379
|
+
/** Upload target, same meaning as on the `file` kind. */
|
|
380
|
+
readonly entityType?: string;
|
|
381
|
+
readonly uploadActionForm?: string;
|
|
382
|
+
readonly entityId?: string;
|
|
383
|
+
readonly fieldName?: string;
|
|
384
|
+
readonly testId?: string;
|
|
362
385
|
} | {
|
|
363
386
|
readonly kind: "date";
|
|
364
387
|
readonly id: string;
|
|
@@ -824,6 +847,9 @@ export type EmbeddedListTotal = {
|
|
|
824
847
|
readonly field: string;
|
|
825
848
|
readonly label: string;
|
|
826
849
|
readonly value: number;
|
|
850
|
+
/** Set for labelled multi-column totals, which have no single column to
|
|
851
|
+
* read the money type from. */
|
|
852
|
+
readonly money?: boolean;
|
|
827
853
|
};
|
|
828
854
|
/** Row-array + totals table for a `createEmbeddedListField` (invoice-
|
|
829
855
|
* positions-style) field — structurally too different from `Input`'s
|
|
@@ -890,6 +916,9 @@ export type FormSectionNavItem = {
|
|
|
890
916
|
* end of the card (normal document flow). */
|
|
891
917
|
export type FormProps = {
|
|
892
918
|
readonly onSubmit: (e?: FormEvent) => void;
|
|
919
|
+
/** Bumped by the host each time a submit fails on field errors; the form
|
|
920
|
+
* then moves focus to its first invalid control. Native forms may ignore it. */
|
|
921
|
+
readonly focusFirstErrorKey?: number;
|
|
893
922
|
readonly children: ReactNode;
|
|
894
923
|
readonly title?: ReactNode;
|
|
895
924
|
/** Optionaler muted Untertitel unter dem Titel (z.B. "Add a new item to
|
|
@@ -1346,6 +1375,12 @@ export type JsonViewProps = {
|
|
|
1346
1375
|
readonly indent?: number;
|
|
1347
1376
|
readonly testId?: string;
|
|
1348
1377
|
};
|
|
1378
|
+
/** Read-only list of stored files (FileRef ids): name, download link and, for
|
|
1379
|
+
* PDFs, an inline preview. Used for `renderer: { format: "file" }` on a detail field. */
|
|
1380
|
+
export type FileValueListProps = {
|
|
1381
|
+
readonly fileIds: readonly string[];
|
|
1382
|
+
readonly testId?: string;
|
|
1383
|
+
};
|
|
1349
1384
|
/** Band above the tabs of a record detail: optional subtitle line plus the
|
|
1350
1385
|
* metrics as a definition list. `children` are `Metric` tiles. */
|
|
1351
1386
|
export type MetricBandProps = {
|
|
@@ -1528,6 +1563,10 @@ export type CorePrimitives = {
|
|
|
1528
1563
|
* CorePrimitives mocks in tests keep compiling — additive rollout of
|
|
1529
1564
|
* a new primitive shouldn't force every test double to grow a stub. */
|
|
1530
1565
|
readonly JsonView?: ComponentType<JsonViewProps>;
|
|
1566
|
+
/** Optional (unlike the other Core-Primitives) so existing partial
|
|
1567
|
+
* CorePrimitives mocks in tests keep compiling — additive rollout of
|
|
1568
|
+
* a new primitive shouldn't force every test double to grow a stub. */
|
|
1569
|
+
readonly FileValueList?: ComponentType<FileValueListProps>;
|
|
1531
1570
|
/** Optional (unlike the other Core-Primitives) so existing partial
|
|
1532
1571
|
* CorePrimitives mocks in tests keep compiling — additive rollout of
|
|
1533
1572
|
* a new primitive shouldn't force every test double to grow a stub. */
|
|
@@ -11,6 +11,9 @@ export type LiveEvent = {
|
|
|
11
11
|
readonly createdAt: string;
|
|
12
12
|
};
|
|
13
13
|
};
|
|
14
|
+
/** Synthetic eventType delivered to every subscribed entity after the stream
|
|
15
|
+
* reconnected: frames during the gap are lost, so consumers must refetch. */
|
|
16
|
+
export declare const LIVE_RESYNC_EVENT_TYPE = "live.resync";
|
|
14
17
|
/** Abonniert Live-Events für eine Entity (aggregateType). Returnt die
|
|
15
18
|
* Unsubscribe-Funktion. Mehrere Subscriptions parallel auf dieselbe
|
|
16
19
|
* Entity sind erlaubt — jede bekommt ihr eigenes Event. */
|
package/dist/sse/live-events.js
CHANGED
|
@@ -11,6 +11,9 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
11
11
|
// beim Native-Renderer wo die Verbindungs-Lifecycle oft nicht dem
|
|
12
12
|
// App-Lifecycle entspricht.
|
|
13
13
|
import { createContext, useContext } from "react";
|
|
14
|
+
/** Synthetic eventType delivered to every subscribed entity after the stream
|
|
15
|
+
* reconnected: frames during the gap are lost, so consumers must refetch. */
|
|
16
|
+
export const LIVE_RESYNC_EVENT_TYPE = "live.resync";
|
|
14
17
|
// Wenn kein Provider da ist, liefern wir einen No-op-Subscriber statt
|
|
15
18
|
// zu crashen. Grund: useQuery({ live: true }) kann optimistisch
|
|
16
19
|
// ausgehakt werden ohne den ganzen Baum abzureißen, wenn die Plattform
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { isOpenToAllGranted } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
3
|
import { createContext, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
4
4
|
import { createRoot } from "react-dom/client";
|
|
5
|
-
import { AppFeaturesProvider, ClientActionsProvider, ColumnRenderersProvider, ContentEditorsProvider, CustomScreensProvider, createSessionEndedSignal, DashboardBodyProvider, DispatcherProvider, DraftStorageProvider, ErrorBoundary, ExtensionSectionsProvider, hasDetailScreen, KumikoScreen, kumikoDefaultTranslations, LiveEventsProvider, LocaleProvider, mergeTranslations, NavProvider, navigateWithReturnTo, PrimitivesProvider, qualifyScreenId, ScreenMissingState, SessionEndedSignalProvider, TokensProvider, toAppSchema, translationsByLocaleFromKeys, useNav, usePrimitives, useTranslation, withSessionEndedDetection, } from "../../index.js";
|
|
5
|
+
import { AppFeaturesProvider, ClientActionsProvider, ColumnRenderersProvider, ContentEditorsProvider, CustomScreensProvider, createSessionEndedSignal, DashboardBodyProvider, DispatcherProvider, DraftStorageProvider, ErrorBoundary, ExtensionSectionsProvider, hasDetailScreen, KumikoScreen, kumikoDefaultTranslations, LeaveGuardDialog, LiveEventsProvider, LocaleProvider, mergeTranslations, NavProvider, navigateWithReturnTo, PrimitivesProvider, qualifyScreenId, ScreenMissingState, SessionEndedSignalProvider, TokensProvider, toAppSchema, translationsByLocaleFromKeys, useNav, usePrimitives, useTranslation, withSessionEndedDetection, } from "../../index.js";
|
|
6
6
|
import { createLiveDispatcher } from "../../live/index.js";
|
|
7
7
|
import { lastSegment } from "../layout/nav-tree.js";
|
|
8
8
|
import { defaultPrimitives, ScreenWidthProvider } from "../primitives/index.js";
|
|
@@ -237,7 +237,7 @@ function KumikoAppRoot(props) {
|
|
|
237
237
|
warnMissingCustomScreens(loaded, props.customScreens);
|
|
238
238
|
setApp(loaded);
|
|
239
239
|
}, schemaIsRoleProjected: isFetchMode, ...(isFetchMode && { onUnmountResetSchema: () => setApp(undefined) }), ...(props.translate !== undefined && { translate: props.translate }), ...(props.onRowClick !== undefined && { onRowClick: props.onRowClick }), ...(props.shell !== undefined && { shell: props.shell }) }));
|
|
240
|
-
return (_jsx(TokensBoot, { children: _jsxs(LocaleProvider, { resolver: props.localeResolver, fallbackBundles: fallbackBundles, children: [_jsx(DocumentLangSync, { resolver: props.localeResolver }), _jsx(PrimitivesProvider, { value: props.primitives, children: _jsx(ScreenWidthProvider, { width: props.screenWidth, children: _jsx(AppFeaturesProvider, { features: app?.features ?? EMPTY_FEATURES, children: _jsx(DispatcherProvider, { dispatcher: props.dispatcher, children: _jsx(DraftStorageProvider, { value: props.draftStorage, children: _jsx(LiveEventsProvider, { value: props.liveEvents, children: _jsx(DashboardBodyProvider, { value: WebDashboardBody, children: _jsx(CustomScreensProvider, { value: props.customScreens, children: _jsx(ColumnRenderersProvider, { value: props.columnRenderers, children: _jsx(ContentEditorsProvider, { value: props.contentEditors, children: _jsx(ExtensionSectionsProvider, { value: props.extensionSectionComponents, children: _jsx(ClientActionsProvider, { value: props.clientActions, children: _jsx(NavProvidersProvider, { value: navProviders, entities: navEntities, children: _jsx(ResolversProvider, { resolvers: props.resolvers, children: _jsx(ToastProvider, { children: _jsxs(ToastActionNotifier, { children: [_jsx(UpdateChecker, {}), _jsx(SessionEndedSignalProvider, { signal: props.sessionEndedSignal, children: stackWrappers(props.providers, stackWrappers(props.gates, screenNode)) })] }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) })] }) }));
|
|
240
|
+
return (_jsx(TokensBoot, { children: _jsxs(LocaleProvider, { resolver: props.localeResolver, fallbackBundles: fallbackBundles, children: [_jsx(DocumentLangSync, { resolver: props.localeResolver }), _jsx(PrimitivesProvider, { value: props.primitives, children: _jsx(ScreenWidthProvider, { width: props.screenWidth, children: _jsx(AppFeaturesProvider, { features: app?.features ?? EMPTY_FEATURES, children: _jsx(DispatcherProvider, { dispatcher: props.dispatcher, children: _jsx(DraftStorageProvider, { value: props.draftStorage, children: _jsx(LiveEventsProvider, { value: props.liveEvents, children: _jsx(DashboardBodyProvider, { value: WebDashboardBody, children: _jsx(CustomScreensProvider, { value: props.customScreens, children: _jsx(ColumnRenderersProvider, { value: props.columnRenderers, children: _jsx(ContentEditorsProvider, { value: props.contentEditors, children: _jsx(ExtensionSectionsProvider, { value: props.extensionSectionComponents, children: _jsx(ClientActionsProvider, { value: props.clientActions, children: _jsx(NavProvidersProvider, { value: navProviders, entities: navEntities, children: _jsx(ResolversProvider, { resolvers: props.resolvers, children: _jsx(ToastProvider, { children: _jsxs(ToastActionNotifier, { children: [_jsx(UpdateChecker, {}), _jsx(LeaveGuardDialog, {}), _jsx(SessionEndedSignalProvider, { signal: props.sessionEndedSignal, children: stackWrappers(props.providers, stackWrappers(props.gates, screenNode)) })] }) }) }) }) }) }) }) }) }) }) }) }) }) }) }) })] }) }));
|
|
241
241
|
}
|
|
242
242
|
// Sits INSIDE the clientFeature gates (e.g. an auth gate) — a gate that
|
|
243
243
|
// renders a login placeholder instead of children keeps this component
|
|
@@ -4,7 +4,7 @@ import { DASHBOARD_STAT_TONES } from "@cosmicdrift/kumiko-types/screen";
|
|
|
4
4
|
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
5
5
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
6
6
|
import { currencyDecimals, EMPTY_VALUE_PLACEHOLDER } from "../../headless/index.js";
|
|
7
|
-
import { dispatcherErrorText, EmbeddedScreenProvider, evalVisibleWhen, extensionSectionName, HeaderActionsBar, KumikoScreen, useEmbeddedScreen, useExtensionSectionComponent, useLocale, useNav, usePageHeaderCompact, usePageHeaderSlotAvailable, usePrimitives, useQuery, useRecordActions, useTranslation, } from "../../index.js";
|
|
7
|
+
import { dispatcherErrorText, EmbeddedScreenProvider, evalVisibleWhen, extensionSectionName, HeaderActionsBar, KumikoScreen, lastSegment, useEmbeddedScreen, useExtensionSectionComponent, useLocale, useNav, usePageHeaderCompact, usePageHeaderSlotAvailable, usePrimitives, useQuery, useRecordActions, useTranslation, useUserRoles, } from "../../index.js";
|
|
8
8
|
import { cn } from "../lib/cn.js";
|
|
9
9
|
import { EmbeddedFormProvider } from "../primitives/index.js";
|
|
10
10
|
import { PageSection } from "../primitives/layout.js";
|
|
@@ -527,8 +527,20 @@ function TextBlockView({ block, translate, }) {
|
|
|
527
527
|
return (_jsx("pre", { "data-block": "code", className: "overflow-x-auto rounded-md bg-muted p-3 text-xs", children: _jsx("code", { children: block.code }) }));
|
|
528
528
|
case "hint":
|
|
529
529
|
return (_jsx("p", { "data-block": "hint", className: "text-xs text-muted-foreground", children: translate(block.text) }));
|
|
530
|
+
case "link":
|
|
531
|
+
return _jsx(TextBlockLink, { block: block, translate: translate });
|
|
530
532
|
}
|
|
531
533
|
}
|
|
534
|
+
function TextBlockLink({ block, translate, }) {
|
|
535
|
+
const { Link } = usePrimitives();
|
|
536
|
+
const nav = useNav();
|
|
537
|
+
const userRoles = useUserRoles();
|
|
538
|
+
if (block.roles !== undefined && !block.roles.some((role) => userRoles?.includes(role))) {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
const target = { screenId: lastSegment(block.screen) };
|
|
542
|
+
return (_jsx(Link, { href: nav.hrefFor(target), onPress: () => nav.navigate(target), testId: `dashboard-text-link-${target.screenId}`, children: translate(block.text) }));
|
|
543
|
+
}
|
|
532
544
|
function textBlockKey(block) {
|
|
533
545
|
switch (block.kind) {
|
|
534
546
|
case "list":
|
package/dist/web/app/nav.js
CHANGED
|
@@ -11,7 +11,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
11
11
|
// Event-Bus). Hand-rolled Listener-Set ist hier idiomatisch: zwei
|
|
12
12
|
// Notify-Trigger (eigenes pushPath, popstate-Event), nicht generalisierbar.
|
|
13
13
|
import { useCallback, useMemo, useSyncExternalStore, } from "react";
|
|
14
|
-
import { formatPath, parsePath, resolveTarget, useNav, } from "../../index.js";
|
|
14
|
+
import { formatPath, hasUnsavedInput, parsePath, requestLeave, resolveTarget, useNav, } from "../../index.js";
|
|
15
15
|
// basePath erlaubt es, die App unter einem URL-Prefix zu mounten
|
|
16
16
|
// (z.B. `/admin`). Read-Pfad strippt den Prefix vor dem parsePath,
|
|
17
17
|
// Write-Pfad prepend'd ihn vor dem pushState/replaceState/href.
|
|
@@ -54,6 +54,12 @@ function prependBasePath(path, basePath) {
|
|
|
54
54
|
// läuft über einen window-Listener, den wir einmalig verdrahten.
|
|
55
55
|
const listeners = new Set();
|
|
56
56
|
let popstateWired = false;
|
|
57
|
+
// URL the app currently shows; popstate has already moved the browser to the
|
|
58
|
+
// target by the time we can ask, so a declined leave must restore this one.
|
|
59
|
+
let committedUrl = "";
|
|
60
|
+
function readUrl() {
|
|
61
|
+
return `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
|
62
|
+
}
|
|
57
63
|
// Stable identity for the omitted-features case — `?? []` would create a
|
|
58
64
|
// fresh array every render, busting the useMemo below on every render for
|
|
59
65
|
// every existing useBrowserNavApi() call site (none of which pass features).
|
|
@@ -64,13 +70,35 @@ function ensurePopstateWired() {
|
|
|
64
70
|
if (typeof window === "undefined")
|
|
65
71
|
return;
|
|
66
72
|
window.addEventListener("popstate", () => {
|
|
67
|
-
|
|
68
|
-
|
|
73
|
+
if (!hasUnsavedInput()) {
|
|
74
|
+
committedUrl = readUrl();
|
|
75
|
+
for (const l of listeners)
|
|
76
|
+
l();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const targetUrl = readUrl();
|
|
80
|
+
window.history.pushState(null, "", committedUrl);
|
|
81
|
+
requestLeave(() => {
|
|
82
|
+
// replaceState, not history.back(): the declined entry is already ours
|
|
83
|
+
// and a forward navigation would otherwise land on the wrong entry.
|
|
84
|
+
window.history.replaceState(null, "", targetUrl);
|
|
85
|
+
committedUrl = targetUrl;
|
|
86
|
+
for (const l of listeners)
|
|
87
|
+
l();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
window.addEventListener("beforeunload", (event) => {
|
|
91
|
+
if (!hasUnsavedInput())
|
|
92
|
+
return;
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
event.returnValue = "";
|
|
69
95
|
});
|
|
70
96
|
popstateWired = true;
|
|
71
97
|
}
|
|
72
98
|
function subscribe(listener) {
|
|
73
99
|
ensurePopstateWired();
|
|
100
|
+
if (typeof window !== "undefined" && listeners.size === 0)
|
|
101
|
+
committedUrl = readUrl();
|
|
74
102
|
listeners.add(listener);
|
|
75
103
|
return () => {
|
|
76
104
|
listeners.delete(listener);
|
|
@@ -118,6 +146,7 @@ function applySearchParamUpdates(updates) {
|
|
|
118
146
|
return;
|
|
119
147
|
const url = `${window.location.pathname}${nextSearch}${window.location.hash}`;
|
|
120
148
|
window.history.replaceState(null, "", url);
|
|
149
|
+
committedUrl = readUrl();
|
|
121
150
|
for (const l of listeners)
|
|
122
151
|
l();
|
|
123
152
|
}
|
|
@@ -137,9 +166,12 @@ function pushPath(path, search) {
|
|
|
137
166
|
(search === undefined || window.location.search === search);
|
|
138
167
|
if (unchanged)
|
|
139
168
|
return;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
169
|
+
requestLeave(() => {
|
|
170
|
+
window.history.pushState(null, "", `${path}${search ?? ""}`);
|
|
171
|
+
committedUrl = readUrl();
|
|
172
|
+
for (const l of listeners)
|
|
173
|
+
l();
|
|
174
|
+
});
|
|
143
175
|
}
|
|
144
176
|
function replacePath(path, search) {
|
|
145
177
|
if (typeof window === "undefined")
|
|
@@ -148,9 +180,19 @@ function replacePath(path, search) {
|
|
|
148
180
|
// to avoid creating a history entry, even when the URL is identical.
|
|
149
181
|
// (pushPath skips no-op pushes; replacePath honors the call so the
|
|
150
182
|
// entry-stack semantics stay predictable.)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
183
|
+
const nextUrl = `${path}${search ?? ""}`;
|
|
184
|
+
const leavesCurrentPage = window.location.pathname !== path ||
|
|
185
|
+
(search !== undefined && window.location.search !== search);
|
|
186
|
+
const apply = () => {
|
|
187
|
+
window.history.replaceState(null, "", nextUrl);
|
|
188
|
+
committedUrl = readUrl();
|
|
189
|
+
for (const l of listeners)
|
|
190
|
+
l();
|
|
191
|
+
};
|
|
192
|
+
if (leavesCurrentPage)
|
|
193
|
+
requestLeave(apply);
|
|
194
|
+
else
|
|
195
|
+
apply();
|
|
154
196
|
}
|
|
155
197
|
/** React-Hook der eine NavApi aus der Browser-History baut. Sollte
|
|
156
198
|
* einmal im App-Root aufgerufen und als value an den shared
|
package/dist/web/index.d.ts
CHANGED
|
@@ -74,5 +74,5 @@ export { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "./ui/resiz
|
|
|
74
74
|
export { SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider } from "./ui/sidebar.tsx";
|
|
75
75
|
export type { KumikoBuild } from "./version/update-checker.tsx";
|
|
76
76
|
export { readLoadedBuild } from "./version/update-checker.tsx";
|
|
77
|
-
export type { AiTextAreaProps, AiTextFieldProps, BooleanFieldProps, ComparisonMetric, DateFieldProps, DrawerProps, FeedRow, FileFieldProps, FloatingPanelGeometry, FloatingPanelProps, InfinityListProps, InfinityListSelection, ModeSwitchVariant, NumberFieldProps, PhotoSlotSpec, PhotoSlotsProps, PlanCardActionSlot, PlanCardLinkSlot, PlanCardPrice, PlanCardProps, PlanGridProps, ProgressListRow, PublicShellProps, PublicShellVariant, QueryTableColumn, QueryTableProps, RangeFieldProps, ResultColumn, SelectFieldProps, SideBySideTableCell, SideBySideTableColumn, SideBySideTableRow, StatDelta, StatTone, StatusBadgeTone, StatusBarEntry, StatusTone, TextareaFieldProps, TextFieldProps, TimeseriesHeight, TimeseriesPoint, TimeseriesReferenceLine, TimeseriesXAxis, TimeseriesYAxis, UploadZoneProps, } from "./widgets/index.ts";
|
|
78
|
-
export { AiTextArea, AiTextField, BooleanField, CollapsibleSection, ComparisonTable, DateField, DetailList, Drawer, EmptyState, ErrorState, FeedList, FileField, FloatingPanel, InfinityList, LoadingState, MiniStat, ModeSwitch, MoneyField, NumberField, PercentField, PhotoSlots, PlanCard, PlanGrid, ProgressBar, ProgressList, PublicShell, QueryTable, RangeField, ResultPanel, ResultTable, SectionCard, SelectField, SideBySideTable, Sparkline, STATUS_TONE_TEXT, StatCard, StatusBadge, StatusBarChart, StepBar, smoothPath, TextareaField, TextField, TimeseriesChart, UploadZone, useDraft, } from "./widgets/index.ts";
|
|
77
|
+
export type { AiTextAreaProps, AiTextFieldProps, BooleanFieldProps, ComparisonMetric, DateFieldProps, DrawerProps, FeedRow, FileFieldProps, FloatingPanelGeometry, FloatingPanelProps, InfinityListProps, InfinityListSelection, ModeSwitchVariant, NumberFieldProps, PdfPreviewSource, PhotoSlotSpec, PhotoSlotsProps, PlanCardActionSlot, PlanCardLinkSlot, PlanCardPrice, PlanCardProps, PlanGridProps, ProgressListRow, PublicShellProps, PublicShellVariant, QueryTableColumn, QueryTableProps, RangeFieldProps, ResultColumn, SelectFieldProps, SideBySideTableCell, SideBySideTableColumn, SideBySideTableRow, StatDelta, StatTone, StatusBadgeTone, StatusBarEntry, StatusTone, TextareaFieldProps, TextFieldProps, TimeseriesHeight, TimeseriesPoint, TimeseriesReferenceLine, TimeseriesXAxis, TimeseriesYAxis, UploadZoneProps, } from "./widgets/index.ts";
|
|
78
|
+
export { AiTextArea, AiTextField, BooleanField, CollapsibleSection, ComparisonTable, DateField, DetailList, Drawer, EmptyState, ErrorState, FeedList, FileField, FloatingPanel, InfinityList, LoadingState, MiniStat, ModeSwitch, MoneyField, NumberField, PdfPreview, PercentField, PhotoSlots, PlanCard, PlanGrid, ProgressBar, ProgressList, PublicShell, QueryTable, RangeField, ResultPanel, ResultTable, SectionCard, SelectField, SideBySideTable, Sparkline, STATUS_TONE_TEXT, StatCard, StatusBadge, StatusBarChart, StepBar, smoothPath, TextareaField, TextField, TimeseriesChart, UploadZone, useDraft, } from "./widgets/index.ts";
|
package/dist/web/index.js
CHANGED
|
@@ -50,4 +50,4 @@ export { applyTokensToCssVars, defaultTokens, lightTokens, useBrowserTokensApi,
|
|
|
50
50
|
export { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "./ui/resizable.js";
|
|
51
51
|
export { SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarProvider } from "./ui/sidebar.js";
|
|
52
52
|
export { readLoadedBuild } from "./version/update-checker.js";
|
|
53
|
-
export { AiTextArea, AiTextField, BooleanField, CollapsibleSection, ComparisonTable, DateField, DetailList, Drawer, EmptyState, ErrorState, FeedList, FileField, FloatingPanel, InfinityList, LoadingState, MiniStat, ModeSwitch, MoneyField, NumberField, PercentField, PhotoSlots, PlanCard, PlanGrid, ProgressBar, ProgressList, PublicShell, QueryTable, RangeField, ResultPanel, ResultTable, SectionCard, SelectField, SideBySideTable, Sparkline, STATUS_TONE_TEXT, StatCard, StatusBadge, StatusBarChart, StepBar, smoothPath, TextareaField, TextField, TimeseriesChart, UploadZone, useDraft, } from "./widgets/index.js";
|
|
53
|
+
export { AiTextArea, AiTextField, BooleanField, CollapsibleSection, ComparisonTable, DateField, DetailList, Drawer, EmptyState, ErrorState, FeedList, FileField, FloatingPanel, InfinityList, LoadingState, MiniStat, ModeSwitch, MoneyField, NumberField, PdfPreview, PercentField, PhotoSlots, PlanCard, PlanGrid, ProgressBar, ProgressList, PublicShell, QueryTable, RangeField, ResultPanel, ResultTable, SectionCard, SelectField, SideBySideTable, Sparkline, STATUS_TONE_TEXT, StatCard, StatusBadge, StatusBarChart, StepBar, smoothPath, TextareaField, TextField, TimeseriesChart, UploadZone, useDraft, } from "./widgets/index.js";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ChevronDown, ChevronRight, Plus, Search } from "lucide-react";
|
|
3
|
-
import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
4
4
|
import { resolveNavigation } from "../../headless/index.js";
|
|
5
|
+
import { createLiveRefetchScheduler } from "../../hooks/live-refetch-scheduler.js";
|
|
5
6
|
import { lastSegment, toAppSchema, useLiveEvents, useNav, useTranslation } from "../../index.js";
|
|
6
7
|
import { KumikoLink } from "../app/nav.js";
|
|
7
8
|
import { useNavEntities, useNavProviders } from "../app/nav-providers-context.js";
|
|
@@ -205,13 +206,20 @@ function useNavProviderChildren(qn, enabled) {
|
|
|
205
206
|
const [raw, setRaw] = useState(null);
|
|
206
207
|
const [error, setError] = useState(null);
|
|
207
208
|
const [attempt, setAttempt] = useState(0);
|
|
209
|
+
// A live refresh keeps the current nodes on screen instead of blanking them.
|
|
210
|
+
const keepNodesOnRefire = useRef(false);
|
|
208
211
|
useEffect(() => {
|
|
209
212
|
if (!enabled || entities.length === 0)
|
|
210
213
|
return;
|
|
211
|
-
const
|
|
214
|
+
const scheduler = createLiveRefetchScheduler(() => {
|
|
215
|
+
keepNodesOnRefire.current = true;
|
|
216
|
+
setAttempt((n) => n + 1);
|
|
217
|
+
});
|
|
218
|
+
const unsubs = entities.map((e) => subscribeLive(e, scheduler.trigger));
|
|
212
219
|
return () => {
|
|
213
220
|
for (const u of unsubs)
|
|
214
221
|
u();
|
|
222
|
+
scheduler.dispose();
|
|
215
223
|
};
|
|
216
224
|
}, [enabled, entities, subscribeLive]);
|
|
217
225
|
// `attempt` triggert SSE-Refresh + erneutes Subscribe — Biome sieht es
|
|
@@ -228,7 +236,9 @@ function useNavProviderChildren(qn, enabled) {
|
|
|
228
236
|
return;
|
|
229
237
|
}
|
|
230
238
|
setError(null);
|
|
231
|
-
|
|
239
|
+
if (!keepNodesOnRefire.current)
|
|
240
|
+
setRaw(null);
|
|
241
|
+
keepNodesOnRefire.current = false;
|
|
232
242
|
try {
|
|
233
243
|
const subscribe = provider();
|
|
234
244
|
try {
|
|
@@ -61,13 +61,13 @@ export function ShellHeader({ schema, user, headerActions, }) {
|
|
|
61
61
|
: crumbs !== undefined && recordTitle !== undefined && crumbs.length > 0
|
|
62
62
|
? [...crumbs.slice(0, -1), { label: recordTitle }, ...crumbs.slice(-1)]
|
|
63
63
|
: crumbs;
|
|
64
|
-
return (_jsxs("header", { "data-kumiko-layout": "shell-header", className: cn("flex h-14 shrink-0 items-center gap-3 border-b border-border pl-1.5 pr-3 md:pl-4 md:pr-6", compact && "relative"), children: [_jsxs("div", { className:
|
|
64
|
+
return (_jsxs("header", { "data-kumiko-layout": "shell-header", className: cn("flex h-14 shrink-0 items-center gap-3 border-b border-border pl-1.5 pr-3 md:pl-4 md:pr-6", compact && "relative"), children: [_jsxs("div", { className: "@container flex min-w-0 flex-1 items-center gap-3", children: [_jsx(SidebarTrigger, { label: t("kumiko.sidebar.toggle") }), shownCrumbs !== undefined && shownCrumbs.length > 0 && (_jsx(Breadcrumb, { className: "min-w-0", "aria-label": t("kumiko.breadcrumb.label"), children: _jsx(BreadcrumbList, { className: "flex-nowrap gap-1.5 sm:gap-1.5", children: shownCrumbs.map((crumb, index) => {
|
|
65
65
|
const screenId = crumb.screenId;
|
|
66
66
|
const isLast = index === shownCrumbs.length - 1;
|
|
67
|
-
return (_jsxs(Fragment, { children: [index > 0 && (_jsx(BreadcrumbSeparator, { className: "hidden text-muted-foreground
|
|
67
|
+
return (_jsxs(Fragment, { children: [index > 0 && (_jsx(BreadcrumbSeparator, { className: "hidden text-muted-foreground @xs:inline-flex [&>svg]:size-3.5" })), _jsx(BreadcrumbItem, { className: cn(isLast ? "min-w-0" : "hidden shrink-0 @xs:inline-flex"), children: screenId !== undefined && !isLast ? (_jsx(BreadcrumbLink, { href: "#", className: "whitespace-nowrap text-foreground-secondary", onClick: (e) => {
|
|
68
68
|
e.preventDefault();
|
|
69
69
|
nav.navigate({ screenId });
|
|
70
|
-
}, children: crumb.label })) : isLast ? (_jsx("h1", { "aria-current": "page", className: "truncate text-lg font-semibold text-foreground", children: titleOverride ?? crumb.label })) : (_jsx(BreadcrumbPage, { className: "whitespace-nowrap text-foreground-secondary", children: crumb.label })) })] }, screenId ?? crumb.label));
|
|
70
|
+
}, children: crumb.label })) : isLast ? (_jsx("h1", { "aria-current": "page", title: titleOverride ?? crumb.label, className: "truncate text-lg font-semibold text-foreground", children: titleOverride ?? crumb.label })) : (_jsx(BreadcrumbPage, { className: "whitespace-nowrap text-foreground-secondary", children: crumb.label })) })] }, screenId ?? crumb.label));
|
|
71
71
|
}) }) })), _jsx("div", { ref: setStatusElement, "data-kumiko-layout": "page-header-status", className: cn("flex items-center gap-2 empty:hidden", compact ? "min-w-0 overflow-hidden" : "shrink-0") })] }), _jsxs("div", { className: cn("ml-auto flex items-center gap-2", compact ? "min-w-0 max-w-[60%]" : "min-w-0 max-w-[50%] shrink-0 sm:max-w-none"), children: [_jsx("div", { ref: setActionsElement, "data-kumiko-layout": "page-header-actions", className: cn("flex min-w-0 items-center gap-2 empty:hidden [&_*]:min-w-0 [&_*]:max-w-full [&_button]:overflow-hidden [&_button]:whitespace-nowrap", compact && "overflow-hidden") }), !compact && headerActions !== undefined && (_jsx("div", { "data-kumiko-layout": "header-actions", className: "flex items-center gap-2", children: headerActions })), hasOverflow && _jsx(HeaderOverflow, { items: overflowItems, headerActions: headerActions })] })] }));
|
|
72
72
|
}
|
|
73
73
|
// A hand-rolled menu instead of Radix DropdownMenu: app headerActions are opaque
|