@live-change/frontend-template 0.9.199 → 0.9.201

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.
Files changed (44) hide show
  1. package/.claude/rules/live-change-backend-actions-views-triggers.md +62 -0
  2. package/.claude/rules/live-change-backend-event-sourcing.md +186 -0
  3. package/.claude/rules/live-change-backend-models-and-relations.md +72 -0
  4. package/.claude/rules/live-change-frontend-vue-primevue.md +26 -0
  5. package/.claude/settings.json +32 -0
  6. package/.claude/skills/create-skills-and-rules/SKILL.md +248 -0
  7. package/.claude/skills/live-change-backend-change-triggers/SKILL.md +186 -0
  8. package/.claude/skills/live-change-design-actions-views-triggers/SKILL.md +462 -0
  9. package/.claude/skills/live-change-design-models-relations/SKILL.md +230 -0
  10. package/.claude/skills/live-change-design-service/SKILL.md +133 -0
  11. package/.claude/skills/live-change-frontend-accessible-objects/SKILL.md +384 -0
  12. package/.claude/skills/live-change-frontend-accessible-objects.md +383 -0
  13. package/.claude/skills/live-change-frontend-action-buttons/SKILL.md +129 -0
  14. package/.claude/skills/live-change-frontend-action-form/SKILL.md +149 -0
  15. package/.claude/skills/live-change-frontend-analytics/SKILL.md +147 -0
  16. package/.claude/skills/live-change-frontend-command-forms/SKILL.md +216 -0
  17. package/.claude/skills/live-change-frontend-data-views/SKILL.md +183 -0
  18. package/.claude/skills/live-change-frontend-editor-form/SKILL.md +240 -0
  19. package/.claude/skills/live-change-frontend-locale-time/SKILL.md +172 -0
  20. package/.claude/skills/live-change-frontend-page-list-detail/SKILL.md +201 -0
  21. package/.claude/skills/live-change-frontend-range-list/SKILL.md +129 -0
  22. package/.claude/skills/live-change-frontend-ssr-setup/SKILL.md +119 -0
  23. package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +88 -0
  24. package/.cursor/rules/live-change-backend-event-sourcing.mdc +185 -0
  25. package/.cursor/rules/live-change-backend-models-and-relations.mdc +62 -0
  26. package/.cursor/skills/create-skills-and-rules.md +248 -0
  27. package/.cursor/skills/live-change-backend-change-triggers.md +186 -0
  28. package/.cursor/skills/live-change-design-actions-views-triggers.md +178 -79
  29. package/.cursor/skills/live-change-design-models-relations.md +112 -50
  30. package/.cursor/skills/live-change-design-service.md +1 -0
  31. package/.cursor/skills/live-change-frontend-accessible-objects.md +384 -0
  32. package/.cursor/skills/live-change-frontend-action-buttons.md +1 -0
  33. package/.cursor/skills/live-change-frontend-action-form.md +9 -3
  34. package/.cursor/skills/live-change-frontend-analytics.md +1 -0
  35. package/.cursor/skills/live-change-frontend-command-forms.md +1 -0
  36. package/.cursor/skills/live-change-frontend-data-views.md +1 -0
  37. package/.cursor/skills/live-change-frontend-editor-form.md +135 -72
  38. package/.cursor/skills/live-change-frontend-locale-time.md +1 -0
  39. package/.cursor/skills/live-change-frontend-page-list-detail.md +1 -0
  40. package/.cursor/skills/live-change-frontend-range-list.md +1 -0
  41. package/.cursor/skills/live-change-frontend-ssr-setup.md +1 -0
  42. package/front/src/router.js +2 -1
  43. package/opencode.json +10 -0
  44. package/package.json +52 -50
@@ -0,0 +1,240 @@
1
+ ---
2
+ name: live-change-frontend-editor-form
3
+ description: Build CRUD editing forms with editorData and AutoField components
4
+ ---
5
+
6
+ # Skill: live-change-frontend-editor-form (Claude Code)
7
+
8
+ Use this skill when you build **CRUD editing forms** with `editorData` and `AutoField` in a LiveChange frontend.
9
+
10
+ ## When to use
11
+
12
+ - You need a create/edit form for a model record.
13
+ - You want draft auto-saving while the user types.
14
+ - You prefer individual `<AutoField>` components over `<AutoEditor>` for layout control.
15
+
16
+ **Not editing a model?** Use `actionData` instead (see `live-change-frontend-action-form` skill).
17
+ **No form fields, just a button?** Use `api.command` (see `live-change-frontend-command-forms` skill).
18
+
19
+ ## Step 1 – Set up editorData (recommended: `await`)
20
+
21
+ Use `await editorData(...)` directly in `<script setup>`, just like `live()`. Suspense handles the loading state.
22
+
23
+ ```javascript
24
+ import { editorData, AutoField, EditorButtons } from '@live-change/frontend-auto-form'
25
+ import { useToast } from 'primevue/usetoast'
26
+ import { useRouter, useRoute } from 'vue-router'
27
+
28
+ const toast = useToast()
29
+ const router = useRouter()
30
+ const route = useRoute()
31
+
32
+ const editor = await editorData({
33
+ service: 'blog',
34
+ model: 'Article',
35
+ identifiers: { article: route.params.article },
36
+ draft: true,
37
+ onSaved: () => {
38
+ toast.add({ severity: 'success', summary: 'Saved', life: 1500 })
39
+ },
40
+ onCreated: (result) => {
41
+ router.push({ name: 'article', params: { article: result } })
42
+ },
43
+ })
44
+ ```
45
+
46
+ For new records, pass empty identifiers:
47
+
48
+ ```javascript
49
+ identifiers: {} // creates a new record
50
+ ```
51
+
52
+ This is the simplest and most readable approach. Use it when identifiers are available at setup time (static values, route params).
53
+
54
+ ## Step 2 – Build the template with AutoField
55
+
56
+ Use `editor.model.properties.*` as definitions and `editor.data.value` for v-model bindings:
57
+
58
+ **Important:** Always wrap in a `<form>` element. `EditorButtons` uses `type="submit"` / `type="reset"` internally — without a parent `<form>`, the buttons do nothing.
59
+
60
+ ```vue
61
+ <template>
62
+ <form @submit.prevent="editor.save()" @reset.prevent="editor.reset()">
63
+ <div class="space-y-4">
64
+ <AutoField
65
+ :definition="editor.model.properties.title"
66
+ v-model="editor.data.value.title"
67
+ :error="editor.propertiesErrors?.title"
68
+ label="Title"
69
+ />
70
+ <AutoField
71
+ :definition="editor.model.properties.body"
72
+ v-model="editor.data.value.body"
73
+ :error="editor.propertiesErrors?.body"
74
+ label="Body"
75
+ />
76
+
77
+ <!-- Custom input inside AutoField — gets label + error automatically -->
78
+ <AutoField
79
+ :definition="editor.model.properties.category"
80
+ v-model="editor.data.value.category"
81
+ :error="editor.propertiesErrors?.category"
82
+ label="Category"
83
+ >
84
+ <Dropdown
85
+ v-model="editor.data.value.category"
86
+ :options="categories"
87
+ optionLabel="name"
88
+ optionValue="id"
89
+ class="w-full"
90
+ />
91
+ </AutoField>
92
+
93
+ <EditorButtons :editor="editor" :resetButton="true" />
94
+ </div>
95
+ </form>
96
+ </template>
97
+ ```
98
+
99
+ No `v-if="editor"` needed — with `await`, the editor is always available after the component loads (Suspense handles the loading state).
100
+
101
+ ## Step 3 – Use EditorButtons (alternative)
102
+
103
+ Instead of manual buttons, use the `EditorButtons` component:
104
+
105
+ ```vue
106
+ <template>
107
+ <form @submit.prevent="editor.save()" @reset.prevent="editor.reset()">
108
+ <!-- fields... -->
109
+ <EditorButtons :editor="editor" :resetButton="true" />
110
+ </form>
111
+ </template>
112
+
113
+ <script setup>
114
+ import { EditorButtons } from '@live-change/frontend-auto-form'
115
+ </script>
116
+ ```
117
+
118
+ `EditorButtons` automatically handles:
119
+ - "Saving draft..." spinner,
120
+ - "Draft changed" hint,
121
+ - validation error message,
122
+ - Save/Create button (disabled when nothing changed),
123
+ - optional Reset button.
124
+
125
+ ## Showing validation errors — 3 approaches
126
+
127
+ **Every field in an `editorData` form must show validation errors.** Never use bare `InputText`/`Dropdown` without error feedback.
128
+
129
+ ### Approach 1 — AutoField without slot (default, simplest)
130
+
131
+ AutoField auto-picks the right input component and renders label + error `Message` automatically:
132
+
133
+ ```vue
134
+ <AutoField :definition="editor.model.properties.name" v-model="editor.data.value.name"
135
+ :error="editor.propertiesErrors?.name" label="Name" />
136
+ ```
137
+
138
+ Use for standard fields where the default input is fine.
139
+
140
+ ### Approach 2 — AutoField with slot (custom input)
141
+
142
+ Wrap a custom input inside AutoField. AutoField still renders label and error:
143
+
144
+ ```vue
145
+ <AutoField :definition="editor.model.properties.depth" v-model="editor.data.value.depth"
146
+ :error="editor.propertiesErrors?.depth" label="Depth">
147
+ <Dropdown v-model="editor.data.value.depth" :options="depthOptions"
148
+ placeholder="Select depth" class="w-full" />
149
+ </AutoField>
150
+ ```
151
+
152
+ Use when you need a non-standard input (custom Dropdown formatting, Popover picker, InputNumber with buttons, etc.). AutoField slots: `#default({ validationResult, uid })`, `#label({ uid })`, `#error({ validationResult })`.
153
+
154
+ ### Approach 3 — Manual `Message` (no AutoField wrapper)
155
+
156
+ When the layout doesn't allow an AutoField wrapper (e.g. sub-fields of an Object property), add error feedback manually:
157
+
158
+ ```vue
159
+ <InputText v-model="editor.data.value.address.street" placeholder="Street" class="w-full" />
160
+ <Message v-if="editor.propertiesErrors?.address" severity="error"
161
+ variant="simple" size="small" class="mt-1">
162
+ {{ editor.propertiesErrors.address }}
163
+ </Message>
164
+ ```
165
+
166
+ Use as a last resort when there's no model property definition for the individual field.
167
+
168
+ ## Step 4 – Reactive identifiers with computedAsync (advanced)
169
+
170
+ When identifiers come from reactive sources that **may change during the component lifetime** (e.g. a prop that changes without page navigation), use `computedAsync`. Create an additional `computed` to alias the data for cleaner template access:
171
+
172
+ ```javascript
173
+ import { computed } from 'vue'
174
+ import { computedAsync } from '@vueuse/core'
175
+ import { editorData, AutoField, EditorButtons } from '@live-change/frontend-auto-form'
176
+
177
+ const editor = computedAsync(() =>
178
+ editorData({
179
+ service: 'blog',
180
+ model: 'Article',
181
+ identifiers: { article: props.articleId },
182
+ draft: true,
183
+ })
184
+ )
185
+
186
+ // Alias for cleaner template access
187
+ const editable = computed(() => editor.value?.value)
188
+ ```
189
+
190
+ Then in the template:
191
+
192
+ ```vue
193
+ <template>
194
+ <form v-if="editor" @submit.prevent="editor.save()" @reset.prevent="editor.reset()">
195
+ <AutoField
196
+ :definition="editor.model.properties.title"
197
+ v-model="editable.value.title"
198
+ :error="editor.propertiesErrors?.title"
199
+ label="Title"
200
+ />
201
+ <EditorButtons :editor="editor" />
202
+ </form>
203
+ </template>
204
+ ```
205
+
206
+ **When to use approach 1 vs approach 2:**
207
+ - Route params (`route.params.id`) → use `await` (approach 1). Route changes cause full page remount.
208
+ - Static identifiers (`{}` for new records) → use `await` (approach 1).
209
+ - Reactive prop that changes without remount → use `computedAsync` (approach 2).
210
+
211
+ ## Key options reference
212
+
213
+ | Option | Default | Description |
214
+ |---|---|---|
215
+ | `service` | required | Service name |
216
+ | `model` | required | Model name |
217
+ | `identifiers` | required | e.g. `{ article: id }` |
218
+ | `draft` | `true` | Auto-save draft while editing |
219
+ | `autoSave` | `false` | Auto-save directly (when `draft: false`) |
220
+ | `debounce` | `600` | Debounce delay in ms |
221
+ | `initialData` | `{}` | Default values for new records |
222
+ | `parameters` | `{}` | Extra params sent with every action |
223
+ | `onSaved` | – | Callback after save |
224
+ | `onCreated` | – | Callback after create (receives result) |
225
+
226
+ ## Key returned properties
227
+
228
+ | Property | Description |
229
+ |---|---|
230
+ | `data` | Editable form data (recommended), it is a ref, it should be used with value - editor.data.value |
231
+ | `value` | Editable form data (obsolete), it is a ref, it should be used with value - editor.value.value |
232
+ | `model` | Model definition (use `.properties.*` for `AutoField`) |
233
+ | `changed` | `true` when there are unsaved changes |
234
+ | `isNew` | `true` when creating (no existing record) |
235
+ | `save()` | Submit to backend |
236
+ | `reset()` | Discard draft, restore saved state |
237
+ | `propertiesErrors` | Server validation errors per property |
238
+ | `saving` | Save in progress |
239
+ | `draftChanged` | Draft auto-saved but not submitted |
240
+ | `savingDraft` | Draft auto-save in progress |
@@ -0,0 +1,172 @@
1
+ ---
2
+ name: live-change-frontend-locale-time
3
+ description: Handle locale, timezone, currentTime, time synchronization and email locale
4
+ ---
5
+
6
+ # Skill: live-change-frontend-locale-time (Claude Code)
7
+
8
+ Use this skill when you work with **locale, language, time, and timezone** in a LiveChange frontend.
9
+
10
+ ## When to use
11
+
12
+ - You need to display dates/times in the user's timezone.
13
+ - You are building a time-sensitive feature (countdown, deadline, scheduling).
14
+ - You need to sync locale with vue-i18n.
15
+ - You are writing an email template that must use the recipient's language.
16
+
17
+ ## Step 1 – Set up locale in App.vue
18
+
19
+ ```javascript
20
+ import { watch } from 'vue'
21
+ import { useI18n } from 'vue-i18n'
22
+ import { useLocale } from '@live-change/vue3-components'
23
+
24
+ const { locale: i18nLocale } = useI18n()
25
+ const locale = useLocale()
26
+
27
+ // Capture browser locale settings and save to backend
28
+ locale.captureLocale()
29
+
30
+ // Watch for locale changes and sync with vue-i18n
31
+ locale.getLocaleObservable()
32
+ watch(() => locale.localeRef.value, (newLocale) => {
33
+ if (newLocale?.language && i18nLocale.value !== newLocale.language) {
34
+ i18nLocale.value = newLocale.language
35
+ }
36
+ }, { immediate: true })
37
+ ```
38
+
39
+ ## Step 2 – Display dates in user timezone
40
+
41
+ Use vue-i18n's `d()` function for formatting and `locale.localTime()` for SSR timezone conversion:
42
+
43
+ ```vue
44
+ <template>
45
+ <span>{{ d(locale.localTime(new Date(article.createdAt)), 'long') }}</span>
46
+ </template>
47
+
48
+ <script setup>
49
+ import { useI18n } from 'vue-i18n'
50
+ import { useLocale } from '@live-change/vue3-components'
51
+
52
+ const { d } = useI18n()
53
+ const locale = useLocale()
54
+ </script>
55
+ ```
56
+
57
+ `locale.localTime(date)` converts server timestamps to the user's timezone during SSR. On the client (browser), it returns the date unchanged since the browser handles timezone natively.
58
+
59
+ ## Step 3 – Use `currentTime` for reactive clocks
60
+
61
+ `currentTime` is a global `Ref<number>` that ticks every 500ms:
62
+
63
+ ```vue
64
+ <template>
65
+ <span>{{ formattedTime }}</span>
66
+ </template>
67
+
68
+ <script setup>
69
+ import { computed } from 'vue'
70
+ import { currentTime } from '@live-change/frontend-base'
71
+ import { useI18n } from 'vue-i18n'
72
+
73
+ const { d } = useI18n()
74
+ const formattedTime = computed(() =>
75
+ isNaN(currentTime.value) ? '' : d(new Date(currentTime.value), 'time')
76
+ )
77
+ </script>
78
+ ```
79
+
80
+ Any component reading `currentTime` re-renders automatically every 500ms.
81
+
82
+ ## Step 4 – Correct clock skew with `useTimeSynchronization`
83
+
84
+ When the client clock differs from the server clock (important for real-time features like quizzes, countdowns, auctions):
85
+
86
+ ```javascript
87
+ import { useTimeSynchronization } from '@live-change/vue3-ssr'
88
+ import { currentTime } from '@live-change/frontend-base'
89
+
90
+ const timeSync = useTimeSynchronization()
91
+
92
+ // Convert client time to server time (reactive computed)
93
+ const serverTime = timeSync.localToServerComputed(currentTime)
94
+
95
+ // Countdown to a server-side deadline
96
+ const timeRemaining = computed(() => deadline.value - serverTime.value)
97
+ const seconds = computed(() => Math.max(0, Math.floor(timeRemaining.value / 1000) % 60))
98
+ const minutes = computed(() => Math.max(0, Math.floor(timeRemaining.value / 1000 / 60)))
99
+ ```
100
+
101
+ Enable time synchronization in `config.js`:
102
+
103
+ ```javascript
104
+ export default {
105
+ timeSynchronization: true,
106
+ // ...
107
+ }
108
+ ```
109
+
110
+ **When to use:** Only when clock skew matters – real-time events, countdown timers, time-limited actions. For simple date display, `currentTime` alone is sufficient.
111
+
112
+ ### Returned object from `useTimeSynchronization()`:
113
+
114
+ | Property | Description |
115
+ |---|---|
116
+ | `diff` | Server-client time offset in ms |
117
+ | `synchronized` | `true` once sync is complete |
118
+ | `serverToLocal(ts)` | Convert server timestamp to local |
119
+ | `localToServer(ts)` | Convert local timestamp to server |
120
+ | `serverToLocalComputed(ts)` | Reactive computed version |
121
+ | `localToServerComputed(ts)` | Reactive computed version |
122
+
123
+ ## Step 5 – Smooth countdown with requestAnimationFrame
124
+
125
+ For sub-500ms precision (e.g. animated countdown knobs):
126
+
127
+ ```javascript
128
+ import { ref } from 'vue'
129
+ import { useRafFn } from '@vueuse/core'
130
+ import { useTimeSynchronization } from '@live-change/vue3-ssr'
131
+
132
+ const rafNow = ref(Date.now())
133
+ useRafFn(() => { rafNow.value = Date.now() })
134
+
135
+ const timeSync = useTimeSynchronization()
136
+ const rafServerTime = timeSync.localToServerComputed(rafNow)
137
+
138
+ const countdown = computed(() =>
139
+ Math.max(0, deadline.value - rafServerTime.value)
140
+ )
141
+ ```
142
+
143
+ ## Step 6 – Locale in email templates
144
+
145
+ Email templates render server-side. They fetch the recipient's locale explicitly:
146
+
147
+ ```javascript
148
+ import { useI18n } from 'vue-i18n'
149
+ import { useLocale } from '@live-change/vue3-components'
150
+
151
+ const { locale: i18nLocale, t } = useI18n()
152
+ const locale = useLocale()
153
+
154
+ // props.json contains { user, client: { session } }
155
+ const data = JSON.parse(json)
156
+
157
+ // Fetch locale for the recipient (not current session)
158
+ await Promise.all([
159
+ locale.getOtherUserOrSessionLocale(data.user, data.client?.session)
160
+ ])
161
+
162
+ // Apply to vue-i18n
163
+ if (locale.getLanguage()) i18nLocale.value = locale.getLanguage()
164
+ ```
165
+
166
+ Key differences from regular pages:
167
+
168
+ | Aspect | Regular page | Email template |
169
+ |---|---|---|
170
+ | Locale source | `locale.getLocale()` (current user) | `locale.getOtherUserOrSessionLocale(user, session)` |
171
+ | Time conversion | `locale.localTime()` only on SSR | Always server-side |
172
+ | Reactive updates | Yes | No (one-shot SSR render) |
@@ -0,0 +1,201 @@
1
+ ---
2
+ name: live-change-frontend-page-list-detail
3
+ description: Build list and detail pages with live data, computed paths, .with() and useClient
4
+ ---
5
+
6
+ # Skill: live-change-frontend-page-list-detail (Claude Code)
7
+
8
+ Use this skill when you need to build a **list + detail** UI in Vue 3 / PrimeVue / Tailwind for a LiveChange backend.
9
+
10
+ ## When to use
11
+
12
+ - You are adding a new list page (devices, orders, etc.).
13
+ - You are adding a detail page for a single object.
14
+ - You want to follow the `live(path)` + `Promise.all` pattern compatible with SSR.
15
+
16
+ ## Step 1 – List page (`src/pages/<resource>/index.vue`)
17
+
18
+ 1. Create the file: `src/pages/<resource>/index.vue`.
19
+ 2. In the `<template>`, follow this layout:
20
+ - container with padding (`container mx-auto p-4`),
21
+ - header with title and “add” button,
22
+ - empty-state card,
23
+ - grid of cards or rows.
24
+
25
+ Example:
26
+
27
+ ```vue
28
+ <template>
29
+ <div class="container mx-auto p-4">
30
+ <div class="flex items-center justify-between mb-6">
31
+ <h1 class="text-2xl font-bold">Devices</h1>
32
+ <Button label="Add" icon="pi pi-plus" @click="openDialog" />
33
+ </div>
34
+
35
+ <Card v-if="devices.value?.length === 0">
36
+ <template #content>
37
+ <p class="text-center text-gray-500">
38
+ No devices yet
39
+ </p>
40
+ </template>
41
+ </Card>
42
+
43
+ <div class="grid gap-4">
44
+ <Card v-for="device in devices.value" :key="device.id">
45
+ <template #content>
46
+ <!-- device content -->
47
+ </template>
48
+ </Card>
49
+ </div>
50
+ </div>
51
+ </template>
52
+
53
+ <script setup>
54
+ import { path, live, api as useApi } from '@live-change/vue3-ssr'
55
+ import Button from 'primevue/button'
56
+ import Card from 'primevue/card'
57
+
58
+ const api = useApi()
59
+
60
+ const [devices] = await Promise.all([
61
+ live(path().deviceManager.myUserDevices({}))
62
+ ])
63
+
64
+ function openDialog() {
65
+ // open dialog for creating a new device
66
+ }
67
+ </script>
68
+
69
+ <route>
70
+ { "name": "devices", "meta": { "signedIn": true } }
71
+ </route>
72
+ ```
73
+
74
+ ## Step 2 – Detail page (`src/pages/<resource>/[id].vue`)
75
+
76
+ 1. Create `src/pages/<resource>/[id].vue`.
77
+ 2. In `script setup`:
78
+ - use `useRoute()` to get the id (`route.params.id`),
79
+ - fetch the main object and related data using `Promise.all`.
80
+
81
+ Example skeleton:
82
+
83
+ ```vue
84
+ <template>
85
+ <div class="container mx-auto p-4 space-y-4" v-if="item.value">
86
+ <Card>
87
+ <template #title>
88
+ {{ item.value.name }}
89
+ </template>
90
+ <template #content>
91
+ <!-- details -->
92
+ </template>
93
+ </Card>
94
+ </div>
95
+ </template>
96
+
97
+ <script setup>
98
+ import { path, live } from '@live-change/vue3-ssr'
99
+ import { useRoute } from 'vue-router'
100
+ import Card from 'primevue/card'
101
+
102
+ const route = useRoute()
103
+ const id = route.params.id
104
+
105
+ const [item] = await Promise.all([
106
+ live(path().myService.myUserItem({ item: id }))
107
+ ])
108
+ </script>
109
+
110
+ <route>
111
+ { "name": "myItem", "meta": { "signedIn": true } }
112
+ </route>
113
+ ```
114
+
115
+ ## Step 3 – Computed paths with reactive parameters
116
+
117
+ When the path depends on reactive values (route params, props), wrap it in `computed()`:
118
+
119
+ ```js
120
+ import { computed, unref } from 'vue'
121
+ import { usePath, live } from '@live-change/vue3-ssr'
122
+ import { useRoute } from 'vue-router'
123
+
124
+ const path = usePath()
125
+ const route = useRoute()
126
+ const deviceId = route.params.device
127
+
128
+ const devicePath = computed(() => path.deviceManager.myUserDevice({ device: unref(deviceId) }))
129
+ const connectionsPath = computed(() =>
130
+ path.deviceManager.deviceOwnedDeviceConnections({ device: unref(deviceId) })
131
+ )
132
+
133
+ const [device, connections] = await Promise.all([
134
+ live(devicePath),
135
+ live(connectionsPath),
136
+ ])
137
+ ```
138
+
139
+ ## Step 4 – Attach related objects with `.with()`
140
+
141
+ Use `.with()` to load related data alongside each item:
142
+
143
+ ```js
144
+ const devicesPath = computed(() =>
145
+ path.deviceManager.myUserDevices({})
146
+ .with(device => path.deviceManager.deviceState({ device: device.id }).bind('state'))
147
+ .with(device => path.userIdentification.identification({
148
+ sessionOrUserType: device.ownerType,
149
+ sessionOrUser: device.owner
150
+ }).bind('ownerProfile'))
151
+ )
152
+
153
+ const [devices] = await Promise.all([live(devicesPath)])
154
+ ```
155
+
156
+ Access in template: `device.state?.online`, `device.ownerProfile?.firstName`.
157
+
158
+ ## Step 5 – Auth guard with `useClient`
159
+
160
+ Use `useClient()` to conditionally show UI or load data based on authentication:
161
+
162
+ ```js
163
+ import { useClient } from '@live-change/vue3-ssr'
164
+
165
+ const client = useClient()
166
+
167
+ // Conditional path (null = no fetch)
168
+ const adminDataPath = computed(() =>
169
+ client.value.roles.includes('admin') && path.deviceManager.allDevices({})
170
+ )
171
+ const [adminData] = await Promise.all([live(adminDataPath)])
172
+ ```
173
+
174
+ Template:
175
+
176
+ ```vue
177
+ <Button v-if="client.roles.includes('admin')" label="Admin panel" />
178
+ <div v-if="!client.user">
179
+ <p>Please sign in</p>
180
+ <router-link :to="{ name: 'user:signIn' }">Sign in</router-link>
181
+ </div>
182
+ ```
183
+
184
+ ## Step 6 – Status tags and simple indicators
185
+
186
+ 1. Use PrimeVue `Tag` for status fields.
187
+ 2. Map statuses to severities (`success`, `secondary`, etc.).
188
+
189
+ Example:
190
+
191
+ ```vue
192
+ <Tag
193
+ :value="conn.status"
194
+ :severity="conn.status === 'online' ? 'success' : 'secondary'"
195
+ />
196
+ ```
197
+
198
+ ## Step 7 – Large lists with RangeViewer
199
+
200
+ For large or infinite-scroll lists, use `<RangeViewer>` instead of loading everything at once. See the `live-change-frontend-range-list` skill for details.
201
+