Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a65671b
Add base schema for files and images
freekh Feb 4, 2026
3aaa97d
Add file gallery component
freekh Feb 4, 2026
1ea7952
Update gallery components
freekh Feb 5, 2026
bf66693
Enrich c.image and c.file with metadata from s.images and s.files
freekh Feb 7, 2026
b5f8f56
Support for enriched metadata for remote as well
freekh Feb 7, 2026
9f89036
Add agent rules
freekh Feb 13, 2026
7a4fa7c
Align types after adding s.files and s.images
freekh Feb 13, 2026
bcc5c57
Fix minor issue related to new files / images
freekh Feb 20, 2026
67d49c8
Add media picker, gallery components
freekh Feb 23, 2026
e7f9586
Fix example
freekh Feb 23, 2026
eb832dd
Refactor s.files and s.images to use RecordSchema
freekh Feb 23, 2026
be22916
Align types after refactor of Internal.getSource
freekh Feb 23, 2026
f9ac1dc
Fix missing moduleMetadata when calling nullable
freekh Feb 24, 2026
fa2e7de
Update val.modules with media.val
freekh Feb 24, 2026
27cae72
Upload images / files to referenced modules
freekh Feb 25, 2026
81c6f8a
Show a clear error if referenced module is missing in val.modules
freekh Feb 25, 2026
6ccbb8e
Limit the amount of s.file s.image referenced modules
freekh Feb 26, 2026
926eaa4
Use accept if s.file / s.image is referenced module
freekh Feb 26, 2026
ea72520
Refactor file and image schemas to use referencedModule instead of mo…
freekh Feb 27, 2026
98cd3c2
Add useFilePatchIds hook and integrate it into FileField and ImageFie…
freekh Feb 27, 2026
dd1b129
Fix urls in media picker used in ImageField and FileField
freekh Feb 27, 2026
fdc5c95
Fix open file properties in file gallery
freekh Feb 28, 2026
5d36bd4
Fix file module gallery
freekh Mar 1, 2026
e094362
Update file gallery, image / file field
freekh Mar 4, 2026
c739e6a
Refactor keys of and referenced files
freekh Mar 5, 2026
6872484
Use referenced files in FilePropertiesModal
freekh Mar 5, 2026
e70dea7
Show tooltip if delete file is disabled
freekh Mar 5, 2026
5c5986d
Remove unsightly border in explorer
freekh Mar 5, 2026
599064c
Improve validation testability
freekh Mar 7, 2026
e6a6a51
Implement remote val for cli validate
freekh Mar 7, 2026
e8e7dc3
Add runValidation test with validation error
freekh Mar 9, 2026
56bd0c0
Add runValidation test with image with fix
freekh Mar 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 204 additions & 0 deletions .agent/rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# Val Codebase Instructions

Instructions for AI assistants working with the Val content management system codebase.

## General rules

1. Never add @ts-expect-error unless explicitly being allowed to do so
2. Never use as any unless explicitly being allowed to do so
3. Ask if you need to use type assertions (`as Something`) - we try to avoid those

## Type System Architecture

### Core Type Hierarchy

Val has a dual type system: **Source** types define data shape, **Selector** types is the user facing types.

```
Source (data) → Selector (access)
─────────────────────────────────────────────
ImageSource → ImageSelector
FileSource<M> → FileSelector<M>
RemoteSource<M> → GenericSelector
RichTextSource<O> → RichTextSelector<O>
SourceObject → ObjectSelector<T>
SourceArray → ArraySelector<T>
string/number/boolean → StringSelector/NumberSelector/BooleanSelector
```

### Key Type Definitions

**Source** (`packages/core/src/source/index.ts`):

```typescript
export type Source =
| SourcePrimitive // string | number | boolean | null
| SourceObject // { [key: string]: Source }
| SourceArray // readonly Source[]
| RemoteSource
| FileSource
| ImageSource
| RichTextSource<RichTextOptions>;
```

**SelectorSource** (`packages/core/src/selector/index.ts`):

```typescript
export type SelectorSource =
| SourcePrimitive
| undefined
| readonly SelectorSource[]
| { [key: string]: SelectorSource }
| ImageSource
| FileSource
| RemoteSource
| RichTextSource<AllRichTextOptions>
| GenericSelector<Source>;
```

**GenericSelector** (`packages/core/src/selector/index.ts`):

```typescript
class GenericSelector<T extends Source> {
[GetSource]: T; // The actual source value
[GetSchema]: Schema<T> | undefined; // Schema for validation
[Path]: SourcePath | undefined; // Path in the module tree
[ValError]: Error | undefined; // Type errors
}
```

### CRITICAL: Adding New Source Types

When adding a new source type, it **MUST** be added to BOTH unions:

1. `Source` in `packages/core/src/source/index.ts`
2. `SelectorSource` in `packages/core/src/selector/index.ts`

Additionally: 3. Create selector type in `packages/core/src/selector/{name}.ts` 4. Add mapping in `Selector<T>` conditional type in `packages/core/src/selector/index.ts`

### FORBIDDEN: Type Intersection Hacks

**NEVER** use type intersections (`&`) to force a type to satisfy constraints:

```typescript
// ❌ WRONG - This is a hack that hides the real problem
export type RichTextSelector<O> = GenericSelector<RichTextSource<O> & Source>;

// ✅ CORRECT - Add missing types to SelectorSource union
export type SelectorSource =
| ...existing types...
| ImageSource // Add missing type here
```

If you see `Type 'X' does not satisfy the constraint 'Source'`, the fix is almost always adding a type to `SelectorSource`, NOT using intersections.

## Schema System

### Schema-Source Relationship

Each Schema class validates and types its corresponding Source type:

| Schema | Source | Factory |
| ------------------- | ------------------- | --------------------- |
| `ImageSchema<T>` | `ImageSource` | `s.image()` |
| `FileSchema<T>` | `FileSource` | `s.file()` |
| `RichTextSchema<O>` | `RichTextSource<O>` | `s.richtext(options)` |
| `ObjectSchema<T>` | `SourceObject` | `s.object({...})` |
| `ArraySchema<T>` | `SourceArray` | `s.array(schema)` |

## Module System

### c.define() Pattern

```typescript
c.define(
"/content/page.val.ts", // Module path
s.object({...}), // Schema
{ ... } // Source data matching schema
)
```

### Source Constructors

```typescript
c.image("/public/val/logo.png", {
width: 100,
height: 100,
mimeType: "image/png",
});
c.file("/public/val/doc.pdf", { mimeType: "application/pdf" });
c.remote("https://...", { mimeType: "image/jpeg" });
```

## UI Architecture

### Shadow DOM Isolation

The Val UI runs inside a Shadow DOM for CSS/JS isolation from the host page:

```typescript
// packages/ui/spa/components/ShadowRoot.tsx
const root = node.attachShadow({ mode: "open" });
// ID: "val-shadow-root"
```

**Implications:**

- CSS must target `:host` (not `:root`) for shadow DOM styles
- External stylesheets must be loaded inside the shadow root
- `document.querySelector` won't find elements inside shadow DOM
- Use `shadowRoot.querySelector` or React refs instead

### CSS Architecture

```css
/* packages/ui/spa/index.css */
@layer base {
:host, /* Shadow DOM */
:root {
/* Regular DOM fallback */
--background: ...;
--foreground: ...;
}
}
```

- Dark mode: `[data-mode="dark"]` selector
- CSS loaded via `/api/val/static/{VERSION}/spa/index.css`
- Event `val-css-loaded` dispatched when styles are ready

### Tailwind Configuration

```javascript
// packages/ui/tailwind.config.js
darkMode: ["class", '[data-mode="dark"]'];
```

Custom color tokens map to CSS variables (e.g., `bg-background` → `var(--background)`).

## Testing

Run tests from root dir with:

```bash
pnpm test # All tests
pnpm test packages/core/src/... # Specific test file
pnpm run -r typecheck # Type checking
pnpm lint
pnpm format
```

### Test rules

1. Never "fix" an issue by changing the test file
2. Prefer to define test data in a type-safe manner using `s` and `c` from `initVal`. Search for examples.

## Common Fixes

### "Type 'X' does not satisfy constraint 'Source'"

→ Add the type to `SelectorSource` union in `packages/core/src/selector/index.ts`

### "Property 'X' does not exist on type 'never'"

→ Check if all variants are handled in conditional types (especially in `ImageNode`, `RichTextSource`)
1 change: 1 addition & 0 deletions .claude/CLAUDE.md
1 change: 1 addition & 0 deletions .cursor/rules/val-rules.md
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
1 change: 1 addition & 0 deletions examples/next/app/external.val.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ export default c.define(
"https://www.instagram.com": { title: "Instagram" },
"https://www.linkedin.com": { title: "LinkedIn" },
"https://www.github.com": { title: "GitHub" },
"https://val.build": { title: "" },
},
);
8 changes: 8 additions & 0 deletions examples/next/content/authors.val.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { s, c, type t } from "../val.config";
import mediaVal from "./media.val";

export const schema = s
.record(
s.object({
name: s.string().minLength(2),
birthdate: s.date().from("1900-01-01").to("2024-01-01").nullable(),
image: s.image(mediaVal).nullable(),
}),
)
.render({
Expand All @@ -20,25 +22,31 @@ export default c.define("/content/authors.val.ts", schema, {
teddy: {
name: "Theodor René Carlsen",
birthdate: null,
image: null,
},
freekh: {
name: "Fredrik Ekholdt",
birthdate: "1981-12-30",
image: null,
},
erlamd: {
name: "Erlend Åmdal",
birthdate: null,
image: null,
},
thoram: {
name: "Thomas Ramirez",
birthdate: null,
image: null,
},
isabjo: {
name: "Isak Bjørnstad",
birthdate: null,
image: null,
},
kimmid: {
name: "Kim Midtlid",
birthdate: null,
image: null,
},
});
18 changes: 18 additions & 0 deletions examples/next/content/media.val.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { c, s } from "../val.config";

export default c.define(
"/content/media.val.ts",
s.images({
accept: "image/*",
directory: "/public/val/images",
alt: s.string().minLength(4),
}),
{
"/public/val/images/logo.png": {
width: 800,
height: 600,
mimeType: "image/png",
alt: "An example image",
},
},
);
1 change: 1 addition & 0 deletions examples/next/val.modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default modules(config, [
{ def: () => import("./content/authors.val") },
{ def: () => import("./app/blogs/[blog]/page.val") },
{ def: () => import("./app/generic/[[...path]]/page.val") },
{ def: () => import("./content/media.val") },
{ def: () => import("./app/page.val") },
{ def: () => import("./app/external.val") },
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { c, s } from "../val.config";

export default c.define(
"/content/basic-errors.val.ts",
s.string().minLength(30),
"Hello World",
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { c, s } from "../val.config";

export default c.define(
"/content/basic-image.val.ts",
s.image(),
c.image("/public/val/image.png"),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { c, s } from "../val.config";

export default c.define(
"/content/basic-valid.val.ts",
s.string(),
"Hello World",
);
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions packages/cli/src/__fixtures__/basic/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "es5",
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node"
},
"exclude": ["node_modules"]
}
5 changes: 5 additions & 0 deletions packages/cli/src/__fixtures__/basic/val.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { initVal } from "@valbuild/core";

const { s, c } = initVal();

export { s, c };
Loading
Loading