@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.
Files changed (51) hide show
  1. package/dist/assets/IBMPlexSans-Bold-Latin1-914f1400.woff2 +0 -0
  2. package/dist/assets/IBMPlexSans-Medium-Latin1-b5610af0.woff2 +0 -0
  3. package/dist/assets/IBMPlexSans-Regular-Latin1-b5ad7bd3.woff2 +0 -0
  4. package/dist/assets/IBMPlexSans-SemiBold-Latin1-fff0ab3a.woff2 +0 -0
  5. package/dist/components/base/DFormFile.vue.d.ts +17 -0
  6. package/dist/components/base/DFormValidFeedback.vue.d.ts +12 -0
  7. package/dist/components/extended/DXDashboard.vue.d.ts +95 -111
  8. package/dist/components/extended/DXDashboardNavbar.vue.d.ts +2 -36
  9. package/dist/components/extended/DXDashboardSidebar.vue.d.ts +60 -79
  10. package/dist/components/extended/DXField.vue.d.ts +16 -0
  11. package/dist/components/extended/DXTable.vue.d.ts +10 -1
  12. package/dist/components/extended/DXTableEditorModal.vue.d.ts +13 -3
  13. package/dist/dashboard-for-laravel.js +11767 -11479
  14. package/dist/dashboard-for-laravel.js.map +1 -1
  15. package/dist/dashboard-for-laravel.umd.cjs +7 -7
  16. package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
  17. package/dist/index.d.ts +4 -2
  18. package/dist/style.css +2 -2
  19. package/dist/types/index.d.ts +60 -0
  20. package/dist/types/navigation.d.ts +153 -3
  21. package/dist/utils/formErrorTargets.d.ts +6 -2
  22. package/dist/utils/formSchema.d.ts +16 -0
  23. package/dist/utils/statCardFit.d.ts +19 -0
  24. package/docs/public/api-reference.json +171 -9
  25. package/docs/public/docs-map.md +4 -2
  26. package/docs/public/llms.txt +4 -2
  27. package/package.json +3 -2
  28. package/resources/css/theme.scss +339 -4
  29. package/resources/fonts/ibm-plex-sans/IBMPlexSans-Bold-Latin1.woff2 +0 -0
  30. package/resources/fonts/ibm-plex-sans/IBMPlexSans-Medium-Latin1.woff2 +0 -0
  31. package/resources/fonts/ibm-plex-sans/IBMPlexSans-Regular-Latin1.woff2 +0 -0
  32. package/resources/fonts/ibm-plex-sans/IBMPlexSans-SemiBold-Latin1.woff2 +0 -0
  33. package/resources/fonts/ibm-plex-sans/OFL.txt +93 -0
  34. package/resources/fonts/ibm-plex-sans/SOURCE.txt +18 -0
  35. package/resources/js/components/base/DFormFile.vue +103 -0
  36. package/resources/js/components/base/DFormValidFeedback.vue +16 -0
  37. package/resources/js/components/extended/DXDashboard.vue +41 -18
  38. package/resources/js/components/extended/DXDashboardNavbar.vue +9 -1
  39. package/resources/js/components/extended/DXDashboardSidebar.vue +25 -3
  40. package/resources/js/components/extended/DXField.vue +86 -23
  41. package/resources/js/components/extended/DXFormField.vue +3 -1
  42. package/resources/js/components/extended/DXStatCard.vue +154 -11
  43. package/resources/js/components/extended/DXTable.vue +62 -6
  44. package/resources/js/components/extended/DXTableEditorModal.vue +35 -2
  45. package/resources/js/composables/useResourceEditor.ts +64 -26
  46. package/resources/js/index.ts +11 -0
  47. package/resources/js/types/index.ts +64 -0
  48. package/resources/js/types/navigation.ts +175 -3
  49. package/resources/js/utils/formErrorTargets.ts +28 -2
  50. package/resources/js/utils/formSchema.ts +27 -0
  51. 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
- /** A readable name for the key: "Price (line 1)", "Delivery date". */
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)"; for any other nested key (a media map's
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
+ }