@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 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/*`.
@@ -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 :title="item.name" subtitle="Beneficiário">
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>
@@ -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, ["data-testid", "aria-expanded"])) : createCommentVNode("v-if", true),
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
- label: "Buscar",
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nexdom/uimed-vue",
3
- "version": "2.0.0-beta.24",
3
+ "version": "2.0.0-beta.26",
4
4
  "description": "A ui framework for NEXDOM applications.",
5
5
  "keywords": [
6
6
  "lib",