cabloy 5.1.97 → 5.1.98

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 (122) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +27 -0
  3. package/cabloy-docs/.vitepress/config.mjs +7 -0
  4. package/cabloy-docs/backend/image-guide.md +544 -0
  5. package/cabloy-docs/frontend/image-guide.md +512 -0
  6. package/cabloy-docs/frontend/introduction.md +2 -0
  7. package/cabloy-docs/frontend/ioc-and-beans.md +34 -3
  8. package/cabloy-docs/frontend/model-architecture.md +2 -0
  9. package/cabloy-docs/frontend/model-resource-owner-pattern.md +2 -0
  10. package/cabloy-docs/frontend/model-state-guide.md +2 -0
  11. package/cabloy-docs/frontend/reading-zova-for-vue-developers.md +10 -8
  12. package/cabloy-docs/frontend/server-data.md +2 -0
  13. package/cabloy-docs/frontend/state-architecture-for-vue-developers.md +218 -0
  14. package/cabloy-docs/frontend/system-startup-guide.md +2 -0
  15. package/cabloy-docs/frontend/zova-vs-vue3-comparison.md +11 -10
  16. package/cabloy-docs/fullstack/image-workflow.md +403 -0
  17. package/cabloy-docs/fullstack/introduction.md +1 -0
  18. package/package.json +1 -1
  19. package/vona/package.original.json +0 -1
  20. package/vona/packages-vona/vona/package.json +1 -1
  21. package/vona/pnpm-lock.yaml +424 -47
  22. package/vona/src/suite/cabloy-basic/modules/basic-siteadmin/src/bean/ssrSite.admin.ts +4 -0
  23. package/vona/src/suite/cabloy-basic/modules/basic-siteweb/src/bean/ssrSite.web.ts +4 -0
  24. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/cli/ssrSite/boilerplate/{{sceneName}}.{{beanName}}.ts_ +3 -0
  25. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/package.json +1 -1
  26. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/service/ssrHandler.ts +11 -2
  27. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/types/ssrSite.ts +6 -0
  28. package/vona/src/suite-vendor/a-cabloy/package.json +1 -1
  29. package/vona/src/suite-vendor/a-file/modules/a-file/cli/fileProvider/boilerplate/{{sceneName}}.{{beanName}}.ts_ +23 -0
  30. package/vona/src/suite-vendor/a-file/modules/a-file/cli/fileScene/boilerplate/{{sceneName}}.{{beanName}}.ts_ +5 -0
  31. package/vona/src/suite-vendor/a-file/modules/a-file/package.json +66 -0
  32. package/vona/src/suite-vendor/a-file/modules/a-file/src/.metadata/index.ts +425 -0
  33. package/vona/src/suite-vendor/a-file/modules/a-file/src/.metadata/locales.ts +18 -0
  34. package/vona/src/suite-vendor/a-file/modules/a-file/src/.metadata/this.ts +2 -0
  35. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.file.ts +322 -0
  36. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.fileProvider.ts +62 -0
  37. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/bean.fileUploadPolicy.ts +196 -0
  38. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.redlock.ts +11 -0
  39. package/vona/src/suite-vendor/a-file/modules/a-file/src/bean/meta.version.ts +38 -0
  40. package/vona/src/suite-vendor/a-file/modules/a-file/src/config/config.ts +16 -0
  41. package/vona/src/suite-vendor/a-file/modules/a-file/src/config/locale/en-us.ts +1 -0
  42. package/vona/src/suite-vendor/a-file/modules/a-file/src/config/locale/zh-cn.ts +1 -0
  43. package/vona/src/suite-vendor/a-file/modules/a-file/src/controller/file.ts +167 -0
  44. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadRequest.ts +33 -0
  45. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDirectUploadResponse.ts +41 -0
  46. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileDownloadRequest.ts +12 -0
  47. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadResponse.ts +59 -0
  48. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadTokenRequest.ts +24 -0
  49. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadTokenResponse.ts +16 -0
  50. package/vona/src/suite-vendor/a-file/modules/a-file/src/dto/fileUploadUrlRequest.ts +33 -0
  51. package/vona/src/suite-vendor/a-file/modules/a-file/src/entity/file.ts +55 -0
  52. package/vona/src/suite-vendor/a-file/modules/a-file/src/entity/fileProvider.ts +24 -0
  53. package/vona/src/suite-vendor/a-file/modules/a-file/src/index.ts +4 -0
  54. package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/fileProvider.ts +7 -0
  55. package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/fileScene.ts +7 -0
  56. package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/fileUploadValidation.ts +15 -0
  57. package/vona/src/suite-vendor/a-file/modules/a-file/src/lib/index.ts +3 -0
  58. package/vona/src/suite-vendor/a-file/modules/a-file/src/model/file.ts +10 -0
  59. package/vona/src/suite-vendor/a-file/modules/a-file/src/model/fileProvider.ts +10 -0
  60. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/file.ts +144 -0
  61. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/fileProvider.ts +111 -0
  62. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/fileScene.ts +53 -0
  63. package/vona/src/suite-vendor/a-file/modules/a-file/src/types/index.ts +3 -0
  64. package/vona/src/suite-vendor/a-file/modules/a-file/tsconfig.build.json +11 -0
  65. package/vona/src/suite-vendor/a-file/modules/a-file/tsconfig.json +7 -0
  66. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/package.json +53 -0
  67. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/.metadata/index.ts +104 -0
  68. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/.metadata/this.ts +2 -0
  69. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/bean/fileProvider.cloudflare.ts +123 -0
  70. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/index.ts +1 -0
  71. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/src/service/fileCloudflare.ts +245 -0
  72. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/tsconfig.build.json +11 -0
  73. package/vona/src/suite-vendor/a-file/modules/file-cloudflare/tsconfig.json +7 -0
  74. package/vona/src/suite-vendor/a-file/modules/file-native/package.json +50 -0
  75. package/vona/src/suite-vendor/a-file/modules/file-native/src/.metadata/index.ts +111 -0
  76. package/vona/src/suite-vendor/a-file/modules/file-native/src/.metadata/this.ts +2 -0
  77. package/vona/src/suite-vendor/a-file/modules/file-native/src/bean/fileProvider.native.ts +125 -0
  78. package/vona/src/suite-vendor/a-file/modules/file-native/src/config/config.ts +7 -0
  79. package/vona/src/suite-vendor/a-file/modules/file-native/src/index.ts +1 -0
  80. package/vona/src/suite-vendor/a-file/modules/file-native/src/service/fileNative.ts +125 -0
  81. package/vona/src/suite-vendor/a-file/modules/file-native/tsconfig.build.json +11 -0
  82. package/vona/src/suite-vendor/a-file/modules/file-native/tsconfig.json +7 -0
  83. package/vona/src/suite-vendor/a-file/package.json +14 -0
  84. package/vona/src/suite-vendor/a-file/tsconfig.base.json +4 -0
  85. package/vona/src/suite-vendor/a-file/tsconfig.json +16 -0
  86. package/vona/src/suite-vendor/a-image/modules/a-image/package.json +4 -1
  87. package/vona/src/suite-vendor/a-image/modules/a-image/src/.metadata/index.ts +61 -3
  88. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.image.ts +336 -152
  89. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.imageProvider.ts +21 -14
  90. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/bean.imageUploadPolicy.ts +59 -11
  91. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/meta.index.ts +12 -0
  92. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/meta.redlock.ts +1 -1
  93. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/meta.version.ts +3 -0
  94. package/vona/src/suite-vendor/a-image/modules/a-image/src/bean/schedule.imageDraftPrune.ts +29 -0
  95. package/vona/src/suite-vendor/a-image/modules/a-image/src/config/config.ts +3 -0
  96. package/vona/src/suite-vendor/a-image/modules/a-image/src/controller/image.ts +24 -54
  97. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadFinalizeRequest.ts +13 -0
  98. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadFinalizeResponse.ts +10 -0
  99. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageDirectUploadResponse.ts +10 -0
  100. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageUploadResponse.ts +10 -1
  101. package/vona/src/suite-vendor/a-image/modules/a-image/src/dto/imageView.ts +10 -1
  102. package/vona/src/suite-vendor/a-image/modules/a-image/src/entity/image.ts +10 -1
  103. package/vona/src/suite-vendor/a-image/modules/a-image/src/lib/imageUploadValidation.ts +15 -0
  104. package/vona/src/suite-vendor/a-image/modules/a-image/src/lib/index.ts +1 -0
  105. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/image.ts +18 -0
  106. package/vona/src/suite-vendor/a-image/modules/a-image/src/types/imageProvider.ts +28 -2
  107. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/package.json +1 -1
  108. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/bean/imageProvider.cloudflare.ts +13 -1
  109. package/vona/src/suite-vendor/a-image/modules/image-cloudflare/src/service/imageCloudflare.ts +26 -0
  110. package/vona/src/suite-vendor/a-image/modules/image-native/package.json +6 -2
  111. package/vona/src/suite-vendor/a-image/modules/image-native/src/.metadata/index.ts +37 -0
  112. package/vona/src/suite-vendor/a-image/modules/image-native/src/bean/imageProvider.native.ts +21 -1
  113. package/vona/src/suite-vendor/a-image/modules/image-native/src/controller/image.ts +57 -0
  114. package/vona/src/suite-vendor/a-image/modules/image-native/src/service/imageNative.ts +142 -34
  115. package/vona/src/suite-vendor/a-image/package.json +1 -1
  116. package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
  117. package/vona/src/suite-vendor/a-vona/modules/a-orm/src/types/entity.ts +2 -4
  118. package/vona/src/suite-vendor/a-vona/modules/a-ormutils/package.json +1 -1
  119. package/vona/src/suite-vendor/a-vona/modules/a-ormutils/src/lib/columns.ts +7 -3
  120. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  121. package/zova/pnpm-lock.yaml +389 -397
  122. package/zova/src/suite/cabloy-basic/modules/basic-image/src/component/formFieldImage/controller.tsx +0 -9
@@ -0,0 +1,512 @@
1
+ # Image Guide
2
+
3
+ This guide explains how to use **Zova image** in Cabloy Basic with a practical, contract-first path.
4
+
5
+ Zova image is not only a file input. It is a frontend image layer built around:
6
+
7
+ - the built-in form-field renderer `basic-image:formFieldImage`
8
+ - the built-in table-cell renderer `basic-image:image`
9
+ - resolved relation fields such as `image` and `sceneImages`
10
+ - upload-token based upload through the generated image API
11
+ - optional crop and resize behavior in the browser
12
+ - shared preview dialogs for form and table usage
13
+
14
+ Use this page together with:
15
+
16
+ - [Form Guide](/frontend/form-guide)
17
+ - [Table Guide](/frontend/table-guide)
18
+ - [TableCell Authoring Cookbook](/frontend/table-cell-cookbook)
19
+ - [API Schema Guide](/frontend/api-schema-guide)
20
+ - [SSR ClientOnly](/frontend/ssr-client-only)
21
+ - [Backend Image Guide](/backend/image-guide)
22
+
23
+ > [!TIP]
24
+ > **Image docs path**
25
+ >
26
+ > - **[Frontend Image Guide](/frontend/image-guide)** — learn `basic-image:formFieldImage`, `basic-image:image`, relation previews, and client-side image-field behavior
27
+ > - **[Backend Image Guide](/backend/image-guide)** — learn image scenes, providers, serializer transforms, delivery policy, and backend lifecycle behavior
28
+ >
29
+ > Use the frontend guide when the question is about forms, tables, previews, or client-side upload UX. Use the backend guide when the question is about image scenes, providers, serializer transforms, signed delivery, or upload policy enforcement.
30
+
31
+ Read this page first when your next question is:
32
+
33
+ - how to wire `basic-image:formFieldImage` into a form
34
+ - how to show image thumbnails in a table with `basic-image:image`
35
+ - why an image field has IDs on one side and resolved relation fields on the other
36
+
37
+ ## What you should learn first
38
+
39
+ If you only remember one idea, remember this one:
40
+
41
+ > In Zova, an image field usually stores image ID data, while resolved relation fields provide the `DtoImageView` objects that the built-in image renderers use for previews, dialogs, and richer display.
42
+
43
+ That leads to two closely related data surfaces:
44
+
45
+ - persisted value such as `imageId` or `sceneImageIds`
46
+ - resolved relation field such as `image` or `sceneImages`
47
+
48
+ The built-in image resources work best when both surfaces are present and named consistently.
49
+
50
+ ## What `basic-image` provides on the frontend
51
+
52
+ The `basic-image` module exposes two main public rendering resources.
53
+
54
+ ### `basic-image:formFieldImage`
55
+
56
+ Use this as the standard image field renderer for forms.
57
+
58
+ It supports:
59
+
60
+ - single-image and multi-image fields
61
+ - upload-token based upload
62
+ - frontend file validation
63
+ - optional crop
64
+ - optional resize and re-encode before upload
65
+ - readonly image cards
66
+ - relation-backed preview refresh after upload
67
+
68
+ ### `basic-image:image`
69
+
70
+ Use this as the standard image table-cell renderer.
71
+
72
+ It supports:
73
+
74
+ - thumbnail rendering
75
+ - relation-first preview lookup
76
+ - fallback URL preview behavior
77
+ - multi-image count badges
78
+ - shared preview dialog opening from the table cell
79
+
80
+ ### Shared preview behavior
81
+
82
+ Both resources use the same preview conventions:
83
+
84
+ - preview URLs are normalized against `sys.config.api.baseURL` when needed
85
+ - preview titles use the field title when available
86
+ - multi-image preview supports keyboard left/right navigation
87
+
88
+ ## Minimum backend/frontend contract
89
+
90
+ The frontend image resources depend on a small but important backend contract.
91
+
92
+ A practical minimum is:
93
+
94
+ 1. the backend field stores image ID data such as `imageId` or `sceneImageIds`
95
+ 2. the backend field metadata points to the built-in render resources
96
+ 3. the backend DTO exposes resolved relation fields such as `image?: DtoImageView` or `sceneImages?: DtoImageView[]`
97
+ 4. the backend image scene defines the authoritative upload policy
98
+
99
+ Representative contract pattern:
100
+
101
+ ```typescript
102
+ ZovaRender.field('basic-image:formFieldImage', {
103
+ imageScene: 'training-student:studentImage',
104
+ relationName: 'image',
105
+ });
106
+
107
+ ZovaRender.cell('basic-image:image', {
108
+ relationName: 'image',
109
+ });
110
+ ```
111
+
112
+ And on the DTO side:
113
+
114
+ ```typescript
115
+ v.serializerTransform('a-image:resolveView', {
116
+ fieldName: 'imageId',
117
+ imageScene: 'training-student:studentImage',
118
+ });
119
+ ```
120
+
121
+ A practical rule is:
122
+
123
+ - frontend validation, crop, and resize improve UX
124
+ - backend image scenes and backend upload-token verification remain the true policy boundary
125
+
126
+ For the backend side of that contract, read [Backend Image Guide](/backend/image-guide), especially the sections on image scenes, upload-token flow, and DTO-side image view resolution.
127
+
128
+ ## One running example through this guide: Student image
129
+
130
+ To keep the guide concrete, the first example uses the Student resource.
131
+
132
+ The Student entity stores one image field:
133
+
134
+ ```typescript
135
+ @Api.field(
136
+ v.title($locale('StudentImage')),
137
+ v.optional(),
138
+ ZovaRender.field('basic-image:formFieldImage', {
139
+ imageScene: 'training-student:studentImage',
140
+ accept: ['image/png', 'image/jpeg', 'image/webp'],
141
+ maxSize: 2 * 1024 * 1024,
142
+ enableCrop: true,
143
+ cropAspectRatio: 1,
144
+ relationName: 'image',
145
+ resize: {
146
+ width: 512,
147
+ height: 512,
148
+ fit: 'cover',
149
+ format: 'jpeg',
150
+ quality: 90,
151
+ },
152
+ }),
153
+ ZovaRender.cell('basic-image:image', { relationName: 'image' }),
154
+ v.tableIdentity(),
155
+ )
156
+ imageId?: TableIdentity;
157
+ ```
158
+
159
+ And the DTO exposes a resolved relation field:
160
+
161
+ ```typescript
162
+ @Api.field(
163
+ ZovaRender.visible(false),
164
+ v.optional(),
165
+ v.serializerTransform('a-image:resolveView', {
166
+ fieldName: 'imageId',
167
+ imageScene: 'training-student:studentImage',
168
+ }),
169
+ v.object(DtoImageView),
170
+ )
171
+ image?: DtoImageView;
172
+ ```
173
+
174
+ This gives the frontend a clean split:
175
+
176
+ - `imageId` is the submitted field value
177
+ - `image` is the resolved relation field
178
+
179
+ The backend explanation for that pairing lives in [Backend Image Guide](/backend/image-guide), where the same example is described from the scene and serializer-transform perspective.
180
+
181
+ ## Step 1: Choose the right image field style
182
+
183
+ Before writing code, choose which of these styles matches your field.
184
+
185
+ ### Style A: built-in image resources
186
+
187
+ Use this when:
188
+
189
+ - standard upload, preview, and thumbnail behavior is enough
190
+ - the field should stay schema-driven
191
+ - you want the shortest path to a working CRUD field
192
+
193
+ This is the preferred default.
194
+
195
+ ### Style B: built-in image field plus custom backend contract
196
+
197
+ Use this when:
198
+
199
+ - the frontend UX can stay standard
200
+ - but the backend scene, DTO, or relation structure is business-specific
201
+ - the field still fits the `basic-image` mental model
202
+
203
+ ### Style C: custom image renderer
204
+
205
+ Use this when:
206
+
207
+ - the form interaction is business-specific
208
+ - the table thumbnail behavior must be custom
209
+ - built-in crop, preview, or value flow is not enough
210
+
211
+ A practical rule is:
212
+
213
+ - start with the built-in image resources first
214
+ - add custom renderers only when the business UI really needs module-owned image behavior
215
+
216
+ ## Step 2: Understand relation naming and `relationName`
217
+
218
+ The built-in image resources infer relation names from the stored field name.
219
+
220
+ Current inference rules are:
221
+
222
+ - `imageId` -> `image`
223
+ - `sceneImageIds` -> `sceneImages`
224
+
225
+ That is why the Student example can naturally pair:
226
+
227
+ - `imageId`
228
+ - `image`
229
+
230
+ and the Record example can naturally pair:
231
+
232
+ - `sceneImageIds`
233
+ - `sceneImages`
234
+
235
+ Use explicit `relationName` when:
236
+
237
+ - the field name does not follow the `...Id` or `...Ids` pattern
238
+ - you want the contract to be more obvious in examples and maintenance
239
+ - nested or details flows should avoid any ambiguity
240
+
241
+ Even when inference would work, setting `relationName` explicitly is often a good public-doc choice because it makes the pairing obvious.
242
+
243
+ ## Step 3: Understand `basic-image:formFieldImage`
244
+
245
+ `basic-image:formFieldImage` is the main form-side image renderer.
246
+
247
+ ### Default behavior
248
+
249
+ The current defaults include:
250
+
251
+ - `accept: ['image/png', 'image/jpeg', 'image/webp']`
252
+ - `maxCount: 1`
253
+ - `enableCrop: true`
254
+ - `cropAspectRatio: 1`
255
+ - `cropShape: 'rect'`
256
+ - `resize: { width: 512, height: 512, fit: 'cover', format: 'jpeg', quality: 90 }`
257
+
258
+ These defaults are helpful, but they are not a replacement for explicit business intent.
259
+
260
+ ### Important options
261
+
262
+ #### Required in practice
263
+
264
+ - `imageScene`
265
+
266
+ Although the option shape is optional at the type level, upload currently throws if no image scene is provided.
267
+
268
+ #### Value-shape options
269
+
270
+ - `multiple`
271
+ - `maxCount`
272
+
273
+ These control whether the field behaves like one image ID or an array of image IDs.
274
+
275
+ #### File-selection and validation options
276
+
277
+ - `accept`
278
+ - `mimeTypes`
279
+ - `extensions`
280
+ - `maxSize`
281
+ - `minSize`
282
+ - `placeholder`
283
+
284
+ These affect frontend input behavior and frontend validation messaging.
285
+
286
+ #### Crop options
287
+
288
+ - `enableCrop`
289
+ - `cropAspectRatio`
290
+ - `cropShape`
291
+
292
+ These control the optional crop dialog.
293
+
294
+ #### Resize options
295
+
296
+ - `resize.width`
297
+ - `resize.height`
298
+ - `resize.fit`
299
+ - `resize.background`
300
+ - `resize.quality`
301
+ - `resize.format`
302
+
303
+ These control browser-side preprocessing before upload.
304
+
305
+ #### Relation option
306
+
307
+ - `relationName`
308
+
309
+ Use this to pair the stored field with its resolved relation field explicitly.
310
+
311
+ ## Step 4: Understand the upload lifecycle
312
+
313
+ The built-in form field follows a consistent frontend flow.
314
+
315
+ 1. the user selects a file or files
316
+ 2. the frontend validates the file type and size
317
+ 3. if enabled, the crop dialog opens
318
+ 4. if configured, the browser resizes and re-encodes the image
319
+ 5. the frontend calls `scope.api.image.createUploadToken(...)`
320
+ 6. the frontend uploads multipart data with `token` and `image`
321
+ 7. the stored field value is updated to the uploaded image ID or image ID array
322
+ 8. the resolved relation field is synchronized so the form can preview the uploaded image immediately
323
+
324
+ That last step is easy to miss, but it is one of the most important behaviors in the current implementation.
325
+
326
+ The field value stores IDs.
327
+
328
+ The resolved relation field stores preview-ready image objects.
329
+
330
+ ## Step 5: Readonly and preview behavior
331
+
332
+ The built-in form field keeps image previews useful even outside edit mode.
333
+
334
+ ### Editable mode
335
+
336
+ In editable mode, the field shows:
337
+
338
+ - select/add/replace button
339
+ - upload spinner
340
+ - validation or upload error message
341
+ - preview cards for current images
342
+ - preview button per image
343
+ - remove button per image
344
+
345
+ ### Readonly mode
346
+
347
+ In readonly mode, the field still renders preview cards.
348
+
349
+ If no images are available, it shows a lightweight empty-state message instead of a file input.
350
+
351
+ ### Preview dialog
352
+
353
+ When an image preview is available, the field opens a shared preview dialog that supports:
354
+
355
+ - large lead image preview
356
+ - filename display
357
+ - multi-image thumbnail selection
358
+ - `1 / N` style counters
359
+ - left/right keyboard navigation
360
+
361
+ ## Step 6: Use the multi-image pattern when the business field is plural
362
+
363
+ The Record example shows the multi-image case.
364
+
365
+ Entity pattern:
366
+
367
+ ```typescript
368
+ @Api.field(
369
+ v.title($locale('ScenePhotos')),
370
+ v.optional(),
371
+ ZovaRender.field('basic-image:formFieldImage', {
372
+ imageScene: 'training-record:sceneImage',
373
+ multiple: true,
374
+ maxCount: 9,
375
+ relationName: 'sceneImages',
376
+ accept: ['image/png', 'image/jpeg', 'image/webp'],
377
+ maxSize: 2 * 1024 * 1024,
378
+ enableCrop: false,
379
+ resize: {
380
+ width: 1280,
381
+ height: 1280,
382
+ fit: 'contain',
383
+ format: 'jpeg',
384
+ quality: 90,
385
+ },
386
+ }),
387
+ ZovaRender.cell('basic-image:image', { relationName: 'sceneImages' }),
388
+ v.array(v.tableIdentity()),
389
+ )
390
+ sceneImageIds?: TableIdentity[];
391
+ ```
392
+
393
+ DTO pattern:
394
+
395
+ ```typescript
396
+ @Api.field(
397
+ ZovaRender.visible(false),
398
+ v.optional(),
399
+ v.serializerTransform('a-image:resolveViews', {
400
+ fieldName: 'sceneImageIds',
401
+ imageScene: 'training-record:sceneImage',
402
+ }),
403
+ v.array(DtoImageView),
404
+ )
405
+ sceneImages?: DtoImageView[];
406
+ ```
407
+
408
+ This is the cleanest pattern when the business field should store many image IDs while the UI needs many preview-ready image objects.
409
+
410
+ ## Step 7: Understand `basic-image:image` in tables
411
+
412
+ `basic-image:image` is the standard image table-cell renderer.
413
+
414
+ ### Public options
415
+
416
+ - `size` — defaults to `40`
417
+ - `fit` — defaults to `cover`
418
+ - `relationName`
419
+
420
+ ### Runtime behavior
421
+
422
+ The cell renderer:
423
+
424
+ - reads the normal table cell value through `next()`
425
+ - tries resolved relation field data first
426
+ - falls back to treating the cell value as URL or URL array data
427
+ - renders a thumbnail
428
+ - adds a `+N` badge when multiple images exist
429
+ - opens the shared preview dialog on click
430
+ - stops row-click propagation while opening the preview
431
+
432
+ A practical rule is:
433
+
434
+ - if the table should show reliable image thumbnails from ID-backed fields, make sure the row data includes the matching resolved relation field
435
+
436
+ ## Step 8: Keep details and nested forms relation-aware
437
+
438
+ There is an important special case in the details flow.
439
+
440
+ The `basic-details` service currently special-cases `basic-image:formFieldImage` so submitted detail items keep the resolved relation field data.
441
+
442
+ That matters because nested or detail-based image editing should preserve:
443
+
444
+ - the stored image ID data
445
+ - the resolved relation field data
446
+
447
+ If your image field appears in details or nested form flows, make sure:
448
+
449
+ - the matching resolved relation field exists
450
+ - the field name follows the expected naming rule, or `relationName` is set explicitly
451
+
452
+ Without that relation pairing, image IDs may still save correctly while preview behavior becomes weaker or incomplete in the detail UI.
453
+
454
+ ## Step 9: Remember the SSR and client-only boundary
455
+
456
+ Crop and resize are browser-side behaviors.
457
+
458
+ The current implementation relies on:
459
+
460
+ - `ClientOnly`
461
+ - browser `File`
462
+ - browser `Image`
463
+ - browser `canvas`
464
+ - object URLs
465
+
466
+ That means:
467
+
468
+ - existing preview data can still render in SSR-safe flows
469
+ - crop and resize behavior only happens on the client
470
+ - the guide should not describe crop or resize as SSR features
471
+
472
+ For the general boundary, read [SSR ClientOnly](/frontend/ssr-client-only).
473
+
474
+ ## Common mistakes to avoid
475
+
476
+ When image fields misbehave, these are the first mistakes to check.
477
+
478
+ 1. **Forgetting `imageScene`**
479
+ - the field may render, but upload cannot work correctly
480
+ 2. **Treating frontend validation as the security boundary**
481
+ - backend image scene policy is still authoritative
482
+ 3. **Storing only image IDs without exposing resolved relation fields**
483
+ - previews, cards, and table thumbnails become weaker
484
+ 4. **Relying on inferred relation names when the field name is nonstandard**
485
+ - set `relationName` explicitly instead
486
+ 5. **Forgetting `multiple: true` for array-backed fields**
487
+ - the UI and data shape will drift apart
488
+ 6. **Describing crop or resize as backend processing**
489
+ - current crop and resize are frontend preprocessing steps before upload
490
+ 7. **Ignoring details/nested-form relation behavior**
491
+ - saved data may be correct while preview state becomes fragile
492
+
493
+ ## Verification checklist
494
+
495
+ When writing or changing frontend image docs, verify in this order.
496
+
497
+ 1. confirm the form-field resource is still `basic-image:formFieldImage`
498
+ 2. confirm the table-cell resource is still `basic-image:image`
499
+ 3. confirm option names still match the current controller and table-cell source
500
+ 4. confirm relation-name inference still matches:
501
+ - `imageId -> image`
502
+ - `...Ids -> ...s`
503
+ 5. confirm the Student example still shows the single-image pattern
504
+ 6. confirm the Record example still shows the multi-image pattern
505
+ 7. confirm the details service still preserves image relation data specially
506
+ 8. run the docs build and confirm the page appears in navigation
507
+
508
+ A good final question is:
509
+
510
+ - does this explanation stay frontend-first and contract-aware, or has it drifted into a backend image internals article?
511
+
512
+ If it stays centered on image fields, relation previews, table thumbnails, upload lifecycle, and client-side behavior, it matches how Zova image currently works.
@@ -41,6 +41,7 @@ Recommended starting set:
41
41
  - [Quickstart](/frontend/quickstart)
42
42
  - [Foundation](/frontend/foundation)
43
43
  - [Reading Zova for Vue Developers](/frontend/reading-zova-for-vue-developers)
44
+ - [State Architecture for Vue Developers](/frontend/state-architecture-for-vue-developers)
44
45
  - [Frontend Source Reading Roadmap](/frontend/frontend-source-reading-roadmap)
45
46
  - [Zova Source Reading Map](/frontend/zova-source-reading-map)
46
47
  - [IoC and Beans](/frontend/ioc-and-beans)
@@ -79,6 +80,7 @@ Use this path when the task is about UI composition, component contracts, form o
79
80
 
80
81
  - [Component Guide](/frontend/component-guide)
81
82
  - [Form Guide](/frontend/form-guide)
83
+ - [Image Guide](/frontend/image-guide)
82
84
  - [Zova Form Under the Hood](/frontend/zova-form-under-the-hood)
83
85
  - [Zova Form Source Reading Map](/frontend/zova-form-source-reading-map)
84
86
  - [Table Guide](/frontend/table-guide)
@@ -8,14 +8,45 @@ That matters because state sharing, resource access, lifecycle behavior, and cro
8
8
 
9
9
  ## The four state-sharing scopes
10
10
 
11
- A key Zova idea is that several common sharing scopes can be handled through one IoC-centered model:
11
+ A key Zova idea is that several common sharing scopes can be handled through one IoC-centered model.
12
+
13
+ In many Vue 3 codebases, developers combine local reactive state, composables, `provide/inject`, and store layers depending on where sharing must happen. Zova asks a different first question: which bean owns this state or behavior, and which container scope should own that bean?
12
14
 
13
15
  - component-internal
14
16
  - between-components
15
17
  - app-global
16
18
  - system-level
17
19
 
18
- This replaces the need to switch constantly between unrelated mechanisms for each sharing scope.
20
+ | Sharing scope | Common Vue 3 pattern | Zova implementation shape |
21
+ | ------------------ | ---------------------------------------------------------- | ---------------------------------------------------------------------------------- |
22
+ | component-internal | local component state or component-local composables | `ctx`-scoped controller or bean ownership |
23
+ | between-components | props/emits, parent-owned composables, or `provide/inject` | hierarchical injection through the same IoC model, often with `host` or `skipSelf` |
24
+ | app-global | app-level stores or app-wide provided state | `app` container beans shared at app or request scope |
25
+ | system-level | module-level singletons or long-lived imported state | `sys` container beans and system-level startup/lifecycle |
26
+
27
+ The most important SSR distinction is between `app-global` and `system-level`: app-global behavior can be tied to an individual app instance or request, while system-level behavior is not request-scoped and can outlive individual SSR requests. For more on that lifecycle boundary, see [System Startup Guide](/frontend/system-startup-guide).
28
+
29
+ The point is not that Vue patterns are wrong. The point is that Zova can keep more of these sharing cases inside one bean-and-container architecture instead of switching between unrelated mechanisms. If you want the broader architectural comparison, read [Zova vs Vue 3 Comparison](/frontend/zova-vs-vue3-comparison). If your main question is how ownership, scope, persistence, cache semantics, and SSR behavior fit together as one large-project state system, continue with [State Architecture for Vue Developers](/frontend/state-architecture-for-vue-developers).
30
+
31
+ A compact mental model looks like this:
32
+
33
+ ```typescript
34
+ class ControllerPage {
35
+ @Use()
36
+ $$localCounterState: CounterState;
37
+
38
+ @Use({ injectionScope: 'host' })
39
+ $$hostCounterState: CounterState;
40
+
41
+ @Use({ injectionScope: 'app' })
42
+ $$appCounterState: CounterState;
43
+
44
+ @Use({ injectionScope: 'sys' })
45
+ $$sysCounterState: CounterState;
46
+ }
47
+ ```
48
+
49
+ This top-level example is intentionally compact. The injection, hierarchical lookup, and scope sections below explain these patterns in more detail.
19
50
 
20
51
  ## The three container types
21
52
 
@@ -104,7 +135,7 @@ In practice, Zova can still preserve the ergonomic class-based development exper
104
135
 
105
136
  ### Hierarchical injection patterns
106
137
 
107
- Hierarchical injection replaces many cases where a generic Vue app would fall back to `provide/inject`.
138
+ Hierarchical injection replaces many common cases where a generic Vue app would fall back to `provide/inject`.
108
139
 
109
140
  Representative child lookup pattern:
110
141
 
@@ -42,6 +42,8 @@ In practice, a model often sits above `$api` and below page rendering:
42
42
 
43
43
  Read together with [Server Data](/frontend/server-data) when deciding which abstraction layer a feature should use.
44
44
 
45
+ If you want the larger docs-style overview of how ownership, scope, persistence, cache semantics, and SSR behavior connect in big Vue applications, read [State Architecture for Vue Developers](/frontend/state-architecture-for-vue-developers) before diving deeper into model internals.
46
+
45
47
  If your next question is how the generic lower-level model runtime works beneath these abstractions, continue with [A-Model Under the Hood](/frontend/a-model-under-the-hood).
46
48
 
47
49
  ## Relationship to TanStack Query
@@ -8,6 +8,8 @@ It uses the current `rest-resource` model as the main source specimen:
8
8
 
9
9
  Read [Model Architecture](/frontend/model-architecture) and [Model State Guide](/frontend/model-state-guide) first if you want the broader Model runtime and helper surface.
10
10
 
11
+ If you want the larger docs-style overview of how ownership, scope, unified model state, and resource boundaries fit together in large Vue systems, read [State Architecture for Vue Developers](/frontend/state-architecture-for-vue-developers) first.
12
+
11
13
  If your next question is how `ModelResource` itself works internally at the source level, continue with [ModelResource Internals Deep Dive](/frontend/model-resource-internals-deep-dive).
12
14
 
13
15
  If your next question is how the generic lower-level model runtime works beneath `ModelResource`, continue with [Model Runtime Under the Hood](/frontend/a-model-under-the-hood).
@@ -4,6 +4,8 @@ This guide explains how to author and consume model-managed state in Zova within
4
4
 
5
5
  Read [Model Architecture](/frontend/model-architecture) first if you want the broader architectural role of Model.
6
6
 
7
+ If your main question is how Model fits into the larger ownership/scope/persistence picture for big Vue applications, read [State Architecture for Vue Developers](/frontend/state-architecture-for-vue-developers) before going deeper into helper families.
8
+
7
9
  If you specifically want the scalable resource-facade pattern, continue with [Model Resource Owner Pattern](/frontend/model-resource-owner-pattern).
8
10
 
9
11
  If you want the generic lower-level model runtime beneath these helper families, continue with [A-Model Under the Hood](/frontend/a-model-under-the-hood).
@@ -29,14 +29,14 @@ That is why Zova code often reads more like controller-oriented application code
29
29
 
30
30
  ## Quick translation table
31
31
 
32
- | If you usually think in Vue terms | Read Zova like this instead |
33
- | --- | --- |
34
- | `setup()` is the main wiring point | `__init__` and bean lifecycle are major wiring points |
35
- | `ref` / `reactive` are the visible state hosts | controller and bean instances are the visible state hosts |
36
- | `computed()` creates local derived refs | `$computed()` usually creates instance-level derived state |
37
- | `useRoute()` pulls route state into the component | page controllers expose `$route`, `$params`, and `$query` as part of the controller surface |
38
- | `provide/inject`, composables, props, and stores are separate sharing tools | bean scopes and IoC are used to unify more sharing patterns under one model |
39
- | template or component render is the obvious center | controller-oriented architecture is the center; render can stay in the controller or move into render beans |
32
+ | If you usually think in Vue terms | Read Zova like this instead |
33
+ | --------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
34
+ | `setup()` is the main wiring point | `__init__` and bean lifecycle are major wiring points |
35
+ | `ref` / `reactive` are the visible state hosts | controller and bean instances are the visible state hosts |
36
+ | `computed()` creates local derived refs | `$computed()` usually creates instance-level derived state |
37
+ | `useRoute()` pulls route state into the component | page controllers expose `$route`, `$params`, and `$query` as part of the controller surface |
38
+ | `provide/inject`, composables, props, and stores are separate sharing tools | bean scopes and IoC are used to unify more sharing patterns under one model |
39
+ | template or component render is the obvious center | controller-oriented architecture is the center; render can stay in the controller or move into render beans |
40
40
 
41
41
  ## Start from the right assumptions
42
42
 
@@ -226,6 +226,8 @@ Use this order when:
226
226
  - you want to read Zova source code accurately
227
227
  - you want to avoid rewriting framework-specific code back toward generic Vue habits
228
228
 
229
+ If your main question is specifically about large-project state organization rather than source-reading posture first, continue next with [State Architecture for Vue Developers](/frontend/state-architecture-for-vue-developers).
230
+
229
231
  ## Common mistakes to avoid
230
232
 
231
233
  ### Mistake 1: "This should become a normal Vue SFC"
@@ -45,6 +45,8 @@ Use OpenAPI SDK generation when you want backend-generated API contracts to driv
45
45
 
46
46
  Use schema-driven layers when metadata itself needs to participate in higher-level frontend behavior such as validation or automatic rendering.
47
47
 
48
+ If your main question is not only which data-access layer to choose, but how data ownership, sharing boundaries, persistence, and SSR semantics fit together in large Vue systems, read [State Architecture for Vue Developers](/frontend/state-architecture-for-vue-developers) together with this page.
49
+
48
50
  ## Why this matters for Cabloy
49
51
 
50
52
  This abstraction ladder is one of the key links between Zova and Vona.