lecodes-cli 0.7.1 → 0.8.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/README.md +3 -3
- package/dist/index.js +2941 -745
- package/package.json +4 -4
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk/canvas/Canvas.ts +16 -0
- package/runtime/sdk/gl/Material.ts +4 -0
- package/runtime/sdk/inject.ts +8 -5
- package/runtime/sdk/plugins/service.ts +47 -0
- package/runtime/sdk/runtime/app.ts +21 -1
- package/runtime/sdk/runtime/appEvents.ts +25 -0
- package/runtime/sdk/runtime/service.ts +3 -2
- package/runtime/sdk/ui/NativeView.ts +2 -1
- package/runtime/sdk/ui/UI.ts +3 -1
- package/runtime/sdk/ui/UIButton.ts +10 -5
- package/runtime/sdk/ui/UIContainer.ts +16 -9
- package/runtime/sdk/ui/UIImage.ts +5 -2
- package/runtime/sdk/ui/UIInput.ts +75 -8
- package/runtime/sdk/ui/UIModal.ts +149 -0
- package/runtime/sdk/ui/UINode.ts +126 -55
- package/runtime/sdk/ui/UIPager.ts +297 -0
- package/runtime/sdk/ui/UIPopover.ts +106 -0
- package/runtime/sdk/ui/UIScreen.ts +11 -10
- package/runtime/sdk/ui/UIScrollable.ts +2 -1
- package/runtime/sdk/ui/UISpacer.ts +1 -1
- package/runtime/sdk/ui/UIText.ts +2 -1
- package/runtime/sdk/ui/UIVideo.ts +2 -1
- package/runtime/sdk/ui/UIVirtualizedList.ts +2 -1
- package/runtime/sdk/ui/UIWidget.ts +19 -22
- package/runtime/sdk/ui/theme.ts +66 -0
- package/runtime/sdk-types.json +1 -1
- package/runtime/sdk/compile/aspectMacro.ts +0 -42
- package/runtime/sdk/compile/assetMacro.ts +0 -45
- package/runtime/sdk/compile/bundler.ts +0 -205
- package/runtime/sdk/compile/compileProject.ts +0 -108
- package/runtime/sdk/compile/header.ts +0 -46
- package/runtime/sdk/compile/index.ts +0 -18
- package/runtime/sdk/compile/libraryImports.ts +0 -47
- package/runtime/sdk/compile/sceneEditor.ts +0 -78
- package/runtime/sdk/compile/sourcemap.ts +0 -25
- package/runtime/sdk/kit/UITabs.ts +0 -105
- package/runtime/sdk/ui/UIScreenHost.ts +0 -213
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { ModalElement, type UIModal, type UIModalStyle } from "./UIModal"
|
|
2
|
+
import { Presentable } from "./presentable"
|
|
3
|
+
import { device } from "../runtime/device"
|
|
4
|
+
import type { BoundingClientRect, ChildrenFn, UINodeChild } from "./UINode"
|
|
5
|
+
|
|
6
|
+
export type UIPopoverStyle = UIModalStyle
|
|
7
|
+
|
|
8
|
+
/** What a popover can anchor to: any element (it reads `getBoundingClientRect()`), or a raw
|
|
9
|
+
* `{x, y}` point — a long-press context menu anchors to the touch position. */
|
|
10
|
+
export type UIPopoverAnchor = { getBoundingClientRect(): BoundingClientRect | null } | { x: number, y: number }
|
|
11
|
+
|
|
12
|
+
const POPOVER_ANIM_MS = 120
|
|
13
|
+
const ANCHOR_GAP = 4 // between the anchor's edge and the popover
|
|
14
|
+
const VIEWPORT_MARGIN = 8 // minimum distance kept to every screen edge
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* An anchored menu: a `UIModal` whose scrim is transparent (invisible but still intercepting —
|
|
18
|
+
* an outside tap dismisses, and nothing underneath can scroll while it's open) and whose
|
|
19
|
+
* position comes from the anchor passed to `show(anchor)`: below the anchor's left edge,
|
|
20
|
+
* flipped above when there's no room, clamped into the viewport. Position is computed once per
|
|
21
|
+
* show — anchored popovers don't follow their anchor. Backs dropdowns, context menus, and
|
|
22
|
+
* tooltips; create it **once** at module scope and reuse it.
|
|
23
|
+
*/
|
|
24
|
+
export interface UIPopover extends UIModal {
|
|
25
|
+
/** Position next to `anchor` (an element or an `{x, y}` point), attach to
|
|
26
|
+
* `Presentable.current` (unless an owner was set via `attachTo`), and play the entrance
|
|
27
|
+
* transition. Without an anchor the popover shows wherever its own style puts it. */
|
|
28
|
+
show(anchor?: UIPopoverAnchor): void,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class PopoverElement extends ModalElement {
|
|
32
|
+
private _anchorRect?: { left: number, top: number, right: number, bottom: number }
|
|
33
|
+
private _placedLeft?: number
|
|
34
|
+
private _placedTop?: number
|
|
35
|
+
|
|
36
|
+
constructor(style: UIPopoverStyle, children: UINodeChild[] | ChildrenFn) {
|
|
37
|
+
super({ overlayColor: "transparent", ...style }, children)
|
|
38
|
+
this.transition({ opacity: 0, duration: POPOVER_ANIM_MS })
|
|
39
|
+
// The flip/clamp decision needs the popover's measured size, which exists only after the
|
|
40
|
+
// mount lays it out — so placement is corrected on layout, inside the entrance fade.
|
|
41
|
+
this.onLayout(({ width, height }) => this._place(width, height))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
show(anchor?: UIPopoverAnchor): void {
|
|
45
|
+
if (anchor) {
|
|
46
|
+
const r = "getBoundingClientRect" in anchor
|
|
47
|
+
? anchor.getBoundingClientRect()
|
|
48
|
+
: { left: anchor.x, top: anchor.y, right: anchor.x, bottom: anchor.y }
|
|
49
|
+
if (r) {
|
|
50
|
+
this._anchorRect = { left: r.left, top: r.top, right: r.right, bottom: r.bottom }
|
|
51
|
+
this._placedLeft = undefined
|
|
52
|
+
this._placedTop = undefined
|
|
53
|
+
// Preliminary spot (below, left-aligned) so the first paint is already close; _place
|
|
54
|
+
// refines it once the size is known.
|
|
55
|
+
this.style({ left: r.left, top: r.bottom + ANCHOR_GAP })
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
this._anchorRect = undefined // unanchored show: the user's own style owns the position
|
|
59
|
+
}
|
|
60
|
+
// A menu belongs to the page it opened on: it hides with it and rides its transition. An
|
|
61
|
+
// explicit attachTo() owner (e.g. a scene HUD's popover) is respected.
|
|
62
|
+
if (!this._owner) this.attachTo(Presentable.current)
|
|
63
|
+
super.show()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private _place(width: number, height: number): void {
|
|
67
|
+
const anchor = this._anchorRect
|
|
68
|
+
if (!anchor || !this._open) return
|
|
69
|
+
const displayWidth = device.width, displayHeight = device.height // 0 on hosts without a size
|
|
70
|
+
let left = anchor.left
|
|
71
|
+
let top = anchor.bottom + ANCHOR_GAP
|
|
72
|
+
if (displayWidth > 0) {
|
|
73
|
+
left = Math.min(left, displayWidth - width - VIEWPORT_MARGIN)
|
|
74
|
+
left = Math.max(left, VIEWPORT_MARGIN)
|
|
75
|
+
}
|
|
76
|
+
if (displayHeight > 0 && top + height > displayHeight - VIEWPORT_MARGIN) {
|
|
77
|
+
const above = anchor.top - ANCHOR_GAP - height
|
|
78
|
+
top = above >= VIEWPORT_MARGIN ? above : Math.max(VIEWPORT_MARGIN, displayHeight - VIEWPORT_MARGIN - height)
|
|
79
|
+
}
|
|
80
|
+
// Idempotence guard: hosts re-fire onLayout on recalc, and writing the same position would
|
|
81
|
+
// trigger another recalc — only write when the target actually moved.
|
|
82
|
+
if (this._placedLeft !== undefined && Math.abs(left - this._placedLeft) < 0.5
|
|
83
|
+
&& this._placedTop !== undefined && Math.abs(top - this._placedTop) < 0.5) return
|
|
84
|
+
this._placedLeft = left
|
|
85
|
+
this._placedTop = top
|
|
86
|
+
this.style({ left, top })
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function UIPopover(): UIPopover;
|
|
91
|
+
export function UIPopover(children: UINodeChild[] | ChildrenFn): UIPopover;
|
|
92
|
+
export function UIPopover(style: UIPopoverStyle): UIPopover;
|
|
93
|
+
export function UIPopover(style: UIPopoverStyle, children: UINodeChild[] | ChildrenFn): UIPopover;
|
|
94
|
+
export function UIPopover(...args: [] | [UINodeChild[] | ChildrenFn | UIPopoverStyle] | [UIPopoverStyle, children: UINodeChild[] | ChildrenFn]): UIPopover {
|
|
95
|
+
if (args.length === 0) {
|
|
96
|
+
return new PopoverElement({}, [])
|
|
97
|
+
} else if (args.length === 1) {
|
|
98
|
+
if (Array.isArray(args[0]) || typeof args[0] === "function") {
|
|
99
|
+
return new PopoverElement({}, args[0])
|
|
100
|
+
} else {
|
|
101
|
+
return new PopoverElement(args[0], [])
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
return new PopoverElement(args[0], args[1])
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TouchStartEvent } from "../runtime/touch"
|
|
2
2
|
import { _bumpNavEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "./presentable"
|
|
3
|
-
import { ContainerElement, type AnimateStyle, type BaseStyle, type Color, type ContainerStyle, type DrawableStyle, type OnLayoutCallback, type PaddingStyle, type ScrollStyle, type Style, type UINodeChild, type ChildrenFn, } from "./UINode"
|
|
3
|
+
import { ContainerElement, type AnimateStyle, type BaseStyle, type Color, type ContainerStyle, type DrawableStyle, type OnLayoutCallback, type BoundingClientRect, type PaddingStyle, type ScrollStyle, type Style, type UINodeChild, type ChildrenFn, } from "./UINode"
|
|
4
4
|
|
|
5
5
|
export type UIScreenStyle = ContainerStyle & DrawableStyle & PaddingStyle & ScrollStyle & BaseStyle & ScrollStyle & { refreshControlColor?: Color }
|
|
6
6
|
|
|
@@ -8,11 +8,11 @@ export type UIScreenStyle = ContainerStyle & DrawableStyle & PaddingStyle & Scro
|
|
|
8
8
|
// plain `UIScreen`, and a `UIScrollableScreen` (a screen that also scrolls). `makeScrollable()`
|
|
9
9
|
// upgrades a plain screen to the scrollable one, where `onScroll`/`onOverscroll`/`onRefresh` live —
|
|
10
10
|
// they mean nothing without scrolling. `UIScrollableScreen extends UIScreen`, so anywhere a
|
|
11
|
-
// `UIScreen`
|
|
11
|
+
// `UIScreen` is accepted (router pages, `UIPager`), a scrollable one works too.
|
|
12
12
|
//
|
|
13
13
|
// This is a subtype split, NOT a `UIScreen<Scrollable>` generic: a generic can't work while `style`
|
|
14
14
|
// is `Style<this,…>`, because `this` is then contravariant and the type becomes invariant in its own
|
|
15
|
-
// params — that was the old `UIScreen<false>`-not-assignable-to-`
|
|
15
|
+
// params — that was the old `UIScreen<false>`-not-assignable-to-`UIScreen<boolean>` bug. See the StyleFn
|
|
16
16
|
// note in UINode.ts; the contract is pinned by tests/ui-types.test.ts.
|
|
17
17
|
export interface UIScreen {
|
|
18
18
|
readonly type: "screen",
|
|
@@ -41,17 +41,18 @@ export interface UIScreen {
|
|
|
41
41
|
onClose(callback: () => void): this
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Keep this screen's built native tree alive when a
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
44
|
+
* Keep this screen's built native tree alive when a host detaches it: instead of being destroyed,
|
|
45
|
+
* its views + layout + live state (scroll position, input text) are retained, so re-hosting the
|
|
46
|
+
* SAME screen object restores it in place rather than rebuilding. `onClose`/`onOpen` still fire on
|
|
47
|
+
* detach/re-attach. You own the lifetime — a kept tree is freed only by `dispose()` or when the
|
|
48
|
+
* project unloads. (Native-only today; web rebuilds.)
|
|
49
49
|
*/
|
|
50
50
|
keepAlive(enabled?: boolean): this
|
|
51
51
|
/** Free a `keepAlive()` screen's retained tree now. No-op unless it's currently detached-and-kept. */
|
|
52
52
|
dispose(): this
|
|
53
53
|
|
|
54
54
|
onLayout(onLayout: OnLayoutCallback): this
|
|
55
|
+
getBoundingClientRect(): BoundingClientRect | null
|
|
55
56
|
|
|
56
57
|
setClass(name: string, enabled: boolean): this
|
|
57
58
|
toggleClass(name: string): this
|
|
@@ -63,8 +64,8 @@ export interface UIScreen {
|
|
|
63
64
|
|
|
64
65
|
/**
|
|
65
66
|
* A `UIScreen` in scroll mode — what `makeScrollable()` returns. Adds the scroll lifecycle that a
|
|
66
|
-
* plain screen doesn't carry. A subtype of `UIScreen`, so it's accepted anywhere a screen
|
|
67
|
-
*
|
|
67
|
+
* plain screen doesn't carry. A subtype of `UIScreen`, so it's accepted anywhere a screen is
|
|
68
|
+
* (router pages, `UIPager`).
|
|
68
69
|
*/
|
|
69
70
|
export interface UIScrollableScreen extends UIScreen {
|
|
70
71
|
readonly scrollable: true,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UIContainerStyle } from "./UIContainer"
|
|
2
|
-
import { type Style, type AnimateStyle, type UINodeChild, type ChildrenFn, type OnLayoutCallback, ContainerElement } from "./UINode"
|
|
2
|
+
import { type Style, type AnimateStyle, type UINodeChild, type ChildrenFn, type OnLayoutCallback, type BoundingClientRect, ContainerElement } from "./UINode"
|
|
3
3
|
|
|
4
4
|
export type ScrollStyle = { showScrollbar?: boolean, overscrollMode?: "none" | "absorb" | "default" }
|
|
5
5
|
|
|
@@ -17,6 +17,7 @@ export interface UIScrollable {
|
|
|
17
17
|
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
18
18
|
|
|
19
19
|
onLayout(onLayout: OnLayoutCallback): this
|
|
20
|
+
getBoundingClientRect(): BoundingClientRect | null
|
|
20
21
|
|
|
21
22
|
setClass(name: string, enabled: boolean): this
|
|
22
23
|
toggleClass(name: string): this
|
|
@@ -14,5 +14,5 @@ export interface UISpacer {
|
|
|
14
14
|
export function UISpacer(): UISpacer
|
|
15
15
|
export function UISpacer(style: UISpacerStyle): UISpacer
|
|
16
16
|
export function UISpacer(style?: UISpacerStyle): UISpacer {
|
|
17
|
-
return new Element("spacer", style
|
|
17
|
+
return new Element("spacer", { flexGrow: 1, ...style })
|
|
18
18
|
}
|
package/runtime/sdk/ui/UIText.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createBinding } from "../core/signals"
|
|
2
|
-
import { Element, type AnimateStyle, type BaseStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type Style, type TextStyle } from "./UINode"
|
|
2
|
+
import { Element, type AnimateStyle, type BaseStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type BoundingClientRect, type Style, type TextStyle } from "./UINode"
|
|
3
3
|
|
|
4
4
|
export type UITextStyle = ElementStyle & TextStyle & DrawableStyle
|
|
5
5
|
|
|
@@ -11,6 +11,7 @@ export interface UIText {
|
|
|
11
11
|
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle & TextStyle>
|
|
12
12
|
|
|
13
13
|
onLayout(onLayout: OnLayoutCallback): this
|
|
14
|
+
getBoundingClientRect(): BoundingClientRect | null
|
|
14
15
|
|
|
15
16
|
setClass(name: string, enabled: boolean): this
|
|
16
17
|
toggleClass(name: string): this
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { VideoPlayer } from "../runtime/media"
|
|
2
2
|
import { _bumpNavEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "./presentable"
|
|
3
|
-
import { Element, type AnimateStyle, type AppearStyle, type BaseStyle, type DisappearStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type Style, type StyleFn } from "./UINode"
|
|
3
|
+
import { Element, type AnimateStyle, type AppearStyle, type BaseStyle, type DisappearStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type BoundingClientRect, type Style, type StyleFn } from "./UINode"
|
|
4
4
|
|
|
5
5
|
export type UIVideoStyle = ElementStyle & DrawableStyle & { objectFit?: "cover" | "contain" | "fill" }
|
|
6
6
|
|
|
@@ -20,6 +20,7 @@ export interface UIVideo {
|
|
|
20
20
|
onBackPressed(callback: () => void): this
|
|
21
21
|
|
|
22
22
|
onLayout(onLayout: OnLayoutCallback): this
|
|
23
|
+
getBoundingClientRect(): BoundingClientRect | null
|
|
23
24
|
|
|
24
25
|
setClass(name: string, enabled: boolean): this
|
|
25
26
|
toggleClass(name: string): this
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Element, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type PaddingStyle, type Style, type UINodeChild } from "./UINode"
|
|
1
|
+
import { Element, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type BoundingClientRect, type PaddingStyle, type Style, type UINodeChild } from "./UINode"
|
|
2
2
|
|
|
3
3
|
export type UIVirtualizedListStyle = DrawableStyle & ElementStyle
|
|
4
4
|
|
|
@@ -45,6 +45,7 @@ export interface UIVirtualizedList<T = unknown> {
|
|
|
45
45
|
/** Порог от верхнего края — подгрузка истории в чате */
|
|
46
46
|
onStartReached(thresholdPx: number, callback: () => void): this
|
|
47
47
|
onLayout(onLayout: OnLayoutCallback): this
|
|
48
|
+
getBoundingClientRect(): BoundingClientRect | null
|
|
48
49
|
|
|
49
50
|
setClass(name: string, enabled: boolean): this
|
|
50
51
|
toggleClass(name: string): this
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TouchStartEvent } from "../runtime/touch"
|
|
2
2
|
import { _descOf, _navSupported, Presentable } from "./presentable"
|
|
3
|
-
import { ContainerElement, type AnimateStyle, type BaseStyle, type Color, type ContainerStyle, type DrawableStyle, type OnLayoutCallback, type PaddingStyle, type PositionStyle, type Style, type UINodeChild, type ChildrenFn } from "./UINode"
|
|
3
|
+
import { ContainerElement, type AnimateStyle, type BaseStyle, type Color, type ContainerStyle, type DrawableStyle, type OnLayoutCallback, type BoundingClientRect, type PaddingStyle, type PositionStyle, type Style, type UINodeChild, type ChildrenFn } from "./UINode"
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
export type UIWidgetStyle = ContainerStyle & DrawableStyle & PaddingStyle & BaseStyle & PositionStyle & {
|
|
@@ -17,22 +17,18 @@ export interface UIWidget {
|
|
|
17
17
|
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle & { overlayColor?: Color | null }>
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* one until `attachTo(null)` (= back to automatic). Set it while the widget is hidden.
|
|
20
|
+
* Attach this widget to a destination (a `Scene`, a `UIScreen`, …): the widget lives on that
|
|
21
|
+
* page's layer — it shows/hides with the page and rides its transition, and a screen pushed
|
|
22
|
+
* on top covers it (a scene HUD behaves like part of the scene). Sticky until `attachTo(null)`
|
|
23
|
+
* (= back to a global overlay). Set it while the widget is hidden.
|
|
25
24
|
*/
|
|
26
25
|
attachTo(owner: Presentable | null): this,
|
|
27
26
|
|
|
28
|
-
/** Show the widget
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
27
|
+
/** Show the widget. By default it is a global overlay above every destination — it stays
|
|
28
|
+
* visible across navigation until `hide()` (dialogs, mini-player, global loader). If the
|
|
29
|
+
* widget was attached via `attachTo(owner)`, it mounts on that page's layer instead and is
|
|
30
|
+
* only visible while the owner is presented. */
|
|
32
31
|
show(): void,
|
|
33
|
-
/** Show the widget as a global overlay above every destination — it stays visible across
|
|
34
|
-
* navigation (mini-player, global loader). */
|
|
35
|
-
showDetached(): void,
|
|
36
32
|
hide(): void,
|
|
37
33
|
|
|
38
34
|
readonly isShow: boolean,
|
|
@@ -45,6 +41,7 @@ export interface UIWidget {
|
|
|
45
41
|
readonly children: UINodeChild[],
|
|
46
42
|
|
|
47
43
|
onLayout(onLayout: OnLayoutCallback): this
|
|
44
|
+
getBoundingClientRect(): BoundingClientRect | null
|
|
48
45
|
onTouchStart(callback: (ev: TouchStartEvent<UIWidget>) => void): this,
|
|
49
46
|
onOverlayTap(callback: () => void): this,
|
|
50
47
|
onBackPressed(callback: () => void): this
|
|
@@ -59,24 +56,24 @@ export interface UIWidget {
|
|
|
59
56
|
export class WidgetElement extends ContainerElement<"widget"> {
|
|
60
57
|
scrollable = false as const
|
|
61
58
|
_owner?: Presentable
|
|
59
|
+
constructor(type: "widget", style: UIWidgetStyle, children: UINodeChild[] | ChildrenFn) {
|
|
60
|
+
// Widgets are absolutely positioned in device coordinates — in the constructor (not the
|
|
61
|
+
// factory) so subclasses like ModalElement inherit it.
|
|
62
|
+
super(type, { position: "absolute", ...style }, children)
|
|
63
|
+
}
|
|
62
64
|
attachTo(owner: Presentable | null): this {
|
|
63
65
|
this._owner = owner ?? undefined
|
|
64
66
|
return this
|
|
65
67
|
}
|
|
66
68
|
show(): void {
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (owner && _navSupported()) {
|
|
71
|
-
_creatorUI.showWidget(this, _descOf(owner))
|
|
69
|
+
// Global overlay unless explicitly attached via attachTo(owner).
|
|
70
|
+
if (this._owner && _navSupported()) {
|
|
71
|
+
_creatorUI.showWidget(this, _descOf(this._owner))
|
|
72
72
|
} else {
|
|
73
|
-
//
|
|
73
|
+
// Unattached (or legacy host): global overlay above navigation.
|
|
74
74
|
_creatorUI.showWidget(this)
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
-
showDetached(): void {
|
|
78
|
-
_creatorUI.showWidget(this)
|
|
79
|
-
}
|
|
80
77
|
hide(): void {
|
|
81
78
|
_creatorUI.hideWidget(this)
|
|
82
79
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// theme() — app theme variables (docs/ui-theme-plan.md).
|
|
2
|
+
//
|
|
3
|
+
// One global variable table lives in the UI core. Styles reference entries with CSS syntax —
|
|
4
|
+
// `color: "var(--primaryColor)"` — resolved at style-apply time in the core, so re-calling
|
|
5
|
+
// theme() with new values re-styles the LIVE UI (dark mode is just a second call). Two access
|
|
6
|
+
// forms with one rule: bare style tokens (`pt: "comfort-top"`) are keywords that may expand to
|
|
7
|
+
// formulas (max(safe-top, knob)); `var(--comfort-top)` always reads the raw table value (the
|
|
8
|
+
// knob itself).
|
|
9
|
+
//
|
|
10
|
+
// theme(values) MERGES into the table and returns typed accessors whose values ARE the var()
|
|
11
|
+
// strings — define once in a module, import where custom keys need typing:
|
|
12
|
+
//
|
|
13
|
+
// export const T = theme({ primaryColor: "#0A84FF", cardBg: "#1C1C1E" })
|
|
14
|
+
// card.style({ bgColor: T.cardBg }) // T.cardBg === "var(--cardBg)"
|
|
15
|
+
//
|
|
16
|
+
// System vars need no import — they're static properties on the callable itself:
|
|
17
|
+
// theme.primaryColor, theme.fontFamily, theme["comfort-top"], …
|
|
18
|
+
//
|
|
19
|
+
// Values: strings pass through (colors, fonts, expressions like "max(safe-top, 24px)");
|
|
20
|
+
// numbers are LENGTHS (px). `null` removes a key (comfort knobs reset to their defaults:
|
|
21
|
+
// 12 vertical, 16 horizontal — the page gutter). Env names (safe-*, vw/vh/vmin/vmax) are
|
|
22
|
+
// host-owned facts, never theme keys — they're skipped with a warning.
|
|
23
|
+
|
|
24
|
+
export type ThemeValues = Record<string, string | number | null>
|
|
25
|
+
|
|
26
|
+
export type ThemeAccessors<T extends ThemeValues> = {
|
|
27
|
+
readonly [K in keyof T]: K extends string ? `var(--${K})` : never
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const RESERVED = ["safe-top", "safe-bottom", "safe-left", "safe-right", "vw", "vh", "vmin", "vmax"]
|
|
31
|
+
|
|
32
|
+
const setTheme = <T extends ThemeValues>(values: T): ThemeAccessors<T> => {
|
|
33
|
+
const accessors: Record<string, string> = {}
|
|
34
|
+
let send: ThemeValues = values
|
|
35
|
+
for (const key of Object.keys(values)) {
|
|
36
|
+
if (RESERVED.includes(key)) {
|
|
37
|
+
console.warn(`theme(): "${key}" is an environment value (host-owned) — ignored`)
|
|
38
|
+
if (send === values) send = { ...values }
|
|
39
|
+
delete send[key]
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
accessors[key] = `var(--${key})`
|
|
43
|
+
}
|
|
44
|
+
// Optional during rollout: hosts without the method simply don't theme (values still type).
|
|
45
|
+
_creatorUI.setTheme?.(send)
|
|
46
|
+
return accessors as ThemeAccessors<T>
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const theme = Object.assign(setTheme, {
|
|
50
|
+
/** The app accent color. Kit components will read it via `var(--primaryColor, <fallback>)`. */
|
|
51
|
+
primaryColor: "var(--primaryColor)",
|
|
52
|
+
/** Secondary / inactive content color for kit components. */
|
|
53
|
+
mutedColor: "var(--mutedColor)",
|
|
54
|
+
/** DEFAULT text color — text without an explicit `color` follows it (hosts source their
|
|
55
|
+
* default from the theme; unset = the host default, white on dark screens). Set it once for
|
|
56
|
+
* a light theme instead of `color: "black"` on every label. */
|
|
57
|
+
color: "var(--color)",
|
|
58
|
+
/** DEFAULT text font — text without an explicit `fontFamily` follows it. */
|
|
59
|
+
fontFamily: "var(--fontFamily)",
|
|
60
|
+
/** Raw comfort knobs (logical px). The bare `comfort-top` style TOKEN applies the safe-area
|
|
61
|
+
* formula — these accessors read the knob value itself, for manual composition. */
|
|
62
|
+
"comfort-top": "var(--comfort-top)",
|
|
63
|
+
"comfort-bottom": "var(--comfort-bottom)",
|
|
64
|
+
"comfort-left": "var(--comfort-left)",
|
|
65
|
+
"comfort-right": "var(--comfort-right)",
|
|
66
|
+
} as const)
|