@orieljs/live 0.1.0-alpha.5 → 0.1.0-alpha.7

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/NOTICE CHANGED
@@ -7,3 +7,10 @@ This product includes software developed by the Oriel project
7
7
  Oriel is licensed under the Apache License, Version 2.0 (see LICENSE).
8
8
  The Oriel name and logo are not covered by that license; see TRADEMARKS.md
9
9
  (https://github.com/orieljs/framework/blob/main/TRADEMARKS.md).
10
+
11
+ @orieljs/support (and @orieljs/framework, which bundles it) includes transliteration data from
12
+ voku/portable-ascii (https://github.com/voku/portable-ascii), Copyright (C) 2019 Lars Moelleken, MIT License.
13
+
14
+ @orieljs/notifications (and @orieljs/framework, which bundles it) includes the HTML and CSS of
15
+ Laravel's default notification mail (laravel/framework, https://github.com/laravel/framework),
16
+ Copyright (c) Taylor Otwell, MIT License.
package/README.md CHANGED
@@ -6,8 +6,7 @@ Server-side Live components for Oriel: Livewire-style components written in TSX,
6
6
  npm install @orieljs/framework
7
7
  ```
8
8
 
9
- ```tsx
10
- // app/Live/Counter.tsx
9
+ ```tsx title="app/Live/Counter.tsx"
11
10
  import { Component } from '@orieljs/framework'
12
11
 
13
12
  export default class Counter extends Component {
@@ -23,27 +22,90 @@ export default class Counter extends Component {
23
22
  }
24
23
  ```
25
24
 
26
- ```ts
27
- // routes/web.ts
25
+ ```ts title="routes/web.ts"
26
+ // @filename: app/Live/Counter.tsx
27
+ import { Component } from '@orieljs/framework'
28
+ export default class Counter extends Component { render() { return <p /> } }
29
+ // ---cut---
28
30
  import { Route } from '@orieljs/framework'
29
31
  import Counter from '../app/Live/Counter'
30
32
 
31
33
  Route.get('/counter', Counter)
32
34
  ```
33
35
 
34
- Embed a component in any view with `<Live component={Counter} />`. In the browser, components are driven by `@orieljs/live-client`.
36
+ Embed a component in any view with `<Live component={Counter} />`. In the browser, components are driven by `@orieljs/live-client`. Public fields are a component's state and its public methods are actions. `mount()` and `render()` may be async (`const user = await request().user<User>()`), and `await this.validate(rules)` returns the validated data, typed from the rules.
35
37
 
36
- ## Pagination
38
+ ## Typed actions
39
+
40
+ `this.$action(method, ...arguments)` writes an action for an `o:` directive, typed against the method:
41
+
42
+ ```tsx title="app/Live/Drafts.tsx"
43
+ import { Component } from '@orieljs/framework'
44
+ import { Post } from '../Models/Post'
45
+
46
+ export default class Drafts extends Component {
47
+ async publish(id: number) {
48
+ const post = await Post.findOrFail(id)
49
+ post.published_at = new Date()
50
+ await post.save()
51
+ }
52
+
53
+ async render() {
54
+ const drafts = await Post.whereNull('published_at').get()
55
+ return (
56
+ <ul>
57
+ {drafts.map((post) => (
58
+ <li>
59
+ {post.title}
60
+ <button type="button" o:click={this.$action(this.publish, post.id)}>Publish</button>
61
+ </li>
62
+ ))}
63
+ </ul>
64
+ )
65
+ }
66
+ }
67
+ ```
68
+
69
+ For post 5 this renders `o:click="publish(5)"`, and since `publish(id: number)` takes a number,
70
+ `this.$action(this.publish, String(post.id))` is a type error. The arguments are written as JSON, so pass ids and
71
+ plain values (strings, numbers, booleans, `null`, arrays and plain objects): a model arrives as its attributes and a
72
+ date as its ISO string, so pass `post.id`, not `post`. The action contains double quotes, so use it as a TSX attribute
73
+ value, which escapes them, not inside an HTML string you write by hand. Rendering throws when the method isn't one of
74
+ the component's actions (a hook, or a `#private` or `@Locked` method). The types check what your pages send; a crafted
75
+ request can still send anything, so an action checks what it is given, as a controller does.
76
+
77
+ ## Confirming an action
78
+
79
+ `o:confirm` asks before an action runs, as Livewire's `wire:confirm`:
37
80
 
38
81
  ```tsx
82
+ declare const tag: { id: number }
83
+ // ---cut---
84
+ <button type="button" class="btn btn-danger" o:click={`delete(${tag.id})`} o:confirm="Delete this tag?">
85
+ Delete
86
+ </button>
87
+ ```
88
+
89
+ The browser's confirm dialog shows the message (an empty `o:confirm` asks "Are you sure?", and `\n` is a line break),
90
+ and the action runs only when it is accepted. `o:confirm-prompt="Type DELETE to confirm|DELETE"` (`o:confirm.prompt`
91
+ in plain HTML) asks with a prompt instead, and runs the action only when the answer is exactly the text after the `|`.
92
+ It works on any element with an `o:` action: `o:click`, `o:submit` on a form, `o:keydown-enter`, and the others. A
93
+ dismissed dialog stops the event, so the actions of the elements around it don't run either. With `-once`, a dismissed
94
+ dialog doesn't use the action up. `o:confirm` guards the actions of the element it sits on: on a form, put it on the
95
+ `<form>` with the `o:submit`. A button inside a form with its own `o:click` needs `type="button"` or `-prevent`
96
+ (`o:click-prevent`), or the browser still submits the form when the dialog is dismissed.
97
+
98
+ ## Pagination
99
+
100
+ ```tsx title="app/Live/Posts.tsx"
39
101
  import { Component, Pagination, WithPagination } from '@orieljs/framework'
40
102
  import { Post } from '../Models/Post'
41
103
 
42
104
  export default class Posts extends WithPagination(Component) {
43
105
  search = ''
44
106
 
45
- updatedSearch() {
46
- this.resetPage()
107
+ async updatedSearch() {
108
+ await this.resetPage()
47
109
  }
48
110
 
49
111
  async render() {
@@ -61,8 +123,81 @@ export default class Posts extends WithPagination(Component) {
61
123
 
62
124
  `paginate()` in the component (in `render()`, in `mount()`, in actions and the update hooks, or in a `@Computed`
63
125
  getter any of them reads) shows the component's `page`, and its links are built on the page's URL. The actions
64
- `nextPage`, `previousPage`, `gotoPage(n)`, `setPage(n)` and `resetPage` move it; `<Pagination>` renders them as
65
- buttons with the kits' `btn` classes. The page starts at 1 and isn't kept in the address bar.
126
+ `nextPage`, `previousPage`, `gotoPage(n)`, `setPage(n)` and `resetPage` move it (they are async, so await them in your
127
+ own methods); `<Pagination>` renders them as buttons with the kits' `btn` classes. The page starts at 1 and isn't kept
128
+ in the address bar.
129
+
130
+ ## File uploads
131
+
132
+ ```tsx title="app/Live/ProfilePhoto.tsx"
133
+ import { Component, TemporaryUploadedFile, WithFileUploads } from '@orieljs/framework'
134
+
135
+ export default class ProfilePhoto extends WithFileUploads(Component) {
136
+ photo: TemporaryUploadedFile | null = null
137
+
138
+ async save() {
139
+ const { photo } = await this.validate({ photo: 'required|image|max:1024' })
140
+ await photo.store('photos')
141
+ this.reset('photo')
142
+ }
143
+
144
+ render() {
145
+ return (
146
+ <form o:submit="save">
147
+ <input type="file" o:model="photo" />
148
+ {this.photo?.isPreviewable() ? <img src={this.photo.temporaryUrl()} alt="Preview" /> : null}
149
+ <button type="submit">Save</button>
150
+ </form>
151
+ )
152
+ }
153
+ }
154
+ ```
155
+
156
+ In a `WithFileUploads` component, `o:model` on a file input uploads the picked file at once, to a temporary
157
+ directory, and sets the property to a `TemporaryUploadedFile` (an array of them with `multiple`). Validate it with the
158
+ file rules, preview it with `temporaryUrl()` (a signed link, for images, audio and video), and `store()` it. In tests,
159
+ `Live.test()`'s `set('photo', file)` uploads a file the same way. Every upload must also pass
160
+ `live.temporary_file_upload.rules`, which allow 12 MB (`max:12288`), but a request body larger than
161
+ `http.max_body_size` (8 MB, for every request) gets a 413: raise it in `config/http.ts` to accept files up to the
162
+ rule's size.
163
+
164
+ ## Lazy components
165
+
166
+ ```tsx title="app/Live/PostCount.tsx"
167
+ import { Component } from '@orieljs/framework'
168
+ import { Post } from '../Models/Post'
169
+
170
+ export default class PostCount extends Component {
171
+ static override lazy = true
172
+ count = 0
173
+
174
+ async mount() {
175
+ this.count = await Post.count()
176
+ }
177
+
178
+ placeholder() {
179
+ return <p>Counting posts…</p>
180
+ }
181
+
182
+ render() {
183
+ return <p>{this.count} posts</p>
184
+ }
185
+ }
186
+ ```
187
+
188
+ With `static override lazy = true`, each `<Live component={PostCount} />` (and the full page) renders the
189
+ placeholder first and mounts the component once it scrolls into view. `lazy` on one embed makes that embed lazy,
190
+ `lazy="on-load"` mounts it as soon as the page loads, and `lazy={false}` turns `static lazy` off. Without
191
+ `placeholder()`, the placeholder is the `live.lazy_placeholder` view, else an empty `<div>`; it needs the same root
192
+ element as `render()`.
193
+
194
+ ## Navigating
195
+
196
+ `<a href="/posts" o:navigate>` shows the next page without a full reload: Live fetches it (from `mousedown`, or on
197
+ hover with `o:navigate-hover`), merges its `<head>`, swaps the `<body>` and starts the new page's components.
198
+ `this.redirect(url, { navigate: true })` does the same from an action. The new page's scripts run as on a full load,
199
+ except a `<head>` script the page already has, and a `<body>` script marked `data-navigate-once` that an earlier page
200
+ already ran.
66
201
 
67
202
  ## Middleware on updates
68
203
 
@@ -70,22 +205,61 @@ Each update re-runs the `auth`, `verified`, `guest` and `can` middleware of the
70
205
  (Livewire's persistent middleware), with that route's parameters bound again, so `can:update,post` sees the post. A
71
206
  signed-out tab gets a 401 and a user who lost the ability a 403, before the component's actions run. On a page behind
72
207
  one of these middleware, a model deleted since the page rendered gets a 404 too; a page with none binds nothing on
73
- updates. The list is `live.persistent_middleware` in `config/live.ts`.
208
+ updates. The list is `live.persistent_middleware`, which a `config/live.ts` of your own can set.
74
209
 
75
210
  ## Flash data in updates
76
211
 
77
212
  A status an action flashes (`session().flash('status', 'Saved.')`) shows in that update's response only, unless the
78
213
  action redirects or navigates: then it is there on the next page. Component state is unaffected.
79
214
 
215
+ ## Status messages announced again
216
+
217
+ Screen readers announce a status region (`role="status"`) when its text changes. When an action brings the same
218
+ message again ("Saved" after a second save), the page doesn't change and nothing is announced. Add `o:announce` to the
219
+ region and Live takes the message out and puts it back a moment later, so it is announced again:
220
+
221
+ ```tsx title="app/Live/Settings.tsx"
222
+ import { Component } from '@orieljs/framework'
223
+
224
+ export default class Settings extends Component {
225
+ name = ''
226
+ #saved = false
227
+
228
+ save() {
229
+ this.#saved = true
230
+ }
231
+
232
+ render() {
233
+ return (
234
+ <form o:submit="save">
235
+ <input o:model="name" aria-label="Name" />
236
+ <button type="submit">Save</button>
237
+ <p role="status" o:announce>
238
+ {this.#saved ? 'Saved' : ''}
239
+ </p>
240
+ </form>
241
+ )
242
+ }
243
+ }
244
+ ```
245
+
246
+ Only actions do this, and an `o:poll` that calls a method counts as one: an update that only syncs `o:model` values,
247
+ refreshes or delivers an event leaves the region alone. Keep a status for one response (a `#private` field, or a
248
+ flashed `status`), so that any other update clears it; a status kept in a public property is announced again by every
249
+ action until it changes, which in a polled component is every tick. Keep an `o:announce` region to text: its children
250
+ are taken out for 100 ms, so a button or input inside it would lose focus.
251
+
80
252
  ## Known limitations
81
253
 
82
- - `o:navigate` does not run scripts in the new page.
83
- - A DOM event whose name contains `-` can't be written as `o:<event>` in TSX, because modifiers are `-` segments (for example `o:model-live`).
254
+ - A DOM event whose name contains `-` can't be an `o:<event>` action, because `-` (and `.`) separate the modifiers (for example `o:model-live`).
255
+ - `temporaryUrl()` previews don't answer range requests, which Safari needs to play a `<video>`.
256
+
257
+ Documentation: https://orieljs.vercel.app/docs/live
84
258
 
85
259
  Status: early alpha. APIs will change.
86
260
 
87
261
  ## License
88
262
 
89
- Oriel is open-source software licensed under the Apache License 2.0 (Apache-2.0; see `LICENSE` and `NOTICE`). The Oriel name and logo are trademarks; see [`TRADEMARKS.md`](https://github.com/orieljs/framework/blob/main/TRADEMARKS.md).
263
+ Oriel is open-source software licensed under the Apache License 2.0 (Apache-2.0; see `LICENSE` and `NOTICE`). The Oriel name and logo are trademarks; see `TRADEMARKS.md`.
90
264
 
91
265
  Website: https://orieljs.vercel.app
package/TRADEMARKS.md ADDED
@@ -0,0 +1,33 @@
1
+ # Oriel trademark policy
2
+
3
+ The Oriel code is open source under the Apache License 2.0. The **Oriel name and logo** are not: the license covers
4
+ the code only (section 6 of the license says so), and the name and logo stay with the project. This page explains
5
+ what you can and can't do with them. The goal is simple: people should always be able to tell what is the real Oriel
6
+ and what isn't.
7
+
8
+ ## You can, without asking
9
+
10
+ - Use, change, ship and sell the code, as the license allows.
11
+ - Say that your app or package is "built with Oriel", "for Oriel" or "compatible with Oriel".
12
+ - Name a package `oriel-<something>` or `<something>-for-oriel` when it works with Oriel, as long as it is clear that
13
+ it is not official (for example, don't publish it under the `@orieljs` npm scope).
14
+ - Use the name and logo to write about Oriel, teach it, or link to it, in articles, talks, videos and courses.
15
+ - Use the unmodified logo to point to the project, as it is (no recolouring, cropping or
16
+ distortion).
17
+
18
+ ## You can't, without written permission
19
+
20
+ - Call a fork, a modified copy or a competing product "Oriel", or anything likely to be confused with it.
21
+ - Use the name or logo as the name, logo or domain of your own product, company or service.
22
+ - Suggest that the Oriel project made, endorses or supports your work when it doesn't.
23
+ - Sell merchandise with the name or logo.
24
+
25
+ ## Forks
26
+
27
+ Forks are welcome. Please give yours a different name and logo, keep the `LICENSE` and `NOTICE` files, and say that
28
+ it is based on Oriel.
29
+
30
+ ## Questions
31
+
32
+ Open an issue at https://github.com/orieljs/framework or email meeeet.dev@gmail.com. If you're unsure whether a use
33
+ is fine, ask; the answer is usually yes.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _orieljs_contracts from '@orieljs/contracts';
2
- import { Class, Rules, MessageBag, LiveEffects, Htmlable, LiveSnapshot, LiveManager, Application, HttpRequest, LiveUpdateRequest, LiveUpdateResponse, OrielError, ErrorKind, MiddlewareClass, Router } from '@orieljs/contracts';
2
+ import { Class, RouteName, RouteArguments, Rules, InferValidatedType, MessageBag, LiveEffects, Htmlable, LiveSnapshot, LiveManager, Application, HttpRequest, LiveUpdateRequest, LiveUpdateResponse, OrielError, ErrorKind, UploadedFile, MiddlewareClass, Router } from '@orieljs/contracts';
3
3
  import { ServiceProvider } from '@orieljs/support';
4
4
 
5
5
  /** Marks Live component classes (Symbol.for, so two copies of the package agree). */
@@ -13,8 +13,11 @@ interface QueuedDispatch {
13
13
  }
14
14
  /** The validation errors of a component (Laravel's MessageBag), for render(): `this.errors.first('title')`. */
15
15
  declare class ErrorBag implements MessageBag {
16
+ #private;
16
17
  private readonly messages;
17
18
  constructor(messages: Record<string, string[]>);
19
+ /** Adds to the component's errors, as addError() does. */
20
+ add(key: string, message: string): void;
18
21
  has(key: string): boolean;
19
22
  first(key?: string): string | undefined;
20
23
  get(key: string): string[];
@@ -45,6 +48,13 @@ declare abstract class Component {
45
48
  static layout?: string;
46
49
  /** The page title passed to the layout as `title`. */
47
50
  static title?: string;
51
+ /**
52
+ * Livewire's #[Lazy]: every embed (and the full page) renders the placeholder first and mounts once the browser shows
53
+ * it. `<Live component={…} lazy={false} />` mounts one at once. The placeholder is the component's
54
+ * `placeholder(params)` method (TSX), else the `live.lazy_placeholder` view, else an empty `<div>`: give it the root
55
+ * element the component renders.
56
+ */
57
+ static lazy?: boolean;
48
58
  /** Static fallbacks for runtimes without decorators: { event: 'method' }, locked names, computed getter names. */
49
59
  static listeners?: Record<string, string>;
50
60
  static locked?: string[];
@@ -58,14 +68,19 @@ declare abstract class Component {
58
68
  redirect(url: string, options?: {
59
69
  navigate?: boolean;
60
70
  }): void;
61
- redirectRoute(name: string, parameters?: unknown, options?: {
71
+ /**
72
+ * redirect() to a named route: `this.redirectRoute('posts.show', post, { navigate: true })`. Once `oriel
73
+ * route:types` has run, the name and the route's parameters are checked, as route()'s are.
74
+ */
75
+ redirectRoute<N extends RouteName>(name: N, ...args: RouteArguments<N, [options?: {
62
76
  navigate?: boolean;
63
- }): void;
77
+ }]>): void;
64
78
  /**
65
- * Validates the public state. On success clears the error bag and returns the validated data; on failure fills the
66
- * error bag and throws (the rest of the action is skipped and the component re-renders with `errors`).
79
+ * Validates the public state. On success clears the error bag and returns the validated data, typed from the rules
80
+ * (`await this.validate({ title: 'required|string' })` is `{ title: string }`); on failure fills the error bag and
81
+ * throws (the rest of the action is skipped and the component re-renders with `errors`).
67
82
  */
68
- validate<T = Record<string, unknown>>(rules: Rules, messages?: Record<string, string>, attributes?: Record<string, string>): Promise<T>;
83
+ validate<const R extends Rules>(rules: R, messages?: Record<string, string>, attributes?: Record<string, string>): Promise<InferValidatedType<R>>;
69
84
  addError(key: string, message: string): void;
70
85
  /** Clears every error, or only those of `keys`. */
71
86
  resetErrorBag(...keys: string[]): void;
@@ -74,6 +89,13 @@ declare abstract class Component {
74
89
  reset(...properties: string[]): void;
75
90
  /** Keeps the current HTML: the response carries the new state but no `html` effect. */
76
91
  skipRender(): void;
92
+ /**
93
+ * An action for an `o:` directive, typed against its method: `o:click={this.$action(this.delete, tag.id)}` writes
94
+ * `delete(5)`, and a string where `delete` takes a number is a type error. The arguments are written as JSON, the
95
+ * trailing `undefined` ones left out so the method's defaults apply. The method must be one of this component's
96
+ * actions (a public method of its class, not a hook and not @Locked); anything else throws while rendering.
97
+ */
98
+ $action<P extends unknown[]>(method: (...params: P) => unknown, ...params: NoInfer<P>): string;
77
99
  /** A full-page component as a route action: Route.get('/counter', Counter). Route parameters go to mount(). */
78
100
  __invoke(): Promise<unknown>;
79
101
  }
@@ -102,7 +124,10 @@ declare class Testable<C extends Component = Component> {
102
124
  private readonly manager;
103
125
  private readonly scope;
104
126
  private constructor();
105
- /** Mounts `component` with `params`, as `<Live component={…} {...params} />` would. */
127
+ /**
128
+ * Mounts `component` with `params`, as `<Live component={…} {...params} />` would. A lazy component mounts at once
129
+ * (Livewire's withoutLazyLoading()); `{ lazy: true }` renders its placeholder, and `call('__lazyLoad', …)` loads it.
130
+ */
106
131
  static create<C extends Component>(component: Class<C> | string, params?: Record<string, unknown>): Promise<Testable<C>>;
107
132
  /** The component instance after the last request. */
108
133
  instance(): C;
@@ -112,7 +137,14 @@ declare class Testable<C extends Component = Component> {
112
137
  html(): string;
113
138
  /** The effects of the last request. */
114
139
  effects(): LiveEffects;
140
+ /**
141
+ * Sets public properties through an update. A file (a FakeFile) or an array of files is uploaded as `o:model` on a
142
+ * file input does it: the upload rules, the temporary disk, then a TemporaryUploadedFile (or an array) in the
143
+ * property, or the upload's errors on it (Livewire's).
144
+ */
115
145
  set(path: string | Record<string, unknown>, value?: unknown): Promise<this>;
146
+ /** Uploads `files` into the property `name` (an array when `multiple`), as the browser's upload calls do. */
147
+ upload(name: string, files: readonly Blob[], multiple?: boolean): Promise<this>;
116
148
  call(method: string, ...params: unknown[]): Promise<this>;
117
149
  toggle(path: string): Promise<this>;
118
150
  /** Dispatches an event to this component, as the browser does for its listeners. */
@@ -125,6 +157,9 @@ declare class Testable<C extends Component = Component> {
125
157
  assertDontSee(...values: string[]): this;
126
158
  /** Each value appears in the rendered HTML as written (not escaped). */
127
159
  assertSeeHtml(...values: string[]): this;
160
+ /** Each value, HTML-escaped, appears in the rendered text: the HTML with its tags stripped (Livewire's assertSeeText). */
161
+ assertSeeText(...values: string[]): this;
162
+ assertDontSeeText(...values: string[]): this;
128
163
  assertDispatched(name: string, params?: Record<string, unknown>): this;
129
164
  assertNotDispatched(name: string): this;
130
165
  /** The error bag has errors for every key given (or any error at all with no keys). */
@@ -142,6 +177,11 @@ interface LiveProps {
142
177
  component: Class<Component> | string;
143
178
  /** Identifies a child inside its parent across re-renders (Livewire's :key / wire:key). */
144
179
  'o:key'?: string | number;
180
+ /**
181
+ * Renders the placeholder and mounts the component once it scrolls into view (`'on-load'`: once the page starts);
182
+ * `false` mounts a `static lazy` component at once.
183
+ */
184
+ lazy?: boolean | 'on-load';
145
185
  [param: string]: unknown;
146
186
  }
147
187
  /** Blade's @livewire('counter', ['count' => 3], key('c1')): the component's first render, as trusted HTML. */
@@ -184,10 +224,25 @@ declare class LiveComponentManager implements LiveManager {
184
224
  isLiveRequest(request: HttpRequest): boolean;
185
225
  /** The first render: HTML with o:id, o:snapshot and o:effects on the root element. */
186
226
  mount(component: string | Class<unknown>, params?: Record<string, unknown>, key?: string): Promise<string>;
187
- mountComponent(component: string | Class<unknown>, params?: Record<string, unknown>, existing?: Component): Promise<ComponentResult>;
227
+ /**
228
+ * Mounts a component: boot(), the params into its properties, mount(params), then its first render. A lazy one
229
+ * (`lazy` in the params, or `static lazy`) renders its placeholder instead and mounts when the browser loads it.
230
+ */
231
+ mountComponent(component: string | Class<unknown>, embedParams?: Record<string, unknown>, existing?: Component): Promise<ComponentResult>;
232
+ /**
233
+ * A lazy component's first render (Livewire's): its placeholder, with the state it has before mount() and the mount
234
+ * params signed for the `__lazyLoad` call the browser makes when it shows it (`o:lazy`, or `o:lazy-on-load`).
235
+ */
236
+ private mountPlaceholder;
188
237
  /** Applies each component's updates and calls, re-renders it and returns its new snapshot and effects. */
189
238
  update(request: LiveUpdateRequest): Promise<LiveUpdateResponse>;
190
239
  updateComponent(snapshotJson: string, updates: Record<string, unknown>, calls: Call[]): Promise<ComponentResult>;
240
+ /**
241
+ * The mount params and placeholder tag of a `__lazyLoad` request, or undefined for any other update. The load comes
242
+ * alone (no updates, no other call: the browser sends it in a request of its own) and its payload must be the one
243
+ * signed for this component: anything else is refused as corrupt (a 403) before the component boots.
244
+ */
245
+ private lazyLoad;
191
246
  /**
192
247
  * The route action of POST /oriel/live/update. Each component's update runs behind the persistent middleware of the
193
248
  * route that rendered its page (Livewire's): `auth`, `verified`, `guest` and `can`, with the page route's parameters
@@ -206,7 +261,7 @@ declare class LiveComponentManager implements LiveManager {
206
261
  /** Makes every computed getter (static `computed` too) cache its value for the rest of the request. */
207
262
  private prepareComputed;
208
263
  private applyUpdate;
209
- /** Runs one call from the browser: a magic action, an event, or a public method. Returns its return value. */
264
+ /** Runs one call from the browser: a magic action, an event, an upload call, or a public method. Returns its return value. */
210
265
  private call;
211
266
  /**
212
267
  * Renders the component (rendering/rendered hooks, nested children tracked) and checks for a single root. A
@@ -226,8 +281,9 @@ declare class LiveComponentManager implements LiveManager {
226
281
  declare const UPDATE_URI = "/oriel/live/update";
227
282
  /**
228
283
  * Binds the LiveManager and the PaginationState that WithPagination components answer, merges the `live` config,
229
- * registers POST /oriel/live/update (named `live.update`, in the `web` group: sessions and CSRF apply), the make:live
230
- * command, and the components under app/Live.
284
+ * registers POST /oriel/live/update (named `live.update`, in the `web` group: sessions and CSRF apply), the upload and
285
+ * preview routes of WithFileUploads (`live.upload-file`, `live.preview-file`), the make:live command, and the
286
+ * components under app/Live.
231
287
  */
232
288
  declare class LiveServiceProvider extends ServiceProvider {
233
289
  register(): void;
@@ -277,6 +333,108 @@ declare class InvalidLiveRequestError extends LiveError {
277
333
  readonly kind: ErrorKind;
278
334
  readonly status: number;
279
335
  }
336
+ /** An upload or preview URL whose signature is missing, wrong or expired (Livewire's 401). Nothing is read or stored. */
337
+ declare class InvalidUploadSignatureError extends LiveError {
338
+ readonly name: string;
339
+ readonly kind: ErrorKind;
340
+ readonly status: number;
341
+ constructor();
342
+ }
343
+ /** A preview of a temporary upload that is no longer on the disk (cleaned up, or stored and deleted). */
344
+ declare class TemporaryFileNotFoundError extends LiveError {
345
+ readonly name: string;
346
+ readonly kind: ErrorKind;
347
+ readonly status: number;
348
+ constructor(filename: string);
349
+ }
350
+ /** temporaryUrl() on an upload whose type has no preview (live.temporary_file_upload.preview_mimes). */
351
+ declare class FileNotPreviewableError extends LiveError {
352
+ readonly name: string;
353
+ constructor(extension: string);
354
+ }
355
+ /** The errors an upload came back with (`_uploadErrored`): the component re-renders with them on the property. */
356
+ declare class UploadValidationError extends LiveError {
357
+ readonly errors: Record<string, string[]>;
358
+ readonly name: string;
359
+ readonly kind: ErrorKind;
360
+ readonly status: number;
361
+ readonly errorBag: string;
362
+ constructor(errors: Record<string, string[]>);
363
+ }
364
+
365
+ /**
366
+ * Livewire's `temporary_file_upload` settings, read from config `live.temporary_file_upload.*` (each one falls back on
367
+ * its own): the disk (the default filesystem disk), the directory on it, the rules every upload must pass, the upload
368
+ * route's middleware, the types a preview is served for, how long an upload URL lasts (minutes), and whether a finished
369
+ * upload removes the temporary files older than a day.
370
+ */
371
+ declare const TEMPORARY_UPLOAD_DEFAULTS: {
372
+ disk: string | null;
373
+ directory: string;
374
+ rules: string | string[];
375
+ middleware: string | string[];
376
+ preview_mimes: string[];
377
+ max_upload_time: number;
378
+ cleanup: boolean;
379
+ };
380
+ /** What a temporary upload is: its stored name on the temporary disk and what the browser sent (signed with it). */
381
+ interface TemporaryFileData {
382
+ /** A random 40-character name with the extension its bytes showed: `live-tmp/<filename>` on the temporary disk. */
383
+ filename: string;
384
+ /** The name the browser gave the file. */
385
+ name: string;
386
+ size: number;
387
+ /** The MIME type the browser claimed. */
388
+ type: string;
389
+ /** The extension of the upload's own type (its bytes, else a plain claimed extension), or ''. */
390
+ extension: string;
391
+ }
392
+ /**
393
+ * Livewire's TemporaryUploadedFile: a file the browser uploaded to the temporary directory before an action stores
394
+ * it. A WithFileUploads component's property holds one (or an array of them) once `o:model` on a file input has
395
+ * uploaded it: validate it like any upload (`'photo' => 'image|max:1024'`), preview it with `temporaryUrl()`, then
396
+ * `store()` it. It travels in the component's snapshot as its data, signed with the rest.
397
+ */
398
+ declare class TemporaryUploadedFile implements UploadedFile {
399
+ #private;
400
+ readonly filename: string;
401
+ readonly clientName: string;
402
+ readonly mimeType: string;
403
+ readonly size: number;
404
+ constructor(data: TemporaryFileData);
405
+ /** What the snapshot carries. */
406
+ toData(): TemporaryFileData;
407
+ /** The file's path on the temporary disk. */
408
+ get path(): string;
409
+ /** The extension of the upload's own type, as FormUploadedFile's extension(): what putFile() names a stored copy by. */
410
+ extension(): string;
411
+ /** The extension of the name the browser gave the file, lower-cased ('' without one). */
412
+ clientExtension(): string;
413
+ /** A random 40-character name with extension(), as FormUploadedFile's. */
414
+ hashName(): string;
415
+ arrayBuffer(): Promise<ArrayBuffer>;
416
+ text(): Promise<string>;
417
+ /** The file's bytes from the temporary disk, opened on the first read. */
418
+ stream(): ReadableStream<Uint8Array>;
419
+ /** Stores a copy under a random name in `directory`, on `disk` (by default the temporary disk, as Livewire's). */
420
+ store(directory: string, disk?: string): Promise<string>;
421
+ storeAs(directory: string, name: string, disk?: string): Promise<string>;
422
+ exists(): Promise<boolean>;
423
+ /** Removes the temporary file (a stored copy stays). */
424
+ delete(): Promise<boolean>;
425
+ /** Whether temporaryUrl() serves this file: its type is in `live.temporary_file_upload.preview_mimes`. */
426
+ isPreviewable(): boolean;
427
+ /**
428
+ * A signed link to the file for `<img src>` or `<video src>` before it is stored. It lasts until the end of the hour
429
+ * at least 30 minutes away (Livewire's), so a re-render keeps the same link and the browser doesn't reload it.
430
+ */
431
+ temporaryUrl(): string;
432
+ }
433
+
434
+ /** Where the browser POSTs a WithFileUploads component's files (Livewire's /livewire/upload-file), named live.upload-file. */
435
+ declare const UPLOAD_URI = "/oriel/live/upload-file";
436
+ /** Where temporaryUrl() links lead (Livewire's /livewire/preview-file/{filename}), named live.preview-file. */
437
+ declare const PREVIEW_URI = "/oriel/live/preview-file";
280
438
 
281
439
  /** The route middleware an update re-applies by default (config live.persistent_middleware), by alias. */
282
440
  declare const PERSISTENT_MIDDLEWARE: string[];
@@ -312,6 +470,14 @@ interface Paginated {
312
470
  */
313
471
  declare function WithPagination<TBase extends ComponentConstructor>(Base: TBase): TBase & (abstract new (...args: any[]) => Paginated);
314
472
 
473
+ /**
474
+ * Livewire's WithFileUploads: `class Profile extends WithFileUploads(Component)`. A file input with `o:model="photo"`
475
+ * uploads the file to the temporary directory as soon as it is picked, then sets `photo` to a TemporaryUploadedFile
476
+ * (`<input type="file" multiple>` sets an array), through `updatingPhoto()`/`updatedPhoto()` like any update. Validate
477
+ * it with the file rules, preview it with `temporaryUrl()`, store it with `store()`.
478
+ */
479
+ declare function WithFileUploads<TBase extends ComponentConstructor>(Base: TBase): TBase;
480
+
315
481
  /**
316
482
  * What <Pagination> reads from a paginator. Structural, since Live can't import Database: paginate()'s
317
483
  * LengthAwarePaginator (with lastPage(), so page buttons) and simplePaginate()'s Paginator (Previous and Next only).
@@ -339,4 +505,4 @@ interface PaginationProps {
339
505
  */
340
506
  declare function Pagination({ paginator, class: className }: PaginationProps): Htmlable;
341
507
 
342
- export { Component, type ComponentResult, Computed, CorruptComponentPayloadError, ErrorBag, InvalidLiveRequestError, Live, LiveComponentManager, LiveComponentNotFoundError, LiveError, type LiveProps, LiveServiceProvider, Locked, MethodNotCallableError, On, PERSISTENT_MIDDLEWARE, type Paginated, Pagination, type PaginationProps, type PaginationSource, PendingEvent, type PersistentMiddleware, PropertyNotUpdatableError, TestRequestScope, Testable, UPDATE_URI, WithPagination, componentName, live, persistentMiddleware };
508
+ export { Component, type ComponentResult, Computed, CorruptComponentPayloadError, ErrorBag, FileNotPreviewableError, InvalidLiveRequestError, InvalidUploadSignatureError, Live, LiveComponentManager, LiveComponentNotFoundError, LiveError, type LiveProps, LiveServiceProvider, Locked, MethodNotCallableError, On, PERSISTENT_MIDDLEWARE, PREVIEW_URI, type Paginated, Pagination, type PaginationProps, type PaginationSource, PendingEvent, type PersistentMiddleware, PropertyNotUpdatableError, TEMPORARY_UPLOAD_DEFAULTS, type TemporaryFileData, TemporaryFileNotFoundError, TemporaryUploadedFile, TestRequestScope, Testable, UPDATE_URI, UPLOAD_URI, UploadValidationError, WithFileUploads, WithPagination, componentName, live, persistentMiddleware };