@live-change/frontend-template 0.9.199 → 0.9.200

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
@@ -1,4 +1,5 @@
1
1
  ---
2
+ name: live-change-frontend-editor-form
2
3
  description: Build CRUD editing forms with editorData and AutoField components
3
4
  ---
4
5
 
@@ -15,104 +16,98 @@ Use this skill when you build **CRUD editing forms** with `editorData` and `Auto
15
16
  **Not editing a model?** Use `actionData` instead (see `live-change-frontend-action-form` skill).
16
17
  **No form fields, just a button?** Use `api.command` (see `live-change-frontend-command-forms` skill).
17
18
 
18
- ## Step 1 – Set up editorData
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.
19
22
 
20
23
  ```javascript
21
- import { ref, getCurrentInstance } from 'vue'
22
- import { AutoField, editorData } from '@live-change/frontend-auto-form'
24
+ import { editorData, AutoField, EditorButtons } from '@live-change/frontend-auto-form'
23
25
  import { useToast } from 'primevue/usetoast'
24
- import { useRouter } from 'vue-router'
26
+ import { useRouter, useRoute } from 'vue-router'
25
27
 
26
28
  const toast = useToast()
27
29
  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()
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
+ })
50
44
  ```
51
45
 
52
- For new records, pass an empty or missing identifier:
46
+ For new records, pass empty identifiers:
53
47
 
54
48
  ```javascript
55
- identifiers: { article: props.articleId } // props.articleId can be undefined for new
49
+ identifiers: {} // creates a new record
56
50
  ```
57
51
 
52
+ This is the simplest and most readable approach. Use it when identifiers are available at setup time (static values, route params).
53
+
58
54
  ## Step 2 – Build the template with AutoField
59
55
 
60
- Use `editor.model.properties.*` as definitions and `editor.value.*` as v-model:
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.
61
59
 
62
60
  ```vue
63
61
  <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"
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"
87
75
  />
88
- </div>
89
76
 
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" />
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" />
101
94
  </div>
102
- </div>
95
+ </form>
103
96
  </template>
104
97
  ```
105
98
 
99
+ No `v-if="editor"` needed — with `await`, the editor is always available after the component loads (Suspense handles the loading state).
100
+
106
101
  ## Step 3 – Use EditorButtons (alternative)
107
102
 
108
103
  Instead of manual buttons, use the `EditorButtons` component:
109
104
 
110
105
  ```vue
111
106
  <template>
112
- <div v-if="editor">
107
+ <form @submit.prevent="editor.save()" @reset.prevent="editor.reset()">
113
108
  <!-- fields... -->
114
109
  <EditorButtons :editor="editor" :resetButton="true" />
115
- </div>
110
+ </form>
116
111
  </template>
117
112
 
118
113
  <script setup>
@@ -127,12 +122,57 @@ Instead of manual buttons, use the `EditorButtons` component:
127
122
  - Save/Create button (disabled when nothing changed),
128
123
  - optional Reset button.
129
124
 
130
- ## Step 4 Reactive identifiers with computedAsync
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
+ ```
131
137
 
132
- When identifiers come from reactive sources (route params, props):
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:
133
171
 
134
172
  ```javascript
173
+ import { computed } from 'vue'
135
174
  import { computedAsync } from '@vueuse/core'
175
+ import { editorData, AutoField, EditorButtons } from '@live-change/frontend-auto-form'
136
176
 
137
177
  const editor = computedAsync(() =>
138
178
  editorData({
@@ -140,12 +180,34 @@ const editor = computedAsync(() =>
140
180
  model: 'Article',
141
181
  identifiers: { article: props.articleId },
142
182
  draft: true,
143
- appContext,
144
- toast,
145
183
  })
146
184
  )
185
+
186
+ // Alias for cleaner template access
187
+ const editable = computed(() => editor.value?.value)
147
188
  ```
148
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
+
149
211
  ## Key options reference
150
212
 
151
213
  | Option | Default | Description |
@@ -165,7 +227,8 @@ const editor = computedAsync(() =>
165
227
 
166
228
  | Property | Description |
167
229
  |---|---|
168
- | `value` | Editable data (use with `v-model`) |
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 |
169
232
  | `model` | Model definition (use `.properties.*` for `AutoField`) |
170
233
  | `changed` | `true` when there are unsaved changes |
171
234
  | `isNew` | `true` when creating (no existing record) |
@@ -1,4 +1,5 @@
1
1
  ---
2
+ name: live-change-frontend-locale-time
2
3
  description: Handle locale, timezone, currentTime, time synchronization and email locale
3
4
  ---
4
5
 
@@ -1,4 +1,5 @@
1
1
  ---
2
+ name: live-change-frontend-page-list-detail
2
3
  description: Build list and detail pages with live data, computed paths, .with() and useClient
3
4
  ---
4
5
 
@@ -1,4 +1,5 @@
1
1
  ---
2
+ name: live-change-frontend-range-list
2
3
  description: Build paginated scrollable lists with RangeViewer, rangeBuckets and .with()
3
4
  ---
4
5
 
@@ -1,4 +1,5 @@
1
1
  ---
2
+ name: live-change-frontend-ssr-setup
2
3
  description: Set up SSR entry points, router, PrimeVue theme and Suspense data loading
3
4
  ---
4
5
 
@@ -10,7 +10,7 @@ import {
10
10
 
11
11
  import { dbAdminRoutes } from "@live-change/db-admin"
12
12
  import { autoFormRoutes } from "@live-change/frontend-auto-form"
13
- import { taskAdminRoutes } from "@live-change/task-frontend"
13
+ import { taskAdminRoutes, cronAdminRoutes } from "@live-change/task-frontend"
14
14
  import { userRoutes } from "@live-change/user-frontend"
15
15
  import { catchAllPagesRoute, contentEditRoutes, pagesSitemap } from "@live-change/content-frontend"
16
16
 
@@ -27,6 +27,7 @@ export function routes(config = {}) {
27
27
  ...contentEditRoutes({ ...config }),
28
28
 
29
29
  ...taskAdminRoutes({ ...config, prefix: '/_task' }),
30
+ ...cronAdminRoutes({ ...config, prefix: '/_cron' }),
30
31
  ...dbAdminRoutes({ prefix: '/_db', route: r => ({ ...r, meta: { ...r.meta, raw: true }}) }),
31
32
  ...catchAllPagesRoute({ ...config }),
32
33
  ]
package/opencode.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "instructions": [
4
+ ".claude/rules/live-change-backend-architecture.md",
5
+ ".claude/rules/live-change-backend-actions-views-triggers.md",
6
+ ".claude/rules/live-change-backend-models-and-relations.md",
7
+ ".claude/rules/live-change-service-structure.md",
8
+ ".claude/rules/live-change-frontend-vue-primevue.md"
9
+ ]
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/frontend-template",
3
- "version": "0.9.199",
3
+ "version": "0.9.200",
4
4
  "scripts": {
5
5
  "memDev": "tsx --inspect --expose-gc server/start.js memDev --enableSessions --initScript ./init.js --dbAccess",
6
6
  "localDevInit": "tsx server/start.js localDev --enableSessions --initScript ./init.js --dbAccess",
@@ -45,54 +45,56 @@
45
45
  "@codemirror/language": "6.10.1",
46
46
  "@dotenvx/dotenvx": "0.27.0",
47
47
  "@fortawesome/fontawesome-free": "^6.7.2",
48
- "@live-change/access-control-frontend": "^0.9.199",
49
- "@live-change/access-control-service": "^0.9.199",
50
- "@live-change/agreement-service": "^0.9.199",
51
- "@live-change/backup-service": "^0.9.199",
52
- "@live-change/blog-frontend": "^0.9.199",
53
- "@live-change/blog-service": "^0.9.199",
54
- "@live-change/cli": "^0.9.199",
55
- "@live-change/content-frontend": "^0.9.199",
56
- "@live-change/content-service": "^0.9.199",
57
- "@live-change/dao": "^0.9.199",
58
- "@live-change/dao-vue3": "^0.9.199",
59
- "@live-change/dao-websocket": "^0.9.199",
60
- "@live-change/db-client": "^0.9.199",
61
- "@live-change/draft-service": "^0.9.199",
62
- "@live-change/email-service": "^0.9.199",
63
- "@live-change/framework": "^0.9.199",
64
- "@live-change/frontend-auto-form": "^0.9.199",
65
- "@live-change/frontend-base": "^0.9.199",
66
- "@live-change/geoip-service": "^0.9.199",
67
- "@live-change/google-authentication-service": "^0.9.199",
68
- "@live-change/image-frontend": "^0.9.199",
69
- "@live-change/linkedin-authentication-service": "^0.9.199",
70
- "@live-change/locale-settings-service": "^0.9.199",
71
- "@live-change/notification-service": "^0.9.199",
72
- "@live-change/password-authentication-service": "^0.9.199",
73
- "@live-change/peer-connection-frontend": "^0.9.199",
74
- "@live-change/peer-connection-service": "^0.9.199",
75
- "@live-change/prosemirror-service": "^0.9.199",
76
- "@live-change/secret-code-service": "^0.9.199",
77
- "@live-change/secret-link-service": "^0.9.199",
78
- "@live-change/security-service": "^0.9.199",
79
- "@live-change/session-service": "^0.9.199",
80
- "@live-change/task-frontend": "^0.9.199",
81
- "@live-change/task-service": "^0.9.199",
82
- "@live-change/timer-service": "^0.9.199",
83
- "@live-change/upload-frontend": "^0.9.199",
84
- "@live-change/upload-service": "^0.9.199",
85
- "@live-change/url-frontend": "^0.9.199",
86
- "@live-change/url-service": "^0.9.199",
87
- "@live-change/user-frontend": "^0.9.199",
88
- "@live-change/user-identification-service": "^0.9.199",
89
- "@live-change/user-service": "^0.9.199",
90
- "@live-change/video-call-frontend": "^0.9.199",
91
- "@live-change/video-call-service": "^0.9.199",
92
- "@live-change/vote-service": "^0.9.199",
93
- "@live-change/vue3-components": "^0.9.199",
94
- "@live-change/vue3-ssr": "^0.9.199",
95
- "@live-change/wysiwyg-frontend": "^0.9.199",
48
+ "@live-change/access-control-frontend": "^0.9.200",
49
+ "@live-change/access-control-service": "^0.9.200",
50
+ "@live-change/agreement-service": "^0.9.200",
51
+ "@live-change/backup-service": "^0.9.200",
52
+ "@live-change/blog-frontend": "^0.9.200",
53
+ "@live-change/blog-service": "^0.9.200",
54
+ "@live-change/cli": "^0.9.200",
55
+ "@live-change/content-frontend": "^0.9.200",
56
+ "@live-change/content-service": "^0.9.200",
57
+ "@live-change/cron-service": "^0.9.200",
58
+ "@live-change/dao": "^0.9.200",
59
+ "@live-change/dao-vue3": "^0.9.200",
60
+ "@live-change/dao-websocket": "^0.9.200",
61
+ "@live-change/db-client": "^0.9.200",
62
+ "@live-change/draft-service": "^0.9.200",
63
+ "@live-change/email-service": "^0.9.200",
64
+ "@live-change/framework": "^0.9.200",
65
+ "@live-change/frontend-auto-form": "^0.9.200",
66
+ "@live-change/frontend-base": "^0.9.200",
67
+ "@live-change/geoip-service": "^0.9.200",
68
+ "@live-change/google-authentication-service": "^0.9.200",
69
+ "@live-change/image-frontend": "^0.9.200",
70
+ "@live-change/linkedin-authentication-service": "^0.9.200",
71
+ "@live-change/locale-settings-service": "^0.9.200",
72
+ "@live-change/notification-service": "^0.9.200",
73
+ "@live-change/password-authentication-service": "^0.9.200",
74
+ "@live-change/peer-connection-frontend": "^0.9.200",
75
+ "@live-change/peer-connection-service": "^0.9.200",
76
+ "@live-change/prosemirror-service": "^0.9.200",
77
+ "@live-change/scope-service": "^0.9.200",
78
+ "@live-change/secret-code-service": "^0.9.200",
79
+ "@live-change/secret-link-service": "^0.9.200",
80
+ "@live-change/security-service": "^0.9.200",
81
+ "@live-change/session-service": "^0.9.200",
82
+ "@live-change/task-frontend": "^0.9.200",
83
+ "@live-change/task-service": "^0.9.200",
84
+ "@live-change/timer-service": "^0.9.200",
85
+ "@live-change/upload-frontend": "^0.9.200",
86
+ "@live-change/upload-service": "^0.9.200",
87
+ "@live-change/url-frontend": "^0.9.200",
88
+ "@live-change/url-service": "^0.9.200",
89
+ "@live-change/user-frontend": "^0.9.200",
90
+ "@live-change/user-identification-service": "^0.9.200",
91
+ "@live-change/user-service": "^0.9.200",
92
+ "@live-change/video-call-frontend": "^0.9.200",
93
+ "@live-change/video-call-service": "^0.9.200",
94
+ "@live-change/vote-service": "^0.9.200",
95
+ "@live-change/vue3-components": "^0.9.200",
96
+ "@live-change/vue3-ssr": "^0.9.200",
97
+ "@live-change/wysiwyg-frontend": "^0.9.200",
96
98
  "@vueuse/core": "^12.3.0",
97
99
  "codeceptjs-assert": "^0.0.5",
98
100
  "compression": "^1.7.5",
@@ -125,5 +127,5 @@
125
127
  "author": "Michał Łaszczewski <michal@laszczewski.pl>",
126
128
  "license": "ISC",
127
129
  "description": "",
128
- "gitHead": "1900043a10cf9ad49b9cc33a539fb973706de962"
130
+ "gitHead": "a509834e600a546297faa7d1534b6f52e66d2e66"
129
131
  }