lecodes-sdk 2.5.1 → 2.6.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.
- package/dist/types/gl/Foliage.d.ts +6 -0
- package/dist/types/ui/UIBottomSheet.d.ts +21 -14
- package/dist/types/ui/UIModal.d.ts +50 -22
- package/dist/types/version.d.ts +1 -1
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/src/bridges/gl.d.ts +4 -1
- package/src/chisel.ts +1 -1
- package/src/compile/compileProject.ts +7 -2
- package/src/gl/Foliage.ts +11 -1
- package/src/ui/UIBottomSheet.ts +39 -30
- package/src/ui/UIModal.ts +128 -31
- package/src/ui/UIWidget.ts +21 -8
- package/src/version.ts +1 -1
package/package.json
CHANGED
package/src/bridges/gl.d.ts
CHANGED
|
@@ -58,6 +58,9 @@ declare global {
|
|
|
58
58
|
stopVR(): void
|
|
59
59
|
addEntityToScene(sceneId: u32, entityId: u32): void
|
|
60
60
|
removeEntityFromScene(sceneId: u32, entityId: u32): void
|
|
61
|
+
/** The renderables the scene holds right now (Filament's Scene::getRenderableCount): a destroyed subtree must take
|
|
62
|
+
* its own back out — checks/owned asserts it. Optional: a host without it leaves the method absent. */
|
|
63
|
+
sceneRenderableCount?(sceneId: u32): u32
|
|
61
64
|
/** @custom */
|
|
62
65
|
warmRender(sceneId: u32, onComplete: () => void): void
|
|
63
66
|
/** Compile every shader variant the scene's materials can need — sun / shadows / fog / skinning
|
|
@@ -852,7 +855,7 @@ declare global {
|
|
|
852
855
|
foliageSetLeaf?(shiver: f32, translucency: f32, leafShadow: f32, skyAo: f32): void
|
|
853
856
|
/** The look of the cards: coverage = alpha × (1 + coverage × mip level) so a distant crown keeps its density as its
|
|
854
857
|
* atlas mips away. */
|
|
855
|
-
foliageSetLook?(coverage: f32): void
|
|
858
|
+
foliageSetLook?(coverage: f32, skyThrough: f32): void
|
|
856
859
|
/** An entity whose live world position bends the vegetation within `radius` metres; <= 0 removes it. The host
|
|
857
860
|
* writes the 8 benders nearest the camera to the shader each frame. */
|
|
858
861
|
foliageSetBender?(entityId: u32, radius: f32): void
|
package/src/chisel.ts
CHANGED
|
@@ -128,7 +128,7 @@ export const chiselVocab: ChiselVocab = {
|
|
|
128
128
|
"style", "class", "named", "theme",
|
|
129
129
|
"onClick", "onTouchStart", "onLongPress", "onMouseEnter", "onLayout", "onOpen", "onClose", "onBack",
|
|
130
130
|
"onScroll", "onScrollRelease", "onOverscroll", "onRefresh", "onSubmit",
|
|
131
|
-
"onSelect", "onChange", "onFocus", "onBlur", "onOverlayTap", "onDetent",
|
|
131
|
+
"onSelect", "onChange", "onFocus", "onBlur", "onOverlayTap", "onDetent", "onDismiss", "bindShow",
|
|
132
132
|
"onEndReached", "onStartReached",
|
|
133
133
|
"append", "insert", "remove", "setContent", "widgets", "keepAlive", "url",
|
|
134
134
|
"detents", "dismissible", "transition", "setSourceRect",
|
|
@@ -112,17 +112,22 @@ export const localAssetBase = (path: string): string => path.split(/[\\/]/).pop(
|
|
|
112
112
|
* order, i.e. on nothing. (2026-09-17: a viewer project carried two asset packs that each ship
|
|
113
113
|
* `SM_Sign_01..03.glb`; the school's floor-standing "CLEANING IN PROGRESS" A-frame was drawn inside
|
|
114
114
|
* the shooting range's wall, where a flat 4 mm plate belonged.)
|
|
115
|
+
*
|
|
116
|
+
* SHARING IS CASE-BLIND: the staging directory lives on Windows / macOS file systems, where `Tire.glb`
|
|
117
|
+
* and `tire.glb` are one file. Compared by exact name both were "unique", staged into the same slot,
|
|
118
|
+
* and the second copy went THROUGH the first one's hard link into the project — lightcheck's Chernobyl
|
|
119
|
+
* `Tire.glb` became the yard's `props/tire.glb` on disk (2026-11-04).
|
|
115
120
|
*/
|
|
116
121
|
export const localAssetNames = (entries: readonly { path: string, type: string }[]): Map<string, string> => {
|
|
117
122
|
const seen = new Map<string, number>()
|
|
118
123
|
for (const e of entries) if (e.type === "resource") {
|
|
119
|
-
const b = localAssetBase(e.path)
|
|
124
|
+
const b = localAssetBase(e.path).toLowerCase()
|
|
120
125
|
seen.set(b, (seen.get(b) ?? 0) + 1)
|
|
121
126
|
}
|
|
122
127
|
const names = new Map<string, string>()
|
|
123
128
|
for (const e of entries) if (e.type === "resource") {
|
|
124
129
|
const b = localAssetBase(e.path)
|
|
125
|
-
names.set(e.path, (seen.get(b) ?? 0) > 1 ? e.path.replace(/^[./]+/, "").replace(/[\\/]/g, "__") : b)
|
|
130
|
+
names.set(e.path, (seen.get(b.toLowerCase()) ?? 0) > 1 ? e.path.replace(/^[./]+/, "").replace(/[\\/]/g, "__") : b)
|
|
126
131
|
}
|
|
127
132
|
return names
|
|
128
133
|
}
|
package/src/gl/Foliage.ts
CHANGED
|
@@ -64,6 +64,12 @@ export type FoliageOptions = {
|
|
|
64
64
|
/** Coverage held over distance: the cards' alpha × (1 + coverage × mip level), so a far crown does not thin out as its
|
|
65
65
|
* atlas mips away. Default 0.25, 0 = none; past ~0.5 a crown turns into a solid mass. */
|
|
66
66
|
coverage?: number
|
|
67
|
+
/** 0..1: THE SKY THROUGH A THIN LEAF. A crown's cards are lit by the sky on their hull normal (out of the crown's centre),
|
|
68
|
+
* so a card below the crown's middle faces DOWN and reads the sky's lower hemisphere — the ground of the environment
|
|
69
|
+
* map, which under an overcast sky is a tenth of the zenith: the lower half of every plant went black (2026-11-04). A
|
|
70
|
+
* leaf is thin: this share of the sky on the OTHER side of the card is added, so the underside of a crown takes this
|
|
71
|
+
* fraction of what its top takes. Default 0.3, 0 = the hull alone. */
|
|
72
|
+
skyThrough?: number
|
|
67
73
|
/** THE PLANTS' CASTER DISTANCE, metres: a plant casts the sun's shadow only this close to the camera (the LOD pass takes
|
|
68
74
|
* the farther ones out of the shadow map; 3 m of hysteresis). Meant for a level with a BAKED ground, which keeps the
|
|
69
75
|
* far shadows while the shadow map holds a fraction of the casters (the Forest's Radeon frame: 6 ms of 39 at 25 m).
|
|
@@ -83,6 +89,7 @@ const state: Required<Omit<FoliageOptions, "wind">> & { wind: Required<FoliageWi
|
|
|
83
89
|
leafShadow: 0.6,
|
|
84
90
|
skyAo: 0.7,
|
|
85
91
|
coverage: 0.25,
|
|
92
|
+
skyThrough: 0.3,
|
|
86
93
|
castDistance: 0,
|
|
87
94
|
}
|
|
88
95
|
|
|
@@ -108,7 +115,10 @@ export const Foliage = {
|
|
|
108
115
|
if (options.leafShadow !== undefined) { state.leafShadow = options.leafShadow; leaf = true }
|
|
109
116
|
if (options.skyAo !== undefined) { state.skyAo = options.skyAo; leaf = true }
|
|
110
117
|
if (leaf) _creator.foliageSetLeaf?.(state.shiver, state.translucency, state.leafShadow, state.skyAo)
|
|
111
|
-
|
|
118
|
+
let look = false
|
|
119
|
+
if (options.coverage !== undefined) { state.coverage = options.coverage; look = true }
|
|
120
|
+
if (options.skyThrough !== undefined) { state.skyThrough = options.skyThrough; look = true }
|
|
121
|
+
if (look) _creator.foliageSetLook?.(state.coverage, state.skyThrough)
|
|
112
122
|
if (options.castDistance !== undefined) { state.castDistance = options.castDistance; _creator.foliageSetCast?.(state.castDistance) }
|
|
113
123
|
},
|
|
114
124
|
|
package/src/ui/UIBottomSheet.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ModalElement, type UIModalStyle } from "./UIModal"
|
|
1
|
+
import { ModalElement, type UIModalShow, type UIModalStyle } from "./UIModal"
|
|
2
2
|
import type { UIWidget } from "./UIWidget"
|
|
3
3
|
import { buildUI, type ChildrenFn, type UIChildArg, type UINodeChild } from "./UINode"
|
|
4
4
|
import { TREE_FLAG_DETENT } from "./tree"
|
|
@@ -15,14 +15,18 @@ const SHEET_ANIM_MS = 250
|
|
|
15
15
|
* node type — docs/bottom-sheet-plan.md): hosts without sheet support show a static bottom panel.
|
|
16
16
|
*
|
|
17
17
|
* With detents, content lays out at the HIGHEST detent's size; lower detents show the top
|
|
18
|
-
* slice. The scrim, `
|
|
19
|
-
*
|
|
18
|
+
* slice. The scrim, `bindShow` / `onDismiss`, `onOpen` / `onClose` all come from the modal
|
|
19
|
+
* machinery; the drag below the lowest detent is the sheet's third dismissal gesture, routed like
|
|
20
|
+
* the scrim tap. The host hears whether a drag may dismiss (`sheetDismissible`):
|
|
21
|
+
* `dismissible(false)`, or a bound sheet with no `onDismiss`, resists instead.
|
|
20
22
|
*
|
|
21
23
|
* ```ts
|
|
22
|
-
* const actions = UIBottomSheet(rows).style({ bgColor: "#1c1c1e", borderRadius: 16 }) // content-sized
|
|
24
|
+
* const actions = UIBottomSheet(rows).style({ bgColor: "#1c1c1e", borderRadius: 16 }) // content-sized, show()
|
|
25
|
+
*
|
|
26
|
+
* const card = UIBottomSheet(place).bindShow(() => data.value.pick != null).onDismiss(on.unpick) // the data's
|
|
23
27
|
*
|
|
24
28
|
* const mapSheet = UIBottomSheet(results).style({ bgColor: "#1c1c1e", borderRadius: 16 })
|
|
25
|
-
* .detents([0.3, 0.6, 1])
|
|
29
|
+
* .detents([0.3, 0.6, 1]).dismissible(false) // persistent: show() once placed
|
|
26
30
|
* .onDetent(i => map.setPadding(...))
|
|
27
31
|
* ```
|
|
28
32
|
*/
|
|
@@ -38,16 +42,21 @@ export interface UIBottomSheet extends UIWidget {
|
|
|
38
42
|
/** Fires on every settle: a finger snap on native hosts, or a `setDetent()` call. */
|
|
39
43
|
onDetent(callback: (index: number) => void): this,
|
|
40
44
|
|
|
41
|
-
/**
|
|
45
|
+
/** Bind the shown state, as on `UIModal`: a function of the data (`bindShow(() =>
|
|
46
|
+
* data.value.pick != null)`) or a signal. */
|
|
47
|
+
bindShow(value: UIModalShow): this,
|
|
48
|
+
/** Slide in to the current detent. No-op while already shown. Bound: as on `UIModal`. */
|
|
42
49
|
show(): void,
|
|
43
|
-
/** Slide out, fade the scrim
|
|
50
|
+
/** Slide out, fade the scrim. No-op while hidden. */
|
|
44
51
|
hide(): void,
|
|
45
|
-
/** True from `show()` until `hide()` starts. */
|
|
46
|
-
readonly isOpen: boolean,
|
|
47
52
|
|
|
48
|
-
/**
|
|
53
|
+
/** The user asked to close — a scrim tap, the back press, a drag below the lowest detent. The
|
|
54
|
+
* one hook for all three; a bound sheet needs it to be dismissible at all (it resists the drag
|
|
55
|
+
* otherwise). When the callback leaves the sheet open, it slides back to its detent. */
|
|
56
|
+
onDismiss(callback: () => void): this,
|
|
57
|
+
/** Called when the sheet opens. */
|
|
49
58
|
onOpen(callback: () => void): this,
|
|
50
|
-
/** Called when the sheet starts closing —
|
|
59
|
+
/** Called when the sheet starts closing — a dismissal, `hide()`, the data. */
|
|
51
60
|
onClose(callback: () => void): this,
|
|
52
61
|
/** `dismissible(false)`: scrim taps, the back button and dragging below the lowest detent no
|
|
53
62
|
* longer close the sheet (a drag collapses to the lowest detent instead). Default `true`. */
|
|
@@ -72,13 +81,23 @@ export class BottomSheetElement extends ModalElement {
|
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
/** @internal The host's GESTURE settles only (programmatic `setDetent` fires JS-side); `-1` =
|
|
75
|
-
* dragged below the lowest detent while dismissible. */
|
|
84
|
+
* dragged below the lowest detent while dismissible — a dismissal like the scrim tap. */
|
|
76
85
|
_emitDetent(index: number): void {
|
|
77
|
-
if (index < 0) { this.
|
|
86
|
+
if (index < 0) { this._dismiss(); return }
|
|
78
87
|
this._detent = index
|
|
79
88
|
for (const callback of this._detentListeners) callback(index)
|
|
80
89
|
}
|
|
81
90
|
|
|
91
|
+
/** @internal The dismiss callbacks left the sheet open (the data said so): the host slid it out
|
|
92
|
+
* on the drag, so put it back on its detent. */
|
|
93
|
+
override _afterDismiss(): void {
|
|
94
|
+
if (this._shown) this.style({ sheetDetent: this._detent } as UIBottomSheetStyle)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
override _syncDismissible(): void {
|
|
98
|
+
this.style({ sheetDismissible: this._dismissAllowed() ? 1 : 0 } as UIBottomSheetStyle)
|
|
99
|
+
}
|
|
100
|
+
|
|
82
101
|
detents(list: number[]): this {
|
|
83
102
|
const sorted = [...list].filter(d => d > 0).sort((a, b) => a - b)
|
|
84
103
|
if (sorted.length === 0) return this
|
|
@@ -108,23 +127,13 @@ export class BottomSheetElement extends ModalElement {
|
|
|
108
127
|
return this
|
|
109
128
|
}
|
|
110
129
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
override hide(): void {
|
|
119
|
-
if (!this._shown) return
|
|
120
|
-
this.style({ sheetDetent: -1 } as UIBottomSheetStyle) // hosts slide out; the hide follows
|
|
121
|
-
super.hide()
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
override dismissible(enabled: boolean): this {
|
|
125
|
-
super.dismissible(enabled)
|
|
126
|
-
this.style({ sheetDismissible: enabled ? 1 : 0 } as UIBottomSheetStyle)
|
|
127
|
-
return this
|
|
130
|
+
/** @internal The display write is the moment the host enters or leaves: the detent rides along —
|
|
131
|
+
* written BEFORE the display, so the node carries the target when the host plays the entrance
|
|
132
|
+
* to it (`-1` on a hide: the host slides out, the hide follows). Every way of opening goes
|
|
133
|
+
* through here: show() / hide(), `open()`, the binding. */
|
|
134
|
+
override _writeDisplay(display: "flex" | "none"): void {
|
|
135
|
+
this.style({ sheetDetent: display === "flex" ? this._detent : -1 } as UIBottomSheetStyle)
|
|
136
|
+
super._writeDisplay(display)
|
|
128
137
|
}
|
|
129
138
|
}
|
|
130
139
|
|
package/src/ui/UIModal.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { buildUI, type BaseStyle, type ChildrenFn, type Color, type DrawableStyl
|
|
|
3
3
|
import type { TransitionPose } from "./presentable"
|
|
4
4
|
import { _widgetPoseId } from "./transitions"
|
|
5
5
|
import { tree } from "./tree"
|
|
6
|
+
import { createBinding, disposeBinding, flushEffects, type Signal } from "../core/signals"
|
|
6
7
|
|
|
7
8
|
export type UIModalStyle = UIWidgetStyle
|
|
8
9
|
|
|
@@ -15,27 +16,49 @@ export type UIModalTransition = DrawableStyle & BaseStyle & {
|
|
|
15
16
|
duration?: number,
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
/** What `bindShow()` takes: a function of the data, or a signal the modal also writes (`false` on
|
|
20
|
+
* a dismissal, `show()` / `hide()` set it). */
|
|
21
|
+
export type UIModalShow = (() => boolean) | Signal<boolean>
|
|
22
|
+
|
|
18
23
|
const MODAL_ANIM_MS = 200
|
|
24
|
+
const SHOW_BINDING = "#show"
|
|
19
25
|
|
|
20
26
|
/**
|
|
21
27
|
* A dialog: a `UIWidget` with the modal boilerplate built in. Comes with a scrim
|
|
22
|
-
* (`overlayColor: "rgba(0, 0, 0, 0.5)"` unless overridden),
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* `
|
|
30
|
-
*
|
|
28
|
+
* (`overlayColor: "rgba(0, 0, 0, 0.5)"` unless overridden), is HIDDEN until shown, animates on
|
|
29
|
+
* every show / hide (a 200 ms fade by default — swap the pose via `transition()`; the runtime
|
|
30
|
+
* plays it as tracks that never touch the style).
|
|
31
|
+
*
|
|
32
|
+
* One state, shown or not, driven one of two ways:
|
|
33
|
+
* - `show()` / `hide()` from code — an action sheet opened by a button;
|
|
34
|
+
* - `bindShow(…)`: a function of the data (`bindShow(() => data.value.confirm)` — the data is the
|
|
35
|
+
* truth, `show()` / `hide()` are refused) or a `Signal<boolean>` the modal reads AND writes.
|
|
36
|
+
*
|
|
37
|
+
* A dismissal — the scrim tap, the back press, a sheet dragged below its lowest detent — is ONE
|
|
38
|
+
* request, `onDismiss`. By default it hides an unbound modal and writes `false` into a bound
|
|
39
|
+
* signal; a bound function has nowhere to write, so a modal bound to one without `onDismiss`
|
|
40
|
+
* cannot be dismissed (a forced choice — the same as `dismissible(false)`). `display` is not a
|
|
41
|
+
* modal's switch: a `.style({ display })` on it throws (a bound display died on the first
|
|
42
|
+
* dismissal, which wrote a static `display: "none"` over the binding — the dialog never opened
|
|
43
|
+
* again).
|
|
31
44
|
*/
|
|
32
45
|
export interface UIModal extends UIWidget {
|
|
33
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Bind the shown state. A function: the modal follows the data, and `show()` / `hide()` are
|
|
48
|
+
* errors — flip the data (`onDismiss` is where a dismissal goes). A signal: read and written —
|
|
49
|
+
* a dismissal sets it `false`, `show()` / `hide()` set it.
|
|
50
|
+
*
|
|
51
|
+
* ```ts
|
|
52
|
+
* UIModal(...).bindShow(() => data.value.confirm).onDismiss(on.cancel) // the data is the truth
|
|
53
|
+
* UIModal(...).bindShow(confirmOpen) // a Signal<boolean>
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
bindShow(value: UIModalShow): this,
|
|
57
|
+
/** Show and play the entrance transition. No-op while already shown. Bound to a signal: writes
|
|
58
|
+
* `true` into it; bound to a function: an error (flip the data). */
|
|
34
59
|
show(): void,
|
|
35
|
-
/** Play the exit transition, then hide. No-op while
|
|
60
|
+
/** Play the exit transition, then hide. No-op while hidden. Bound: as `show()`. */
|
|
36
61
|
hide(): void,
|
|
37
|
-
/** True from `show()` until `hide()` starts (already false during the exit animation). */
|
|
38
|
-
readonly isOpen: boolean,
|
|
39
62
|
|
|
40
63
|
/**
|
|
41
64
|
* Replace the show/hide animation. `hidden` is the off-screen pose — `show()` animates from
|
|
@@ -49,13 +72,17 @@ export interface UIModal extends UIWidget {
|
|
|
49
72
|
*/
|
|
50
73
|
transition(hidden: UIModalTransition): this,
|
|
51
74
|
|
|
52
|
-
/**
|
|
75
|
+
/** The user asked to close: a scrim tap, the back press, a sheet dragged below its lowest
|
|
76
|
+
* detent. Replaces the default (hide, or `false` into the signal): the callback decides —
|
|
77
|
+
* usually the action that flips the data the modal is bound to. */
|
|
78
|
+
onDismiss(callback: () => void): this,
|
|
79
|
+
/** Called when the modal is shown — whoever showed it. */
|
|
53
80
|
onOpen(callback: () => void): this,
|
|
54
|
-
/** Called when the modal starts
|
|
55
|
-
* block flipping its display. */
|
|
81
|
+
/** Called when the modal starts hiding — whoever hid it. */
|
|
56
82
|
onClose(callback: () => void): this,
|
|
57
|
-
/** `dismissible(false)
|
|
58
|
-
*
|
|
83
|
+
/** `dismissible(false)`: the dismissal gestures are off — the scrim tap does nothing, the back
|
|
84
|
+
* press is swallowed, a sheet resists below its lowest detent (a forced choice, the persistent
|
|
85
|
+
* map sheet). Default `true`. */
|
|
59
86
|
dismissible(enabled: boolean): this,
|
|
60
87
|
}
|
|
61
88
|
|
|
@@ -64,16 +91,32 @@ export class ModalElement extends WidgetElement {
|
|
|
64
91
|
_transition: UIModalTransition = { opacity: 0 }
|
|
65
92
|
readonly _openListeners: (() => void)[] = []
|
|
66
93
|
readonly _closeListeners: (() => void)[] = []
|
|
94
|
+
readonly _dismissListeners: (() => void)[] = []
|
|
95
|
+
/** @internal `bindShow` was called: show() / hide() are not the modal's to call (a signal's: they write it). */
|
|
96
|
+
_bound = false
|
|
97
|
+
/** @internal The signal `bindShow()` was given, which a dismissal writes. */
|
|
98
|
+
_showSignal?: Signal<boolean>
|
|
67
99
|
private _poseObj?: TransitionPose
|
|
68
100
|
private _poseKey = ""
|
|
69
101
|
|
|
70
102
|
constructor(style: UIModalStyle, children: UINodeChild[] | ChildrenFn) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
this.
|
|
103
|
+
// Hidden by default: a dialog is shown, never found open — show() / bindShow() flip it.
|
|
104
|
+
super("widget", { overlayColor: "rgba(0, 0, 0, 0.5)", display: "none", ...style }, children)
|
|
105
|
+
this.onOverlayTap(() => this._dismiss())
|
|
106
|
+
this.onBack(() => this._dismiss())
|
|
74
107
|
this._syncPose()
|
|
75
108
|
}
|
|
76
109
|
|
|
110
|
+
/** @internal `display` is not a modal's switch. Refused on every write of the app's — `.style({
|
|
111
|
+
* display })`, `el.style.display =` — and let through for the widget's own (`_ownWrite`). During
|
|
112
|
+
* the constructor the field is still undefined (it is initialised after `super()`), which lets
|
|
113
|
+
* the default `display: "none"` through. */
|
|
114
|
+
_mapStyle(style: any): void {
|
|
115
|
+
if ("display" in style && this._ownWrite === false) {
|
|
116
|
+
throw new Error("a modal's `display` is not its switch — a dialog, a sheet or a menu is shown with show() / hide(), or bound to the data with bindShow(…), never by its display")
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
77
120
|
// The pose the runtime plays on a display flip: the hidden pose + its duration, plus the scrim
|
|
78
121
|
// fade — only when the modal actually has a scrim (overlayColor: null opts out; a track on a
|
|
79
122
|
// null scrim would create the intercepting layer) and the pose doesn't drive the scrim itself.
|
|
@@ -106,27 +149,80 @@ export class ModalElement extends WidgetElement {
|
|
|
106
149
|
return this
|
|
107
150
|
}
|
|
108
151
|
|
|
152
|
+
bindShow(value: UIModalShow): this {
|
|
153
|
+
if (this._fx !== undefined) disposeBinding(this, SHOW_BINDING)
|
|
154
|
+
this._bound = true
|
|
155
|
+
let read: () => boolean
|
|
156
|
+
if (typeof value === "function") { this._showSignal = undefined; read = value }
|
|
157
|
+
else { this._showSignal = value; read = () => value.value }
|
|
158
|
+
// The first value is the START state (bound before the modal is placed, it shows with its
|
|
159
|
+
// place); every later one is a flip, played like a show() / hide().
|
|
160
|
+
let initial = true
|
|
161
|
+
createBinding(this, SHOW_BINDING, () => {
|
|
162
|
+
const shown = !!read()
|
|
163
|
+
if (this._h.id !== 0) {
|
|
164
|
+
if (initial && tree().parent(this._h.id) === 0) this._writeDisplay(shown ? "flex" : "none")
|
|
165
|
+
else this._setOpen(shown)
|
|
166
|
+
}
|
|
167
|
+
initial = false
|
|
168
|
+
})
|
|
169
|
+
this._syncDismissible()
|
|
170
|
+
return this
|
|
171
|
+
}
|
|
172
|
+
|
|
109
173
|
override show(): void {
|
|
110
|
-
if (this.
|
|
111
|
-
this.
|
|
112
|
-
|
|
174
|
+
if (this._showSignal) { this._showSignal.value = true; return }
|
|
175
|
+
if (this._bound) { console.error("show() on a modal bound with bindShow(() => …) — flip the data it follows"); return }
|
|
176
|
+
this._setOpen(true)
|
|
113
177
|
}
|
|
114
178
|
|
|
115
179
|
override hide(): void {
|
|
116
|
-
if (
|
|
180
|
+
if (this._showSignal) { this._showSignal.value = false; return }
|
|
181
|
+
if (this._bound) { console.error("hide() on a modal bound with bindShow(() => …) — flip the data it follows"); return }
|
|
182
|
+
this._setOpen(false)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
override _setOpen(open: boolean): void {
|
|
117
186
|
this._syncPose()
|
|
118
|
-
super.
|
|
187
|
+
super._setOpen(open)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** @internal A dismissal gesture — the scrim tap, the back press, a sheet dragged below its
|
|
191
|
+
* lowest detent — asks to close. The default hides an unbound modal and writes `false` into a
|
|
192
|
+
* bound signal; a bound function has no default (the data decides, through `onDismiss`). */
|
|
193
|
+
_dismiss(): void {
|
|
194
|
+
if (!this._dismissible) return
|
|
195
|
+
if (this._dismissListeners.length > 0) {
|
|
196
|
+
for (const callback of this._dismissListeners.slice()) callback()
|
|
197
|
+
flushEffects() // the data's flip lands now, so the sheet below knows whether it stays
|
|
198
|
+
this._afterDismiss()
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
if (this._showSignal) { this._showSignal.value = false; return }
|
|
202
|
+
if (!this._bound) this._setOpen(false)
|
|
119
203
|
}
|
|
120
204
|
|
|
121
|
-
|
|
122
|
-
|
|
205
|
+
/** @internal After the dismiss callbacks: a sheet still shown puts itself back on its detent. */
|
|
206
|
+
_afterDismiss(): void {}
|
|
207
|
+
|
|
208
|
+
/** @internal Can a gesture close it? What the host is told (a sheet's drag resists otherwise). */
|
|
209
|
+
_dismissAllowed(): boolean {
|
|
210
|
+
return this._dismissible && (!this._bound || this._showSignal !== undefined || this._dismissListeners.length > 0)
|
|
123
211
|
}
|
|
124
212
|
|
|
213
|
+
/** @internal The host's copy of `_dismissAllowed()` (a sheet writes `sheetDismissible`). */
|
|
214
|
+
_syncDismissible(): void {}
|
|
215
|
+
|
|
125
216
|
/** @internal onOpen / onClose follow the intent, whoever flipped it. */
|
|
126
217
|
override _shownChanged(shown: boolean): void {
|
|
127
218
|
for (const callback of (shown ? this._openListeners : this._closeListeners).slice()) callback()
|
|
128
219
|
}
|
|
129
220
|
|
|
221
|
+
onDismiss(callback: () => void): this {
|
|
222
|
+
this._dismissListeners.push(callback)
|
|
223
|
+
this._syncDismissible()
|
|
224
|
+
return this
|
|
225
|
+
}
|
|
130
226
|
onOpen(callback: () => void): this {
|
|
131
227
|
this._openListeners.push(callback)
|
|
132
228
|
return this
|
|
@@ -137,15 +233,16 @@ export class ModalElement extends WidgetElement {
|
|
|
137
233
|
}
|
|
138
234
|
dismissible(enabled: boolean): this {
|
|
139
235
|
this._dismissible = enabled
|
|
236
|
+
this._syncDismissible()
|
|
140
237
|
return this
|
|
141
238
|
}
|
|
142
239
|
}
|
|
143
240
|
|
|
144
241
|
const makeModal = (style: any, children: any) => new ModalElement(style, children)
|
|
145
242
|
|
|
146
|
-
/** Create a modal dialog (see {@link UIModal}):
|
|
147
|
-
* `
|
|
148
|
-
* Same argument forms as `UIColumn`. */
|
|
243
|
+
/** Create a modal dialog (see {@link UIModal}): give it to its screen (`.widgets(dialog)`) and
|
|
244
|
+
* `show()` it from a tap, bind it to the data with `bindShow(() => …)`, or `show()` a parentless
|
|
245
|
+
* one from anywhere. Same argument forms as `UIColumn`. */
|
|
149
246
|
export function UIModal(...children: UIChildArg[]): UIModal;
|
|
150
247
|
export function UIModal(children: UINodeChild[] | ChildrenFn): UIModal;
|
|
151
248
|
export function UIModal(...args: any[]): UIModal {
|
package/src/ui/UIWidget.ts
CHANGED
|
@@ -73,15 +73,25 @@ export class WidgetElement extends ContainerElement<"widget"> {
|
|
|
73
73
|
// factory) so subclasses like ModalElement inherit it.
|
|
74
74
|
super(type, { position: "absolute", ...style }, children)
|
|
75
75
|
}
|
|
76
|
-
show(): void {
|
|
76
|
+
show(): void { this._setOpen(true) }
|
|
77
|
+
hide(): void { this._setOpen(false) }
|
|
78
|
+
/** @internal The body of show() / hide(): what every way of opening a widget runs — the calls,
|
|
79
|
+
* a modal's `open` binding. Subclasses wrap it (a modal syncs its pose, a sheet its detent). */
|
|
80
|
+
_setOpen(open: boolean): void {
|
|
77
81
|
if (this._h.id === 0) return
|
|
82
|
+
if (!open) {
|
|
83
|
+
if (!this._shown) return
|
|
84
|
+
this._setShown(false)
|
|
85
|
+
this._writeDisplay("none")
|
|
86
|
+
return
|
|
87
|
+
}
|
|
78
88
|
const parentId = tree().parent(this._h.id)
|
|
79
89
|
if (parentId !== 0 && this._shown) return
|
|
80
90
|
if (parentId === 0) {
|
|
81
91
|
// No place of its own: the app layer takes it HIDDEN, and the flip below shows it — a show()
|
|
82
92
|
// is a display flip wherever the widget is, so its pose plays (a modal's fade) exactly as on
|
|
83
93
|
// a placed one. Appended shown, it would merely appear: a widget shows with its place, no pose.
|
|
84
|
-
this.
|
|
94
|
+
this._writeDisplay("none")
|
|
85
95
|
_appLayer().append(this as any) // pins it: _pinned reads the display
|
|
86
96
|
} else {
|
|
87
97
|
// Shown = on top of its root: a show() moves the widget to the TAIL of its parent's children
|
|
@@ -94,13 +104,16 @@ export class WidgetElement extends ContainerElement<"widget"> {
|
|
|
94
104
|
tree().insert(parentId, this._h.id, siblings.length - 1)
|
|
95
105
|
}
|
|
96
106
|
}
|
|
97
|
-
this.
|
|
107
|
+
this._writeDisplay("flex")
|
|
98
108
|
this._setShown(true)
|
|
99
109
|
}
|
|
100
|
-
hide
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
110
|
+
/** @internal True while the widget writes its own display (show / hide / open): the one display
|
|
111
|
+
* write a modal lets through — the app's `display` on a modal is refused (ModalElement._mapStyle). */
|
|
112
|
+
_ownWrite = false
|
|
113
|
+
/** @internal The widget's own display write. */
|
|
114
|
+
_writeDisplay(display: "flex" | "none"): void {
|
|
115
|
+
this._ownWrite = true
|
|
116
|
+
try { this.style({ display } as any) } finally { this._ownWrite = false }
|
|
104
117
|
}
|
|
105
118
|
get isShown(): boolean {
|
|
106
119
|
return this._shown
|
|
@@ -115,7 +128,7 @@ export class WidgetElement extends ContainerElement<"widget"> {
|
|
|
115
128
|
}
|
|
116
129
|
if (owner) {
|
|
117
130
|
// Hidden first: a widget shows with its place, and the old contract showed it on show() only.
|
|
118
|
-
this.
|
|
131
|
+
this._writeDisplay("none")
|
|
119
132
|
owner.widgets.append(this as any)
|
|
120
133
|
}
|
|
121
134
|
return this
|
package/src/version.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// same manifest). Its MAJOR is the bundle ↔ runtime contract: a bundle's `// sdk:` header line
|
|
5
5
|
// and a host's embedded version must agree on the major for the host to run the bundle; minor
|
|
6
6
|
// and patch never gate anything (features are detected, never versioned).
|
|
7
|
-
export const SDK_VERSION = "2.
|
|
7
|
+
export const SDK_VERSION = "2.6.0"
|
|
8
8
|
|
|
9
9
|
/** The major of a semver string, or null when it is not one. */
|
|
10
10
|
export const sdkMajor = (version: string | null | undefined): number | null => {
|