@live-change/frontend-template 0.9.197 → 0.9.199

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 (46) hide show
  1. package/.claude/rules/live-change-backend-actions-views-triggers.md +184 -0
  2. package/.claude/rules/live-change-backend-architecture.md +126 -0
  3. package/.claude/rules/live-change-backend-models-and-relations.md +188 -0
  4. package/.claude/rules/live-change-frontend-vue-primevue.md +291 -0
  5. package/.claude/rules/live-change-service-structure.md +89 -0
  6. package/.claude/skills/create-skills-and-rules.md +196 -0
  7. package/.claude/skills/live-change-design-actions-views-triggers.md +190 -0
  8. package/.claude/skills/live-change-design-models-relations.md +173 -0
  9. package/.claude/skills/live-change-design-service.md +132 -0
  10. package/.claude/skills/live-change-frontend-action-buttons.md +128 -0
  11. package/.claude/skills/live-change-frontend-action-form.md +143 -0
  12. package/.claude/skills/live-change-frontend-analytics.md +146 -0
  13. package/.claude/skills/live-change-frontend-command-forms.md +215 -0
  14. package/.claude/skills/live-change-frontend-data-views.md +182 -0
  15. package/.claude/skills/live-change-frontend-editor-form.md +177 -0
  16. package/.claude/skills/live-change-frontend-locale-time.md +171 -0
  17. package/.claude/skills/live-change-frontend-page-list-detail.md +200 -0
  18. package/.claude/skills/live-change-frontend-range-list.md +128 -0
  19. package/.claude/skills/live-change-frontend-ssr-setup.md +118 -0
  20. package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +202 -0
  21. package/.cursor/rules/live-change-backend-architecture.mdc +131 -0
  22. package/.cursor/rules/live-change-backend-models-and-relations.mdc +194 -0
  23. package/.cursor/rules/live-change-frontend-vue-primevue.mdc +290 -0
  24. package/.cursor/rules/live-change-service-structure.mdc +107 -0
  25. package/.cursor/skills/live-change-design-actions-views-triggers.md +197 -0
  26. package/.cursor/skills/live-change-design-models-relations.md +168 -0
  27. package/.cursor/skills/live-change-design-service.md +75 -0
  28. package/.cursor/skills/live-change-frontend-action-buttons.md +128 -0
  29. package/.cursor/skills/live-change-frontend-action-form.md +143 -0
  30. package/.cursor/skills/live-change-frontend-analytics.md +146 -0
  31. package/.cursor/skills/live-change-frontend-command-forms.md +215 -0
  32. package/.cursor/skills/live-change-frontend-data-views.md +182 -0
  33. package/.cursor/skills/live-change-frontend-editor-form.md +177 -0
  34. package/.cursor/skills/live-change-frontend-locale-time.md +171 -0
  35. package/.cursor/skills/live-change-frontend-page-list-detail.md +200 -0
  36. package/.cursor/skills/live-change-frontend-range-list.md +128 -0
  37. package/.cursor/skills/live-change-frontend-ssr-setup.md +119 -0
  38. package/README.md +71 -0
  39. package/package.json +50 -50
  40. package/server/app.config.js +35 -0
  41. package/server/services.list.js +2 -0
  42. package/.nx/workspace-data/file-map.json +0 -195
  43. package/.nx/workspace-data/nx_files.nxt +0 -0
  44. package/.nx/workspace-data/project-graph.json +0 -8
  45. package/.nx/workspace-data/project-graph.lock +0 -0
  46. package/.nx/workspace-data/source-maps.json +0 -1
@@ -0,0 +1,177 @@
1
+ ---
2
+ description: Build CRUD editing forms with editorData and AutoField components
3
+ ---
4
+
5
+ # Skill: live-change-frontend-editor-form (Claude Code)
6
+
7
+ Use this skill when you build **CRUD editing forms** with `editorData` and `AutoField` in a LiveChange frontend.
8
+
9
+ ## When to use
10
+
11
+ - You need a create/edit form for a model record.
12
+ - You want draft auto-saving while the user types.
13
+ - You prefer individual `<AutoField>` components over `<AutoEditor>` for layout control.
14
+
15
+ **Not editing a model?** Use `actionData` instead (see `live-change-frontend-action-form` skill).
16
+ **No form fields, just a button?** Use `api.command` (see `live-change-frontend-command-forms` skill).
17
+
18
+ ## Step 1 – Set up editorData
19
+
20
+ ```javascript
21
+ import { ref, getCurrentInstance } from 'vue'
22
+ import { AutoField, editorData } from '@live-change/frontend-auto-form'
23
+ import { useToast } from 'primevue/usetoast'
24
+ import { useRouter } from 'vue-router'
25
+
26
+ const toast = useToast()
27
+ const router = useRouter()
28
+ const appContext = getCurrentInstance().appContext
29
+
30
+ const editor = ref(null)
31
+
32
+ async function loadEditor() {
33
+ editor.value = await editorData({
34
+ service: 'blog',
35
+ model: 'Article',
36
+ identifiers: { article: props.articleId },
37
+ draft: true,
38
+ appContext,
39
+ toast,
40
+ onSaved: () => {
41
+ toast.add({ severity: 'success', summary: 'Saved', life: 1500 })
42
+ },
43
+ onCreated: (result) => {
44
+ router.push({ name: 'article', params: { article: result } })
45
+ },
46
+ })
47
+ }
48
+
49
+ loadEditor()
50
+ ```
51
+
52
+ For new records, pass an empty or missing identifier:
53
+
54
+ ```javascript
55
+ identifiers: { article: props.articleId } // props.articleId can be undefined for new
56
+ ```
57
+
58
+ ## Step 2 – Build the template with AutoField
59
+
60
+ Use `editor.model.properties.*` as definitions and `editor.value.*` as v-model:
61
+
62
+ ```vue
63
+ <template>
64
+ <div v-if="editor" class="space-y-4">
65
+ <AutoField
66
+ :definition="editor.model.properties.title"
67
+ v-model="editor.value.title"
68
+ :error="editor.propertiesErrors?.title"
69
+ label="Title"
70
+ />
71
+ <AutoField
72
+ :definition="editor.model.properties.body"
73
+ v-model="editor.value.body"
74
+ :error="editor.propertiesErrors?.body"
75
+ label="Body"
76
+ />
77
+
78
+ <!-- Manual field alongside AutoField -->
79
+ <div>
80
+ <label class="block text-sm font-medium mb-1">Category</label>
81
+ <Dropdown
82
+ v-model="editor.value.category"
83
+ :options="categories"
84
+ optionLabel="name"
85
+ optionValue="id"
86
+ class="w-full"
87
+ />
88
+ </div>
89
+
90
+ <!-- Buttons -->
91
+ <div class="flex gap-2 justify-end">
92
+ <Button v-if="!editor.isNew" @click="editor.save()"
93
+ label="Save" icon="pi pi-save"
94
+ :disabled="!editor.changed" />
95
+ <Button v-else @click="editor.save()"
96
+ label="Create" icon="pi pi-plus" severity="success"
97
+ :disabled="!editor.changed" />
98
+ <Button @click="editor.reset()"
99
+ label="Reset" icon="pi pi-eraser"
100
+ :disabled="!editor.changed" />
101
+ </div>
102
+ </div>
103
+ </template>
104
+ ```
105
+
106
+ ## Step 3 – Use EditorButtons (alternative)
107
+
108
+ Instead of manual buttons, use the `EditorButtons` component:
109
+
110
+ ```vue
111
+ <template>
112
+ <div v-if="editor">
113
+ <!-- fields... -->
114
+ <EditorButtons :editor="editor" :resetButton="true" />
115
+ </div>
116
+ </template>
117
+
118
+ <script setup>
119
+ import { EditorButtons } from '@live-change/frontend-auto-form'
120
+ </script>
121
+ ```
122
+
123
+ `EditorButtons` automatically handles:
124
+ - "Saving draft..." spinner,
125
+ - "Draft changed" hint,
126
+ - validation error message,
127
+ - Save/Create button (disabled when nothing changed),
128
+ - optional Reset button.
129
+
130
+ ## Step 4 – Reactive identifiers with computedAsync
131
+
132
+ When identifiers come from reactive sources (route params, props):
133
+
134
+ ```javascript
135
+ import { computedAsync } from '@vueuse/core'
136
+
137
+ const editor = computedAsync(() =>
138
+ editorData({
139
+ service: 'blog',
140
+ model: 'Article',
141
+ identifiers: { article: props.articleId },
142
+ draft: true,
143
+ appContext,
144
+ toast,
145
+ })
146
+ )
147
+ ```
148
+
149
+ ## Key options reference
150
+
151
+ | Option | Default | Description |
152
+ |---|---|---|
153
+ | `service` | required | Service name |
154
+ | `model` | required | Model name |
155
+ | `identifiers` | required | e.g. `{ article: id }` |
156
+ | `draft` | `true` | Auto-save draft while editing |
157
+ | `autoSave` | `false` | Auto-save directly (when `draft: false`) |
158
+ | `debounce` | `600` | Debounce delay in ms |
159
+ | `initialData` | `{}` | Default values for new records |
160
+ | `parameters` | `{}` | Extra params sent with every action |
161
+ | `onSaved` | – | Callback after save |
162
+ | `onCreated` | – | Callback after create (receives result) |
163
+
164
+ ## Key returned properties
165
+
166
+ | Property | Description |
167
+ |---|---|
168
+ | `value` | Editable data (use with `v-model`) |
169
+ | `model` | Model definition (use `.properties.*` for `AutoField`) |
170
+ | `changed` | `true` when there are unsaved changes |
171
+ | `isNew` | `true` when creating (no existing record) |
172
+ | `save()` | Submit to backend |
173
+ | `reset()` | Discard draft, restore saved state |
174
+ | `propertiesErrors` | Server validation errors per property |
175
+ | `saving` | Save in progress |
176
+ | `draftChanged` | Draft auto-saved but not submitted |
177
+ | `savingDraft` | Draft auto-save in progress |
@@ -0,0 +1,171 @@
1
+ ---
2
+ description: Handle locale, timezone, currentTime, time synchronization and email locale
3
+ ---
4
+
5
+ # Skill: live-change-frontend-locale-time (Claude Code)
6
+
7
+ Use this skill when you work with **locale, language, time, and timezone** in a LiveChange frontend.
8
+
9
+ ## When to use
10
+
11
+ - You need to display dates/times in the user's timezone.
12
+ - You are building a time-sensitive feature (countdown, deadline, scheduling).
13
+ - You need to sync locale with vue-i18n.
14
+ - You are writing an email template that must use the recipient's language.
15
+
16
+ ## Step 1 – Set up locale in App.vue
17
+
18
+ ```javascript
19
+ import { watch } from 'vue'
20
+ import { useI18n } from 'vue-i18n'
21
+ import { useLocale } from '@live-change/vue3-components'
22
+
23
+ const { locale: i18nLocale } = useI18n()
24
+ const locale = useLocale()
25
+
26
+ // Capture browser locale settings and save to backend
27
+ locale.captureLocale()
28
+
29
+ // Watch for locale changes and sync with vue-i18n
30
+ locale.getLocaleObservable()
31
+ watch(() => locale.localeRef.value, (newLocale) => {
32
+ if (newLocale?.language && i18nLocale.value !== newLocale.language) {
33
+ i18nLocale.value = newLocale.language
34
+ }
35
+ }, { immediate: true })
36
+ ```
37
+
38
+ ## Step 2 – Display dates in user timezone
39
+
40
+ Use vue-i18n's `d()` function for formatting and `locale.localTime()` for SSR timezone conversion:
41
+
42
+ ```vue
43
+ <template>
44
+ <span>{{ d(locale.localTime(new Date(article.createdAt)), 'long') }}</span>
45
+ </template>
46
+
47
+ <script setup>
48
+ import { useI18n } from 'vue-i18n'
49
+ import { useLocale } from '@live-change/vue3-components'
50
+
51
+ const { d } = useI18n()
52
+ const locale = useLocale()
53
+ </script>
54
+ ```
55
+
56
+ `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.
57
+
58
+ ## Step 3 – Use `currentTime` for reactive clocks
59
+
60
+ `currentTime` is a global `Ref<number>` that ticks every 500ms:
61
+
62
+ ```vue
63
+ <template>
64
+ <span>{{ formattedTime }}</span>
65
+ </template>
66
+
67
+ <script setup>
68
+ import { computed } from 'vue'
69
+ import { currentTime } from '@live-change/frontend-base'
70
+ import { useI18n } from 'vue-i18n'
71
+
72
+ const { d } = useI18n()
73
+ const formattedTime = computed(() =>
74
+ isNaN(currentTime.value) ? '' : d(new Date(currentTime.value), 'time')
75
+ )
76
+ </script>
77
+ ```
78
+
79
+ Any component reading `currentTime` re-renders automatically every 500ms.
80
+
81
+ ## Step 4 – Correct clock skew with `useTimeSynchronization`
82
+
83
+ When the client clock differs from the server clock (important for real-time features like quizzes, countdowns, auctions):
84
+
85
+ ```javascript
86
+ import { useTimeSynchronization } from '@live-change/vue3-ssr'
87
+ import { currentTime } from '@live-change/frontend-base'
88
+
89
+ const timeSync = useTimeSynchronization()
90
+
91
+ // Convert client time to server time (reactive computed)
92
+ const serverTime = timeSync.localToServerComputed(currentTime)
93
+
94
+ // Countdown to a server-side deadline
95
+ const timeRemaining = computed(() => deadline.value - serverTime.value)
96
+ const seconds = computed(() => Math.max(0, Math.floor(timeRemaining.value / 1000) % 60))
97
+ const minutes = computed(() => Math.max(0, Math.floor(timeRemaining.value / 1000 / 60)))
98
+ ```
99
+
100
+ Enable time synchronization in `config.js`:
101
+
102
+ ```javascript
103
+ export default {
104
+ timeSynchronization: true,
105
+ // ...
106
+ }
107
+ ```
108
+
109
+ **When to use:** Only when clock skew matters – real-time events, countdown timers, time-limited actions. For simple date display, `currentTime` alone is sufficient.
110
+
111
+ ### Returned object from `useTimeSynchronization()`:
112
+
113
+ | Property | Description |
114
+ |---|---|
115
+ | `diff` | Server-client time offset in ms |
116
+ | `synchronized` | `true` once sync is complete |
117
+ | `serverToLocal(ts)` | Convert server timestamp to local |
118
+ | `localToServer(ts)` | Convert local timestamp to server |
119
+ | `serverToLocalComputed(ts)` | Reactive computed version |
120
+ | `localToServerComputed(ts)` | Reactive computed version |
121
+
122
+ ## Step 5 – Smooth countdown with requestAnimationFrame
123
+
124
+ For sub-500ms precision (e.g. animated countdown knobs):
125
+
126
+ ```javascript
127
+ import { ref } from 'vue'
128
+ import { useRafFn } from '@vueuse/core'
129
+ import { useTimeSynchronization } from '@live-change/vue3-ssr'
130
+
131
+ const rafNow = ref(Date.now())
132
+ useRafFn(() => { rafNow.value = Date.now() })
133
+
134
+ const timeSync = useTimeSynchronization()
135
+ const rafServerTime = timeSync.localToServerComputed(rafNow)
136
+
137
+ const countdown = computed(() =>
138
+ Math.max(0, deadline.value - rafServerTime.value)
139
+ )
140
+ ```
141
+
142
+ ## Step 6 – Locale in email templates
143
+
144
+ Email templates render server-side. They fetch the recipient's locale explicitly:
145
+
146
+ ```javascript
147
+ import { useI18n } from 'vue-i18n'
148
+ import { useLocale } from '@live-change/vue3-components'
149
+
150
+ const { locale: i18nLocale, t } = useI18n()
151
+ const locale = useLocale()
152
+
153
+ // props.json contains { user, client: { session } }
154
+ const data = JSON.parse(json)
155
+
156
+ // Fetch locale for the recipient (not current session)
157
+ await Promise.all([
158
+ locale.getOtherUserOrSessionLocale(data.user, data.client?.session)
159
+ ])
160
+
161
+ // Apply to vue-i18n
162
+ if (locale.getLanguage()) i18nLocale.value = locale.getLanguage()
163
+ ```
164
+
165
+ Key differences from regular pages:
166
+
167
+ | Aspect | Regular page | Email template |
168
+ |---|---|---|
169
+ | Locale source | `locale.getLocale()` (current user) | `locale.getOtherUserOrSessionLocale(user, session)` |
170
+ | Time conversion | `locale.localTime()` only on SSR | Always server-side |
171
+ | Reactive updates | Yes | No (one-shot SSR render) |
@@ -0,0 +1,200 @@
1
+ ---
2
+ description: Build list and detail pages with live data, computed paths, .with() and useClient
3
+ ---
4
+
5
+ # Skill: live-change-frontend-page-list-detail (Claude Code)
6
+
7
+ Use this skill when you need to build a **list + detail** UI in Vue 3 / PrimeVue / Tailwind for a LiveChange backend.
8
+
9
+ ## When to use
10
+
11
+ - You are adding a new list page (devices, orders, etc.).
12
+ - You are adding a detail page for a single object.
13
+ - You want to follow the `live(path)` + `Promise.all` pattern compatible with SSR.
14
+
15
+ ## Step 1 – List page (`src/pages/<resource>/index.vue`)
16
+
17
+ 1. Create the file: `src/pages/<resource>/index.vue`.
18
+ 2. In the `<template>`, follow this layout:
19
+ - container with padding (`container mx-auto p-4`),
20
+ - header with title and “add” button,
21
+ - empty-state card,
22
+ - grid of cards or rows.
23
+
24
+ Example:
25
+
26
+ ```vue
27
+ <template>
28
+ <div class="container mx-auto p-4">
29
+ <div class="flex items-center justify-between mb-6">
30
+ <h1 class="text-2xl font-bold">Devices</h1>
31
+ <Button label="Add" icon="pi pi-plus" @click="openDialog" />
32
+ </div>
33
+
34
+ <Card v-if="devices.value?.length === 0">
35
+ <template #content>
36
+ <p class="text-center text-gray-500">
37
+ No devices yet
38
+ </p>
39
+ </template>
40
+ </Card>
41
+
42
+ <div class="grid gap-4">
43
+ <Card v-for="device in devices.value" :key="device.id">
44
+ <template #content>
45
+ <!-- device content -->
46
+ </template>
47
+ </Card>
48
+ </div>
49
+ </div>
50
+ </template>
51
+
52
+ <script setup>
53
+ import { path, live, api as useApi } from '@live-change/vue3-ssr'
54
+ import Button from 'primevue/button'
55
+ import Card from 'primevue/card'
56
+
57
+ const api = useApi()
58
+
59
+ const [devices] = await Promise.all([
60
+ live(path().deviceManager.myUserDevices({}))
61
+ ])
62
+
63
+ function openDialog() {
64
+ // open dialog for creating a new device
65
+ }
66
+ </script>
67
+
68
+ <route>
69
+ { "name": "devices", "meta": { "signedIn": true } }
70
+ </route>
71
+ ```
72
+
73
+ ## Step 2 – Detail page (`src/pages/<resource>/[id].vue`)
74
+
75
+ 1. Create `src/pages/<resource>/[id].vue`.
76
+ 2. In `script setup`:
77
+ - use `useRoute()` to get the id (`route.params.id`),
78
+ - fetch the main object and related data using `Promise.all`.
79
+
80
+ Example skeleton:
81
+
82
+ ```vue
83
+ <template>
84
+ <div class="container mx-auto p-4 space-y-4" v-if="item.value">
85
+ <Card>
86
+ <template #title>
87
+ {{ item.value.name }}
88
+ </template>
89
+ <template #content>
90
+ <!-- details -->
91
+ </template>
92
+ </Card>
93
+ </div>
94
+ </template>
95
+
96
+ <script setup>
97
+ import { path, live } from '@live-change/vue3-ssr'
98
+ import { useRoute } from 'vue-router'
99
+ import Card from 'primevue/card'
100
+
101
+ const route = useRoute()
102
+ const id = route.params.id
103
+
104
+ const [item] = await Promise.all([
105
+ live(path().myService.myUserItem({ item: id }))
106
+ ])
107
+ </script>
108
+
109
+ <route>
110
+ { "name": "myItem", "meta": { "signedIn": true } }
111
+ </route>
112
+ ```
113
+
114
+ ## Step 3 – Computed paths with reactive parameters
115
+
116
+ When the path depends on reactive values (route params, props), wrap it in `computed()`:
117
+
118
+ ```js
119
+ import { computed, unref } from 'vue'
120
+ import { usePath, live } from '@live-change/vue3-ssr'
121
+ import { useRoute } from 'vue-router'
122
+
123
+ const path = usePath()
124
+ const route = useRoute()
125
+ const deviceId = route.params.device
126
+
127
+ const devicePath = computed(() => path.deviceManager.myUserDevice({ device: unref(deviceId) }))
128
+ const connectionsPath = computed(() =>
129
+ path.deviceManager.deviceOwnedDeviceConnections({ device: unref(deviceId) })
130
+ )
131
+
132
+ const [device, connections] = await Promise.all([
133
+ live(devicePath),
134
+ live(connectionsPath),
135
+ ])
136
+ ```
137
+
138
+ ## Step 4 – Attach related objects with `.with()`
139
+
140
+ Use `.with()` to load related data alongside each item:
141
+
142
+ ```js
143
+ const devicesPath = computed(() =>
144
+ path.deviceManager.myUserDevices({})
145
+ .with(device => path.deviceManager.deviceState({ device: device.id }).bind('state'))
146
+ .with(device => path.userIdentification.identification({
147
+ sessionOrUserType: device.ownerType,
148
+ sessionOrUser: device.owner
149
+ }).bind('ownerProfile'))
150
+ )
151
+
152
+ const [devices] = await Promise.all([live(devicesPath)])
153
+ ```
154
+
155
+ Access in template: `device.state?.online`, `device.ownerProfile?.firstName`.
156
+
157
+ ## Step 5 – Auth guard with `useClient`
158
+
159
+ Use `useClient()` to conditionally show UI or load data based on authentication:
160
+
161
+ ```js
162
+ import { useClient } from '@live-change/vue3-ssr'
163
+
164
+ const client = useClient()
165
+
166
+ // Conditional path (null = no fetch)
167
+ const adminDataPath = computed(() =>
168
+ client.value.roles.includes('admin') && path.deviceManager.allDevices({})
169
+ )
170
+ const [adminData] = await Promise.all([live(adminDataPath)])
171
+ ```
172
+
173
+ Template:
174
+
175
+ ```vue
176
+ <Button v-if="client.roles.includes('admin')" label="Admin panel" />
177
+ <div v-if="!client.user">
178
+ <p>Please sign in</p>
179
+ <router-link :to="{ name: 'user:signIn' }">Sign in</router-link>
180
+ </div>
181
+ ```
182
+
183
+ ## Step 6 – Status tags and simple indicators
184
+
185
+ 1. Use PrimeVue `Tag` for status fields.
186
+ 2. Map statuses to severities (`success`, `secondary`, etc.).
187
+
188
+ Example:
189
+
190
+ ```vue
191
+ <Tag
192
+ :value="conn.status"
193
+ :severity="conn.status === 'online' ? 'success' : 'secondary'"
194
+ />
195
+ ```
196
+
197
+ ## Step 7 – Large lists with RangeViewer
198
+
199
+ For large or infinite-scroll lists, use `<RangeViewer>` instead of loading everything at once. See the `live-change-frontend-range-list` skill for details.
200
+
@@ -0,0 +1,128 @@
1
+ ---
2
+ description: Build paginated scrollable lists with RangeViewer, rangeBuckets and .with()
3
+ ---
4
+
5
+ # Skill: live-change-frontend-range-list (Claude Code)
6
+
7
+ Use this skill when you build **paginated, scrollable lists** backed by DAO ranges in a LiveChange frontend.
8
+
9
+ ## When to use
10
+
11
+ - You need a list that loads items in pages (infinite scroll).
12
+ - The list is backed by a DAO range view (e.g. `articlesByCreatedAt`).
13
+ - You want to attach related objects to each item via `.with()`.
14
+
15
+ ## Step 1 – Define the path function
16
+
17
+ The path function receives a `range` object (with `gt`, `gte`, `lt`, `lte`, `limit`, `reverse`) and returns a DAO path.
18
+
19
+ Use `reverseRange()` to display items newest-first:
20
+
21
+ ```javascript
22
+ import { reverseRange } from '@live-change/vue3-ssr'
23
+
24
+ function articlesPathRange(range) {
25
+ return path.blog.articlesByCreatedAt({ ...reverseRange(range) })
26
+ }
27
+ ```
28
+
29
+ ## Step 2 – Attach related objects with `.with()`
30
+
31
+ Chain `.with()` calls to load related data for each item:
32
+
33
+ ```javascript
34
+ function articlesPathRange(range) {
35
+ return path.blog.articlesByCreatedAt({ ...reverseRange(range) })
36
+ .with(article => path.userIdentification.identification({
37
+ sessionOrUserType: article.authorType,
38
+ sessionOrUser: article.author
39
+ }).bind('authorProfile'))
40
+ .with(article => path.blog.articleStats({ article: article.id }).bind('stats'))
41
+ }
42
+ ```
43
+
44
+ Each `.with()` call:
45
+ - receives a proxy of the item,
46
+ - builds a path to the related data,
47
+ - calls `.bind('fieldName')` to attach the result under that field name.
48
+
49
+ Nested `.with()` is also supported:
50
+
51
+ ```javascript
52
+ function eventsPathRange(range) {
53
+ return path.myService.allEvents({ ...reverseRange(range) })
54
+ .with(event => path.myService.eventState({ event: event.id }).bind('state')
55
+ .with(state => path.myService.roundPairs({ event: event.id, round: state.round }).bind('roundPairs'))
56
+ )
57
+ }
58
+ ```
59
+
60
+ ## Step 3 – Use `<RangeViewer>` in the template
61
+
62
+ ```vue
63
+ <template>
64
+ <RangeViewer
65
+ :pathFunction="articlesPathRange"
66
+ :canLoadTop="false"
67
+ canDropBottom
68
+ loadBottomSensorSize="3000px"
69
+ dropBottomSensorSize="5000px"
70
+ >
71
+ <template #empty>
72
+ <p class="text-center text-surface-500 my-4">No articles yet.</p>
73
+ </template>
74
+ <template #default="{ item: article }">
75
+ <Card class="mb-2">
76
+ <template #content>
77
+ <h3>{{ article.title }}</h3>
78
+ <p class="text-sm text-surface-500">By {{ article.authorProfile?.firstName }}</p>
79
+ </template>
80
+ </Card>
81
+ </template>
82
+ </RangeViewer>
83
+ </template>
84
+ ```
85
+
86
+ Key props:
87
+
88
+ | Prop | Default | Description |
89
+ |---|---|---|
90
+ | `pathFunction` | required | `(range) => path` – builds the DAO path for a given range |
91
+ | `bucketSize` | `20` | Items per page |
92
+ | `canLoadTop` / `canLoadBottom` | `true` | Whether loading in each direction is allowed |
93
+ | `canDropTop` / `canDropBottom` | `false` | Whether to drop pages that scrolled far out of view |
94
+ | `loadBottomSensorSize` | `'500px'` | How far before the bottom to trigger loading (increase for smoother UX) |
95
+ | `dropBottomSensorSize` | `'5000px'` | How far to keep before dropping |
96
+ | `frozen` | `false` | Pause live updates |
97
+
98
+ Slots:
99
+
100
+ | Slot | Props | Description |
101
+ |---|---|---|
102
+ | `default` | `{ item, bucket, itemIndex, bucketIndex }` | Render each item |
103
+ | `empty` | – | Shown when there are no items |
104
+ | `loadingTop` / `loadingBottom` | – | Loading spinners |
105
+ | `changedTop` / `changedBottom` | – | Indicators when data changed while frozen |
106
+
107
+ ## Step 4 – Low-level `rangeBuckets` (optional)
108
+
109
+ For advanced control, use `rangeBuckets` directly:
110
+
111
+ ```javascript
112
+ import { rangeBuckets, reverseRange } from '@live-change/vue3-ssr'
113
+
114
+ const { buckets, loadBottom, dropTop, freeze, unfreeze } = await rangeBuckets(
115
+ (range) => path.blog.articlesByCreatedAt({ ...reverseRange(range) }),
116
+ { bucketSize: 20 }
117
+ )
118
+ ```
119
+
120
+ Iterate in the template:
121
+
122
+ ```vue
123
+ <template v-for="(bucket, bi) in buckets.value" :key="bi">
124
+ <div v-for="(item, ii) in bucket.data.value" :key="item.id">
125
+ <!-- render item -->
126
+ </div>
127
+ </template>
128
+ ```