@donotdev/cli 0.0.8 → 0.0.11

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 (47) hide show
  1. package/dependencies-matrix.json +177 -76
  2. package/dist/bin/commands/build.js +2 -2
  3. package/dist/bin/commands/bump.js +578 -94
  4. package/dist/bin/commands/cacheout.js +2 -2
  5. package/dist/bin/commands/create-app.js +46 -9
  6. package/dist/bin/commands/create-project.js +63 -10
  7. package/dist/bin/commands/deploy.js +114 -25
  8. package/dist/bin/commands/dev.js +2 -2
  9. package/dist/bin/commands/emu.js +2 -2
  10. package/dist/bin/commands/format.js +2 -2
  11. package/dist/bin/commands/lint.js +2 -2
  12. package/dist/bin/commands/preview.js +2 -2
  13. package/dist/bin/commands/sync-secrets.js +2 -2
  14. package/dist/bin/dndev.js +7 -4
  15. package/dist/bin/donotdev.js +7 -4
  16. package/dist/index.js +177 -33
  17. package/package.json +5 -4
  18. package/templates/app-next/src/config/app.ts.example +1 -1
  19. package/templates/app-vite/index.html.example +24 -2
  20. package/templates/app-vite/src/config/app.ts.example +1 -1
  21. package/templates/app-vite/src/pages/FormPageExample.tsx.example +8 -5
  22. package/templates/app-vite/src/pages/ListPageExample.tsx.example +4 -7
  23. package/templates/root-consumer/.claude/agents/architect.md.example +313 -0
  24. package/templates/root-consumer/.claude/agents/builder.md.example +329 -0
  25. package/templates/root-consumer/.claude/agents/coder.md.example +87 -0
  26. package/templates/root-consumer/.claude/agents/extractor.md.example +235 -0
  27. package/templates/root-consumer/.claude/agents/polisher.md.example +359 -0
  28. package/templates/root-consumer/.claude/agents/prompt-engineer.md.example +85 -0
  29. package/templates/root-consumer/.claude/commands/brainstorm.md.example +133 -0
  30. package/templates/root-consumer/.claude/commands/build.md.example +109 -0
  31. package/templates/root-consumer/.claude/commands/design.md.example +136 -0
  32. package/templates/root-consumer/.claude/commands/polish.md.example +145 -0
  33. package/templates/root-consumer/.cursor/mcp.json.example +8 -0
  34. package/templates/root-consumer/.firebaserc.example +5 -0
  35. package/templates/root-consumer/.mcp.json.example +8 -0
  36. package/templates/root-consumer/CLAUDE.md.example +146 -0
  37. package/templates/root-consumer/entities/ExampleEntity.ts.example +2 -1
  38. package/templates/root-consumer/entities/demo.ts.example +1 -1
  39. package/templates/root-consumer/firestore.indexes.json.example +4 -0
  40. package/templates/root-consumer/firestore.rules.example +11 -0
  41. package/templates/root-consumer/guides/dndev/AGENT_START_HERE.md.example +15 -12
  42. package/templates/root-consumer/guides/dndev/COMPONENTS_CRUD.md.example +9 -6
  43. package/templates/root-consumer/guides/dndev/COMPONENT_API.md.example +195 -0
  44. package/templates/root-consumer/guides/dndev/INDEX.md.example +3 -1
  45. package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +485 -57
  46. package/templates/root-consumer/guides/wai-way/entity_patterns.md.example +1 -1
  47. 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`, `InquiryFormTemplate`, `InquiryAdminTemplate`) | `@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
@@ -25,9 +41,16 @@ export const productEntity = defineEntity({
25
41
  price: {
26
42
  name: 'price',
27
43
  label: 'price',
28
- type: 'number',
44
+ type: 'price', // Structured: { amount, currency?, vatIncluded?, discountPercent? }
29
45
  visibility: 'guest',
30
- 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
+ },
31
54
  },
32
55
  image: {
33
56
  name: 'image',
@@ -50,7 +73,72 @@ export const productEntity = defineEntity({
50
73
 
51
74
  ---
52
75
 
53
- ## 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
54
142
 
55
143
  ### Field Visibility (who SEES)
56
144
  | Level | Who |
@@ -59,13 +147,54 @@ export const productEntity = defineEntity({
59
147
  | `'user'` | Authenticated |
60
148
  | `'admin'` | Admins |
61
149
  | `'super'` | Super admins |
150
+ | `'technical'` | Admins only (read-only in forms; e.g. scope field) |
151
+ | `'owner'` | Only when the current user is a stakeholder (see **Stakeholder Access** below) |
62
152
  | `'hidden'` | Never |
63
153
 
154
+ ### Stakeholder Access (ownership)
155
+
156
+ For marketplace-style entities: documents **public when available**, **private to stakeholders** (e.g. partner, customer) when booked.
157
+
158
+ **1. Add `ownership` to the entity:**
159
+
160
+ ```typescript
161
+ ownership: {
162
+ ownerFields: ['providerId', 'customerId'], // Document fields whose value is a user id
163
+ publicCondition: [ // When can anyone read? (AND together)
164
+ { field: 'status', op: '==', value: 'available' },
165
+ ],
166
+ },
167
+ ```
168
+
169
+ **2. Use `visibility: 'owner'`** for fields only stakeholders should see (e.g. internal notes, customer contact after booking):
170
+
171
+ ```typescript
172
+ internalNotes: {
173
+ name: 'internalNotes',
174
+ label: 'Internal notes',
175
+ type: 'textarea',
176
+ visibility: 'owner', // Returned only when request.auth.uid matches one of ownerFields
177
+ editable: true,
178
+ validation: { required: false },
179
+ },
180
+ ```
181
+
182
+ **3. Firestore rules (read and update):** Use the framework helper, then paste the condition into your hand-written `firestore.rules`:
183
+
184
+ ```typescript
185
+ import { generateFirestoreRuleCondition } from '@donotdev/core';
186
+
187
+ const condition = generateFirestoreRuleCondition(scheduleEntity.ownership);
188
+ // Paste into firestore.rules: allow read, update: if <condition>;
189
+ ```
190
+
191
+ **Behavior:** `listCard_` returns only documents matching `publicCondition` (e.g. public slots). `list_` returns only documents where the current user is in one of `ownerFields` ("mine"). Same condition is used for **allow read** and **allow update** so owners can update (e.g. status to cancelled, notes). For multiple owner fields, prefer an `ownerIds` array and `array-contains` in rules and queries.
192
+
64
193
  ### Entity Access (who CAN DO)
65
194
  ```typescript
66
195
  access: {
67
196
  create: 'admin', // default
68
- read: 'guest', // default
197
+ read: 'guest', // default
69
198
  update: 'admin', // default
70
199
  delete: 'admin', // default
71
200
  }
@@ -97,10 +226,13 @@ export const crud = createCrudFunctions(entities);
97
226
 
98
227
  ## 4. Frontend Usage
99
228
 
100
- ### Create/Edit Page
229
+ Use components directly from `@donotdev/ui`. They handle routing automatically - no navigate handlers needed.
230
+
231
+ #### Create/Edit Page
101
232
 
102
233
  ```tsx
103
- import { EntityFormRenderer, useCrud } from '@donotdev/crud';
234
+ import { useCrud } from '@donotdev/crud';
235
+ import { EntityFormRenderer, useParams } from '@donotdev/ui';
104
236
  import { productEntity } from 'entities';
105
237
 
106
238
  export default function ProductPage() {
@@ -115,10 +247,9 @@ export default function ProductPage() {
115
247
 
116
248
  const handleSubmit = async (formData: any) => {
117
249
  isNew ? await add(formData) : await update(id!, formData);
118
- navigate('/products');
250
+ // Navigation happens automatically via cancelPath (defaults to /products)
119
251
  };
120
252
 
121
- // DON'T mount form until data ready (edit mode)
122
253
  if (!isNew && !data) return <Spinner />;
123
254
 
124
255
  return (
@@ -127,82 +258,270 @@ export default function ProductPage() {
127
258
  operation={isNew ? 'create' : 'edit'}
128
259
  defaultValues={data}
129
260
  onSubmit={handleSubmit}
261
+ // Cancel automatically navigates to /products (or cancelPath if provided)
262
+ cancelPath="/products"
130
263
  />
131
264
  );
132
265
  }
133
266
  ```
134
267
 
135
- ### List Page
268
+ #### List Page
136
269
 
137
270
  ```tsx
138
- import { EntityList } from '@donotdev/crud';
271
+ import { EntityList, PageContainer } from '@donotdev/ui';
139
272
  import { useAuth } from '@donotdev/auth';
140
- import { PageContainer, useNavigate } from '@donotdev/ui';
141
273
  import { productEntity } from 'entities';
142
274
 
143
275
  export default function ProductsPage() {
144
276
  const user = useAuth('user');
145
- const navigate = useNavigate();
146
277
 
147
278
  return (
148
279
  <PageContainer>
149
280
  <EntityList
150
281
  entity={productEntity}
151
282
  userRole={user?.role}
152
- // Required: Edit handler - navigates to edit page when edit button clicked
153
- onEdit={(id) => navigate(`/products/${id}`)}
154
- // Optional: Create handler - navigates to create page when "Add New" clicked
155
- onCreate={() => navigate('/products/new')}
156
- // Optional: View handler - called when row is clicked
157
- onView={(id) => navigate(`/products/${id}`)}
283
+ // Routing: basePath defaults to /products. View/Edit = basePath/:id, Create = basePath/new
284
+ // Optional: basePath="/admin/products" or onClick={(id) => openSheet(id)}
158
285
  />
159
286
  </PageContainer>
160
287
  );
161
288
  }
162
289
  ```
163
290
 
164
- **Props:**
165
- - `onEdit` **(required)** - Function `(id: string) => void` called when edit button is clicked. Typically navigates to `/${collection}/${id}` (e.g., `/products/${id}`).
166
- - `onCreate` **(optional)** - Function `() => void` called when "Add New" button is clicked. Typically navigates to `/${collection}/new`.
167
- - `onView` **(optional)** - Function `(id: string) => void` called when a table row is clicked. Useful for view-only pages or detail navigation.
168
- - `userRole` **(optional)** - Current user role for field visibility filtering (backend still enforces security).
291
+ #### Card Grid
169
292
 
170
- **Routing Convention:**
171
- - Default edit route: `/${entity.collection}/${id}` (e.g., `/products/abc123`)
172
- - You must create a page at `/${collection}/:id` route to handle the edit page
293
+ ```tsx
294
+ import { EntityCardList, PageContainer } from '@donotdev/ui';
295
+ import { productEntity } from 'entities';
173
296
 
174
- ### Card Grid
297
+ export default function ShopPage() {
298
+ return (
299
+ <PageContainer>
300
+ <EntityCardList
301
+ entity={productEntity}
302
+ // Routing: basePath defaults to /products. View = basePath/:id
303
+ // Optional: basePath="/shop/products" or onClick={(id) => openSheet(id)}
304
+ />
305
+ </PageContainer>
306
+ );
307
+ }
308
+ ```
309
+
310
+ #### Detail/View Page
175
311
 
176
312
  ```tsx
177
- import { EntityCardList } from '@donotdev/crud';
178
- import { PageContainer, useNavigate } from '@donotdev/ui';
313
+ import { EntityDisplayRenderer, useParams } from '@donotdev/ui';
179
314
  import { productEntity } from 'entities';
180
315
 
181
- export default function ShopPage() {
316
+ export default function ProductDetailPage() {
317
+ const { id } = useParams<{ id: string }>();
318
+
319
+ return (
320
+ <EntityDisplayRenderer
321
+ entity={productEntity}
322
+ id={id}
323
+ // Auto-fetches data, shows loading state
324
+ // Read-only display of all visible fields
325
+ />
326
+ );
327
+ }
328
+ ```
329
+
330
+ #### Custom Detail Page with Favorites
331
+
332
+ For custom detail pages, add favorites support using `useEntityFavorites`:
333
+
334
+ ```tsx
335
+ import { useState, useEffect } from 'react';
336
+ import { Heart } from 'lucide-react';
337
+ import { PageContainer, Section, Button, Stack } from '@donotdev/components';
338
+ import { useParams, useNavigate } from '@donotdev/ui';
339
+ import { useCrud, useEntityFavorites } from '@donotdev/crud';
340
+ import { useTranslation } from '@donotdev/core';
341
+ import { productEntity } from 'entities';
342
+
343
+ export default function ProductDetailPage() {
344
+ const { id } = useParams<{ id: string }>();
182
345
  const navigate = useNavigate();
346
+ const { get } = useCrud(productEntity);
347
+ const { isFavorite, toggleFavorite } = useEntityFavorites({
348
+ collection: productEntity.collection,
349
+ });
350
+ const { t } = useTranslation('crud');
351
+ const [data, setData] = useState<any>(null);
352
+ const [loading, setLoading] = useState(true);
353
+
354
+ useEffect(() => {
355
+ if (id) {
356
+ get(id).then((result) => {
357
+ setData(result);
358
+ setLoading(false);
359
+ });
360
+ }
361
+ }, [id, get]);
362
+
363
+ if (loading) return <div>Loading...</div>;
364
+ if (!data) return <div>Not found</div>;
183
365
 
184
366
  return (
185
367
  <PageContainer>
186
- <EntityCardList
368
+ <Section>
369
+ <Stack direction="row" gap="medium" align="center">
370
+ <h1>{data.name}</h1>
371
+ <Button
372
+ variant={isFavorite(id) ? 'primary' : 'outline'}
373
+ icon={<Heart size={18} fill={isFavorite(id) ? 'currentColor' : 'none'} />}
374
+ onClick={() => toggleFavorite(id)}
375
+ >
376
+ {isFavorite(id) ? t('favorites.saved') : t('favorites.save')}
377
+ </Button>
378
+ </Stack>
379
+ {/* Your custom detail page content */}
380
+ </Section>
381
+ </PageContainer>
382
+ );
383
+ }
384
+ ```
385
+
386
+ **Note:** `EntityCardList` automatically includes favorites support - heart icons on cards and a favorites filter toggle. No configuration needed.
387
+
388
+ #### Product Shop (Specialized Template)
389
+
390
+ For e-commerce/shop pages with price, image, and category support, use the specialized template:
391
+
392
+ ```tsx
393
+ import { ProductCardListTemplate } from '@donotdev/templates';
394
+ import { productEntity } from 'entities';
395
+
396
+ export default function ShopPage() {
397
+ return (
398
+ <PageContainer>
399
+ <ProductCardListTemplate
187
400
  entity={productEntity}
188
- // Required: View handler - called when card is clicked
189
- onView={(id) => navigate(`/products/${id}`)}
401
+ // Auto-detects price, image, category fields
402
+ // Routing: basePath defaults to /products. Optional: basePath or onClick
403
+ />
404
+ </PageContainer>
405
+ );
406
+ }
407
+ ```
408
+
409
+ #### Contact/Inquiry Form (Specialized Template)
410
+
411
+ For contact forms that create both a Customer and an Inquiry record:
412
+
413
+ ```tsx
414
+ import { InquiryFormTemplate } from '@donotdev/templates';
415
+ import { customerEntity, inquiryEntity } from 'entities';
416
+
417
+ export default function ContactPage() {
418
+ return (
419
+ <PageContainer>
420
+ <InquiryFormTemplate
421
+ customerEntity={customerEntity}
422
+ inquiryEntity={inquiryEntity}
423
+ // Optional: contextId="car123" - links inquiry to a car/product
424
+ // Optional: contextName="BMW X5 2024" - shown in message placeholder
425
+ // Optional: contextDetails="BMW X5 2024 • 50,000 km • €45,000" - detailed placeholder
426
+ // Optional: messageField="message" - defaults to 'message'
427
+ // Optional: contextField="carId" - defaults to 'carId'
428
+ // Optional: customerFields={['firstName', 'lastName', 'email', 'phone']} - fields to show
429
+ // Optional: onSuccess={() => navigate('/thank-you')}
430
+ />
431
+ </PageContainer>
432
+ );
433
+ }
434
+ ```
435
+
436
+ **What it does:**
437
+ - Creates a Customer record (with `findOrCreate` logic via `uniqueKeys` - prevents duplicates by email/phone)
438
+ - Creates an Inquiry record linked to the customer
439
+ - Handles GDPR consent tracking
440
+ - Auto-fills message with context details if provided
441
+ - Shows success state after submission
442
+
443
+ **Requirements:**
444
+ - Customer entity must have `uniqueKeys` configured (e.g., `{ fields: ['email'], findOrCreate: true }`)
445
+ - Inquiry entity should have `customerId` field (type: `reference`)
446
+ - Both entities should have `status` field (defaults to `'draft'`)
447
+
448
+ #### Inquiry Admin Dashboard (Specialized Template)
449
+
450
+ For admin pages to review and respond to inquiries with one-click actions:
451
+
452
+ ```tsx
453
+ import { InquiryAdminTemplate } from '@donotdev/templates';
454
+ import { customerEntity, inquiryEntity } from 'entities';
455
+
456
+ export default function InquiriesAdminPage() {
457
+ return (
458
+ <PageContainer>
459
+ <InquiryAdminTemplate
460
+ customerEntity={customerEntity}
461
+ inquiryEntity={inquiryEntity}
462
+ // Optional: customerBasePath="/customers" - defaults to '/customers'
190
463
  />
191
464
  </PageContainer>
192
465
  );
193
466
  }
194
467
  ```
195
468
 
196
- **Props:**
197
- - `onView` **(required)** - Function `(id: string) => void` called when a card is clicked. Typically navigates to `/${collection}/${id}` (e.g., `/products/${id}`).
198
- - `cols` **(optional)** - Responsive column breakpoints `[mobile, tablet, desktop, wide]` (default: `[1, 2, 3, 4]`).
199
- - `staleTime` **(optional)** - Cache stale time in milliseconds (default: 30 minutes).
200
- - `filter` **(optional)** - Filter function `(item: any) => boolean` to filter items client-side.
201
- - `hideFilters` **(optional)** - Hide filters section (default: `false`).
469
+ **What it does:**
470
+ - Shows all inquiries in a responsive card grid (1 column mobile, 2 columns desktop)
471
+ - Displays inquiry message preview (first 150 chars)
472
+ - Shows customer info inline (name, email, phone)
473
+ - One-click actions:
474
+ - **Email** - Opens `mailto:` link (includes subject if `carId` present)
475
+ - **Call** - Opens `tel:` link
476
+ - **View Customer** - Navigates to customer detail page
477
+ - **Mark Responded** - Updates inquiry status to `'responded'` (only shown if not already responded)
478
+ - Status badges with icons (✓ for responded, ⏰ for pending)
479
+ - Auto-sorted by priority: available → draft → responded → deleted (then by date, newest first)
480
+
481
+ **Features:**
482
+ - Fetches all inquiries and customers in parallel
483
+ - Efficient customer lookup via Map (no N+1 queries)
484
+ - Loading state with spinner
485
+ - Empty state message
486
+ - Responsive grid layout
202
487
 
203
488
  ---
204
489
 
205
- ## 5. Field Types
490
+ ## 5. Component Props
491
+
492
+ ### EntityList
493
+ - `basePath` **(optional)** - Base path for view/edit/create. Defaults to `/${collection}`. View/Edit = basePath/:id, Create = basePath/new
494
+ - `onClick` **(optional)** - Called when user clicks a row. If provided, overrides navigation (e.g. open sheet)
495
+ - `userRole` **(optional)** - Current user role for field visibility
496
+
497
+ ### EntityCardList
498
+ - `basePath` **(optional)** - Base path for view. Defaults to `/${collection}`. View = basePath/:id
499
+ - `onClick` **(optional)** - Called when user clicks a card. If provided, overrides navigation (e.g. open sheet)
500
+ - `cols` **(optional)** - `[mobile, tablet, desktop, wide]` (default: `[1, 2, 3, 4]`)
501
+ - `staleTime` **(optional)** - Cache stale time in ms (default: 30 min)
502
+ - `filter` **(optional)** - `(item: any) => boolean` - Client-side filter
503
+ - `hideFilters` **(optional)** - Hide filters section
504
+
505
+ **Favorites:** Automatically enabled - heart icons on cards, favorites filter toggle in filters section. Uses `useEntityFavorites` hook internally with `entity.collection` as storage key.
506
+
507
+ ### EntityFormRenderer
508
+ - `entity` **(required)** - Entity definition
509
+ - `onSubmit` **(required)** - `(data: any) => Promise<void>` - Submit handler
510
+ - `defaultValues` **(optional)** - Initial form values (for edit mode)
511
+ - `operation` **(optional)** - `'create' | 'edit'` (auto-detected from defaultValues)
512
+ - `viewerRole` **(optional)** - Role for field visibility/editability
513
+ - `cancelPath` **(optional)** - Path to navigate on cancel (defaults to `/${collection}`)
514
+ - `onCancel` **(optional)** - Cancel button handler (takes precedence over cancelPath)
515
+ - `cancelText` **(optional)** - Cancel button text (null to hide)
516
+
517
+ ### EntityDisplayRenderer
518
+ - `entity` **(required)** - Entity definition
519
+ - `id` **(required)** - Entity ID to fetch and display
520
+ - `viewerRole` **(optional)** - Role for field visibility
521
+
522
+ ---
523
+
524
+ ## 6. Field Types
206
525
 
207
526
  ### Text Inputs
208
527
  - `text` - Single-line text input
@@ -216,6 +535,8 @@ export default function ShopPage() {
216
535
 
217
536
  ### Numbers
218
537
  - `number` - Numeric input
538
+ - `currency` - Single amount with currency symbol (value: number; options: `fieldSpecific.currency`)
539
+ - `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.
219
540
  - `range` - Slider input
220
541
  - `year` - Year input (combobox: type or select from dropdown)
221
542
 
@@ -261,6 +582,42 @@ export default function ShopPage() {
261
582
  - `submit` - Submit button (uncontrolled)
262
583
  - `reset` - Reset button (uncontrolled)
263
584
 
585
+ ### Price field type (structured)
586
+
587
+ Use `type: 'price'` when you need amount, currency, VAT label, and optional discount in one field. Replaces separate "price" + "specialPrice" number fields.
588
+
589
+ **Definition:**
590
+ ```typescript
591
+ price: {
592
+ name: 'price',
593
+ label: 'price',
594
+ type: 'price',
595
+ visibility: 'guest',
596
+ validation: { required: true },
597
+ options: {
598
+ fieldSpecific: {
599
+ defaultCurrency: 'EUR', // Omit → 'EUR'. Used when value.currency is unset
600
+ currencies: ['EUR', 'USD'], // Omit → list = [EUR, USD, GBP, CHF, JPY, KRW]. One item e.g. ['EUR'] → no dropdown, single currency
601
+ optionsTitle: 'Price options', // Omit → 'Price options'. Aria-label for the expand button
602
+ },
603
+ },
604
+ },
605
+ ```
606
+
607
+ **Value shape (stored in DB):**
608
+ ```ts
609
+ { amount: number; currency?: string; vatIncluded?: boolean; discountPercent?: number }
610
+ ```
611
+ - Only `discountPercent` is stored for discount; effective price is computed as `amount * (1 - discountPercent/100)` for display.
612
+
613
+ **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.
614
+
615
+ **Display:** Formatted amount + "VAT Incl." when `vatIncluded`. When `discountPercent > 0`: original amount crossed out + effective price.
616
+
617
+ **Templates:** `CarCardListTemplate`, `CarDetailTemplate`, `ProductCardListTemplate` use the price field when present and show a "Sale" (or `price.badge.sale` i18n) badge when discounted.
618
+
619
+ **Filtering:** Range filter on `amount`; optional "deals" filter (items with `discountPercent > 0`).
620
+
264
621
  ### Select Options
265
622
 
266
623
  ```typescript
@@ -282,7 +639,7 @@ Use `type: 'year'` for year inputs (combobox with type-or-select UX):
282
639
  ```typescript
283
640
  year: {
284
641
  type: 'year',
285
- validation: {
642
+ validation: {
286
643
  required: true,
287
644
  min: 1900, // Optional: defaults to 1900
288
645
  max: 2100 // Optional: defaults to current year + 10
@@ -306,20 +663,17 @@ transmission: {
306
663
 
307
664
  ---
308
665
 
309
- ## 6. Custom Fields & Schemas
666
+ ## 7. Custom Fields & Schemas
310
667
 
311
668
  ### Custom Field Types with UI Components
312
669
 
313
670
  For custom field types that need custom UI components:
314
671
 
315
672
  ```typescript
316
- import { registerFieldType, type ControlledFieldProps } from '@donotdev/crud';
317
- import { useController } from 'react-hook-form';
318
- import * as v from 'valibot';
319
-
320
- // Custom controlled component MUST use framework's useController
321
- import { useController, registerFieldType } from '@donotdev/crud';
673
+ import { registerFieldType, useController } from '@donotdev/crud';
322
674
  import type { ControlledFieldProps } from '@donotdev/crud';
675
+ import { defineEntity } from '@donotdev/core';
676
+ import * as v from 'valibot';
323
677
 
324
678
  function RepairOperationsField({
325
679
  fieldConfig,
@@ -335,7 +689,7 @@ function RepairOperationsField({
335
689
 
336
690
  // Use field.value and field.onChange for form state
337
691
  const value = (field.value as any) || [];
338
-
692
+
339
693
  return (
340
694
  <div>
341
695
  <label>{t(fieldConfig.label)}</label>
@@ -376,7 +730,7 @@ export const carEntity = defineEntity({
376
730
  });
377
731
  ```
378
732
 
379
- **Important:**
733
+ **Important:**
380
734
  - Custom controlled components receive `control` prop, NOT `field` prop
381
735
  - You MUST use `useController` hook to get `field` and `fieldState`
382
736
  - Define schema in `validation.schema` - it's the single source of truth
@@ -386,6 +740,7 @@ export const carEntity = defineEntity({
386
740
  For custom validation without custom UI, just define the schema:
387
741
 
388
742
  ```typescript
743
+ import { defineEntity } from '@donotdev/core';
389
744
  import * as v from 'valibot';
390
745
 
391
746
  export const carEntity = defineEntity({
@@ -436,28 +791,30 @@ export const carEntity = defineEntity({
436
791
 
437
792
  ---
438
793
 
439
- ## 7. Hooks API
794
+ ## 8. Hooks API
440
795
 
441
796
  ```typescript
797
+ import { useCrud, useCrudList, useCrudCardList } from '@donotdev/crud';
798
+
442
799
  // Actions
443
800
  const { add, update, delete: remove, get } = useCrud(productEntity);
444
801
 
445
802
  // List (auto-fetch for tables)
446
- const { items, loading } = useCrudList(productEntity);
803
+ const { items, loading, refresh } = useCrudList(productEntity);
447
804
 
448
805
  // Cards (optimized for public)
449
- const { items, loading, loadMore } = useCrudCardList(productEntity);
806
+ const { items, loading, refresh } = useCrudCardList(productEntity);
450
807
  ```
451
808
 
452
809
  ---
453
810
 
454
- ## 8. Custom Form Layouts
811
+ ## 9. Custom Form Layouts
455
812
 
456
813
  Need a custom layout instead of `EntityFormRenderer`? Use `useEntityForm` directly.
457
814
 
458
815
  ```tsx
459
816
  import { useId, useEffect } from 'react';
460
- import { useEntityForm, useCrud, UploadProvider } from '@donotdev/crud';
817
+ import { useEntityForm, useCrud, UploadProvider, useController } from '@donotdev/crud';
461
818
  import { productEntity } from 'entities';
462
819
 
463
820
  export function CustomProductForm() {
@@ -498,7 +855,7 @@ export function CustomProductForm() {
498
855
 
499
856
  ---
500
857
 
501
- ## 9. i18n
858
+ ## 10. i18n
502
859
 
503
860
  **Namespace:** Automatically set to `entity-${entity.name.toLowerCase()}` (e.g., `entity-product`)
504
861
 
@@ -528,9 +885,80 @@ export const productEntity = defineEntity({
528
885
 
529
886
  Field `label` → translation key in `fields.*`
530
887
 
888
+ ### Status Field Translations
889
+
890
+ The `status` field uses CRUD namespace translations with entity-specific overrides:
891
+
892
+ **Translation Fallback Order:**
893
+ 1. **Entity namespace** (`entity-{name}`) - User overrides take priority
894
+ 2. **CRUD namespace** (`crud`) - Framework defaults
895
+
896
+ **Framework Defaults** (in `crud` namespace):
897
+ - `crud:status.draft` → "Draft"
898
+ - `crud:status.available` → "Available"
899
+ - `crud:status.deleted` → "Deleted"
900
+
901
+ **Override in Entity Translation File:**
902
+
903
+ ```json
904
+ // locales/entity-inquiry_en.json
905
+ {
906
+ "status": {
907
+ "draft": "New",
908
+ "available": "Read",
909
+ "deleted": "Closed"
910
+ }
911
+ }
912
+ ```
913
+
914
+ **How it works:**
915
+ - Framework components (`EntityList`, `EntityFormRenderer`, etc.) automatically use `[entity.namespace, 'crud']` translation namespaces
916
+ - Status labels are translated via `translateLabel()` which tries entity namespace first, then falls back to `crud`
917
+ - No `dndev` namespace fallback - CRUD is optional and doesn't pollute core framework translations
918
+
919
+ **Example:** For an `inquiry` entity:
920
+ - If `entity-inquiry_en.json` has `"status.draft": "New"` → displays "New"
921
+ - If not found → falls back to `crud:status.draft` → displays "Draft"
922
+ - Never falls back to `dndev` namespace
923
+
924
+ ---
925
+
926
+ ## 11. Display Utilities
927
+
928
+ For custom displays, use `formatValue` from `@donotdev/crud`:
929
+
930
+ ```tsx
931
+ import { formatValue, translateFieldLabel } from '@donotdev/crud';
932
+ import { useTranslation } from '@donotdev/core';
933
+ import { productEntity } from 'entities';
934
+
935
+ function ProductCard({ item }) {
936
+ const { t } = useTranslation(productEntity.namespace);
937
+
938
+ // Format any field value using entity config
939
+ const priceDisplay = formatValue(
940
+ item.price,
941
+ productEntity.fields.price,
942
+ t,
943
+ { compact: true } // compact mode for lists/cards
944
+ );
945
+
946
+ // Get translated field label
947
+ const priceLabel = translateFieldLabel('price', productEntity.fields.price, t);
948
+
949
+ return (
950
+ <div>
951
+ <span>{priceLabel}: {priceDisplay}</span>
952
+ </div>
953
+ );
954
+ }
955
+ ```
956
+
957
+ **`formatValue` handles all field types:** dates, numbers, selects (translates option labels), images, files, etc.
958
+
531
959
  ---
532
960
 
533
- ## 10. Routing Convention
961
+ ## 12. Routing Convention
534
962
 
535
963
  | Route | Purpose |
536
964
  |-------|---------|