nuxt-unified-ui 0.5.17 → 0.5.18

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/README.md CHANGED
@@ -20,7 +20,7 @@ vpr serve
20
20
 
21
21
  ## Agent Skills
22
22
 
23
- One installable Agent Skill lives under `skills/nuxt-unified-ui/` (`npx skills` compatible). It covers the layer API **and** mandatory Nuxt code style (forms, dialogs, radashi, formatting). When an agent finishes implementing work, it runs `/nuxt-unified-ui apply`: one subagent per `.vue`, `.ts`, or `.js` file applies the skill's logical, structural, and code-style rules — to every file on `dev` / `main` / `master`, or to the current branch's changed files elsewhere. You can also invoke `/nuxt-unified-ui apply` directly.
23
+ One installable Agent Skill lives under `skills/nuxt-unified-ui/` (`npx skills` compatible). It covers the layer API **and** mandatory Nuxt code style (forms, dialogs, radashi, formatting). When an agent finishes implementing work, it runs `/nuxt-unified-ui apply`: one subagent per `.vue`, `.ts`, or `.js` file, launched using a cheap fast model, applies the skill's logical, structural, and code-style rules. On `dev`, `main`, or `master`, it targets uncommitted files and falls back to the whole project when the branch is clean; outside a Git worktree it also targets the whole project. On other branches, it targets files changed relative to the base branch. You can also invoke `/nuxt-unified-ui apply` directly.
24
24
 
25
25
  ```bash
26
26
  npx skills add . --list
@@ -163,6 +163,16 @@ function handleRowSelect(_event, row) {
163
163
  }
164
164
 
165
165
 
166
+ const rowSelectHandler = computed(() => {
167
+ if (!props.rowTo) {
168
+ return undefined;
169
+ }
170
+ else {
171
+ return handleRowSelect;
172
+ }
173
+ });
174
+
175
+
166
176
  /* pagination */
167
177
 
168
178
  const pageSizeItems = computed(() => {
@@ -194,7 +204,7 @@ const pageSizeItems = computed(() => {
194
204
  :column-pinning="columnPinning"
195
205
  :ui="tableUi"
196
206
  :meta="props.meta"
197
- @select="props.rowTo ? handleRowSelect : undefined">
207
+ :on-select="rowSelectHandler">
198
208
 
199
209
  <template v-for="(_, name) in $slots" #[name]="slotData">
200
210
  <slot
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nuxt-unified-ui",
3
3
  "type": "module",
4
- "version": "0.5.17",
4
+ "version": "0.5.18",
5
5
  "main": "./nuxt.config.ts",
6
6
  "types": "./index.d.ts",
7
7
  "exports": {
@@ -8,9 +8,10 @@ description: >-
8
8
  resource-manager, createUnifiedResourceController). Use when package.json
9
9
  depends on nuxt-unified-ui, nuxt.config extends it, or code uses these
10
10
  APIs. Whenever implementation work is finished, run `/nuxt-unified-ui
11
- apply`, which applies the skill file by file to the whole project (on dev,
12
- main, or master) or to the current branch's changed files; users can also
13
- invoke it directly.
11
+ apply`, which applies the skill file by file to uncommitted work (on dev,
12
+ main, or master) or to the current branch's changed files, falling back to
13
+ the whole project when there is no Git worktree or the default branch is
14
+ clean; users can also invoke it directly.
14
15
  ---
15
16
 
16
17
  # nuxt-unified-ui
@@ -21,7 +22,7 @@ The main agent decides what code exists, where it lives, and which APIs it calls
21
22
 
22
23
  Whenever you finish implementing work that added or edited a `.vue`, `.js`, or `.ts` file, run `/nuxt-unified-ui apply` before your final reply: follow [references/apply.md](references/apply.md). Do the same when the skill is invoked with the argument `apply`, or the user asks to apply nuxt-unified-ui across the project or the current branch. Invoked without an argument, the skill only loads its guidance for the task at hand.
23
24
 
24
- On `dev`, `main`, or `master` it processes every `.vue`, `.js`, and `.ts` file; on any other branch, only the files that branch added or changed relative to its base branch. One subagent per file applies the logical, structural, and code-style rules, then the main agent carries out the cross-file follow-ups.
25
+ On `dev`, `main`, or `master` it processes uncommitted `.vue`, `.js`, and `.ts` files; when that branch is clean, or the project is not a Git worktree, it processes every eligible file. On any other branch, it processes only the files that branch added or changed relative to its base branch. One subagent per file, launched using a cheap fast model, applies the logical, structural, and code-style rules, then the main agent carries out the cross-file follow-ups.
25
26
 
26
27
  ## APIs
27
28
 
@@ -116,7 +117,67 @@ Make these choices while implementing. The apply pipeline checks them again afte
116
117
 
117
118
  ## i18n
118
119
 
120
+ **i18n is optional. Conform to the project's current state.**
121
+
122
+ - If the project does not translate its own strings (literal text, no `$t`, no locale files of its own), keep writing literal strings. Do not introduce `$t`, locale files, or i18n config unless the user decides to integrate i18n.
123
+ - If the project already uses i18n in another layout, follow that layout.
124
+ - The rules below apply once the project has decided to integrate i18n, or already follows them. This layer's own `$t` usage does not count as a decision for the project.
125
+ - Examples in the references often use English literals for brevity. In an i18n project those strings are `$t('...')` keys.
126
+
127
+ **When i18n is integrated**
128
+
119
129
  - Every user-facing string goes through `$t`, in templates and in script. `$t` is available in both without calling `useI18n()`.
120
- - Add each new key to every locale file the project has (the layer ships `en.json` and `de.json` in `i18n/locales/`).
121
- - `un.*` keys belong to this layer. `common.*` holds shared labels such as submit and cancel. App keys go under a feature namespace (`patients.single.title`).
122
- - Examples in the references often use English literals for brevity. In real code those strings are `$t('...')` keys.
130
+ - Global i18n settings — `strategy`, `defaultLocale`, and the full `locales` list (codes, names, languages) — go in the `nuxt.config.ts` of the **aarde layer**: the project's base app layer that extends `nuxt-unified-ui` (for example `layers/100-aarde/`). If the project has no aarde layer, use the layer that extends `nuxt-unified-ui`.
131
+ - Every other layer (feature) keeps its own locale files in its own `i18n/locales/<code>.json`. In its own `nuxt.config.ts` it declares only those files — `i18n: { locales: [{ code, file }] }` — and no other i18n settings. `@nuxtjs/i18n` loads a layer's locale files only when that layer lists them, then merges the same locale from every layer into one message tree.
132
+ - The layer that owns a file is the nearest directory above it with a `nuxt.config`. Its strings go in that layer's locale files only — never in another layer's or the host's.
133
+ - Each layer's locale file has **exactly one top-level key**: the layer's namespace, such as `patients` or `billing`. Every key nests under it (`$t('patients.single.title')`), so merged trees never clash.
134
+ - Add every new key to each locale that layer declares.
135
+ - This layer owns two top-level keys: `un` for its components and `common` for shared labels (`common.submit`, `common.cancel`, `common.close`, …). Reuse `common.*` instead of duplicating those labels, but never add keys to `un` or `common` from another layer.
136
+
137
+ Aarde layer `nuxt.config.ts` (fragment):
138
+
139
+ ```ts
140
+ i18n: {
141
+ strategy: 'no_prefix',
142
+ defaultLocale: 'en',
143
+ locales: [
144
+ {
145
+ code: 'en',
146
+ name: 'English',
147
+ file: 'en.json',
148
+ },
149
+ {
150
+ code: 'de',
151
+ name: 'Deutsch',
152
+ file: 'de.json',
153
+ },
154
+ ],
155
+ },
156
+ ```
157
+
158
+ Feature layer `nuxt.config.ts` (fragment) and its `i18n/locales/en.json`:
159
+
160
+ ```ts
161
+ i18n: {
162
+ locales: [
163
+ {
164
+ code: 'en',
165
+ file: 'en.json',
166
+ },
167
+ {
168
+ code: 'de',
169
+ file: 'de.json',
170
+ },
171
+ ],
172
+ },
173
+ ```
174
+
175
+ ```json
176
+ {
177
+ "patients": {
178
+ "single": {
179
+ "title": "Patient"
180
+ }
181
+ }
182
+ }
183
+ ```
@@ -17,13 +17,36 @@ Do not edit files while subagents are running.
17
17
 
18
18
  Run from the repository root.
19
19
 
20
+ First check whether the project is a Git worktree:
21
+
22
+ ```bash
23
+ git rev-parse --is-inside-work-tree >/dev/null 2>&1
24
+ ```
25
+
26
+ If it is not, select every eligible file in the project:
27
+
28
+ ```bash
29
+ rg --files --hidden -g '*.vue' -g '*.js' -g '*.ts' -g '!*.d.ts'
30
+ ```
31
+
32
+ Otherwise, get the branch:
33
+
20
34
  ```bash
21
35
  branch=$(git branch --show-current)
22
36
  ```
23
37
 
24
38
  If `branch` is empty (detached HEAD), stop and ask the user which files to process.
25
39
 
26
- **On `dev`, `main`, or `master`** — every tracked or new, non-ignored file:
40
+ **On `dev`, `main`, or `master`** — select only added or changed, uncommitted files (staged, unstaged, or untracked):
41
+
42
+ ```bash
43
+ {
44
+ git diff --name-only --diff-filter=AMR HEAD -- '*.vue' '*.js' '*.ts' ':!*.d.ts'
45
+ git ls-files --others --exclude-standard -- '*.vue' '*.js' '*.ts' ':!*.d.ts'
46
+ } | sort -u
47
+ ```
48
+
49
+ If this selects no files because all work has been committed, select every tracked or new, non-ignored eligible file instead:
27
50
 
28
51
  ```bash
29
52
  git ls-files --cached --others --exclude-standard -- '*.vue' '*.js' '*.ts' ':!*.d.ts'
@@ -53,12 +76,14 @@ The first line is the base: `<commits> <ref> <merge-base sha>`. If there is no o
53
76
  } | sort -u
54
77
  ```
55
78
 
56
- Deleted files are excluded. Tell the user the branch, the base (when there is one), and how many files were selected. If none were selected, stop.
79
+ Deleted files are excluded. Tell the user whether the project is outside Git or on a branch, the base (when there is one), whether the clean default-branch fallback was used, and how many files were selected. If none were selected, stop.
57
80
 
58
81
  ### 2. Launch one subagent per file
59
82
 
60
83
  Launch exactly one subagent per selected file — never several files in one subagent. Run them in parallel batches. Each subagent edits only its own file, so parallel runs do not conflict.
61
84
 
85
+ **Model:** launch every per-file subagent using a cheap fast model. The main agent keeps its own model for steps 1, 3, and 5.
86
+
62
87
  Use this prompt:
63
88
 
64
89
  ```text
@@ -75,13 +100,13 @@ Return the report described there.
75
100
 
76
101
  When every subagent has reported, collect their follow-ups, remove duplicates, and apply them **one at a time** in the main agent:
77
102
 
78
- - **`i18n`** — add each key to every locale file the project has. Use the reported English text for English; translate for other locales when confident, otherwise use the English text and list those keys in the summary.
103
+ - **`i18n`** — add each key to the locale files of the layer that owns the reporting file, following the i18n rules in `SKILL.md` (that layer's `i18n/locales/`, under its single top-level key, in every locale it declares). When the layer has no locale files yet, create them and add its `i18n: { locales: [{ code, file }] }` declaration for the locales the aarde layer defines; global i18n settings stay in the aarde layer. Use the reported English text for English; translate for other locales when confident, otherwise use the English text and list those keys in the summary.
79
104
  - **`split`** / **`move`** / **`promote`** — apply the file-structure rules from `SKILL.md`: create or move the files, then update every caller and import.
80
105
  - **`other`** — apply cross-file changes that follow directly from the skill (for example a missing REST route of a resource). List anything that needs a product decision in the summary instead of guessing.
81
106
 
82
107
  ### 4. Second round for what step 3 touched
83
108
 
84
- Launch the same per-file subagent (step 2) for every `.vue`, `.js`, or `.ts` file created or edited in step 3, then apply their `i18n` follow-ups. Do not start a third round: list any other follow-ups from this round in the summary.
109
+ Launch the same per-file subagent (step 2, using a cheap fast model) for every `.vue`, `.js`, or `.ts` file created or edited in step 3, then apply their `i18n` follow-ups. Do not start a third round: list any other follow-ups from this round in the summary.
85
110
 
86
111
  ### 5. Verify and summarize
87
112
 
@@ -106,7 +131,7 @@ You bring one `.vue`, `.js`, or `.ts` file in line with the nuxt-unified-ui skil
106
131
  3. **Logical pass** — fix in place:
107
132
  - Replace hand-rolled code with the APIs in `SKILL.md`: raw `$fetch` / `useFetch` → `ufetch` / `useUFetch`; hand-built modals → dialog launchers; direct `radashi` imports → `radXxx`; and so on.
108
133
  - Apply the decisions in `SKILL.md` and the rules of the references you read: page `definePageMeta.name` and SEO, dialog logic in `onClick`, named routes, `to` for navigation-only actions, a dumb `un-table`, reactive fetch gates, component conventions.
109
- - Move user-facing literals to `$t('...')` keys. Record each new key as an `i18n` follow-up.
134
+ - Only when the project has integrated i18n (`SKILL.md` i18n rules): move user-facing literals to `$t('...')` keys under the owning layer's top-level key. Reuse existing keys, including this layer's `common.*` labels. Record each new key as an `i18n` follow-up with its full path. In a project without i18n, leave literal strings as they are.
110
135
  - Make `atoms` / `libs` imports relative.
111
136
  4. **Structural pass** — decide, do not execute:
112
137
  - Two or more independent responsibilities → `split` follow-up with each responsibility and its suggested path.
@@ -53,10 +53,10 @@ Shape, in this order:
53
53
  2. The line `/* responsibility */`
54
54
  3. One blank line
55
55
  4. The job, as short `//` comments. Use several short lines. Do not write one long line, and do not put the job in a second `/* */` block
56
- 5. One blank line
56
+ 5. **Two** blank lines
57
57
  6. The rest of the file
58
58
 
59
- That last blank line is the blank line the next section or the first statement already requires. Do not add a second one.
59
+ Those two blank lines are the whole gap between the header and what follows — whether that is a section comment, an import, or the first statement. Never one, never three.
60
60
 
61
61
  `.js` / `.ts`: this block is the start of the file. Nothing comes before the leading blank line. Imports, when the file has them, start after step 5.
62
62
 
@@ -69,6 +69,7 @@ That last blank line is the blank line the next section or the first statement a
69
69
  // Issues a session token
70
70
  // after checking the login payload.
71
71
 
72
+
72
73
  import { join } from 'node:path';
73
74
  ```
74
75
 
@@ -80,6 +81,7 @@ import { join } from 'node:path';
80
81
  // Renders the login form
81
82
  // and submits credentials.
82
83
 
84
+
83
85
  /* login */
84
86
  ```
85
87
 
@@ -96,6 +98,17 @@ import { join } from 'node:path';
96
98
  <script setup>
97
99
  ```
98
100
 
101
+ ```ts
102
+ // ❌ only one blank line under the // lines
103
+
104
+ /* responsibility */
105
+
106
+ // Issues a session token
107
+ // after checking the login payload.
108
+
109
+ import { join } from 'node:path';
110
+ ```
111
+
99
112
  The `//` lines name the job. They do not narrate steps, list options, or repeat the file name.
100
113
 
101
114
  ---
@@ -433,6 +446,24 @@ const form = {
433
446
  };
434
447
  ```
435
448
 
449
+ ### Parameter type literals
450
+
451
+ The multi-line rule covers object **values**, not types. An object type written inline as the type of a function parameter stays on **one line**, however many members it has:
452
+
453
+ ```ts
454
+ export async function launchDialog(options: { component: any, props: any }) {
455
+ ...
456
+ }
457
+
458
+ // ❌
459
+ export async function launchDialog(options: {
460
+ component: any,
461
+ props: any,
462
+ }) {
463
+ ...
464
+ }
465
+ ```
466
+
436
467
  ### Multiline assignment isolation
437
468
 
438
469
  A **multiline assignment** is any `const` / `let` / `var` / `export const` declaration, or any `=` reassignment, whose statement spans more than one line (usually a multi-line object, array, call, or destructure: `useUFetch`, `computed(() => { … })`, `defineProps({ … })`, …).
@@ -484,6 +515,7 @@ How this combines with other spacing:
484
515
  | Next to a single-line statement (including consecutive refs) | Isolation **wins**: one blank before and after |
485
516
  | First statement after a function `{` that already wants a blank, or last before a `}` that wants one | **Share** that blank — do not add a second |
486
517
  | Declaration-group or major-step boundary (already **two** blanks) | Keep the **two** |
518
+ | First statement right after the responsibility header | Keep the header's **two** |
487
519
  | Blank already required after `/* section */` | That blank **is** the before-blank |
488
520
  | Single-line assignment, including `{}` / `[]` | Not multiline — no isolation |
489
521
  | Tiny / single-block function whose only statement is the assignment | Stay **flush** with the braces |
@@ -584,7 +616,7 @@ Omit unused properties; keep the rest in this order:
584
616
 
585
617
  | Object | Order |
586
618
  |--------|-------|
587
- | Action objects (`:actions`, `:append-actions`, table row actions) | `vIf` → `actionType` → `variant` → `color` → `icon` → `label` → `tooltip` → `warning` → `disabled` → `to` → `href` → `onClick` → `items` |
619
+ | Action objects (`:actions`, `:append-actions`, table row actions) | `vIf` → `actionType` → `variant` → `color` → `icon` → `label` → `trailingIcon` → `tooltip` → `warning` → `disabled` → `to` → `href` → `onClick` → `items` |
588
620
  | Table column defs | `accessorKey` (or `id`) → `header` → others |
589
621
  | Tab / select / menu item objects | `value` → `icon` → `label` → others |
590
622
  | `ufetch` options | see [Fetch calls](#fetch-calls) |
@@ -681,7 +713,7 @@ A lone `v-if` with no `v-else` is one child and stays flush. Each tag looks only
681
713
  <u-button
682
714
  variant="subtle"
683
715
  icon="lucide:log-out"
684
- @click="handleLogout"
716
+ @click="handleLogout()"
685
717
  />
686
718
 
687
719
  </p>
@@ -723,7 +755,7 @@ A lone `v-if` with no `v-else` is one child and stays flush. Each tag looks only
723
755
  - **Childless, more than one attribute or any multiline attribute:** opening `<tag` on its own line, one attribute per line, `/>` on its own line.
724
756
  - **Tags with children:** all attributes stay on the **same line** as the opening tag, regardless of count or length. This applies to every tag, including `u-modal` and structural `<template>` wrappers.
725
757
  - **The only split trigger for tags with children is a multiline attribute**: an array, object, or function literal bound to an attribute that spans multiple lines. Then the opening `<tag` goes on its own line, every attribute gets its own line, and the multiline value is formatted like a JS literal.
726
- - References, calls, and scalar expressions (`:field="field"`, `v-bind="radOmit(action, [...])"`, `@click="handleSave"`) are **not** multiline attributes. A single-pair object with scalar values (`:ui="{ content: 'max-w-5xl' }"`) stays inline. Multi-key or nested object / array bindings are multiline attributes.
758
+ - References, calls, and scalar expressions (`:field="field"`, `v-bind="radOmit(action, [...])"`, `@click="handleSave()"`) are **not** multiline attributes. A single-pair object with scalar values (`:ui="{ content: 'max-w-5xl' }"`) stays inline. Multi-key or nested object / array bindings are multiline attributes.
727
759
 
728
760
  ```vue
729
761
  <!-- ✅ children, no multiline attribute — one line -->
@@ -744,13 +776,13 @@ A lone `v-if` with no `v-else` is one child and stays flush. Each tag looks only
744
776
  <u-button
745
777
  variant="subtle"
746
778
  icon="lucide:refresh-ccw"
747
- @click="refresh"
779
+ @click="refresh()"
748
780
  />
749
781
  ```
750
782
 
751
783
  ```vue
752
784
  <!-- ❌ childless multi-attribute tag on one line -->
753
- <u-button variant="subtle" icon="lucide:refresh-ccw" @click="refresh" />
785
+ <u-button variant="subtle" icon="lucide:refresh-ccw" @click="refresh()" />
754
786
 
755
787
  <!-- ❌ empty open/close pair -->
756
788
  <div class="grow"></div>
@@ -800,7 +832,8 @@ Applies whenever attributes are on separate lines:
800
832
 
801
833
  Shortcuts:
802
834
 
803
- - `u-button`: `variant` → `color` → `size` → `icon` → label/value → `block` → `disabled` → `loading-auto` → events
835
+ - `img`: `src` / `:src` is always the **first** attribute, before everything else (including `ref` and `id`); the rest follow the order above
836
+ - `u-button`: `variant` → `color` → `size` → `icon` → label/value → `trailing-icon` → `block` → `disabled` → `loading-auto` → events
804
837
  - `u-input` / `u-select*`: `:placeholder`, `:label` → `:loading`, `:disabled` → `:items` → `class` → `v-model` → events
805
838
  - `un-table`: `:columns` → `class` / `:ui` → `:loading` → `:data` → `hide-pagination` → `:total-items` → `:items-per-page-items` → `:row-to` → `v-model:items-per-page` → `v-model:current-page` → `sticky-actions` → `:actions` → `:extra-actions` → `:meta` (page size model always before page model)
806
839
 
@@ -809,13 +842,55 @@ Shortcuts:
809
842
  - Simple scalars and simple ternaries stay inline.
810
843
  - When one condition changes several attributes, labels / icons, or an object binding such as `to`, use sibling `<template v-if>` / `v-else-if` / `v-else` branches with explicit component variants instead of nested ternaries.
811
844
 
845
+ ### Event handlers
846
+
847
+ A function bound with `@event` is always called explicitly, with its arguments written out — never passed as a bare reference:
848
+
849
+ | The function receives | Write |
850
+ |-----------------------|-------|
851
+ | No argument | `@event="handleSave()"` |
852
+ | The event's one argument | `@event="handleSelect($event)"` |
853
+ | Several event arguments | `@event="(value, index) => handleMove(value, index)"` |
854
+
855
+ Name arrow parameters after what the event passes.
856
+
857
+ When converting a bare reference (`@click="handleSave"`), keep what Vue passed it: if the function declares parameters, pass the event's arguments explicitly (`$event` for one, an arrow for several); if it declares none, call it with `()`. If a component event's argument count is unclear, check its `emits` / docs; if still unclear, pass `$event`.
858
+
859
+ Inline statements that are not a single function call (`@update:page="currentPage = $event"`, `@update:open="!$event && emit('close')"`) stay as they are. This rule covers `@` bindings only — `onClick` in action objects and watcher callbacks still take references (see [Naming](#naming)).
860
+
861
+ ```vue
862
+ <!-- ✅ -->
863
+ <u-button
864
+ variant="subtle"
865
+ icon="lucide:save"
866
+ @click="handleSave()"
867
+ />
868
+
869
+ <u-select
870
+ :items="statusItems"
871
+ @update:model-value="handleStatusChange($event)"
872
+ />
873
+
874
+ <draggable-list
875
+ :items="items"
876
+ @move="(item, index) => handleMove(item, index)"
877
+ />
878
+
879
+ <!-- ❌ bare references -->
880
+ <u-button
881
+ variant="subtle"
882
+ icon="lucide:save"
883
+ @click="handleSave"
884
+ />
885
+ ```
886
+
812
887
  ### Default attribute values
813
888
 
814
889
  Omit props that restate a default:
815
890
 
816
891
  | Component / context | Convention |
817
892
  |---------------------|------------|
818
- | `un-table` `actions` / `extraActions` objects | omit `variant: 'subtle'` — `un-table` already sets it. Keep `variant` on every other button and action object; it is not a default there |
893
+ | `un-table` `actions` / `extraActions` objects | omit `variant: 'subtle'` — `un-table` already sets it. Do not add or remove `variant` on any other button or action object; which variant it uses is a design choice, not formatting |
819
894
  | `u-badge` | omit `color` for neutral (`undefined` in ternaries, never `color="neutral"`); no `size` |
820
895
  | `u-tooltip` | do not set `:delay-duration` |
821
896
 
@@ -869,6 +944,7 @@ Same whitespace, brace, literal, and call rules as script blocks, starting with
869
944
  // Creates an authentication token
870
945
  // after checking the login body.
871
946
 
947
+
872
948
  export default defineEventHandler(async event => {
873
949
 
874
950
  await assertRateLimit({
@@ -906,7 +982,7 @@ export default defineEventHandler(async event => {
906
982
 
907
983
  ## Checklist before finishing an edit
908
984
 
909
- - [ ] Responsibility header: blank line, `/* responsibility */`, blank line, short `//` lines, blank line; in Vue inside `<script setup>`; nothing above it
985
+ - [ ] Responsibility header: blank line, `/* responsibility */`, blank line, short `//` lines, two blank lines; in Vue inside `<script setup>`; nothing above it
910
986
  - [ ] `<script setup>` without `lang="ts"`; no TS annotations in Vue
911
987
  - [ ] 2-space indent; single quotes; semicolons; trailing commas in multi-line literals
912
988
  - [ ] Every domain section starts with `/* section name */` + blank line; sections separated by two blank lines; imports co-located
@@ -914,7 +990,7 @@ export default defineEventHandler(async event => {
914
990
  - [ ] Multiline assignments have exactly one blank line before and after (shared with required blanks; group boundaries keep two)
915
991
  - [ ] Workflow functions: blank after `{`, double blanks between major steps, blank before `}`; tiny helpers tight; single-block functions flush; return-only decisions as one `if` / `else if` / `else` chain
916
992
  - [ ] Braces everywhere; `else` / `catch` on their own line
917
- - [ ] Script literals multi-line, except `{}` / `[]`; no statement wrapped only for length; `fn(arg, {` on one line
993
+ - [ ] Script literals multi-line, except `{}` / `[]`; inline object types of function parameters on one line; no statement wrapped only for length; `fn(arg, {` on one line
918
994
  - [ ] Page scripts: section order, `/* params */` shape, `/* seo */` placement and call order
919
995
  - [ ] `useUFetch` shape; `ufetch` options order; destructure names `xxxData` / `isXxxPending` / `refreshXxx`; `response` for `ufetch` results
920
996
  - [ ] `handleXxx` handlers; `it` for short callbacks; descriptive loop names; structure-returning computeds use block + `return`; handler references, not wrappers
@@ -923,6 +999,7 @@ export default defineEventHandler(async event => {
923
999
  - [ ] Child spacing: one child flush; 2+ children with exactly one blank after the opener, between children, and before the closer (each branch counts)
924
1000
  - [ ] Attribute wrapping: tags with children single-line unless a multiline attribute forces a split; childless tags self-closing, split when more than one attribute; `>` / `/>` placement
925
1001
  - [ ] Attribute order; redundant defaults omitted
1002
+ - [ ] `@event` bindings call their function explicitly: `fn()`, `fn($event)`, or `(a, b) => fn(a, b)`; no bare references
926
1003
  - [ ] Text / `{{ }}` on its own line
927
1004
  - [ ] `atoms` / `libs` imports relative; file not moved
928
1005
  - [ ] No behavior change
@@ -22,7 +22,6 @@ await launchChoicePickerDialog({
22
22
  text: 'Are you sure you want to submit your application?',
23
23
  startButtons: [
24
24
  {
25
- variant: 'subtle',
26
25
  icon: 'lucide:check',
27
26
  label: 'Submit',
28
27
  onClick: async () => {
@@ -63,7 +62,6 @@ await launchFormPickerDialog({
63
62
  firstName: 'John',
64
63
  },
65
64
  submitButton: {
66
- variant: 'subtle',
67
65
  icon: 'lucide:send',
68
66
  label: 'Submit',
69
67
  onClick: async form => {
@@ -12,6 +12,7 @@ Schema-driven forms via `useForm` / `un-form`. Field type is selected with **`id
12
12
  // Collects the applicant's
13
13
  // personal details.
14
14
 
15
+
15
16
  /* form */
16
17
 
17
18
  const { form, formTag } = useForm({
@@ -178,6 +179,7 @@ A custom element is a layer-private component in `app/atoms/`. It receives the `
178
179
  // Renders a text input
179
180
  // for a custom form field.
180
181
 
182
+
181
183
  /* interface */
182
184
 
183
185
  const props = defineProps({
@@ -206,6 +208,7 @@ Register it once from a Nuxt plugin. The identifier must be unique among built-i
206
208
  // Registers the custom text
207
209
  // form element.
208
210
 
211
+
209
212
  export default defineNuxtPlugin(() => {
210
213
  registerFormExtraElement({
211
214
  identifier: 'custom-text',
@@ -21,6 +21,7 @@ Consumers must:
21
21
  // Configures the host app
22
22
  // on top of nuxt-unified-ui.
23
23
 
24
+
24
25
  import { pathRelativeToBase } from 'nuxt-unified-ui';
25
26
 
26
27
 
@@ -12,6 +12,7 @@ What every page in this stack contains and how it navigates.
12
12
  // Shows one patient
13
13
  // and the actions for that record.
14
14
 
15
+
15
16
  /* page */
16
17
 
17
18
  definePageMeta({
@@ -50,6 +50,7 @@ Minimal example:
50
50
  // Registers the users resource
51
51
  // on the unified app registry.
52
52
 
53
+
53
54
  const { schema, type, inferred } = parseSchema({
54
55
  'name': 'string',
55
56
  'username': 'string',
@@ -161,6 +162,7 @@ Every file is a thin wrapper:
161
162
  // Lists flash cards
162
163
  // for admins.
163
164
 
165
+
164
166
  export default defineEventHandler(async event => {
165
167
  return handleResourceList({
166
168
  resource: 'flashCards',
@@ -192,6 +194,7 @@ Generic page (one page for all standard resources), `pages/dashboard/resources/[
192
194
  // Manages any standard resource
193
195
  // named by the route.
194
196
 
197
+
195
198
  /* page */
196
199
 
197
200
  definePageMeta({
@@ -263,6 +266,7 @@ Example: `pages/resources/users.vue` (named route, not necessarily under `dashbo
263
266
  // Manages users with onboarding
264
267
  // and password reset actions.
265
268
 
269
+
266
270
  /* page */
267
271
 
268
272
  definePageMeta({
@@ -307,7 +311,6 @@ async function handleOnboardUser() {
307
311
  },
308
312
  ],
309
313
  submitButton: {
310
- variant: 'subtle',
311
314
  label: 'Onboard',
312
315
  onClick: async form => {
313
316
 
@@ -335,7 +338,6 @@ async function handleResetPassword(user) {
335
338
  text: `Send a password reset to ${user.name}?`,
336
339
  startButtons: [
337
340
  {
338
- variant: 'subtle',
339
341
  icon: 'lucide:check',
340
342
  label: 'Reset',
341
343
  onClick: async () => {
@@ -7,9 +7,10 @@ Toast helpers, the `un-*` layout components, and the button / badge / icon conve
7
7
  This is the only place these rules are stated.
8
8
 
9
9
  - **Icons** are always Lucide: `lucide:*`.
10
- - **Buttons** set `variant: 'subtle'` (`variant="subtle"`) explicitly: every `u-button` and every action object — `un-card` `actions` / `subtitleActions` / `appendActions`, dialog `submitButton` / `startButtons` / `endButtons`, `<resource-manager>` `actions`. Nuxt UI's default variant is `solid`, so leaving `variant` out is not the same.
11
- - **`un-table` row actions are the one exception**: `actions` / `extraActions` (and `<resource-manager>` `resource-actions`, which feed them) omit `variant`, because `un-table` already renders them subtle.
12
- - **Cancel / dismiss** actions use `variant: 'ghost'` instead. Never use `ghost` on primary, submit, row, or any other non-Cancel action.
10
+ - **Bottom-of-card action buttons use the default variant — set no `variant`**: `un-card` `actions` (also with `verticalActions`), form picker `submitButton`, and choice picker `startButtons` / `endButtons`.
11
+ - **All other buttons set `variant: 'subtle'` (`variant="subtle"`) explicitly**: top-of-card buttons — `un-card` `appendActions` / `subtitleActions`, `<resource-manager>` `actions` (rendered as its card's `appendActions`) — and standalone `u-button`s. Nuxt UI's default variant is `solid`, so leaving `variant` out is not the same.
12
+ - **`un-table` row actions** (`actions` / `extraActions`, and `<resource-manager>` `resource-actions`, which feed them) omit `variant`, because `un-table` already renders them subtle.
13
+ - **Cancel / dismiss** actions use `variant: 'ghost'`, including in a bottom action row. Never use `ghost` on primary, submit, row, or any other non-Cancel action.
13
14
  - **Async buttons** use `loading-auto` instead of a hand-rolled `isLoading` flag, unless something else depends on that flag.
14
15
  - **Badges** (`u-badge`) are always `variant="subtle"` with `icon` + `:label` (no default-slot text), no `size`, and no `color` for neutral states.
15
16