@omnitend/dashboard-for-laravel 0.44.0 → 0.46.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 (42) 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/DXLoading.vue.d.ts +3 -0
  11. package/dist/components/extended/DXTable.vue.d.ts +10 -1
  12. package/dist/dashboard-for-laravel.js +9783 -9564
  13. package/dist/dashboard-for-laravel.js.map +1 -1
  14. package/dist/dashboard-for-laravel.umd.cjs +7 -7
  15. package/dist/dashboard-for-laravel.umd.cjs.map +1 -1
  16. package/dist/index.d.ts +3 -1
  17. package/dist/style.css +2 -2
  18. package/dist/types/navigation.d.ts +153 -3
  19. package/dist/utils/statCardFit.d.ts +19 -0
  20. package/docs/public/api-reference.json +50 -8
  21. package/docs/public/docs-map.md +4 -2
  22. package/docs/public/llms.txt +4 -2
  23. package/package.json +3 -2
  24. package/resources/css/theme.scss +384 -4
  25. package/resources/fonts/ibm-plex-sans/IBMPlexSans-Bold-Latin1.woff2 +0 -0
  26. package/resources/fonts/ibm-plex-sans/IBMPlexSans-Medium-Latin1.woff2 +0 -0
  27. package/resources/fonts/ibm-plex-sans/IBMPlexSans-Regular-Latin1.woff2 +0 -0
  28. package/resources/fonts/ibm-plex-sans/IBMPlexSans-SemiBold-Latin1.woff2 +0 -0
  29. package/resources/fonts/ibm-plex-sans/OFL.txt +93 -0
  30. package/resources/fonts/ibm-plex-sans/SOURCE.txt +18 -0
  31. package/resources/js/components/base/DFormFile.vue +103 -0
  32. package/resources/js/components/base/DFormValidFeedback.vue +16 -0
  33. package/resources/js/components/extended/DXDashboard.vue +41 -18
  34. package/resources/js/components/extended/DXDashboardNavbar.vue +9 -1
  35. package/resources/js/components/extended/DXDashboardSidebar.vue +25 -3
  36. package/resources/js/components/extended/DXField.vue +36 -8
  37. package/resources/js/components/extended/DXLoading.vue +18 -11
  38. package/resources/js/components/extended/DXStatCard.vue +154 -11
  39. package/resources/js/components/extended/DXTable.vue +60 -10
  40. package/resources/js/index.ts +9 -0
  41. package/resources/js/types/navigation.ts +175 -3
  42. package/resources/js/utils/statCardFit.ts +210 -0
@@ -62,9 +62,11 @@ export { default as DDropdownItem } from "./components/base/DDropdownItem.vue";
62
62
  export { default as DForm } from "./components/base/DForm.vue";
63
63
  export { default as DFormCheckbox } from "./components/base/DFormCheckbox.vue";
64
64
  export { default as DFormCheckboxGroup } from "./components/base/DFormCheckboxGroup.vue";
65
+ export { default as DFormFile } from "./components/base/DFormFile.vue";
65
66
  export { default as DFormGroup } from "./components/base/DFormGroup.vue";
66
67
  export { default as DFormInput } from "./components/base/DFormInput.vue";
67
68
  export { default as DFormInvalidFeedback } from "./components/base/DFormInvalidFeedback.vue";
69
+ export { default as DFormValidFeedback } from "./components/base/DFormValidFeedback.vue";
68
70
  export { default as DFormOtp } from "./components/base/DFormOtp.vue";
69
71
  export { default as DFormRadio } from "./components/base/DFormRadio.vue";
70
72
  export { default as DFormRadioGroup } from "./components/base/DFormRadioGroup.vue";
@@ -216,7 +218,14 @@ export type {
216
218
  NavigationItem,
217
219
  NavigationGroup,
218
220
  Navigation,
221
+ NavigationItemOf,
219
222
  NavbarSearchAlign,
220
223
  NavbarActionsOnMobile,
221
224
  SidebarVariant,
225
+ DashboardUser,
226
+ DashboardNavbarSlots,
227
+ DashboardSidebarSlots,
228
+ DashboardSlotExtras,
229
+ DashboardSlots,
230
+ PrefixedDashboardSlots,
222
231
  } from "./types/navigation";
@@ -8,7 +8,13 @@ export interface NavigationItem {
8
8
  visible?: boolean;
9
9
  }
10
10
 
11
- export interface NavigationGroup {
11
+ /**
12
+ * A group of sidebar links. `TItem` is the app's own item type, for menus whose
13
+ * items carry extra keys (a permission, a route name): DXDashboard and
14
+ * DXDashboardSidebar infer it from the `navigation` prop, so the `link` slot's
15
+ * `item` has those keys too. Defaults to `NavigationItem`.
16
+ */
17
+ export interface NavigationGroup<TItem extends NavigationItem = NavigationItem> {
12
18
  label?: string;
13
19
  /**
14
20
  * Stable identifier for the group's collapsible open/closed state. Defaults
@@ -17,7 +23,7 @@ export interface NavigationGroup {
17
23
  * state attached to the right group.
18
24
  */
19
25
  key?: string;
20
- items: NavigationItem[];
26
+ items: TItem[];
21
27
  /**
22
28
  * Per-group override for the sidebar's `collapsibleGroups` behaviour.
23
29
  * Set to `false` to keep a group permanently expanded (no toggle header)
@@ -29,7 +35,26 @@ export interface NavigationGroup {
29
35
  visible?: boolean;
30
36
  }
31
37
 
32
- export type Navigation = NavigationGroup[];
38
+ /** The sidebar's navigation: its groups, in order. See `NavigationGroup` for `TItem`. */
39
+ export type Navigation<TItem extends NavigationItem = NavigationItem> = NavigationGroup<TItem>[];
40
+
41
+ /**
42
+ * The item type of a navigation, as the `link` slot's `item` receives it:
43
+ * `NavigationItemOf<Navigation<AppItem>>` is `AppItem`. An `any` navigation
44
+ * (untyped page props) gives `any`, and an empty `[]` gives `NavigationItem`.
45
+ *
46
+ * DXDashboard and DXDashboardSidebar are generic over the whole navigation
47
+ * (`TNavigation`) and derive the item from it, rather than generic over the
48
+ * item: TypeScript infers `any` for a type parameter that IS the prop's type,
49
+ * but not for one nested inside it (`{ items: TItem[] }[]`), where an `any`
50
+ * navigation would have fallen back to `NavigationItem` and rejected the app's
51
+ * extra keys.
52
+ */
53
+ export type NavigationItemOf<TNavigation extends Navigation> = [
54
+ TNavigation[number]["items"][number],
55
+ ] extends [never]
56
+ ? NavigationItem
57
+ : TNavigation[number]["items"][number];
33
58
 
34
59
  /**
35
60
  * Horizontal alignment of the navbar search slot content within its region:
@@ -54,3 +79,150 @@ export type SidebarVariant = "light" | "dark";
54
79
  * Shared by DXDashboard (which forwards it) and DXDashboardNavbar.
55
80
  */
56
81
  export type NavbarActionsOnMobile = "wrap" | "hide";
82
+
83
+ /**
84
+ * The signed-in user DXDashboardNavbar (and DXDashboard, which forwards it)
85
+ * shows in the avatar dropdown. Extra keys are allowed and reach the
86
+ * user-menu slots.
87
+ */
88
+ export interface DashboardUser {
89
+ name: string;
90
+ email: string;
91
+ [key: string]: any;
92
+ }
93
+
94
+ /** A slot that receives no props. */
95
+ type NoSlotProps = Record<never, never>;
96
+
97
+ /*
98
+ * The slot interfaces below are what `defineSlots` uses and what an editor
99
+ * shows on hover. The API docs (`docs:generate:ai`) still read each slot from
100
+ * the `@slot`/`@binding` comments in the component's template: vue-docgen-api
101
+ * only reads `defineSlots` members whose keys are bare identifiers, and it
102
+ * drops the binding types when it does, so every key here is QUOTED on
103
+ * purpose. Keep the text in step with the template comments.
104
+ */
105
+
106
+ /** Slots of DXDashboardNavbar, with their slot props. All are optional. */
107
+ export interface DashboardNavbarSlots {
108
+ /** Custom hamburger/menu icon. Defaults to a three-line SVG. */
109
+ "menu-icon"?(props: NoSlotProps): any;
110
+ /**
111
+ * Search input or component. Fills the middle of the bar at every width,
112
+ * aligned per `searchAlign`. The region is a size container named
113
+ * `dx-navbar-search`; a `<kbd>` hint inside it is hidden when it is
114
+ * narrower than 16rem.
115
+ */
116
+ "search"?(props: NoSlotProps): any;
117
+ /**
118
+ * Page-level primary actions (e.g. a Create button). Right-aligned next to
119
+ * the user menu from `md` up; below, wraps to its own full-width row or is
120
+ * hidden entirely per `actionsOnMobile`.
121
+ */
122
+ "actions"?(props: {
123
+ /** The current page title, for context. */
124
+ pageTitle: string;
125
+ }): any;
126
+ /** Replaces the entire user menu (the default avatar dropdown). */
127
+ "user-menu"?(props: {
128
+ /** The signed-in user; `null` for a guest. */
129
+ user: DashboardUser | null;
130
+ }): any;
131
+ /**
132
+ * Custom user avatar/icon in the dropdown trigger. To decorate the default
133
+ * avatar (e.g. add a notification dot) rather than replace it, render
134
+ * `DXUserAvatar`. Only rendered for a signed-in user.
135
+ */
136
+ "user-icon"?(props: {
137
+ /** The first letter of the user's name. */
138
+ initial: string;
139
+ /** The signed-in user. */
140
+ user: DashboardUser;
141
+ }): any;
142
+ /** Items for the default user dropdown menu. Only rendered for a signed-in user. */
143
+ "user-menu-items"?(props: {
144
+ /** The signed-in user. */
145
+ user: DashboardUser;
146
+ }): any;
147
+ }
148
+
149
+ /**
150
+ * Slots of DXDashboardSidebar, with their slot props. All are optional.
151
+ * `TItem` is the navigation's item type (see `NavigationGroup`).
152
+ */
153
+ export interface DashboardSidebarSlots<TItem extends NavigationItem = NavigationItem> {
154
+ /**
155
+ * Brand/logo area in the sidebar header. Defaults to the title's initial
156
+ * plus the full title (hidden when collapsed).
157
+ */
158
+ "brand"?(props: {
159
+ /** Whether the sidebar rail is collapsed to icons only. */
160
+ collapsed: boolean;
161
+ /** The sidebar title text. */
162
+ title: string;
163
+ }): any;
164
+ /**
165
+ * Renders a single navigation item link. Defaults to an anchor with
166
+ * optional icon, label, and badge.
167
+ */
168
+ "link"?(props: {
169
+ /**
170
+ * The navigation item (label, url, icon, badge, badgeColor), typed as the
171
+ * app's own item type when its items carry extra keys.
172
+ */
173
+ item: TItem;
174
+ /** Whether this item matches the current route. */
175
+ isActive: boolean;
176
+ /** Whether the sidebar rail is collapsed to icons only. */
177
+ collapsed: boolean;
178
+ /** Whether the item's group is currently expanded. */
179
+ isExpanded: boolean;
180
+ }): any;
181
+ /**
182
+ * Utility/secondary links pinned to the bottom of the sidebar (help,
183
+ * changelog, sign-out, …), below the nav groups.
184
+ */
185
+ "footer"?(props: {
186
+ /** Whether the sidebar rail is collapsed to icons only. */
187
+ collapsed: boolean;
188
+ }): any;
189
+ }
190
+
191
+ /**
192
+ * The two bindings DXDashboard adds to every forwarded `sidebar-*` and
193
+ * `navbar-*` slot. They are bound after the child's own slot props, so they
194
+ * win a name clash.
195
+ */
196
+ export interface DashboardSlotExtras {
197
+ /** Shows/hides the sidebar. */
198
+ toggleSidebar: () => void;
199
+ /** Whether the sidebar is currently hidden. */
200
+ sidebarHidden: boolean;
201
+ }
202
+
203
+ /** The props a (possibly optional) slot function receives. */
204
+ type SlotPropsOf<Slot> = NonNullable<Slot> extends (props: infer Props) => any ? Props : never;
205
+
206
+ /**
207
+ * A child component's slots renamed with `Prefix` and given the dashboard's
208
+ * extra bindings: `{ brand(props: P) }` becomes
209
+ * `{ "sidebar-brand"(props: P & DashboardSlotExtras) }`. Derived from the
210
+ * child's slot types, so a slot added to the child is typed here for free.
211
+ */
212
+ export type PrefixedDashboardSlots<Slots, Prefix extends string> = {
213
+ [Name in keyof Slots as `${Prefix}${Name & string}`]?: (
214
+ props: Omit<SlotPropsOf<Slots[Name]>, keyof DashboardSlotExtras> & DashboardSlotExtras,
215
+ ) => any;
216
+ };
217
+
218
+ /**
219
+ * Slots of DXDashboard: the page content (`default`), every
220
+ * DXDashboardSidebar slot as `sidebar-<name>` and every DXDashboardNavbar
221
+ * slot as `navbar-<name>`. An unprefixed name (`#search`) is not forwarded.
222
+ * `TItem` is the navigation's item type (see `NavigationGroup`).
223
+ */
224
+ export type DashboardSlots<TItem extends NavigationItem = NavigationItem> = {
225
+ /** The main page content. */
226
+ default?(props: NoSlotProps): any;
227
+ } & PrefixedDashboardSlots<DashboardSidebarSlots<TItem>, "sidebar-"> &
228
+ PrefixedDashboardSlots<DashboardNavbarSlots, "navbar-">;
@@ -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
+ }