nuxt-unified-ui 0.5.1 → 0.5.3
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 +2 -2
- package/skills/nuxt-unified-ui/SKILL.md +19 -8
- package/skills/nuxt-unified-ui/references/code-style.md +186 -37
- package/skills/nuxt-unified-ui/references/dialogs-impl.md +1 -1
- package/skills/nuxt-unified-ui/references/dialogs.md +11 -0
- package/skills/nuxt-unified-ui/references/resources.md +285 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-unified-ui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.3",
|
|
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.
|
|
23
|
+
"@iconify-json/lucide": "1.2.124",
|
|
24
24
|
"@nuxt/kit": "4.5.2",
|
|
25
25
|
"@nuxt/ui": "4.10.0",
|
|
26
26
|
"@nuxtjs/i18n": "10.6.0",
|
|
@@ -5,10 +5,12 @@ description: >-
|
|
|
5
5
|
install/extend the layer, required CSS, modules/config, radashi radXxx
|
|
6
6
|
auto-imports, un-form / useForm, launchFormPickerDialog /
|
|
7
7
|
launchChoicePickerDialog, toast helpers, un-card / un-typography, pages /
|
|
8
|
-
definePageMeta, ufetch / useUFetch wrapping,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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.
|
|
12
14
|
---
|
|
13
15
|
|
|
14
16
|
# nuxt-unified-ui
|
|
@@ -23,6 +25,7 @@ This is the **only** installable skill in this repo. Deep topics live under `ref
|
|
|
23
25
|
|
|
24
26
|
- Installing / extending `nuxt-unified-ui` as a Nuxt layer
|
|
25
27
|
- Using `un-form`, dialogs, toasts, `un-card`, radashi `radXxx`, etc.
|
|
28
|
+
- Declaring or customizing **unified resources** (plugins, REST, dashboard)
|
|
26
29
|
- **Whenever generating or editing Nuxt/Vue/server code** that must follow the unified look (whitespace, wrapping, template shape, sectioning)
|
|
27
30
|
|
|
28
31
|
## References (read as needed)
|
|
@@ -30,6 +33,7 @@ This is the **only** installable skill in this repo. Deep topics live under `ref
|
|
|
30
33
|
| Topic | File |
|
|
31
34
|
|-------|------|
|
|
32
35
|
| **Code style (mandatory)** | [references/code-style.md](references/code-style.md) |
|
|
36
|
+
| Unified resources (plugins → REST → dashboard) | [references/resources.md](references/resources.md) |
|
|
33
37
|
| Pages / routing | [references/pages.md](references/pages.md) |
|
|
34
38
|
| Data fetching (`ufetch` / `useUFetch`) | [references/data-fetching.md](references/data-fetching.md) |
|
|
35
39
|
| Layer install + required CSS | [references/layer-setup.md](references/layer-setup.md) |
|
|
@@ -46,20 +50,24 @@ This is the **only** installable skill in this repo. Deep topics live under `ref
|
|
|
46
50
|
|
|
47
51
|
## Code style (read [code-style.md](references/code-style.md) before writing code)
|
|
48
52
|
|
|
49
|
-
**Always apply** to Vue SFCs and app/server `.ts` files. Higher-level idea: code should **scan vertically** —
|
|
53
|
+
**Always apply** to Vue SFCs and app/server `.ts` files. Higher-level idea: code should **scan vertically** — named sections, declaration-kind groups, multi-line literals, and predictable template wrapping.
|
|
50
54
|
|
|
51
55
|
Absolute highlights:
|
|
52
56
|
|
|
53
57
|
- `<script setup>` only — **never** `lang="ts"`; no TS annotations in Vue (runtime prop types)
|
|
54
58
|
- 2-space indent; single quotes; semicolons; trailing commas in multi-line literals
|
|
55
|
-
-
|
|
59
|
+
- **`.js` / `.ts` file start:** two leading blank lines, **except** when the file starts with imports — then **no** blank lines before the first `import`
|
|
60
|
+
- Every `<script setup>` section starts with `/* section name */`, then a blank line
|
|
61
|
+
- Within a section, group declarations by kind (imports, refs, computeds, watchers, functions, etc.): two blank lines between groups; no blanks between consecutive refs; one blank between consecutive members of other groups
|
|
56
62
|
- Non-trivial async/functions: blank line after `{`, double blank between major steps, blank before `}`
|
|
63
|
+
- A function dedicated to choosing a return value from multiple criteria uses one exhaustive `if` / `else if` / `else` chain; broader functions may use guard clauses and early returns
|
|
57
64
|
- `else` / `catch` on their own line after `}`
|
|
58
65
|
- Script object literals always multi-line (even one property)
|
|
59
66
|
- Kebab-case tags (`u-button`, `un-card`)
|
|
60
67
|
- `v-if` / `v-for` on `<template>` wrappers — not on rendered nodes
|
|
61
|
-
-
|
|
62
|
-
- `
|
|
68
|
+
- If a condition changes several component attributes, prefer explicit `<template v-if>` / `v-else` component variants over nested ternaries and overly dynamic bindings
|
|
69
|
+
- **2+ attributes → one per line** except structural **`template` wrappers** and **`u-modal`** (keep either 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
|
|
70
|
+
- `{{ ... }}` on its own line (static + dynamic text may mix)
|
|
63
71
|
- `/* section */` comments; imports co-located under the section that uses them
|
|
64
72
|
- Light naming: `handleXxx` handlers, `it` in short callbacks, descriptive `for...of`, computeds use block + `return`
|
|
65
73
|
- Pages: explicit `definePageMeta.name`, reactive route params, named `navigateTo` / `nuxt-link` → [pages.md](references/pages.md)
|
|
@@ -139,6 +147,7 @@ From `nuxt.config.ts`: `@vueuse/nuxt`, `@nuxt/ui`, `@nuxtjs/i18n`; `ui.colorMode
|
|
|
139
147
|
| Utilities | `radXxx` → [radashi.md](references/radashi.md) |
|
|
140
148
|
| New page / route | [pages.md](references/pages.md) |
|
|
141
149
|
| List/detail fetch or mutation | [data-fetching.md](references/data-fetching.md) |
|
|
150
|
+
| New / custom resource | [resources.md](references/resources.md) |
|
|
142
151
|
| Formatting any of the above | [code-style.md](references/code-style.md) |
|
|
143
152
|
|
|
144
153
|
## Do / don’t
|
|
@@ -150,6 +159,7 @@ From `nuxt.config.ts`: `@vueuse/nuxt`, `@nuxt/ui`, `@nuxtjs/i18n`; `ui.colorMode
|
|
|
150
159
|
- Use field `identifier` for element kind; `type` only for HTML input types
|
|
151
160
|
- Handle dialog actions in `onClick`
|
|
152
161
|
- Follow code style for every generated file
|
|
162
|
+
- Resources: plugin → full REST set → dashboard nav / custom `<resource-manager>` page → [resources.md](references/resources.md)
|
|
153
163
|
|
|
154
164
|
**Don’t**
|
|
155
165
|
|
|
@@ -158,3 +168,4 @@ From `nuxt.config.ts`: `@vueuse/nuxt`, `@nuxt/ui`, `@nuxtjs/i18n`; `ui.colorMode
|
|
|
158
168
|
- Use PascalCase component tags in templates
|
|
159
169
|
- Set choice-button `value` unless the await result must distinguish buttons
|
|
160
170
|
- Assume color mode is enabled (layer disables it)
|
|
171
|
+
- Reimplement resource CRUD in route files (use `handleResource*`; customize via dedicated pages + domain APIs)
|
|
@@ -10,20 +10,20 @@ This document is about the **look and shape** of code — whitespace, wrapping,
|
|
|
10
10
|
|
|
11
11
|
Write code so a reader can **scan vertically** and see structure before details.
|
|
12
12
|
|
|
13
|
-
1. **
|
|
14
|
-
|
|
13
|
+
1. **Sections contain declaration-kind groups.**
|
|
14
|
+
Start each logical `<script setup>` domain with a `/* section name */` comment and a blank line. Within that section, group imports, refs, computeds, watchers, functions, and other declarations by kind. Double blank lines separate groups; refs stay tightly stacked; consecutive members of other groups have one blank line between them.
|
|
15
15
|
|
|
16
|
-
2. **
|
|
17
|
-
A non-trivial function
|
|
16
|
+
2. **Function body spacing follows the work.**
|
|
17
|
+
A non-trivial workflow function is a mini-document: blank line after `{`, full-block guards, double blanks between major steps, and a blank before `}`. Small helpers and functions whose whole job is choosing a return value stay compact.
|
|
18
18
|
|
|
19
19
|
3. **One idea per line in structured data.**
|
|
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.
|
|
23
|
+
Structural directives live on one-line `<template>` wrappers so the rendered node stays a clean component/element. Rendered component 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
|
-
`/* section */` labels
|
|
26
|
+
`/* section */` labels define logical domains, while declaration-kind spacing exposes the structure inside each domain. Imports sit in the section that needs them, not in a hoisted pile at the top.
|
|
27
27
|
|
|
28
28
|
6. **Names that match role.**
|
|
29
29
|
Handlers read as actions (`handleLogin`), short callbacks use `it`, loops use real nouns. Shape and naming reinforce each other so you rarely need narrating comments.
|
|
@@ -48,20 +48,63 @@ 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
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
96
|
+
- Start every logical `<script setup>` domain with `/* section name */`, followed by a blank line.
|
|
97
|
+
- Within each section, group declarations by kind: imports, props/models, refs, computeds, watchers/lifecycle, functions, and outlets.
|
|
98
|
+
- Use **two blank lines between different declaration groups** within a section.
|
|
99
|
+
- Keep consecutive refs together with **no blank lines**.
|
|
100
|
+
- Use **one blank line between consecutive declarations** in other same-kind groups, including computeds, watchers, and functions.
|
|
101
|
+
- Keep a blank line before each section comment; the preceding group's double-boundary spacing still applies.
|
|
61
102
|
- Blank line before `</script>`.
|
|
62
103
|
- **Two** blank lines between `</script>` and `<template>`.
|
|
63
104
|
|
|
64
|
-
|
|
105
|
+
Declaration-group spacing is independent from spacing inside a function body. Do not add a new section comment merely because the declaration kind changes.
|
|
106
|
+
|
|
107
|
+
### Non-trivial workflow functions
|
|
65
108
|
|
|
66
109
|
Applies to async handlers, multi-step loaders, and non-trivial callbacks:
|
|
67
110
|
|
|
@@ -113,6 +156,26 @@ async function handleSubmitSelection(items) {
|
|
|
113
156
|
|
|
114
157
|
Same for a `finally` that only flips one flag.
|
|
115
158
|
|
|
159
|
+
### Return-only decision functions
|
|
160
|
+
|
|
161
|
+
When a function's whole job is to choose and return a value from multiple criteria, express the complete decision as one compact `if` / `else if` / `else` chain:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
function getSortLabel(column) {
|
|
165
|
+
if (sortedColumn.value !== column) {
|
|
166
|
+
return `Sort ${column} descending`;
|
|
167
|
+
}
|
|
168
|
+
else if (sortDirection.value === 'desc') {
|
|
169
|
+
return `Sort ${column} ascending`;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
return `Clear ${column} sorting`;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Use this pattern only when value selection is essentially the function's entire body. Functions that perform broader work may use guard clauses and early returns when those make the workflow clearer; do not force their logic into an exhaustive chain.
|
|
178
|
+
|
|
116
179
|
### `else` / `catch`
|
|
117
180
|
|
|
118
181
|
```ts
|
|
@@ -185,6 +248,33 @@ const response = await ufetch(
|
|
|
185
248
|
- Single-key object binding may stay inline: `:ui="{ content: 'max-w-7xl' }"`
|
|
186
249
|
- Multi-key template object/array bindings are multi-line
|
|
187
250
|
- Simple scalars and simple ternaries stay inline; break only when branches become objects/arrays or nested structure
|
|
251
|
+
- When one condition changes several attributes, labels/icons, or an object-shaped binding such as `to`, use adjacent `<template v-if>` / `v-else-if` / `v-else` branches with explicit component variants. Prefer small markup duplication over nested ternaries and overly dynamic attributes.
|
|
252
|
+
|
|
253
|
+
```vue
|
|
254
|
+
<template v-if="state === 'complete'">
|
|
255
|
+
<u-badge
|
|
256
|
+
variant="subtle"
|
|
257
|
+
color="success"
|
|
258
|
+
icon="lucide:circle-check"
|
|
259
|
+
label="Completed"
|
|
260
|
+
/>
|
|
261
|
+
</template>
|
|
262
|
+
<template v-else-if="state === 'in-progress'">
|
|
263
|
+
<u-badge
|
|
264
|
+
variant="subtle"
|
|
265
|
+
color="warning"
|
|
266
|
+
icon="lucide:clock"
|
|
267
|
+
label="In Progress"
|
|
268
|
+
/>
|
|
269
|
+
</template>
|
|
270
|
+
<template v-else>
|
|
271
|
+
<u-badge
|
|
272
|
+
variant="subtle"
|
|
273
|
+
icon="lucide:circle"
|
|
274
|
+
label="Not Started"
|
|
275
|
+
/>
|
|
276
|
+
</template>
|
|
277
|
+
```
|
|
188
278
|
|
|
189
279
|
---
|
|
190
280
|
|
|
@@ -224,18 +314,49 @@ Group with `/* name */`:
|
|
|
224
314
|
| domain names | `/* login */`, `/* resource */`, `/* captcha */`, … |
|
|
225
315
|
| `/* outlets */` | `defineExpose` |
|
|
226
316
|
|
|
227
|
-
|
|
317
|
+
The comment names a logical domain, not a declaration kind. Follow it with a blank line, then organize that domain into declaration-kind groups. Avoid comments that only restate obvious option names.
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
/* resource */
|
|
321
|
+
|
|
322
|
+
import ResourceExplorerCell from '~/atoms/resource-explorer-cell.vue';
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
const itemsPerPage = ref(20);
|
|
326
|
+
const currentPage = ref(1);
|
|
327
|
+
const sortedColumn = ref('createdAt');
|
|
328
|
+
const sortDirection = ref('desc');
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
const sort = computed(() => {
|
|
332
|
+
return `${sortedColumn.value}:${sortDirection.value}`;
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const hasResources = computed(() => {
|
|
336
|
+
return !!resourcesData.value?.length;
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
watchImmediate(resourcePath, refreshResources);
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
function getSortIcon(column) {
|
|
344
|
+
return sortedColumn.value === column ? 'lucide:arrow-down' : 'lucide:arrow-up-down';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function refreshAll() {
|
|
348
|
+
refreshResources();
|
|
349
|
+
}
|
|
350
|
+
```
|
|
228
351
|
|
|
229
352
|
### Script ordering
|
|
230
353
|
|
|
231
354
|
**Components / dialogs**
|
|
232
355
|
|
|
233
|
-
1. `/* interface */`
|
|
234
|
-
2.
|
|
235
|
-
3.
|
|
236
|
-
4.
|
|
237
|
-
5. Handlers / async functions
|
|
238
|
-
6. `/* outlets */` / `defineExpose` if needed
|
|
356
|
+
1. `/* interface */` section
|
|
357
|
+
2. Domain sections in reading order
|
|
358
|
+
3. Within each section: imports, refs, computeds, watchers/lifecycle, functions
|
|
359
|
+
4. `/* outlets */` / `defineExpose` section if needed
|
|
239
360
|
|
|
240
361
|
**Pages**
|
|
241
362
|
|
|
@@ -300,7 +421,9 @@ Keep tight `v-if` / `v-else` chains adjacent (no blank line between matching bra
|
|
|
300
421
|
### Attribute wrapping (hard rule)
|
|
301
422
|
|
|
302
423
|
- **0–1 attributes:** may stay on one line with the tag
|
|
303
|
-
- **
|
|
424
|
+
- **Structural `<template>` wrappers:** keep the opening tag on one line, even with several directives, keys, or dynamic slot bindings
|
|
425
|
+
- **2+ attributes on rendered elements/components:** one attribute per line — **except `u-modal`**
|
|
426
|
+
- **`u-modal` only:** keep **all** attributes on the **same single line** as the tag (do not wrap), even when there are many
|
|
304
427
|
|
|
305
428
|
```vue
|
|
306
429
|
<!-- ✅ 0–1 attributes — inline OK -->
|
|
@@ -310,14 +433,17 @@ Keep tight `v-if` / `v-else` chains adjacent (no blank line between matching bra
|
|
|
310
433
|
</u-form-field>
|
|
311
434
|
<u-icon name="lucide:check" />
|
|
312
435
|
|
|
313
|
-
<!-- ✅
|
|
314
|
-
<
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
436
|
+
<!-- ✅ structural template wrappers stay on one line -->
|
|
437
|
+
<template v-for="column in columns" :key="column.accessorKey" #[column.accessorKey+'-cell']="{ row }">
|
|
438
|
+
...
|
|
439
|
+
</template>
|
|
440
|
+
|
|
441
|
+
<!-- ✅ u-modal — always one line (exception) -->
|
|
442
|
+
<u-modal :ui="{ content: 'max-w-5xl' }" scrollable @update:open="!$event && emit('close')">
|
|
318
443
|
...
|
|
319
444
|
</u-modal>
|
|
320
445
|
|
|
446
|
+
<!-- ✅ other components — 2+ attributes, one per line -->
|
|
321
447
|
<u-button
|
|
322
448
|
variant="subtle"
|
|
323
449
|
icon="lucide:refresh-ccw"
|
|
@@ -350,11 +476,12 @@ Prefer defaults by **omitting** props rather than restating them:
|
|
|
350
476
|
|
|
351
477
|
| Component / context | Default convention |
|
|
352
478
|
|---------------------|--------------------|
|
|
353
|
-
| `u-button` | Prefer `variant="subtle"`
|
|
479
|
+
| `u-button` | Prefer `variant="subtle"` (or omit) for normal actions |
|
|
480
|
+
| Cancel buttons only | Use `variant="ghost"` / `variant: 'ghost'` — **only** for Cancel dismiss actions; do not use `ghost` on other buttons |
|
|
354
481
|
| Async `u-button` clicks | Prefer `loading-auto` over hand-rolled `isLoading` when nothing else depends on that flag |
|
|
355
482
|
| `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"`) |
|
|
356
483
|
| `u-tooltip` | Do not set `:delay-duration` — use the default delay |
|
|
357
|
-
| Action objects where subtle is the local default | Omit `variant: 'subtle'` unless overriding |
|
|
484
|
+
| Action objects where subtle is the local default | Omit `variant: 'subtle'` unless overriding; Cancel actions set `variant: 'ghost'` |
|
|
358
485
|
| Icons | Always `lucide:*` prefix |
|
|
359
486
|
|
|
360
487
|
```vue
|
|
@@ -417,12 +544,24 @@ Closing tags for block components (`</un-card>`, `</u-modal>`, …) always on th
|
|
|
417
544
|
|
|
418
545
|
### Text interpolation
|
|
419
546
|
|
|
420
|
-
Put `{{ ... }}` on its own line:
|
|
547
|
+
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:
|
|
421
548
|
|
|
422
549
|
```vue
|
|
423
550
|
<h1 class="text-2xl font-semibold">
|
|
424
551
|
Login
|
|
425
552
|
</h1>
|
|
553
|
+
|
|
554
|
+
<!-- ✅ static + dynamic together -->
|
|
555
|
+
<h1 class="text-2xl font-semibold">
|
|
556
|
+
Welcome, {{ user.name }}!
|
|
557
|
+
</h1>
|
|
558
|
+
|
|
559
|
+
<h2 class="mt-1">
|
|
560
|
+
{{ flashCardData?.category?.name }} - by {{ flashCardData?.owner?.name }}
|
|
561
|
+
</h2>
|
|
562
|
+
|
|
563
|
+
<!-- ❌ glued to tags -->
|
|
564
|
+
<h1 class="text-2xl font-semibold">{{ user.name }}</h1>
|
|
426
565
|
```
|
|
427
566
|
|
|
428
567
|
### Root structure
|
|
@@ -521,9 +660,11 @@ Pass handler **references** into action objects / watchers when possible (`onCli
|
|
|
521
660
|
|
|
522
661
|
## Server / plain TS files
|
|
523
662
|
|
|
524
|
-
Same whitespace, brace, literal, and call-formatting rules as script blocks:
|
|
663
|
+
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:
|
|
525
664
|
|
|
526
665
|
```ts
|
|
666
|
+
|
|
667
|
+
|
|
527
668
|
export default defineEventHandler(async event => {
|
|
528
669
|
|
|
529
670
|
await assertRateLimit({
|
|
@@ -557,8 +698,6 @@ export default defineEventHandler(async event => {
|
|
|
557
698
|
});
|
|
558
699
|
```
|
|
559
700
|
|
|
560
|
-
Leading blank line at top of file is fine when the local tree uses it. Prefer `async event =>` style consistent with siblings.
|
|
561
|
-
|
|
562
701
|
---
|
|
563
702
|
|
|
564
703
|
## Anti-patterns (quick)
|
|
@@ -571,14 +710,20 @@ Leading blank line at top of file is fine when the local tree uses it. Prefer `a
|
|
|
571
710
|
| `if (!x) return;` | braced block |
|
|
572
711
|
| `} else {` | `}\nelse {` |
|
|
573
712
|
| `toastSuccess({ title: 'x' })` one-liner object | multi-line object + trailing comma |
|
|
574
|
-
| 3 attrs on one line | one attr per line |
|
|
713
|
+
| 3 attrs on one line (rendered element/component other than `u-modal`) | one attr per line |
|
|
714
|
+
| Multi-line `u-modal` attrs | keep `u-modal` attrs on one line |
|
|
575
715
|
| `>` on its own line after attrs | `>` after last attr |
|
|
576
|
-
| `{{ x }}` glued to tags | interpolation on its own line |
|
|
716
|
+
| `{{ x }}` glued to tags | interpolation on its own line (static + dynamic mix OK) |
|
|
717
|
+
| `ghost` on non-Cancel buttons | `ghost` only for Cancel |
|
|
577
718
|
| Hoisted import block | imports co-located under section |
|
|
578
719
|
| `computed(() => [ ... ])` | `computed(() => { return [ ... ]; })` |
|
|
720
|
+
| Early-return ladder in a return-only decision function | Compact exhaustive `if` / `else if` / `else` |
|
|
721
|
+
| Multiple nested ternaries across component attributes | Explicit component variants in `<template v-if>` / `v-else` branches |
|
|
579
722
|
| `color="neutral"` on badge | omit `color` / use `undefined` |
|
|
580
723
|
| `ufetch(\n url,\n {` | `ufetch(url, {` on one line |
|
|
581
724
|
| One-line `useUFetch(...)` | URL on next line; options multi-line |
|
|
725
|
+
| `.ts`/`.js` with no leading blanks (and no imports) | two blank lines at file start |
|
|
726
|
+
| Blank lines before first `import` | `import` on line 1 |
|
|
582
727
|
|
|
583
728
|
---
|
|
584
729
|
|
|
@@ -586,17 +731,21 @@ Leading blank line at top of file is fine when the local tree uses it. Prefer `a
|
|
|
586
731
|
|
|
587
732
|
- [ ] `<script setup>` without `lang="ts"`; no TS annotations in Vue
|
|
588
733
|
- [ ] 2-space indent; single quotes; semicolons; trailing commas in multi-line literals
|
|
589
|
-
- [ ]
|
|
590
|
-
- [ ]
|
|
734
|
+
- [ ] `.js`/`.ts`: two leading blank lines, or imports flush at line 1 (no blanks before first import)
|
|
735
|
+
- [ ] Every `<script setup>` section starts with `/* section name */`, followed by a blank line
|
|
736
|
+
- [ ] Declarations are grouped by kind inside each section: two blanks between groups; no blanks between refs; one blank between other same-kind declarations
|
|
737
|
+
- [ ] Non-trivial workflow functions: blank after `{`, double blanks between major steps, blank before `}`
|
|
591
738
|
- [ ] Tiny helpers stay tight
|
|
739
|
+
- [ ] Return-only multi-criteria functions use a compact exhaustive `if` / `else if` / `else`; broader functions may use early returns
|
|
592
740
|
- [ ] `else` / `catch` on new line
|
|
593
741
|
- [ ] Script objects multi-line; template single-key objects may be inline
|
|
594
742
|
- [ ] Kebab-case component tags
|
|
595
743
|
- [ ] `v-if` / `v-for` on `<template>` wrappers
|
|
596
|
-
- [ ]
|
|
597
|
-
- [ ]
|
|
598
|
-
- [ ] `
|
|
599
|
-
- [ ]
|
|
744
|
+
- [ ] Conditional states that change several attributes use explicit `<template v-if>` component variants, not nested ternaries
|
|
745
|
+
- [ ] Structural `<template>` wrappers stay on one line; rendered elements/components with 2+ attributes wrap one per line (**`u-modal` stays on one line**); closing `>` / `/>` placement is correct
|
|
746
|
+
- [ ] Attribute order + default-value omissions respected (`subtle`, **Cancel → `ghost` only**, no neutral color noise, `loading-auto`)
|
|
747
|
+
- [ ] `{{ }}` on own line (static + dynamic mix OK)
|
|
748
|
+
- [ ] Every section is named; imports are co-located with the section that uses them
|
|
600
749
|
- [ ] `handleXxx` for action handlers; `it` for short callbacks; descriptive loop names
|
|
601
750
|
- [ ] Computeds that return structures use block + `return`
|
|
602
751
|
- [ ] Pages: explicit `definePageMeta.name`, reactive route params, named navigation ([pages.md](pages.md))
|
|
@@ -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,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)
|