@donotdev/cli 0.0.7 → 0.0.9

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 (37) hide show
  1. package/README.md +3 -18
  2. package/dependencies-matrix.json +54 -45
  3. package/dist/bin/commands/build.js +19 -5
  4. package/dist/bin/commands/bump.js +30 -5
  5. package/dist/bin/commands/cacheout.js +36 -19
  6. package/dist/bin/commands/create-app.js +73 -7
  7. package/dist/bin/commands/create-project.js +90 -8
  8. package/dist/bin/commands/deploy.js +130 -27
  9. package/dist/bin/commands/dev.js +23 -7
  10. package/dist/bin/commands/emu.js +28 -12
  11. package/dist/bin/commands/format.js +39 -22
  12. package/dist/bin/commands/lint.js +36 -19
  13. package/dist/bin/commands/preview.js +24 -8
  14. package/dist/bin/commands/sync-secrets.js +19 -5
  15. package/dist/bin/dndev.js +7 -20
  16. package/dist/bin/donotdev.js +7 -20
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +209 -100
  20. package/dist/index.js.map +1 -1
  21. package/package.json +4 -3
  22. package/templates/app-next/src/config/app.ts.example +1 -1
  23. package/templates/app-vite/index.html.example +24 -2
  24. package/templates/app-vite/src/config/app.ts.example +1 -1
  25. package/templates/app-vite/src/pages/FormPageExample.tsx.example +8 -5
  26. package/templates/app-vite/src/pages/ListPageExample.tsx.example +4 -7
  27. package/templates/root-consumer/.firebaserc.example +5 -0
  28. package/templates/root-consumer/entities/ExampleEntity.ts.example +2 -1
  29. package/templates/root-consumer/entities/demo.ts.example +15 -1
  30. package/templates/root-consumer/eslint.config.js.example +2 -80
  31. package/templates/root-consumer/firestore.indexes.json.example +4 -0
  32. package/templates/root-consumer/firestore.rules.example +11 -0
  33. package/templates/root-consumer/guides/dndev/COMPONENTS_CRUD.md.example +9 -6
  34. package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +376 -38
  35. package/templates/root-consumer/guides/dndev/SETUP_I18N.md.example +46 -0
  36. package/templates/root-consumer/guides/wai-way/entity_patterns.md.example +1 -1
  37. package/templates/root-consumer/storage.rules.example +8 -0
@@ -4,6 +4,22 @@
4
4
 
5
5
  ---
6
6
 
7
+ ## Quick Import Reference
8
+
9
+ | What | Package |
10
+ |------|---------|
11
+ | **Hooks** (`useCrud`, `useCrudList`, `useCrudCardList`, `useEntityForm`, `useEntityFavorites`) | `@donotdev/crud` |
12
+ | **Display Utilities** (`formatValue`, `DisplayFieldRenderer`, `EntityFilters`, `translateFieldLabel`) | `@donotdev/crud` |
13
+ | **Form Components** (`FormFieldRenderer`, `Controlled*Field`, `UploadProvider`) | `@donotdev/crud` |
14
+ | **Routing-Aware Components** (`EntityFormRenderer`, `EntityList`, `EntityCardList`, `EntityDisplayRenderer`) | `@donotdev/ui` |
15
+ | **Specialized Templates** (`ProductCardListTemplate`, `CarCardListTemplate`) | `@donotdev/templates` |
16
+ | **Routing** (`useNavigate`, `Link`, `useParams`) | `@donotdev/ui` |
17
+ | **Entity Definition** (`defineEntity`) | `@donotdev/core` |
18
+
19
+ **Architecture:** Hooks and display utilities live in `@donotdev/crud`. Routing-aware page components that compose CRUD with navigation live in `@donotdev/ui`.
20
+
21
+ ---
22
+
7
23
  ## 1. Define Entity
8
24
 
9
25
  ```typescript
@@ -13,6 +29,7 @@ import { defineEntity } from '@donotdev/core';
13
29
  export const productEntity = defineEntity({
14
30
  name: 'Product',
15
31
  collection: 'products',
32
+ // namespace: 'entity-product', // Optional: defaults to entity-${name.toLowerCase()}
16
33
  fields: {
17
34
  name: {
18
35
  name: 'name',
@@ -24,9 +41,16 @@ export const productEntity = defineEntity({
24
41
  price: {
25
42
  name: 'price',
26
43
  label: 'price',
27
- type: 'number',
44
+ type: 'price', // Structured: { amount, currency?, vatIncluded?, discountPercent? }
28
45
  visibility: 'guest',
29
- validation: { required: true }
46
+ validation: { required: true },
47
+ // Optional: omit options entirely → defaultCurrency 'EUR', list = [EUR, USD, GBP, CHF, JPY, KRW]
48
+ options: {
49
+ fieldSpecific: {
50
+ defaultCurrency: 'EUR', // Omit → 'EUR'. Used when value.currency is unset
51
+ currencies: ['EUR', 'USD'], // Omit → framework default (6 currencies). One item e.g. ['EUR'] → no dropdown, EUR only
52
+ },
53
+ },
30
54
  },
31
55
  image: {
32
56
  name: 'image',
@@ -49,7 +73,72 @@ export const productEntity = defineEntity({
49
73
 
50
74
  ---
51
75
 
52
- ## 2. Visibility & Access
76
+ ## 2. Multi-Tenancy (Company/Tenant Scoping)
77
+
78
+ For multi-tenant apps where data belongs to a company/tenant/workspace:
79
+
80
+ ### Step 1: Register Scope Provider (once at app startup)
81
+
82
+ ```typescript
83
+ // src/main.tsx or src/App.tsx
84
+ import { registerScopeProvider } from '@donotdev/core';
85
+ import { useCurrentCompanyStore } from './stores/currentCompanyStore';
86
+
87
+ // Register before app renders
88
+ registerScopeProvider('company', () =>
89
+ useCurrentCompanyStore.getState().currentCompanyId
90
+ );
91
+ ```
92
+
93
+ ### Step 2: Add `scope` to Entity Definition
94
+
95
+ ```typescript
96
+ // entities/client.ts
97
+ import { defineEntity } from '@donotdev/core';
98
+
99
+ export const clientEntity = defineEntity({
100
+ name: 'Client',
101
+ collection: 'clients',
102
+ scope: { field: 'companyId', provider: 'company' }, // <-- Add this
103
+ fields: {
104
+ // NO need to manually define companyId - framework adds it automatically
105
+ name: { name: 'name', label: 'name', type: 'text', visibility: 'user', validation: { required: true } },
106
+ // ...
107
+ }
108
+ });
109
+ ```
110
+
111
+ ### What Happens Automatically
112
+
113
+ | Operation | Behavior |
114
+ |-----------|----------|
115
+ | `add()` / `set()` | Auto-injects `companyId` from scope provider |
116
+ | `useCrudList()` | Auto-filters by `companyId == currentCompanyId` |
117
+ | `useCrudCardList()` | Auto-filters by `companyId == currentCompanyId` |
118
+ | `query()` | Auto-adds `where companyId == currentCompanyId` |
119
+
120
+ **Result:** Zero boilerplate. Users only see data from their current company.
121
+
122
+ ### Scope Field Properties
123
+
124
+ The auto-added scope field has:
125
+ - `type: 'reference'` (references the scoped collection, e.g., `companies`)
126
+ - `visibility: 'technical'` (hidden from regular users)
127
+ - `editable: 'create-only'` (cannot change scope after creation)
128
+
129
+ ### Multiple Scope Types
130
+
131
+ You can register multiple scope providers for different use cases:
132
+
133
+ ```typescript
134
+ registerScopeProvider('company', () => companyStore.getState().currentCompanyId);
135
+ registerScopeProvider('workspace', () => workspaceStore.getState().currentWorkspaceId);
136
+ registerScopeProvider('tenant', () => tenantStore.getState().currentTenantId);
137
+ ```
138
+
139
+ ---
140
+
141
+ ## 3. Visibility & Access
53
142
 
54
143
  ### Field Visibility (who SEES)
55
144
  | Level | Who |
@@ -64,7 +153,7 @@ export const productEntity = defineEntity({
64
153
  ```typescript
65
154
  access: {
66
155
  create: 'admin', // default
67
- read: 'guest', // default
156
+ read: 'guest', // default
68
157
  update: 'admin', // default
69
158
  delete: 'admin', // default
70
159
  }
@@ -96,10 +185,13 @@ export const crud = createCrudFunctions(entities);
96
185
 
97
186
  ## 4. Frontend Usage
98
187
 
99
- ### Create/Edit Page
188
+ Use components directly from `@donotdev/ui`. They handle routing automatically - no navigate handlers needed.
189
+
190
+ #### Create/Edit Page
100
191
 
101
192
  ```tsx
102
- import { EntityFormRenderer, useCrud } from '@donotdev/crud';
193
+ import { useCrud } from '@donotdev/crud';
194
+ import { EntityFormRenderer, useParams } from '@donotdev/ui';
103
195
  import { productEntity } from 'entities';
104
196
 
105
197
  export default function ProductPage() {
@@ -114,10 +206,9 @@ export default function ProductPage() {
114
206
 
115
207
  const handleSubmit = async (formData: any) => {
116
208
  isNew ? await add(formData) : await update(id!, formData);
117
- navigate('/products');
209
+ // Navigation happens automatically via cancelPath (defaults to /products)
118
210
  };
119
211
 
120
- // DON'T mount form until data ready (edit mode)
121
212
  if (!isNew && !data) return <Spinner />;
122
213
 
123
214
  return (
@@ -126,35 +217,191 @@ export default function ProductPage() {
126
217
  operation={isNew ? 'create' : 'edit'}
127
218
  defaultValues={data}
128
219
  onSubmit={handleSubmit}
220
+ // Cancel automatically navigates to /products (or cancelPath if provided)
221
+ cancelPath="/products"
129
222
  />
130
223
  );
131
224
  }
132
225
  ```
133
226
 
134
- ### List Page
227
+ #### List Page
135
228
 
136
229
  ```tsx
137
- import { EntityList } from '@donotdev/crud';
230
+ import { EntityList, PageContainer } from '@donotdev/ui';
231
+ import { useAuth } from '@donotdev/auth';
138
232
  import { productEntity } from 'entities';
139
233
 
140
234
  export default function ProductsPage() {
141
- return <EntityList entity={productEntity} onRowClick={(p) => navigate(`/products/${p.id}`)} />;
235
+ const user = useAuth('user');
236
+
237
+ return (
238
+ <PageContainer>
239
+ <EntityList
240
+ entity={productEntity}
241
+ userRole={user?.role}
242
+ // Routing: basePath defaults to /products. View/Edit = basePath/:id, Create = basePath/new
243
+ // Optional: basePath="/admin/products" or onClick={(id) => openSheet(id)}
244
+ />
245
+ </PageContainer>
246
+ );
142
247
  }
143
248
  ```
144
249
 
145
- ### Card Grid
250
+ #### Card Grid
146
251
 
147
252
  ```tsx
148
- import { EntityCardList } from '@donotdev/crud';
253
+ import { EntityCardList, PageContainer } from '@donotdev/ui';
254
+ import { productEntity } from 'entities';
149
255
 
150
256
  export default function ShopPage() {
151
- return <EntityCardList entity={productEntity} />;
257
+ return (
258
+ <PageContainer>
259
+ <EntityCardList
260
+ entity={productEntity}
261
+ // Routing: basePath defaults to /products. View = basePath/:id
262
+ // Optional: basePath="/shop/products" or onClick={(id) => openSheet(id)}
263
+ />
264
+ </PageContainer>
265
+ );
266
+ }
267
+ ```
268
+
269
+ #### Detail/View Page
270
+
271
+ ```tsx
272
+ import { EntityDisplayRenderer, useParams } from '@donotdev/ui';
273
+ import { productEntity } from 'entities';
274
+
275
+ export default function ProductDetailPage() {
276
+ const { id } = useParams<{ id: string }>();
277
+
278
+ return (
279
+ <EntityDisplayRenderer
280
+ entity={productEntity}
281
+ id={id}
282
+ // Auto-fetches data, shows loading state
283
+ // Read-only display of all visible fields
284
+ />
285
+ );
286
+ }
287
+ ```
288
+
289
+ #### Custom Detail Page with Favorites
290
+
291
+ For custom detail pages, add favorites support using `useEntityFavorites`:
292
+
293
+ ```tsx
294
+ import { useState, useEffect } from 'react';
295
+ import { Heart } from 'lucide-react';
296
+ import { PageContainer, Section, Button, Stack } from '@donotdev/components';
297
+ import { useParams, useNavigate } from '@donotdev/ui';
298
+ import { useCrud, useEntityFavorites } from '@donotdev/crud';
299
+ import { useTranslation } from '@donotdev/core';
300
+ import { productEntity } from 'entities';
301
+
302
+ export default function ProductDetailPage() {
303
+ const { id } = useParams<{ id: string }>();
304
+ const navigate = useNavigate();
305
+ const { get } = useCrud(productEntity);
306
+ const { isFavorite, toggleFavorite } = useEntityFavorites({
307
+ collection: productEntity.collection,
308
+ });
309
+ const { t } = useTranslation('crud');
310
+ const [data, setData] = useState<any>(null);
311
+ const [loading, setLoading] = useState(true);
312
+
313
+ useEffect(() => {
314
+ if (id) {
315
+ get(id).then((result) => {
316
+ setData(result);
317
+ setLoading(false);
318
+ });
319
+ }
320
+ }, [id, get]);
321
+
322
+ if (loading) return <div>Loading...</div>;
323
+ if (!data) return <div>Not found</div>;
324
+
325
+ return (
326
+ <PageContainer>
327
+ <Section>
328
+ <Stack direction="row" gap="medium" align="center">
329
+ <h1>{data.name}</h1>
330
+ <Button
331
+ variant={isFavorite(id) ? 'primary' : 'outline'}
332
+ icon={<Heart size={18} fill={isFavorite(id) ? 'currentColor' : 'none'} />}
333
+ onClick={() => toggleFavorite(id)}
334
+ >
335
+ {isFavorite(id) ? t('favorites.saved') : t('favorites.save')}
336
+ </Button>
337
+ </Stack>
338
+ {/* Your custom detail page content */}
339
+ </Section>
340
+ </PageContainer>
341
+ );
342
+ }
343
+ ```
344
+
345
+ **Note:** `EntityCardList` automatically includes favorites support - heart icons on cards and a favorites filter toggle. No configuration needed.
346
+
347
+ #### Product Shop (Specialized Template)
348
+
349
+ For e-commerce/shop pages with price, image, and category support, use the specialized template:
350
+
351
+ ```tsx
352
+ import { ProductCardListTemplate } from '@donotdev/templates';
353
+ import { productEntity } from 'entities';
354
+
355
+ export default function ShopPage() {
356
+ return (
357
+ <PageContainer>
358
+ <ProductCardListTemplate
359
+ entity={productEntity}
360
+ // Auto-detects price, image, category fields
361
+ // Routing: basePath defaults to /products. Optional: basePath or onClick
362
+ />
363
+ </PageContainer>
364
+ );
152
365
  }
153
366
  ```
154
367
 
155
368
  ---
156
369
 
157
- ## 5. Field Types
370
+ ## 5. Component Props
371
+
372
+ ### EntityList
373
+ - `basePath` **(optional)** - Base path for view/edit/create. Defaults to `/${collection}`. View/Edit = basePath/:id, Create = basePath/new
374
+ - `onClick` **(optional)** - Called when user clicks a row. If provided, overrides navigation (e.g. open sheet)
375
+ - `userRole` **(optional)** - Current user role for field visibility
376
+
377
+ ### EntityCardList
378
+ - `basePath` **(optional)** - Base path for view. Defaults to `/${collection}`. View = basePath/:id
379
+ - `onClick` **(optional)** - Called when user clicks a card. If provided, overrides navigation (e.g. open sheet)
380
+ - `cols` **(optional)** - `[mobile, tablet, desktop, wide]` (default: `[1, 2, 3, 4]`)
381
+ - `staleTime` **(optional)** - Cache stale time in ms (default: 30 min)
382
+ - `filter` **(optional)** - `(item: any) => boolean` - Client-side filter
383
+ - `hideFilters` **(optional)** - Hide filters section
384
+
385
+ **Favorites:** Automatically enabled - heart icons on cards, favorites filter toggle in filters section. Uses `useEntityFavorites` hook internally with `entity.collection` as storage key.
386
+
387
+ ### EntityFormRenderer
388
+ - `entity` **(required)** - Entity definition
389
+ - `onSubmit` **(required)** - `(data: any) => Promise<void>` - Submit handler
390
+ - `defaultValues` **(optional)** - Initial form values (for edit mode)
391
+ - `operation` **(optional)** - `'create' | 'edit'` (auto-detected from defaultValues)
392
+ - `viewerRole` **(optional)** - Role for field visibility/editability
393
+ - `cancelPath` **(optional)** - Path to navigate on cancel (defaults to `/${collection}`)
394
+ - `onCancel` **(optional)** - Cancel button handler (takes precedence over cancelPath)
395
+ - `cancelText` **(optional)** - Cancel button text (null to hide)
396
+
397
+ ### EntityDisplayRenderer
398
+ - `entity` **(required)** - Entity definition
399
+ - `id` **(required)** - Entity ID to fetch and display
400
+ - `viewerRole` **(optional)** - Role for field visibility
401
+
402
+ ---
403
+
404
+ ## 6. Field Types
158
405
 
159
406
  ### Text Inputs
160
407
  - `text` - Single-line text input
@@ -168,7 +415,10 @@ export default function ShopPage() {
168
415
 
169
416
  ### Numbers
170
417
  - `number` - Numeric input
418
+ - `currency` - Single amount with currency symbol (value: number; options: `fieldSpecific.currency`)
419
+ - `price` - **Structured price:** amount + currency, VAT label, discount %. Value: `{ amount, currency?, vatIncluded?, discountPercent? }`. Options: `fieldSpecific.defaultCurrency`, `fieldSpecific.currencies` (e.g. `['EUR', 'USD']`; one currency = no dropdown). Form: amount + collapsible options (currency if >1, VAT Incl., discount %). Display: formatted amount; when discounted shows original crossed out + effective price. Filterable by amount (range); "deals" = items with discount.
171
420
  - `range` - Slider input
421
+ - `year` - Year input (combobox: type or select from dropdown)
172
422
 
173
423
  ### Boolean
174
424
  - `checkbox` - Checkbox input
@@ -212,6 +462,42 @@ export default function ShopPage() {
212
462
  - `submit` - Submit button (uncontrolled)
213
463
  - `reset` - Reset button (uncontrolled)
214
464
 
465
+ ### Price field type (structured)
466
+
467
+ Use `type: 'price'` when you need amount, currency, VAT label, and optional discount in one field. Replaces separate "price" + "specialPrice" number fields.
468
+
469
+ **Definition:**
470
+ ```typescript
471
+ price: {
472
+ name: 'price',
473
+ label: 'price',
474
+ type: 'price',
475
+ visibility: 'guest',
476
+ validation: { required: true },
477
+ options: {
478
+ fieldSpecific: {
479
+ defaultCurrency: 'EUR', // Omit → 'EUR'. Used when value.currency is unset
480
+ currencies: ['EUR', 'USD'], // Omit → list = [EUR, USD, GBP, CHF, JPY, KRW]. One item e.g. ['EUR'] → no dropdown, single currency
481
+ optionsTitle: 'Price options', // Omit → 'Price options'. Aria-label for the expand button
482
+ },
483
+ },
484
+ },
485
+ ```
486
+
487
+ **Value shape (stored in DB):**
488
+ ```ts
489
+ { amount: number; currency?: string; vatIncluded?: boolean; discountPercent?: number }
490
+ ```
491
+ - Only `discountPercent` is stored for discount; effective price is computed as `amount * (1 - discountPercent/100)` for display.
492
+
493
+ **Form:** Main input = amount (number) with label "Price (symbol)". Collapsible "+" opens: currency select (if more than one in `currencies`), VAT included (checkbox), discount (%). Single currency → no dropdown, value still stored.
494
+
495
+ **Display:** Formatted amount + "VAT Incl." when `vatIncluded`. When `discountPercent > 0`: original amount crossed out + effective price.
496
+
497
+ **Templates:** `CarCardListTemplate`, `CarDetailTemplate`, `ProductCardListTemplate` use the price field when present and show a "Sale" (or `price.badge.sale` i18n) badge when discounted.
498
+
499
+ **Filtering:** Range filter on `amount`; optional "deals" filter (items with `discountPercent > 0`).
500
+
215
501
  ### Select Options
216
502
 
217
503
  ```typescript
@@ -226,14 +512,18 @@ category: {
226
512
  }
227
513
  ```
228
514
 
229
- ### Year Dropdown
515
+ ### Year Field
230
516
 
231
- ```typescript
232
- import { yearOptions } from '@donotdev/crud';
517
+ Use `type: 'year'` for year inputs (combobox with type-or-select UX):
233
518
 
519
+ ```typescript
234
520
  year: {
235
- type: 'select',
236
- validation: { options: yearOptions(1990) } // 1990 to now, descending
521
+ type: 'year',
522
+ validation: {
523
+ required: true,
524
+ min: 1900, // Optional: defaults to 1900
525
+ max: 2100 // Optional: defaults to current year + 10
526
+ }
237
527
  }
238
528
  ```
239
529
 
@@ -253,20 +543,17 @@ transmission: {
253
543
 
254
544
  ---
255
545
 
256
- ## 6. Custom Fields & Schemas
546
+ ## 7. Custom Fields & Schemas
257
547
 
258
548
  ### Custom Field Types with UI Components
259
549
 
260
550
  For custom field types that need custom UI components:
261
551
 
262
552
  ```typescript
263
- import { registerFieldType, type ControlledFieldProps } from '@donotdev/crud';
264
- import { useController } from 'react-hook-form';
265
- import * as v from 'valibot';
266
-
267
- // Custom controlled component MUST use framework's useController
268
- import { useController, registerFieldType } from '@donotdev/crud';
553
+ import { registerFieldType, useController } from '@donotdev/crud';
269
554
  import type { ControlledFieldProps } from '@donotdev/crud';
555
+ import { defineEntity } from '@donotdev/core';
556
+ import * as v from 'valibot';
270
557
 
271
558
  function RepairOperationsField({
272
559
  fieldConfig,
@@ -282,7 +569,7 @@ function RepairOperationsField({
282
569
 
283
570
  // Use field.value and field.onChange for form state
284
571
  const value = (field.value as any) || [];
285
-
572
+
286
573
  return (
287
574
  <div>
288
575
  <label>{t(fieldConfig.label)}</label>
@@ -323,7 +610,7 @@ export const carEntity = defineEntity({
323
610
  });
324
611
  ```
325
612
 
326
- **Important:**
613
+ **Important:**
327
614
  - Custom controlled components receive `control` prop, NOT `field` prop
328
615
  - You MUST use `useController` hook to get `field` and `fieldState`
329
616
  - Define schema in `validation.schema` - it's the single source of truth
@@ -333,6 +620,7 @@ export const carEntity = defineEntity({
333
620
  For custom validation without custom UI, just define the schema:
334
621
 
335
622
  ```typescript
623
+ import { defineEntity } from '@donotdev/core';
336
624
  import * as v from 'valibot';
337
625
 
338
626
  export const carEntity = defineEntity({
@@ -383,28 +671,30 @@ export const carEntity = defineEntity({
383
671
 
384
672
  ---
385
673
 
386
- ## 7. Hooks API
674
+ ## 8. Hooks API
387
675
 
388
676
  ```typescript
677
+ import { useCrud, useCrudList, useCrudCardList } from '@donotdev/crud';
678
+
389
679
  // Actions
390
680
  const { add, update, delete: remove, get } = useCrud(productEntity);
391
681
 
392
682
  // List (auto-fetch for tables)
393
- const { items, loading } = useCrudList(productEntity);
683
+ const { items, loading, refresh } = useCrudList(productEntity);
394
684
 
395
685
  // Cards (optimized for public)
396
- const { items, loading, loadMore } = useCrudCardList(productEntity);
686
+ const { items, loading, refresh } = useCrudCardList(productEntity);
397
687
  ```
398
688
 
399
689
  ---
400
690
 
401
- ## 8. Custom Form Layouts
691
+ ## 9. Custom Form Layouts
402
692
 
403
693
  Need a custom layout instead of `EntityFormRenderer`? Use `useEntityForm` directly.
404
694
 
405
695
  ```tsx
406
696
  import { useId, useEffect } from 'react';
407
- import { useEntityForm, useCrud, UploadProvider } from '@donotdev/crud';
697
+ import { useEntityForm, useCrud, UploadProvider, useController } from '@donotdev/crud';
408
698
  import { productEntity } from 'entities';
409
699
 
410
700
  export function CustomProductForm() {
@@ -445,11 +735,24 @@ export function CustomProductForm() {
445
735
 
446
736
  ---
447
737
 
448
- ## 9. i18n
738
+ ## 10. i18n
739
+
740
+ **Namespace:** Automatically set to `entity-${entity.name.toLowerCase()}` (e.g., `entity-product`)
741
+
742
+ You can customize it by setting `namespace` in your entity definition:
449
743
 
450
- **Namespace:** `entity-${entity.name.toLowerCase()}`
744
+ ```typescript
745
+ export const productEntity = defineEntity({
746
+ name: 'Product',
747
+ collection: 'products',
748
+ namespace: 'custom-namespace', // Optional: override default
749
+ fields: { ... }
750
+ });
751
+ ```
752
+
753
+ **Default behavior:** If not specified, the namespace is automatically set to `entity-${entity.name.toLowerCase()}`.
451
754
 
452
- **File:** `locales/entity-product_en.json`
755
+ **Translation file:** `locales/entity-product_en.json` (or your custom namespace)
453
756
  ```json
454
757
  {
455
758
  "fields": {
@@ -464,7 +767,42 @@ Field `label` → translation key in `fields.*`
464
767
 
465
768
  ---
466
769
 
467
- ## 10. Routing Convention
770
+ ## 11. Display Utilities
771
+
772
+ For custom displays, use `formatValue` from `@donotdev/crud`:
773
+
774
+ ```tsx
775
+ import { formatValue, translateFieldLabel } from '@donotdev/crud';
776
+ import { useTranslation } from '@donotdev/core';
777
+ import { productEntity } from 'entities';
778
+
779
+ function ProductCard({ item }) {
780
+ const { t } = useTranslation(productEntity.namespace);
781
+
782
+ // Format any field value using entity config
783
+ const priceDisplay = formatValue(
784
+ item.price,
785
+ productEntity.fields.price,
786
+ t,
787
+ { compact: true } // compact mode for lists/cards
788
+ );
789
+
790
+ // Get translated field label
791
+ const priceLabel = translateFieldLabel('price', productEntity.fields.price, t);
792
+
793
+ return (
794
+ <div>
795
+ <span>{priceLabel}: {priceDisplay}</span>
796
+ </div>
797
+ );
798
+ }
799
+ ```
800
+
801
+ **`formatValue` handles all field types:** dates, numbers, selects (translates option labels), images, files, etc.
802
+
803
+ ---
804
+
805
+ ## 12. Routing Convention
468
806
 
469
807
  | Route | Purpose |
470
808
  |-------|---------|
@@ -184,4 +184,50 @@ const { languages, currentLanguage } = useLanguageSelector();
184
184
 
185
185
  ---
186
186
 
187
+ ## Advanced: Shared Entity Translations (Monorepos)
188
+
189
+ **Auto-detected:** The framework automatically scans `../../entities/locales/*_*.json` for shared entity translations. If the path exists, translations are loaded; if not, nothing happens.
190
+
191
+ **File naming:** `entity-car_en.json`, `entity-car_fr.json`
192
+
193
+ **Merge behavior:** Base translations from shared package, app can override any key. Deep merge, app wins.
194
+
195
+ **Example structure:**
196
+ ```
197
+ monorepo/
198
+ entities/
199
+ locales/
200
+ entity-car_en.json # ✅ Auto-detected - no config needed
201
+ entity-car_fr.json
202
+ apps/
203
+ admin/ # ✅ Just works - framework auto-detects
204
+ public/ # ✅ Just works - framework auto-detects
205
+ ```
206
+
207
+ **Custom paths:** If your shared translations are in a different location, use `additionalPaths`:
208
+
209
+ **Vite:**
210
+ ```ts
211
+ // vite.config.ts
212
+ export default defineViteConfig({
213
+ appConfig,
214
+ i18n: {
215
+ additionalPaths: ['../../packages/shared/locales'] // Custom path
216
+ }
217
+ })
218
+ ```
219
+
220
+ **Next.js:**
221
+ ```ts
222
+ // next.config.ts
223
+ export default defineNextConfig({
224
+ appConfig,
225
+ i18n: {
226
+ additionalPaths: ['../../packages/shared/locales'] // Custom path
227
+ }
228
+ })
229
+ ```
230
+
231
+ ---
232
+
187
233
  **Drop files, get languages. Framework handles the rest.**
@@ -369,7 +369,7 @@ export const reviewEntity = defineEntity({
369
369
  rating: {
370
370
  name: 'rating',
371
371
  label: 'rating',
372
- type: 'number',
372
+ type: 'rating',
373
373
  visibility: 'guest',
374
374
  validation: { required: true, min: 1, max: 5 },
375
375
  },
@@ -0,0 +1,8 @@
1
+ rules_version = '2';
2
+ service firebase.storage {
3
+ match /b/{bucket}/o {
4
+ match /{allPaths=**} {
5
+ allow read, write: if request.auth != null;
6
+ }
7
+ }
8
+ }