@cosmicdrift/kumiko-renderer 0.398.0 → 0.400.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 (84) hide show
  1. package/dist/app/entity-edit-prefill.js +7 -1
  2. package/dist/app/entity-list-facet-specs.js +2 -0
  3. package/dist/app/entity-list-screen.js +3 -3
  4. package/dist/app/form-schema.js +42 -17
  5. package/dist/app/list-facets.d.ts +7 -4
  6. package/dist/app/list-facets.js +23 -6
  7. package/dist/app/projection-detail-body.js +7 -0
  8. package/dist/app/projection-detail-header-model.d.ts +1 -1
  9. package/dist/app/projection-detail-header-model.js +2 -2
  10. package/dist/app/projection-detail-header-region.d.ts +1 -0
  11. package/dist/app/projection-detail-header-region.js +1 -1
  12. package/dist/app/projection-detail-header.js +4 -2
  13. package/dist/app/projection-detail-navigation.d.ts +1 -0
  14. package/dist/app/projection-detail-navigation.js +9 -1
  15. package/dist/app/projection-list-body.js +1 -1
  16. package/dist/app/reference-facet-bridge.d.ts +14 -2
  17. package/dist/app/reference-facet-bridge.js +37 -18
  18. package/dist/app/use-projection-list-query.d.ts +2 -2
  19. package/dist/app/use-projection-list-query.js +2 -1
  20. package/dist/app/use-related-list-tab-counts.d.ts +5 -0
  21. package/dist/app/use-related-list-tab-counts.js +68 -0
  22. package/dist/components/embedded-list-field.js +69 -91
  23. package/dist/components/embedded-list-paste.d.ts +2 -0
  24. package/dist/components/embedded-list-paste.js +73 -0
  25. package/dist/components/related-list-section.js +7 -11
  26. package/dist/components/render-edit-fields-section.js +1 -1
  27. package/dist/components/render-edit-helpers.js +2 -2
  28. package/dist/components/render-field-choice-inputs.js +1 -1
  29. package/dist/data-table-facet-types.d.ts +5 -0
  30. package/dist/embedded-list-types.d.ts +117 -0
  31. package/dist/embedded-list-types.js +1 -0
  32. package/dist/headless/form/form-controller.js +9 -1
  33. package/dist/headless/index.d.ts +1 -1
  34. package/dist/headless/index.js +1 -1
  35. package/dist/headless/view-model/edit.js +40 -50
  36. package/dist/headless/view-model/embedded-list-cells.d.ts +12 -0
  37. package/dist/headless/view-model/embedded-list-cells.js +49 -0
  38. package/dist/headless/view-model/embedded-list.d.ts +1 -1
  39. package/dist/headless/view-model/embedded-list.js +1 -1
  40. package/dist/headless/view-model/index.d.ts +1 -1
  41. package/dist/headless/view-model/index.js +1 -1
  42. package/dist/headless/view-model/list.js +2 -3
  43. package/dist/headless/view-model/types.d.ts +23 -6
  44. package/dist/hooks/related-list-parent-payload.d.ts +2 -0
  45. package/dist/hooks/related-list-parent-payload.js +7 -0
  46. package/dist/i18n-defaults.js +11 -0
  47. package/dist/primitives.d.ts +4 -88
  48. package/dist/web/layout/notification-bell.d.ts +5 -0
  49. package/dist/web/layout/notification-bell.js +39 -0
  50. package/dist/web/layout/shell-header.js +4 -1
  51. package/dist/web/layout/use-in-app-inbox.d.ts +15 -0
  52. package/dist/web/layout/use-in-app-inbox.js +61 -0
  53. package/dist/web/lib/file-upload-form-data.d.ts +2 -0
  54. package/dist/web/lib/file-upload-form-data.js +2 -0
  55. package/dist/web/lib/upload-file-ref.d.ts +1 -1
  56. package/dist/web/primitives/data-table-cell.d.ts +2 -0
  57. package/dist/web/primitives/data-table-cell.js +1 -1
  58. package/dist/web/primitives/embedded-file-cell.d.ts +11 -0
  59. package/dist/web/primitives/embedded-file-cell.js +50 -0
  60. package/dist/web/primitives/embedded-list-cards.d.ts +38 -0
  61. package/dist/web/primitives/embedded-list-cards.js +71 -0
  62. package/dist/web/primitives/embedded-list-cell.d.ts +41 -0
  63. package/dist/web/primitives/embedded-list-cell.js +132 -0
  64. package/dist/web/primitives/embedded-list-input.d.ts +1 -1
  65. package/dist/web/primitives/embedded-list-input.js +29 -142
  66. package/dist/web/primitives/facet-filter.js +4 -0
  67. package/dist/web/primitives/facet-search-filter.d.ts +9 -0
  68. package/dist/web/primitives/facet-search-filter.js +31 -0
  69. package/dist/web/primitives/file-upload.d.ts +2 -1
  70. package/dist/web/primitives/file-upload.js +2 -1
  71. package/dist/web/primitives/input-choice-kinds.js +1 -1
  72. package/dist/web/primitives/input-optional-props.d.ts +2 -1
  73. package/dist/web/primitives/input-optional-props.js +2 -1
  74. package/dist/web/primitives/locale-number.d.ts +3 -0
  75. package/dist/web/primitives/locale-number.js +8 -0
  76. package/dist/web/primitives/number-input.d.ts +2 -1
  77. package/dist/web/primitives/number-input.js +4 -11
  78. package/dist/web/primitives/segmented-select.d.ts +6 -1
  79. package/dist/web/primitives/segmented-select.js +8 -4
  80. package/dist/web/sse/live-events.js +14 -3
  81. package/dist/web/widgets/status-badge.d.ts +1 -0
  82. package/dist/web/widgets/status-badge.js +1 -1
  83. package/package.json +5 -5
  84. package/src/changes.json +18 -0
@@ -1,5 +1,6 @@
1
- import { availableSelectOptions, evalFieldCondition, isExtensionEditSection, isFieldsEditSection, isFormDerivationInputMode, isUnusedFallbackOperand, isWriteFormEditSection, normalizeEditField, parseRefTarget, sectionFieldSpecs, WRITE_FORM_SECTION_ENTITY, } from "@cosmicdrift/kumiko-framework/ui-types";
2
- import { buildOptionLabels, embeddedCellLabelKey, embeddedCellOptionLabelKey, fieldLabelKey, fieldOptionLabelKey, } from "./list.js";
1
+ import { availableSelectOptions, evalFieldCondition, isExtensionEditSection, isFieldRequired, isFieldsEditSection, isFieldVisible, isFormDerivationInputMode, isUnusedFallbackOperand, isWriteFormEditSection, normalizeEditField, parseRefTarget, sectionFieldSpecs, WRITE_FORM_SECTION_ENTITY, } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import { buildEmbeddedListCell } from "./embedded-list-cells.js";
3
+ import { buildOptionLabels, fieldLabelKey, fieldOptionLabelKey } from "./list.js";
3
4
  // Resolves a writeForm section's own fieldDefs/fields through the same
4
5
  // per-field pipeline as a plain fields section, by reusing the pipeline
5
6
  // itself rather than duplicating it: wrap fieldDefs/fields as a throwaway
@@ -161,15 +162,22 @@ function selectableOptions(fieldDef, fieldName, values) {
161
162
  const current = values[fieldName];
162
163
  return fieldDef.options.filter((option) => available.includes(option) || option === current);
163
164
  }
165
+ function deriveSelectDisplay(fieldDef) {
166
+ const declaredDisplay = fieldDef.type === "multiSelect" || fieldDef.type === "select" ? fieldDef.display : undefined;
167
+ // The renderer's heuristic counts options, so a filtered list would switch widgets with the discriminator.
168
+ const display = declaredDisplay ??
169
+ (fieldDef.type === "select" && fieldDef.conditionalOptions ? "dropdown" : undefined);
170
+ return {
171
+ ...(display !== undefined && { display }),
172
+ ...(fieldDef.type === "select" &&
173
+ fieldDef.optionTones !== undefined && { optionTones: fieldDef.optionTones }),
174
+ };
175
+ }
164
176
  function deriveSelectFieldHints(fieldDef, translate, featureName, entityName, fieldName, values) {
165
177
  const options = selectableOptions(fieldDef, fieldName, values);
166
178
  const optionLabels = options !== undefined
167
179
  ? buildOptionLabels(translate, (value) => fieldOptionLabelKey(featureName, entityName, fieldName, value), options)
168
180
  : undefined;
169
- const declaredDisplay = fieldDef.type === "multiSelect" || fieldDef.type === "select" ? fieldDef.display : undefined;
170
- // The renderer's heuristic counts options, so a filtered list would switch widgets with the discriminator.
171
- const display = declaredDisplay ??
172
- (fieldDef.type === "select" && fieldDef.conditionalOptions ? "dropdown" : undefined);
173
181
  const columns = fieldDef.type === "multiSelect" ? fieldDef.columns : undefined;
174
182
  const maxRows = fieldDef.type === "multiSelect" ? fieldDef.maxRows : undefined;
175
183
  return {
@@ -177,7 +185,7 @@ function deriveSelectFieldHints(fieldDef, translate, featureName, entityName, fi
177
185
  ...deriveSelectOptionsAvailabilityQuery(fieldDef),
178
186
  ...(options !== undefined && { options }),
179
187
  ...(optionLabels !== undefined && { optionLabels }),
180
- ...(display !== undefined && { display }),
188
+ ...deriveSelectDisplay(fieldDef),
181
189
  ...(columns !== undefined && { columns }),
182
190
  ...(maxRows !== undefined && { maxRows }),
183
191
  };
@@ -195,10 +203,6 @@ function deriveTextFieldHints(fieldDef) {
195
203
  ...("sensitive" in fieldDef && fieldDef.sensitive === true && { sensitive: true }),
196
204
  };
197
205
  }
198
- // files/images field defs carry no `required` property.
199
- function isRequiredByEntity(fieldDef) {
200
- return "required" in fieldDef && fieldDef.required === true;
201
- }
202
206
  function deriveDateFieldHints(fieldDef) {
203
207
  const wallClock = fieldDef.type === "timestamp" && fieldDef.locatedBy !== undefined ? true : undefined;
204
208
  const dateBounds = fieldDef.type === "date" ||
@@ -287,48 +291,16 @@ function deriveFileFieldHints(fieldDef, entityName, fieldName, entityId, actionF
287
291
  ...deriveImageHints(fieldDef),
288
292
  };
289
293
  }
290
- function deriveEmbeddedCellSelectHints(subField, translate, featureName, entityName, fieldName, subFieldName) {
291
- if (subField.type !== "select")
292
- return {};
293
- const options = subField.options ?? [];
294
- return {
295
- options,
296
- optionLabels: buildOptionLabels(translate, (value) => embeddedCellOptionLabelKey(featureName, entityName, fieldName, subFieldName, value), options),
297
- };
298
- }
299
- function deriveEmbeddedCellReferenceHints(subField, featureName) {
300
- if (subField.type !== "reference")
301
- return {};
302
- const target = subField.entity !== undefined ? parseRefTarget(subField.entity, featureName) : undefined;
303
- return {
304
- ...(target !== undefined && { refEntity: target.entityName, refFeature: target.featureName }),
305
- ...(subField.labelField !== undefined && { refLabelField: subField.labelField }),
306
- ...(subField.optionsQuery !== undefined && { refOptionsQuery: subField.optionsQuery }),
307
- };
308
- }
309
- function buildEmbeddedListCell(subFieldName, subField, translate, featureName, entityName, fieldName, listSpec) {
310
- return {
311
- field: subFieldName,
312
- label: translate(embeddedCellLabelKey(featureName, entityName, fieldName, subFieldName)),
313
- type: subField.type,
314
- required: subField.required === true,
315
- ...deriveEmbeddedCellSelectHints(subField, translate, featureName, entityName, fieldName, subFieldName),
316
- ...deriveEmbeddedCellReferenceHints(subField, featureName),
317
- ...(subField.type === "decimal" && subField.scale !== undefined && { scale: subField.scale }),
318
- ...(subField.type === "select" &&
319
- subField.display !== undefined && { display: subField.display }),
320
- ...(listSpec?.readOnlyCells?.includes(subFieldName) === true && { readOnly: true }),
321
- };
322
- }
323
294
  // Embedded-LIST field (`multiple: true`) — per-cell metadata for a renderer
324
295
  // to draw one row per array item (invoice-positions-style table). A plain
325
296
  // (non-list) embedded field emits none of this; the renderer tells the two
326
297
  // apart by whether embeddedListCells is set, not by `type` (which stays
327
298
  // "embedded" either way).
328
- function deriveEmbeddedListHints(fieldDef, translate, featureName, entityName, fieldName, resolvedCurrency, listSpec) {
299
+ function deriveEmbeddedListHints(fieldDef, translate, featureName, entityName, fieldName, resolvedCurrency, listSpec, entityId) {
329
300
  const embeddedListDef = fieldDef.type === "embedded" && fieldDef.multiple === true ? fieldDef : undefined;
301
+ const cellContext = { translate, featureName, entityName, fieldName, listSpec };
330
302
  const embeddedListCells = embeddedListDef !== undefined
331
- ? Object.entries(embeddedListDef.schema).map(([subFieldName, subField]) => buildEmbeddedListCell(subFieldName, subField, translate, featureName, entityName, fieldName, listSpec))
303
+ ? Object.entries(embeddedListDef.schema).map(([subFieldName, subField]) => buildEmbeddedListCell(subFieldName, subField, cellContext))
332
304
  : undefined;
333
305
  return {
334
306
  ...(embeddedListCells !== undefined && { embeddedListCells }),
@@ -349,6 +321,14 @@ function deriveEmbeddedListHints(fieldDef, translate, featureName, entityName, f
349
321
  ...(embeddedListDef !== undefined && { embeddedListCurrency: resolvedCurrency }),
350
322
  ...(embeddedListDef !== undefined &&
351
323
  listSpec?.fixedRows === true && { embeddedListFixedRows: true }),
324
+ ...(embeddedListCells?.some((cell) => cell.type === "images" || cell.type === "files") ===
325
+ true && {
326
+ embeddedListUploadTarget: {
327
+ entityType: entityName,
328
+ fieldName,
329
+ ...(entityId !== undefined && { entityId }),
330
+ },
331
+ }),
352
332
  };
353
333
  }
354
334
  function buildExtensionSectionViewModel(sectionSpec, translate) {
@@ -378,6 +358,17 @@ function isLockedByDerivation(derivation, values, field) {
378
358
  !isFormDerivationInputMode(derivation, values, field);
379
359
  return isDerivedAndLocked || isUnusedFallbackOperand(derivation, values, field);
380
360
  }
361
+ // A screen-spec condition overrides the entity's own visibleWhen / required / requiredWhen.
362
+ function isEditFieldVisible(normalized, fieldDef, values) {
363
+ return normalized.visible !== undefined
364
+ ? evalFieldCondition(normalized.visible, values)
365
+ : isFieldVisible(fieldDef, values);
366
+ }
367
+ function isEditFieldRequired(normalized, fieldDef, values) {
368
+ return normalized.required !== undefined
369
+ ? evalFieldCondition(normalized.required, values)
370
+ : isFieldRequired(fieldDef, values);
371
+ }
381
372
  export function computeEditViewModel(input) {
382
373
  const { screen, entity, values, translate, featureName, mode, entityId, derivation, actionFormScreenQn, } = input;
383
374
  const excludedFields = new Set(mode === undefined ? [] : screen.writeExcludedFields?.[mode]);
@@ -419,7 +410,7 @@ export function computeEditViewModel(input) {
419
410
  const effectiveType = declaredRefTarget !== undefined ? "reference" : fieldDef.type;
420
411
  const label = translate(screen.fieldLabels?.[normalized.field] ??
421
412
  fieldLabelKey(featureName, screen.entity, normalized.field));
422
- const visible = evalCondition(normalized.visible, true, values);
413
+ const visible = isEditFieldVisible(normalized, fieldDef, values);
423
414
  // `readOnly` (camelCase) is the name on both sides: EditFieldSpec
424
415
  // in the engine, and the view-model emitted here. One convention
425
416
  // through the stack beats translating at the boundary.
@@ -431,8 +422,7 @@ export function computeEditViewModel(input) {
431
422
  // field that's required at the entity-level but marked required:
432
423
  // false on the screen (e.g. a soft-onboarding wizard that
433
424
  // collects less up-front) respects the screen override.
434
- const entityRequired = isRequiredByEntity(fieldDef);
435
- const required = !lockedByDerivation && evalCondition(normalized.required, entityRequired, values);
425
+ const required = !lockedByDerivation && isEditFieldRequired(normalized, fieldDef, values);
436
426
  // ponytail: "EUR" mirrors DEFAULT_CURRENCIES[0] from
437
427
  // framework/src/engine/field-helpers.ts — headless has no dependency
438
428
  // on that module, so the literal is duplicated here instead of
@@ -444,7 +434,7 @@ export function computeEditViewModel(input) {
444
434
  const numericHints = deriveNumericFieldHints(fieldDef, resolvedCurrency);
445
435
  const referenceHints = deriveReferenceFieldHints(fieldDef, normalized, declaredRefTarget, featureName);
446
436
  const fileHints = deriveFileFieldHints(fieldDef, screen.entity, normalized.field, entityId, actionFormScreenQn);
447
- const embeddedListHints = deriveEmbeddedListHints(fieldDef, translate, featureName, screen.entity, normalized.field, resolvedCurrency, normalized.embeddedList);
437
+ const embeddedListHints = deriveEmbeddedListHints(fieldDef, translate, featureName, screen.entity, normalized.field, resolvedCurrency, normalized.embeddedList, entityId);
448
438
  const view = {
449
439
  field: normalized.field,
450
440
  label,
@@ -0,0 +1,12 @@
1
+ import type { EmbeddedListEditSpec } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import type { EmbeddedSubFieldDef } from "@cosmicdrift/kumiko-types/fields";
3
+ import type { EmbeddedListCellViewModel, Translate } from "./types.ts";
4
+ type CellContext = {
5
+ readonly translate: Translate;
6
+ readonly featureName: string;
7
+ readonly entityName: string;
8
+ readonly fieldName: string;
9
+ readonly listSpec: EmbeddedListEditSpec | undefined;
10
+ };
11
+ export declare function buildEmbeddedListCell(subFieldName: string, subField: EmbeddedSubFieldDef, ctx: CellContext): EmbeddedListCellViewModel;
12
+ export {};
@@ -0,0 +1,49 @@
1
+ import { parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import { buildOptionLabels, embeddedCellLabelKey, embeddedCellOptionLabelKey } from "./list.js";
3
+ function selectHints(subField, subFieldName, ctx) {
4
+ if (subField.type !== "select")
5
+ return {};
6
+ return {
7
+ options: subField.options,
8
+ optionLabels: buildOptionLabels(ctx.translate, (value) => embeddedCellOptionLabelKey(ctx.featureName, ctx.entityName, ctx.fieldName, subFieldName, value), subField.options),
9
+ ...(subField.display !== undefined && { display: subField.display }),
10
+ ...(subField.optionTones !== undefined && { optionTones: subField.optionTones }),
11
+ };
12
+ }
13
+ function referenceHints(subField, featureName) {
14
+ if (subField.type !== "reference")
15
+ return {};
16
+ const target = parseRefTarget(subField.entity, featureName);
17
+ return {
18
+ refEntity: target.entityName,
19
+ refFeature: target.featureName,
20
+ ...(subField.labelField !== undefined && { refLabelField: subField.labelField }),
21
+ ...(subField.optionsQuery !== undefined && { refOptionsQuery: subField.optionsQuery }),
22
+ };
23
+ }
24
+ function fileHints(subField) {
25
+ if (subField.type !== "images" && subField.type !== "files")
26
+ return {};
27
+ return {
28
+ ...(subField.maxCount !== undefined && { maxCount: subField.maxCount }),
29
+ ...(subField.accept !== undefined && { accept: subField.accept }),
30
+ ...(subField.type === "images" &&
31
+ subField.capture !== undefined && { capture: subField.capture }),
32
+ };
33
+ }
34
+ export function buildEmbeddedListCell(subFieldName, subField, ctx) {
35
+ return {
36
+ field: subFieldName,
37
+ label: ctx.translate(embeddedCellLabelKey(ctx.featureName, ctx.entityName, ctx.fieldName, subFieldName)),
38
+ type: subField.type,
39
+ required: subField.required === true,
40
+ ...(subField.visibleWhen !== undefined && { visibleWhen: subField.visibleWhen }),
41
+ ...(subField.requiredWhen !== undefined && { requiredWhen: subField.requiredWhen }),
42
+ ...selectHints(subField, subFieldName, ctx),
43
+ ...referenceHints(subField, ctx.featureName),
44
+ ...(subField.type === "decimal" && subField.scale !== undefined && { scale: subField.scale }),
45
+ ...fileHints(subField),
46
+ ...(ctx.listSpec?.readOnlyCells?.includes(subFieldName) === true && { readOnly: true }),
47
+ ...(ctx.listSpec?.cardHiddenCells?.includes(subFieldName) === true && { hiddenInCard: true }),
48
+ };
49
+ }
@@ -2,7 +2,7 @@ import type { EmbeddedDerivedCellDef } from "@cosmicdrift/kumiko-framework/ui-ty
2
2
  import type { FieldIssue } from "../dispatcher/index.ts";
3
3
  export type EmbeddedDerivedOp = EmbeddedDerivedCellDef["op"];
4
4
  export type { DerivedCellRoundingTarget } from "@cosmicdrift/kumiko-framework/ui-types";
5
- export { computeDerivedCellValue, roundDerivedCellValue, } from "@cosmicdrift/kumiko-framework/ui-types";
5
+ export { computeDerivedCellValue, roundDerivedCellValue, withDerivedCells, } from "@cosmicdrift/kumiko-framework/ui-types";
6
6
  /** Sums a numeric/money/decimal column across all rows. Non-numeric or
7
7
  * missing values count as 0. Money columns are minor-unit integers —
8
8
  * the sum stays in minor units, never rounds through a major-unit float. */
@@ -1,6 +1,6 @@
1
1
  // Canonical implementation moved to packages/framework/src/engine/embedded-derived.ts
2
2
  // (the write-schema preprocess needs it server-side too).
3
- export { computeDerivedCellValue, roundDerivedCellValue, } from "@cosmicdrift/kumiko-framework/ui-types";
3
+ export { computeDerivedCellValue, roundDerivedCellValue, withDerivedCells, } from "@cosmicdrift/kumiko-framework/ui-types";
4
4
  /** Sums a numeric/money/decimal column across all rows. Non-numeric or
5
5
  * missing values count as 0. Money columns are minor-unit integers —
6
6
  * the sum stays in minor units, never rounds through a major-unit float. */
@@ -1,7 +1,7 @@
1
1
  export type { ComputeEditViewModelInput, EditFormMode } from "./edit.ts";
2
2
  export { computeEditViewModel, computeRelatedListSectionViewModel } from "./edit.ts";
3
3
  export type { DerivedCellRoundingTarget, EmbeddedDerivedOp, EmbeddedListIssueGroups, } from "./embedded-list.ts";
4
- export { computeDerivedCellValue, groupEmbeddedListIssues, roundDerivedCellValue, sumEmbeddedListColumn, } from "./embedded-list.ts";
4
+ export { computeDerivedCellValue, groupEmbeddedListIssues, roundDerivedCellValue, sumEmbeddedListColumn, withDerivedCells, } from "./embedded-list.ts";
5
5
  export type { ComputeListViewModelInput } from "./list.ts";
6
6
  export { buildOptionLabels, computeListViewModel, embeddedCellLabelKey, embeddedCellOptionLabelKey, fieldLabelKey, fieldOptionLabelKey, fieldOptionLabelKeyPrefix, } from "./list.ts";
7
7
  export type { CellLink, CellLinkItem, EditExtensionSectionViewModel, EditFieldSpec, EditFieldsSectionViewModel, EditFieldViewModel, EditRelatedListSectionViewModel, EditSectionSpec, EditSectionViewModel, EditViewModel, EditWriteFormSectionViewModel, EmbeddedListCellViewModel, FieldConditionCtx, FieldRenderer, ListColumnFileDisplay, ListColumnSpec, ListColumnViewModel, ListRowViewModel, ListViewModel, RuntimeCellLink, RuntimeCellLinkItems, RuntimeRenderer, ScreenSlots, Translate, } from "./types.ts";
@@ -1,3 +1,3 @@
1
1
  export { computeEditViewModel, computeRelatedListSectionViewModel } from "./edit.js";
2
- export { computeDerivedCellValue, groupEmbeddedListIssues, roundDerivedCellValue, sumEmbeddedListColumn, } from "./embedded-list.js";
2
+ export { computeDerivedCellValue, groupEmbeddedListIssues, roundDerivedCellValue, sumEmbeddedListColumn, withDerivedCells, } from "./embedded-list.js";
3
3
  export { buildOptionLabels, computeListViewModel, embeddedCellLabelKey, embeddedCellOptionLabelKey, fieldLabelKey, fieldOptionLabelKey, fieldOptionLabelKeyPrefix, } from "./list.js";
@@ -136,9 +136,8 @@ function derivedFieldColumn(input, column) {
136
136
  field: column.field,
137
137
  label: translatedColumnLabel(input, column),
138
138
  type: derivedDef.valueType,
139
- // Display-only: a header sort would round-trip to the server, which has
140
- // no column to sort by (see DerivedFieldDef). Never offer the affordance.
141
- sortable: false,
139
+ // The client schema carries only `sortable` (never `sql`), so test the flag, not the SQL shape.
140
+ sortable: "sortable" in derivedDef && derivedDef.sortable === true,
142
141
  ...(column.renderer !== undefined && { renderer: column.renderer }),
143
142
  ...(column.hideOnNarrow === true && { hideOnNarrow: true }),
144
143
  };
@@ -1,4 +1,4 @@
1
- import type { EditFieldSpec, EditSectionSpec, FieldAffix, FieldFillAction, FieldIconKey, FieldRenderer, IconKey, ListBulkAction, ListColumnSpec, ListFacetSpec, ListSortSpec, ListTableOptions, OptionsQueryPayload, PlatformComponent, RelatedListEmptyStateVariant, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionMode, ScreenSlots, SelectOptionTone } from "@cosmicdrift/kumiko-framework/ui-types";
1
+ import type { EditFieldSpec, EditSectionSpec, EmbeddedDerivedCellDef, FieldAffix, FieldCondition, FieldFillAction, FieldIconKey, FieldRenderer, IconKey, ListBulkAction, ListColumnSpec, ListFacetSpec, ListSortSpec, ListTableOptions, OptionsQueryPayload, PlatformComponent, RelatedListEmptyStateVariant, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionMode, ScreenSlots, SelectOptionTone } from "@cosmicdrift/kumiko-framework/ui-types";
2
2
  export type RuntimeRenderer = (value: unknown, row?: Readonly<Record<string, unknown>>) => string;
3
3
  /** Where a cell's text links to. `href` comes from NavApi.hrefFor (new-tab and
4
4
  * copy-link work); `onPress` is the in-app navigation for a plain click. */
@@ -79,15 +79,26 @@ export type ListViewModel = {
79
79
  export type EmbeddedListCellViewModel = {
80
80
  readonly field: string;
81
81
  readonly label: string;
82
- readonly type: "text" | "number" | "boolean" | "date" | "money" | "decimal" | "select" | "reference" | "timestamp";
82
+ readonly type: "text" | "number" | "boolean" | "date" | "money" | "decimal" | "select" | "reference" | "timestamp" | "images" | "files";
83
83
  readonly required: boolean;
84
+ /** Per-row state: hidden / required for a row where the condition holds. */
85
+ readonly visibleWhen?: Exclude<FieldCondition, boolean>;
86
+ readonly requiredWhen?: Exclude<FieldCondition, boolean>;
84
87
  /** Only for `type: "select"`. */
85
88
  readonly options?: readonly string[];
89
+ /** Only for `type: "select"`; tone of each option's badge or selected segment. */
90
+ readonly optionTones?: Readonly<Partial<Record<string, SelectOptionTone>>>;
91
+ /** Only for `type: "images" | "files"`. */
92
+ readonly capture?: "user" | "environment";
93
+ readonly maxCount?: number;
94
+ readonly accept?: readonly string[];
86
95
  readonly optionLabels?: Readonly<Record<string, string>>;
87
96
  /** Only for `type: "select"`; `"radio"` renders segmented buttons instead of a dropdown. */
88
97
  readonly display?: "radio" | "dropdown";
89
98
  /** Cell is shown but not editable (`EditFieldSpec.embeddedList.readOnlyCells`). */
90
99
  readonly readOnly?: boolean;
100
+ /** Not shown on phone cards (`EditFieldSpec.embeddedList.cardHiddenCells`). */
101
+ readonly hiddenInCard?: boolean;
91
102
  /** Only for `type: "reference"`. */
92
103
  readonly refEntity?: string;
93
104
  readonly refFeature?: string;
@@ -140,6 +151,8 @@ export type EditFieldViewModel = {
140
151
  * `type: "select"` when SelectFieldDef.display is "radio" or "dropdown" —
141
152
  * the renderer then skips its own radio-vs-dropdown heuristic. */
142
153
  readonly display?: "checkboxes" | "dropdown" | "radio";
154
+ /** `type: "select"` only: tone per option, shown on the selected radio segment. */
155
+ readonly optionTones?: Readonly<Partial<Record<string, SelectOptionTone>>>;
143
156
  /** Set for `type: "multiSelect"` when MultiSelectFieldDef.columns is
144
157
  * defined — column count for the checkbox grid at the widest breakpoint.
145
158
  * Ignored unless `display` is "checkboxes". */
@@ -249,10 +262,7 @@ export type EditFieldViewModel = {
249
262
  readonly embeddedListMaxItems?: number;
250
263
  /** Sub-field name → how to compute its cell from other sub-fields of the
251
264
  * same row. From EmbeddedFieldDef.derived. */
252
- readonly embeddedListDerived?: Readonly<Record<string, {
253
- readonly op: "multiply" | "sum" | "subtract" | "percent";
254
- readonly from: readonly string[];
255
- }>>;
265
+ readonly embeddedListDerived?: Readonly<Record<string, EmbeddedDerivedCellDef>>;
256
266
  /** Footer totals: a sub-field name, or a labelled sum of several. From EmbeddedFieldDef.totals. */
257
267
  readonly embeddedListTotals?: readonly (string | {
258
268
  readonly label: string;
@@ -261,6 +271,13 @@ export type EditFieldViewModel = {
261
271
  /** No add, remove, duplicate or move; cells stay editable. From
262
272
  * EditFieldSpec.embeddedList.fixedRows. */
263
273
  readonly embeddedListFixedRows?: boolean;
274
+ /** Where photos of a row's file cells are uploaded: the entity and the top-level list field;
275
+ * the cell name travels as `subField`. Set when the list has a file cell. */
276
+ readonly embeddedListUploadTarget?: {
277
+ readonly entityType: string;
278
+ readonly fieldName: string;
279
+ readonly entityId?: string;
280
+ };
264
281
  /** Currency for the whole embedded list's money cells and totals row —
265
282
  * currency lives on the head aggregate (EntityDefinition.defaultCurrency),
266
283
  * not per row. Falls back to "EUR" when the entity has no defaultCurrency. */
@@ -0,0 +1,2 @@
1
+ import type { EditRelatedListSection } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ export declare function buildRelatedListParentPayload(section: Pick<EditRelatedListSection, "parentFilter" | "parentParam">, parentId: string): Readonly<Record<string, unknown>>;
@@ -0,0 +1,7 @@
1
+ // `parentFilter` sends the parent id as a server-side `filter` clause: the generic
2
+ // `<entity>:list` query understands `filter`, not an arbitrary `parentParam` key.
3
+ export function buildRelatedListParentPayload(section, parentId) {
4
+ return section.parentFilter !== undefined
5
+ ? { filter: { field: section.parentFilter.field, op: "eq", value: parentId } }
6
+ : { [section.parentParam ?? "id"]: parentId };
7
+ }
@@ -50,6 +50,9 @@ export const kumikoDefaultTranslations = {
50
50
  "kumiko.field.writeOnly.willRemove": "Removed on save",
51
51
  "kumiko.field.embedded-list.actions": "Actions",
52
52
  "kumiko.field.embedded-list.cell-label": "{column}, row {row}",
53
+ "kumiko.field.embedded-list.limit-range": "Limit {min} to {max}",
54
+ "kumiko.field.embedded-list.limit-min": "At least {min}",
55
+ "kumiko.field.embedded-list.limit-max": "At most {max}",
53
56
  "kumiko.field.file-list.add": "Add file",
54
57
  "kumiko.field.file-list.move-up": "Move file up",
55
58
  "kumiko.field.file-list.move-down": "Move file down",
@@ -242,6 +245,14 @@ export const kumikoDefaultTranslations = {
242
245
  "kumiko.nav.expand": "Expand",
243
246
  "kumiko.nav.collapse": "Collapse",
244
247
  "kumiko.nav.search": "Search navigation…",
248
+ "kumiko.notifications.label": "Notifications",
249
+ "kumiko.notifications.labelUnread": "Notifications, {count} unread",
250
+ "kumiko.notifications.title": "Notifications",
251
+ "kumiko.notifications.empty": "No notifications yet.",
252
+ "kumiko.notifications.markAllRead": "Mark all as read",
253
+ "kumiko.notifications.loadMore": "Load more",
254
+ "kumiko.notifications.unread": "Unread",
255
+ "kumiko.notifications.loadFailed": "Notifications could not be loaded.",
245
256
  "kumiko.nav.language": "Language",
246
257
  "kumiko.workspace.switch": "Switch workspace",
247
258
  // Workspace-Switcher fallback label when activeId points at no visible workspace.
@@ -4,8 +4,10 @@ import { type ComponentType, type FormEvent, type KeyboardEvent, type ReactNode,
4
4
  import type { DataTableBulk } from "./data-table-bulk-types.ts";
5
5
  import type { DataTableDateRangeFacet, DataTableFacet } from "./data-table-facet-types.ts";
6
6
  import type { DataTableTableBlock } from "./data-table-view-types.ts";
7
+ import type { EmbeddedListInputProps } from "./embedded-list-types.ts";
7
8
  import type { FieldIssue, ListColumnViewModel, ListRowViewModel, RuntimeRenderer } from "./headless/index.ts";
8
9
  import type { MetricBandProps, MetricProps } from "./metric-types.ts";
10
+ export type { EmbeddedListCellType, EmbeddedListColumn, EmbeddedListInputProps, EmbeddedListTotal, } from "./embedded-list-types.ts";
9
11
  export type { RuntimeRenderer };
10
12
  /** Escape hatch for `data-*` test hooks (e.g. an E2E selector) that don't
11
13
  * warrant a dedicated typed prop. Web merges these onto the rendered
@@ -460,6 +462,8 @@ export type InputProps = {
460
462
  * decides; the web impl keeps its short-option-set heuristic. Custom
461
463
  * impls may ignore it — it is a request, not a contract. */
462
464
  readonly display?: "radio" | "dropdown";
465
+ /** Tone per option value; the selected segment of a radio presentation takes its colors. */
466
+ readonly optionTones?: Readonly<Partial<Record<string, SelectOptionTone>>>;
463
467
  } | ({
464
468
  readonly kind: "combobox";
465
469
  readonly id: string;
@@ -797,94 +801,6 @@ export type DataTableProps = {
797
801
  readonly renderExpandedRow?: (row: ListRowViewModel) => ReactNode;
798
802
  } & DataTableTableBlock;
799
803
  export type DataTableTableBlockProps = Pick<DataTableProps, keyof DataTableTableBlock>;
800
- /** Cell type for one column of an embedded-list field. Mirrors
801
- * EmbeddedListCellViewModel["type"] from `@cosmicdrift/kumiko-renderer/headless`. */
802
- export type EmbeddedListCellType = "text" | "number" | "boolean" | "date" | "money" | "decimal" | "select" | "reference" | "timestamp";
803
- /** One column of an embedded-list table (one entry per key of the source
804
- * EmbeddedFieldDef's `schema`). */
805
- export type EmbeddedListColumn = {
806
- readonly field: string;
807
- readonly label: string;
808
- readonly type: EmbeddedListCellType;
809
- readonly required: boolean;
810
- /** Read-only, value supplied by the caller (already computed from
811
- * sibling cells) — not directly user-editable. */
812
- readonly derived: boolean;
813
- /** Shown but not editable by the user; unlike `derived` it is not computed. */
814
- readonly readOnly?: boolean;
815
- /** Only for `type: "select"`. */
816
- readonly options?: readonly string[];
817
- readonly optionLabels?: Readonly<Record<string, string>>;
818
- /** Only for `type: "select"`; `"radio"` renders segmented buttons, the
819
- * default is a dropdown. */
820
- readonly display?: "radio" | "dropdown";
821
- /** Only for `type: "reference"` — pre-fetched by the caller, ONE query
822
- * per column shared across every row (not one query per cell). */
823
- readonly referenceOptions?: readonly {
824
- readonly value: string;
825
- readonly label: string;
826
- }[];
827
- readonly referenceLoading?: boolean;
828
- };
829
- /** One entry of an embedded-list totals row (e.g. an invoice's total
830
- * amount, summed across all rows by the caller). */
831
- export type EmbeddedListTotal = {
832
- readonly field: string;
833
- readonly label: string;
834
- readonly value: number;
835
- /** Set for labelled multi-column totals, which have no single column to
836
- * read the money type from. */
837
- readonly money?: boolean;
838
- };
839
- /** Row-array + totals table for a `createEmbeddedListField` (invoice-
840
- * positions-style) field — structurally too different from `Input`'s
841
- * value/onChange union to fit there, so it's its own primitive, same as
842
- * `DataTable`. Controlled: the caller (the eventual form-field wrapper)
843
- * computes rows/derived values/issue groups and wires every callback;
844
- * this primitive only renders and reports interaction. */
845
- export type EmbeddedListInputProps = {
846
- readonly id: string;
847
- readonly columns: readonly EmbeddedListColumn[];
848
- readonly rows: readonly Readonly<Record<string, unknown>>[];
849
- readonly totals?: readonly EmbeddedListTotal[];
850
- /** Currency for money cells and the totals row — one value for the whole
851
- * list (currency lives on the head aggregate, not the row). Web impl
852
- * falls back to "EUR" when absent. */
853
- readonly currency?: string;
854
- /** Whole-list read-only (e.g. a released invoice) — hides row-mutation
855
- * affordances and disables every cell. */
856
- readonly disabled?: boolean;
857
- /** Rows are fixed: no add, remove, duplicate or move; cells stay editable. */
858
- readonly fixedRows?: boolean;
859
- readonly minItems?: number;
860
- readonly maxItems?: number;
861
- /** Shown under the whole list (e.g. under the totals row). */
862
- readonly listIssues?: readonly FieldIssue[];
863
- /** Keyed by row index. */
864
- readonly rowIssues?: Readonly<Record<number, readonly FieldIssue[]>>;
865
- /** Keyed `${rowIndex}.${field}`. */
866
- readonly cellIssues?: Readonly<Record<string, readonly FieldIssue[]>>;
867
- readonly onCellChange: (rowIndex: number, field: string, value: unknown) => void;
868
- readonly onAddRow: () => void;
869
- readonly onRemoveRow: (rowIndex: number) => void;
870
- readonly onDuplicateRow: (rowIndex: number) => void;
871
- readonly onMoveRow: (fromIndex: number, toIndex: number) => void;
872
- /** Tab/newline-delimited clipboard paste starting at (rowIndex,
873
- * columnIndex) — parsed to a 2D string grid by the Web impl (paste
874
- * events are a browser-only concept); undefined caller = no paste
875
- * handling wired up. */
876
- readonly onPasteCells?: (rowIndex: number, columnIndex: number, grid: readonly (readonly string[])[]) => void;
877
- readonly addLabel: string;
878
- /** Accessible name of the (otherwise headerless) row-actions column. */
879
- readonly actionsLabel: string;
880
- readonly removeLabel: string;
881
- readonly duplicateLabel: string;
882
- readonly moveUpLabel: string;
883
- readonly moveDownLabel: string;
884
- readonly emptyLabel: string;
885
- readonly emptyCtaLabel: string;
886
- readonly testId?: string;
887
- };
888
804
  export type { FormWidth };
889
805
  /** One entry of a form's "on this page" navigation: `id` matches the
890
806
  * `SectionProps.id` of the section it scrolls to. */
@@ -0,0 +1,5 @@
1
+ import type { ScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import { type ReactNode } from "react";
3
+ export declare function NotificationBell({ screens, }: {
4
+ readonly screens: readonly ScreenDefinition[];
5
+ }): ReactNode;
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Bell } from "lucide-react";
3
+ import { Popover as PopoverPrimitive } from "radix-ui";
4
+ import { useState } from "react";
5
+ import { lastSegment, useLocale, useNav, useTranslation } from "../../index.js";
6
+ import { useRelativeTimeFormatter } from "../app/relative-time.js";
7
+ import { cn } from "../lib/cn.js";
8
+ import { useInAppInbox } from "./use-in-app-inbox.js";
9
+ const MAX_BADGE_COUNT = 99;
10
+ const triggerClass = "relative inline-flex size-8 shrink-0 items-center justify-center rounded-md border border-input " +
11
+ "bg-background text-foreground shadow-xs transition-colors hover:bg-accent " +
12
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";
13
+ const panelClass = "z-50 flex max-h-[28rem] w-[min(22rem,calc(100vw-1.5rem))] flex-col overflow-hidden rounded-md border " +
14
+ "bg-popover text-popover-foreground shadow-md";
15
+ export function NotificationBell({ screens, }) {
16
+ const t = useTranslation();
17
+ const nav = useNav();
18
+ const locale = useLocale().locale();
19
+ const formatRelative = useRelativeTimeFormatter(locale);
20
+ const [open, setOpen] = useState(false);
21
+ const inbox = useInAppInbox(open);
22
+ const badge = inbox.unreadCount > MAX_BADGE_COUNT ? `${MAX_BADGE_COUNT}+` : String(inbox.unreadCount);
23
+ const label = inbox.unreadCount > 0
24
+ ? t("kumiko.notifications.labelUnread", { count: badge })
25
+ : t("kumiko.notifications.label");
26
+ const openRow = async (row) => {
27
+ if (!row.isRead)
28
+ await inbox.markRead(row.id);
29
+ const target = row.target;
30
+ if (target === null)
31
+ return;
32
+ if (!screens.some((screen) => screen.id === target.screenId))
33
+ return;
34
+ const screenId = lastSegment(target.screenId);
35
+ setOpen(false);
36
+ nav.navigate(target.entityId === undefined ? { screenId } : { screenId, entityId: target.entityId });
37
+ };
38
+ return (_jsxs(PopoverPrimitive.Root, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverPrimitive.Trigger, { asChild: true, children: _jsxs("button", { type: "button", "aria-label": label, "data-kumiko-layout": "notification-bell", className: triggerClass, children: [_jsx(Bell, { className: "size-4", "aria-hidden": "true" }), inbox.unreadCount > 0 && (_jsx("span", { "data-kumiko-layout": "notification-bell-badge", className: "absolute -right-1.5 -top-1.5 flex min-w-4 items-center justify-center rounded-full bg-destructive px-1 text-[10px] font-semibold leading-4 text-destructive-foreground", children: badge }))] }) }), _jsx(PopoverPrimitive.Portal, { children: _jsxs(PopoverPrimitive.Content, { align: "end", sideOffset: 6, className: panelClass, children: [_jsxs("div", { className: "flex items-center justify-between gap-2 border-b border-border px-3 py-2", children: [_jsx("h2", { className: "text-sm font-semibold", children: t("kumiko.notifications.title") }), inbox.unreadCount > 0 && (_jsx("button", { type: "button", className: "text-xs text-primary hover:underline", onClick: () => void inbox.markAllRead(), children: t("kumiko.notifications.markAllRead") }))] }), _jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto", children: [inbox.failed && (_jsx("p", { role: "alert", className: "px-3 py-4 text-sm text-destructive", children: t("kumiko.notifications.loadFailed") })), !inbox.failed && !inbox.loading && inbox.rows.length === 0 && (_jsx("p", { className: "px-3 py-6 text-center text-sm text-muted-foreground", children: t("kumiko.notifications.empty") })), _jsx("ul", { children: inbox.rows.map((row) => (_jsx("li", { children: _jsxs("button", { type: "button", "data-unread": row.isRead ? undefined : "true", className: "flex w-full items-start gap-2 border-b border-border px-3 py-2 text-left hover:bg-accent focus-visible:bg-accent focus-visible:outline-none", onClick: () => void openRow(row), children: [_jsx("span", { "aria-hidden": "true", className: cn("mt-1.5 size-2 shrink-0 rounded-full", row.isRead ? "bg-transparent" : "bg-primary") }), !row.isRead && (_jsx("span", { className: "sr-only", children: t("kumiko.notifications.unread") })), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: cn("block truncate text-sm", !row.isRead && "font-semibold"), children: row.title }), row.body !== null && (_jsx("span", { className: "block truncate text-xs text-muted-foreground", children: row.body })), _jsx("span", { className: "block text-xs text-muted-foreground", children: formatRelative(Date.parse(row.createdAt)) })] })] }) }, row.id))) }), inbox.hasMore && (_jsx("button", { type: "button", className: "w-full px-3 py-2 text-sm text-primary hover:bg-accent", onClick: () => void inbox.loadMore(), children: t("kumiko.notifications.loadMore") }))] })] }) })] }));
39
+ }
@@ -7,6 +7,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
7
7
  // The `data-kumiko-layout="shell-header"` marker drives `--shell-header-height`
8
8
  // in styles.css (:has() selector) — the single source for the header height
9
9
  // that Drawer's `belowHeader` prop also reads.
10
+ import { IN_APP_NOTIFICATION_FEATURE } from "@cosmicdrift/kumiko-types/in-app-notification-types";
10
11
  import { MoreHorizontal } from "lucide-react";
11
12
  import { Fragment, useEffect, useId, useMemo, useRef, useState, } from "react";
12
13
  import { resolveNavigation } from "../../headless/index.js";
@@ -17,6 +18,7 @@ import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbP
17
18
  import { SidebarTrigger } from "../ui/sidebar.js";
18
19
  import { HeaderOverflowMenuContext, headerOverflowMenuItemClass } from "./header-overflow-menu.js";
19
20
  import { buildNavRegistrySliceForApp } from "./nav-tree.js";
21
+ import { NotificationBell } from "./notification-bell.js";
20
22
  import { usePageHeaderSlot } from "./page-header-slot.js";
21
23
  import { activeNavLabel, resolveDetailBreadcrumb, screenTitleKey, } from "./shell-breadcrumb.js";
22
24
  import { SHELL_HEADER_CONTROLS_CLASSES } from "./uniform-bar-controls.js";
@@ -35,6 +37,7 @@ export function ShellHeader({ schema, user, headerActions, allowedNavQns, }) {
35
37
  return resolveNavigation({ source, ...(user !== undefined && { user }) });
36
38
  }, [appSchema, user, allowedNavQns]);
37
39
  const allScreens = useMemo(() => appSchema.features.flatMap((f) => f.screens), [appSchema.features]);
40
+ const hasInAppChannel = appSchema.features.some((feature) => feature.featureName === IN_APP_NOTIFICATION_FEATURE);
38
41
  const slot = usePageHeaderSlot();
39
42
  const standInScreenId = slot?.breadcrumbScreenId;
40
43
  const screenId = standInScreenId ?? nav.route?.screenId;
@@ -78,7 +81,7 @@ export function ShellHeader({ schema, user, headerActions, allowedNavQns, }) {
78
81
  e.preventDefault();
79
82
  nav.navigate({ screenId });
80
83
  }, 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));
81
- }) }) })), _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(SHELL_HEADER_CONTROLS_CLASSES, "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 })] })] }));
84
+ }) }) })), _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(SHELL_HEADER_CONTROLS_CLASSES, "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") }), user !== undefined && hasInAppChannel && _jsx(NotificationBell, { screens: allScreens }), !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 })] })] }));
82
85
  }
83
86
  // A hand-rolled menu instead of Radix DropdownMenu: app headerActions are opaque
84
87
  // nodes with their own popovers, and the panel stays mounted while closed so their
@@ -0,0 +1,15 @@
1
+ import { type InAppInboxRow } from "@cosmicdrift/kumiko-types/in-app-notification-types";
2
+ export declare const IN_APP_INBOX_PAGE_SIZE = 20;
3
+ export type InAppInbox = {
4
+ readonly unreadCount: number;
5
+ readonly rows: readonly InAppInboxRow[];
6
+ readonly hasMore: boolean;
7
+ readonly loading: boolean;
8
+ readonly failed: boolean;
9
+ readonly loadMore: () => Promise<void>;
10
+ readonly markRead: (id: number) => Promise<void>;
11
+ readonly markAllRead: () => Promise<void>;
12
+ };
13
+ /** Headless state of the notification bell: the unread count always, the list only while `open`.
14
+ * The delivered signal refetches both; no polling. */
15
+ export declare function useInAppInbox(open: boolean): InAppInbox;