@valancex/common 0.12.0 → 0.14.0

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/LICENSE +21 -0
  2. package/dist/app.d.ts +11 -4
  3. package/dist/app.js +177 -32
  4. package/dist/client.d.ts +4 -1
  5. package/dist/client.js +20 -4
  6. package/dist/color-scheme.d.ts +12 -0
  7. package/dist/color-scheme.js +15 -0
  8. package/dist/desktop.d.ts +24 -0
  9. package/dist/desktop.js +72 -0
  10. package/dist/early-replay.d.ts +57 -0
  11. package/dist/early-replay.js +75 -0
  12. package/dist/early.d.ts +8 -0
  13. package/dist/early.js +30 -0
  14. package/dist/form.d.ts +165 -0
  15. package/dist/form.js +388 -0
  16. package/dist/http.d.ts +4 -1
  17. package/dist/http.js +7 -1
  18. package/dist/index.d.ts +167 -4
  19. package/dist/index.js +68 -0
  20. package/dist/internal.d.ts +7 -0
  21. package/dist/internal.js +7 -0
  22. package/dist/navigation.d.ts +6 -0
  23. package/dist/navigation.js +5 -0
  24. package/dist/open-system.d.ts +12 -0
  25. package/dist/open-system.js +31 -0
  26. package/dist/open.d.ts +23 -0
  27. package/dist/open.js +47 -0
  28. package/dist/platform.d.ts +5 -0
  29. package/dist/platform.js +4 -0
  30. package/dist/primitives.d.ts +21 -0
  31. package/dist/primitives.js +792 -31
  32. package/dist/record.d.ts +4 -0
  33. package/dist/record.js +14 -0
  34. package/dist/scope.d.ts +11 -2
  35. package/dist/scope.js +10 -1
  36. package/dist/server.js +4 -2
  37. package/dist/store-file.d.ts +4 -0
  38. package/dist/store-file.js +49 -0
  39. package/dist/store.d.ts +40 -0
  40. package/dist/store.js +60 -0
  41. package/dist/targets.d.ts +8 -0
  42. package/dist/targets.js +22 -0
  43. package/dist/vocabulary.d.ts +47 -2
  44. package/dist/vocabulary.js +24 -2
  45. package/dist/web-app.d.ts +6 -0
  46. package/dist/web-app.js +20 -0
  47. package/package.json +14 -5
package/dist/index.d.ts CHANGED
@@ -7,10 +7,22 @@ export interface PageState {
7
7
  readonly page: string;
8
8
  /** How much room the page has: `compact` (under 600 px wide: a phone), `medium` (600 to 839) or `expanded` (840 and up). The framework keeps it current as the window changes; a layout reads it as `viewport`. */
9
9
  readonly viewport: Viewport;
10
+ /** The pages visited, in order, for `back` and `forward`: `go` adds the page it goes to after the one shown (and drops any that were ahead). */
11
+ readonly trail: ReadonlyArray<string>;
12
+ /** Where in `trail` the page shown is. */
13
+ readonly cursor: number;
14
+ /** The colors the person's system asks for: `light` or `dark`. The framework keeps it current (the page's `prefers-color-scheme`, the window's system theme); a layout reads it as `colorScheme`. The server renders `light`; the page corrects it as it starts. */
15
+ readonly colorScheme: ColorScheme;
10
16
  }
17
+ /** The system's color preference. */
18
+ export type ColorScheme = "light" | "dark";
19
+ /** What an application asks its window to look like: a scheme, or `system` for the person's own. */
20
+ export type Appearance = ColorScheme | "system";
11
21
  /** Window size classes, the ones Material Design and iOS size classes agree on closely enough to design for. */
12
22
  export type Viewport = "compact" | "medium" | "expanded";
13
23
  export declare const FIELD: unique symbol;
24
+ /** The brand of a state field that holds a form (`Form.field()`): the application then takes the person to the first field in error after a failed submit, with nothing written. */
25
+ export declare const FORM: unique symbol;
14
26
  /** A state field: its type and its default, declared together. */
15
27
  export interface Field<A = unknown> {
16
28
  readonly [FIELD]: true;
@@ -21,6 +33,15 @@ export interface Field<A = unknown> {
21
33
  * Declares one field of the application's state with its type and its default in one place. The framework builds the state's schema and its initial value from the fields; the default is
22
34
  * checked against the type (by TypeScript, and again when the application is built).
23
35
  */
36
+ export declare const DERIVED: unique symbol;
37
+ /** A value worked out from the state, and the type it has. A template reads it by name, like a state field, and no command changes it. */
38
+ export interface Derived<A = unknown> {
39
+ readonly [DERIVED]: true;
40
+ readonly schema: Schema.Schema<A, any, never>;
41
+ readonly from: (state: any) => A;
42
+ }
43
+ /** What `derived` is given: `derive(type, (state) => value)`. The state is the application's, as a command sees it. */
44
+ export type DeriveMaker<S> = <A, I = A>(schema: Schema.Schema<A, I, never>, from: (state: S) => A) => Derived<A>;
24
45
  export declare const field: <A, I = A>(schema: Schema.Schema<A, I, never>, initial: NoInfer<A>) => Field<A>;
25
46
  /** Fields declared one by one. */
26
47
  export type Fields = Readonly<Record<string, Field<any>>>;
@@ -49,27 +70,105 @@ export interface Component {
49
70
  readonly [COMPONENT]: true;
50
71
  readonly template: Template;
51
72
  /** Present when the component declares its props (an omitted side of a declared interface is none). Absent: the contract is inferred from the template and its uses. */
52
- readonly props?: Readonly<Record<string, Schema.Schema<any, any, never>>>;
73
+ readonly props?: Readonly<Record<string, PropDeclaration>>;
53
74
  readonly events?: Readonly<Record<string, Schema.Schema<any, any, never>>>;
75
+ /** The prop that is the component's accessible name, when its template passes the name of whoever uses it through to a control (see `Contract.name`). */
76
+ readonly name?: string;
77
+ /** The state and commands of one occurrence of the component, when it has any (see `unit`). */
78
+ readonly unit?: Valance.Unit;
54
79
  }
80
+ export declare const SNIPPET: unique symbol;
81
+ /** A prop that is a piece of template: what an occurrence writes between its tags (`children`), or as a `{#snippet}` of that name among them. */
82
+ export interface SnippetDeclaration {
83
+ readonly [SNIPPET]: true;
84
+ /** The types of the snippet's parameters, in order; `{@render name(a, b)}` passes exactly these. */
85
+ readonly params: ReadonlyArray<Schema.Schema<any, any, never>>;
86
+ /** Whether an occurrence may leave it out. Rendering it then needs `{#if has(name)}` around the render (`possibly-absent-snippet`). */
87
+ readonly isOptional: boolean;
88
+ /** The same snippet, which an occurrence may leave out: `snippet(Item).optional()`. */
89
+ optional(): SnippetDeclaration;
90
+ }
91
+ /**
92
+ * Declares a prop whose value is a snippet, with the types of its parameters: `children: snippet()` is the content written between the component's tags; `row: snippet(Item, Schema.Number)` is a
93
+ * `{#snippet row(item, index)}` an occurrence passes and the template renders with `{@render row(item, index)}`. The prop named `children` is reserved for the content between the tags and takes no
94
+ * parameters. A snippet is not data: it has no default, is not a field of a record or an event's payload, and is declared on a component that has a template. `snippet(...).optional()` is one an occurrence may leave out (an absent one draws nothing), and `children: snippet().optional()` is content an occurrence may leave out.
95
+ */
96
+ export declare const snippet: (...params: ReadonlyArray<Schema.Schema<any, any, never>>) => SnippetDeclaration;
97
+ /** Whether a declaration is a snippet's. */
98
+ export declare const isSnippet: (value: unknown) => value is SnippetDeclaration;
99
+ /** A prop is declared by its schema, by `field(schema, default)` when an occurrence that leaves it out should get a value, or by `snippet(…)` when it is a piece of template. */
100
+ export type PropDeclaration = Schema.Schema<any, any, never> | Field<any> | SnippetDeclaration;
101
+ /** Whether a prop is declared with a default. */
102
+ export declare const isFormField: (value: unknown) => boolean;
103
+ export declare const isField: (value: unknown) => value is Field;
55
104
  export interface Contract<P extends string, E extends string> {
56
105
  /**
57
- * The inputs the template reads, each with its type. Optional when the schema admits `undefined`. A prop has no default: the model has none. With neither `props` nor `events` the contract is
58
- * inferred; with either, the interface is exactly what is written (an omitted side is none).
106
+ * The inputs the template reads, each with its type. Optional when the schema admits `undefined`. `field(schema, default)` gives a prop a default: an occurrence of the component that leaves
107
+ * the prop out gets the default (for a component with a template; a primitive ignores it), and the prop may then be left out. A string-literal union (`Schema.Literal("a", "b")`) is a closed set:
108
+ * the model checks every literal written for it. With neither `props` nor `events` the contract is inferred; with either, the interface is exactly what is written (an omitted side is none).
59
109
  */
60
110
  readonly props?: {
61
- readonly [K in P]: Schema.Schema<any, any, never>;
111
+ readonly [K in P]: PropDeclaration;
62
112
  };
63
113
  /** The events the template raises, each with its payload's schema; `Schema.Void` is no payload. */
64
114
  readonly events?: {
65
115
  readonly [K in E]: Schema.Schema<any, any, never>;
66
116
  };
117
+ /**
118
+ * The prop that is the component's accessible name: a component whose template draws a control and takes its name from whoever uses it (a labelled checkbox of the design system) says which
119
+ * prop that is, and the toolchain holds both sides to it. The prop must be a string every use writes (the model reports a use that omits it, where it is written), and the template must read it;
120
+ * a control in the template that has no name of its own (no `label`, not inside a `<label>`) is then not reported by `unnamed-control`, because the name arrives from the use.
121
+ */
122
+ readonly name?: P;
123
+ /**
124
+ * State that belongs to one occurrence of the component, such as whether a disclosure is open or which tab is chosen: `unit({ state, commands })`. The template reads its fields and runs its commands
125
+ * by name, like any others; the application never sees them. A component with a unit states its interface (`props` and `events`, empty if it has none).
126
+ */
127
+ readonly unit?: Valance.Unit;
67
128
  }
68
129
  /**
69
130
  * Defines a component: its template and an optional contract. Register it once, in `defineApp`'s `components`; the templates that write its tag use it, and nothing about its interface is
70
131
  * written again there. Where the toolchain has generated the template's type, a contract that disagrees with the template's keys is a compile-time error.
71
132
  */
72
133
  export declare function component<P extends string = string, E extends string = string>(template: Template<P, E>, contract?: Contract<NoInfer<P>, NoInfer<E>>): Component;
134
+ /** What a unit's commands change: some of its fields. */
135
+ type Changes<S> = Partial<S>;
136
+ /** One command of a unit: a function of the state as the person saw it that returns the fields it changes, or the same with parameters and the events it announces. */
137
+ export type UnitCommandSpec<S, P extends Schema.Struct.Fields = Schema.Struct.Fields> = ((state: S) => Changes<S>) | {
138
+ /** The command's parameters, in order: `{ n: Schema.Number }`. Each is a parameter of the command in the model. */
139
+ readonly params?: P;
140
+ readonly run: (state: S, args: Schema.Struct.Type<P>) => Changes<S>;
141
+ /** Events of the component the command announces to its caller, each with the payload it carries, worked out from the state the command leaves: `{ change: (next) => next.at }`. */
142
+ readonly raises?: Readonly<Record<string, (next: S, args: Schema.Struct.Type<P>) => unknown>>;
143
+ };
144
+ /** A command with parameters, typed: the parameters' types reach `run` and `raises`. */
145
+ export declare const unitCommand: <S, P extends Schema.Struct.Fields>(spec: {
146
+ readonly params: P;
147
+ readonly run: (state: S, args: Schema.Struct.Type<P>) => Changes<S>;
148
+ readonly raises?: Readonly<Record<string, (next: S, args: Schema.Struct.Type<P>) => unknown>>;
149
+ }) => UnitCommandSpec<S, P>;
150
+ /**
151
+ * Declares the state and commands of one occurrence of a component (Tier 2; a component with no unit needs none of it). The state is typed with defaults, as application state is; a command is a
152
+ * pure function of the state as the person saw it that returns the fields it changes:
153
+ *
154
+ * export default component(template, {
155
+ * props: { label: Schema.String },
156
+ * events: {},
157
+ * unit: unit({ state: { open: field(Schema.Boolean, false) }, commands: { toggle: (state) => ({ open: !state.open }) } }),
158
+ * });
159
+ *
160
+ * The template reads `open` and runs `toggle()`; the application holds neither. The state belongs to the occurrence: it follows its row when a keyed list is re-sorted, and starts again from the defaults
161
+ * when the occurrence leaves the page and returns. State that must outlive that (or that another part of the application reads) is application state, not a unit. A field named in `controllable` is
162
+ * owned by the caller when it writes a prop of the same name (the prop must be declared, optional and of the same type): the prop is then the value, the unit's change to it is not applied, and
163
+ * `raises` tells the caller what the person chose.
164
+ */
165
+ export declare const unit: <F extends Fields>(definition: {
166
+ readonly state: F;
167
+ readonly commands: {
168
+ readonly [name: string]: UnitCommandSpec<StateOf<F>, any>;
169
+ };
170
+ readonly controllable?: ReadonlyArray<Extract<keyof F, string>>;
171
+ }) => Valance.Unit<StateOf<F>>;
73
172
  /**
74
173
  * What the document's title is made of. `page` is the page's own part (default: its title, from the Markdown front matter), `app` the application's name (default: the site's name), `status` what is
75
174
  * going on right now and is not part of any page (an unread count, "Saving…"): it is written first, in parentheses. The project's format in \`valance.json\` (`"title": "{page} · {site}"`) puts them
@@ -81,7 +180,22 @@ export interface TitleParts {
81
180
  readonly app?: string;
82
181
  readonly status?: string;
83
182
  }
183
+ /** What differs between targets: the templates. Everything else in the definition (state, commands, title, http) is the application's, and the same on every target. */
184
+ export interface TargetParts {
185
+ readonly layout?: Component;
186
+ readonly components?: ReadonlyArray<Component>;
187
+ readonly override?: ReadonlyArray<Component>;
188
+ }
84
189
  export interface AppDefinition<S extends StateInput = {}> {
190
+ /**
191
+ * The templates of one target, for a project with more than one (`valance.web.json` and `valance.desktop.json`): `targets: { web: { layout, components }, desktop: { layout } }`. A target's
192
+ * `layout` replaces the top-level one, and its `components` and `override` are added to it. A target's templates are compiled against that target's vocabulary only; the state, the commands and
193
+ * the content are the one application's.
194
+ */
195
+ readonly targets?: {
196
+ readonly web?: TargetParts;
197
+ readonly desktop?: TargetParts;
198
+ };
85
199
  /** The page's shell. Omit it to use the built-in layout. Its template must be named `layout.mprx`, and it has no props or events: its inputs are the state and the commands. */
86
200
  readonly layout?: Component;
87
201
  /** Every component of the application besides the layout and the overrides. A template uses one by writing its tag; one that no template writes yet is not part of the program. */
@@ -109,13 +223,62 @@ export interface AppDefinition<S extends StateInput = {}> {
109
223
  readonly http?: {
110
224
  readonly interceptors?: ReadonlyArray<Interceptor>;
111
225
  };
226
+ /**
227
+ * The appearance this application asks of its window, for a state: `appearance: (state) => state.theme` (`"light"`, `"dark"` or `"system"`). A page draws its own colors, so it is the desktop
228
+ * window's: the window follows the system's theme unless this says otherwise. Absent: the system's.
229
+ */
230
+ readonly appearance?: (state: PageState & StateOf<S>) => Appearance;
231
+ /**
232
+ * The state fields that outlive a run: `persist: ["items", "theme"]`. They are loaded when the application starts and saved when they change, through the platform's `Store` (the page's
233
+ * storage on the web, a file in the user's data directory on the desktop), under the application's name. What was stored and no longer fits a field's type is ignored, not loaded. Where a
234
+ * platform has no store the application runs as it would without this. A field not listed is not kept (a `navigation` flag on one target should not follow the person to another).
235
+ */
236
+ readonly persist?: ReadonlyArray<Extract<keyof StateOf<S>, string> | "navigation">;
237
+ /**
238
+ * A guard for moving between pages by `go`, `back` and `forward`: the router's `Guard`, asked before the move. It is told where the application is going from and to (a route named for the
239
+ * page, with no parameters), the state, and why: `kind` is `"programmatic"` for `go` and `"history"` for `back` and `forward`. `true` lets it through, `false` stops it (nothing changes),
240
+ * a `redirect(...)` sends it to another page. A guard that throws or fails stops the move. Without one every move is allowed. A page's own Back and Forward are the browser's: the router
241
+ * guards those as `"popstate"`.
242
+ */
243
+ readonly guard?: NavigationGuard;
244
+ /**
245
+ * Values worked out from the state, for a template to read: `derived: (derive) => ({ open: derive(Schema.Number, (state) => state.items.filter((item) => !item.done).length) })`. Each has its
246
+ * type, as a state field does. They are computed from the state each time it changes, on every target, and a command cannot change one (change the state they come from). The function must
247
+ * be pure and must not throw.
248
+ */
249
+ readonly derived?: (derive: DeriveMaker<PageState & StateOf<S>>) => {
250
+ readonly [name: string]: Derived<any>;
251
+ };
112
252
  /** The commands a layout can run. `command` makes one: `command((state) => nextState)` or `command(InputSchema, (input, state) => nextState)`. */
113
253
  readonly commands?: (command: Valance.CommandMaker<PageState & StateOf<S>>) => Valance.CommandTable<any, Valance.Ambient>;
114
254
  }
255
+ /** What a navigation guard is told; the router's `GuardContext`, spelled out so the author's entry needs nothing but VALANCE and effect. */
256
+ export interface NavigationContext {
257
+ readonly from: {
258
+ readonly name: string;
259
+ readonly params: Readonly<Record<string, unknown>>;
260
+ readonly query: Readonly<Record<string, unknown>>;
261
+ } | undefined;
262
+ readonly to: {
263
+ readonly name: string;
264
+ readonly params: Readonly<Record<string, unknown>>;
265
+ readonly query: Readonly<Record<string, unknown>>;
266
+ };
267
+ readonly state: unknown;
268
+ readonly kind: "link" | "popstate" | "programmatic" | "history" | "initial" | "close";
269
+ readonly data: Readonly<Record<string, unknown>>;
270
+ }
271
+ /** `true` allows, `false` stops, and `{ _tag: "Redirect", name }` (the router's `redirect(page)`) goes to another page; it may answer now, later or as an Effect. */
272
+ export type NavigationGuard = (context: NavigationContext) => unknown;
115
273
  /** The brand that says a value came from `defineApp`. */
116
274
  export declare const APP: unique symbol;
117
275
  export interface DefinedApp extends AppDefinition<any> {
118
276
  readonly [APP]: true;
119
277
  }
120
278
  /** Declares the application's component tree, state and commands. It starts nothing; the framework runs it. */
279
+ /** What `derived` makes, for the toolchain: the function is called with the one `derive`. */
280
+ export declare const derivedOf: (author: {
281
+ readonly derived?: AppDefinition<any>["derived"];
282
+ } | undefined) => Readonly<Record<string, Derived<any>>>;
121
283
  export declare const defineApp: <S extends StateInput = {}>(definition: AppDefinition<S>) => DefinedApp;
284
+ export * as Form from "./form.js";
package/dist/index.js CHANGED
@@ -1,11 +1,35 @@
1
+ import { UNIT } from "@valancex/valance";
2
+ import { Schema } from "effect";
1
3
  // ---- state -------------------------------------------------------------------------------------------------------------------------------------------------
2
4
  export const FIELD = Symbol.for("valancex.common.field");
5
+ /** The brand of a state field that holds a form (`Form.field()`): the application then takes the person to the first field in error after a failed submit, with nothing written. */
6
+ export const FORM = Symbol.for("valancex.common.form");
3
7
  /**
4
8
  * Declares one field of the application's state with its type and its default in one place. The framework builds the state's schema and its initial value from the fields; the default is
5
9
  * checked against the type (by TypeScript, and again when the application is built).
6
10
  */
11
+ export const DERIVED = Symbol.for("valancex.common.derived");
12
+ const derive = (schema, from) => ({ [DERIVED]: true, schema: schema, from });
7
13
  export const field = (schema, initial) => ({ [FIELD]: true, schema: schema, initial });
8
14
  export const COMPONENT = Symbol.for("valancex.common.component");
15
+ export const SNIPPET = Symbol.for("valancex.common.snippet");
16
+ /**
17
+ * Declares a prop whose value is a snippet, with the types of its parameters: `children: snippet()` is the content written between the component's tags; `row: snippet(Item, Schema.Number)` is a
18
+ * `{#snippet row(item, index)}` an occurrence passes and the template renders with `{@render row(item, index)}`. The prop named `children` is reserved for the content between the tags and takes no
19
+ * parameters. A snippet is not data: it has no default, is not a field of a record or an event's payload, and is declared on a component that has a template. `snippet(...).optional()` is one an occurrence may leave out (an absent one draws nothing), and `children: snippet().optional()` is content an occurrence may leave out.
20
+ */
21
+ export const snippet = (...params) => declare(params, false);
22
+ const declare = (params, isOptional) => ({
23
+ [SNIPPET]: true,
24
+ params,
25
+ isOptional,
26
+ optional: () => declare(params, true),
27
+ });
28
+ /** Whether a declaration is a snippet's. */
29
+ export const isSnippet = (value) => typeof value === "object" && value !== null && SNIPPET in value;
30
+ /** Whether a prop is declared with a default. */
31
+ export const isFormField = (value) => typeof value === "object" && value !== null && FORM in value;
32
+ export const isField = (value) => typeof value === "object" && value !== null && FIELD in value;
9
33
  /**
10
34
  * Defines a component: its template and an optional contract. Register it once, in `defineApp`'s `components`; the templates that write its tag use it, and nothing about its interface is
11
35
  * written again there. Where the toolchain has generated the template's type, a contract that disagrees with the template's keys is a compile-time error.
@@ -16,9 +40,53 @@ export function component(template, contract = {}) {
16
40
  template,
17
41
  ...(contract.props === undefined ? {} : { props: contract.props }),
18
42
  ...(contract.events === undefined ? {} : { events: contract.events }),
43
+ ...(contract.name === undefined ? {} : { name: contract.name }),
44
+ ...(contract.unit === undefined ? {} : { unit: contract.unit }),
19
45
  };
20
46
  }
47
+ /** A command with parameters, typed: the parameters' types reach `run` and `raises`. */
48
+ export const unitCommand = (spec) => spec;
49
+ /**
50
+ * Declares the state and commands of one occurrence of a component (Tier 2; a component with no unit needs none of it). The state is typed with defaults, as application state is; a command is a
51
+ * pure function of the state as the person saw it that returns the fields it changes:
52
+ *
53
+ * export default component(template, {
54
+ * props: { label: Schema.String },
55
+ * events: {},
56
+ * unit: unit({ state: { open: field(Schema.Boolean, false) }, commands: { toggle: (state) => ({ open: !state.open }) } }),
57
+ * });
58
+ *
59
+ * The template reads `open` and runs `toggle()`; the application holds neither. The state belongs to the occurrence: it follows its row when a keyed list is re-sorted, and starts again from the defaults
60
+ * when the occurrence leaves the page and returns. State that must outlive that (or that another part of the application reads) is application state, not a unit. A field named in `controllable` is
61
+ * owned by the caller when it writes a prop of the same name (the prop must be declared, optional and of the same type): the prop is then the value, the unit's change to it is not applied, and
62
+ * `raises` tells the caller what the person chose.
63
+ */
64
+ export const unit = (definition) => {
65
+ const entries = Object.entries(definition.state);
66
+ const fields = Object.fromEntries(entries.map(([name, each]) => [name, { schema: each.schema, initial: each.initial }]));
67
+ const schema = Schema.Struct(Object.fromEntries(entries.map(([name, each]) => [name, each.schema])));
68
+ return {
69
+ [UNIT]: true,
70
+ schema: schema,
71
+ fields,
72
+ controllable: definition.controllable ?? [],
73
+ commands: Object.fromEntries(Object.entries(definition.commands).map(([name, command]) => [
74
+ name,
75
+ typeof command === "function"
76
+ ? { run: command }
77
+ : {
78
+ ...(command.params === undefined ? {} : { input: Schema.Struct(command.params) }),
79
+ run: command.run,
80
+ ...(command.raises === undefined ? {} : { raises: command.raises }),
81
+ },
82
+ ])),
83
+ };
84
+ };
21
85
  /** The brand that says a value came from `defineApp`. */
22
86
  export const APP = Symbol.for("valancex.common");
23
87
  /** Declares the application's component tree, state and commands. It starts nothing; the framework runs it. */
88
+ /** What `derived` makes, for the toolchain: the function is called with the one `derive`. */
89
+ export const derivedOf = (author) => author?.derived?.(derive) ?? {};
24
90
  export const defineApp = (definition) => ({ ...definition, [APP]: true });
91
+ // ---- forms ---------------------------------------------------------------------------------------------------------------------------------------------------
92
+ export * as Form from "./form.js";
@@ -1,7 +1,14 @@
1
1
  export * from "./app.js";
2
+ export * from "./web-app.js";
2
3
  export * from "./model.js";
3
4
  export * from "./primitives.js";
4
5
  export * from "./routes.js";
5
6
  export * from "./scope.js";
6
7
  export * from "./state.js";
7
8
  export * from "./vocabulary.js";
9
+ export * from "./targets.js";
10
+ export * from "./store.js";
11
+ export * from "./open.js";
12
+ export * from "./navigation.js";
13
+ export * from "./platform.js";
14
+ export { earlyHead, earlyShell, earlyShellLimit } from "./early.js";
package/dist/internal.js CHANGED
@@ -1,9 +1,16 @@
1
1
  // What the build tools share with the runtime: the content model, the tag table, the scope a layout reads, the state and the application they make. `@valancex/cli` builds with these and the generated
2
2
  // entries run them, so there is one definition of each. Not for applications: an application's code is `@valancex/common` (the entry point).
3
3
  export * from "./app.js";
4
+ export * from "./web-app.js";
4
5
  export * from "./model.js";
5
6
  export * from "./primitives.js";
6
7
  export * from "./routes.js";
7
8
  export * from "./scope.js";
8
9
  export * from "./state.js";
9
10
  export * from "./vocabulary.js";
11
+ export * from "./targets.js";
12
+ export * from "./store.js";
13
+ export * from "./open.js";
14
+ export * from "./navigation.js";
15
+ export * from "./platform.js";
16
+ export { earlyHead, earlyShell, earlyShellLimit } from "./early.js";
@@ -0,0 +1,6 @@
1
+ import * as Nexus from "@valancex/nexus";
2
+ /** The platform has no history of its own: the engine keeps the list (`trail` and `cursor`). */
3
+ export interface NavigationOwner {
4
+ readonly owner: "engine";
5
+ }
6
+ export declare const Navigation: Nexus.Capability.Capability<NavigationOwner>;
@@ -0,0 +1,5 @@
1
+ // Who keeps the list of places the person has been (docs/application-model/NAVIGATION.md). A page has the browser's: its session history holds the list, restores it, and moves along it with the
2
+ // person's Back and Forward. A window has none, so the engine keeps the list in its state; a platform that supplies this capability is one of those, and only there does `persist: ["navigation"]`
3
+ // do anything (restoring a stored page on the web would argue with the address, which is the truth there).
4
+ import * as Nexus from "@valancex/nexus";
5
+ export const Navigation = Nexus.Capability.define("valance/navigation");
@@ -0,0 +1,12 @@
1
+ import type { OpenClient } from "./open.js";
2
+ /** The program and arguments that open `address` on `platform`. */
3
+ export declare const openerFor: (address: string, platform?: NodeJS.Platform) => {
4
+ readonly program: string;
5
+ readonly args: ReadonlyArray<string>;
6
+ };
7
+ type Spawn = (program: string, args: ReadonlyArray<string>) => {
8
+ readonly on: (event: "error", listener: (error: Error) => void) => unknown;
9
+ readonly unref: () => void;
10
+ };
11
+ export declare const systemOpen: (spawn?: Spawn, platform?: NodeJS.Platform) => OpenClient;
12
+ export {};
@@ -0,0 +1,31 @@
1
+ // The desktop's opener: the system's own, given the address as an argument (never through a shell, so the address is data and nothing else). Node only.
2
+ import { Effect } from "effect";
3
+ import { spawn as nodeSpawn } from "node:child_process";
4
+ import { OpenError, refusal } from "./open.js";
5
+ /** The program and arguments that open `address` on `platform`. */
6
+ export const openerFor = (address, platform = process.platform) => platform === "win32"
7
+ ? { program: "rundll32", args: ["url.dll,FileProtocolHandler", address] }
8
+ : platform === "darwin"
9
+ ? { program: "open", args: [address] }
10
+ : { program: "xdg-open", args: [address] };
11
+ export const systemOpen = (spawn = (program, args) => nodeSpawn(program, [...args], { detached: true, stdio: "ignore" }), platform = process.platform) => ({
12
+ open: (address) => {
13
+ const reason = refusal(address);
14
+ if (reason !== undefined) {
15
+ return Effect.fail(new OpenError("Refused", reason));
16
+ }
17
+ const { program, args } = openerFor(address, platform);
18
+ return Effect.async((resume) => {
19
+ try {
20
+ const child = spawn(program, args);
21
+ child.on("error", () => resume(Effect.fail(new OpenError("Unavailable", `${program} could not be started`))));
22
+ child.unref();
23
+ // handed over: the opener is the system's, and does not report back to a program that asked
24
+ setTimeout(() => resume(Effect.void), 0);
25
+ }
26
+ catch {
27
+ resume(Effect.fail(new OpenError("Unavailable", `${program} could not be started`)));
28
+ }
29
+ });
30
+ },
31
+ });
package/dist/open.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import * as Nexus from "@valancex/nexus";
2
+ import { Effect } from "effect";
3
+ export declare class OpenError extends Error {
4
+ readonly kind: "Refused" | "Unavailable";
5
+ readonly _tag = "OpenError";
6
+ constructor(kind: "Refused" | "Unavailable", message: string);
7
+ }
8
+ export interface OpenClient {
9
+ /** Opens `address` in the program the platform keeps for it. Resolves when it was handed over, not when anything finished. */
10
+ readonly open: (address: string) => Effect.Effect<void, OpenError>;
11
+ }
12
+ /** The capability: ask for it in a command (`Open.open`), and the platform says whether it has one. */
13
+ export declare const Open: Nexus.Capability.Capability<OpenClient>;
14
+ /** The schemes an address may have to be opened. */
15
+ export declare const OPENABLE: readonly ["https:", "http:", "mailto:"];
16
+ /** Why `address` may not be opened, or `undefined` when it may: an absolute address of an openable scheme, without a control character, of a sensible length. */
17
+ export declare const refusal: (address: string) => string | undefined;
18
+ /** Opens `address` through the platform's opener. Requires the `Open` capability. */
19
+ export declare const open: (address: string) => Effect.Effect<void, OpenError | Nexus.Capability.CapabilityUnavailableError, Nexus.Capability.EnvironmentShape>;
20
+ /** The page's opener: a new tab, with no link back to this page. */
21
+ export declare const webOpen: (win?: {
22
+ readonly open: (url: string, target: string, features: string) => unknown;
23
+ }) => OpenClient;
package/dist/open.js ADDED
@@ -0,0 +1,47 @@
1
+ // Sending a person to an address outside the application: a web page in their browser, a mail in their mail program. NEXUS and VALANCE hold no such thing (their rule): it is a capability the
2
+ // platform supplies. The web's is a new tab; the desktop's is the system's own opener (`./open-system`). Only the addresses a person expects a link to open are opened: a script, a file or a
3
+ // program's own scheme is refused, whatever text asked.
4
+ import * as Nexus from "@valancex/nexus";
5
+ import { Effect } from "effect";
6
+ export class OpenError extends Error {
7
+ kind;
8
+ _tag = "OpenError";
9
+ constructor(kind, message) {
10
+ super(message);
11
+ this.kind = kind;
12
+ this.name = "OpenError";
13
+ }
14
+ }
15
+ /** The capability: ask for it in a command (`Open.open`), and the platform says whether it has one. */
16
+ export const Open = Nexus.Capability.define("valance/open");
17
+ /** The schemes an address may have to be opened. */
18
+ export const OPENABLE = ["https:", "http:", "mailto:"];
19
+ /** Why `address` may not be opened, or `undefined` when it may: an absolute address of an openable scheme, without a control character, of a sensible length. */
20
+ export const refusal = (address) => {
21
+ // oxlint-disable-next-line no-control-regex -- a control character in an address is how one is smuggled into a command line
22
+ if (address.length === 0 || address.length > 2048 || /[\u0000-\u001f\u007f\s]/.test(address)) {
23
+ return "an address is one line of text, at most 2048 characters";
24
+ }
25
+ try {
26
+ const url = new URL(address);
27
+ return OPENABLE.includes(url.protocol) ? undefined : `a ${url.protocol} address is not opened (only ${OPENABLE.join(", ")})`;
28
+ }
29
+ catch {
30
+ return "not an absolute address";
31
+ }
32
+ };
33
+ /** Opens `address` through the platform's opener. Requires the `Open` capability. */
34
+ export const open = (address) => Effect.flatMap(Nexus.Capability.require(Open), (client) => client.open(address));
35
+ /** The page's opener: a new tab, with no link back to this page. */
36
+ export const webOpen = (win) => ({
37
+ open: (address) => {
38
+ const reason = refusal(address);
39
+ if (reason !== undefined) {
40
+ return Effect.fail(new OpenError("Refused", reason));
41
+ }
42
+ const target = win ?? globalThis.window;
43
+ return target === undefined
44
+ ? Effect.fail(new OpenError("Unavailable", "this page cannot open a tab"))
45
+ : Effect.try({ try: () => void target.open(address, "_blank", "noopener,noreferrer"), catch: () => new OpenError("Unavailable", "the page's tab was blocked") });
46
+ },
47
+ });
@@ -0,0 +1,5 @@
1
+ import * as Nexus from "@valancex/nexus";
2
+ /** A platform that supplies each `[capability, implementation]` given. */
3
+ export declare const platformOf: (capabilities: ReadonlyArray<readonly [{
4
+ readonly id: string;
5
+ }, unknown]>) => Nexus.Application.Platform;
@@ -0,0 +1,4 @@
1
+ // What a platform supplies the application: its capabilities, by contract. `Web.run` and `Native.run` each take one; the web's is `webPlatform` (`./http`), the desktop's is made in `./desktop`.
2
+ import * as Nexus from "@valancex/nexus";
3
+ /** A platform that supplies each `[capability, implementation]` given. */
4
+ export const platformOf = (capabilities) => Nexus.Capability.EnvironmentLive(new Map(capabilities.map(([capability, implementation]) => [capability.id, { _tag: "Available", implementation }])));
@@ -1,2 +1,23 @@
1
1
  import * as Web from "@valancex/valance/web";
2
2
  export declare const primitives: Web.WebPrimitives;
3
+ /** What each tag's props and events are, as the specification states them (the derivation from the table alone cannot say whether a prop is required, nor type one realized as text). */
4
+ export declare const declared: Readonly<Record<string, {
5
+ readonly props: Readonly<Record<string, {
6
+ readonly type: Readonly<Record<string, unknown>>;
7
+ readonly required: boolean;
8
+ }>>;
9
+ readonly events: Readonly<Record<string, {
10
+ readonly payload?: Readonly<Record<string, unknown>>;
11
+ }>>;
12
+ }>>;
13
+ /** The tags that were renamed (decision P7), old to new: a template that writes the old one is told the new. */
14
+ export declare const renamed: Readonly<Record<string, string>>;
15
+ /** The props that were renamed (decision I29), by tag, old to new: a template that writes the old one is told the new. */
16
+ export declare const renamedProps: Readonly<Record<string, Readonly<Record<string, string>>>>;
17
+ /** What a use of a tag must satisfy besides its props (`vlx` checks it): the tags that need a name (a `label` prop, or a `label` around them), the tags a tag may contain only, and the tags a tag must be inside. */
18
+ export declare const rules: {
19
+ readonly named: ReadonlyArray<string>;
20
+ readonly modal: ReadonlyArray<string>;
21
+ readonly only: Readonly<Record<string, ReadonlyArray<string>>>;
22
+ readonly within: Readonly<Record<string, ReadonlyArray<string>>>;
23
+ };