nuxt-unified-ui 0.5.0 → 0.5.2

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-unified-ui",
3
3
  "type": "module",
4
- "version": "0.5.0",
4
+ "version": "0.5.2",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./index.d.ts",
7
7
  "exports": {
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "dependencies": {
22
22
  "@formkit/tempo": "1.1.0",
23
- "@iconify-json/lucide": "1.2.122",
23
+ "@iconify-json/lucide": "1.2.123",
24
24
  "@nuxt/kit": "4.5.2",
25
25
  "@nuxt/ui": "4.10.0",
26
26
  "@nuxtjs/i18n": "10.6.0",
@@ -4,10 +4,13 @@ description: >-
4
4
  Single skill for the nuxt-unified-ui Nuxt layer and mandatory Nuxt code style:
5
5
  install/extend the layer, required CSS, modules/config, radashi radXxx
6
6
  auto-imports, un-form / useForm, launchFormPickerDialog /
7
- launchChoicePickerDialog, toast helpers, un-card / un-typography, and the
8
- whitespace/formatting/code-shape conventions for all Nuxt-generated code. Use
9
- when working in or consuming nuxt-unified-ui, or whenever generating Vue/Nuxt
10
- code that must match unified code style.
7
+ launchChoicePickerDialog, toast helpers, un-card / un-typography, pages /
8
+ definePageMeta, ufetch / useUFetch wrapping, unified resources
9
+ (server plugins, REST handleResource*, resource-manager dashboard /
10
+ customization), attribute order/defaults, and whitespace/formatting
11
+ conventions for all Nuxt-generated code. Use when working in or consuming
12
+ nuxt-unified-ui, declaring or customizing resources, or whenever generating
13
+ Vue/Nuxt code that must match unified code style.
11
14
  ---
12
15
 
13
16
  # nuxt-unified-ui
@@ -22,6 +25,7 @@ This is the **only** installable skill in this repo. Deep topics live under `ref
22
25
 
23
26
  - Installing / extending `nuxt-unified-ui` as a Nuxt layer
24
27
  - Using `un-form`, dialogs, toasts, `un-card`, radashi `radXxx`, etc.
28
+ - Declaring or customizing **unified resources** (plugins, REST, dashboard)
25
29
  - **Whenever generating or editing Nuxt/Vue/server code** that must follow the unified look (whitespace, wrapping, template shape, sectioning)
26
30
 
27
31
  ## References (read as needed)
@@ -29,6 +33,9 @@ This is the **only** installable skill in this repo. Deep topics live under `ref
29
33
  | Topic | File |
30
34
  |-------|------|
31
35
  | **Code style (mandatory)** | [references/code-style.md](references/code-style.md) |
36
+ | Unified resources (plugins → REST → dashboard) | [references/resources.md](references/resources.md) |
37
+ | Pages / routing | [references/pages.md](references/pages.md) |
38
+ | Data fetching (`ufetch` / `useUFetch`) | [references/data-fetching.md](references/data-fetching.md) |
32
39
  | Layer install + required CSS | [references/layer-setup.md](references/layer-setup.md) |
33
40
  | Public surface inventory | [references/public-surface.md](references/public-surface.md) |
34
41
  | Forms (`useForm` / `un-form`) | [references/forms.md](references/forms.md) |
@@ -49,16 +56,19 @@ Absolute highlights:
49
56
 
50
57
  - `<script setup>` only — **never** `lang="ts"`; no TS annotations in Vue (runtime prop types)
51
58
  - 2-space indent; single quotes; semicolons; trailing commas in multi-line literals
59
+ - **`.js` / `.ts` file start:** two leading blank lines, **except** when the file starts with imports — then **no** blank lines before the first `import`
52
60
  - Double blank lines between major sections; blank line before `</script>`; **two** blanks before `<template>`
53
61
  - Non-trivial async/functions: blank line after `{`, double blank between major steps, blank before `}`
54
62
  - `else` / `catch` on their own line after `}`
55
63
  - Script object literals always multi-line (even one property)
56
64
  - Kebab-case tags (`u-button`, `un-card`)
57
65
  - `v-if` / `v-for` on `<template>` wrappers — not on rendered nodes
58
- - **2+ attributes → one per line**; non-self-closing `>` on same line as last attr; multi-line self-closing `/>` on its own line
59
- - `{{ ... }}` on its own line
66
+ - **2+ attributes → one per line** except **`u-modal`** (keep all `u-modal` attrs on one line); attribute order + default omissions (`variant="subtle"`, **Cancel only → `ghost`**, omit neutral `color`, `loading-auto`); non-self-closing `>` on same line as last attr; multi-line self-closing `/>` on its own line
67
+ - `{{ ... }}` on its own line (static + dynamic text may mix)
60
68
  - `/* section */` comments; imports co-located under the section that uses them
61
69
  - Light naming: `handleXxx` handlers, `it` in short callbacks, descriptive `for...of`, computeds use block + `return`
70
+ - Pages: explicit `definePageMeta.name`, reactive route params, named `navigateTo` / `nuxt-link` → [pages.md](references/pages.md)
71
+ - Fetching: `ufetch(url, {` one line; `useUFetch` with URL on next line + `data*Data` / `is*Loading` / `refresh*` → [data-fetching.md](references/data-fetching.md)
62
72
 
63
73
  ---
64
74
 
@@ -132,6 +142,9 @@ From `nuxt.config.ts`: `@vueuse/nuxt`, `@nuxt/ui`, `@nuxtjs/i18n`; `ui.colorMode
132
142
  | Page chrome | `un-typography` + `un-card` |
133
143
  | Custom field | `registerFormExtraElement` in a plugin |
134
144
  | Utilities | `radXxx` → [radashi.md](references/radashi.md) |
145
+ | New page / route | [pages.md](references/pages.md) |
146
+ | List/detail fetch or mutation | [data-fetching.md](references/data-fetching.md) |
147
+ | New / custom resource | [resources.md](references/resources.md) |
135
148
  | Formatting any of the above | [code-style.md](references/code-style.md) |
136
149
 
137
150
  ## Do / don’t
@@ -143,6 +156,7 @@ From `nuxt.config.ts`: `@vueuse/nuxt`, `@nuxt/ui`, `@nuxtjs/i18n`; `ui.colorMode
143
156
  - Use field `identifier` for element kind; `type` only for HTML input types
144
157
  - Handle dialog actions in `onClick`
145
158
  - Follow code style for every generated file
159
+ - Resources: plugin → full REST set → dashboard nav / custom `<resource-manager>` page → [resources.md](references/resources.md)
146
160
 
147
161
  **Don’t**
148
162
 
@@ -151,3 +165,4 @@ From `nuxt.config.ts`: `@vueuse/nuxt`, `@nuxt/ui`, `@nuxtjs/i18n`; `ui.colorMode
151
165
  - Use PascalCase component tags in templates
152
166
  - Set choice-button `value` unless the await result must distinguish buttons
153
167
  - Assume color mode is enabled (layer disables it)
168
+ - Reimplement resource CRUD in route files (use `handleResource*`; customize via dedicated pages + domain APIs)
@@ -20,7 +20,7 @@ Write code so a reader can **scan vertically** and see structure before details.
20
20
  Object/array literals in script are multi-line with trailing commas — even single-property objects passed to helpers (`toastSuccess`, `ufetch` options, etc.). Compact one-liners hide diffs and force horizontal reading.
21
21
 
22
22
  4. **Templates are layout, not mini-scripts.**
23
- Structural directives live on `<template>` wrappers so the rendered node stays a clean component/element. Attributes wrap predictably; closing `>` / `/>` placement is consistent; interpolations sit on their own line so markup nesting is obvious.
23
+ Structural directives live on `<template>` wrappers so the rendered node stays a clean component/element. Attributes wrap predictably (`u-modal` stays one line); closing `>` / `/>` placement is consistent; interpolations sit on their own line (static + dynamic text may share one `{{ ... }}` or surrounding text).
24
24
 
25
25
  5. **Section comments are the map.**
26
26
  `/* section */` labels replace scavenger hunts through long `<script setup>` blocks. Imports sit next to the section that needs them, not in a hoisted pile at the top.
@@ -48,11 +48,49 @@ When editing an existing file, **absolute rules below always win**. For choices
48
48
  | Component tags | lowercase kebab-case (`u-button`, `un-card`) — never PascalCase |
49
49
  | Braces | always for `if` / `else` / `for` / `while` — no brace-less single-liners |
50
50
  | `else` / `catch` | on their **own line** after `}` |
51
+ | `.js` / `.ts` file start | **two** blank lines at the top, **unless** the file starts with imports — then **no** blank lines before the first `import` |
51
52
 
52
53
  ---
53
54
 
54
55
  ## Vertical whitespace (script / TS)
55
56
 
57
+ ### File start (`.js` / `.ts` only)
58
+
59
+ Start of every `.js` and `.ts` file must have **two blank lines**, except if it has imports at the start of the file — then put **no** blank lines before the first import.
60
+
61
+ ```ts
62
+ // ✅ no imports — exactly two blank lines, then code
63
+
64
+
65
+ export default defineEventHandler(async event => {
66
+ ...
67
+ });
68
+ ```
69
+
70
+ ```ts
71
+ // ✅ imports at start — import on line 1, no leading blanks
72
+ import { join } from 'node:path';
73
+
74
+
75
+ const { schema, type, inferred } = parseSchema({
76
+ ...
77
+ });
78
+ ```
79
+
80
+ ```ts
81
+ // ❌ missing leading blanks when there are no imports
82
+ export default defineEventHandler(async event => {
83
+ ...
84
+ });
85
+ ```
86
+
87
+ ```ts
88
+ // ❌ blank lines before the first import
89
+ import { join } from 'node:path';
90
+ ```
91
+
92
+ Vue SFCs are unchanged: `<script setup>` begins immediately inside the script block (no artificial leading blanks at the top of the `.vue` file).
93
+
56
94
  ### Section rhythm
57
95
 
58
96
  - **Double blank line** between major boundaries: after `defineProps`/`defineEmits` blocks, between `/* section */` domains, before handler functions, between major async steps.
@@ -300,7 +338,8 @@ Keep tight `v-if` / `v-else` chains adjacent (no blank line between matching bra
300
338
  ### Attribute wrapping (hard rule)
301
339
 
302
340
  - **0–1 attributes:** may stay on one line with the tag
303
- - **2+ attributes:** one attribute per line (always)
341
+ - **2+ attributes:** one attribute per line — **except `u-modal`**
342
+ - **`u-modal` only:** keep **all** attributes on the **same single line** as the tag (do not wrap), even when there are many
304
343
 
305
344
  ```vue
306
345
  <!-- ✅ 0–1 attributes — inline OK -->
@@ -310,14 +349,12 @@ Keep tight `v-if` / `v-else` chains adjacent (no blank line between matching bra
310
349
  </u-form-field>
311
350
  <u-icon name="lucide:check" />
312
351
 
313
- <!-- ✅ 2+ attributes — one per line -->
314
- <u-modal
315
- :ui="{ content: 'max-w-5xl' }"
316
- scrollable
317
- @update:open="!$event && emit('close')">
352
+ <!-- ✅ u-modalalways one line (exception) -->
353
+ <u-modal :ui="{ content: 'max-w-5xl' }" scrollable @update:open="!$event && emit('close')">
318
354
  ...
319
355
  </u-modal>
320
356
 
357
+ <!-- ✅ other components — 2+ attributes, one per line -->
321
358
  <u-button
322
359
  variant="subtle"
323
360
  icon="lucide:refresh-ccw"
@@ -330,18 +367,65 @@ Keep tight `v-if` / `v-else` chains adjacent (no blank line between matching bra
330
367
  When wrapping, order attributes as:
331
368
 
332
369
  1. Refs / identity: `ref`, `id`, `name`
333
- 2. Visual props: `variant`, `color`, `size`, `icon`, static `label`
370
+ 2. Component visual props: `variant`, `color`, `size`, `icon`, static `label`
334
371
  3. Static presentation: `class`, `style`
335
372
  4. Data bindings: `:items`, `:data`, `:placeholder`, `:value`, dynamic `:label`, …
336
373
  5. `v-model` / `:model-value` / `v-model:*`
337
- 6. Navigation / state: `to`, `href`, `block`, `disabled`, `loading`, `loading-auto`, `fluid-body`, …
374
+ 6. Navigation / state: `to`, `href`, `block`, `disabled`, `loading`, `loading-auto`, `fluid-body`, `scrollable`,
338
375
  7. Events last: `@click`, `@update:*`, …
339
376
 
340
377
  Practical shortcuts:
341
378
 
342
379
  - `u-button`: `variant` → `color` → `size` → `icon` → label/value → `block` → `disabled` → `loading-auto` → events
343
- - `u-input` / `u-select*`: user-facing props → `:loading`/`:disabled` → `:items` → `class` → `v-model` → events
380
+ - `u-input` / `u-select*`: user-facing props (`:placeholder`, `:label`) transient state (`:loading`, `:disabled`)data source (`:items`) → `class` → `v-model` → events
381
+ - Action objects (`:actions`, `:append-actions`, table row actions): `vIf` → `color` → `icon` → `label` → `tooltip` → `disabled` → `to` → `onClick`
382
+ - Tab / select item objects: `value` → `icon` → `label`
344
383
 
384
+ ### Default attribute values (omit noise)
385
+
386
+ Prefer defaults by **omitting** props rather than restating them:
387
+
388
+ | Component / context | Default convention |
389
+ |---------------------|--------------------|
390
+ | `u-button` | Prefer `variant="subtle"` (or omit) for normal actions |
391
+ | Cancel buttons only | Use `variant="ghost"` / `variant: 'ghost'` — **only** for Cancel dismiss actions; do not use `ghost` on other buttons |
392
+ | Async `u-button` clicks | Prefer `loading-auto` over hand-rolled `isLoading` when nothing else depends on that flag |
393
+ | `u-badge` | Always `variant="subtle"`; use `icon` + `:label` (no default slot text); **do not** set `size`; **omit** `color` for neutral (use `undefined` in ternaries, never `color="neutral"`) |
394
+ | `u-tooltip` | Do not set `:delay-duration` — use the default delay |
395
+ | Action objects where subtle is the local default | Omit `variant: 'subtle'` unless overriding; Cancel actions set `variant: 'ghost'` |
396
+ | Icons | Always `lucide:*` prefix |
397
+
398
+ ```vue
399
+ <!-- ✅ omit neutral color; subtle badge -->
400
+ <u-badge
401
+ variant="subtle"
402
+ :label="item.name"
403
+ />
404
+
405
+ <!-- ✅ ternary falls back with undefined -->
406
+ <u-badge
407
+ variant="subtle"
408
+ :color="item.digital ? 'info' : undefined"
409
+ :label="item.digital ? 'Digital' : 'Physical'"
410
+ />
411
+
412
+ <!-- ❌ restating neutral / wrong variant -->
413
+ <u-badge
414
+ variant="outline"
415
+ color="neutral"
416
+ :label="item.name"
417
+ />
418
+ ```
419
+
420
+ ```vue
421
+ <!-- ✅ async button -->
422
+ <u-button
423
+ variant="subtle"
424
+ icon="lucide:trash"
425
+ loading-auto
426
+ @click="handleDelete()"
427
+ />
428
+ ```
345
429
  ### `>` and `/>` placement
346
430
 
347
431
  **Non-self-closing**, multi-attribute: `>` on the **same line** as the last attribute:
@@ -371,12 +455,24 @@ Closing tags for block components (`</un-card>`, `</u-modal>`, …) always on th
371
455
 
372
456
  ### Text interpolation
373
457
 
374
- Put `{{ ... }}` on its own line:
458
+ Put `{{ ... }}` on its own line (not glued onto the opening/closing tag). **Static + dynamic text may mix** in the same interpolation or around it — do not force template literals / split words solely to isolate dynamics:
375
459
 
376
460
  ```vue
377
461
  <h1 class="text-2xl font-semibold">
378
462
  Login
379
463
  </h1>
464
+
465
+ <!-- ✅ static + dynamic together -->
466
+ <h1 class="text-2xl font-semibold">
467
+ Welcome, {{ user.name }}!
468
+ </h1>
469
+
470
+ <h2 class="mt-1">
471
+ {{ flashCardData?.category?.name }} - by {{ flashCardData?.owner?.name }}
472
+ </h2>
473
+
474
+ <!-- ❌ glued to tags -->
475
+ <h1 class="text-2xl font-semibold">{{ user.name }}</h1>
380
476
  ```
381
477
 
382
478
  ### Root structure
@@ -408,6 +504,38 @@ Refs unwrap automatically — do not write `.value` in template expressions or i
408
504
 
409
505
  ---
410
506
 
507
+ ## Pages, routing, and data fetching
508
+
509
+ These are part of the same “shape” conventions when generating app code:
510
+
511
+ - Pages / named routes / reactive params → [pages.md](pages.md)
512
+ - `ufetch` / `useUFetch` wrapping, naming, options order → [data-fetching.md](data-fetching.md)
513
+
514
+ ### `useUFetch` wrapping (summary)
515
+
516
+ ```ts
517
+ const { data: ordersData, pending: isOrdersLoading, refresh: refreshOrders } = useUFetch(
518
+ computed(() => `/api/patients/${patientUid.value}/orders`),
519
+ {
520
+ query: {
521
+ limit: itemsPerPage,
522
+ },
523
+ },
524
+ );
525
+ ```
526
+
527
+ - First line: destructure + `= useUFetch(`
528
+ - Next line: URL (string or `computed`)
529
+ - Optional multi-line options object
530
+ - Closing `);` alone on the last line
531
+ - Consecutive `useUFetch` calls: **one** blank line between them
532
+
533
+ ### `ufetch` wrapping (summary)
534
+
535
+ Keep `ufetch(url, {` on one line — do not break the URL above `{`.
536
+
537
+ ---
538
+
411
539
  ## Light naming (reading shape)
412
540
 
413
541
  | Context | Convention |
@@ -443,9 +571,11 @@ Pass handler **references** into action objects / watchers when possible (`onCli
443
571
 
444
572
  ## Server / plain TS files
445
573
 
446
- Same whitespace, brace, literal, and call-formatting rules as script blocks:
574
+ Same whitespace, brace, literal, and call-formatting rules as script blocks. Honor the **file-start** rule (two leading blanks, or imports flush at line 1). Prefer `async event =>` style consistent with siblings:
447
575
 
448
576
  ```ts
577
+
578
+
449
579
  export default defineEventHandler(async event => {
450
580
 
451
581
  await assertRateLimit({
@@ -479,8 +609,6 @@ export default defineEventHandler(async event => {
479
609
  });
480
610
  ```
481
611
 
482
- Leading blank line at top of file is fine when the local tree uses it. Prefer `async event =>` style consistent with siblings.
483
-
484
612
  ---
485
613
 
486
614
  ## Anti-patterns (quick)
@@ -493,11 +621,18 @@ Leading blank line at top of file is fine when the local tree uses it. Prefer `a
493
621
  | `if (!x) return;` | braced block |
494
622
  | `} else {` | `}\nelse {` |
495
623
  | `toastSuccess({ title: 'x' })` one-liner object | multi-line object + trailing comma |
496
- | 3 attrs on one line | one attr per line |
624
+ | 3 attrs on one line (non-`u-modal`) | one attr per line |
625
+ | Multi-line `u-modal` attrs | keep `u-modal` attrs on one line |
497
626
  | `>` on its own line after attrs | `>` after last attr |
498
- | `{{ x }}` glued to tags | interpolation on its own line |
627
+ | `{{ x }}` glued to tags | interpolation on its own line (static + dynamic mix OK) |
628
+ | `ghost` on non-Cancel buttons | `ghost` only for Cancel |
499
629
  | Hoisted import block | imports co-located under section |
500
630
  | `computed(() => [ ... ])` | `computed(() => { return [ ... ]; })` |
631
+ | `color="neutral"` on badge | omit `color` / use `undefined` |
632
+ | `ufetch(\n url,\n {` | `ufetch(url, {` on one line |
633
+ | One-line `useUFetch(...)` | URL on next line; options multi-line |
634
+ | `.ts`/`.js` with no leading blanks (and no imports) | two blank lines at file start |
635
+ | Blank lines before first `import` | `import` on line 1 |
501
636
 
502
637
  ---
503
638
 
@@ -505,6 +640,7 @@ Leading blank line at top of file is fine when the local tree uses it. Prefer `a
505
640
 
506
641
  - [ ] `<script setup>` without `lang="ts"`; no TS annotations in Vue
507
642
  - [ ] 2-space indent; single quotes; semicolons; trailing commas in multi-line literals
643
+ - [ ] `.js`/`.ts`: two leading blank lines, or imports flush at line 1 (no blanks before first import)
508
644
  - [ ] Double blanks between major sections; blank line before `</script>`; two blanks before `<template>`
509
645
  - [ ] Non-trivial functions: blank after `{`, double blank between major steps, blank before `}`
510
646
  - [ ] Tiny helpers stay tight
@@ -512,9 +648,11 @@ Leading blank line at top of file is fine when the local tree uses it. Prefer `a
512
648
  - [ ] Script objects multi-line; template single-key objects may be inline
513
649
  - [ ] Kebab-case component tags
514
650
  - [ ] `v-if` / `v-for` on `<template>` wrappers
515
- - [ ] 2+ attributes → one per line; `>` same line as last attr; multi-line self-closing `/>` on own line
516
- - [ ] Attribute order respected
517
- - [ ] `{{ }}` on own line
651
+ - [ ] 2+ attributes → one per line (**`u-modal` attrs stay on one line**); `>` same line as last attr; multi-line self-closing `/>` on own line
652
+ - [ ] Attribute order + default-value omissions respected (`subtle`, **Cancel → `ghost` only**, no neutral color noise, `loading-auto`)
653
+ - [ ] `{{ }}` on own line (static + dynamic mix OK)
518
654
  - [ ] Section comments + import co-location where the file has sections
519
655
  - [ ] `handleXxx` for action handlers; `it` for short callbacks; descriptive loop names
520
656
  - [ ] Computeds that return structures use block + `return`
657
+ - [ ] Pages: explicit `definePageMeta.name`, reactive route params, named navigation ([pages.md](pages.md))
658
+ - [ ] Fetching: `ufetch` / `useUFetch` wrap styles and destructure names ([data-fetching.md](data-fetching.md))
@@ -0,0 +1,196 @@
1
+ # Data fetching (`ufetch` / `useUFetch`)
2
+
3
+ Prefer host-app unified API helpers (`ufetch`, `useUFetch`) over raw `useFetch` / `useAsyncData` / `$fetch` for app API calls. Formatting rules below are mandatory; wire paths to whatever your app’s API plugin expects (relative paths — do not manually prepend `baseApiUrl`).
4
+
5
+ Combine with [code-style.md](code-style.md).
6
+
7
+ ## `ufetch` (imperative)
8
+
9
+ One-off requests (submit, delete, button click):
10
+
11
+ ```ts
12
+ const response = await ufetch(`/api/resources/${id}`, {
13
+ silent: true,
14
+ method: 'post',
15
+ body: {
16
+ field: value,
17
+ },
18
+ });
19
+ ```
20
+
21
+ Rules:
22
+
23
+ - Name the result `response` (not `result`), unless a more specific name is clearer (`loginResponse`)
24
+ - Inline `body` / `query` objects in the options — extract only when large/reused
25
+ - `silent: true` — suppress automatic error toast when handling errors locally
26
+ - `responseType: 'blob'` — file downloads
27
+
28
+ ### Options order
29
+
30
+ - Mutations: behavior flags (`silent`, `responseType`) → `method` → `body` → `query` / other
31
+ - Reads without `method`/`body`: behavior flags before `query`
32
+
33
+ ### Call wrapping (critical)
34
+
35
+ Keep `ufetch(url, {` on **one line**. Do not break the URL onto its own line above `{`:
36
+
37
+ ```ts
38
+ // ✅
39
+ const response = await ufetch(`/api/items/${item.uid}/documents`, {
40
+ silent: true,
41
+ });
42
+
43
+ // ❌
44
+ const response = await ufetch(
45
+ `/api/items/${item.uid}/documents`,
46
+ {
47
+ silent: true,
48
+ },
49
+ );
50
+ ```
51
+
52
+ ### After mutation
53
+
54
+ ```ts
55
+ const response = await ufetch(url, {
56
+ method: 'post',
57
+ body: {
58
+ field: value,
59
+ },
60
+ });
61
+
62
+
63
+ await refresh();
64
+
65
+ toastSuccess({
66
+ title: 'Created successfully.',
67
+ });
68
+
69
+ formValue.value = '';
70
+ ```
71
+
72
+ Call `refresh()` **before** resetting local form UI state. Put side effects in dialog button `onClick` when the mutation is launched from a picker (see [dialogs.md](dialogs.md)).
73
+
74
+ ### Response guards
75
+
76
+ Fail-fast after `ufetch`:
77
+
78
+ 1. Special non-success statuses first when relevant
79
+ 2. Invalid success → early `return toastError({ ... })`
80
+ 3. Success path without deep `else` nesting
81
+
82
+ Prefer direct access on `response` (`response.status`) over optional chaining when the call is expected to return a body.
83
+
84
+ ---
85
+
86
+ ## `useUFetch` (reactive)
87
+
88
+ For route/param/reactive-driven lists and detail loads:
89
+
90
+ ```ts
91
+ const { data: ordersData, pending: isOrdersPending, refresh: refreshOrders } = useUFetch(
92
+ computed(() => `/api/patients/${patientUid.value}/orders`),
93
+ {
94
+ query: {
95
+ page: computed(() => currentPage.value - 1),
96
+ limit: itemsPerPage,
97
+ search: searchTerm,
98
+ },
99
+ },
100
+ );
101
+ ```
102
+
103
+ ### Wrapping style (critical)
104
+
105
+ Always format as:
106
+
107
+ 1. `const { ... } = useUFetch(` on the first line
108
+ 2. URL argument on the next line (string **or** `computed(() => ...)`)
109
+ 3. Optional options object as a multi-line second argument
110
+ 4. Closing `);` on its own line
111
+
112
+ ```ts
113
+ // ✅ string URL + options
114
+ const { data: mediaData, pending: isMediaPending, refresh: refreshMedia } = useUFetch(
115
+ '/api/media',
116
+ {
117
+ query: {
118
+ 'sort': '_id:-1',
119
+ 'limit': itemsPerPage,
120
+ },
121
+ },
122
+ );
123
+
124
+ // ✅ computed URL only
125
+ const { data: patientData, pending: isPatientPending, refresh: refreshPatient } = useUFetch(
126
+ computed(() => `/api/patients/${patientUid.value}`),
127
+ );
128
+
129
+ // ❌ crammed one-liner or URL broken like ufetch's wrong form without the useUFetch indent pattern
130
+ ```
131
+
132
+ ### Destructuring names
133
+
134
+ | Key | Convention | Example |
135
+ |-----|------------|---------|
136
+ | `data` | suffix `Data` | `ordersData`, `flashCardsData` |
137
+ | `pending` | `is` + name + `Pending` | `isOrdersPending` |
138
+ | `refresh` | prefix `refresh` | `refreshOrders` |
139
+
140
+ ### Spacing between consecutive calls
141
+
142
+ When several `useUFetch` calls sit in the same section, use **one** blank line between them (not two):
143
+
144
+ ```ts
145
+ const { data: mediaData, pending: isMediaPending, refresh: refreshMedia } = useUFetch(
146
+ '/api/media',
147
+ {
148
+ query: {
149
+ 'limit': itemsPerPage,
150
+ },
151
+ },
152
+ );
153
+
154
+ const { data: mediaCountData, refresh: refreshMediaCount } = useUFetch(
155
+ '/api/media/count',
156
+ );
157
+ ```
158
+
159
+ Major section boundaries elsewhere still use double blanks.
160
+
161
+ ### Conditional fetching
162
+
163
+ When the request must wait on a prop/id, prefer a reactive gate (e.g. reactive `method` or `enabled`) over one-shot `immediate: !!prop` evaluated only at mount:
164
+
165
+ ```ts
166
+ const { data: itemsData, pending: isItemsPending, refresh: refreshItems } = useUFetch(
167
+ computed(() => `/api/groups/${props.groupUid}/items`),
168
+ {
169
+ method: computed(() => props.groupUid ? 'get' : ''),
170
+ },
171
+ );
172
+ ```
173
+
174
+ Do **not** use `{ immediate: !!props.groupUid }` when the dependency can appear later.
175
+
176
+ ### Query values
177
+
178
+ - Refs and computeds are fine inside `query`
179
+ - Keep query object multi-line with trailing commas
180
+ - Quoted keys are fine when matching API conventions (`'filter'`, `'sort'`)
181
+
182
+ ---
183
+
184
+ ## Do / don’t
185
+
186
+ **Do**
187
+
188
+ - Use `ufetch` / `useUFetch` for app API traffic
189
+ - Keep `ufetch(url, {` on one line; wrap `useUFetch` with URL on the next line
190
+ - Refresh lists before clearing local form state after mutations
191
+
192
+ **Don’t**
193
+
194
+ - Reach for raw `$fetch` / `useFetch` for the same app API
195
+ - Manually prepend `baseApiUrl`
196
+ - Break `useUFetch` destructuring into ad-hoc formatting that hides the URL argument
@@ -27,7 +27,7 @@ Default action row:
27
27
 
28
28
  1. Submit (merged `submitButton`, label default `$t('common.submit')`)
29
29
  2. Spacer
30
- 3. Cancel (ghost, `$t('common.cancel')`)
30
+ 3. Cancel (`variant: 'ghost'` only here — `$t('common.cancel')`)
31
31
 
32
32
  `handleSubmit`:
33
33
 
@@ -153,6 +153,14 @@ Avoid setting `value` on custom buttons unless the await result must distinguish
153
153
 
154
154
  Action entries support `actionType: 'spacer'` (flex grow) and optional `tooltip`. Buttons use `loading-auto`.
155
155
 
156
+ Cancel / dismiss actions use `variant: 'ghost'` (or `variant="ghost"`). Do **not** use `ghost` on primary/submit/other actions — prefer omit / `subtle`.
157
+
158
+ Hand-rolled dialog shells keep **`u-modal` attributes on one line** (see [code-style.md](code-style.md)):
159
+
160
+ ```vue
161
+ <u-modal :ui="{ content: 'max-w-5xl' }" scrollable @update:open="!$event && emit('close')">
162
+ ```
163
+
156
164
  ## Do / don’t
157
165
 
158
166
  **Do**
@@ -160,9 +168,12 @@ Action entries support `actionType: 'spacer'` (flex grow) and optional `tooltip`
160
168
  - Prefer `launchFormPickerDialog` / `launchChoicePickerDialog` over hand-rolled `u-modal` for these flows
161
169
  - Handle actions in button / submit `onClick`
162
170
  - Keep field lists consistent with `un-form` (`identifier`, not `type`, for element kind)
171
+ - Mark Cancel with `variant: 'ghost'`; keep `u-modal` attrs on one line
163
172
 
164
173
  **Don’t**
165
174
 
166
175
  - Call `toast*` without `u-app` / toaster setup
167
176
  - Set `value` on choice buttons by default — omit it unless needed
168
177
  - Put primary dialog logic only after `await` when `onClick` should own it
178
+ - Use `ghost` on non-Cancel buttons
179
+ - Wrap `u-modal` attributes across multiple lines
@@ -0,0 +1,151 @@
1
+ # Pages and routing
2
+
3
+ Page shape conventions for Nuxt apps using this stack. Always combine with [code-style.md](code-style.md).
4
+
5
+ ## Page script skeleton
6
+
7
+ ```vue
8
+ <script setup>
9
+
10
+ /* page */
11
+
12
+ definePageMeta({
13
+ name: 'dashboard.patients.single',
14
+ });
15
+
16
+ useHead({
17
+ title: $t('patients.single.pageTitle'),
18
+ });
19
+
20
+
21
+ const route = useRoute();
22
+
23
+ const patientUid = computed(() => {
24
+ return route.params.patientUid;
25
+ });
26
+
27
+
28
+ /* data */
29
+
30
+ const { data: patientData, pending: isPatientLoading, refresh: refreshPatient } = useUFetch(
31
+ computed(() => `/api/patients/${patientUid.value}`),
32
+ );
33
+
34
+
35
+ /* handlers */
36
+
37
+ async function handleAction() {
38
+
39
+ ...
40
+
41
+ }
42
+
43
+ </script>
44
+
45
+
46
+ <template>
47
+ <div>
48
+
49
+ <h1 class="text-2xl font-semibold">
50
+ {{ $t('patients.single.title') }}
51
+ </h1>
52
+
53
+ <!-- content -->
54
+
55
+ </div>
56
+ </template>
57
+ ```
58
+
59
+ ## Script ordering (pages)
60
+
61
+ 1. `/* page */` — `definePageMeta`, then `useHead` (head may move below data when title depends on fetched data)
62
+ 2. Route / params (`useRoute`, reactive param computeds)
63
+ 3. Domain sections (`/* data */`, forms, etc.) including `useUFetch`
64
+ 4. Watchers / lifecycle
65
+ 5. Handlers (`handleXxx`)
66
+
67
+ ## `definePageMeta`
68
+
69
+ - **Always** set an explicit `name`
70
+ - Dot notation: `dashboard.home`, `flash-cards.single`, `authentication.login`
71
+ - `layout: 'empty'` for login / full-bleed auth-style pages only
72
+ - Never use `layout: false`
73
+
74
+ ```ts
75
+ definePageMeta({
76
+ name: 'authentication.login',
77
+ layout: 'empty',
78
+ });
79
+ ```
80
+
81
+ ## Dynamic routes
82
+
83
+ - Files: `[patientUid].vue`, `[flashCardSlug]/index.vue`
84
+ - Params: **camelCase** in brackets and when reading `route.params`
85
+ - Keep params **reactive** for async workflows — do not snapshot once into a bare `const`:
86
+
87
+ ```ts
88
+ const route = useRoute();
89
+
90
+ const flashCardSlug = computed(() => {
91
+ return route.params.flashCardSlug;
92
+ });
93
+ ```
94
+
95
+ ## Navigation
96
+
97
+ Always prefer **named routes**:
98
+
99
+ ```ts
100
+ await navigateTo({
101
+ name: 'authentication.account',
102
+ });
103
+ ```
104
+
105
+ ```vue
106
+ <nuxt-link
107
+ :to="{
108
+ name: 'flash-cards.single',
109
+ params: {
110
+ flashCardSlug,
111
+ },
112
+ }">
113
+ ...
114
+ </nuxt-link>
115
+ ```
116
+
117
+ ### Navigation in action objects
118
+
119
+ When an action only navigates, use `to` — not `onClick: () => navigateTo(...)`:
120
+
121
+ ```ts
122
+ {
123
+ icon: 'lucide:arrow-left',
124
+ label: 'Back',
125
+ to: {
126
+ name: 'orders.single',
127
+ params: {
128
+ orderUid,
129
+ },
130
+ },
131
+ }
132
+ ```
133
+
134
+ ## Page headings
135
+
136
+ - Primary title: `h1` with `class="text-2xl font-semibold"` (match local siblings if they consistently differ)
137
+ - Subtitle / secondary line: size-based hierarchy (`text-sm` / `text-xs`) — see text-hierarchy rules in [code-style.md](code-style.md)
138
+
139
+ ## Item / tab object property order
140
+
141
+ For objects in `:items` arrays (tabs, selects, menus):
142
+
143
+ `value` → `icon` → `label` → other props
144
+
145
+ ```ts
146
+ {
147
+ value: 'preview',
148
+ icon: 'lucide:eye',
149
+ label: $t('section.preview'),
150
+ }
151
+ ```
@@ -0,0 +1,285 @@
1
+ # Unified resources
2
+
3
+ How CRUD **resources** are declared and consumed in apps built on this stack. Use this whenever adding or customizing a resource end-to-end.
4
+
5
+ ## Mental model
6
+
7
+ A resource is one Mongo collection + typed schema + optional field meta, registered on a global `app` registry, exposed through a fixed REST file set, and managed in the dashboard via `<resource-manager>`.
8
+
9
+ | Layer | Role |
10
+ |-------|------|
11
+ | Nitro plugin (`*-resource.ts`) | `parseSchema` → augment `UnifiedAppRegistry` → `app.foo = { dbo: createUnifiedResourceController(...) }` |
12
+ | REST under `server/api/{kebab-plural}/` | Thin handlers calling `handleResource*` |
13
+ | Dashboard | Generic `[resourceName]` page, or a dedicated page wrapping `<resource-manager>` with extra actions |
14
+
15
+ Naming:
16
+
17
+ | Context | Shape | Example |
18
+ |---------|-------|---------|
19
+ | Registry / `dbo` / `handleResource*` `resource` | camelCase plural | `flashCards`, `authorizationRoles`, `users` |
20
+ | API folder + URL path | kebab-case plural | `flash-cards`, `authorization-roles`, `users` |
21
+ | Dashboard `resourceName` param | same as API path | `flash-cards` |
22
+ | Permissions (when used) | `admin.{kebab-plural}.{action}` | `admin.users.list`, `admin.flash-cards.create` |
23
+
24
+ Client helpers (`useResourceName`) derive:
25
+
26
+ - `resource` → Pascal singular (internal)
27
+ - `resourcePath` → kebab plural (API)
28
+ - `title` / `titlePlural` → display strings
29
+
30
+ Pass either camelCase or kebab-case into `<resource-manager resource="...">`; path/title conversion is handled for you. Prefer the name that matches the surrounding context (dashboard nav uses kebab; dedicated pages often pass camel like `users`).
31
+
32
+ ---
33
+
34
+ ## 1. Declare in a server plugin
35
+
36
+ File: `server/plugins/{name}-resource.ts`
37
+
38
+ Pattern:
39
+
40
+ 1. Two leading blank lines (no imports) **or** imports flush at line 1 — see [code-style.md](code-style.md).
41
+ 2. `parseSchema({ ... })` → `{ schema, type, inferred }`.
42
+ 3. `declare global { interface UnifiedAppRegistry { ... } }` so `app.resourceName` is typed.
43
+ 4. `defineNitroPlugin` assigns `app.resourceName = { dbo: createUnifiedResourceController({ resource, schema, type, meta? }) }`.
44
+
45
+ Minimal example:
46
+
47
+ ```ts
48
+
49
+
50
+ const { schema, type, inferred } = parseSchema({
51
+ 'name': 'string',
52
+ 'username': 'string',
53
+ });
54
+
55
+
56
+ declare global {
57
+ interface UnifiedAppRegistry {
58
+ users: {
59
+ dbo: UnifiedResourceController<typeof inferred>
60
+ };
61
+ }
62
+ };
63
+
64
+
65
+ export default defineNitroPlugin(() => {
66
+ app.users = {
67
+ dbo: createUnifiedResourceController({
68
+ resource: 'users',
69
+ schema,
70
+ type,
71
+ }),
72
+ };
73
+ });
74
+ ```
75
+
76
+ With relations and UI meta:
77
+
78
+ ```ts
79
+ createUnifiedResourceController({
80
+ resource: 'flashCards',
81
+ schema,
82
+ type,
83
+ meta: {
84
+ owner: {
85
+ resource: 'users',
86
+ },
87
+ category: {
88
+ resource: 'flashCardCategories',
89
+ },
90
+ description: {
91
+ hideInTable: true,
92
+ },
93
+ cards: {
94
+ children: {
95
+ backText: {
96
+ longText: true,
97
+ },
98
+ },
99
+ },
100
+ },
101
+ });
102
+ ```
103
+
104
+ Extra non-dbo fields on the registry entry are allowed (e.g. `app.media.directory` alongside `dbo`).
105
+
106
+ ### Schema DSL (`parseSchema`)
107
+
108
+ Keys are field names; optional fields use a trailing `?` in the key (`'tags?'`, `'permissions?'`).
109
+
110
+ Common value shapes:
111
+
112
+ | Value | Meaning |
113
+ |-------|---------|
114
+ | `'string'`, `'number'`, `'boolean'` | Scalars |
115
+ | `'string[]'` | String array (tags UI) |
116
+ | `'Record<string, string>'` | String map |
117
+ | `[{ ...fields }, '[]']` | Array of nested objects (series UI) |
118
+
119
+ `createUnifiedResourceController` validates create/update with the compiled ArkType `type`, auto-adds `_id` / `createdAt` / nested `_id`s, and uses `resource` as the Mongo collection name.
120
+
121
+ ### Field `meta` (drives schema API + forms + table)
122
+
123
+ | Meta | Effect |
124
+ |------|--------|
125
+ | `resource: 'otherResource'` | Relation: populate + form `identifier: 'resource'` (or `media` when target is `media`) |
126
+ | `hidden: true` | Omit from form fields |
127
+ | `hideInTable: true` | Omit from table columns (still in forms unless `hidden`) |
128
+ | `longText: true` | Textarea |
129
+ | `enum: [{ label, value }]` | Select |
130
+ | `labelFormat: '...'` | Date field |
131
+ | `width` | Form field width hint |
132
+ | `children: { ... }` | Nested meta for object-array item fields |
133
+
134
+ `GET .../schema` returns schema properties merged with this meta; the client maps that into form fields and columns via `useResourceMeta`.
135
+
136
+ ---
137
+
138
+ ## 2. Standard REST routes
139
+
140
+ For each resource, add the full set under `server/api/{kebab-plural}/`:
141
+
142
+ | File | Handler | Typical permission suffix |
143
+ |------|---------|---------------------------|
144
+ | `schema.get.ts` | `handleResourceSchema` | `.schema` |
145
+ | `index.get.ts` | `handleResourceList` | `.list` |
146
+ | `index.post.ts` | `handleResourceCreate` | `.create` |
147
+ | `count.get.ts` | `handleResourceCount` | `.count` |
148
+ | `[resourceId].get.ts` | `handleResourceRetrieve` | `.retrieve` |
149
+ | `[resourceId].patch.ts` | `handleResourceUpdate` | `.update` |
150
+ | `[resourceId].delete.ts` | `handleResourceDelete` | `.delete` |
151
+
152
+ Every file is a thin wrapper:
153
+
154
+ ```ts
155
+
156
+
157
+ export default defineEventHandler(async event => {
158
+ return handleResourceList({
159
+ resource: 'flashCards',
160
+ event,
161
+ permission: 'admin.flash-cards.list',
162
+ });
163
+ });
164
+ ```
165
+
166
+ Rules:
167
+
168
+ - `resource` is always the **camelCase registry key**, not the folder name.
169
+ - `permission` is optional; omit when the route is open to any caller that already passed other gates, or when auth is handled elsewhere. Prefer consistent `admin.{kebab}.{action}` when protecting admin CRUD.
170
+ - Do not reimplement list/create/update logic in route files — extend via custom endpoints next to the resource (e.g. `/api/authentication/onboard-user`) when the default CRUD is not enough.
171
+
172
+ List query helpers (handled inside `handleResourceList` / siblings): `filter`, `select`, `sort`, `skip`, `limit`, `populate`, and `single=xtruex` for a single `find`.
173
+
174
+ ---
175
+
176
+ ## 3. Dashboard: standard declaration
177
+
178
+ Generic page (one page for all standard resources):
179
+
180
+ ```vue
181
+ <!-- pages/dashboard/resources/[resourceName].vue -->
182
+ definePageMeta({
183
+ name: 'dashboard.resources.single',
184
+ layout: 'dashboard',
185
+ middleware: ['is-authenticated'],
186
+ });
187
+
188
+ // template
189
+ <resource-manager :resource="resourceName" />
190
+ ```
191
+
192
+ Nav links pass the **kebab API segment**:
193
+
194
+ ```ts
195
+ {
196
+ label: 'Flash Cards',
197
+ to: {
198
+ name: 'dashboard.resources.single',
199
+ params: {
200
+ resourceName: 'flash-cards',
201
+ },
202
+ },
203
+ },
204
+ ```
205
+
206
+ `<resource-manager>` provides Create / Refresh toolbar actions, table via `<resource-explorer-table>`, and per-row Edit / Delete dialogs wired to `/api/{resourcePath}`.
207
+
208
+ ---
209
+
210
+ ## 4. Dashboard: customize (users pattern)
211
+
212
+ When default Create/Edit is wrong for the domain, **do not** overload the generic `[resourceName]` page. Add a dedicated page that still uses `<resource-manager>` but supplies extra actions.
213
+
214
+ Example: `pages/resources/users.vue` (named route, not necessarily under `dashboard/resources/`):
215
+
216
+ ```vue
217
+ definePageMeta({
218
+ name: 'dashboard.resources.users',
219
+ layout: 'dashboard',
220
+ middleware: ['is-authenticated'],
221
+ });
222
+
223
+
224
+ const resourceManagerEl = useTemplateRef('resourceManager');
225
+
226
+
227
+ // custom handlers → domain APIs (onboard, reset-password, …)
228
+
229
+
230
+ <template>
231
+ <resource-manager
232
+ ref="resourceManager"
233
+ resource="users"
234
+ :actions="[
235
+ {
236
+ variant: 'subtle',
237
+ icon: 'lucide:user-plus',
238
+ label: 'Onboard User',
239
+ onClick: handleOnboardUser,
240
+ },
241
+ ]"
242
+ :resource-actions="[
243
+ {
244
+ icon: 'lucide:key-round',
245
+ tooltip: 'Reset Password',
246
+ onClick: handleResetPassword,
247
+ },
248
+ ]"
249
+ />
250
+ </template>
251
+ ```
252
+
253
+ Customization knobs on `<resource-manager>`:
254
+
255
+ | Prop | Placement | Merge order |
256
+ |------|-----------|-------------|
257
+ | `actions` | Card toolbar | After default **Create**, before **Refresh** |
258
+ | `resourceActions` | Per-row table actions | Before default **Edit** / **Delete** |
259
+ | `ref` → `refreshResources()` | After custom mutations | Call so the table reloads |
260
+
261
+ Point nav at the custom named route instead of `dashboard.resources.single`:
262
+
263
+ ```ts
264
+ to: {
265
+ name: 'dashboard.resources.users',
266
+ },
267
+ ```
268
+
269
+ Custom handlers typically:
270
+
271
+ 1. `launchFormPickerDialog` / `launchChoicePickerDialog` with explicit `fields`
272
+ 2. `ufetch` to a **domain** endpoint (not necessarily the generic resource CRUD)
273
+ 3. `await resourceManagerEl.value?.refreshResources()`
274
+ 4. `toastSuccess` / `toastError`
275
+
276
+ Keep the standard REST resource routes even when the UI is customized — other tools and the generic schema/list still depend on them.
277
+
278
+ ---
279
+
280
+ ## Checklist: new resource
281
+
282
+ - [ ] `server/plugins/{name}-resource.ts` — schema, registry, `createUnifiedResourceController`, meta for relations/UI
283
+ - [ ] Full `server/api/{kebab-plural}/` route set with camelCase `resource` (+ permissions if admin)
284
+ - [ ] Dashboard: nav → `dashboard.resources.single` + kebab `resourceName`, **or** dedicated page + `actions` / `resource-actions`
285
+ - [ ] Follow [code-style.md](code-style.md) (including file-start blank lines)