@revenexx/studio-shared 0.4.0 → 0.4.2

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 (110) hide show
  1. package/dist/module.d.mts +5 -3
  2. package/dist/module.json +2 -2
  3. package/dist/module.mjs +7 -3
  4. package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue +1 -1
  5. package/dist/runtime/components/Dashboard/DashboardGrid.vue +45 -1
  6. package/dist/runtime/components/Dashboard/DashboardNameDialog.vue +1 -1
  7. package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue +1 -1
  8. package/dist/runtime/components/Dashboard/DashboardShareDialog.vue +1 -1
  9. package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue +1 -1
  10. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.d.vue.ts +2 -2
  11. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue +14 -3
  12. package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue.d.ts +2 -2
  13. package/dist/runtime/components/DataTable/DataTable.d.vue.ts +73 -34
  14. package/dist/runtime/components/DataTable/DataTable.vue +352 -52
  15. package/dist/runtime/components/DataTable/DataTable.vue.d.ts +73 -34
  16. package/dist/runtime/components/DataTable/FacetPanel.vue +57 -8
  17. package/dist/runtime/components/DataTable/FilterMenu.vue +5 -5
  18. package/dist/runtime/components/DataTable/SavedViewsMenu.vue +61 -12
  19. package/dist/runtime/components/DataTable/SortMenu.vue +4 -1
  20. package/dist/runtime/components/DataTable/ToolbarLabelTrigger.d.vue.ts +23 -0
  21. package/dist/runtime/components/DataTable/ToolbarLabelTrigger.vue +29 -0
  22. package/dist/runtime/components/DataTable/ToolbarLabelTrigger.vue.d.ts +23 -0
  23. package/dist/runtime/components/DataTable/types.d.ts +17 -0
  24. package/dist/runtime/components/Form/FieldGrid.vue +1 -1
  25. package/dist/runtime/components/Form/SaveBar.d.vue.ts +1 -1
  26. package/dist/runtime/components/Form/SaveBar.vue.d.ts +1 -1
  27. package/dist/runtime/components/Media/MediaFolderTree.d.vue.ts +4 -4
  28. package/dist/runtime/components/Media/MediaFolderTree.vue +1 -2
  29. package/dist/runtime/components/Media/MediaFolderTree.vue.d.ts +4 -4
  30. package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +11 -0
  31. package/dist/runtime/components/Media/MediaPickerDialog.vue +206 -54
  32. package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +11 -0
  33. package/dist/runtime/components/Media/MediaUploader.vue +1 -1
  34. package/dist/runtime/components/Shared/AppIcon.d.vue.ts +1 -1
  35. package/dist/runtime/components/Shared/AppIcon.vue.d.ts +1 -1
  36. package/dist/runtime/components/Shared/CardSizeToggle.d.vue.ts +11 -0
  37. package/dist/runtime/components/Shared/CardSizeToggle.vue +19 -0
  38. package/dist/runtime/components/Shared/CardSizeToggle.vue.d.ts +11 -0
  39. package/dist/runtime/components/Shared/DetailSheet.d.vue.ts +1 -1
  40. package/dist/runtime/components/Shared/DetailSheet.vue +9 -5
  41. package/dist/runtime/components/Shared/DetailSheet.vue.d.ts +1 -1
  42. package/dist/runtime/components/Shared/EmptyNote.d.vue.ts +19 -0
  43. package/dist/runtime/components/Shared/EmptyNote.vue +25 -0
  44. package/dist/runtime/components/Shared/EmptyNote.vue.d.ts +19 -0
  45. package/dist/runtime/components/Shared/KpiTile.d.vue.ts +6 -0
  46. package/dist/runtime/components/Shared/KpiTile.vue +10 -9
  47. package/dist/runtime/components/Shared/KpiTile.vue.d.ts +6 -0
  48. package/dist/runtime/components/Shared/PageHeader.vue +15 -4
  49. package/dist/runtime/components/Shared/RecordLayout.vue +1 -2
  50. package/dist/runtime/components/Shared/RecordRailCard.vue +3 -3
  51. package/dist/runtime/components/Shared/SelectionBar.d.vue.ts +22 -0
  52. package/dist/runtime/components/Shared/SelectionBar.vue +40 -0
  53. package/dist/runtime/components/Shared/SelectionBar.vue.d.ts +22 -0
  54. package/dist/runtime/components/Shared/SidebarNav.d.vue.ts +36 -2
  55. package/dist/runtime/components/Shared/SidebarNav.vue +74 -25
  56. package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +36 -2
  57. package/dist/runtime/components/Shared/SidebarNavAreas.d.vue.ts +7 -0
  58. package/dist/runtime/components/Shared/SidebarNavAreas.vue +291 -0
  59. package/dist/runtime/components/Shared/SidebarNavAreas.vue.d.ts +7 -0
  60. package/dist/runtime/components/Shared/SidebarNavLink.d.vue.ts +1 -9
  61. package/dist/runtime/components/Shared/SidebarNavLink.vue +31 -5
  62. package/dist/runtime/components/Shared/SidebarNavLink.vue.d.ts +1 -9
  63. package/dist/runtime/components/Shared/SortHeader.vue +6 -2
  64. package/dist/runtime/components/Shared/StateCard.vue +1 -1
  65. package/dist/runtime/components/Shared/TabBar.vue +6 -7
  66. package/dist/runtime/components/Shared/Wizard.d.vue.ts +11 -1
  67. package/dist/runtime/components/Shared/Wizard.vue +9 -5
  68. package/dist/runtime/components/Shared/Wizard.vue.d.ts +11 -1
  69. package/dist/runtime/composables/useMediaLibrary.d.ts +12 -0
  70. package/dist/runtime/composables/useSavedViews.d.ts +11 -1
  71. package/dist/runtime/composables/useSavedViews.js +4 -4
  72. package/dist/runtime/composables/useWidgetFigures.d.ts +12 -0
  73. package/dist/runtime/composables/useWidgetFigures.js +24 -0
  74. package/dist/runtime/composables/useWidgetHeader.d.ts +15 -0
  75. package/dist/runtime/composables/useWidgetHeader.js +15 -0
  76. package/dist/runtime/lib/auditEnvelope.d.ts +43 -0
  77. package/dist/runtime/lib/auditEnvelope.js +55 -0
  78. package/dist/runtime/lib/cardSize.d.ts +13 -0
  79. package/dist/runtime/lib/cardSize.js +20 -0
  80. package/dist/runtime/lib/dashboardIcons.js +2 -0
  81. package/dist/runtime/lib/dataTableFilters.d.ts +22 -0
  82. package/dist/runtime/lib/dataTableFilters.js +5 -0
  83. package/dist/runtime/lib/facets.d.ts +13 -0
  84. package/dist/runtime/lib/formLayout.d.ts +74 -4
  85. package/dist/runtime/lib/formLayout.js +53 -6
  86. package/dist/runtime/lib/gridTracks.d.ts +14 -1
  87. package/dist/runtime/lib/gridTracks.js +13 -2
  88. package/dist/runtime/lib/isoLists.d.ts +37 -2
  89. package/dist/runtime/lib/isoLists.js +54 -1
  90. package/dist/runtime/lib/notice.d.ts +14 -0
  91. package/dist/runtime/lib/notice.js +19 -0
  92. package/dist/runtime/lib/pageSize.d.ts +15 -0
  93. package/dist/runtime/lib/pageSize.js +26 -0
  94. package/dist/runtime/lib/sidebarAreas.d.ts +46 -0
  95. package/dist/runtime/lib/sidebarAreas.js +25 -0
  96. package/dist/runtime/lib/sidebarNav.d.ts +39 -2
  97. package/dist/runtime/lib/sidebarNav.js +30 -4
  98. package/dist/runtime/lib/solarIconMap.d.ts +2 -1
  99. package/dist/runtime/lib/solarIconMap.js +50 -4
  100. package/dist/runtime/lib/swipeToClose.d.ts +25 -0
  101. package/dist/runtime/lib/swipeToClose.js +118 -0
  102. package/dist/runtime/lib/tableExport.d.ts +31 -0
  103. package/dist/runtime/lib/tableExport.js +143 -0
  104. package/dist/runtime/lib/talkback.d.ts +5 -0
  105. package/dist/runtime/lib/talkback.js +8 -0
  106. package/dist/runtime/lib/talkbackTokenBatch.d.ts +35 -0
  107. package/dist/runtime/lib/talkbackTokenBatch.js +101 -0
  108. package/dist/runtime/types/nav.d.ts +9 -0
  109. package/dist/types.d.mts +4 -0
  110. package/package.json +4 -3
@@ -0,0 +1,24 @@
1
+ import { getCurrentInstance, inject, onBeforeUnmount, provide, ref, toValue, watch } from "vue";
2
+ const WIDGET_FIGURES = Symbol("widgetFigures");
3
+ export function provideWidgetFigures() {
4
+ const figures = ref(null);
5
+ provide(WIDGET_FIGURES, figures);
6
+ return figures;
7
+ }
8
+ export function useWidgetFigures(figures) {
9
+ const target = inject(WIDGET_FIGURES, null);
10
+ if (!target) return;
11
+ watch(
12
+ () => toValue(figures)?.filter((f) => f.label && f.value) ?? null,
13
+ (next) => {
14
+ target.value = next?.length ? next : null;
15
+ },
16
+ { immediate: true, deep: true }
17
+ );
18
+ if (getCurrentInstance()) onBeforeUnmount(() => {
19
+ target.value = null;
20
+ });
21
+ }
22
+ export function figuresText(figures) {
23
+ return figures?.map((f) => `${f.label}: ${f.value}`).join("\n") ?? "";
24
+ }
@@ -0,0 +1,15 @@
1
+ import type { Ref } from 'vue';
2
+ export interface WidgetHeaderStatus {
3
+ /** Show the loading indicator. */
4
+ busy: boolean;
5
+ /** One short line; truncated to fit beside the title. */
6
+ text: string;
7
+ /** How far along, 0 to 100. Omit when unknown: the frame shows a spinner. */
8
+ progress?: number;
9
+ }
10
+ /** Called by the frame: the status its widget may set, provided to the widget below it. */
11
+ export declare function provideWidgetHeader(): Ref<WidgetHeaderStatus | null>;
12
+ /** Called by a widget: set or clear the status its frame shows. */
13
+ export declare function useWidgetHeader(): {
14
+ setStatus: (next: WidgetHeaderStatus | null) => void;
15
+ };
@@ -0,0 +1,15 @@
1
+ import { getCurrentInstance, inject, onBeforeUnmount, provide, ref } from "vue";
2
+ const WIDGET_HEADER_STATUS = Symbol("widgetHeaderStatus");
3
+ export function provideWidgetHeader() {
4
+ const status = ref(null);
5
+ provide(WIDGET_HEADER_STATUS, status);
6
+ return status;
7
+ }
8
+ export function useWidgetHeader() {
9
+ const status = inject(WIDGET_HEADER_STATUS, null);
10
+ function setStatus(next) {
11
+ if (status) status.value = next;
12
+ }
13
+ if (getCurrentInstance()) onBeforeUnmount(() => setStatus(null));
14
+ return { setStatus };
15
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The impersonation audit envelope (PE-1040, ADR-0114 / PE-1012).
3
+ *
4
+ * While an operator impersonates a tenant, Console's `/operator/impersonate`
5
+ * answers with a signed `audit` value next to the brokered token. Sent as
6
+ * `X-Revenexx-Audit`, it tells Baseline's history which human acted behind the
7
+ * operator subject. Baseline checks the signature, the tenant, the token's
8
+ * `sub` and the expiry, and ignores anything that does not hold, so a missing,
9
+ * stale or foreign envelope only costs the name in the history, never a write.
10
+ *
11
+ * Two switches keep it harmless:
12
+ * - nothing is held outside impersonation: `useOperatorSession` stores the
13
+ * value on install/renew and clears it on exit, and a Console that sends no
14
+ * `audit` field stores nothing;
15
+ * - nothing is sent until the runtime config names the target
16
+ * (`NUXT_PUBLIC_AUDIT_ENVELOPE_TARGETS=postgrest,gateway`). A browser can
17
+ * only send a header the server's CORS preflight allows, and a header it
18
+ * does not allow fails the whole request, so each target is switched on once
19
+ * its edge lists the header.
20
+ *
21
+ * Pure (no Nuxt imports), so the cockpit, studio-commerce and tests share it.
22
+ */
23
+ /** The header Baseline reads the envelope from. */
24
+ export declare const AUDIT_HEADER = "X-Revenexx-Audit";
25
+ /** Where the envelope can go. Each one is switched on in the runtime config. */
26
+ export type AuditEnvelopeTarget = 'postgrest' | 'gateway';
27
+ /** localStorage key: the envelope survives the reload a tenant switch does. */
28
+ export declare const AUDIT_ENVELOPE_STORAGE_KEY = "console.auditEnvelope";
29
+ /** The value Console sent, if it is one we may send on. */
30
+ export declare function parseAuditEnvelope(value: unknown): string | null;
31
+ /** Store (or, with null, drop) the envelope of the current operator session. */
32
+ export declare function setAuditEnvelope(value: unknown): void;
33
+ /** The envelope of the current operator session, or null. */
34
+ export declare function currentAuditEnvelope(): string | null;
35
+ /** `"postgrest, gateway"` (or an array) → the targets it names. */
36
+ export declare function auditEnvelopeTargets(configured: unknown): Set<AuditEnvelopeTarget>;
37
+ /**
38
+ * The header to add to a request for `target`: `{}` unless the target is
39
+ * switched on and an operator session holds an envelope.
40
+ */
41
+ export declare function auditHeaders(target: AuditEnvelopeTarget, configured: unknown): Record<string, string>;
42
+ /** For tests: forget what was read from storage. */
43
+ export declare function resetAuditEnvelopeCache(): void;
@@ -0,0 +1,55 @@
1
+ export const AUDIT_HEADER = "X-Revenexx-Audit";
2
+ export const AUDIT_ENVELOPE_STORAGE_KEY = "console.auditEnvelope";
3
+ const ENVELOPE = /^v1\.[\w-]{1,64}\.[\w-]{1,4096}\.[\w-]{16,256}$/;
4
+ let held;
5
+ function storage() {
6
+ try {
7
+ return typeof window !== "undefined" && window.localStorage ? window.localStorage : null;
8
+ } catch {
9
+ return null;
10
+ }
11
+ }
12
+ export function parseAuditEnvelope(value) {
13
+ if (typeof value !== "string") return null;
14
+ const trimmed = value.trim();
15
+ return ENVELOPE.test(trimmed) ? trimmed : null;
16
+ }
17
+ export function setAuditEnvelope(value) {
18
+ held = parseAuditEnvelope(value);
19
+ const store = storage();
20
+ if (!store) return;
21
+ try {
22
+ if (held) store.setItem(AUDIT_ENVELOPE_STORAGE_KEY, held);
23
+ else store.removeItem(AUDIT_ENVELOPE_STORAGE_KEY);
24
+ } catch {
25
+ }
26
+ }
27
+ export function currentAuditEnvelope() {
28
+ if (held === void 0) {
29
+ let stored = null;
30
+ try {
31
+ stored = storage()?.getItem(AUDIT_ENVELOPE_STORAGE_KEY) ?? null;
32
+ } catch {
33
+ stored = null;
34
+ }
35
+ held = parseAuditEnvelope(stored);
36
+ }
37
+ return held;
38
+ }
39
+ export function auditEnvelopeTargets(configured) {
40
+ const raw = Array.isArray(configured) ? configured.join(",") : typeof configured === "string" ? configured : "";
41
+ const out = /* @__PURE__ */ new Set();
42
+ for (const part of raw.split(",")) {
43
+ const name = part.trim().toLowerCase();
44
+ if (name === "postgrest" || name === "gateway") out.add(name);
45
+ }
46
+ return out;
47
+ }
48
+ export function auditHeaders(target, configured) {
49
+ if (!auditEnvelopeTargets(configured).has(target)) return {};
50
+ const envelope = currentAuditEnvelope();
51
+ return envelope ? { [AUDIT_HEADER]: envelope } : {};
52
+ }
53
+ export function resetAuditEnvelopeCache() {
54
+ held = void 0;
55
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Small or large cards in a gallery (Mediathek, PIM assets) — the choice the
3
+ * `CardSizeToggle` makes, remembered per viewer on this browser.
4
+ *
5
+ * Browser storage is a convenience here: a private window or blocked storage
6
+ * simply starts at the large cards again.
7
+ */
8
+ export type CardSize = 'comfortable' | 'compact';
9
+ export declare function parseCardSize(value: unknown): CardSize;
10
+ export declare function storedCardSize(key: string): CardSize | null;
11
+ export declare function storeCardSize(key: string, size: CardSize): void;
12
+ /** The gallery grid's columns per size: more, smaller cards when compact. */
13
+ export declare function cardGridClass(size: CardSize): string;
@@ -0,0 +1,20 @@
1
+ export function parseCardSize(value) {
2
+ return value === "compact" ? "compact" : "comfortable";
3
+ }
4
+ export function storedCardSize(key) {
5
+ try {
6
+ const v = typeof localStorage === "undefined" ? null : localStorage.getItem(key);
7
+ return v === "compact" || v === "comfortable" ? v : null;
8
+ } catch {
9
+ return null;
10
+ }
11
+ }
12
+ export function storeCardSize(key, size) {
13
+ try {
14
+ localStorage.setItem(key, size);
15
+ } catch {
16
+ }
17
+ }
18
+ export function cardGridClass(size) {
19
+ return size === "compact" ? "grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6" : "grid-cols-1 min-[480px]:grid-cols-2 md:grid-cols-3 2xl:grid-cols-4";
20
+ }
@@ -1,4 +1,5 @@
1
1
  import {
2
+ Atom,
2
3
  Bag,
3
4
  BillList,
4
5
  Bolt,
@@ -22,6 +23,7 @@ import {
22
23
  Widget
23
24
  } from "#solar-icons";
24
25
  export const DASHBOARD_ICONS = {
26
+ Atom,
25
27
  Bag,
26
28
  BillList,
27
29
  Bolt,
@@ -81,3 +81,25 @@ export declare function countByValue<T>(rows: readonly T[], getter: FilterGetter
81
81
  * whose value happens to be `''` must not be the only thing that survives it.
82
82
  */
83
83
  export declare function matchesFilter<T>(getter: FilterGetter<T>, row: T, active: string | readonly string[] | null | undefined): boolean;
84
+ /** One dimension as the counting below needs it: its key and how a row reads under it. */
85
+ export interface FilterDimension<T> {
86
+ key: string;
87
+ getter: FilterGetter<T>;
88
+ }
89
+ /**
90
+ * How many rows each option of one dimension would leave, given what is ticked.
91
+ * `rows` is what the search already leaves; a caller without a search passes
92
+ * the whole list.
93
+ *
94
+ * Counted over the rows that survive every OTHER dimension's selection, and not
95
+ * this dimension's own. With "Status: Active" ticked, "Channel: Email 3" means
96
+ * three active email rows — the number a click on Email will actually show —
97
+ * where counting the whole list offered rows the Active choice had already
98
+ * removed ("Ready 3" that yielded nothing, PO-313).
99
+ *
100
+ * Not this dimension's own selection, because inside one dimension the choices
101
+ * are ANY (see `matchesFilter`): ticking Draft beside Active ADDS the drafts, so
102
+ * "Draft" has to keep counting the drafts rather than drop to zero the moment
103
+ * Active is ticked.
104
+ */
105
+ export declare function facetCounts<T>(rows: readonly T[], dimensions: readonly FilterDimension<T>[], active: Readonly<Record<string, readonly string[] | undefined>>, dimension: FilterDimension<T>): Record<string, number>;
@@ -21,3 +21,8 @@ export function matchesFilter(getter, row, active) {
21
21
  const values = filterValues(getter, row);
22
22
  return wanted.some((v) => values.includes(v));
23
23
  }
24
+ export function facetCounts(rows, dimensions, active, dimension) {
25
+ const others = dimensions.filter((d) => d !== dimension && active[d.key]?.length);
26
+ const narrowed = others.length ? rows.filter((row) => others.every((d) => matchesFilter(d.getter, row, active[d.key]))) : rows;
27
+ return countByValue(narrowed, dimension.getter);
28
+ }
@@ -54,6 +54,11 @@ export interface FacetOption {
54
54
  count?: number;
55
55
  /** Greyed and unclickable, kept visible so the dimension still reads whole. */
56
56
  disabled?: boolean;
57
+ /**
58
+ * A second, smaller line — the customer number or e-mail that tells two
59
+ * options of the same name apart (a searched facet's hits, Michael 19.10.).
60
+ */
61
+ description?: string;
57
62
  }
58
63
  /** One node of a `tree` facet. */
59
64
  export interface FacetNode {
@@ -89,6 +94,14 @@ export interface Facet {
89
94
  * nothing, so offering it as a pair of checkboxes offers a dead end.
90
95
  */
91
96
  single?: boolean;
97
+ /**
98
+ * `check` over a set that grows with the tenant — organisations, contacts,
99
+ * price lists: the pane asks the server for what matches the panel's search
100
+ * box (an empty term browses the first page) instead of listing `options`.
101
+ * `options` then carries only the values already picked, with their names,
102
+ * so a ticked value stays on screen and the chips can name it.
103
+ */
104
+ search?: (term: string) => Promise<FacetOption[]>;
92
105
  }
93
106
  /** `{ from, to }`, either side optional. */
94
107
  export interface FacetRange {
@@ -29,12 +29,81 @@ export declare function resolveSpan(field: SpanSource): FieldSpan;
29
29
  /**
30
30
  * The grid-item class for a span inside `FieldGrid`.
31
31
  *
32
- * The grid is one column until its OWN width reaches the `@md` container size
33
- * (28rem) and two above it — a container query, not a viewport breakpoint, so
32
+ * The grid is one column until its OWN width reaches the `@lg` container size
33
+ * (32rem) and two above it — a container query, not a viewport breakpoint, so
34
34
  * the same grid is right in a 512 px dialog, beside a section nav, and in a
35
35
  * full-width page. A full field spans both tracks once there are two.
36
36
  */
37
37
  export declare function spanClass(span: FieldSpan): string;
38
+ /**
39
+ * The label/value grid of a record card (Michael 19.10.: „ich will es gerne
40
+ * zweispaltig haben“). The read card and the same card opened with
41
+ * „Bearbeiten“ both draw their rows into this grid, so nothing moves when the
42
+ * values turn into inputs.
43
+ *
44
+ * One column of label/value rows until the grid itself is 832 px wide, two
45
+ * above — row-major, so the fields read left to right, top to bottom. A
46
+ * container query on the card body, not the viewport: a rail card, a dialog
47
+ * and a record page on a 1440 px window stay one column. The label keeps its
48
+ * 160 px column in each cell, so 832 px is where a half cell still leaves the
49
+ * value 228 px — a country with its flag and code, a currency, a date. Any
50
+ * narrower and the second column only squeezes inputs into stubs.
51
+ */
52
+ export declare const PAIR_GRID_CLASS = "grid grid-cols-1 gap-x-8 gap-y-2.5 @min-[832px]:grid-cols-2 [clip-path:inset(-24px_-24px_1px_-24px)]";
53
+ /** The grid-item class for a span inside {@link PAIR_GRID_CLASS}: a full field takes both columns. */
54
+ export declare function pairSpanClass(span: FieldSpan): string;
55
+ /** Every cell carries a faint rule underneath, so the eye finds each row fast (Michael 19.10.). */
56
+ export declare const PAIR_CELL_CLASS = "min-w-0 border-b border-black/[0.06] pb-2.5";
57
+ /**
58
+ * The compact control size on every form (Michael 19.10.: „stelle die
59
+ * Formularfelder auf die kleinen um … wir verlieren viel Höhe“): inputs,
60
+ * selects, comboboxes, lookups and number fields as tall as the card's own
61
+ * buttons (32 px, the design system's `FIELD_HEIGHT_SM`). Joined controls —
62
+ * phone (code + number), an input with a unit or currency addon — are an
63
+ * input group and follow too, unless the addon stacks above or below. Set on the field's
64
+ * wrapper so every control inside — studio's and ours — follows without a
65
+ * size prop threaded through each one. Textareas and the per-language stack
66
+ * keep their own height.
67
+ */
68
+ export declare const COMPACT_CONTROLS_CLASS = "[&_[data-slot=input]]:h-8 [&_[data-slot=select-trigger]]:h-8 [&_[data-slot=combobox-select-trigger]]:h-8 [&_[data-slot=combobox-anchor]]:min-h-8 [&_[data-slot=number-field-input]]:h-8 [&_[data-slot=input-group]:not(:has(>[data-align^=block]))]:h-8";
69
+ /**
70
+ * The scroll body of every modal that scrolls its fields between a fixed head
71
+ * and foot: the create wizard, the edit lightbox, an action's fields. A scroll
72
+ * container clips everything at its padding edge, focus rings included, so a
73
+ * field at the top or the side lost a stripe of its ring (Michael 19.10.:
74
+ * „betrifft das alle Modals?“). The body bleeds into the dialog's own gutter
75
+ * (24 px, 16 px on the phone sheet) and 4 px above and below, then pads the
76
+ * same amount back: content stays where it was, the ring gets room, and the
77
+ * scrollbar sits at the dialog edge. Add a height (`flex-1` in a flex column
78
+ * or a `max-h-*`) at the call site.
79
+ */
80
+ export declare const DIALOG_SCROLL_CLASS = "overflow-y-auto overscroll-contain -mx-6 px-6 -my-1 py-1 max-md:-mx-4 max-md:px-4";
81
+ /** {@link DIALOG_SCROLL_CLASS} filling the rest of a flex-column dialog. */
82
+ export declare const DIALOG_BODY_CLASS = "min-h-0 flex-1 overflow-y-auto overscroll-contain -mx-6 px-6 -my-1 py-1 max-md:-mx-4 max-md:px-4";
83
+ /**
84
+ * How wide a field's CONTROL is inside its cell (Charge 2: full-width pages,
85
+ * single fields sized by their content). The cell keeps its grid track; only
86
+ * the input shrinks — a postal code is five characters, not half a 1,400 px
87
+ * row. Label and hint keep the cell width.
88
+ *
89
+ * `null` = as wide as the cell (names, free text, references, anything long).
90
+ */
91
+ export type FieldFit = 'xs' | 'sm' | 'md' | null;
92
+ export interface FitSource {
93
+ type?: string | null;
94
+ name?: string | null;
95
+ format?: string | null;
96
+ /** An explicit fit wins; `auto`/absent lets the field decide. */
97
+ fit?: FieldFit | 'auto' | 'full' | string | null;
98
+ }
99
+ /** The control width a field gets when nothing says otherwise. */
100
+ export declare function fieldFit(field: FitSource): FieldFit;
101
+ /**
102
+ * The class that caps the control (everything in the field except its label
103
+ * and its hint/error paragraphs) at the fit's width. Whole literals for the
104
+ * Tailwind scanner.
105
+ */
106
+ export declare function fitClass(fit: FieldFit): string;
38
107
  /** Below this many sections a navigation is noise — the form fits in a glance. */
39
108
  export declare const SECTION_NAV_MIN = 3;
40
109
  /**
@@ -68,8 +137,9 @@ export declare function isScrolledToEnd(scrollTop: number, clientHeight: number,
68
137
  /**
69
138
  * The tracks of the layout grid, by which side columns are present.
70
139
  *
71
- * The form column is capped at 48rem (~760 px, the mockup's readable width) and
72
- * centred; the navigation is 10rem, the context column 16rem. Each side column
140
+ * The form column takes the full width (S19, full-width pages: no centred
141
+ * 48rem column — single fields size themselves by content); the navigation
142
+ * is 10rem, the context column 16rem. Each side column
73
143
  * only exists from the container width it fits at — the nav from `@3xl`
74
144
  * (48rem), the context column from `@5xl` (64rem) — and below that the layout
75
145
  * is the bare form column. Container queries again, because the space a form
@@ -19,7 +19,10 @@ const FULL_WIDTH_TYPES = /* @__PURE__ */ new Set([
19
19
  "record-view",
20
20
  "address",
21
21
  "tree-assign",
22
- "line-selection"
22
+ "line-selection",
23
+ // Read-side spellings of the same values on a record card.
24
+ "markets",
25
+ "gallery"
23
26
  ]);
24
27
  export function defaultSpan(fieldType) {
25
28
  if (!fieldType) return "half";
@@ -30,7 +33,51 @@ export function resolveSpan(field) {
30
33
  return defaultSpan(field.type);
31
34
  }
32
35
  export function spanClass(span) {
33
- return span === "full" ? "col-span-1 @md:col-span-2" : "col-span-1";
36
+ return span === "full" ? "col-span-1 @lg:col-span-2" : "col-span-1";
37
+ }
38
+ export const PAIR_GRID_CLASS = "grid grid-cols-1 gap-x-8 gap-y-2.5 @min-[832px]:grid-cols-2 [clip-path:inset(-24px_-24px_1px_-24px)]";
39
+ export function pairSpanClass(span) {
40
+ return span === "full" ? `${PAIR_CELL_CLASS} @min-[832px]:col-span-2` : PAIR_CELL_CLASS;
41
+ }
42
+ export const PAIR_CELL_CLASS = "min-w-0 border-b border-black/[0.06] pb-2.5";
43
+ export const COMPACT_CONTROLS_CLASS = "[&_[data-slot=input]]:h-8 [&_[data-slot=select-trigger]]:h-8 [&_[data-slot=combobox-select-trigger]]:h-8 [&_[data-slot=combobox-anchor]]:min-h-8 [&_[data-slot=number-field-input]]:h-8 [&_[data-slot=input-group]:not(:has(>[data-align^=block]))]:h-8";
44
+ export const DIALOG_SCROLL_CLASS = "overflow-y-auto overscroll-contain -mx-6 px-6 -my-1 py-1 max-md:-mx-4 max-md:px-4";
45
+ export const DIALOG_BODY_CLASS = `min-h-0 flex-1 ${DIALOG_SCROLL_CLASS}`;
46
+ const XS_NAME = /(^|_)(zip|postal_code|postcode|plz|position|sort|sort_order|priority|rank|index|moq|min_order(_qty|_quantity)?|order_interval|qty|quantity|days|day_count|hours|percent|pct|rate|discount|decimals|step)($|_)/i;
47
+ const SM_NAME = /(^|_)(code|sku|gtin|ean|iso|country|country_code|currency|vat_rate|tax_rate|customs_tariff|tariff|hs_code|unit|uom|packaging_unit|weight|width|height|depth|length|color|threshold|limit|amount|price|total|budget)($|_)/i;
48
+ const MD_NAME = /(^|_)(phone|mobile|fax|tel|email|vat_id|customer_number|number|iban|bic|username|slug|key)($|_)/i;
49
+ export function fieldFit(field) {
50
+ if (field.fit === "xs" || field.fit === "sm" || field.fit === "md") return field.fit;
51
+ if (field.fit === "full") return null;
52
+ const type = String(field.type ?? "text").trim().toLowerCase();
53
+ const name = String(field.name ?? "");
54
+ if (defaultSpan(type) === "full") return null;
55
+ if (type === "boolean" || type === "reference" || type === "relation") return null;
56
+ if (type === "date" || type === "time") return "sm";
57
+ if (type === "datetime") return "md";
58
+ if (type === "money" || type === "measure" || field.format === "money") return "sm";
59
+ if (field.format === "percent") return "xs";
60
+ if (type === "number") return XS_NAME.test(name) ? "xs" : "sm";
61
+ if (type === "tel" || type === "email" || type === "url") return "md";
62
+ if (type === "locale") return null;
63
+ if (type === "select" && /(^|_)(country|currency|language)($|_)/i.test(name)) return "md";
64
+ if (type === "select") return SM_NAME.test(name) ? "sm" : "md";
65
+ if (XS_NAME.test(name)) return "xs";
66
+ if (SM_NAME.test(name)) return "sm";
67
+ if (MD_NAME.test(name)) return "md";
68
+ return null;
69
+ }
70
+ export function fitClass(fit) {
71
+ switch (fit) {
72
+ case "xs":
73
+ return "[&>*:not(label):not(p)]:max-w-32";
74
+ case "sm":
75
+ return "[&>*:not(label):not(p)]:max-w-52";
76
+ case "md":
77
+ return "[&>*:not(label):not(p)]:max-w-80";
78
+ default:
79
+ return "";
80
+ }
34
81
  }
35
82
  export const SECTION_NAV_MIN = 3;
36
83
  export function showSectionNav(mode, sectionCount) {
@@ -53,12 +100,12 @@ export function isScrolledToEnd(scrollTop, clientHeight, scrollHeight) {
53
100
  }
54
101
  export function layoutGridClass(nav, context) {
55
102
  if (nav && context)
56
- return "grid grid-cols-[minmax(0,48rem)] justify-center gap-6 @3xl:grid-cols-[10rem_minmax(0,48rem)] @5xl:grid-cols-[10rem_minmax(0,48rem)_16rem]";
103
+ return "grid grid-cols-[minmax(0,1fr)] gap-6 @3xl:grid-cols-[10rem_minmax(0,1fr)] @5xl:grid-cols-[10rem_minmax(0,1fr)_16rem]";
57
104
  if (nav)
58
- return "grid grid-cols-[minmax(0,48rem)] justify-center gap-6 @3xl:grid-cols-[10rem_minmax(0,48rem)]";
105
+ return "grid grid-cols-[minmax(0,1fr)] gap-6 @3xl:grid-cols-[10rem_minmax(0,1fr)]";
59
106
  if (context)
60
- return "grid grid-cols-[minmax(0,48rem)] justify-center gap-6 @5xl:grid-cols-[minmax(0,48rem)_16rem]";
61
- return "grid grid-cols-[minmax(0,48rem)] justify-center gap-6";
107
+ return "grid grid-cols-[minmax(0,1fr)] gap-6 @5xl:grid-cols-[minmax(0,1fr)_16rem]";
108
+ return "grid grid-cols-[minmax(0,1fr)] gap-6";
62
109
  }
63
110
  export const NAV_COLUMN_CLASS = "hidden @3xl:block";
64
111
  export const CONTEXT_COLUMN_CLASS = "hidden @5xl:block";
@@ -26,6 +26,19 @@
26
26
  * more. Below this a column stops being a column and becomes a smear.
27
27
  */
28
28
  export declare const FLEX_TRACK_FLOOR = "120px";
29
+ /**
30
+ * A flexible column's floor that keeps its one-line heading inside the track.
31
+ *
32
+ * A heading stays on one line up to a length (`whitespace-nowrap`); squeezed
33
+ * to the 120px floor, „Was es bedeutet“ with its (?) and sort arrow ran into
34
+ * „Aktiv“ beside it (e2e 18.10.). An estimate, not a measurement: about 7.5px
35
+ * per character at the header's text size, plus room for the icons beside it.
36
+ * `undefined` when the plain floor is enough.
37
+ */
38
+ export declare function headerFloor(label: string | undefined | null, opts?: {
39
+ icons?: number;
40
+ maxChars?: number;
41
+ }): string | undefined;
29
42
  /**
30
43
  * One column's declared width, resolved to a track with a floor under it.
31
44
  *
@@ -52,4 +65,4 @@ export declare function trackFor(width: string | undefined | null, floor?: strin
52
65
  * where the manifest put it. On a list without one it is the trailing data
53
66
  * column that grows, which is where the eye is least likely to miss the space.
54
67
  */
55
- export declare function gridTemplateColumns(widths: Array<string | undefined | null>, floor?: string): string;
68
+ export declare function gridTemplateColumns(widths: Array<string | undefined | null>, floor?: string, floors?: Array<string | undefined>): string;
@@ -1,4 +1,10 @@
1
1
  export const FLEX_TRACK_FLOOR = "120px";
2
+ export function headerFloor(label, opts = {}) {
3
+ const text = (label ?? "").trim();
4
+ if (!text || opts.maxChars != null && text.length > opts.maxChars) return void 0;
5
+ const px = Math.ceil(text.length * 7.5) + (opts.icons ?? 0) * 18;
6
+ return px > Number.parseFloat(FLEX_TRACK_FLOOR) ? `${px}px` : void 0;
7
+ }
2
8
  const MINMAX_FR = /^minmax\(\s*[^,]+,\s*([\d.]+fr)\s*\)$/i;
3
9
  const BARE_FR = /^([\d.]+)fr$/i;
4
10
  export function trackFor(width, floor = FLEX_TRACK_FLOOR) {
@@ -18,8 +24,13 @@ function absorbing(track) {
18
24
  const parts = MINMAX.exec(track);
19
25
  return `minmax(${parts ? parts[2] : track},1fr)`;
20
26
  }
21
- export function gridTemplateColumns(widths, floor) {
22
- const tracks = widths.map((w) => trackFor(w, floor));
27
+ export function gridTemplateColumns(widths, floor, floors) {
28
+ const tracks = widths.map((w, i) => {
29
+ const own = floors?.[i];
30
+ const track = trackFor(w, own ?? floor);
31
+ if (own && /^\d+(\.\d+)?px$/.test(track) && Number.parseFloat(track) < Number.parseFloat(own)) return own;
32
+ return track;
33
+ });
23
34
  if (tracks.length === 0) return "";
24
35
  if (!tracks.some(isFlexible)) {
25
36
  const last = tracks.length - 1;
@@ -1,6 +1,6 @@
1
1
  /**
2
- * The two international lists every studio ends up needing: countries
3
- * (ISO 3166-1 alpha-2) and currencies (ISO 4217).
2
+ * The international lists every studio ends up needing: countries
3
+ * (ISO 3166-1 alpha-2), currencies (ISO 4217) and languages (ISO 639-1).
4
4
  *
5
5
  * They live in `studio-shared` rather than in a studio because a country is
6
6
  * not a Commerce concern — Mail wants a sender country, Analytics wants a
@@ -59,3 +59,38 @@ export declare function currencyOptions(locale?: string): IsoOption[];
59
59
  export declare function countryLabel(code: unknown, locale?: string): string;
60
60
  /** `'EUR'` → `'Euro'`. Unknown codes come back unchanged. */
61
61
  export declare function currencyLabel(code: unknown, locale?: string): string;
62
+ /** ISO 639-1, unsorted. Use `languageOptions()` to display them. */
63
+ export declare function languageCodes(): readonly string[];
64
+ /** Every language, sorted by its name in `locale` — „Deutsch“, „Englisch“ … */
65
+ export declare function languageOptions(locale?: string): IsoOption[];
66
+ /** `'de'` → `'Deutsch'`, `'de-AT'` → `'Deutsch (Österreich)'`. Unknown codes come back unchanged. */
67
+ export declare function languageLabel(code: unknown, locale?: string): string;
68
+ /**
69
+ * The circle flag a currency is drawn with (Michael 18.10.: icons in the
70
+ * currency picker, the same round look as the countries). The vendored flag
71
+ * set has no currency symbols, so a currency borrows its issuer's flag: the
72
+ * euro the EU's, every national currency its country's — ISO 4217 codes start
73
+ * with the issuing country (`CHF` → CH, `GBP` → GB). Supranational and special
74
+ * codes (`X…`: CFA franc, gold, SDR) have no single issuer and get the neutral
75
+ * disc. Returns the lowercase slug `CircleFlag` takes.
76
+ */
77
+ export declare function currencyFlag(code: unknown): string;
78
+ /** `'da'` → `'DK'`; null for a language with no single home country. */
79
+ export declare function likelyCountry(language: unknown): string | null;
80
+ /**
81
+ * The circle flag a language or locale is drawn with, as the lowercase slug
82
+ * `CircleFlag` takes: the chosen region when there is one (`de-AT` → `at`),
83
+ * otherwise the language's likely country (`de` → `de`, `en` → `gb`), and
84
+ * `''` when neither exists — the caller then shows its neutral globe.
85
+ */
86
+ export declare function localeFlag(code: unknown): string;
87
+ /**
88
+ * A locale code split into the two choices a picker offers — language and an
89
+ * optional region — and put back together (`de` + `AT` → `de-AT`). Null when
90
+ * the runtime cannot canonicalise it, which is how a nonsense tag is refused.
91
+ */
92
+ export declare function composeLocale(language: string | null | undefined, region?: string | null): string | null;
93
+ export declare function splitLocale(code: unknown): {
94
+ language: string | null;
95
+ region: string | null;
96
+ };
@@ -1,6 +1,9 @@
1
1
  const COUNTRY_CODES = Object.freeze(
2
2
  "AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW".split(" ")
3
3
  );
4
+ const LANGUAGE_CODES = Object.freeze(
5
+ "aa ab ae af ak am an ar as av ay az ba be bg bi bm bn bo br bs ca ce ch co cr cs cu cv cy da de dv dz ee el en eo es et eu fa ff fi fj fo fr fy ga gd gl gn gu gv ha he hi ho hr ht hu hy hz ia id ie ig ii ik io is it iu ja jv ka kg ki kj kk kl km kn ko kr ks ku kv kw ky la lb lg li ln lo lt lu lv mg mh mi mk ml mn mr ms mt my na nb nd ne ng nl nn no nr nv ny oc oj om or os pa pi pl ps pt qu rm rn ro ru rw sa sc sd se sg si sk sl sm sn so sq sr ss st su sv sw ta te tg th ti tk tl tn to tr ts tt tw ty ug uk ur uz ve vi vo wa wo xh yi yo za zh zu".split(" ")
6
+ );
4
7
  export const DEFAULT_ISO_LOCALE = "de";
5
8
  export function countryCodes() {
6
9
  return COUNTRY_CODES;
@@ -31,7 +34,10 @@ function collator(locale) {
31
34
  function build(codes, type, locale) {
32
35
  const names = displayNames(type, locale);
33
36
  const compare = collator(locale).compare;
34
- return codes.map((code) => ({ code, label: names?.of(code) || code })).sort((a, b) => compare(a.label, b.label));
37
+ return codes.map((code) => {
38
+ const name = names?.of(code);
39
+ return { code, label: name || code, named: !!name && name.toLowerCase() !== code.toLowerCase() };
40
+ }).sort((a, b) => Number(b.named) - Number(a.named) || compare(a.label, b.label)).map(({ code, label }) => ({ code, label }));
35
41
  }
36
42
  const cache = /* @__PURE__ */ new Map();
37
43
  function cached(key, make) {
@@ -60,3 +66,50 @@ export function currencyLabel(code, locale = DEFAULT_ISO_LOCALE) {
60
66
  if (!key) return "";
61
67
  return displayNames("currency", locale)?.of(key) || key;
62
68
  }
69
+ export function languageCodes() {
70
+ return LANGUAGE_CODES;
71
+ }
72
+ export function languageOptions(locale = DEFAULT_ISO_LOCALE) {
73
+ return cached(`language:${locale}`, () => build(LANGUAGE_CODES, "language", locale));
74
+ }
75
+ export function languageLabel(code, locale = DEFAULT_ISO_LOCALE) {
76
+ const key = typeof code === "string" ? code.trim().replace("_", "-") : "";
77
+ if (!key) return "";
78
+ return displayNames("language", locale)?.of(key) || key;
79
+ }
80
+ export function currencyFlag(code) {
81
+ const key = typeof code === "string" ? code.trim().toUpperCase() : "";
82
+ if (key === "EUR") return "eu";
83
+ if (!/^[A-Z]{3}$/.test(key) || key.startsWith("X")) return "xx";
84
+ const issuer = key.slice(0, 2);
85
+ return COUNTRY_CODES.includes(issuer) ? issuer.toLowerCase() : "xx";
86
+ }
87
+ const LIKELY_COUNTRY = Object.freeze(Object.fromEntries(
88
+ "aa:ET ab:GE af:ZA ak:GH am:ET an:ES ar:EG as:IN av:RU ay:BO az:AZ ba:RU be:BY bg:BG bi:VU bm:ML bn:BD bo:CN br:FR bs:BA ca:ES ce:RU ch:GU co:FR cr:CA cs:CZ cv:RU cy:GB da:DK de:DE dv:MV dz:BT ee:GH el:GR en:GB es:ES et:EE eu:ES fa:IR ff:SN fi:FI fj:FJ fo:FO fr:FR fy:NL ga:IE gd:GB gl:ES gn:PY gu:IN gv:IM ha:NG he:IL hi:IN ho:PG hr:HR ht:HT hu:HU hy:AM hz:NA id:ID ig:NG ii:CN ik:US is:IS it:IT iu:CA ja:JP jv:ID ka:GE kg:CD ki:KE kj:NA kk:KZ kl:GL km:KH kn:IN ko:KR kr:NG ks:IN ku:TR kv:RU kw:GB ky:KG lb:LU lg:UG li:NL ln:CD lo:LA lt:LT lu:CD lv:LV mg:MG mh:MH mi:NZ mk:MK ml:IN mn:MN mr:IN ms:MY mt:MT my:MM na:NR nb:NO nd:ZW ne:NP ng:NA nl:NL nn:NO no:NO nr:ZA nv:US ny:MW oc:FR oj:CA om:ET or:IN os:GE pa:IN pl:PL ps:AF pt:PT qu:PE rm:CH rn:BI ro:RO ru:RU rw:RW sa:IN sc:IT sd:PK se:NO sg:CF si:LK sk:SK sl:SI sm:WS sn:ZW so:SO sq:AL sr:RS ss:ZA st:ZA su:ID sv:SE sw:TZ ta:IN te:IN tg:TJ th:TH ti:ET tk:TM tl:PH tn:ZA to:TO tr:TR ts:ZA tt:RU tw:GH ty:PF ug:CN uk:UA ur:PK uz:UZ ve:ZA vi:VN wa:BE wo:SN xh:ZA yo:NG za:CN zh:CN zu:ZA".split(" ").map((pair) => pair.split(":"))
89
+ ));
90
+ export function likelyCountry(language) {
91
+ const key = typeof language === "string" ? language.trim().toLowerCase() : "";
92
+ return LIKELY_COUNTRY[key] ?? null;
93
+ }
94
+ export function localeFlag(code) {
95
+ const { language, region } = splitLocale(code);
96
+ if (region) return region.toLowerCase();
97
+ return likelyCountry(language)?.toLowerCase() ?? "";
98
+ }
99
+ export function composeLocale(language, region) {
100
+ const lang = (language ?? "").trim().toLowerCase();
101
+ if (!/^[a-z]{2,3}$/.test(lang)) return null;
102
+ const reg = (region ?? "").trim().toUpperCase();
103
+ const tag = reg ? `${lang}-${reg}` : lang;
104
+ try {
105
+ return Intl.getCanonicalLocales(tag)[0] ?? null;
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+ export function splitLocale(code) {
111
+ const raw = typeof code === "string" ? code.trim().replace("_", "-") : "";
112
+ if (!raw) return { language: null, region: null };
113
+ const [language, region] = raw.split("-");
114
+ return { language: language?.toLowerCase() || null, region: region && /^[A-Za-z]{2}$/.test(region) ? region.toUpperCase() : null };
115
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Headline + subtitle out of one paragraph (EmptyNote, Michael 19.10.).
3
+ *
4
+ * Copy written as a single paragraph — a placeholder tab, an empty-state
5
+ * sentence — still has to read as a headline with an explanation under it.
6
+ * The first sentence heads, the rest explains. An abbreviation ("z. B.",
7
+ * "bzw.", "Nr.") does not end a sentence, so a short or dotted word before
8
+ * the period never splits.
9
+ */
10
+ export interface NoticeParts {
11
+ title: string;
12
+ description: string | null;
13
+ }
14
+ export declare function splitNotice(text: string | null | undefined): NoticeParts;