@omnitend/dashboard-for-laravel 0.45.0 → 0.47.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/assets/IBMPlexSans-Bold-Latin1-914f1400.woff2 +0 -0
- package/dist/assets/IBMPlexSans-Medium-Latin1-b5610af0.woff2 +0 -0
- package/dist/assets/IBMPlexSans-Regular-Latin1-b5ad7bd3.woff2 +0 -0
- package/dist/assets/IBMPlexSans-SemiBold-Latin1-fff0ab3a.woff2 +0 -0
- package/dist/components/base/DFormFile.vue.d.ts +17 -0
- package/dist/components/base/DFormValidFeedback.vue.d.ts +12 -0
- package/dist/components/extended/DXDashboard.vue.d.ts +95 -111
- package/dist/components/extended/DXDashboardNavbar.vue.d.ts +2 -36
- package/dist/components/extended/DXDashboardSidebar.vue.d.ts +60 -79
- package/dist/components/extended/DXField.vue.d.ts +16 -0
- package/dist/components/extended/DXTable.vue.d.ts +10 -1
- package/dist/components/extended/DXTableEditorModal.vue.d.ts +13 -3
- package/dist/dashboard-for-laravel.js +11767 -11479
- package/dist/dashboard-for-laravel.js.map +1 -1
- package/dist/dashboard-for-laravel.umd.cjs +7 -7
- package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/style.css +2 -2
- package/dist/types/index.d.ts +60 -0
- package/dist/types/navigation.d.ts +153 -3
- package/dist/utils/formErrorTargets.d.ts +6 -2
- package/dist/utils/formSchema.d.ts +16 -0
- package/dist/utils/statCardFit.d.ts +19 -0
- package/docs/public/api-reference.json +171 -9
- package/docs/public/docs-map.md +4 -2
- package/docs/public/llms.txt +4 -2
- package/package.json +3 -2
- package/resources/css/theme.scss +339 -4
- package/resources/fonts/ibm-plex-sans/IBMPlexSans-Bold-Latin1.woff2 +0 -0
- package/resources/fonts/ibm-plex-sans/IBMPlexSans-Medium-Latin1.woff2 +0 -0
- package/resources/fonts/ibm-plex-sans/IBMPlexSans-Regular-Latin1.woff2 +0 -0
- package/resources/fonts/ibm-plex-sans/IBMPlexSans-SemiBold-Latin1.woff2 +0 -0
- package/resources/fonts/ibm-plex-sans/OFL.txt +93 -0
- package/resources/fonts/ibm-plex-sans/SOURCE.txt +18 -0
- package/resources/js/components/base/DFormFile.vue +103 -0
- package/resources/js/components/base/DFormValidFeedback.vue +16 -0
- package/resources/js/components/extended/DXDashboard.vue +41 -18
- package/resources/js/components/extended/DXDashboardNavbar.vue +9 -1
- package/resources/js/components/extended/DXDashboardSidebar.vue +25 -3
- package/resources/js/components/extended/DXField.vue +86 -23
- package/resources/js/components/extended/DXFormField.vue +3 -1
- package/resources/js/components/extended/DXStatCard.vue +154 -11
- package/resources/js/components/extended/DXTable.vue +62 -6
- package/resources/js/components/extended/DXTableEditorModal.vue +35 -2
- package/resources/js/composables/useResourceEditor.ts +64 -26
- package/resources/js/index.ts +11 -0
- package/resources/js/types/index.ts +64 -0
- package/resources/js/types/navigation.ts +175 -3
- package/resources/js/utils/formErrorTargets.ts +28 -2
- package/resources/js/utils/formSchema.ts +27 -0
- package/resources/js/utils/statCardFit.ts +210 -0
|
@@ -31,7 +31,10 @@ export interface ErrorTarget {
|
|
|
31
31
|
fieldKey: string | null;
|
|
32
32
|
/** The tab holding the owning field, or `null` (flat form, or unowned). */
|
|
33
33
|
tabKey: string | null;
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* A readable name for the key: "Price (line 1)", "Delivery date", or a
|
|
36
|
+
* row's own name from the field's `rowLabel`.
|
|
37
|
+
*/
|
|
35
38
|
label: string;
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -137,6 +140,26 @@ function defaultLabel(
|
|
|
137
140
|
return typeof field.label === "function" ? field.label(model) : field.label;
|
|
138
141
|
}
|
|
139
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The field's `rowLabel` for one row, or `null` when it has none, names
|
|
145
|
+
* nothing (empty or not a string) or throws: the caller then falls back to
|
|
146
|
+
* "<label> (line N)", so a broken row name can never hide an error.
|
|
147
|
+
*/
|
|
148
|
+
function rowLabelFor(
|
|
149
|
+
field: FieldDefinition,
|
|
150
|
+
row: Record<string, any>,
|
|
151
|
+
index: number,
|
|
152
|
+
): string | null {
|
|
153
|
+
if (typeof field.rowLabel !== "function") return null;
|
|
154
|
+
let name: unknown;
|
|
155
|
+
try {
|
|
156
|
+
name = field.rowLabel(row, index);
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return typeof name === "string" && name.trim() !== "" ? name : null;
|
|
161
|
+
}
|
|
162
|
+
|
|
140
163
|
/**
|
|
141
164
|
* Resolve every key in `errors` to its owning field, tab and label, with the
|
|
142
165
|
* display line for each message. Rows come in tab order, then field order
|
|
@@ -151,7 +174,8 @@ function defaultLabel(
|
|
|
151
174
|
*
|
|
152
175
|
* Labels: the owning field's label; for `field.N.sub` the sub-field's label
|
|
153
176
|
* (when the field defines `fields` and one has that key) or the field's own,
|
|
154
|
-
* plus "(line N+1)"
|
|
177
|
+
* plus "(line N+1)" — unless the field's `rowLabel` names that row, when
|
|
178
|
+
* the row's name is the whole label; for any other nested key (a media map's
|
|
155
179
|
* `image_media.<uuid>`) the field's label. An unowned key is humanised.
|
|
156
180
|
*
|
|
157
181
|
* Display lines: "Label: message", except that the label is left off when
|
|
@@ -254,6 +278,8 @@ export function resolveErrorTargets(
|
|
|
254
278
|
// label resolves against the row here too.
|
|
255
279
|
const row = getByPath(model, `${field.key}.${first}`);
|
|
256
280
|
const rowModel = row !== null && typeof row === "object" ? row : {};
|
|
281
|
+
const rowName = rowLabelFor(field, rowModel, Number(first));
|
|
282
|
+
if (rowName !== null) return rowName;
|
|
257
283
|
const base = subField ? labelFor(subField, rowModel) : labelFor(field);
|
|
258
284
|
return `${base} (line ${Number(first) + 1})`;
|
|
259
285
|
};
|
|
@@ -124,3 +124,30 @@ export function isFieldVisible(
|
|
|
124
124
|
export function isSubmittableField(field: { submit?: boolean }): boolean {
|
|
125
125
|
return field.submit !== false;
|
|
126
126
|
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The payload keys to drop on submit: every field key for which NO field with
|
|
130
|
+
* that key is both submittable (`submit !== false`) and visible (#228).
|
|
131
|
+
*
|
|
132
|
+
* Decided per KEY, across all the fields that share it, never per field. Two
|
|
133
|
+
* fields may share a key: an editable select plus a read-only display twin
|
|
134
|
+
* (`submit: false`) that shows instead for system-created rows. Stripping a
|
|
135
|
+
* key because ONE of its fields is presentational or hidden removed it from
|
|
136
|
+
* every save, including while the select was the visible field in use. A key
|
|
137
|
+
* whose fields are all presentational (#110) or all hidden (#117) is still
|
|
138
|
+
* dropped.
|
|
139
|
+
*/
|
|
140
|
+
export function nonSubmittedFieldKeys<
|
|
141
|
+
TField extends { key: string; submit?: boolean },
|
|
142
|
+
>(fields: readonly TField[], isVisible: (field: TField) => boolean): Set<string> {
|
|
143
|
+
const submittedKeys = new Set(
|
|
144
|
+
fields
|
|
145
|
+
.filter((field) => isSubmittableField(field) && isVisible(field))
|
|
146
|
+
.map((field) => field.key),
|
|
147
|
+
);
|
|
148
|
+
return new Set(
|
|
149
|
+
fields
|
|
150
|
+
.map((field) => field.key)
|
|
151
|
+
.filter((key) => !submittedKeys.has(key)),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* DXStatCard's value fit (internal, not exported from the package).
|
|
3
|
+
*
|
|
4
|
+
* Fit to content (#213): a numeric value is 1.75rem unless it would overflow
|
|
5
|
+
* its box, and then the largest size (to 0.25px) at which it fits, down to
|
|
6
|
+
* the 1.125rem floor.
|
|
7
|
+
*
|
|
8
|
+
* Measure and check: every candidate size is applied and the value's real
|
|
9
|
+
* rendered width compared with the box. Nothing assumes the width is
|
|
10
|
+
* proportional to the font size, because it is not: Linux Chromium rounds
|
|
11
|
+
* glyph advances per size, so the width rises in steps of several pixels,
|
|
12
|
+
* and slot content can have fixed-size parts (px padding, a fixed-width
|
|
13
|
+
* marker). The search tries 1.75rem, then the proportional estimate, then
|
|
14
|
+
* halves the remaining interval, always keeping the largest size that fitted
|
|
15
|
+
* and the smallest that did not, so the result fits by construction.
|
|
16
|
+
*
|
|
17
|
+
* Every card due a fit joins one queue, flushed in rounds: write a candidate
|
|
18
|
+
* size on every unfinished card, then read them all (one forced layout per
|
|
19
|
+
* round, not one per card). A short value is done after round one; a long
|
|
20
|
+
* one takes at most MAX_ROUNDS.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** One card's fit: its elements, and the box width it was last fitted to. */
|
|
24
|
+
export interface FitTarget {
|
|
25
|
+
box: HTMLElement;
|
|
26
|
+
value: HTMLElement;
|
|
27
|
+
/** Only numeric values are fitted; text wraps instead. */
|
|
28
|
+
isNumeric: () => boolean;
|
|
29
|
+
/** -1 when the next resize entry must refit whatever its width. */
|
|
30
|
+
fittedBoxWidth: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const FIT_SIZE_PROPERTY = "--dx-stat-card-fit-size";
|
|
34
|
+
const MAX_SIZE_REM = 1.75;
|
|
35
|
+
const FLOOR_SIZE_REM = 1.125;
|
|
36
|
+
/** The fit's precision: sizes are tried on a 0.25px grid. */
|
|
37
|
+
const SIZE_STEP_PX = 0.25;
|
|
38
|
+
/** Layout rounding (1/64px) never counts as overflow. */
|
|
39
|
+
const FIT_EPSILON_PX = 0.02;
|
|
40
|
+
/** 1.75rem, the estimate, then halving 10px down to 0.25px (6 halvings). */
|
|
41
|
+
const MAX_ROUNDS = 8;
|
|
42
|
+
|
|
43
|
+
interface Search {
|
|
44
|
+
target: FitTarget;
|
|
45
|
+
boxWidth: number;
|
|
46
|
+
/** Largest size known to fit (or the floor, which is used regardless). */
|
|
47
|
+
fitting: number;
|
|
48
|
+
/** Smallest size known to overflow. */
|
|
49
|
+
overflowing: number;
|
|
50
|
+
/** The size written this round. */
|
|
51
|
+
candidate: number;
|
|
52
|
+
/** The size the value had before this fit, kept if it cannot be measured. */
|
|
53
|
+
previousSize: string;
|
|
54
|
+
measurable: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const queuedTargets = new Set<FitTarget>();
|
|
58
|
+
const registeredTargets = new Set<FitTarget>();
|
|
59
|
+
let microtaskQueued = false;
|
|
60
|
+
let frameQueued = false;
|
|
61
|
+
|
|
62
|
+
const snapDown = (size: number): number => Math.floor(size / SIZE_STEP_PX) * SIZE_STEP_PX;
|
|
63
|
+
|
|
64
|
+
/** The next size to try, strictly between the bounds, or null when settled. */
|
|
65
|
+
function nextCandidate(search: Search, estimate: number | null): number | null {
|
|
66
|
+
const low = search.fitting + SIZE_STEP_PX;
|
|
67
|
+
const high = search.overflowing - SIZE_STEP_PX;
|
|
68
|
+
if (low > high + 0.001) return null;
|
|
69
|
+
const ideal = estimate ?? (search.fitting + search.overflowing) / 2;
|
|
70
|
+
return Math.min(high, Math.max(low, snapDown(ideal)));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function flushFits(): void {
|
|
74
|
+
microtaskQueued = false;
|
|
75
|
+
frameQueued = false;
|
|
76
|
+
const targets: FitTarget[] = [];
|
|
77
|
+
for (const target of queuedTargets) {
|
|
78
|
+
if (target.value.isConnected) {
|
|
79
|
+
targets.push(target);
|
|
80
|
+
} else {
|
|
81
|
+
// Detached (a deactivated <KeepAlive>): it comes back with a box of
|
|
82
|
+
// the width it left with, which a resize entry would skip, so it
|
|
83
|
+
// forgets that width and refits on return.
|
|
84
|
+
target.fittedBoxWidth = -1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
queuedTargets.clear();
|
|
88
|
+
if (targets.length === 0) return;
|
|
89
|
+
|
|
90
|
+
const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
91
|
+
const maxSize = MAX_SIZE_REM * rootFontSize;
|
|
92
|
+
const floorSize = FLOOR_SIZE_REM * rootFontSize;
|
|
93
|
+
|
|
94
|
+
// Round one: every numeric value at 1.75rem, laid out at its natural
|
|
95
|
+
// width (the box is inline-size contained, so this never changes the
|
|
96
|
+
// box's own width).
|
|
97
|
+
let searches: Search[] = [];
|
|
98
|
+
for (const target of targets) {
|
|
99
|
+
if (!target.isNumeric()) {
|
|
100
|
+
target.value.style.removeProperty(FIT_SIZE_PROPERTY);
|
|
101
|
+
target.fittedBoxWidth = -1;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const previousSize = target.value.style.getPropertyValue(FIT_SIZE_PROPERTY);
|
|
105
|
+
target.value.style.width = "max-content";
|
|
106
|
+
target.value.style.setProperty(FIT_SIZE_PROPERTY, `${maxSize}px`);
|
|
107
|
+
searches.push({
|
|
108
|
+
target,
|
|
109
|
+
boxWidth: 0,
|
|
110
|
+
fitting: floorSize,
|
|
111
|
+
overflowing: maxSize,
|
|
112
|
+
candidate: maxSize,
|
|
113
|
+
previousSize,
|
|
114
|
+
measurable: true,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const allSearches = searches;
|
|
118
|
+
|
|
119
|
+
for (let round = 0; round < MAX_ROUNDS && searches.length > 0; round += 1) {
|
|
120
|
+
// Read every card (one layout; the boxes too in round one) ...
|
|
121
|
+
const widths = searches.map((search) => {
|
|
122
|
+
if (round === 0) search.boxWidth = search.target.box.getBoundingClientRect().width;
|
|
123
|
+
return search.target.value.getBoundingClientRect().width;
|
|
124
|
+
});
|
|
125
|
+
const unfinished: Search[] = [];
|
|
126
|
+
searches.forEach((search, index) => {
|
|
127
|
+
const naturalWidth = widths[index]!;
|
|
128
|
+
if (round === 0) {
|
|
129
|
+
// In layout px, as the ResizeObserver reports it: `boxWidth` is in
|
|
130
|
+
// transformed px, which differ under a `transform: scale()` ancestor
|
|
131
|
+
// and made every fit's own height change read as a new width. The
|
|
132
|
+
// box has no padding or border, so its computed width is its content
|
|
133
|
+
// width.
|
|
134
|
+
search.target.fittedBoxWidth = parseFloat(getComputedStyle(search.target.box).width);
|
|
135
|
+
// Hidden (display: none) or empty: nothing to fit until it shows.
|
|
136
|
+
if (search.boxWidth <= 0 || naturalWidth <= 0) {
|
|
137
|
+
search.measurable = false;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (naturalWidth <= search.boxWidth + FIT_EPSILON_PX) search.fitting = search.candidate;
|
|
142
|
+
else search.overflowing = search.candidate;
|
|
143
|
+
if (search.fitting >= maxSize) return;
|
|
144
|
+
// ... then write the next candidate on each one still searching.
|
|
145
|
+
// The proportional estimate first: exact where the width scales
|
|
146
|
+
// with the size (macOS), a good first split where it does not.
|
|
147
|
+
const estimate = round === 0 ? (maxSize * search.boxWidth) / naturalWidth : null;
|
|
148
|
+
const candidate = nextCandidate(search, estimate);
|
|
149
|
+
if (candidate === null) return;
|
|
150
|
+
search.candidate = candidate;
|
|
151
|
+
search.target.value.style.setProperty(FIT_SIZE_PROPERTY, `${candidate}px`);
|
|
152
|
+
unfinished.push(search);
|
|
153
|
+
});
|
|
154
|
+
searches = unfinished;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const search of allSearches) {
|
|
158
|
+
const { value } = search.target;
|
|
159
|
+
const size = search.measurable ? `${search.fitting}px` : search.previousSize;
|
|
160
|
+
if (size) value.style.setProperty(FIT_SIZE_PROPERTY, size);
|
|
161
|
+
else value.style.removeProperty(FIT_SIZE_PROPERTY);
|
|
162
|
+
value.style.removeProperty("width");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Queue a fit. "microtask" runs before the next paint, for mounts, content
|
|
168
|
+
* changes, reactivation and font loads. "frame" is for ResizeObserver
|
|
169
|
+
* callbacks: a fit inside the observer's delivery (microtasks run there
|
|
170
|
+
* too) changes the box's HEIGHT, which the browser reports as a
|
|
171
|
+
* "ResizeObserver loop" error, so a resize refits in the next frame.
|
|
172
|
+
*/
|
|
173
|
+
export function queueFit(target: FitTarget, timing: "microtask" | "frame"): void {
|
|
174
|
+
queuedTargets.add(target);
|
|
175
|
+
if (timing === "microtask") {
|
|
176
|
+
if (microtaskQueued) return;
|
|
177
|
+
microtaskQueued = true;
|
|
178
|
+
queueMicrotask(() => {
|
|
179
|
+
if (microtaskQueued) flushFits();
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (frameQueued || microtaskQueued) return;
|
|
184
|
+
frameQueued = true;
|
|
185
|
+
requestAnimationFrame(() => {
|
|
186
|
+
if (frameQueued) flushFits();
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Web fonts (the figures face, the body face) swap in after first paint and
|
|
191
|
+
// change the value's width without changing the box's, so each finished
|
|
192
|
+
// batch of font loads refits every mounted card. One listener serves all
|
|
193
|
+
// cards; it is attached at the first registration, so a load already under
|
|
194
|
+
// way then still ends in a `loadingdone` it hears.
|
|
195
|
+
function refitAll(): void {
|
|
196
|
+
for (const target of registeredTargets) queueFit(target, "microtask");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function registerTarget(target: FitTarget): void {
|
|
200
|
+
registeredTargets.add(target);
|
|
201
|
+
if (registeredTargets.size > 1 || typeof document === "undefined" || !document.fonts) return;
|
|
202
|
+
document.fonts.addEventListener("loadingdone", refitAll);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function unregisterTarget(target: FitTarget): void {
|
|
206
|
+
registeredTargets.delete(target);
|
|
207
|
+
queuedTargets.delete(target);
|
|
208
|
+
if (registeredTargets.size > 0 || typeof document === "undefined" || !document.fonts) return;
|
|
209
|
+
document.fonts.removeEventListener("loadingdone", refitAll);
|
|
210
|
+
}
|