@revenexx/studio-shared 0.3.2 → 0.4.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/module.d.mts +6 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +12 -1
- package/dist/runtime/components/Dashboard/DashboardConfirmDialog.d.vue.ts +16 -0
- package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue +39 -0
- package/dist/runtime/components/Dashboard/DashboardConfirmDialog.vue.d.ts +16 -0
- package/dist/runtime/components/Dashboard/DashboardControls.d.vue.ts +58 -0
- package/dist/runtime/components/Dashboard/DashboardControls.vue +209 -0
- package/dist/runtime/components/Dashboard/DashboardControls.vue.d.ts +58 -0
- package/dist/runtime/components/Dashboard/DashboardGrid.d.vue.ts +17 -0
- package/dist/runtime/components/Dashboard/DashboardGrid.vue +87 -0
- package/dist/runtime/components/Dashboard/DashboardGrid.vue.d.ts +17 -0
- package/dist/runtime/components/Dashboard/DashboardNameDialog.d.vue.ts +19 -0
- package/dist/runtime/components/Dashboard/DashboardNameDialog.vue +61 -0
- package/dist/runtime/components/Dashboard/DashboardNameDialog.vue.d.ts +19 -0
- package/dist/runtime/components/Dashboard/DashboardPickerDialog.d.vue.ts +15 -0
- package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue +100 -0
- package/dist/runtime/components/Dashboard/DashboardPickerDialog.vue.d.ts +15 -0
- package/dist/runtime/components/Dashboard/DashboardShareDialog.d.vue.ts +28 -0
- package/dist/runtime/components/Dashboard/DashboardShareDialog.vue +155 -0
- package/dist/runtime/components/Dashboard/DashboardShareDialog.vue.d.ts +28 -0
- package/dist/runtime/components/Dashboard/DashboardTopBar.d.vue.ts +64 -0
- package/dist/runtime/components/Dashboard/DashboardTopBar.vue +79 -0
- package/dist/runtime/components/Dashboard/DashboardTopBar.vue.d.ts +64 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.d.vue.ts +44 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue +153 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetConfigDialog.vue.d.ts +44 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetFrame.d.vue.ts +28 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue +80 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetFrame.vue.d.ts +28 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetHost.d.vue.ts +7 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetHost.vue +30 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetHost.vue.d.ts +7 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetIcon.d.vue.ts +9 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetIcon.vue +13 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetIcon.vue.d.ts +9 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.d.vue.ts +24 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.vue +140 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetLibrary.vue.d.ts +24 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetPreview.d.vue.ts +23 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetPreview.vue +124 -0
- package/dist/runtime/components/Dashboard/DashboardWidgetPreview.vue.d.ts +23 -0
- package/dist/runtime/components/DataTable/ColumnsMenu.d.vue.ts +2 -1
- package/dist/runtime/components/DataTable/ColumnsMenu.vue.d.ts +2 -1
- package/dist/runtime/components/DataTable/DataTable.d.vue.ts +105 -28
- package/dist/runtime/components/DataTable/DataTable.vue +403 -38
- package/dist/runtime/components/DataTable/DataTable.vue.d.ts +105 -28
- package/dist/runtime/components/DataTable/FacetChips.d.vue.ts +17 -0
- package/dist/runtime/components/DataTable/FacetChips.vue +72 -0
- package/dist/runtime/components/DataTable/FacetChips.vue.d.ts +17 -0
- package/dist/runtime/components/DataTable/FacetPanel.d.vue.ts +21 -0
- package/dist/runtime/components/DataTable/FacetPanel.vue +359 -0
- package/dist/runtime/components/DataTable/FacetPanel.vue.d.ts +21 -0
- package/dist/runtime/components/DataTable/FilterMenu.d.vue.ts +6 -5
- package/dist/runtime/components/DataTable/FilterMenu.vue.d.ts +6 -5
- package/dist/runtime/components/DataTable/SavedViewsMenu.d.vue.ts +6 -1
- package/dist/runtime/components/DataTable/SavedViewsMenu.vue +83 -63
- package/dist/runtime/components/DataTable/SavedViewsMenu.vue.d.ts +6 -1
- package/dist/runtime/components/DataTable/SortMenu.d.vue.ts +9 -3
- package/dist/runtime/components/DataTable/SortMenu.vue +40 -12
- package/dist/runtime/components/DataTable/SortMenu.vue.d.ts +9 -3
- package/dist/runtime/components/DataTable/ViewModeSwitch.d.vue.ts +4 -2
- package/dist/runtime/components/DataTable/ViewModeSwitch.vue.d.ts +4 -2
- package/dist/runtime/components/Form/FieldCell.d.vue.ts +32 -0
- package/dist/runtime/components/Form/FieldCell.vue +46 -0
- package/dist/runtime/components/Form/FieldCell.vue.d.ts +32 -0
- package/dist/runtime/components/Form/FieldGrid.d.vue.ts +13 -0
- package/dist/runtime/components/Form/FieldGrid.vue +11 -0
- package/dist/runtime/components/Form/FieldGrid.vue.d.ts +13 -0
- package/dist/runtime/components/Form/FormContextCard.d.vue.ts +33 -0
- package/dist/runtime/components/Form/FormContextCard.vue +56 -0
- package/dist/runtime/components/Form/FormContextCard.vue.d.ts +33 -0
- package/dist/runtime/components/Form/FormLayout.d.vue.ts +47 -0
- package/dist/runtime/components/Form/FormLayout.vue +169 -0
- package/dist/runtime/components/Form/FormLayout.vue.d.ts +47 -0
- package/dist/runtime/components/Form/FormSection.d.vue.ts +25 -0
- package/dist/runtime/components/Form/FormSection.vue +48 -0
- package/dist/runtime/components/Form/FormSection.vue.d.ts +25 -0
- package/dist/runtime/components/Form/SaveBar.d.vue.ts +44 -0
- package/dist/runtime/components/Form/SaveBar.vue +71 -0
- package/dist/runtime/components/Form/SaveBar.vue.d.ts +44 -0
- package/dist/runtime/components/Media/MediaFolderTree.d.vue.ts +2 -1
- package/dist/runtime/components/Media/MediaFolderTree.vue.d.ts +2 -1
- package/dist/runtime/components/Media/MediaPickerDialog.d.vue.ts +4 -3
- package/dist/runtime/components/Media/MediaPickerDialog.vue.d.ts +4 -3
- package/dist/runtime/components/Media/MediaUploader.d.vue.ts +4 -3
- package/dist/runtime/components/Media/MediaUploader.vue.d.ts +4 -3
- package/dist/runtime/components/Shared/AppIcon.d.vue.ts +21 -0
- package/dist/runtime/components/Shared/AppIcon.vue +47 -0
- package/dist/runtime/components/Shared/AppIcon.vue.d.ts +21 -0
- package/dist/runtime/components/Shared/BuildLogsFlyout.d.vue.ts +2 -1
- package/dist/runtime/components/Shared/BuildLogsFlyout.vue.d.ts +2 -1
- package/dist/runtime/components/Shared/DetailSheet.d.vue.ts +60 -0
- package/dist/runtime/components/Shared/DetailSheet.vue +207 -0
- package/dist/runtime/components/Shared/DetailSheet.vue.d.ts +60 -0
- package/dist/runtime/components/Shared/FloatingPanel.d.vue.ts +7 -6
- package/dist/runtime/components/Shared/FloatingPanel.vue.d.ts +7 -6
- package/dist/runtime/components/Shared/GitDeploymentSettings.d.vue.ts +2 -1
- package/dist/runtime/components/Shared/GitDeploymentSettings.vue.d.ts +2 -1
- package/dist/runtime/components/Shared/KpiTile.d.vue.ts +2 -1
- package/dist/runtime/components/Shared/KpiTile.vue.d.ts +2 -1
- package/dist/runtime/components/Shared/ListSurface.d.vue.ts +3 -2
- package/dist/runtime/components/Shared/ListSurface.vue.d.ts +3 -2
- package/dist/runtime/components/Shared/PageHeader.d.vue.ts +7 -6
- package/dist/runtime/components/Shared/PageHeader.vue.d.ts +7 -6
- package/dist/runtime/components/Shared/RecordLayout.d.vue.ts +8 -7
- package/dist/runtime/components/Shared/RecordLayout.vue.d.ts +8 -7
- package/dist/runtime/components/Shared/RecordRailCard.d.vue.ts +3 -2
- package/dist/runtime/components/Shared/RecordRailCard.vue.d.ts +3 -2
- package/dist/runtime/components/Shared/SidebarNav.d.vue.ts +3 -2
- package/dist/runtime/components/Shared/SidebarNav.vue.d.ts +3 -2
- package/dist/runtime/components/Shared/SidebarNavLink.d.vue.ts +2 -1
- package/dist/runtime/components/Shared/SidebarNavLink.vue.d.ts +2 -1
- package/dist/runtime/components/Shared/SortHeader.d.vue.ts +3 -2
- package/dist/runtime/components/Shared/SortHeader.vue +6 -2
- package/dist/runtime/components/Shared/SortHeader.vue.d.ts +3 -2
- package/dist/runtime/components/Shared/StateCard.d.vue.ts +6 -5
- package/dist/runtime/components/Shared/StateCard.vue.d.ts +6 -5
- package/dist/runtime/components/Shared/StudioCard.d.vue.ts +5 -4
- package/dist/runtime/components/Shared/StudioCard.vue.d.ts +5 -4
- package/dist/runtime/components/Shared/TabBar.d.vue.ts +9 -10
- package/dist/runtime/components/Shared/TabBar.vue +26 -7
- package/dist/runtime/components/Shared/TabBar.vue.d.ts +9 -10
- package/dist/runtime/components/Shared/ViewTab.d.vue.ts +2 -1
- package/dist/runtime/components/Shared/ViewTab.vue.d.ts +2 -1
- package/dist/runtime/components/Shared/Wizard.d.vue.ts +10 -7
- package/dist/runtime/components/Shared/Wizard.vue.d.ts +10 -7
- package/dist/runtime/composables/useDashboards.d.ts +27 -0
- package/dist/runtime/composables/useDashboards.js +143 -0
- package/dist/runtime/composables/useDetailSheet.d.ts +20 -0
- package/dist/runtime/composables/useDetailSheet.js +50 -0
- package/dist/runtime/composables/usePlatformTenant.d.ts +6 -0
- package/dist/runtime/composables/useSavedViews.d.ts +44 -0
- package/dist/runtime/composables/useSavedViews.js +44 -0
- package/dist/runtime/composables/useStickyListView.d.ts +60 -0
- package/dist/runtime/composables/useStickyListView.js +32 -0
- package/dist/runtime/lib/activeTenant.d.ts +58 -0
- package/dist/runtime/lib/activeTenant.js +45 -0
- package/dist/runtime/lib/appIcon.d.ts +48 -0
- package/dist/runtime/lib/appIcon.js +27 -0
- package/dist/runtime/lib/canonicalJson.d.ts +43 -0
- package/dist/runtime/lib/canonicalJson.js +38 -0
- package/dist/runtime/lib/dashboardIcons.d.ts +4 -0
- package/dist/runtime/lib/dashboardIcons.js +50 -0
- package/dist/runtime/lib/detailSheet.d.ts +43 -0
- package/dist/runtime/lib/detailSheet.js +34 -0
- package/dist/runtime/lib/facets.d.ts +200 -0
- package/dist/runtime/lib/facets.js +171 -0
- package/dist/runtime/lib/formLayout.d.ts +94 -0
- package/dist/runtime/lib/formLayout.js +70 -0
- package/dist/runtime/lib/formLayoutContext.d.ts +26 -0
- package/dist/runtime/lib/formLayoutContext.js +1 -0
- package/dist/runtime/lib/gridTracks.d.ts +55 -0
- package/dist/runtime/lib/gridTracks.js +29 -0
- package/dist/runtime/lib/rowMove.d.ts +38 -0
- package/dist/runtime/lib/rowMove.js +20 -0
- package/dist/runtime/lib/solarIconMap.d.ts +15 -0
- package/dist/runtime/lib/solarIconMap.js +280 -0
- package/dist/runtime/lib/solarIcons.d.ts +37 -0
- package/dist/runtime/lib/solarIcons.js +25 -0
- package/dist/runtime/lib/widgetRegistry.d.ts +124 -0
- package/dist/runtime/lib/widgetRegistry.js +66 -0
- package/dist/runtime/types/dashboard.d.ts +95 -0
- package/dist/runtime/types/dashboard.js +2 -0
- package/dist/types.d.mts +12 -0
- package/package.json +3 -2
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One facet model for every way a list gets narrowed (PE-6xx).
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* The Commerce Studio grew seven mechanisms for the same job. Five of them
|
|
7
|
+
* describe the identical thing in different words:
|
|
8
|
+
*
|
|
9
|
+
* - `filters[type=select]` — a column, a vocabulary, options
|
|
10
|
+
* - `filters[type=multi-select]`— the same, several values
|
|
11
|
+
* - `filters[type=boolean]` — the same, two values
|
|
12
|
+
* - `filters[type=text]` — the same column, matched by substring
|
|
13
|
+
* - `filters[type=date-range]` — the same column, bounded
|
|
14
|
+
* - `rail.sections[type=facet]` — a column, a vocabulary, options. Again.
|
|
15
|
+
* - `rail.sections[type=tree]` — a column reached through a junction
|
|
16
|
+
*
|
|
17
|
+
* Each was rendered by its own control, in its own place, with its own commit
|
|
18
|
+
* behaviour, and none of them could hold more than one value except the one
|
|
19
|
+
* type nobody declared. This module is the single shape all of them reduce to,
|
|
20
|
+
* so a surface declares a dimension once and the CONTROL is a rendering
|
|
21
|
+
* decision rather than a second grammar.
|
|
22
|
+
*
|
|
23
|
+
* ## A facet is a question, not a widget
|
|
24
|
+
*
|
|
25
|
+
* `kind` says what shape the answer takes — a set of options, a typed string, a
|
|
26
|
+
* bounded range, a node in a hierarchy. It does NOT say where the control
|
|
27
|
+
* lives, because there is only one: the filter panel. An earlier pass gave a
|
|
28
|
+
* facet a `place` so a catalogue tree could stay pinned in a left rail, and
|
|
29
|
+
* that hedge put the studio back where it started — two places to look, two
|
|
30
|
+
* commit rules, and no way for an app author to know which one a dimension
|
|
31
|
+
* belonged in.
|
|
32
|
+
*
|
|
33
|
+
* ## Every facet is multi-valued
|
|
34
|
+
*
|
|
35
|
+
* `check` and `tree` hold a LIST. That is the point of the rework: "active or
|
|
36
|
+
* draft" was not a question the product could ask, because a `select` mapped to
|
|
37
|
+
* `eq.` and a submenu could only tick one row. Within a facet the values are
|
|
38
|
+
* OR'd, across facets AND'd — "a draft or a lead, in Germany".
|
|
39
|
+
*
|
|
40
|
+
* `text` and `date-range` hold one value each because there is only one to
|
|
41
|
+
* hold; they are still facets, and they still live in the panel rather than
|
|
42
|
+
* growing a second control in the toolbar.
|
|
43
|
+
*/
|
|
44
|
+
export type FacetKind = 'check' | 'text' | 'date-range' | 'tree';
|
|
45
|
+
export interface FacetOption {
|
|
46
|
+
value: string;
|
|
47
|
+
label: string;
|
|
48
|
+
/**
|
|
49
|
+
* How many rows match. Shown where known — "Churned 4" is the difference
|
|
50
|
+
* between a guess and a decision. Left `undefined` where the surface could
|
|
51
|
+
* not ask cheaply; a facet with no counts renders bare rather than lying
|
|
52
|
+
* with a zero.
|
|
53
|
+
*/
|
|
54
|
+
count?: number;
|
|
55
|
+
/** Greyed and unclickable, kept visible so the dimension still reads whole. */
|
|
56
|
+
disabled?: boolean;
|
|
57
|
+
}
|
|
58
|
+
/** One node of a `tree` facet. */
|
|
59
|
+
export interface FacetNode {
|
|
60
|
+
value: string;
|
|
61
|
+
label: string;
|
|
62
|
+
count?: number;
|
|
63
|
+
children?: FacetNode[];
|
|
64
|
+
}
|
|
65
|
+
export interface Facet {
|
|
66
|
+
/** Stable id. Namespaces this facet's values in the flat selection. */
|
|
67
|
+
key: string;
|
|
68
|
+
label: string;
|
|
69
|
+
kind: FacetKind;
|
|
70
|
+
/** `check` — the values on offer. */
|
|
71
|
+
options?: FacetOption[];
|
|
72
|
+
/** `tree` — the hierarchy on offer. */
|
|
73
|
+
nodes?: FacetNode[];
|
|
74
|
+
/** Placeholder for a `text` facet's input. */
|
|
75
|
+
placeholder?: string;
|
|
76
|
+
/** One line under the pane saying what the facet actually matches. */
|
|
77
|
+
hint?: string;
|
|
78
|
+
/**
|
|
79
|
+
* The pane is still loading its options. Renders a skeleton instead of an
|
|
80
|
+
* empty list, which otherwise reads as "this dimension has no values".
|
|
81
|
+
*/
|
|
82
|
+
loading?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* One value at a time — picking a second replaces the first.
|
|
85
|
+
*
|
|
86
|
+
* For a dimension whose values are alternatives rather than a set. A list's
|
|
87
|
+
* work queues are the case this exists for: "waiting for approval" AND
|
|
88
|
+
* "blocked" at once is not a wider question, it is a query that matches
|
|
89
|
+
* nothing, so offering it as a pair of checkboxes offers a dead end.
|
|
90
|
+
*/
|
|
91
|
+
single?: boolean;
|
|
92
|
+
}
|
|
93
|
+
/** `{ from, to }`, either side optional. */
|
|
94
|
+
export interface FacetRange {
|
|
95
|
+
from?: string;
|
|
96
|
+
to?: string;
|
|
97
|
+
}
|
|
98
|
+
export type FacetValue = string[] | string | FacetRange;
|
|
99
|
+
/**
|
|
100
|
+
* What is selected, keyed by facet. A map rather than the flat
|
|
101
|
+
* `"status:active"` array the design system's own panel takes, because `text`
|
|
102
|
+
* and `date-range` values do not survive being squashed into a string that is
|
|
103
|
+
* split on its first colon — a typed `"12:30"` or an ISO instant would break
|
|
104
|
+
* apart. {@link facetValueIds} produces the flat form where a component wants
|
|
105
|
+
* it (chips, aria labels).
|
|
106
|
+
*/
|
|
107
|
+
export type FacetSelection = Record<string, FacetValue>;
|
|
108
|
+
/**
|
|
109
|
+
* Is this value "no answer"? The one place that decides, because three
|
|
110
|
+
* different guesses at it is how a cleared filter keeps narrowing a query.
|
|
111
|
+
*
|
|
112
|
+
* `false` is a real answer, not emptiness — a boolean facet whose only tick is
|
|
113
|
+
* "No" must survive. Which is why this checks the SHAPE and never truthiness.
|
|
114
|
+
*/
|
|
115
|
+
export declare function isEmptyFacetValue(value: FacetValue | null | undefined): boolean;
|
|
116
|
+
/** The selection with every empty entry dropped. What a query should read. */
|
|
117
|
+
export declare function pruneSelection(selection: FacetSelection): FacetSelection;
|
|
118
|
+
/**
|
|
119
|
+
* How many values are set inside one facet — the badge on its row in the panel.
|
|
120
|
+
*
|
|
121
|
+
* A `text` or `date-range` facet counts as one when answered: the badge says
|
|
122
|
+
* "this dimension is narrowing", and "1" is the honest number for one typed
|
|
123
|
+
* string. Counting its characters, or a range's two bounds as two, would make
|
|
124
|
+
* the panel's total disagree with the number of chips beside it.
|
|
125
|
+
*/
|
|
126
|
+
export declare function countInFacet(selection: FacetSelection, key: string): number;
|
|
127
|
+
/** Everything set, across every facet. The count on the toolbar trigger. */
|
|
128
|
+
export declare function countSelected(facets: Facet[], selection: FacetSelection): number;
|
|
129
|
+
/** Depth-first walk, descending only into expanded nodes. */
|
|
130
|
+
export declare function flattenNodes(nodes: FacetNode[], expanded: ReadonlySet<string>): Array<{
|
|
131
|
+
node: FacetNode;
|
|
132
|
+
depth: number;
|
|
133
|
+
}>;
|
|
134
|
+
/** Every node, flat and depthless. What a search filters over. */
|
|
135
|
+
export declare function allNodes(nodes: FacetNode[]): FacetNode[];
|
|
136
|
+
/**
|
|
137
|
+
* The path from a root down to `value`, inclusive. What a chip reads
|
|
138
|
+
* ("Tools › Hand tools"): a deep node's own label is frequently ambiguous on
|
|
139
|
+
* its own — three branches of a catalogue each have an "Accessories".
|
|
140
|
+
*/
|
|
141
|
+
export declare function nodePath(nodes: FacetNode[], value: string): FacetNode[] | null;
|
|
142
|
+
/**
|
|
143
|
+
* The ids on the path ABOVE `value`, so a node selected from a saved view or a
|
|
144
|
+
* restored URL is revealed rather than left selected and invisible.
|
|
145
|
+
*/
|
|
146
|
+
export declare function expandTo(nodes: FacetNode[], value: string | null | undefined): Set<string>;
|
|
147
|
+
/** The set a fresh tree opens with: the roots, and nothing deeper. */
|
|
148
|
+
export declare function initialExpanded(nodes: FacetNode[]): Set<string>;
|
|
149
|
+
export interface AppliedFacet {
|
|
150
|
+
/** Flat id, unique across the selection — the chip's `key`. */
|
|
151
|
+
id: string;
|
|
152
|
+
facetKey: string;
|
|
153
|
+
facetLabel: string;
|
|
154
|
+
/** What the chip says. A tree node carries its path. */
|
|
155
|
+
label: string;
|
|
156
|
+
/** The single value this chip removes. `undefined` clears the whole facet. */
|
|
157
|
+
value?: string;
|
|
158
|
+
}
|
|
159
|
+
/** The flat ids of one facet's values, for a chip `key` or an aria label. */
|
|
160
|
+
export declare function facetValueIds(key: string, value: FacetValue): string[];
|
|
161
|
+
/**
|
|
162
|
+
* What is filtering right now, in the order the facets are DECLARED rather
|
|
163
|
+
* than the order they were clicked — a chip strip that reshuffles itself as you
|
|
164
|
+
* work is one people stop reading.
|
|
165
|
+
*
|
|
166
|
+
* `rangeLabel` and `textLabel` are passed in rather than built here: a range
|
|
167
|
+
* reads through the operator's own date format and a typed string wants
|
|
168
|
+
* quotation marks the UI language agrees with, and neither belongs in a pure
|
|
169
|
+
* derivation lib.
|
|
170
|
+
*/
|
|
171
|
+
export declare function appliedFacets(facets: Facet[], selection: FacetSelection, format: {
|
|
172
|
+
range: (range: FacetRange) => string;
|
|
173
|
+
text: (value: string) => string;
|
|
174
|
+
}): AppliedFacet[];
|
|
175
|
+
/**
|
|
176
|
+
* Tick or untick one value of a multi-valued facet, returning a NEW selection.
|
|
177
|
+
*
|
|
178
|
+
* A facet whose last value is removed is deleted from the map rather than left
|
|
179
|
+
* as an empty array: `pruneSelection` would drop it anyway, and a lingering
|
|
180
|
+
* `{ status: [] }` makes two selections that mean the same thing compare
|
|
181
|
+
* unequal — which is what a dirty-check and a saved view both rely on.
|
|
182
|
+
*/
|
|
183
|
+
export declare function toggleFacetValue(selection: FacetSelection, key: string, value: string, on: boolean, opts?: {
|
|
184
|
+
single?: boolean;
|
|
185
|
+
}): FacetSelection;
|
|
186
|
+
/** Set (or clear) a `text` / `date-range` facet, returning a NEW selection. */
|
|
187
|
+
export declare function setFacetValue(selection: FacetSelection, key: string, value: FacetValue): FacetSelection;
|
|
188
|
+
/** Remove one chip. Clears the whole facet for the single-valued kinds. */
|
|
189
|
+
export declare function clearFacetValue(selection: FacetSelection, key: string, value?: string): FacetSelection;
|
|
190
|
+
/**
|
|
191
|
+
* Do two selections mean the same thing? Drives the Apply button's dot and the
|
|
192
|
+
* decision to re-query at all.
|
|
193
|
+
*
|
|
194
|
+
* Order-insensitive within a facet: ticking A then B and ticking B then A are
|
|
195
|
+
* the same filter, and a dirty flag that disagreed would offer an Apply that
|
|
196
|
+
* changes nothing.
|
|
197
|
+
*/
|
|
198
|
+
export declare function sameSelection(a: FacetSelection, b: FacetSelection): boolean;
|
|
199
|
+
/** Deep-enough copy for a draft. The values are strings, arrays and flat pairs. */
|
|
200
|
+
export declare function copySelection(selection: FacetSelection): FacetSelection;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
export function isEmptyFacetValue(value) {
|
|
2
|
+
if (value === null || value === void 0) return true;
|
|
3
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
4
|
+
if (typeof value === "string") return value.trim() === "";
|
|
5
|
+
if (typeof value === "object") {
|
|
6
|
+
const range = value;
|
|
7
|
+
return !range.from?.trim() && !range.to?.trim();
|
|
8
|
+
}
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
export function pruneSelection(selection) {
|
|
12
|
+
const out = {};
|
|
13
|
+
for (const [key, value] of Object.entries(selection)) {
|
|
14
|
+
if (!isEmptyFacetValue(value)) out[key] = value;
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
export function countInFacet(selection, key) {
|
|
19
|
+
const value = selection[key];
|
|
20
|
+
if (isEmptyFacetValue(value)) return 0;
|
|
21
|
+
return Array.isArray(value) ? value.length : 1;
|
|
22
|
+
}
|
|
23
|
+
export function countSelected(facets, selection) {
|
|
24
|
+
return facets.reduce((n, facet) => n + countInFacet(selection, facet.key), 0);
|
|
25
|
+
}
|
|
26
|
+
export function flattenNodes(nodes, expanded) {
|
|
27
|
+
const out = [];
|
|
28
|
+
const walk = (list, depth) => {
|
|
29
|
+
for (const node of list) {
|
|
30
|
+
out.push({ node, depth });
|
|
31
|
+
if (node.children?.length && expanded.has(node.value)) walk(node.children, depth + 1);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
walk(nodes, 0);
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
export function allNodes(nodes) {
|
|
38
|
+
const out = [];
|
|
39
|
+
const walk = (list) => {
|
|
40
|
+
for (const node of list) {
|
|
41
|
+
out.push(node);
|
|
42
|
+
if (node.children?.length) walk(node.children);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
walk(nodes);
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
export function nodePath(nodes, value) {
|
|
49
|
+
const walk = (list, trail) => {
|
|
50
|
+
for (const node of list) {
|
|
51
|
+
const here = [...trail, node];
|
|
52
|
+
if (node.value === value) return here;
|
|
53
|
+
if (node.children?.length) {
|
|
54
|
+
const hit = walk(node.children, here);
|
|
55
|
+
if (hit) return hit;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
};
|
|
60
|
+
return walk(nodes, []);
|
|
61
|
+
}
|
|
62
|
+
export function expandTo(nodes, value) {
|
|
63
|
+
if (!value) return /* @__PURE__ */ new Set();
|
|
64
|
+
const path = nodePath(nodes, value);
|
|
65
|
+
if (!path) return /* @__PURE__ */ new Set();
|
|
66
|
+
return new Set(path.slice(0, -1).map((node) => node.value));
|
|
67
|
+
}
|
|
68
|
+
export function initialExpanded(nodes) {
|
|
69
|
+
return new Set(nodes.filter((node) => node.children?.length).map((node) => node.value));
|
|
70
|
+
}
|
|
71
|
+
export function facetValueIds(key, value) {
|
|
72
|
+
if (isEmptyFacetValue(value)) return [];
|
|
73
|
+
if (Array.isArray(value)) return value.map((one) => `${key}:${one}`);
|
|
74
|
+
return [`${key}:`];
|
|
75
|
+
}
|
|
76
|
+
function optionLabel(facet, value) {
|
|
77
|
+
if (facet.kind === "tree") {
|
|
78
|
+
const path = nodePath(facet.nodes ?? [], value);
|
|
79
|
+
return path?.length ? path.map((node) => node.label).join(" \u203A ") : value;
|
|
80
|
+
}
|
|
81
|
+
return facet.options?.find((option) => option.value === value)?.label ?? value;
|
|
82
|
+
}
|
|
83
|
+
export function appliedFacets(facets, selection, format) {
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const facet of facets) {
|
|
86
|
+
const value = selection[facet.key];
|
|
87
|
+
if (isEmptyFacetValue(value)) continue;
|
|
88
|
+
if (Array.isArray(value)) {
|
|
89
|
+
for (const one of value) {
|
|
90
|
+
out.push({
|
|
91
|
+
id: `${facet.key}:${one}`,
|
|
92
|
+
facetKey: facet.key,
|
|
93
|
+
facetLabel: facet.label,
|
|
94
|
+
label: optionLabel(facet, one),
|
|
95
|
+
value: one
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
out.push({
|
|
101
|
+
id: `${facet.key}:`,
|
|
102
|
+
facetKey: facet.key,
|
|
103
|
+
facetLabel: facet.label,
|
|
104
|
+
label: typeof value === "string" ? format.text(value) : format.range(value)
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
export function toggleFacetValue(selection, key, value, on, opts = {}) {
|
|
110
|
+
if (opts.single) {
|
|
111
|
+
const next2 = { ...selection };
|
|
112
|
+
if (on) next2[key] = [value];
|
|
113
|
+
else delete next2[key];
|
|
114
|
+
return next2;
|
|
115
|
+
}
|
|
116
|
+
const current = selection[key];
|
|
117
|
+
const set = new Set(Array.isArray(current) ? current : []);
|
|
118
|
+
if (on) set.add(value);
|
|
119
|
+
else set.delete(value);
|
|
120
|
+
const next = { ...selection };
|
|
121
|
+
if (set.size) next[key] = [...set];
|
|
122
|
+
else delete next[key];
|
|
123
|
+
return next;
|
|
124
|
+
}
|
|
125
|
+
export function setFacetValue(selection, key, value) {
|
|
126
|
+
const next = { ...selection };
|
|
127
|
+
if (isEmptyFacetValue(value)) delete next[key];
|
|
128
|
+
else next[key] = value;
|
|
129
|
+
return next;
|
|
130
|
+
}
|
|
131
|
+
export function clearFacetValue(selection, key, value) {
|
|
132
|
+
if (value === void 0) {
|
|
133
|
+
const next = { ...selection };
|
|
134
|
+
delete next[key];
|
|
135
|
+
return next;
|
|
136
|
+
}
|
|
137
|
+
return toggleFacetValue(selection, key, value, false);
|
|
138
|
+
}
|
|
139
|
+
export function sameSelection(a, b) {
|
|
140
|
+
const left = pruneSelection(a);
|
|
141
|
+
const right = pruneSelection(b);
|
|
142
|
+
const keys = Object.keys(left);
|
|
143
|
+
if (keys.length !== Object.keys(right).length) return false;
|
|
144
|
+
for (const key of keys) {
|
|
145
|
+
const l = left[key];
|
|
146
|
+
const r = right[key];
|
|
147
|
+
if (r === void 0) return false;
|
|
148
|
+
if (Array.isArray(l) !== Array.isArray(r)) return false;
|
|
149
|
+
if (Array.isArray(l) && Array.isArray(r)) {
|
|
150
|
+
if (l.length !== r.length) return false;
|
|
151
|
+
const sorted = [...r].sort();
|
|
152
|
+
if ([...l].sort().some((one, i) => one !== sorted[i])) return false;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (typeof l === "string" || typeof r === "string") {
|
|
156
|
+
if (l !== r) return false;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const lr = l;
|
|
160
|
+
const rr = r;
|
|
161
|
+
if ((lr.from ?? "") !== (rr.from ?? "") || (lr.to ?? "") !== (rr.to ?? "")) return false;
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
export function copySelection(selection) {
|
|
166
|
+
const out = {};
|
|
167
|
+
for (const [key, value] of Object.entries(selection)) {
|
|
168
|
+
out[key] = Array.isArray(value) ? [...value] : typeof value === "string" ? value : { ...value };
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The decisions behind the shared form layout (PE-976), as pure functions.
|
|
3
|
+
*
|
|
4
|
+
* `FormLayout`, `FormSection`, `FieldGrid`, `FieldCell`, `FormContextCard` and
|
|
5
|
+
* `SaveBar` draw a form; this file decides the parts that have to agree across
|
|
6
|
+
* every surface that draws one — how wide a field is when nobody said, when the
|
|
7
|
+
* section navigation appears, which section the navigation marks while the
|
|
8
|
+
* page scrolls, and which grid tracks the layout gets. The Commerce renderer
|
|
9
|
+
* (mapping a manifest), hand-written cockpit forms and the manifest lint all
|
|
10
|
+
* read the same answers from here, so a phone number cannot be half width in
|
|
11
|
+
* one place and full width in another.
|
|
12
|
+
*
|
|
13
|
+
* The class strings live here as whole literals on purpose: Tailwind finds
|
|
14
|
+
* classes by scanning source text, so a class assembled at runtime from parts
|
|
15
|
+
* would never be generated. This directory is under the host's `@source`.
|
|
16
|
+
*/
|
|
17
|
+
/** How much of a two-column field grid a field takes. */
|
|
18
|
+
export type FieldSpan = 'half' | 'full';
|
|
19
|
+
/** The width a field of `fieldType` gets when nothing says otherwise. */
|
|
20
|
+
export declare function defaultSpan(fieldType: string | null | undefined): FieldSpan;
|
|
21
|
+
/** What `resolveSpan` reads — a manifest field fits, so does a hand-written one. */
|
|
22
|
+
export interface SpanSource {
|
|
23
|
+
type?: string | null;
|
|
24
|
+
/** `auto` (or absent) means "the type decides". */
|
|
25
|
+
span?: 'auto' | FieldSpan | string | null;
|
|
26
|
+
}
|
|
27
|
+
/** An explicit `half` / `full` wins; `auto`, absent or unknown falls back to the type. */
|
|
28
|
+
export declare function resolveSpan(field: SpanSource): FieldSpan;
|
|
29
|
+
/**
|
|
30
|
+
* The grid-item class for a span inside `FieldGrid`.
|
|
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
|
|
34
|
+
* the same grid is right in a 512 px dialog, beside a section nav, and in a
|
|
35
|
+
* full-width page. A full field spans both tracks once there are two.
|
|
36
|
+
*/
|
|
37
|
+
export declare function spanClass(span: FieldSpan): string;
|
|
38
|
+
/** Below this many sections a navigation is noise — the form fits in a glance. */
|
|
39
|
+
export declare const SECTION_NAV_MIN = 3;
|
|
40
|
+
/**
|
|
41
|
+
* Whether the left section navigation is drawn.
|
|
42
|
+
* `auto` = from {@link SECTION_NAV_MIN} sections on; a boolean forces it.
|
|
43
|
+
*/
|
|
44
|
+
export declare function showSectionNav(mode: boolean | 'auto', sectionCount: number): boolean;
|
|
45
|
+
/** One section as the scroll spy sees it: its top edge, relative to the viewport of the scroller. */
|
|
46
|
+
export interface SectionOffset {
|
|
47
|
+
id: string;
|
|
48
|
+
top: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Which section the navigation marks.
|
|
52
|
+
*
|
|
53
|
+
* The last section whose top edge has scrolled up to `threshold` (the line just
|
|
54
|
+
* under the sticky header) is the one being read. Two edges need a rule of
|
|
55
|
+
* their own: before anything has scrolled the first section is current even if
|
|
56
|
+
* it starts lower than the line, and once the scroller cannot move further the
|
|
57
|
+
* LAST section is current — a short final section can never reach the line,
|
|
58
|
+
* and a navigation that can never mark it reads as broken.
|
|
59
|
+
*
|
|
60
|
+
* `sections` must be in document order. Returns `null` for no sections.
|
|
61
|
+
*/
|
|
62
|
+
export declare function activeSectionId(sections: readonly SectionOffset[], options: {
|
|
63
|
+
threshold: number;
|
|
64
|
+
atEnd?: boolean;
|
|
65
|
+
}): string | null;
|
|
66
|
+
/** A scroller has reached its end, within a pixel or two of subpixel rounding. */
|
|
67
|
+
export declare function isScrolledToEnd(scrollTop: number, clientHeight: number, scrollHeight: number): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* The tracks of the layout grid, by which side columns are present.
|
|
70
|
+
*
|
|
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
|
|
73
|
+
* only exists from the container width it fits at — the nav from `@3xl`
|
|
74
|
+
* (48rem), the context column from `@5xl` (64rem) — and below that the layout
|
|
75
|
+
* is the bare form column. Container queries again, because the space a form
|
|
76
|
+
* gets is the content pane beside a 256 px studio sidebar, not the window.
|
|
77
|
+
*/
|
|
78
|
+
export declare function layoutGridClass(nav: boolean, context: boolean): string;
|
|
79
|
+
/** Visibility of the side columns, matching the breakpoints in {@link layoutGridClass}. */
|
|
80
|
+
export declare const NAV_COLUMN_CLASS = "hidden @3xl:block";
|
|
81
|
+
export declare const CONTEXT_COLUMN_CLASS = "hidden @5xl:block";
|
|
82
|
+
/** One fact in a `FormContextCard`. Empty values render as an em dash. */
|
|
83
|
+
export interface FormContextRow {
|
|
84
|
+
/** Stable key; the `value` slot receives the whole row. */
|
|
85
|
+
key: string;
|
|
86
|
+
label: string;
|
|
87
|
+
value?: string | number | null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Up to two initials for an avatar: first letters of the first and last word (split on spaces, `-`, `_`),
|
|
91
|
+
* or the first two letters of a single word. Letters only — `"ACME GmbH (Test)"`
|
|
92
|
+
* reads as `AT`, not `A(`. Empty for nothing usable.
|
|
93
|
+
*/
|
|
94
|
+
export declare function initialsOf(name: string | null | undefined): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const FULL_WIDTH_TYPES = /* @__PURE__ */ new Set([
|
|
2
|
+
"textarea",
|
|
3
|
+
"json",
|
|
4
|
+
"key-value",
|
|
5
|
+
"multi-select",
|
|
6
|
+
"market-assignment",
|
|
7
|
+
"repeater",
|
|
8
|
+
"record-list",
|
|
9
|
+
"string-list",
|
|
10
|
+
"fieldset",
|
|
11
|
+
"rich-text",
|
|
12
|
+
"richtext",
|
|
13
|
+
"markdown",
|
|
14
|
+
"media",
|
|
15
|
+
"media-collection",
|
|
16
|
+
"image",
|
|
17
|
+
"localized-text",
|
|
18
|
+
"rules",
|
|
19
|
+
"record-view",
|
|
20
|
+
"address",
|
|
21
|
+
"tree-assign",
|
|
22
|
+
"line-selection"
|
|
23
|
+
]);
|
|
24
|
+
export function defaultSpan(fieldType) {
|
|
25
|
+
if (!fieldType) return "half";
|
|
26
|
+
return FULL_WIDTH_TYPES.has(fieldType.trim().toLowerCase()) ? "full" : "half";
|
|
27
|
+
}
|
|
28
|
+
export function resolveSpan(field) {
|
|
29
|
+
if (field.span === "half" || field.span === "full") return field.span;
|
|
30
|
+
return defaultSpan(field.type);
|
|
31
|
+
}
|
|
32
|
+
export function spanClass(span) {
|
|
33
|
+
return span === "full" ? "col-span-1 @md:col-span-2" : "col-span-1";
|
|
34
|
+
}
|
|
35
|
+
export const SECTION_NAV_MIN = 3;
|
|
36
|
+
export function showSectionNav(mode, sectionCount) {
|
|
37
|
+
if (mode === "auto") return sectionCount >= SECTION_NAV_MIN;
|
|
38
|
+
return mode && sectionCount > 0;
|
|
39
|
+
}
|
|
40
|
+
export function activeSectionId(sections, options) {
|
|
41
|
+
if (!sections.length) return null;
|
|
42
|
+
if (options.atEnd) return sections[sections.length - 1].id;
|
|
43
|
+
let active = sections[0].id;
|
|
44
|
+
for (const section of sections) {
|
|
45
|
+
if (section.top <= options.threshold) active = section.id;
|
|
46
|
+
else break;
|
|
47
|
+
}
|
|
48
|
+
return active;
|
|
49
|
+
}
|
|
50
|
+
export function isScrolledToEnd(scrollTop, clientHeight, scrollHeight) {
|
|
51
|
+
if (scrollHeight <= clientHeight + 2) return false;
|
|
52
|
+
return scrollTop + clientHeight >= scrollHeight - 2;
|
|
53
|
+
}
|
|
54
|
+
export function layoutGridClass(nav, context) {
|
|
55
|
+
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]";
|
|
57
|
+
if (nav)
|
|
58
|
+
return "grid grid-cols-[minmax(0,48rem)] justify-center gap-6 @3xl:grid-cols-[10rem_minmax(0,48rem)]";
|
|
59
|
+
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";
|
|
62
|
+
}
|
|
63
|
+
export const NAV_COLUMN_CLASS = "hidden @3xl:block";
|
|
64
|
+
export const CONTEXT_COLUMN_CLASS = "hidden @5xl:block";
|
|
65
|
+
export function initialsOf(name) {
|
|
66
|
+
const words = (name ?? "").split(/[\s_-]+/).map((w) => w.replace(/[^\p{L}\p{N}]/gu, "")).filter(Boolean);
|
|
67
|
+
if (!words.length) return "";
|
|
68
|
+
if (words.length === 1) return words[0].slice(0, 2).toUpperCase();
|
|
69
|
+
return (words[0][0] + words[words.length - 1][0]).toUpperCase();
|
|
70
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `FormLayout` shares with the parts inside it (PE-976).
|
|
3
|
+
*
|
|
4
|
+
* `FormSection` registers itself so the navigation is built from the sections
|
|
5
|
+
* actually on the page — their ids and titles, in document order — instead of a
|
|
6
|
+
* second list the page has to keep in step. `SaveBar` reads the grid tracks so
|
|
7
|
+
* its buttons line up under the form column rather than under the nav.
|
|
8
|
+
*
|
|
9
|
+
* Kept out of `components/` so the module's component scan never mistakes it
|
|
10
|
+
* for a component.
|
|
11
|
+
*/
|
|
12
|
+
import type { ComputedRef, InjectionKey } from 'vue';
|
|
13
|
+
export interface FormSectionEntry {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
el: () => HTMLElement | null;
|
|
17
|
+
}
|
|
18
|
+
export interface FormLayoutContext {
|
|
19
|
+
register: (entry: FormSectionEntry) => void;
|
|
20
|
+
unregister: (id: string) => void;
|
|
21
|
+
/** Grid classes of the layout row, reused by the save bar. */
|
|
22
|
+
gridClass: ComputedRef<string>;
|
|
23
|
+
navVisible: ComputedRef<boolean>;
|
|
24
|
+
contextVisible: ComputedRef<boolean>;
|
|
25
|
+
}
|
|
26
|
+
export declare const FORM_LAYOUT_KEY: InjectionKey<FormLayoutContext>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const FORM_LAYOUT_KEY = Symbol("studio-shared.form-layout");
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grid track sizing for the data table's columns.
|
|
3
|
+
*
|
|
4
|
+
* A column declares either a fixed size it wants (`'120px'`, `'max-content'`)
|
|
5
|
+
* or nothing at all, meaning "give it what is left". CSS Grid expresses the
|
|
6
|
+
* second as an `fr` track, and an `fr` track's automatic minimum is the one
|
|
7
|
+
* thing that makes it dangerous: `minmax(0,1fr)` may be given ZERO.
|
|
8
|
+
*
|
|
9
|
+
* That is not hypothetical. On `/commerce/contacts` thirteen columns declare
|
|
10
|
+
* 1340px of fixed track, the gaps add 96px, and the container is 1438px — so
|
|
11
|
+
* the only flexible column, the one holding the record's NAME, was sized 0px
|
|
12
|
+
* and the last column was clipped by the panel's `overflow: hidden`. The
|
|
13
|
+
* anchor of every row, invisible, on the surface that needs it most.
|
|
14
|
+
*
|
|
15
|
+
* `datatable-ux` already names the rule: a column whose resting state must
|
|
16
|
+
* stay legible gets a FLOOR, not a share. This applies it to the tracks the
|
|
17
|
+
* component generates, so a manifest gets it without having to ask — and the
|
|
18
|
+
* table overflows its box, where a reader can scroll to what was cut off,
|
|
19
|
+
* rather than silently dropping a column to nothing.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The narrowest a flexible column may become.
|
|
23
|
+
*
|
|
24
|
+
* Roughly the record-name cell at its minimum: the 28px icon tile, its gap,
|
|
25
|
+
* and enough left for a truncated word plus the ellipsis that says there is
|
|
26
|
+
* more. Below this a column stops being a column and becomes a smear.
|
|
27
|
+
*/
|
|
28
|
+
export declare const FLEX_TRACK_FLOOR = "120px";
|
|
29
|
+
/**
|
|
30
|
+
* One column's declared width, resolved to a track with a floor under it.
|
|
31
|
+
*
|
|
32
|
+
* Fixed sizes are left exactly as declared — they are a statement about the
|
|
33
|
+
* content, and shrinking them is the manifest author's call, not this
|
|
34
|
+
* function's. Only the flexible ones are touched, and only their minimum.
|
|
35
|
+
*/
|
|
36
|
+
export declare function trackFor(width: string | undefined | null, floor?: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* The `grid-template-columns` value for a row of columns.
|
|
39
|
+
*
|
|
40
|
+
* A row spans `1 / -1` — the first track line to the last — so the tracks are
|
|
41
|
+
* also how WIDE a row is. Where every column declares a fixed size, they sum to
|
|
42
|
+
* less than the panel and nothing takes the remainder: on
|
|
43
|
+
* `/commerce/procurement/purchase-requests` the tracks and their gaps came to
|
|
44
|
+
* 1486px inside a 1698px box, and the header band and every row background
|
|
45
|
+
* stopped there while the toolbar and the footer went the whole way. A table
|
|
46
|
+
* torn off two centimetres short of its own panel.
|
|
47
|
+
*
|
|
48
|
+
* So when no column asked for a share, the LAST one absorbs what is left, and
|
|
49
|
+
* it keeps its declared size as the minimum it may never go below. On these
|
|
50
|
+
* lists the last column is the row actions — content right-aligned, so the
|
|
51
|
+
* menu button stays pinned to the right edge and every other column is exactly
|
|
52
|
+
* where the manifest put it. On a list without one it is the trailing data
|
|
53
|
+
* column that grows, which is where the eye is least likely to miss the space.
|
|
54
|
+
*/
|
|
55
|
+
export declare function gridTemplateColumns(widths: Array<string | undefined | null>, floor?: string): string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const FLEX_TRACK_FLOOR = "120px";
|
|
2
|
+
const MINMAX_FR = /^minmax\(\s*[^,]+,\s*([\d.]+fr)\s*\)$/i;
|
|
3
|
+
const BARE_FR = /^([\d.]+)fr$/i;
|
|
4
|
+
export function trackFor(width, floor = FLEX_TRACK_FLOOR) {
|
|
5
|
+
if (!width) return `minmax(${floor},1fr)`;
|
|
6
|
+
const declared = width.trim();
|
|
7
|
+
const weighted = MINMAX_FR.exec(declared);
|
|
8
|
+
if (weighted) return `minmax(${floor},${weighted[1].toLowerCase()})`;
|
|
9
|
+
const bare = BARE_FR.exec(declared);
|
|
10
|
+
if (bare) return `minmax(${floor},${bare[1]}fr)`;
|
|
11
|
+
return declared;
|
|
12
|
+
}
|
|
13
|
+
function isFlexible(track) {
|
|
14
|
+
return /\dfr\s*\)?$/i.test(track);
|
|
15
|
+
}
|
|
16
|
+
const MINMAX = /^minmax\(\s*([^,]+?)\s*,\s*(.+?)\s*\)$/i;
|
|
17
|
+
function absorbing(track) {
|
|
18
|
+
const parts = MINMAX.exec(track);
|
|
19
|
+
return `minmax(${parts ? parts[2] : track},1fr)`;
|
|
20
|
+
}
|
|
21
|
+
export function gridTemplateColumns(widths, floor) {
|
|
22
|
+
const tracks = widths.map((w) => trackFor(w, floor));
|
|
23
|
+
if (tracks.length === 0) return "";
|
|
24
|
+
if (!tracks.some(isFlexible)) {
|
|
25
|
+
const last = tracks.length - 1;
|
|
26
|
+
tracks[last] = absorbing(tracks[last]);
|
|
27
|
+
}
|
|
28
|
+
return tracks.join(" ");
|
|
29
|
+
}
|