@nexdom/uimed-vue 2.0.0-beta.30 → 2.0.0-beta.32
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/AGENTS.md +3 -2
- package/dist/components.d.ts +1 -1
- package/dist/composables.d.ts +87 -1
- package/dist/composables.js +112 -2
- package/dist/plugins.d.ts +2 -1
- package/dist/plugins.js +18 -1
- package/dist/styles/fonts/fonts.css +1 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -26,7 +26,7 @@ Peer dependencies (`@mdi/font`, `@nexdom/shared`, `resize-observer-polyfill`, `s
|
|
|
26
26
|
|
|
27
27
|
### Setup
|
|
28
28
|
|
|
29
|
-
Add the Vite plugin, which compiles the components' styles with the library's font and Sass settings
|
|
29
|
+
Add the Vite plugin, which compiles the components' styles with the library's font and Sass settings, and loads the icon font with `font-display`, so it doesn't block rendering while it loads. Without it, components lose the Unimed Slab font:
|
|
30
30
|
|
|
31
31
|
```ts
|
|
32
32
|
// vite.config.ts
|
|
@@ -203,8 +203,9 @@ Use an existing composable (e.g. `use-toast`) as the reference, and deliver in t
|
|
|
203
203
|
```
|
|
204
204
|
|
|
205
205
|
- Component layout: `src/components/<name>/<name>.vue`, `types.ts` for prop/option types, `__tests__/<name>.test.ts` for unit tests. Components that belong to a group live in the group's folder (e.g. the internal `src/components/dialogs/toast.vue`).
|
|
206
|
-
- Composables live in a group folder: `src/composables/<group>/<name>.ts`, with `types.ts` and `__tests__/` in the group folder (e.g. `src/composables/dialogs/use-toast.ts`).
|
|
206
|
+
- Composables live in a group folder: `src/composables/<group>/<name>.ts`, with `types.ts` and `__tests__/` in the group folder (e.g. `src/composables/dialogs/use-toast.ts`). A composable (`use-*.ts`) must depend on `vue` or `vue-router`, directly or through other modules (enforced by dependency-cruiser's `composables-use-vue` rule).
|
|
207
207
|
- Library-wide configuration constants that aren't components or composables live in `src/consts/<name>.ts` (e.g. `src/consts/locale.ts`, the language set by `createUimed()` and `vueTestUtilsPluginUimed()`).
|
|
208
|
+
- Pure helpers that don't use Vue live in `src/utils/<name>.ts`, with tests in `src/utils/__tests__/` (e.g. `src/utils/is-deep-equal.ts`).
|
|
208
209
|
- Public components/composables are re-exported from `src/components/index.ts` and `src/composables/index.ts`. Prop types stay internal (`types.ts` is not re-exported); consumers derive them with `ComponentProps` (see [Usage](#usage)).
|
|
209
210
|
- Every publicly exported component must use the `U` prefix on its export identifier (e.g., `UButton`, `UMain`), while internal file names and component names remain unprefixed.
|
|
210
211
|
- Use [JSDoc](https://jsdoc.app/about-getting-started) on every method/prop/type intended to be part of the public API — it's the primary documentation surface and supports markdown/code examples.
|
package/dist/components.d.ts
CHANGED
|
@@ -433,8 +433,8 @@ declare const __VLS_export$14: import("vue", { with: { "resolution-mode": "impor
|
|
|
433
433
|
"onUpdate:modelValue"?: ((value: string) => any) | undefined;
|
|
434
434
|
}>, {
|
|
435
435
|
variant: TextFieldVariant;
|
|
436
|
-
type: TextFieldType;
|
|
437
436
|
clearable: boolean;
|
|
437
|
+
type: TextFieldType;
|
|
438
438
|
}, {}, {}, {}, string, import("vue", { with: { "resolution-mode": "import" } }).ComponentProvideOptions, false, {}, any>;
|
|
439
439
|
//#endregion
|
|
440
440
|
//#region src/components/inputs/checkbox/types.d.ts
|
package/dist/composables.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { i as FeedbackColorVariant, n as ButtonVariant, r as ColorVariant } from "./types-DNT_odv7.js";
|
|
2
|
+
import { ComputedRef, MaybeRefOrGetter } from "vue";
|
|
2
3
|
//#region src/composables/dialogs/types.d.ts
|
|
3
4
|
interface ToastOptions {
|
|
4
5
|
/**
|
|
@@ -190,4 +191,89 @@ declare function useConfirm(): {
|
|
|
190
191
|
isRunning: import("vue").ComputedRef<boolean>;
|
|
191
192
|
};
|
|
192
193
|
//#endregion
|
|
193
|
-
|
|
194
|
+
//#region src/composables/navigation/types.d.ts
|
|
195
|
+
/**
|
|
196
|
+
* Values compared by `useUnsavedChanges`.
|
|
197
|
+
*/
|
|
198
|
+
interface UnsavedChangesValues<T> {
|
|
199
|
+
/**
|
|
200
|
+
* Original (last saved) value. To mark the form as saved, assign it a copy of the current value.
|
|
201
|
+
*/
|
|
202
|
+
previous: MaybeRefOrGetter<T>;
|
|
203
|
+
/**
|
|
204
|
+
* Current value of the form.
|
|
205
|
+
*/
|
|
206
|
+
current: MaybeRefOrGetter<T>;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Options of `useUnsavedChanges`.
|
|
210
|
+
*/
|
|
211
|
+
interface UnsavedChangesOptions {
|
|
212
|
+
/**
|
|
213
|
+
* Plain text displayed as the confirmation dialog's title. HTML isn't interpreted.
|
|
214
|
+
* @default "Alterações não salvas"
|
|
215
|
+
*/
|
|
216
|
+
title?: string;
|
|
217
|
+
/**
|
|
218
|
+
* Plain text displayed as the confirmation dialog's message. HTML isn't interpreted.
|
|
219
|
+
* @default "Existem alterações que ainda não foram salvas. Deseja sair sem salvar?"
|
|
220
|
+
*/
|
|
221
|
+
message?: string;
|
|
222
|
+
/**
|
|
223
|
+
* Text displayed on the button which leaves the page.
|
|
224
|
+
* @default "Sair sem salvar"
|
|
225
|
+
*/
|
|
226
|
+
confirmText?: string;
|
|
227
|
+
/**
|
|
228
|
+
* Text displayed on the button which cancels the navigation.
|
|
229
|
+
* @default "Continuar editando"
|
|
230
|
+
*/
|
|
231
|
+
cancelText?: string;
|
|
232
|
+
/**
|
|
233
|
+
* Applies a color to the button which leaves the page.
|
|
234
|
+
* One of `primary`, `secondary`, `positive`, `informative`, `caution`, or `danger`.
|
|
235
|
+
* @default "danger"
|
|
236
|
+
*/
|
|
237
|
+
color?: ColorVariant;
|
|
238
|
+
/**
|
|
239
|
+
* Also asks for the browser's native confirmation on reload (F5), tab close or leaving the app
|
|
240
|
+
* through an external link.
|
|
241
|
+
* @default true
|
|
242
|
+
*/
|
|
243
|
+
beforeUnload?: boolean;
|
|
244
|
+
}
|
|
245
|
+
//#endregion
|
|
246
|
+
//#region src/composables/navigation/use-unsaved-changes.d.ts
|
|
247
|
+
/**
|
|
248
|
+
* Composable which compares the original and the current values of a form, and asks the user to
|
|
249
|
+
* confirm before leaving the page while they differ.
|
|
250
|
+
*
|
|
251
|
+
* Must be called in the `setup` of a component. Leaving through vue-router is only guarded in
|
|
252
|
+
* components rendered by a `<RouterView>`, and the confirmation dialog is displayed by `UMain`.
|
|
253
|
+
*
|
|
254
|
+
* @param values - The original (`previous`) and current (`current`) values, as refs, getters or
|
|
255
|
+
* reactive objects. They're compared in depth: key order doesn't matter, a key holding `undefined`
|
|
256
|
+
* equals a missing key, dates are compared by their time and arrays item by item, in order.
|
|
257
|
+
* @param options - Texts and color of the confirmation dialog, and whether to ask for the
|
|
258
|
+
* browser's native confirmation before unloading the page.
|
|
259
|
+
* @returns `hasChanges`, a computed property which is `true` while the values differ
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* ```ts
|
|
263
|
+
* const saved = ref({ name: "Maria" });
|
|
264
|
+
* const form = ref(structuredClone(toRaw(saved.value)));
|
|
265
|
+
*
|
|
266
|
+
* const { hasChanges } = useUnsavedChanges({ previous: saved, current: form });
|
|
267
|
+
*
|
|
268
|
+
* async function save() {
|
|
269
|
+
* await api.save(form.value);
|
|
270
|
+
* // Marks the form as saved
|
|
271
|
+
* saved.value = structuredClone(toRaw(form.value));
|
|
272
|
+
* }
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
declare function useUnsavedChanges<T>(values: UnsavedChangesValues<T>, options?: UnsavedChangesOptions): {
|
|
276
|
+
hasChanges: ComputedRef<boolean>;
|
|
277
|
+
};
|
|
278
|
+
//#endregion
|
|
279
|
+
export { useConfirm, useDialog, useRunOrToast, useToast, useUnsavedChanges };
|
package/dist/composables.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { i as useDialog, o as useToast, t as openDialog } from "./use-dialog-Ck2_ZW7T.js";
|
|
2
|
-
import { computed, ref } from "vue";
|
|
2
|
+
import { computed, onActivated, onDeactivated, onMounted, onUnmounted, ref, toValue, watch } from "vue";
|
|
3
|
+
import { onBeforeRouteLeave } from "vue-router";
|
|
3
4
|
//#region src/composables/dialogs/use-run-or-toast.ts
|
|
4
5
|
const { toast } = useToast();
|
|
5
6
|
/**
|
|
@@ -114,4 +115,113 @@ function useConfirm() {
|
|
|
114
115
|
};
|
|
115
116
|
}
|
|
116
117
|
//#endregion
|
|
117
|
-
|
|
118
|
+
//#region src/utils/is-deep-equal.ts
|
|
119
|
+
/**
|
|
120
|
+
* Compares two values in depth, as a form would: key order doesn't matter, a key holding
|
|
121
|
+
* `undefined` equals a missing key, dates are compared by their time and arrays item by item, in
|
|
122
|
+
* order. Only plain objects are compared by their keys: other objects (e.g. `File`, `Map`, `Set`
|
|
123
|
+
* or class instances) and the remaining values are compared with `Object.is`, so `NaN` equals
|
|
124
|
+
* `NaN`, `0` differs from `-0`, and `null`, `""` and `undefined` differ from each other.
|
|
125
|
+
*/
|
|
126
|
+
function isDeepEqual(a, b) {
|
|
127
|
+
if (Object.is(a, b)) return true;
|
|
128
|
+
if (a instanceof Date && b instanceof Date) return Object.is(a.getTime(), b.getTime());
|
|
129
|
+
if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((item, index) => isDeepEqual(item, b[index]));
|
|
130
|
+
if (!isPlainObject(a) || !isPlainObject(b)) return false;
|
|
131
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])].every((key) => isDeepEqual(a[key], b[key]));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Whether the value is an object literal or an object without prototype. Checks the prototype's
|
|
135
|
+
* prototype instead of comparing it to `Object.prototype`, since objects from other realms (e.g.
|
|
136
|
+
* iframes, or `structuredClone` in some test environments) have their own `Object.prototype`.
|
|
137
|
+
*/
|
|
138
|
+
function isPlainObject(value) {
|
|
139
|
+
if (value == null) return false;
|
|
140
|
+
const prototype = Object.getPrototypeOf(value);
|
|
141
|
+
return prototype === null || Object.getPrototypeOf(prototype) === null;
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/composables/navigation/use-unsaved-changes.ts
|
|
145
|
+
/**
|
|
146
|
+
* Calls whose page is on display with unsaved changes. The `beforeunload` listener is registered
|
|
147
|
+
* while at least one of them exists, so a call that stops guarding doesn't remove the guard of the
|
|
148
|
+
* others.
|
|
149
|
+
*/
|
|
150
|
+
const unloadGuards = /* @__PURE__ */ new Set();
|
|
151
|
+
function preventUnload(event) {
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
}
|
|
154
|
+
function updateUnloadListener() {
|
|
155
|
+
if (unloadGuards.size > 0) globalThis.addEventListener("beforeunload", preventUnload);
|
|
156
|
+
else globalThis.removeEventListener("beforeunload", preventUnload);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Composable which compares the original and the current values of a form, and asks the user to
|
|
160
|
+
* confirm before leaving the page while they differ.
|
|
161
|
+
*
|
|
162
|
+
* Must be called in the `setup` of a component. Leaving through vue-router is only guarded in
|
|
163
|
+
* components rendered by a `<RouterView>`, and the confirmation dialog is displayed by `UMain`.
|
|
164
|
+
*
|
|
165
|
+
* @param values - The original (`previous`) and current (`current`) values, as refs, getters or
|
|
166
|
+
* reactive objects. They're compared in depth: key order doesn't matter, a key holding `undefined`
|
|
167
|
+
* equals a missing key, dates are compared by their time and arrays item by item, in order.
|
|
168
|
+
* @param options - Texts and color of the confirmation dialog, and whether to ask for the
|
|
169
|
+
* browser's native confirmation before unloading the page.
|
|
170
|
+
* @returns `hasChanges`, a computed property which is `true` while the values differ
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```ts
|
|
174
|
+
* const saved = ref({ name: "Maria" });
|
|
175
|
+
* const form = ref(structuredClone(toRaw(saved.value)));
|
|
176
|
+
*
|
|
177
|
+
* const { hasChanges } = useUnsavedChanges({ previous: saved, current: form });
|
|
178
|
+
*
|
|
179
|
+
* async function save() {
|
|
180
|
+
* await api.save(form.value);
|
|
181
|
+
* // Marks the form as saved
|
|
182
|
+
* saved.value = structuredClone(toRaw(form.value));
|
|
183
|
+
* }
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
function useUnsavedChanges(values, options = {}) {
|
|
187
|
+
const hasChanges = computed(() => !isDeepEqual(toValue(values.previous), toValue(values.current)));
|
|
188
|
+
const { confirm } = useConfirm();
|
|
189
|
+
/** Answer of the dialog on display, shared by every navigation made while it's open. */
|
|
190
|
+
let pending;
|
|
191
|
+
onBeforeRouteLeave(() => {
|
|
192
|
+
if (!hasChanges.value) return true;
|
|
193
|
+
pending ??= confirm({
|
|
194
|
+
title: options.title ?? "Alterações não salvas",
|
|
195
|
+
message: options.message ?? "Existem alterações que ainda não foram salvas. Deseja sair sem salvar?",
|
|
196
|
+
confirmText: options.confirmText ?? "Sair sem salvar",
|
|
197
|
+
cancelText: options.cancelText ?? "Continuar editando",
|
|
198
|
+
color: options.color ?? "danger"
|
|
199
|
+
}, async () => {}).then((answer) => answer !== false).finally(() => pending = void 0);
|
|
200
|
+
return pending;
|
|
201
|
+
});
|
|
202
|
+
/** Whether the component is on display, which is false while `<KeepAlive>` keeps it deactivated. */
|
|
203
|
+
let isActive = true;
|
|
204
|
+
if (options.beforeUnload ?? true) {
|
|
205
|
+
onMounted(() => watch(hasChanges, updateBeforeUnload, { immediate: true }));
|
|
206
|
+
onUnmounted(deactivate);
|
|
207
|
+
onDeactivated(deactivate);
|
|
208
|
+
onActivated(() => {
|
|
209
|
+
isActive = true;
|
|
210
|
+
updateBeforeUnload();
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/** Identifies this call in `unloadGuards`. */
|
|
214
|
+
const unloadGuard = {};
|
|
215
|
+
function updateBeforeUnload() {
|
|
216
|
+
if (hasChanges.value && isActive) unloadGuards.add(unloadGuard);
|
|
217
|
+
else unloadGuards.delete(unloadGuard);
|
|
218
|
+
updateUnloadListener();
|
|
219
|
+
}
|
|
220
|
+
function deactivate() {
|
|
221
|
+
isActive = false;
|
|
222
|
+
updateBeforeUnload();
|
|
223
|
+
}
|
|
224
|
+
return { hasChanges };
|
|
225
|
+
}
|
|
226
|
+
//#endregion
|
|
227
|
+
export { useConfirm, useDialog, useRunOrToast, useToast, useUnsavedChanges };
|
package/dist/plugins.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Plugin, UserConfig } from "vite-plus";
|
|
2
2
|
//#region src/plugins.d.ts
|
|
3
3
|
/**
|
|
4
|
-
* Vite plugin that compiles the components' styles with the library's font and Sass settings
|
|
4
|
+
* Vite plugin that compiles the components' styles with the library's font and Sass settings, and
|
|
5
|
+
* loads the icon font with `font-display: swap`, so it doesn't block rendering while it loads.
|
|
5
6
|
* Required: without it, the components lose the Unimed Slab font. Needs `sass-embedded`, a peer
|
|
6
7
|
* dependency.
|
|
7
8
|
*
|
package/dist/plugins.js
CHANGED
|
@@ -10,8 +10,13 @@ const vuetifyStylesheets = "/node_modules/vuetify/lib/";
|
|
|
10
10
|
* Vite doesn't compile. The URL still serves the compiled stylesheet, which Vite loads separately
|
|
11
11
|
*/
|
|
12
12
|
const uncompiledStylesheetQuery = /[?&](?:raw|url)\b/;
|
|
13
|
+
/** Matches the ids of the icon font's stylesheet, with or without a query and Windows separators */
|
|
14
|
+
const iconFontStylesheet = /[\\/]node_modules[\\/]@mdi[\\/]font[\\/]css[\\/]materialdesignicons\.css(?:\?|$)/;
|
|
15
|
+
/** Start of the icon font's `@font-face` rule, the only one in its stylesheet */
|
|
16
|
+
const iconFontFace = "@font-face {";
|
|
13
17
|
/**
|
|
14
|
-
* Vite plugin that compiles the components' styles with the library's font and Sass settings
|
|
18
|
+
* Vite plugin that compiles the components' styles with the library's font and Sass settings, and
|
|
19
|
+
* loads the icon font with `font-display: swap`, so it doesn't block rendering while it loads.
|
|
15
20
|
* Required: without it, the components lose the Unimed Slab font. Needs `sass-embedded`, a peer
|
|
16
21
|
* dependency.
|
|
17
22
|
*
|
|
@@ -55,6 +60,18 @@ function vitePluginUimed() {
|
|
|
55
60
|
compiler = void 0;
|
|
56
61
|
await (await current)?.dispose();
|
|
57
62
|
}
|
|
63
|
+
}, {
|
|
64
|
+
name: "uimed:icon-font",
|
|
65
|
+
enforce: "pre",
|
|
66
|
+
transform: {
|
|
67
|
+
filter: { id: {
|
|
68
|
+
include: iconFontStylesheet,
|
|
69
|
+
exclude: uncompiledStylesheetQuery
|
|
70
|
+
} },
|
|
71
|
+
handler(code) {
|
|
72
|
+
return code.replace(iconFontFace, `${iconFontFace} font-display: swap;`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
58
75
|
}];
|
|
59
76
|
}
|
|
60
77
|
/**
|