@nexdom/uimed-vue 2.0.0-beta.24 → 2.0.0-beta.26
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 +2 -2
- package/dist/components.d.ts +12 -2
- package/dist/components.js +124 -9
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -151,7 +151,7 @@ Use an existing component (e.g. `button`) as the reference, and deliver all of t
|
|
|
151
151
|
|
|
152
152
|
1. `src/components/<name>/<name>.vue` (following the pattern in [Code conventions](#code-conventions)), `types.ts` and `__tests__/<name>.test.ts`.
|
|
153
153
|
2. The `U`-prefixed export in `src/components/index.ts`.
|
|
154
|
-
3. A usage guide at `docs/guide/components/<name>.md`, with `<demo>` examples (and a `<playground>` section when the component has configurable props), and an API reference at `docs/api/components/<name>.md` (props, events and slots tables). Both in Portuguese.
|
|
154
|
+
3. A usage guide at `docs/guide/components/<name>.md`, with `<demo>` examples (and a `<playground>` section when the component has configurable props), and an API reference at `docs/api/components/<name>.md` (props, events and slots tables). Both in Portuguese. In a demo's template inside a `.md` file, don't put an attribute with a template literal after one with an arrow function in the same tag: the Markdown type-check (`vue-tsc`) reads the `>` of `=>` as the end of the tag and fails with `TS1003`. Order the attributes so the template literal comes first.
|
|
155
155
|
4. Both pages registered in the sidebar at `docs/.vitepress/config.ts`.
|
|
156
156
|
5. `e2e/components/<name>.spec.ts` covering the guide's interactive examples and a "UI consistency" screenshot check (plus accessible snapshots where relevant). Commit the generated files under `__snapshots__`/`__screenshot__`.
|
|
157
157
|
6. The full CI sequence passing locally.
|
|
@@ -173,7 +173,7 @@ Use an existing composable (e.g. `use-toast`) as the reference, and deliver in t
|
|
|
173
173
|
- A new component/composable must be listed in its guide page (`docs/guide/…`), its API page (`docs/api/…`), both sidebars in `docs/.vitepress/config.ts`, the guide index table (`docs/guide/index.md`) and the API index list (`docs/api/index.md`).
|
|
174
174
|
- Never write CSS or `<style>` blocks, and expose styling to consumers only through props.
|
|
175
175
|
- Adjustments that Vuetify exposes as Sass variables (e.g. a font family or a line height) go in `src/styles/settings.scss`, which `vitePluginUimed()` compiles into Vuetify's stylesheets in the apps. That file only configures variables: still no CSS rules.
|
|
176
|
-
- Don't add spacing to Vuetify components: no padding or margin utility classes (`pa-*`, `pt-6`, `px-4`, `ma-2`, ...) and no spacing props overriding the defaults. Vuetify already applies Material Design 3 spacing, and overriding it breaks what it handles on its own, such as button paddings and the scrollbar of scrollable content. If the default spacing looks wrong, stop and ask instead of adjusting it.
|
|
176
|
+
- Don't add spacing to Vuetify components: no padding or margin utility classes (`pa-*`, `pt-6`, `px-4`, `ma-2`, ...) and no spacing props overriding the defaults. Vuetify already applies Material Design 3 spacing, and overriding it breaks what it handles on its own, such as button paddings and the scrollbar of scrollable content. If the default spacing looks wrong, stop and ask instead of adjusting it. The one accepted layout margin is `mt-auto` on `v-card-actions` (as in `USection`), which pushes the actions to the bottom of a full-height card without setting any spacing value.
|
|
177
177
|
- Prefer a Vuetify component's own props to building its parts by hand (e.g. `title`/`text` on `v-card` instead of `v-card-item` > `v-card-title` > `v-card-text`). A non-spacing utility class is acceptable only when no prop covers the need (e.g. `text-wrap` on a card title), with a comment saying why.
|
|
178
178
|
- Keep components thin: a component wires props, slots and events to Vuetify. Logic beyond that (focus management, queues, DOM lookups, comparisons) goes into an internal composable with its own unit tests.
|
|
179
179
|
- Path aliases: `@/*` → `src/*`, `@e2e/*` → `e2e/*`.
|
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
|
-
clearable: boolean;
|
|
437
436
|
type: TextFieldType;
|
|
437
|
+
clearable: boolean;
|
|
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
|
|
@@ -1289,6 +1289,12 @@ type DataSetItemProps = {
|
|
|
1289
1289
|
* @default "primary"
|
|
1290
1290
|
*/
|
|
1291
1291
|
variant?: SectionVariant;
|
|
1292
|
+
/**
|
|
1293
|
+
* Actions of the record, like "Editar" and "Excluir", displayed at the
|
|
1294
|
+
* bottom of its card, just like on `USection`. When undefined or empty, no
|
|
1295
|
+
* actions area is rendered.
|
|
1296
|
+
*/
|
|
1297
|
+
actions?: SectionAction[];
|
|
1292
1298
|
/**
|
|
1293
1299
|
* Component id to use on automated tests.
|
|
1294
1300
|
*/
|
|
@@ -1316,7 +1322,11 @@ type DataSetItemSlots = {
|
|
|
1316
1322
|
* <template>
|
|
1317
1323
|
* <u-data-set :items="patients">
|
|
1318
1324
|
* <template #default="{ item }">
|
|
1319
|
-
* <u-data-set-item
|
|
1325
|
+
* <u-data-set-item
|
|
1326
|
+
* :title="item.name"
|
|
1327
|
+
* subtitle="Beneficiário"
|
|
1328
|
+
* :actions="[{ label: 'Editar', variant: 'secondary', onClick: () => edit(item) }]"
|
|
1329
|
+
* >
|
|
1320
1330
|
* <u-table :headers="['Plano', 'Idade']" :items="[[item.plan, item.age]]" vertical />
|
|
1321
1331
|
* </u-data-set-item>
|
|
1322
1332
|
* </template>
|
package/dist/components.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as messages, n as owners, r as requests, s as useVuetifyColor } from "./use-dialog-Ck2_ZW7T.js";
|
|
2
|
-
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, guardReactiveProps, inject, isRef, mergeModels, mergeProps, normalizeClass, normalizeProps, onMounted, onScopeDispose, onUnmounted, openBlock, provide, ref, renderList, renderSlot, resolveDynamicComponent, shallowRef, toDisplayString, toValue, unref, useId, useModel, useSlots, useTemplateRef, watch, withCtx, withModifiers } from "vue";
|
|
1
|
+
import { a as messages, n as owners$1, r as requests, s as useVuetifyColor } from "./use-dialog-Ck2_ZW7T.js";
|
|
2
|
+
import { Fragment, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, guardReactiveProps, inject, isRef, mergeModels, mergeProps, nextTick, normalizeClass, normalizeProps, onMounted, onScopeDispose, onUnmounted, openBlock, provide, ref, renderList, renderSlot, resolveDynamicComponent, shallowRef, toDisplayString, toValue, unref, useId, useModel, useSlots, useTemplateRef, watch, withCtx, withKeys, withModifiers } from "vue";
|
|
3
3
|
import { pt } from "vuetify/locale";
|
|
4
4
|
import { VApp, VAppBar, VAppBarNavIcon, VAppBarTitle, VAutocomplete, VAvatar, VBadge, VBtn, VCard, VCardActions, VCardText, VCheckboxBtn, VCol, VCombobox, VContainer, VDataIterator, VDatePicker, VDialog, VDivider, VEmptyState, VExpansionPanel, VExpansionPanelText, VExpansionPanelTitle, VExpansionPanels, VForm, VIcon, VIconBtn, VImg, VList, VListGroup, VListItem, VListItemAction, VListSubheader, VLocaleProvider, VMain, VMenu, VNavigationDrawer, VPagination, VProgressLinear, VRow, VSkeletonLoader, VSnackbarQueue, VTable, VTextField, VTimePicker } from "vuetify/components";
|
|
5
5
|
import { formatDateTime, isEmail, isEmpty, isPhone, isUrl } from "@nexdom/shared/utils";
|
|
@@ -15,6 +15,101 @@ function isExternalRoute(route) {
|
|
|
15
15
|
return typeof route === "string" && route.startsWith("http");
|
|
16
16
|
}
|
|
17
17
|
//#endregion
|
|
18
|
+
//#region src/composables/navigation/use-navigation-search-shortcut.ts
|
|
19
|
+
/**
|
|
20
|
+
* Mounted navigation menus, in mounting order. Only the first one answers the shortcut, so pages
|
|
21
|
+
* with more than one `UMain` (like the docs) don't open every menu at once.
|
|
22
|
+
*/
|
|
23
|
+
const owners = ref([]);
|
|
24
|
+
/**
|
|
25
|
+
* Focus targets where the shortcut is left to the page: overlays (dialogs, confirmations and
|
|
26
|
+
* menus) and other modals, the activators of open menus (which keep the focus when opened by a
|
|
27
|
+
* click, and would pull it back from the navigation menu) and rich text editors, which usually
|
|
28
|
+
* take Ctrl+K to insert links.
|
|
29
|
+
*/
|
|
30
|
+
const IGNORED_TARGETS = [
|
|
31
|
+
".v-overlay",
|
|
32
|
+
"dialog",
|
|
33
|
+
"[aria-modal='true']",
|
|
34
|
+
"[aria-haspopup][aria-expanded='true']",
|
|
35
|
+
"[contenteditable]:not([contenteditable='false'])"
|
|
36
|
+
].join(", ");
|
|
37
|
+
/** Accessible name of the search field, whose placeholder also shows the shortcut. */
|
|
38
|
+
const SEARCH_NAME = "Buscar";
|
|
39
|
+
/**
|
|
40
|
+
* Shortcut keys for the current platform. Starts with the non-macOS keys and switches on mount,
|
|
41
|
+
* so server-rendered pages (the docs) hydrate without mismatches.
|
|
42
|
+
*/
|
|
43
|
+
function useNavigationShortcutKeys() {
|
|
44
|
+
const isMac = ref(false);
|
|
45
|
+
onMounted(() => isMac.value = isMacOS());
|
|
46
|
+
return {
|
|
47
|
+
label: computed(() => isMac.value ? "⌘K" : "Ctrl+K"),
|
|
48
|
+
ariaKeyshortcuts: computed(() => isMac.value ? "Meta+K" : "Control+K")
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Listens to Ctrl+K (⌘K on macOS) on `window` while mounted: opens the menu, waits for it to
|
|
53
|
+
* render (the drawer is `inert` while closed) and focuses the search field (a disabled field, as
|
|
54
|
+
* while loading, can't take the focus). When the menu opened by the shortcut closes with the focus
|
|
55
|
+
* inside it, the focus goes back to where it was.
|
|
56
|
+
*/
|
|
57
|
+
function useNavigationSearchShortcut(options) {
|
|
58
|
+
const { open, field } = options;
|
|
59
|
+
const owner = Symbol("navigation-search-shortcut");
|
|
60
|
+
/** Element focused before the shortcut opened the menu, which gets the focus back on close. */
|
|
61
|
+
let origin = null;
|
|
62
|
+
onMounted(() => {
|
|
63
|
+
owners.value.push(owner);
|
|
64
|
+
globalThis.addEventListener("keydown", onKeydown, { capture: true });
|
|
65
|
+
findInput().setAttribute("aria-label", SEARCH_NAME);
|
|
66
|
+
});
|
|
67
|
+
onUnmounted(() => {
|
|
68
|
+
owners.value = owners.value.filter((item) => item !== owner);
|
|
69
|
+
globalThis.removeEventListener("keydown", onKeydown, { capture: true });
|
|
70
|
+
});
|
|
71
|
+
watch(open, (isOpen) => {
|
|
72
|
+
if (isOpen) return;
|
|
73
|
+
const target = origin;
|
|
74
|
+
origin = null;
|
|
75
|
+
if (!findInput().closest(".v-navigation-drawer").contains(document.activeElement)) return;
|
|
76
|
+
if (target instanceof HTMLElement || target instanceof SVGElement) target.focus();
|
|
77
|
+
});
|
|
78
|
+
function onKeydown(event) {
|
|
79
|
+
if (owners.value[0] !== owner || isIgnoredTarget(event)) return;
|
|
80
|
+
if (isShortcut(event)) openAndFocus(event);
|
|
81
|
+
else if (event.key === "Escape" && origin) close();
|
|
82
|
+
}
|
|
83
|
+
async function openAndFocus(event) {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
if (!open.value) origin = document.activeElement;
|
|
86
|
+
open.value = true;
|
|
87
|
+
await nextTick();
|
|
88
|
+
const input = findInput();
|
|
89
|
+
input.focus({ preventScroll: true });
|
|
90
|
+
input.select();
|
|
91
|
+
}
|
|
92
|
+
function findInput() {
|
|
93
|
+
return field.value.$el.querySelector("input");
|
|
94
|
+
}
|
|
95
|
+
function close() {
|
|
96
|
+
open.value = false;
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
...useNavigationShortcutKeys(),
|
|
100
|
+
close
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function isMacOS() {
|
|
104
|
+
return navigator.userAgent.includes("Macintosh");
|
|
105
|
+
}
|
|
106
|
+
function isShortcut(event) {
|
|
107
|
+
return (isMacOS() ? event.metaKey : event.ctrlKey) && !event.shiftKey && !event.altKey && !event.isComposing && event.key.toLowerCase() === "k";
|
|
108
|
+
}
|
|
109
|
+
function isIgnoredTarget(event) {
|
|
110
|
+
return event.target instanceof Element && event.target.closest(IGNORED_TARGETS) !== null;
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
18
113
|
//#region src/components/app-bar/app-bar.vue?vue&type=script&setup=true&lang.ts
|
|
19
114
|
const _hoisted_1$3 = { class: "opacity-60" };
|
|
20
115
|
//#endregion
|
|
@@ -69,6 +164,7 @@ var app_bar_default = /* @__PURE__ */ defineComponent({
|
|
|
69
164
|
const props = __props;
|
|
70
165
|
const notificationsOpen = useModel(__props, "notificationsOpen");
|
|
71
166
|
const navigationOpen = useModel(__props, "navigationOpen");
|
|
167
|
+
const { ariaKeyshortcuts: navigationKeyshortcuts } = useNavigationShortcutKeys();
|
|
72
168
|
const helpBtnDataTestid = computed(() => `${props.dataTestid}-help`);
|
|
73
169
|
const notificationsBtnDataTestid = computed(() => `${props.dataTestid}-notifications`);
|
|
74
170
|
const navigationBtnDataTestid = computed(() => `${props.dataTestid}-navigation`);
|
|
@@ -221,8 +317,13 @@ var app_bar_default = /* @__PURE__ */ defineComponent({
|
|
|
221
317
|
"data-testid": navigationBtnDataTestid.value,
|
|
222
318
|
"aria-expanded": navigationOpen.value,
|
|
223
319
|
"aria-label": "Menu de navegação",
|
|
320
|
+
"aria-keyshortcuts": unref(navigationKeyshortcuts),
|
|
224
321
|
onClick: _cache[0] || (_cache[0] = ($event) => navigationOpen.value = !navigationOpen.value)
|
|
225
|
-
}, null, 8, [
|
|
322
|
+
}, null, 8, [
|
|
323
|
+
"data-testid",
|
|
324
|
+
"aria-expanded",
|
|
325
|
+
"aria-keyshortcuts"
|
|
326
|
+
])) : createCommentVNode("v-if", true),
|
|
226
327
|
__props.logo ? (openBlock(), createBlock(unref(VImg), {
|
|
227
328
|
key: 1,
|
|
228
329
|
src: props.logo,
|
|
@@ -488,6 +589,10 @@ var navigation_menu_default = /* @__PURE__ */ defineComponent({
|
|
|
488
589
|
const props = __props;
|
|
489
590
|
const open = useModel(__props, "modelValue");
|
|
490
591
|
const search = ref("");
|
|
592
|
+
const { label: shortcutLabel, close } = useNavigationSearchShortcut({
|
|
593
|
+
open,
|
|
594
|
+
field: useTemplateRef("searchField")
|
|
595
|
+
});
|
|
491
596
|
const filteredItems = computed(() => filterItems());
|
|
492
597
|
const searchDataTestid = computed(() => `${props.dataTestid}-search`);
|
|
493
598
|
const query = computed(() => search.value.toLowerCase());
|
|
@@ -518,17 +623,20 @@ var navigation_menu_default = /* @__PURE__ */ defineComponent({
|
|
|
518
623
|
"data-testid": props.dataTestid,
|
|
519
624
|
"aria-busy": props.loading || void 0,
|
|
520
625
|
absolute: "",
|
|
521
|
-
temporary: ""
|
|
626
|
+
temporary: "",
|
|
627
|
+
onKeydown: withKeys(unref(close), ["esc"])
|
|
522
628
|
}, {
|
|
523
629
|
prepend: withCtx(() => [createElementVNode("div", _hoisted_1$2, [createVNode(text_field_default, {
|
|
630
|
+
ref: "searchField",
|
|
524
631
|
modelValue: search.value,
|
|
525
632
|
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => search.value = $event),
|
|
526
633
|
type: "search",
|
|
527
|
-
|
|
634
|
+
placeholder: `Buscar (${unref(shortcutLabel)})`,
|
|
528
635
|
"data-testid": searchDataTestid.value,
|
|
529
636
|
disabled: props.loading
|
|
530
637
|
}, null, 8, [
|
|
531
638
|
"modelValue",
|
|
639
|
+
"placeholder",
|
|
532
640
|
"data-testid",
|
|
533
641
|
"disabled"
|
|
534
642
|
])])]),
|
|
@@ -561,7 +669,8 @@ var navigation_menu_default = /* @__PURE__ */ defineComponent({
|
|
|
561
669
|
}, 8, [
|
|
562
670
|
"modelValue",
|
|
563
671
|
"data-testid",
|
|
564
|
-
"aria-busy"
|
|
672
|
+
"aria-busy",
|
|
673
|
+
"onKeydown"
|
|
565
674
|
]);
|
|
566
675
|
};
|
|
567
676
|
}
|
|
@@ -892,7 +1001,7 @@ var dialog_host_default = /* @__PURE__ */ defineComponent({
|
|
|
892
1001
|
__name: "dialog-host",
|
|
893
1002
|
setup(__props) {
|
|
894
1003
|
const owner = Symbol("dialog");
|
|
895
|
-
const isOwner = computed(() => owners.value[0] === owner);
|
|
1004
|
+
const isOwner = computed(() => owners$1.value[0] === owner);
|
|
896
1005
|
const next = computed(() => isOwner.value ? requests.value[0] : void 0);
|
|
897
1006
|
/** Dialog on display, kept until its leave transition ends. */
|
|
898
1007
|
const request = shallowRef();
|
|
@@ -921,8 +1030,8 @@ var dialog_host_default = /* @__PURE__ */ defineComponent({
|
|
|
921
1030
|
persistent: () => isRunning.value,
|
|
922
1031
|
afterLeave: showNext
|
|
923
1032
|
});
|
|
924
|
-
onMounted(() => owners.value.push(owner));
|
|
925
|
-
onUnmounted(() => owners.value = owners.value.filter((item) => item !== owner));
|
|
1033
|
+
onMounted(() => owners$1.value.push(owner));
|
|
1034
|
+
onUnmounted(() => owners$1.value = owners$1.value.filter((item) => item !== owner));
|
|
926
1035
|
watch(next, show);
|
|
927
1036
|
function show() {
|
|
928
1037
|
if (request.value) return;
|
|
@@ -2253,6 +2362,10 @@ var data_set_item_default = /* @__PURE__ */ defineComponent({
|
|
|
2253
2362
|
type: String,
|
|
2254
2363
|
required: false
|
|
2255
2364
|
},
|
|
2365
|
+
actions: {
|
|
2366
|
+
type: Array,
|
|
2367
|
+
required: false
|
|
2368
|
+
},
|
|
2256
2369
|
dataTestid: {
|
|
2257
2370
|
type: String,
|
|
2258
2371
|
required: false
|
|
@@ -2266,6 +2379,7 @@ var data_set_item_default = /* @__PURE__ */ defineComponent({
|
|
|
2266
2379
|
title: props.title,
|
|
2267
2380
|
subtitle: props.subtitle,
|
|
2268
2381
|
variant: props.variant,
|
|
2382
|
+
actions: props.actions,
|
|
2269
2383
|
"data-testid": props.dataTestid,
|
|
2270
2384
|
"full-height": ""
|
|
2271
2385
|
}, {
|
|
@@ -2278,6 +2392,7 @@ var data_set_item_default = /* @__PURE__ */ defineComponent({
|
|
|
2278
2392
|
"title",
|
|
2279
2393
|
"subtitle",
|
|
2280
2394
|
"variant",
|
|
2395
|
+
"actions",
|
|
2281
2396
|
"data-testid"
|
|
2282
2397
|
]);
|
|
2283
2398
|
};
|