@donotdev/cli 0.0.13 → 0.0.15
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.
- package/dependencies-matrix.json +357 -89
- package/dist/bin/commands/agent-setup.d.ts +6 -0
- package/dist/bin/commands/agent-setup.d.ts.map +1 -0
- package/dist/bin/commands/agent-setup.js +629 -0
- package/dist/bin/commands/agent-setup.js.map +1 -0
- package/dist/bin/commands/build.js +131 -50
- package/dist/bin/commands/bump.js +137 -49
- package/dist/bin/commands/cacheout.js +50 -21
- package/dist/bin/commands/create-app.js +270 -261
- package/dist/bin/commands/create-project.js +418 -197
- package/dist/bin/commands/deploy.js +1752 -712
- package/dist/bin/commands/dev.js +151 -35
- package/dist/bin/commands/emu.js +228 -70
- package/dist/bin/commands/format.js +50 -21
- package/dist/bin/commands/lint.js +50 -21
- package/dist/bin/commands/preview.js +155 -35
- package/dist/bin/commands/supabase-setup.d.ts +6 -0
- package/dist/bin/commands/supabase-setup.d.ts.map +1 -0
- package/dist/bin/commands/supabase-setup.js +7 -0
- package/dist/bin/commands/supabase-setup.js.map +1 -0
- package/dist/bin/commands/sync-secrets.js +224 -46
- package/dist/bin/commands/type-check.d.ts +14 -0
- package/dist/bin/commands/type-check.d.ts.map +1 -0
- package/dist/bin/commands/type-check.js +314 -0
- package/dist/bin/commands/type-check.js.map +1 -0
- package/dist/bin/commands/wai.js +7399 -11
- package/dist/bin/dndev.js +27 -2
- package/dist/bin/donotdev.js +27 -2
- package/dist/index.js +3960 -2996
- package/package.json +2 -2
- package/templates/app-demo/src/App.tsx.example +1 -0
- package/templates/app-demo/src/pages/FullPage.tsx.example +2 -2
- package/templates/app-demo/src/pages/components/DemoLayout.tsx.example +2 -2
- package/templates/app-demo/src/themes.css.example +5 -12
- package/templates/app-expo/.env.example +64 -0
- package/templates/app-expo/.expo/README.md.example +5 -0
- package/templates/app-expo/.gitignore.example +36 -0
- package/templates/app-expo/README.md.example +58 -0
- package/templates/app-expo/app/.gitkeep +2 -0
- package/templates/app-expo/app/_layout.tsx.example +41 -0
- package/templates/app-expo/app/form.tsx.example +52 -0
- package/templates/app-expo/app/index.tsx.example +89 -0
- package/templates/app-expo/app/list.tsx.example +32 -0
- package/templates/app-expo/app/profile.tsx.example +76 -0
- package/templates/app-expo/app/signin.tsx.example +53 -0
- package/templates/app-expo/app.json.example +39 -0
- package/templates/app-expo/babel.config.js.example +10 -0
- package/templates/app-expo/eas.json.example +20 -0
- package/templates/app-expo/expo-env.d.ts.example +4 -0
- package/templates/app-expo/metro.config.js.example +20 -0
- package/templates/app-expo/service-account-key.json.example +12 -0
- package/templates/app-expo/tsconfig.json.example +19 -0
- package/templates/app-next/.env.example +4 -33
- package/templates/app-next/src/app/ClientLayout.tsx.example +2 -0
- package/templates/app-next/src/app/layout.tsx.example +7 -6
- package/templates/app-next/src/globals.css.example +2 -11
- package/templates/app-next/src/pages/HomePage.tsx.example +1 -1
- package/templates/app-next/src/themes.css.example +10 -13
- package/templates/app-vite/.env.example +3 -32
- package/templates/app-vite/index.html.example +2 -24
- package/templates/app-vite/src/App.tsx.example +2 -0
- package/templates/app-vite/src/globals.css.example +2 -12
- package/templates/app-vite/src/pages/FormPageExample.tsx.example +1 -2
- package/templates/app-vite/src/pages/HomePage.tsx.example +1 -1
- package/templates/app-vite/src/themes.css.example +109 -79
- package/templates/app-vite/vercel.json.example +11 -0
- package/templates/functions-firebase/build.mjs.example +2 -72
- package/templates/functions-firebase/functions-firebase/.env.example.example +23 -25
- package/templates/functions-firebase/functions-firebase/build.mjs.example +2 -72
- package/templates/functions-firebase/functions-firebase/tsconfig.json.example +1 -1
- package/templates/functions-supabase/supabase/functions/cancel-subscription/index.ts.example +7 -0
- package/templates/functions-supabase/supabase/functions/change-plan/index.ts.example +11 -0
- package/templates/functions-supabase/supabase/functions/create-checkout-session/index.ts.example +11 -0
- package/templates/functions-supabase/supabase/functions/create-customer-portal/index.ts.example +7 -0
- package/templates/functions-supabase/supabase/functions/crud/index.ts.example +16 -0
- package/templates/functions-supabase/supabase/functions/delete-account/index.ts.example +7 -0
- package/templates/functions-supabase/supabase/functions/get-custom-claims/index.ts.example +7 -0
- package/templates/functions-supabase/supabase/functions/get-user-auth-status/index.ts.example +7 -0
- package/templates/functions-supabase/supabase/functions/refresh-subscription-status/index.ts.example +7 -0
- package/templates/functions-supabase/supabase/functions/remove-custom-claims/index.ts.example +7 -0
- package/templates/functions-supabase/supabase/functions/set-custom-claims/index.ts.example +7 -0
- package/templates/functions-supabase/supabase/migrations/20250101000000_idempotency.sql +24 -0
- package/templates/functions-supabase/supabase/migrations/20250101000001_rate_limits.sql +22 -0
- package/templates/functions-supabase/supabase/migrations/20250101000002_cleanup_jobs.sql +28 -0
- package/templates/functions-supabase/supabase/migrations/20250101000003_operation_metrics.sql +28 -0
- package/templates/functions-vercel/functions-vercel/tsconfig.json.example +1 -1
- package/templates/functions-vercel/functions-vercel/vercel.json.example +1 -1
- package/templates/functions-vercel/vercel.json.example +1 -1
- package/templates/github/github/workflows/firebase-deploy.yml.example +1 -1
- package/templates/github/workflows/firebase-deploy.yml.example +1 -1
- package/templates/overlay-firebase/env.fragment.example +34 -0
- package/templates/overlay-firebase/env.fragment.expo.example +34 -0
- package/templates/overlay-firebase/env.fragment.nextjs.example +34 -0
- package/templates/overlay-firebase/src/config/providers.expo.ts.example +49 -0
- package/templates/overlay-firebase/src/config/providers.ts.example +23 -0
- package/templates/overlay-supabase/env.fragment.example +7 -0
- package/templates/overlay-supabase/env.fragment.expo.example +7 -0
- package/templates/overlay-supabase/env.fragment.nextjs.example +7 -0
- package/templates/overlay-supabase/src/config/providers.expo.ts.example +35 -0
- package/templates/overlay-supabase/src/config/providers.ts.example +33 -0
- package/templates/overlay-supabase/vercel.headers.example +23 -0
- package/templates/overlay-supabase/vercel.json.example +22 -0
- package/templates/overlay-vercel/env.fragment.example +34 -0
- package/templates/overlay-vercel/env.fragment.nextjs.example +34 -0
- package/templates/overlay-vercel/src/config/providers.ts.example +24 -0
- package/templates/root-consumer/.claude/agents/architect.md.example +2 -310
- package/templates/root-consumer/.claude/agents/builder.md.example +2 -326
- package/templates/root-consumer/.claude/agents/coder.md.example +2 -83
- package/templates/root-consumer/.claude/agents/extractor.md.example +2 -231
- package/templates/root-consumer/.claude/agents/polisher.md.example +2 -132
- package/templates/root-consumer/.claude/agents/prompt-engineer.md.example +2 -81
- package/templates/root-consumer/.claude/commands/brainstorm.md.example +1 -1
- package/templates/root-consumer/.claude/commands/build.md.example +1 -1
- package/templates/root-consumer/.claude/commands/design.md.example +1 -1
- package/templates/root-consumer/.claude/commands/grill.md.example +30 -0
- package/templates/root-consumer/.claude/commands/polish.md.example +1 -1
- package/templates/root-consumer/.claude/commands/techdebt.md.example +28 -0
- package/templates/root-consumer/.clinerules.example +1 -0
- package/templates/root-consumer/.cursor/rules/no-docs.mdc.example +15 -0
- package/templates/root-consumer/.cursorrules.example +1 -0
- package/templates/root-consumer/.dndev/args.json.example +6 -0
- package/templates/root-consumer/.gemini/settings.json.example +2 -2
- package/templates/root-consumer/.github/copilot-instructions.md.example +1 -0
- package/templates/root-consumer/.windsurfrules.example +1 -0
- package/templates/root-consumer/AI.md.example +25 -108
- package/templates/root-consumer/CLAUDE.md.example +1 -128
- package/templates/root-consumer/CONVENTIONS.md.example +1 -0
- package/templates/root-consumer/GEMINI.md.example +1 -0
- package/templates/root-consumer/firebase.json.example +1 -1
- package/templates/root-consumer/guides/dndev/AGENT_START_HERE.md.example +54 -0
- package/templates/root-consumer/guides/dndev/COMPONENTS_ADV.md.example +0 -18
- package/templates/root-consumer/guides/dndev/COMPONENTS_UI.md.example +1 -1
- package/templates/root-consumer/guides/dndev/ENV_SETUP.md.example +99 -30
- package/templates/root-consumer/guides/dndev/GOTCHAS.md.example +186 -0
- package/templates/root-consumer/guides/dndev/INDEX.md.example +4 -1
- package/templates/root-consumer/guides/dndev/SETUP_CRUD.md.example +143 -12
- package/templates/root-consumer/guides/dndev/SETUP_FIREBASE.md.example +9 -3
- package/templates/root-consumer/guides/dndev/SETUP_FUNCTIONS.md.example +12 -7
- package/templates/root-consumer/guides/dndev/SETUP_SOC2.md.example +234 -0
- package/templates/root-consumer/guides/dndev/SETUP_SUPABASE.md.example +124 -0
- package/templates/root-consumer/guides/dndev/SETUP_THEMES.md.example +6 -2
- package/templates/root-consumer/guides/dndev/SETUP_VERCEL.md.example +176 -0
- package/templates/root-consumer/guides/dndev/USE_ROUTING.md.example +5 -9
- package/templates/root-consumer/guides/dndev/essences_reference.css.example +174 -0
- package/templates/root-consumer/guides/wai-way/agents/builder.md.example +10 -0
- package/templates/root-consumer/guides/wai-way/agents/extractor.md.example +25 -5
- package/templates/root-consumer/guides/wai-way/agents/polisher.md.example +13 -2
- package/templates/root-consumer/guides/wai-way/blueprints/0_brainstorm.md.example +2 -2
- package/templates/root-consumer/guides/wai-way/blueprints/1_scaffold.md.example +47 -11
- package/templates/root-consumer/guides/wai-way/blueprints/3_compose.md.example +15 -4
- package/templates/root-consumer/guides/wai-way/spec_template.md.example +7 -6
- package/templates/app-payload/.env.example +0 -28
- package/templates/app-payload/README.md.example +0 -233
- package/templates/app-payload/collections/Company.ts.example +0 -125
- package/templates/app-payload/collections/Hero.ts.example +0 -62
- package/templates/app-payload/collections/Media.ts.example +0 -41
- package/templates/app-payload/collections/Products.ts.example +0 -115
- package/templates/app-payload/collections/Services.ts.example +0 -104
- package/templates/app-payload/collections/Testimonials.ts.example +0 -92
- package/templates/app-payload/collections/Users.ts.example +0 -35
- package/templates/app-payload/src/server.ts.example +0 -79
- package/templates/app-payload/tsconfig.json.example +0 -24
|
@@ -20,6 +20,34 @@
|
|
|
20
20
|
|
|
21
21
|
---
|
|
22
22
|
|
|
23
|
+
## 0. Provider configuration (required)
|
|
24
|
+
|
|
25
|
+
CRUD operations use the **CRUD provider** registered at app startup. You must call `configureProviders()` before any component uses `useCrud`.
|
|
26
|
+
|
|
27
|
+
- **Where:** In a dedicated module, e.g. `src/config/providers.ts`.
|
|
28
|
+
- **When:** Import that module from your root component (e.g. `App.tsx`) so it runs before any CRUD usage.
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// App.tsx
|
|
32
|
+
import './config/providers'; // Before any useCrud
|
|
33
|
+
// ...
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// config/providers.ts
|
|
38
|
+
import { configureProviders } from '@donotdev/core';
|
|
39
|
+
import { FirestoreAdapter } from '@donotdev/firebase'; // or SupabaseCrudAdapter from '@donotdev/supabase'
|
|
40
|
+
|
|
41
|
+
configureProviders({
|
|
42
|
+
crud: new FirestoreAdapter(),
|
|
43
|
+
// auth, storage optional
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If you skip this step, `useCrud` will throw at runtime: "Provider \"crud\" not available. Call configureProviders() at app startup."
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
23
51
|
## 1. Define Entity
|
|
24
52
|
|
|
25
53
|
```typescript
|
|
@@ -80,7 +108,7 @@ For multi-tenant apps where data belongs to a company/tenant/workspace:
|
|
|
80
108
|
### Step 1: Register Scope Provider (once at app startup)
|
|
81
109
|
|
|
82
110
|
```typescript
|
|
83
|
-
// src/
|
|
111
|
+
// src/App.tsx
|
|
84
112
|
import { registerScopeProvider } from '@donotdev/core';
|
|
85
113
|
import { useCurrentCompanyStore } from './stores/currentCompanyStore';
|
|
86
114
|
|
|
@@ -665,9 +693,15 @@ transmission: {
|
|
|
665
693
|
|
|
666
694
|
## 7. Custom Fields & Schemas
|
|
667
695
|
|
|
668
|
-
|
|
696
|
+
A custom field type (when the requirement is not a built-in type) needs up to four things: **(1)** entity field with your custom `type` and `validation.schema`, **(2)** form component(s) so the field is editable, **(3)** optional **display** formatter so list/card/detail views render the value correctly, **(4)** optional **filter** so the field appears in EntityFilters with the right filter UI. All four are wired through a single `registerFieldType({ ... })` call.
|
|
697
|
+
|
|
698
|
+
> **Custom type strings work out of the box.** Just use any string as the `type` — no declaration file or module augmentation needed. The framework accepts any string while still autocompleting built-in types.
|
|
699
|
+
>
|
|
700
|
+
> `CustomFieldOptionsMap` augmentation (see below) is only needed if you want type-checked `options.fieldSpecific` for your custom type.
|
|
669
701
|
|
|
670
|
-
|
|
702
|
+
### Form (required for editable fields)
|
|
703
|
+
|
|
704
|
+
You must register a **controlled component** so the field appears in forms. Optionally register an **uncontrolled component** (e.g. for submit/reset-style fields).
|
|
671
705
|
|
|
672
706
|
```typescript
|
|
673
707
|
import { registerFieldType, useController } from '@donotdev/crud';
|
|
@@ -687,7 +721,6 @@ function RepairOperationsField({
|
|
|
687
721
|
control: control,
|
|
688
722
|
});
|
|
689
723
|
|
|
690
|
-
// Use field.value and field.onChange for form state
|
|
691
724
|
const value = (field.value as any) || [];
|
|
692
725
|
|
|
693
726
|
return (
|
|
@@ -701,13 +734,13 @@ function RepairOperationsField({
|
|
|
701
734
|
);
|
|
702
735
|
}
|
|
703
736
|
|
|
704
|
-
//
|
|
737
|
+
// Minimal registration (form only)
|
|
705
738
|
registerFieldType({
|
|
706
739
|
type: 'repairOperations',
|
|
707
740
|
controlledComponent: RepairOperationsField,
|
|
708
741
|
});
|
|
709
742
|
|
|
710
|
-
//
|
|
743
|
+
// Entity with schema
|
|
711
744
|
export const carEntity = defineEntity({
|
|
712
745
|
name: 'Car',
|
|
713
746
|
collection: 'cars',
|
|
@@ -715,11 +748,10 @@ export const carEntity = defineEntity({
|
|
|
715
748
|
repairs: {
|
|
716
749
|
name: 'repairs',
|
|
717
750
|
label: 'repairs',
|
|
718
|
-
type: 'repairOperations'
|
|
751
|
+
type: 'repairOperations',
|
|
719
752
|
visibility: 'admin',
|
|
720
753
|
validation: {
|
|
721
754
|
required: false,
|
|
722
|
-
// Custom Valibot schema - single source of truth
|
|
723
755
|
schema: v.nullish(v.array(v.object({
|
|
724
756
|
operation: v.string(),
|
|
725
757
|
cost: v.number(),
|
|
@@ -730,10 +762,109 @@ export const carEntity = defineEntity({
|
|
|
730
762
|
});
|
|
731
763
|
```
|
|
732
764
|
|
|
733
|
-
**Important:**
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
765
|
+
**Important:** Custom controlled components receive `control` prop, NOT `field` prop. You MUST use `useController` to get `field` and `fieldState`. Define schema in `validation.schema` — it's the single source of truth.
|
|
766
|
+
|
|
767
|
+
### Display (list/card/detail)
|
|
768
|
+
|
|
769
|
+
Without a **displayFormatter**, list/card/detail views show the raw value (or a fallback). To control how the value is rendered in read-only views, pass `displayFormatter` in the same `registerFieldType` call. Signature: `(value, fieldConfig, t, options?) => string | ReactNode`. Use `options?.compact` for list/card vs detail.
|
|
770
|
+
|
|
771
|
+
```typescript
|
|
772
|
+
// Example: format a custom object for display
|
|
773
|
+
displayFormatter: (value, fieldConfig, t, options) => {
|
|
774
|
+
if (value == null) return '';
|
|
775
|
+
const arr = Array.isArray(value) ? value : [];
|
|
776
|
+
if (options?.compact) return `${arr.length} item(s)`;
|
|
777
|
+
return arr.map((item: any) => `${item.operation}: ${item.cost}`).join(', ') || '—';
|
|
778
|
+
}
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
### Filter (EntityFilters)
|
|
782
|
+
|
|
783
|
+
For the field to appear in the list/card **filters** section (EntityFilters), set **filterable: true** and **filterType** in `registerFieldType`. The filter type determines the filter UI:
|
|
784
|
+
|
|
785
|
+
| filterType | Use for |
|
|
786
|
+
|----------------|--------|
|
|
787
|
+
| `'text'` | Free text search |
|
|
788
|
+
| `'range'` | Numbers, dates (min/max) |
|
|
789
|
+
| `'select'` | Single choice (e.g. enum) |
|
|
790
|
+
| `'multiselect'`| Multiple choices |
|
|
791
|
+
| `'address'` | Address-based filter |
|
|
792
|
+
| `'none'` | Not filterable (omit or set filterable: false) |
|
|
793
|
+
|
|
794
|
+
```typescript
|
|
795
|
+
registerFieldType({
|
|
796
|
+
type: 'repairOperations',
|
|
797
|
+
controlledComponent: RepairOperationsField,
|
|
798
|
+
displayFormatter: (value, fieldConfig, t, options) => { /* ... */ },
|
|
799
|
+
filterable: true,
|
|
800
|
+
filterType: 'text', // or 'range', 'select', 'multiselect', 'address', 'none'
|
|
801
|
+
});
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### Full example: form + display + filter
|
|
805
|
+
|
|
806
|
+
One registration with form, displayFormatter, and filter:
|
|
807
|
+
|
|
808
|
+
```typescript
|
|
809
|
+
import { registerFieldType, useController } from '@donotdev/crud';
|
|
810
|
+
import type { ControlledFieldProps } from '@donotdev/crud';
|
|
811
|
+
import { defineEntity } from '@donotdev/core';
|
|
812
|
+
import * as v from 'valibot';
|
|
813
|
+
|
|
814
|
+
function StatusTierField({ fieldConfig, control, errors, t }: ControlledFieldProps) {
|
|
815
|
+
const { field, fieldState } = useController({ name: fieldConfig.name, control });
|
|
816
|
+
return (
|
|
817
|
+
<div>
|
|
818
|
+
<label>{t(fieldConfig.label)}</label>
|
|
819
|
+
<select value={field.value ?? ''} onChange={(e) => field.onChange(e.target.value)}>
|
|
820
|
+
<option value="">—</option>
|
|
821
|
+
<option value="basic">Basic</option>
|
|
822
|
+
<option value="premium">Premium</option>
|
|
823
|
+
</select>
|
|
824
|
+
{fieldState?.error && <span className="error">{fieldState.error.message}</span>}
|
|
825
|
+
</div>
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
registerFieldType({
|
|
830
|
+
type: 'statusTier',
|
|
831
|
+
controlledComponent: StatusTierField,
|
|
832
|
+
displayFormatter: (value) => (value ? String(value) : '—'),
|
|
833
|
+
filterable: true,
|
|
834
|
+
filterType: 'select',
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
export const productEntity = defineEntity({
|
|
838
|
+
name: 'Product',
|
|
839
|
+
collection: 'products',
|
|
840
|
+
fields: {
|
|
841
|
+
statusTier: {
|
|
842
|
+
name: 'statusTier',
|
|
843
|
+
label: 'statusTier',
|
|
844
|
+
type: 'statusTier',
|
|
845
|
+
visibility: 'guest',
|
|
846
|
+
validation: { schema: v.optional(v.picklist(['basic', 'premium'])) },
|
|
847
|
+
},
|
|
848
|
+
},
|
|
849
|
+
});
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
### Typing custom field options
|
|
853
|
+
|
|
854
|
+
To get type-checked `options.fieldSpecific` for custom types (e.g. `extractDistrictCode: boolean`), augment the framework's `CustomFieldOptionsMap` in your app:
|
|
855
|
+
|
|
856
|
+
```typescript
|
|
857
|
+
// e.g. in src/types/crud.d.ts or next to your entity
|
|
858
|
+
import '@donotdev/core';
|
|
859
|
+
declare module '@donotdev/core' {
|
|
860
|
+
interface CustomFieldOptionsMap {
|
|
861
|
+
'repairOperations': { maxItems?: number };
|
|
862
|
+
'isousou-address': { extractDistrictCode?: boolean };
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
Then in entity fields you can use `type: 'repairOperations'` and `options: { fieldSpecific: { maxItems: 10 } }` without type errors or `as any`.
|
|
737
868
|
|
|
738
869
|
### Custom Schemas (No Custom UI)
|
|
739
870
|
|
|
@@ -112,23 +112,29 @@ This handles everything:
|
|
|
112
112
|
|
|
113
113
|
## Secrets (Stripe, OAuth, etc.)
|
|
114
114
|
|
|
115
|
-
Server-side secrets go in `functions/.env`, not the app `.env
|
|
115
|
+
Server-side secrets go in `functions/.env`, not the app `.env`.
|
|
116
|
+
|
|
117
|
+
**We NEVER ask for secret keys.** You place them yourself:
|
|
116
118
|
|
|
117
119
|
```bash
|
|
118
120
|
# functions/.env
|
|
119
121
|
STRIPE_SECRET_KEY=sk_live_...
|
|
120
122
|
STRIPE_WEBHOOK_SECRET=whsec_...
|
|
123
|
+
SUPABASE_SERVICE_ROLE_KEY=eyJ... # if using Supabase
|
|
121
124
|
GITHUB_CLIENT_SECRET=...
|
|
122
125
|
```
|
|
123
126
|
|
|
124
|
-
|
|
127
|
+
Then sync to your runtime and CI/CD:
|
|
125
128
|
|
|
126
129
|
```bash
|
|
127
|
-
dndev sync-secrets
|
|
130
|
+
dndev sync-secrets # Firebase Secret Manager / Vercel env
|
|
131
|
+
dndev sync-secrets --target github # GitHub Secrets (for CI/CD workflows)
|
|
128
132
|
```
|
|
129
133
|
|
|
130
134
|
Secrets are auto-loaded by Cloud Functions at runtime. Never put server secrets in `VITE_*` variables — those are exposed to the browser.
|
|
131
135
|
|
|
136
|
+
See [ENV_SETUP.md → Secrets Philosophy](./ENV_SETUP.md#secrets-philosophy) for the full policy.
|
|
137
|
+
|
|
132
138
|
---
|
|
133
139
|
|
|
134
140
|
## Cloud Run IAM (Technical Detail)
|
|
@@ -47,21 +47,26 @@ export const crud = createCrudFunctions(entities);
|
|
|
47
47
|
|
|
48
48
|
## Custom Functions
|
|
49
49
|
|
|
50
|
-
**
|
|
50
|
+
**Use `createFunction` — handles config, validation, auth, rate limiting, metrics automatically:**
|
|
51
51
|
|
|
52
52
|
```typescript
|
|
53
|
-
import
|
|
54
|
-
import {
|
|
53
|
+
import * as v from 'valibot';
|
|
54
|
+
import { createFunction } from '@donotdev/functions/firebase';
|
|
55
55
|
import { getFirebaseAdminFirestore } from '@donotdev/firebase/server';
|
|
56
|
-
import { handleError } from '@donotdev/core/server';
|
|
57
56
|
|
|
58
|
-
|
|
57
|
+
const schema = v.object({ productId: v.string() });
|
|
58
|
+
|
|
59
|
+
export const getProductDetails = createFunction(schema, 'get_product_details', async (data, { uid }) => {
|
|
59
60
|
const db = getFirebaseAdminFirestore();
|
|
60
|
-
|
|
61
|
-
return
|
|
61
|
+
const doc = await db.collection('products').doc(data.productId).get();
|
|
62
|
+
return doc.data();
|
|
62
63
|
});
|
|
63
64
|
```
|
|
64
65
|
|
|
66
|
+
**That's it.** Rate limiting, metrics, auth, schema validation — all included by default. No config needed.
|
|
67
|
+
|
|
68
|
+
**Advanced:** Use `createBaseFunction` if you need custom config (memory, timeout, region override).
|
|
69
|
+
|
|
65
70
|
---
|
|
66
71
|
|
|
67
72
|
## Post-Deployment: Cloud Run IAM
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# Setup: SOC2 Readiness
|
|
2
|
+
|
|
3
|
+
**DoNotDev auto-enforces SOC2 baseline controls.** Zero config needed for the MVP. This guide covers advanced controls and audit verification.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Zero-Config Baseline (Always On)
|
|
8
|
+
|
|
9
|
+
When you wire `DndevSecurity` into your app, these controls activate automatically:
|
|
10
|
+
|
|
11
|
+
| Control | Default | SOC2 Criteria |
|
|
12
|
+
|---------|---------|---------------|
|
|
13
|
+
| Structured audit logging (all CRUD + auth) | ✅ Automatic | CC7.1 |
|
|
14
|
+
| Rate limiting (100 writes/min, 500 reads/min) | ✅ Automatic | CC6.6 |
|
|
15
|
+
| Brute-force lockout (5 attempts → 15 min) | ✅ Automatic | CC6.1 |
|
|
16
|
+
| Session timeout (8h idle) | ✅ Automatic | CC6.1 |
|
|
17
|
+
| Field-level RBAC + visibility | ✅ Always on (entity config) | CC6.3 |
|
|
18
|
+
| Supabase RLS / Firestore default-deny | ✅ By convention | CC6.3 |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 1. Wire `DndevSecurity` (Required Once)
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
// src/security.ts
|
|
26
|
+
import { DndevSecurity } from '@donotdev/security/server';
|
|
27
|
+
|
|
28
|
+
export const security = new DndevSecurity({
|
|
29
|
+
// Optional: override defaults
|
|
30
|
+
// rateLimit: { writesPerMin: 50, readsPerMin: 200 },
|
|
31
|
+
// lockout: { maxAttempts: 3, lockoutMs: 30 * 60 * 1000 },
|
|
32
|
+
// sessionTimeoutMs: 4 * 60 * 60 * 1000,
|
|
33
|
+
|
|
34
|
+
// Required for PII encryption (see § PII Encryption below)
|
|
35
|
+
// piiSecret: process.env.PII_SECRET,
|
|
36
|
+
|
|
37
|
+
// Optional: send anomaly alerts to your SIEM/Slack
|
|
38
|
+
// anomalyHandler: async (alert) => { await fetch(process.env.SIEM_WEBHOOK!, { method: 'POST', body: JSON.stringify(alert) }); },
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// src/main.ts (or app entry point)
|
|
44
|
+
import { security } from './security';
|
|
45
|
+
import { crudService } from './crud'; // your CrudService instance
|
|
46
|
+
import { auth } from './auth'; // your SupabaseAuth or FirebaseSDK instance
|
|
47
|
+
|
|
48
|
+
crudService.setSecurity(security);
|
|
49
|
+
auth.setSecurity(security);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 2. PII Encryption (Opt-In — C1 Confidentiality)
|
|
55
|
+
|
|
56
|
+
Required only for entities storing personal data (email, phone, SSN, health data, etc.).
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// entities/user.ts
|
|
60
|
+
export const userEntity = defineEntity({
|
|
61
|
+
name: 'User',
|
|
62
|
+
collection: 'users',
|
|
63
|
+
security: {
|
|
64
|
+
piiFields: ['email', 'phone', 'dateOfBirth'], // Encrypted at rest with AES-256-GCM
|
|
65
|
+
},
|
|
66
|
+
fields: { /* ... */ },
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Add to .env (min 32 chars, generate with: openssl rand -hex 32)
|
|
72
|
+
PII_SECRET=your-secret-key-min-32-chars-here
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Pass to security:
|
|
76
|
+
```typescript
|
|
77
|
+
export const security = new DndevSecurity({
|
|
78
|
+
piiSecret: process.env.PII_SECRET,
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 3. MFA Enforcement (Opt-In — CC6.1)
|
|
85
|
+
|
|
86
|
+
Enforce MFA for specific roles on sensitive entities:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
export const paymentEntity = defineEntity({
|
|
90
|
+
name: 'Payment',
|
|
91
|
+
collection: 'payments',
|
|
92
|
+
security: {
|
|
93
|
+
requireMfa: 'admin', // 'admin' | 'manager' | 'user'
|
|
94
|
+
},
|
|
95
|
+
fields: { /* ... */ },
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Enable MFA in your provider:
|
|
100
|
+
- **Supabase:** Dashboard → Auth → Multi Factor Authentication → Enable TOTP
|
|
101
|
+
- **Firebase:** Firebase Console → Authentication → Multi-factor Auth → Enable
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 4. Data Retention (Opt-In — P6 Privacy)
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
export const auditLogEntity = defineEntity({
|
|
109
|
+
name: 'AuditLog',
|
|
110
|
+
collection: 'audit_logs',
|
|
111
|
+
security: {
|
|
112
|
+
retention: { days: 365 }, // Auto-flag records for purge after 1 year
|
|
113
|
+
},
|
|
114
|
+
fields: { /* ... */ },
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Run retention purge (e.g., in a scheduled Cloud Function):
|
|
119
|
+
```typescript
|
|
120
|
+
import { privacyManager } from '@donotdev/security/server';
|
|
121
|
+
// privacyManager.shouldPurge(record.createdAt, retentionDays) → boolean
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## 5. Right to Erasure (GDPR Art. 17 / P8)
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { DndevSecurity } from '@donotdev/security/server';
|
|
130
|
+
|
|
131
|
+
// In your account deletion handler:
|
|
132
|
+
await security.privacyManager.eraseUser(userId, async (uid) => {
|
|
133
|
+
// Your custom erasure logic per collection
|
|
134
|
+
await db.from('orders').update({ userId: null }).eq('userId', uid);
|
|
135
|
+
await db.from('profiles').delete().eq('id', uid);
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 6. Health Monitoring (Opt-In — A1 Availability)
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// src/client/health.ts
|
|
145
|
+
import { HealthMonitor } from '@donotdev/security';
|
|
146
|
+
|
|
147
|
+
const dbMonitor = new HealthMonitor({ failureThreshold: 3, successThreshold: 2, timeoutMs: 5000 });
|
|
148
|
+
|
|
149
|
+
// Wrap all downstream DB calls through the circuit breaker
|
|
150
|
+
export const safeQuery = (fn: () => Promise<unknown>) => dbMonitor.protect(fn);
|
|
151
|
+
|
|
152
|
+
// Expose a health endpoint (Next.js example)
|
|
153
|
+
// app/api/health/route.ts
|
|
154
|
+
export async function GET() {
|
|
155
|
+
return Response.json({ status: dbMonitor.checkLiveness() });
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 7. Anomaly Detection (Opt-In — CC7.2)
|
|
162
|
+
|
|
163
|
+
Triggered automatically when brute-force or bulk-delete thresholds are exceeded. Wire a handler to send alerts:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
export const security = new DndevSecurity({
|
|
167
|
+
anomalyHandler: async (alert) => {
|
|
168
|
+
// Send to Slack, PagerDuty, Datadog, etc.
|
|
169
|
+
await fetch(process.env.SLACK_WEBHOOK!, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
body: JSON.stringify({ text: `🚨 SOC2 Anomaly: ${alert.type} — ${alert.details}` }),
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 8. Audit Log Output
|
|
180
|
+
|
|
181
|
+
Audit events write to `process.stdout` as NDJSON — pipe to your SIEM:
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{"timestamp":"2025-01-15T10:23:00.000Z","type":"crud.create","userId":"u_123","resource":"products","resourceId":"p_456","ip":"192.168.1.1"}
|
|
185
|
+
{"timestamp":"2025-01-15T10:23:01.000Z","type":"auth.login.failure","userId":"unknown","resource":"auth","ip":"10.0.0.5","details":"brute_force_lockout"}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Cloud Logging (Firebase / GCP):** Stdout is captured automatically.
|
|
189
|
+
**Datadog:** Set `DD_LOGS_INJECTION=true` and pipe stdout to the Datadog agent.
|
|
190
|
+
**Supabase:** Export `process.stderr` anomaly alerts to Logflare.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## SOC2 Readiness Check
|
|
195
|
+
|
|
196
|
+
Run before any audit:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
dn soc2 # Scan current app
|
|
200
|
+
dn soc2 --app my-app # Scan specific app
|
|
201
|
+
dn soc2 --json # Machine-readable JSON output (CI/CD)
|
|
202
|
+
dn soc2 --verbose # Show details for passing checks too
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Exit codes:** `0` = all checks pass, `1` = one or more checks failed (CI-friendly).
|
|
206
|
+
|
|
207
|
+
**Checks run:**
|
|
208
|
+
|
|
209
|
+
| Check | Criteria | What It Looks For |
|
|
210
|
+
|-------|----------|--------------------|
|
|
211
|
+
| SecurityContext wired | CC6.1 | `DndevSecurity` in source |
|
|
212
|
+
| PII encryption secret | C1 | `PII_SECRET` in env when `piiFields` defined |
|
|
213
|
+
| Firestore default-deny | CC6.3 | `allow read, write: if false` in `firestore.rules` |
|
|
214
|
+
| Supabase RLS | CC6.3 | `ENABLE ROW LEVEL SECURITY` in SQL migrations |
|
|
215
|
+
| Audit logging | CC7.1 | `.audit()` / `AuditLogger` usage |
|
|
216
|
+
| Rate limiting | CC6.6 | `rateLimit` / `DndevRateLimiter` usage |
|
|
217
|
+
| Retention policies | P6 | `retention.days` in entity config |
|
|
218
|
+
| Health monitoring | A1 | `HealthMonitor` or `/api/health` endpoint |
|
|
219
|
+
| MFA enforcement | CC6.1 | `requireMfa` in sensitive entities |
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Coverage After Full Setup
|
|
224
|
+
|
|
225
|
+
| SOC2 Criteria | Control | Status |
|
|
226
|
+
|---------------|---------|--------|
|
|
227
|
+
| CC6.1 Auth hardening | Lockout + session timeout + MFA | Framework-enforced |
|
|
228
|
+
| CC6.3 Authorization | RBAC + field visibility + RLS/Firestore rules | Framework-enforced |
|
|
229
|
+
| CC6.6 Rate limiting | Per-user sliding window | Framework-enforced |
|
|
230
|
+
| CC7.1 Audit logging | All CRUD + auth events as NDJSON | Framework-enforced |
|
|
231
|
+
| CC7.2 Anomaly detection | Auth failures + bulk ops threshold alerts | Opt-in handler |
|
|
232
|
+
| A1 Availability | Circuit breaker + liveness probes | Opt-in `HealthMonitor` |
|
|
233
|
+
| C1 Confidentiality | AES-256-GCM field encryption | Opt-in `piiFields` |
|
|
234
|
+
| P1–P8 Privacy | Retention policies + right-to-erasure | Opt-in `retention` |
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Setup: Supabase
|
|
2
|
+
|
|
3
|
+
**From zero to a working Supabase backend: env, tables, RLS, and adapter behavior.**
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Step 1: Run Supabase Setup
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
dndev supabase:setup
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This command:
|
|
14
|
+
- Lets you choose the target app (if you have an `apps/` directory)
|
|
15
|
+
- Asks for your **public** Supabase project URL and anon key
|
|
16
|
+
- Writes `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY` to your app's `.env`
|
|
17
|
+
|
|
18
|
+
**We only ask for public credentials** (safe to ship in your client bundle). We never ask for the service_role key.
|
|
19
|
+
|
|
20
|
+
Get URL and anon key from: [Supabase Dashboard](https://supabase.com/dashboard) → your project → **Settings → API**.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Step 2: Generate Tables from Entities
|
|
25
|
+
|
|
26
|
+
The framework can generate PostgreSQL migrations from your entity definitions (same source as the schema used by the app).
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
dn generate sql
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**What it does:**
|
|
33
|
+
- Discovers entities (e.g. in `entities/` or your configured `--entity-dir`)
|
|
34
|
+
- For each entity: `CREATE TABLE` with columns mapped from field types (text, number, boolean, timestamptz, uuid, jsonb, etc.)
|
|
35
|
+
- Adds technical columns: `id` (uuid, default `gen_random_uuid()`), `user_id`, `created_at`, `updated_at` (with `DEFAULT now()`), `created_by_id`, `updated_by_id`, `status`
|
|
36
|
+
- Enables **Row Level Security (RLS)** and creates policies so rows are scoped by `auth.uid() = user_id`
|
|
37
|
+
- Adds a trigger so `updated_at` is set automatically on every `UPDATE`
|
|
38
|
+
|
|
39
|
+
**Options (optional):**
|
|
40
|
+
- `--entity-dir <path>` — where to find entity files (default: from app root)
|
|
41
|
+
- `--output-dir <path>` — where to write migrations (default: `supabase/migrations`)
|
|
42
|
+
- `--no-single-file` — one migration file per entity instead of one combined file
|
|
43
|
+
|
|
44
|
+
Output is written to `supabase/migrations/` (or your `--output-dir`) as a timestamped `.sql` file. Apply it with the Supabase CLI or Dashboard SQL editor.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Step 3: Apply Migrations
|
|
49
|
+
|
|
50
|
+
After generating SQL:
|
|
51
|
+
|
|
52
|
+
**Option A — Supabase CLI (recommended)**
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
supabase db push
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
(or `supabase migration up` if you manage migrations locally)
|
|
59
|
+
|
|
60
|
+
**Option B — Dashboard**
|
|
61
|
+
|
|
62
|
+
Copy the contents of the generated migration file into the SQL Editor in the Supabase Dashboard and run it.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Step 4: Adapter Behavior (DB-Managed Timestamps)
|
|
67
|
+
|
|
68
|
+
Tables use **snake_case** column names and **timestamptz** for `created_at` / `updated_at`. The framework expects **camelCase** and **ISO date strings** in the app.
|
|
69
|
+
|
|
70
|
+
The **Supabase CRUD adapter** handles this automatically:
|
|
71
|
+
|
|
72
|
+
| Direction | Behavior |
|
|
73
|
+
|-----------|----------|
|
|
74
|
+
| **Read** (get, query, subscribe) | Rows from Supabase are normalized: snake_case → camelCase (e.g. `created_at` → `createdAt`), and timestamp columns are converted to ISO strings. Your UI receives the same shape as with Firebase. |
|
|
75
|
+
| **Write** (add, set, update) | The adapter does **not** send `createdAt` / `updatedAt` (or snake equivalents). The database sets them via `DEFAULT now()` and the `updated_at` trigger. |
|
|
76
|
+
|
|
77
|
+
So you never set timestamps in app code when using Supabase — the DB owns them.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Environment Variables
|
|
82
|
+
|
|
83
|
+
**Client (Vite):** in `apps/<app>/.env`
|
|
84
|
+
|
|
85
|
+
| Variable | Purpose |
|
|
86
|
+
|----------|---------|
|
|
87
|
+
| `VITE_SUPABASE_URL` | Project URL (public) |
|
|
88
|
+
| `VITE_SUPABASE_ANON_KEY` | Anon key (public, safe in bundle) |
|
|
89
|
+
|
|
90
|
+
**Server (e.g. API routes, Edge Functions):** use the same URL and `SUPABASE_SERVICE_ROLE_KEY` for admin operations. Never expose the service_role key to the client. Put it in `functions/.env` or your host’s env (Vercel, etc.).
|
|
91
|
+
|
|
92
|
+
See [ENV_SETUP.md](./ENV_SETUP.md) for where to put secrets.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Storage (Optional)
|
|
97
|
+
|
|
98
|
+
If your app uploads files, create a storage bucket in the Supabase Dashboard (e.g. `uploads`). The default bucket name used by the framework is `uploads`. Configure public or RLS policies in the Dashboard as needed.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Hosting the frontend
|
|
103
|
+
|
|
104
|
+
Supabase gives you **Auth, Postgres, Storage, and Edge Functions**. It does **not** host your built frontend (Vite/Next SPA). You need a separate host for the `dist/` output.
|
|
105
|
+
|
|
106
|
+
**We recommend:**
|
|
107
|
+
|
|
108
|
+
- **Vercel** — Connect your repo, set `VITE_SUPABASE_URL` and `VITE_SUPABASE_ANON_KEY` in the project env, then deploy. Good fit for Next.js or Vite.
|
|
109
|
+
- We scaffold **vercel.json**; run `dndev deploy` and choose Frontend (Vercel) or Frontend + Edge Functions. Set `VITE_SUPABASE_*` in Vercel project env.
|
|
110
|
+
|
|
111
|
+
**Deploy:** Frontend goes to Vercel (scaffolded vercel.json); Edge Functions to Supabase. It does **not** deploy your Supabase app’s frontend to Vercel/Netlify. For that, use the host’s dashboard or CLI (e.g. `vercel`, `netlify deploy`) after building. See [ENV_SETUP.md](./ENV_SETUP.md) for production env vars on Vercel.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Local Development
|
|
116
|
+
|
|
117
|
+
- **Against hosted Supabase:** After `supabase:setup`, run `bun dev` — the app talks to your Supabase project.
|
|
118
|
+
- **Local Supabase:** Install the [Supabase CLI](https://supabase.com/docs/guides/cli) and run `supabase start` for a local Postgres + Auth + Storage stack. Point `VITE_SUPABASE_URL` and keys to the local instance.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Summary
|
|
123
|
+
|
|
124
|
+
**`dndev supabase:setup`** → paste URL + anon key → **`dn generate sql`** → apply migrations → **`bun dev`**. The adapter normalizes read (snake→camel, ISO) and leaves timestamps to the DB on write.
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# Setup: Themes
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Single source of truth:** `src/themes.css`. Import it from `globals.css`; do not set font/color overrides in globals. Framework handles theme switching.
|
|
4
|
+
|
|
5
|
+
**Default essence = SaaS** (Inter, neutral). Optional essences (Brutalist, Luxury) are in the scaffold; they do **not** apply until you set the class on `<html>` (e.g. `class="brutalist"`) or use the theme switcher.
|
|
6
|
+
|
|
7
|
+
**Reference:** Copy Brutalist/Luxury blocks from `guides/dndev/essences_reference.css` into your `src/themes.css` if you need them. Default-essence fonts (Inter, Space Grotesk, Playfair, Roboto) are bundled via `@donotdev/ui`; no Google Fonts, no `public/fonts/` required.
|
|
4
8
|
|
|
5
9
|
---
|
|
6
10
|
|
|
7
11
|
## Standard Use
|
|
8
12
|
|
|
9
|
-
**File:** `src/themes.css` (scaffolded with
|
|
13
|
+
**File:** `src/themes.css` (scaffolded with light, dark, and optional Brutalist/Luxury)
|
|
10
14
|
|
|
11
15
|
**Override colors:**
|
|
12
16
|
```css
|