lecodes-sdk 2.0.7 → 2.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/global.d.ts +6 -0
  2. package/dist/types/g2/Scene2D.d.ts +4 -0
  3. package/dist/types/gl/Light.d.ts +36 -1
  4. package/dist/types/gl/Scene.d.ts +4 -0
  5. package/dist/types/inject.d.ts +2 -1
  6. package/dist/types/runtime/rpc.d.ts +2 -0
  7. package/dist/types/server/context.d.ts +4 -0
  8. package/dist/types/server/db/defineDb.d.ts +1 -1
  9. package/dist/types/server/db/fields.d.ts +25 -1
  10. package/dist/types/server/db/marci/query.d.ts +39 -2
  11. package/dist/types/server/db/types.d.ts +13 -7
  12. package/dist/types/server/files/db.d.ts +39 -0
  13. package/dist/types/server/files/models.d.ts +131 -0
  14. package/dist/types/server/inject.d.ts +1 -0
  15. package/dist/types/ui/NativeView.d.ts +3 -0
  16. package/dist/types/ui/UI.d.ts +1 -0
  17. package/dist/types/ui/UILayer.d.ts +25 -0
  18. package/dist/types/ui/UIModal.d.ts +20 -16
  19. package/dist/types/ui/UIPopover.d.ts +5 -3
  20. package/dist/types/ui/UIVideo.d.ts +3 -0
  21. package/dist/types/ui/UIWidget.d.ts +25 -24
  22. package/dist/types/ui/presentable.d.ts +10 -0
  23. package/dist/types/ui/transitions.d.ts +2 -1
  24. package/dist/types/ui/tree.d.ts +1 -0
  25. package/dist/types/version.d.ts +1 -1
  26. package/dist/types.json +1 -1
  27. package/package.json +3 -2
  28. package/prompts/3d-scene-files.md +3 -3
  29. package/prompts/3d-scene.md +1 -1
  30. package/prompts/dist/3d-app.md +4 -4
  31. package/src/bridges/gl.d.ts +5 -0
  32. package/src/bridges/tree.d.ts +24 -12
  33. package/src/chisel.ts +1 -1
  34. package/src/compile/libraryImports.ts +9 -0
  35. package/src/compile/serverTypes.ts +6 -0
  36. package/src/g2/Scene2D.ts +10 -0
  37. package/src/gl/Light.ts +78 -2
  38. package/src/gl/Material.ts +3 -0
  39. package/src/gl/Scene.ts +11 -1
  40. package/src/inject.ts +2 -0
  41. package/src/runtime/rpc.ts +15 -3
  42. package/src/server/context.ts +4 -0
  43. package/src/server/db/defineDb.ts +62 -9
  44. package/src/server/db/fields.ts +21 -1
  45. package/src/server/db/httpTransport.ts +9 -1
  46. package/src/server/db/marci/query.ts +71 -4
  47. package/src/server/db/types.ts +16 -5
  48. package/src/server/files/db.ts +182 -0
  49. package/src/server/files/host.ts +496 -0
  50. package/src/server/files/models.ts +116 -0
  51. package/src/server/host.ts +17 -4
  52. package/src/server/inject.ts +1 -0
  53. package/src/server/runtime.ts +41 -0
  54. package/src/server/validate.ts +5 -0
  55. package/src/ui/NativeView.ts +11 -0
  56. package/src/ui/UI.ts +1 -0
  57. package/src/ui/UIBottomSheet.ts +8 -7
  58. package/src/ui/UILayer.ts +88 -0
  59. package/src/ui/UIModal.ts +59 -43
  60. package/src/ui/UINode.ts +9 -1
  61. package/src/ui/UIPopover.ts +23 -9
  62. package/src/ui/UIVideo.ts +11 -0
  63. package/src/ui/UIWidget.ts +70 -44
  64. package/src/ui/presentable.ts +11 -1
  65. package/src/ui/transitions.ts +36 -7
  66. package/src/ui/tree.ts +4 -0
  67. package/src/version.ts +1 -1
  68. package/tests/helpers/fakeTree.ts +7 -3
package/dist/global.d.ts CHANGED
@@ -124,6 +124,7 @@ declare global {
124
124
  const UIImage: typeof SDK.UIImage
125
125
  const UIInput: typeof SDK.UIInput
126
126
  const UIModal: typeof SDK.UIModal
127
+ const UIOverlay: typeof SDK.UIOverlay
127
128
  const UIPager: typeof SDK.UIPager
128
129
  const UIPopover: typeof SDK.UIPopover
129
130
  const UIRow: typeof SDK.UIRow
@@ -480,6 +481,8 @@ declare global {
480
481
  type StepHandler = SDK.StepHandler
481
482
  type StepWarpOptions = SDK.StepWarpOptions
482
483
  type StopOptions = SDK.StopOptions
484
+ type StoredFile = SDK.StoredFile
485
+ type StoredImage = SDK.StoredImage
483
486
  type System<K extends string, S extends SystemHost = Scene, E extends EventMap = {}> = SDK.System<K, S, E>
484
487
  type SystemHost = SDK.SystemHost
485
488
  type TabDef = SDK.TabDef
@@ -554,6 +557,7 @@ declare global {
554
557
  type UIVideoStyle = SDK.UIVideoStyle
555
558
  type UIVirtualizedList<T = unknown> = SDK.UIVirtualizedList<T>
556
559
  type UIWidget = SDK.UIWidget
560
+ type UIWidgetContent = SDK.UIWidgetContent
557
561
  type UIWidgetStyle = SDK.UIWidgetStyle
558
562
  type Unit = SDK.Unit
559
563
  type UnlitMaterialDef = SDK.UnlitMaterialDef
@@ -580,6 +584,8 @@ declare global {
580
584
  const request: typeof SERVER.request
581
585
  const t: typeof SERVER.t
582
586
  type ApiError = SERVER.ApiError
587
+ type StoredFile = SERVER.StoredFile
588
+ type StoredImage = SERVER.StoredImage
583
589
  }
584
590
 
585
591
  export {}
@@ -1,6 +1,7 @@
1
1
  import { type ColorInput } from "../core/color";
2
2
  import { type Aspect, type AspectCtor, type FieldOf, type TargetOf } from "../core/Aspect";
3
3
  import { Presentable, type DismissOptions, type PresentOptions } from "../ui/presentable";
4
+ import { type UIWidgetContent } from "../ui/UILayer";
4
5
  import { type Vec2Like } from "../math/vec";
5
6
  import type { ClickEvent, TouchStartEvent } from "../runtime/touch";
6
7
  import { Camera2D } from "./Camera2D";
@@ -67,6 +68,9 @@ export declare class Scene2D implements Presentable {
67
68
  /** @deprecated Renamed `onBack` (2026-09-26). An alias for the projects compiled before the
68
69
  * rename; removed with the release that recompiles them. */
69
70
  onBackPressed(callback: () => void): this;
71
+ /** The UI over the scene — widgets (a HUD, a dialog), laid out over the scene while it is
72
+ * presented; touches outside a widget's box reach the scene. */
73
+ setContent(content: UIWidgetContent): this;
70
74
  /** Make this the active scene — shows it as the current destination (replaces a screen /
71
75
  * another scene; only the active scene renders). */
72
76
  open(options?: PresentOptions): this;
@@ -73,6 +73,23 @@ export type PointOptions = {
73
73
  */
74
74
  bakeArea?: readonly [number, number];
75
75
  };
76
+ export type SpotOptions = {
77
+ /**
78
+ * Luminous power in lumens, as a point light's. The cone does not change the brightness: widen it and the
79
+ * same lumens light a larger patch at the same level. On its axis a spot is 4x brighter than a point light of
80
+ * the same lumens (its light is not spread over the whole sphere). Default 1000.
81
+ */
82
+ intensity?: number;
83
+ color?: ColorInput;
84
+ /** Metres of influence along the cone, the performance knob as for a point light. Default 10. */
85
+ range?: number;
86
+ /** The cone's full angle in degrees, up to 180: past it the light contributes nothing. Default 45. */
87
+ angle?: number;
88
+ /** The full angle in degrees of the cone's fully lit core; the light fades from it out to `angle`. Default 0.75 x `angle`. */
89
+ innerAngle?: number;
90
+ /** A spot's shadow is one 2D shadow map (a point light's is a cubemap); off by default. */
91
+ castShadows?: boolean;
92
+ };
76
93
  export declare class Light extends Node {
77
94
  private _intensity;
78
95
  /** Tween `intensity` / `color` (a flash, a sunrise) and the transform — see {@link Node.animateTo}. */
@@ -94,7 +111,25 @@ export declare class Light extends Node {
94
111
  static point(options?: PointOptions): Light;
95
112
  /** Whether this host can create point lights at all. */
96
113
  static get supportsPoint(): boolean;
97
- /** Live intensity (sun: lux, point: lumens) — animate a flash without rebuilding the light. */
114
+ _innerAngle: number;
115
+ /**
116
+ * A spot light — a flashlight, a headlight, a stage light. It shines along the node's forward (-Z): position
117
+ * it like any node and aim it with `lookAt`. Real time only: the lightmap bake takes no spot lights, so a spot
118
+ * lights the baked statics live too.
119
+ *
120
+ * Feature-detected like `Light.point`: on a host without it the node exists and lights nothing
121
+ * (`Light.supportsSpot`).
122
+ */
123
+ static spot(options?: SpotOptions): Light;
124
+ /** Whether this host can create spot lights. */
125
+ static get supportsSpot(): boolean;
126
+ /** A spot's full cone angle in degrees — live (a flashlight's focus). */
127
+ get angle(): number;
128
+ set angle(value: number);
129
+ /** A spot's fully lit core, full angle in degrees — live. Setting `angle` scales it along. */
130
+ get innerAngle(): number;
131
+ set innerAngle(value: number);
132
+ /** Live intensity (sun: lux, point / spot: lumens) — animate a flash without rebuilding the light. */
98
133
  get intensity(): number;
99
134
  set intensity(value: number);
100
135
  _holdDark(dark: boolean): void;
@@ -1,6 +1,7 @@
1
1
  import { type ColorInput } from "../core/color";
2
2
  import { type Aspect, type AspectCtor, type FieldOf, type TargetOf } from "../core/Aspect";
3
3
  import { Presentable, type DismissOptions, type PresentOptions } from "../ui/presentable";
4
+ import { type UIWidgetContent } from "../ui/UILayer";
4
5
  import type { ClickEvent, TouchStartEvent } from "../runtime/touch";
5
6
  import type { FetchResponse } from "../runtime/fetch";
6
7
  import { Camera } from "./Camera";
@@ -247,6 +248,9 @@ export declare class Scene implements Presentable {
247
248
  /** @deprecated Renamed `onBack` (2026-09-26). An alias for the projects compiled before the
248
249
  * rename; removed with the release that recompiles them. */
249
250
  onBackPressed(callback: () => void): this;
251
+ /** The UI over the scene — widgets (a HUD, a dialog), laid out over the scene while it is
252
+ * presented; touches outside a widget's box reach the scene. `scene.setContent([hud])`. */
253
+ setContent(content: UIWidgetContent): this;
250
254
  /** Make this the active scene — shows it as the current destination (replaces a screen /
251
255
  * another scene; only the active scene renders). The runtime activates the engine as part of
252
256
  * presenting the destination. */
@@ -23,6 +23,7 @@ export { WebSocket } from "./runtime/net";
23
23
  export { Net, NetPlayer, Replicated, NetEntity, type NetMessage, type NetRole, type NetStatus, type NetEvents, type NetLaunch, type NetInput, type NetKind, type NetTransform, type NetCorrection } from "./net";
24
24
  export { __rpc, __channel, __serverOnly, RpcError } from "./runtime/rpc";
25
25
  export type { ChannelSubscription } from "./server/channel";
26
+ export type { StoredFile, StoredImage } from "./server/files/models";
26
27
  export { AudioPlayer, VideoPlayer } from "./runtime/media";
27
28
  export { Sound, type SoundOptions } from "./audio/Sound";
28
29
  export { Voice, type VoiceEvents } from "./audio/Voice";
@@ -54,7 +55,7 @@ export { defineScene2d, Scene2dHandle } from "./g2/defineScene2d";
54
55
  export type { Scene2dDef, Scene2dNodeDef, Scene2dEnv, SpriteSourceDef, TilemapSourceDef, Camera2dNodeDef, LoadedScene2d } from "./g2/defineScene2d";
55
56
  export { cells, encodeCells, type CellsData } from "./g2/cells";
56
57
  export { CameraFollow } from "./g2/scenarios2d";
57
- export { UIScreen, type UIScreenStyle, UIRow, UIColumn, UIBox, type UIContainerStyle, UIText, type UITextStyle, UIButton, type UIButtonStyle, UIImage, type UIImageStyle, UIVideo, type UIVideoStyle, UIInput, UITextArea, type UIInputStyle, UIScrollable, type UIScrollableStyle, UIPager, type UIPagerStyle, UITabs, type UITabDef, defineTabs, type TabDef, type TabsHandle, UIWidget, type UIWidgetStyle, UIModal, type UIModalStyle, UIBottomSheet, type UIBottomSheetStyle, UIPopover, type UIPopoverStyle, UISpacer, type UISpacerStyle, UIVirtualizedList, Router, NativeView, type UINativeViewStyle, registerFont, __uiMap, __UIColumn, __UIRow, __UIBox, __UIButton, __UIScreen, __UIScrollable, __UIWidget, } from "./ui/UI";
58
+ export { UIScreen, type UIScreenStyle, UIRow, UIColumn, UIBox, type UIContainerStyle, UIText, type UITextStyle, UIButton, type UIButtonStyle, UIImage, type UIImageStyle, UIVideo, type UIVideoStyle, UIInput, UITextArea, type UIInputStyle, UIScrollable, type UIScrollableStyle, UIPager, type UIPagerStyle, UITabs, type UITabDef, defineTabs, type TabDef, type TabsHandle, UIWidget, type UIWidgetStyle, UIOverlay, type UIWidgetContent, UIModal, type UIModalStyle, UIBottomSheet, type UIBottomSheetStyle, UIPopover, type UIPopoverStyle, UISpacer, type UISpacerStyle, UIVirtualizedList, Router, NativeView, type UINativeViewStyle, registerFont, __uiMap, __UIColumn, __UIRow, __UIBox, __UIButton, __UIScreen, __UIScrollable, __UIWidget, } from "./ui/UI";
58
59
  export { Presentable } from "./ui/UI";
59
60
  export type { DismissOptions, PresentOptions, Transition, TransitionName, TransitionPose, TransitionSpec } from "./ui/UI";
60
61
  export type { UINode, UINodeChild } from "./ui/UINode";
@@ -9,6 +9,8 @@
9
9
  *
10
10
  * Wire (implemented by the runner):
11
11
  * POST <serverUrl>/api/<id> body {"args":[…]}, `authorization: Bearer <session>` when known
12
+ * — with a `File` among the arguments: multipart, the field `args` is
13
+ * that JSON with `{"$file": <i>}` where the file stood, `file<i>` the files
12
14
  * → 200 {"ok":true,"result":…,"session"?:"…"} | {"ok":false,"status":n,"message":"…","session"?:"…"}
13
15
  * WS <serverUrl>/ws the channel socket — its frames are ./wire.ts
14
16
  *
@@ -14,6 +14,10 @@ export type RequestContext = {
14
14
  sessionToken?: string;
15
15
  /** Set by the auth runtime after resolving the token — read via `db.auth`, not here. */
16
16
  auth?: unknown;
17
+ /** The files a multipart call carried, as the receiving process left them (./files/host.ts `Upload`). */
18
+ uploads?: unknown[];
19
+ /** What `/files/<id>/<name>` is appended to in a stored file's url: the backend's own base, as this request reached it. */
20
+ filesBase?: string;
17
21
  };
18
22
  type Provider = () => RequestContext | undefined;
19
23
  /** Host hook (runner / test harness): install the current-request provider. */
@@ -7,7 +7,7 @@
7
7
  * `defineDb(models, { transport })` or provided by the host through `setDbTransport()` — user code
8
8
  * never sees a connection string.
9
9
  */
10
- import type { FieldDef } from "./fields";
10
+ import { type FieldDef } from "./fields";
11
11
  import { type MarciTransport } from "./marci/query";
12
12
  import type { Db, Fields, Model, Schema, ValidateRefs } from "./types";
13
13
  export declare const model: <F extends Fields>(fields: F) => Model<F>;
@@ -6,8 +6,9 @@
6
6
  * mutated), so `const s = t.string(); { a: s, b: s.optional() }` behaves as expected.
7
7
  */
8
8
  import type { JsonValue } from "./marci/query";
9
+ import type { StoredFile, StoredImage } from "../files/models";
9
10
  export type ScalarKind = "string" | "int" | "float" | "bool" | "date" | "uuid" | "";
10
- export type FieldKind = "scalar" | "json" | "enum" | "struct" | "one" | "many" | "list";
11
+ export type FieldKind = "scalar" | "json" | "enum" | "struct" | "one" | "many" | "list" | "file";
11
12
  export type OnDelete = "cascade" | "setNull" | "restrict";
12
13
  /** Phantom metadata — the type-level twin of `FieldDef`. */
13
14
  export type FieldMeta = {
@@ -58,6 +59,10 @@ export type FieldDef = {
58
59
  fields?: Record<string, Field<any, any>>;
59
60
  /** one / many / list */
60
61
  model?: string;
62
+ /** file: a picture (`t.image()`) — `max` is the most its longer side may be, px. */
63
+ image?: {
64
+ max: number;
65
+ };
61
66
  /** many: the field on the related model that holds the reference (`@bind`); resolved by defineDb when omitted. */
62
67
  back?: string;
63
68
  };
@@ -136,6 +141,25 @@ export declare const t: {
136
141
  kind: "many";
137
142
  model: N;
138
143
  }>>;
144
+ /**
145
+ * A stored file: written as a `File` (an upload — an endpoint's parameter), read as
146
+ * `{ url, name, type, size, width?, height? }`. `.optional()` = may be empty, `.array()` = an ordered
147
+ * list of files. It is deleted with its row — or when the field is given another file, or `null`.
148
+ */
149
+ file: () => Field<StoredFile, Meta<{
150
+ kind: "file";
151
+ }>>;
152
+ /**
153
+ * A stored PICTURE — a `t.file()` that takes images only and keeps them fit to show: turned the way
154
+ * the camera meant, without its metadata (a photo's place and time), as WebP, and no larger than
155
+ * `max` px on its longer side (2048 when not said; a picture is never enlarged).
156
+ * Read as a `StoredFile` whose `width` and `height` are always there. A GIF is kept as it is.
157
+ */
158
+ image: (options?: {
159
+ max?: number;
160
+ }) => Field<StoredImage, Meta<{
161
+ kind: "file";
162
+ }>>;
139
163
  /** Ordered relation list stored inline (`@list`) — hand-arranged collections, duplicates allowed. */
140
164
  list: <N extends string>(model: N) => Field<never, Meta<{
141
165
  kind: "list";
@@ -197,6 +197,15 @@ export type MarciOp = {
197
197
  query?: any;
198
198
  data?: any;
199
199
  id?: any;
200
+ journal?: JournalArgs;
201
+ };
202
+ /** The arguments of a journal action (`$journalOpen` / `$journalRead` / `$journalDrop`). `wait` is in seconds. */
203
+ export type JournalArgs = {
204
+ name: string;
205
+ on?: readonly JournalOp[];
206
+ after?: number;
207
+ limit?: number;
208
+ wait?: number;
200
209
  };
201
210
  export type MarciTransport = {
202
211
  exec(op: MarciOp): Promise<any>;
@@ -291,13 +300,41 @@ export interface Query<T extends ModelTypes, Sel = T["scalars"]> extends Promise
291
300
  /** @deprecated use `.select(query).first()` (removed in the next minor). */
292
301
  findFirst<Q extends T["query"] = {}>(query?: Q): Op<Rows<T, Q> | null>;
293
302
  }
294
- /** `db.<model>`: the root query, plus `reindex()` for models with a `@custom` (vector / full-text) index. */
303
+ /** What a journal can record. Only deletes so far. */
304
+ export type JournalOp = "delete";
305
+ /** One recorded change. `row` is the row as it last was: its id and every scalar field. */
306
+ export type JournalEntry<T extends ModelTypes, O extends JournalOp = JournalOp> = {
307
+ seq: number;
308
+ op: O;
309
+ row: Rows<T, T["scalars"]>;
310
+ };
311
+ export type JournalOptions<O extends JournalOp> = {
312
+ /** The operations to record. A journal that exists with other ones is an error, not a redefinition. */
313
+ on: O | readonly O[];
314
+ /** `false`: the loop ends when the journal is read through. By default it waits for the next entry. */
315
+ wait?: boolean;
316
+ };
317
+ /**
318
+ * A named, durable log of a model's changes. It starts recording when it is first asked for and keeps
319
+ * every entry until the loop that reads it has moved past it — an entry whose loop body threw, or that
320
+ * was never reached, is delivered again to the next reader of the same name.
321
+ */
322
+ export interface Journal<T extends ModelTypes, O extends JournalOp = JournalOp> extends AsyncIterable<JournalEntry<T, O>> {
323
+ /** Drops the journal with whatever it still holds; the model's writes stop paying for it. */
324
+ drop(): Promise<void>;
325
+ }
326
+ /**
327
+ * `db.<model>`: the root query, plus `reindex()` for models with a `@custom` (vector / full-text) index and
328
+ * `$journal(name, { on })` — the journal of this model's changes under that name, created on first use.
329
+ */
295
330
  export type Collection<T extends ModelTypes> = Query<T> & (T["reindex"] extends true ? {
296
331
  reindex(): Op<{
297
332
  ok: boolean;
298
333
  indexed: number;
299
334
  }>;
300
- } : {});
335
+ } : {}) & {
336
+ $journal<O extends JournalOp>(name: string, options: JournalOptions<O>): Journal<T, O>;
337
+ };
301
338
  /** One field of a model, in slot order: `n` name, `k` key | body | one | many, `m` the relation's target model. */
302
339
  export type FieldDesc = {
303
340
  n: string;
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import type { Field, FieldMeta } from "./fields";
10
10
  import type { IdentityPublicFields, SessionPublicFields } from "../auth/models";
11
+ import type { StoredFile, UploadedFile as File } from "../files/models";
11
12
  import type { CompareNumValue, CompareRefListValue, CompareRefValue, CompareStrValue, CompareValue, CustomSearchValue, FullTextSearch, JsonCondition, JsonPathWhere, Op, PrimitiveListUpdate, Query as MarciQuery, RefListUpdate, RefListUpdateOrdered, RefUpdate, RefUpdateStruct, Rows, Sub, UpdateNumValue, VectorSearch, WhereValue } from "./marci/query";
12
13
  export type Fields = Record<string, Field<any, any>>;
13
14
  export type Model<F extends Fields = Fields> = {
@@ -48,7 +49,12 @@ export type Row<S, N extends keyof S> = Id<S, N> & {
48
49
  type StructRow<S, F extends Fields> = {
49
50
  [K in keyof F]: FieldRow<S, F[K]>;
50
51
  };
52
+ /** What a query without a selection returns besides the id: the scalars, and the files. */
51
53
  type ScalarKeys<F extends Fields> = {
54
+ [K in keyof F]: MetaOf<F[K]>["kind"] extends "scalar" | "enum" | "json" | "file" ? K : never;
55
+ }[keyof F];
56
+ /** The fields a row is ordered and aggregated by. */
57
+ type ValueKeys<F extends Fields> = {
52
58
  [K in keyof F]: MetaOf<F[K]>["kind"] extends "scalar" | "enum" | "json" ? K : never;
53
59
  }[keyof F];
54
60
  type NumKeys<F extends Fields> = {
@@ -60,9 +66,9 @@ export type Scalars<S, N extends keyof S> = Pick<Row<S, N>, (ScalarKeys<FieldsOf
60
66
  export type ScalarSelect<S, N extends keyof S> = {
61
67
  [K in (ScalarKeys<FieldsOf<S[N]>> | keyof Id<S, N>) & keyof Row<S, N>]: true;
62
68
  };
63
- type FieldInsert<S, Fld, M extends FieldMeta = MetaOf<Fld>> = M["kind"] extends "one" ? Nullable<M, Id<S, ModelName<S, Fld>>> : M["kind"] extends "many" | "list" ? Id<S, ModelName<S, Fld>>[] : M["kind"] extends "struct" ? Nullable<M, StructInsert<S, StructFieldsOf<Fld>>> : Nullable<M, Arr<M, TsOf<Fld>>>;
69
+ type FieldInsert<S, Fld, M extends FieldMeta = MetaOf<Fld>> = M["kind"] extends "one" ? Nullable<M, Id<S, ModelName<S, Fld>>> : M["kind"] extends "many" | "list" ? Id<S, ModelName<S, Fld>>[] : M["kind"] extends "struct" ? Nullable<M, StructInsert<S, StructFieldsOf<Fld>>> : M["kind"] extends "file" ? Nullable<M, Arr<M, File>> : Nullable<M, Arr<M, TsOf<Fld>>>;
64
70
  type RequiredKeys<F extends Fields> = {
65
- [K in keyof F]: K extends IdKeys<F> ? never : MetaOf<F[K]>["optional"] extends true ? never : MetaOf<F[K]>["hasDefault"] extends true ? never : MetaOf<F[K]>["kind"] extends "many" | "list" ? never : K;
71
+ [K in keyof F]: K extends IdKeys<F> ? never : MetaOf<F[K]>["optional"] extends true ? never : MetaOf<F[K]>["hasDefault"] extends true ? never : MetaOf<F[K]>["kind"] extends "many" | "list" ? never : MetaOf<F[K]>["kind"] extends "file" ? (MetaOf<F[K]>["array"] extends true ? never : K) : K;
66
72
  }[keyof F];
67
73
  type Simplify<T> = {
68
74
  [K in keyof T]: T[K];
@@ -74,7 +80,7 @@ type InsertOf<S, F extends Fields> = Simplify<{
74
80
  }>;
75
81
  export type Insert<S, N extends keyof S> = InsertOf<S, FieldsOf<S[N]>>;
76
82
  type StructInsert<S, F extends Fields> = InsertOf<S, F>;
77
- type FieldUpdate<S, Fld, M extends FieldMeta = MetaOf<Fld>> = M["kind"] extends "one" ? Nullable<M, RefUpdate<Id<S, ModelName<S, Fld>>>> : M["kind"] extends "many" ? RefListUpdate<Id<S, ModelName<S, Fld>>> : M["kind"] extends "list" ? RefListUpdateOrdered<Id<S, ModelName<S, Fld>>> : M["kind"] extends "struct" ? RefUpdateStruct<StructInsert<S, StructFieldsOf<Fld>>, UpdateOf<S, StructFieldsOf<Fld>>> | null : M["array"] extends true ? TsOf<Fld>[] | PrimitiveListUpdate<TsOf<Fld>> : Nullable<M, TsOf<Fld> | (IsNum<M> extends true ? UpdateNumValue : never)>;
83
+ type FieldUpdate<S, Fld, M extends FieldMeta = MetaOf<Fld>> = M["kind"] extends "one" ? Nullable<M, RefUpdate<Id<S, ModelName<S, Fld>>>> : M["kind"] extends "many" ? RefListUpdate<Id<S, ModelName<S, Fld>>> : M["kind"] extends "list" ? RefListUpdateOrdered<Id<S, ModelName<S, Fld>>> : M["kind"] extends "struct" ? RefUpdateStruct<StructInsert<S, StructFieldsOf<Fld>>, UpdateOf<S, StructFieldsOf<Fld>>> | null : M["kind"] extends "file" ? Nullable<M, Arr<M, File | StoredFile>> : M["array"] extends true ? TsOf<Fld>[] | PrimitiveListUpdate<TsOf<Fld>> : Nullable<M, TsOf<Fld> | (IsNum<M> extends true ? UpdateNumValue : never)>;
78
84
  type UpdateOf<S, F extends Fields> = {
79
85
  [K in Exclude<keyof F, IdKeys<F>>]?: FieldUpdate<S, F[K]>;
80
86
  };
@@ -87,7 +93,7 @@ type StructSelect<S, F extends Fields> = {
87
93
  [K in keyof F]?: FieldSelect<S, F[K]>;
88
94
  };
89
95
  type ScalarWhere<T, M extends FieldMeta> = Nullable<M, CompareValue<T>> | (IsNum<M> extends true ? CompareNumValue<T> : M["scalar"] extends "string" ? CompareStrValue<T> : never);
90
- type FieldWhere<S, Fld, M extends FieldMeta = MetaOf<Fld>> = M["kind"] extends "one" ? CompareRefValue<Where<S, ModelName<S, Fld>> | null> : M["kind"] extends "many" | "list" ? CompareRefListValue<Where<S, ModelName<S, Fld>>> : M["kind"] extends "struct" ? CompareRefValue<WhereValue<StructWhereFields<S, StructFieldsOf<Fld>>> | null> : M["kind"] extends "json" ? JsonPathWhere | JsonCondition : M["array"] extends true ? TsOf<Fld>[] : ScalarWhere<TsOf<Fld>, M> | CustomSearchValue<FullTextSearch> | CustomSearchValue<VectorSearch>;
96
+ type FieldWhere<S, Fld, M extends FieldMeta = MetaOf<Fld>> = M["kind"] extends "one" ? CompareRefValue<Where<S, ModelName<S, Fld>> | null> : M["kind"] extends "many" | "list" ? CompareRefListValue<Where<S, ModelName<S, Fld>>> : M["kind"] extends "struct" ? CompareRefValue<WhereValue<StructWhereFields<S, StructFieldsOf<Fld>>> | null> : M["kind"] extends "json" ? JsonPathWhere | JsonCondition : M["kind"] extends "file" ? never : M["array"] extends true ? TsOf<Fld>[] : ScalarWhere<TsOf<Fld>, M> | CustomSearchValue<FullTextSearch> | CustomSearchValue<VectorSearch>;
91
97
  type WhereFieldsOf<S, F extends Fields> = {
92
98
  [K in keyof F]?: FieldWhere<S, F[K]>;
93
99
  };
@@ -96,7 +102,7 @@ type IdWhere<S, N extends keyof S> = {
96
102
  };
97
103
  export type Where<S, N extends keyof S> = WhereValue<WhereFieldsOf<S, FieldsOf<S[N]>> & IdWhere<S, N>>;
98
104
  type StructWhereFields<S, F extends Fields> = WhereFieldsOf<S, F>;
99
- type OrderKeys<S, N extends keyof S> = (ScalarKeys<FieldsOf<S[N]>> | keyof Id<S, N>) & keyof Row<S, N>;
105
+ type OrderKeys<S, N extends keyof S> = (ValueKeys<FieldsOf<S[N]>> | keyof Id<S, N>) & keyof Row<S, N>;
100
106
  /** ONE field and its direction: the engine sorts by a single field, so a second key is a type error. */
101
107
  export type Order<S, N extends keyof S> = {
102
108
  [K in OrderKeys<S, N>]: {
@@ -118,8 +124,8 @@ export type AggregateQuery<S, N extends keyof S> = {
118
124
  $count?: true;
119
125
  $sum?: NumKeys<FieldsOf<S[N]>> & string;
120
126
  $avg?: NumKeys<FieldsOf<S[N]>> & string;
121
- $min?: (ScalarKeys<FieldsOf<S[N]>> | keyof Id<S, N>) & string;
122
- $max?: (ScalarKeys<FieldsOf<S[N]>> | keyof Id<S, N>) & string;
127
+ $min?: (ValueKeys<FieldsOf<S[N]>> | keyof Id<S, N>) & string;
128
+ $max?: (ValueKeys<FieldsOf<S[N]>> | keyof Id<S, N>) & string;
123
129
  };
124
130
  /** The per-model type bag marcidb's generic `Query<T>` is parametrised by (what its codegen emits as `UserTypes`). */
125
131
  export type Types<S, N extends keyof S> = {
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The db layer's half of stored files (./models.ts): what turns a file FIELD — a value in a row — into
3
+ * operations on the platform's `File` model, and back.
4
+ *
5
+ * read the field is selected as the reverse relation it is (the `File` rows that point at the
6
+ * row, in order) and comes back as a `StoredFile`, or a list of them
7
+ * insert the row is inserted without the field; each upload's `File` row is then pointed at it
8
+ * (`ref` to the insert's id) — one transaction
9
+ * update the row's files that are no longer listed are deleted, the uploads are pointed at it —
10
+ * one transaction with the update itself
11
+ *
12
+ * An upload written to a `t.image()` field is first made a picture fit to show (the host's work,
13
+ * ./host.ts): that is why a plan is awaited. What it became — its type, its size — goes to its row
14
+ * in the operation that attaches it.
15
+ *
16
+ * A file is written from the FILE's side (its reference to the owner): that is the side the database
17
+ * cascades over. Only an upload of the current request is ever attached: a `StoredFile` the app hands
18
+ * back means "keep this one", and one of another row is not moved — a client that knows the url of
19
+ * someone else's file cannot take it.
20
+ */
21
+ import type { MarciOp } from "../db/marci/query";
22
+ import { type FileField } from "./models";
23
+ type Plan = {
24
+ op: MarciOp;
25
+ after: MarciOp[];
26
+ };
27
+ export type FileLayer = {
28
+ /** The operation as the database runs it, and the ones that follow it in the same transaction.
29
+ * `index` is the operation's place in that transaction (what `ref` counts from). */
30
+ plan(op: MarciOp, index: number): Promise<Plan>;
31
+ /** The database's answer to a planned operation, as the project reads it. */
32
+ result(op: MarciOp, value: any): any;
33
+ };
34
+ /**
35
+ * `fields`: the schema's `t.file()` fields. `relations`: model → relation field → its target model.
36
+ * `scalars`: model → what a query without a selection returns (file fields included).
37
+ */
38
+ export declare const createFileLayer: (fields: FileField[], relations: Record<string, Record<string, string>>, scalars: Record<string, string[]>) => FileLayer;
39
+ export {};
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Stored files: `t.file()` on a model.
3
+ *
4
+ * A project never declares where a file lives. A model that has a `t.file()` field makes the db carry
5
+ * one more model, the PLATFORM's `File` — a row per stored file: what it is (name, type, size, an
6
+ * image's dimensions) and whose it is. The owner is a reference FROM the file TO its row, with
7
+ * `onDelete("cascade")`, one optional reference per `t.file()` field of the schema (`postImage`,
8
+ * `userAvatar`): deleting a post — by name, or because its author was deleted — deletes its files in
9
+ * the same transaction, by the database's own rule. The field on the owner is the reverse side of
10
+ * that reference.
11
+ *
12
+ * The bytes are the host's (./host.ts): a file on disk named by the row's id. They go only because
13
+ * the row went — the host reads the deletions from marcidb's journal of this model.
14
+ *
15
+ * What a project sees is a VALUE: it writes a `File` (an upload, an endpoint's parameter) and reads a
16
+ * `StoredFile`. The url is computed when the row is read, never stored.
17
+ */
18
+ import { type Field } from "../db/fields";
19
+ export declare const FILE_MODEL = "File";
20
+ /** A stored file as a project reads it. */
21
+ export type StoredFile = {
22
+ /** Where the bytes are served from. Public, not guessable; it never changes for this file. */
23
+ url: string;
24
+ name: string;
25
+ /** The media type: `image/jpeg`, `video/mp4`, `application/pdf`. */
26
+ type: string;
27
+ /** Bytes. */
28
+ size: number;
29
+ /** An image's dimensions in pixels (absent for anything else). */
30
+ width?: number;
31
+ height?: number;
32
+ };
33
+ /** A stored picture (`t.image()`): its size is always known. */
34
+ export type StoredImage = StoredFile & {
35
+ width: number;
36
+ height: number;
37
+ };
38
+ /**
39
+ * A file as an endpoint receives it — the app's `File` (a picked file, a photo), uploaded with the
40
+ * call. Written to a `t.file()` field, it is stored; an endpoint that writes it nowhere drops it.
41
+ */
42
+ export type UploadedFile = {
43
+ readonly name: string;
44
+ readonly size: number;
45
+ readonly type: string;
46
+ };
47
+ /** One `t.file()` field of the schema; `image` = it is a `t.image()`, with its rule. */
48
+ export type FileField = {
49
+ model: string;
50
+ key: string;
51
+ array: boolean;
52
+ owner: string;
53
+ image?: ImageRule;
54
+ };
55
+ export type ImageRule = {
56
+ max: number;
57
+ };
58
+ /** What a file became when it was made fit for an image field: the facts its row carries from then on. */
59
+ export type ImageFacts = {
60
+ name: string;
61
+ type: string;
62
+ size: number;
63
+ width: number;
64
+ height: number;
65
+ };
66
+ /** The reference on `File` that says "I am this field of that row": `Post.image` → `postImage`. */
67
+ export declare const fileOwnerKey: (model: string, key: string) => string;
68
+ /** What a data browser may show of a file (the rest is the platform's bookkeeping). */
69
+ export declare const filePublicFields: () => {
70
+ name: Field<string, import("../db/fields").Meta<{
71
+ kind: "scalar";
72
+ scalar: "string";
73
+ }>>;
74
+ type: Field<string, import("../db/fields").Meta<{
75
+ kind: "scalar";
76
+ scalar: "string";
77
+ }>>;
78
+ size: Field<number, import("../db/fields").Meta<{
79
+ kind: "scalar";
80
+ scalar: "int";
81
+ }>>;
82
+ width: Field<number, {
83
+ kind: "scalar";
84
+ scalar: "int";
85
+ optional: true;
86
+ hasDefault: false;
87
+ array: false;
88
+ isId: false;
89
+ model: "";
90
+ }>;
91
+ height: Field<number, {
92
+ kind: "scalar";
93
+ scalar: "int";
94
+ optional: true;
95
+ hasDefault: false;
96
+ array: false;
97
+ isId: false;
98
+ model: "";
99
+ }>;
100
+ createdAt: Field<number, {
101
+ kind: "scalar";
102
+ scalar: "date";
103
+ optional: false;
104
+ hasDefault: true;
105
+ array: false;
106
+ isId: false;
107
+ model: "";
108
+ }>;
109
+ };
110
+ /** The whole `File` of a schema with these `t.file()` fields. */
111
+ export declare const fileFields: (fields: FileField[]) => Record<string, Field<any, any>>;
112
+ /**
113
+ * What the host (./host.ts) gives the db layer of a bundle — a global, because the bundle carries its
114
+ * own copy of this module. Absent (a unit test without a host): nothing is an upload, urls are relative.
115
+ */
116
+ export type FilesSeam = {
117
+ /** The id of the `File` row behind an upload of the current request; undefined for anything else —
118
+ * a value the app sent as JSON is never an upload, whatever it looks like. */
119
+ uploaded(value: unknown): string | undefined;
120
+ url(id: string, name: string): string;
121
+ /** Make the upload `id` fit for a `t.image()` field (its bytes are rewritten in place) and say what
122
+ * it is now. Throws an `ApiError` for what is no picture. */
123
+ image(id: string, rule: ImageRule): Promise<ImageFacts>;
124
+ /** A write that may have removed rows of `File` went through the db (a delete, a rewritten file field): their bytes are due. */
125
+ dirty: boolean;
126
+ };
127
+ export declare const filesSeam: () => FilesSeam | undefined;
128
+ export declare const setFilesSeam: (value: FilesSeam | null) => void;
129
+ /** The row a `StoredFile` the app handed back stands for (by its url); undefined for anything else. */
130
+ export declare const storedFileId: (value: unknown) => string | undefined;
131
+ export declare const fileUrl: (id: string, name: string) => string;
@@ -2,3 +2,4 @@ export { t, model, defineDb, ref } from "./db";
2
2
  export { ApiError } from "./errors";
3
3
  export { channel } from "./channel";
4
4
  export { request } from "./context";
5
+ export type { StoredFile, StoredImage } from "./files/models";
@@ -1,3 +1,4 @@
1
+ import { type UIWidgetContent } from "./UILayer";
1
2
  import { type DismissOptions, type PresentOptions } from "./presentable";
2
3
  import { type BaseStyle, type DrawableStyle, type ElementStyle, type UIElementBase } from "./UINode";
3
4
  export type UINativeViewStyle = ElementStyle & DrawableStyle;
@@ -26,6 +27,8 @@ export interface NativeView extends UIElementBase<UINativeViewStyle, DrawableSty
26
27
  /** Subscribe to an event the native view emits (`map.on("markerTap", cb)`). */
27
28
  on(event: string, callback: (data?: any) => void): this;
28
29
  off(event: string, callback: (data?: any) => void): this;
30
+ /** The UI over the view when presented: widgets over it (`Presentable.setContent`). */
31
+ setContent(content: UIWidgetContent): this;
29
32
  }
30
33
  /**
31
34
  * Create an instance of a host-registered platform view. `NativeView.isSupported(name)` reports
@@ -7,6 +7,7 @@ export { UIImage, type UIImageStyle } from './UIImage';
7
7
  export { UIVideo, type UIVideoStyle } from './UIVideo';
8
8
  export { UIInput, UITextArea, type UIInputStyle } from './UIInput';
9
9
  export { UIWidget, type UIWidgetStyle } from './UIWidget';
10
+ export { UIOverlay, type UIWidgetContent } from './UILayer';
10
11
  export { UIModal, type UIModalStyle } from './UIModal';
11
12
  export { UIBottomSheet, type UIBottomSheetStyle } from './UIBottomSheet';
12
13
  export { UIPopover, type UIPopoverStyle, type UIPopoverAnchor } from './UIPopover';
@@ -0,0 +1,25 @@
1
+ import type { UIWidget } from "./UIWidget";
2
+ export type UIWidgetContent = (UIWidget | null | undefined | false)[] | (() => (UIWidget | null | undefined | false)[]);
3
+ /**
4
+ * The app layer: the widgets above EVERY destination, always shown — a global loader, a mini-player
5
+ * that follows the user across screens, a toast with actions. Widgets that belong to a place go in
6
+ * that place instead: a dialog among its screen's children (`UIScreen(..., dialog)`), a HUD in
7
+ * `scene.setContent([hud])`. A widget shown with `show()` while it has no parent lands here.
8
+ *
9
+ * ```ts
10
+ * UIOverlay.append(miniPlayer)
11
+ * miniPlayer.hide() // display: none — still here, ready for show()
12
+ * ```
13
+ */
14
+ export declare const UIOverlay: Readonly<{
15
+ /** Add widgets at the end (on top). */
16
+ append(...widgets: (UIWidget | null | undefined | false)[]): void;
17
+ /** Insert widgets at `index` (0 = the bottom of the layer). */
18
+ insert(index: number, ...widgets: (UIWidget | null | undefined | false)[]): void;
19
+ /** Take widgets out of the layer (they stay valid; a dropped one is freed). */
20
+ remove(...widgets: (UIWidget | null | undefined | false)[]): void;
21
+ /** Replace the layer's widgets — a list, or a function for reactive content. */
22
+ setContent(content: UIWidgetContent): void;
23
+ /** The layer's widgets, bottom to top (a snapshot read from the runtime). */
24
+ readonly children: UIWidget[];
25
+ }>;