lecodes-cli 0.10.4 → 0.11.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 +1 -1
- package/dist/index.js +3117 -1575
- package/package.json +1 -1
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk/compile/aspectMacro.ts +42 -0
- package/runtime/sdk/compile/assetIconMacro.ts +382 -0
- package/runtime/sdk/compile/assetMacro.ts +45 -0
- package/runtime/sdk/compile/assetName.ts +50 -0
- package/runtime/sdk/compile/bundler.ts +244 -0
- package/runtime/sdk/compile/compileProject.ts +106 -0
- package/runtime/sdk/compile/detectEntry.ts +125 -0
- package/runtime/sdk/compile/fontMacro.ts +459 -0
- package/runtime/sdk/compile/fontRegistry.ts +78 -0
- package/runtime/sdk/compile/header.ts +55 -0
- package/runtime/sdk/compile/index.ts +53 -0
- package/runtime/sdk/compile/libraryImports.ts +52 -0
- package/runtime/sdk/compile/sceneEditor.ts +78 -0
- package/runtime/sdk/compile/sfnt.ts +98 -0
- package/runtime/sdk/compile/sourcemap.ts +25 -0
- package/runtime/sdk/inject.ts +2 -0
- package/runtime/sdk/ui/NativeView.ts +3 -0
- package/runtime/sdk/ui/UI.ts +9 -1
- package/runtime/sdk/ui/UIBottomSheet.ts +10 -16
- package/runtime/sdk/ui/UIButton.ts +18 -16
- package/runtime/sdk/ui/UIContainer.ts +38 -46
- package/runtime/sdk/ui/UIImage.ts +8 -5
- package/runtime/sdk/ui/UIInput.ts +6 -6
- package/runtime/sdk/ui/UIModal.ts +10 -16
- package/runtime/sdk/ui/UINode.ts +105 -36
- package/runtime/sdk/ui/UIPager.ts +27 -15
- package/runtime/sdk/ui/UIPopover.ts +10 -16
- package/runtime/sdk/ui/UIScreen.ts +20 -21
- package/runtime/sdk/ui/UIScrollable.ts +37 -29
- package/runtime/sdk/ui/UISpacer.ts +2 -0
- package/runtime/sdk/ui/UITabs.ts +175 -0
- package/runtime/sdk/ui/UIText.ts +5 -0
- package/runtime/sdk/ui/UIVideo.ts +5 -0
- package/runtime/sdk/ui/UIVirtualizedList.ts +33 -39
- package/runtime/sdk/ui/UIWidget.ts +22 -18
- package/runtime/sdk/ui/fonts.ts +3 -0
- package/runtime/sdk/ui/router.ts +13 -0
- package/runtime/sdk/ui/theme.ts +21 -30
- package/runtime/sdk-types.json +1 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Pre-chisel rewrite of user-library bare-specifier imports. chisel's resolver hard-errors on any
|
|
2
|
+
// bare import (the whole SDK is globals), so a project that imports a component library by its slug
|
|
3
|
+
// —`import { Card } from "acme-ui"`— is rewritten here to the injected library tree under
|
|
4
|
+
// `/__lib/<slug>/…` BEFORE chisel runs. Only libraries present in the dependency map are known
|
|
5
|
+
// slugs; any other bare specifier is an author error, surfaced with a friendly message instead of
|
|
6
|
+
// chisel's generic one. Static string-literal specifiers only — dynamic `import()` is unsupported
|
|
7
|
+
// (documented). Chisel itself is untouched. See docs/library-system-plan.md §4.
|
|
8
|
+
|
|
9
|
+
/** Cheap pre-check so the regexes don't run over files with no imports at all. */
|
|
10
|
+
export const hasImportStatement = (src: string): boolean => src.includes("import") || src.includes("from")
|
|
11
|
+
|
|
12
|
+
// A static `from "…"` clause (import-from OR export-from). Anchored at a statement boundary
|
|
13
|
+
// (line start / `;` / `}`) and disallowing quotes/backticks/semicolons between the keyword and
|
|
14
|
+
// `from`, so it can never span into a string literal or a neighbouring statement. Named-import
|
|
15
|
+
// lists may span multiple lines (newlines are allowed between the keyword and `from`).
|
|
16
|
+
const IMPORT_FROM_RE = /(^|[;\n}])(\s*(?:import|export)\b[^"'`;]*?\bfrom\s*)(["'])([^"'\n]+)\3/g
|
|
17
|
+
|
|
18
|
+
// A side-effect `import "…"` (a bare quote directly after the keyword — not `import x`, not `import(`).
|
|
19
|
+
const SIDE_EFFECT_RE = /(^|[;\n}])(\s*import\s*)(["'])([^"'\n]+)\3/g
|
|
20
|
+
|
|
21
|
+
const isRelativeOrAbsolute = (spec: string): boolean =>
|
|
22
|
+
spec.startsWith("/") || spec.startsWith("./") || spec.startsWith("../")
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Rewrite every known-library bare import in `src` to its `/__lib/<slug>/…` path. Relative and
|
|
26
|
+
* absolute specifiers pass through untouched; an unknown bare specifier throws a friendly Error.
|
|
27
|
+
* `slugs` is the set of library slugs available to this project (its dependency map's keys).
|
|
28
|
+
*/
|
|
29
|
+
export const rewriteLibraryImports = (src: string, slugs: Set<string>): string => {
|
|
30
|
+
const rewriteSpec = (spec: string): string => {
|
|
31
|
+
if (isRelativeOrAbsolute(spec)) return spec
|
|
32
|
+
const slash = spec.indexOf("/")
|
|
33
|
+
const slug = slash === -1 ? spec : spec.slice(0, slash)
|
|
34
|
+
if (slugs.has(slug)) {
|
|
35
|
+
// `acme-ui` → `/__lib/acme-ui/index`; `acme-ui/theme` → `/__lib/acme-ui/theme`.
|
|
36
|
+
const sub = slash === -1 ? "/index" : spec.slice(slash)
|
|
37
|
+
return `/__lib/${slug}${sub}`
|
|
38
|
+
}
|
|
39
|
+
// Legacy worker-module imports (`import { UIScreen } from "ui"`) predate the globals-only SDK —
|
|
40
|
+
// chisel would reject them anyway; say so here instead of "Unknown library".
|
|
41
|
+
if (slug === "ui" || slug === "creator" || slug === "utils") {
|
|
42
|
+
throw new Error(`import from "${slug}": the SDK needs no imports — UIScreen, Scene, etc. are globals; remove this import line.`)
|
|
43
|
+
}
|
|
44
|
+
throw new Error(`Unknown library "${slug}" — add it in Settings → Libraries, or use a relative path like "./${slug}".`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let out = src.replace(IMPORT_FROM_RE, (_m, boundary, head, quote, spec) =>
|
|
48
|
+
`${boundary}${head}${quote}${rewriteSpec(spec)}${quote}`)
|
|
49
|
+
out = out.replace(SIDE_EFFECT_RE, (m, boundary, head, quote, spec) =>
|
|
50
|
+
isRelativeOrAbsolute(spec) ? m : `${boundary}${head}${quote}${rewriteSpec(spec)}${quote}`)
|
|
51
|
+
return out
|
|
52
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// The scene-editor compile composition, shared by every host that compiles an edit-mode bundle
|
|
2
|
+
// for a .scene.ts file: the platform backend (src/scene/compileSceneEditor.ts), the standalone
|
|
3
|
+
// scene-editor dev server, and (later) the lecodes CLI. One bundle on purpose: chisel's
|
|
4
|
+
// method-granular DCE trims each bundle's SDK copy to its own calls, so a separately-compiled
|
|
5
|
+
// harness could not call scene methods on objects the scene bundle created.
|
|
6
|
+
|
|
7
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs"
|
|
8
|
+
import { join } from "node:path"
|
|
9
|
+
import type { CompileEntry } from "./compileProject"
|
|
10
|
+
|
|
11
|
+
/** Where the harness sources land in the compile's file map. The dotfolder can't collide with
|
|
12
|
+
* project assets (the tree forbids leading-dot names) and reads clearly in compile errors. */
|
|
13
|
+
export const SCENE_HARNESS_DIR = "./.scene-editor"
|
|
14
|
+
|
|
15
|
+
/** The virtual entrypoint path (`entryOverride`). */
|
|
16
|
+
export const SCENE_ENTRY_PATH = "./.scene-entry.ts"
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read the harness sources (packages/projects/src/scene-editor) from a directory:
|
|
20
|
+
* `LECODES_SCENE_HARNESS_DIR` env override first (deployments vendor the sources, like
|
|
21
|
+
* LECODES_SDK_INJECT does for the SDK), else the given fallback dir. Returns name → source.
|
|
22
|
+
*/
|
|
23
|
+
export const loadSceneHarness = (fallbackDir?: string): Record<string, string> => {
|
|
24
|
+
const override = process.env.LECODES_SCENE_HARNESS_DIR
|
|
25
|
+
const dir = override && existsSync(override) ? override
|
|
26
|
+
: fallbackDir && existsSync(fallbackDir) ? fallbackDir
|
|
27
|
+
: null
|
|
28
|
+
if (!dir) throw new Error("scene-editor harness sources not found (set LECODES_SCENE_HARNESS_DIR)")
|
|
29
|
+
const out: Record<string, string> = {}
|
|
30
|
+
for (const f of readdirSync(dir)) {
|
|
31
|
+
if (f.endsWith(".ts")) out[f] = readFileSync(join(dir, f), "utf8")
|
|
32
|
+
}
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The extra entries that turn a project compile into a scene-editor compile: the harness modules
|
|
38
|
+
* plus a virtual entry that (1) sets the edit-mode flag in its own module — imports hoist, so the
|
|
39
|
+
* flag must run as a module, first — (2) imports the scene, which registers its handle on
|
|
40
|
+
* `__lecodesScenes`, (3) imports the project's `*.editor.ts` plugin files (editor bundles ONLY —
|
|
41
|
+
* production compiles never import them, so they cost zero bytes in shipped `app.js`), and
|
|
42
|
+
* (4) imports the harness main, which installs `__lecodesSceneHarness`.
|
|
43
|
+
* FULL extensions everywhere — exact-match specifiers need no resolver guesswork on dotted names,
|
|
44
|
+
* so even the unpatched published chisel resolves the `.scene.ts` import.
|
|
45
|
+
*
|
|
46
|
+
* `scenePath` is the project path of the scene file ("./scenes/city.scene.ts"); `projectPaths` is
|
|
47
|
+
* the project's file list — `.editor.ts` entries are picked out of it. Compile with
|
|
48
|
+
* `entryOverride: SCENE_ENTRY_PATH`, `header: false`, `format: "iife"`, `editor: true`.
|
|
49
|
+
*/
|
|
50
|
+
export const sceneEditorEntries = (
|
|
51
|
+
scenePath: string,
|
|
52
|
+
harness: Record<string, string>,
|
|
53
|
+
projectPaths: string[] = [],
|
|
54
|
+
): CompileEntry[] => {
|
|
55
|
+
const entries: CompileEntry[] = Object.entries(harness).map(([ name, text ]) => ({
|
|
56
|
+
path: `${SCENE_HARNESS_DIR}/${name}`,
|
|
57
|
+
type: "text" as const,
|
|
58
|
+
text,
|
|
59
|
+
}))
|
|
60
|
+
entries.push({
|
|
61
|
+
path: `${SCENE_HARNESS_DIR}/.edit-mode.ts`,
|
|
62
|
+
type: "text",
|
|
63
|
+
text: `;(globalThis as any).__lecodesSceneEdit = true\nexport {}\n`,
|
|
64
|
+
})
|
|
65
|
+
const editorImports = projectPaths
|
|
66
|
+
.filter((p) => p.endsWith(".editor.ts"))
|
|
67
|
+
.map((p) => `import "${p}"\n`)
|
|
68
|
+
.join("")
|
|
69
|
+
entries.push({
|
|
70
|
+
path: SCENE_ENTRY_PATH,
|
|
71
|
+
type: "text",
|
|
72
|
+
text: `import "${SCENE_HARNESS_DIR}/.edit-mode.ts"\n`
|
|
73
|
+
+ `import "${scenePath}"\n`
|
|
74
|
+
+ editorImports
|
|
75
|
+
+ `import "${SCENE_HARNESS_DIR}/main.ts"\n`,
|
|
76
|
+
})
|
|
77
|
+
return entries
|
|
78
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Minimal sfnt (TTF/OTF) reader for the `font()` compile macro: family name from the `name` table,
|
|
2
|
+
// weight/italic from `OS/2` (with a `head.macStyle` fallback). Reads only what the macro needs —
|
|
3
|
+
// this is not a font sanitizer or validator.
|
|
4
|
+
|
|
5
|
+
export type SfntInfo = { family: string, weight: number, italic: boolean }
|
|
6
|
+
|
|
7
|
+
const TAG_TTF = 0x00010000
|
|
8
|
+
const TAG_OTTO = 0x4f54544f // 'OTTO' (CFF outlines)
|
|
9
|
+
const TAG_TTCF = 0x74746366 // 'ttcf' (collection)
|
|
10
|
+
const TAG_WOFF = 0x774f4646 // 'wOFF'
|
|
11
|
+
const TAG_WOF2 = 0x774f4632 // 'wOF2'
|
|
12
|
+
|
|
13
|
+
/** Parse a .ttf/.otf. Throws a message meant to surface verbatim in a compile error. */
|
|
14
|
+
export const parseSfnt = (bytes: Uint8Array): SfntInfo => {
|
|
15
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
16
|
+
if (bytes.byteLength < 12) throw new Error("not a font file (truncated)")
|
|
17
|
+
const version = dv.getUint32(0)
|
|
18
|
+
if (version === TAG_TTCF) throw new Error("font collections (.ttc) aren't supported — export a single face")
|
|
19
|
+
if (version === TAG_WOFF || version === TAG_WOF2) throw new Error("WOFF/WOFF2 project fonts aren't supported — use the .ttf/.otf")
|
|
20
|
+
if (version !== TAG_TTF && version !== TAG_OTTO) throw new Error("not a TTF/OTF font file")
|
|
21
|
+
|
|
22
|
+
const numTables = dv.getUint16(4)
|
|
23
|
+
const tables = new Map<string, { offset: number, length: number }>()
|
|
24
|
+
for (let i = 0; i < numTables; i++) {
|
|
25
|
+
const rec = 12 + i * 16
|
|
26
|
+
if (rec + 16 > bytes.byteLength) throw new Error("not a font file (truncated table directory)")
|
|
27
|
+
const tag = String.fromCharCode(bytes[rec], bytes[rec + 1], bytes[rec + 2], bytes[rec + 3])
|
|
28
|
+
tables.set(tag, { offset: dv.getUint32(rec + 8), length: dv.getUint32(rec + 12) })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const family = readFamily(dv, bytes, tables.get("name"))
|
|
32
|
+
if (!family) throw new Error("no family name in the font's `name` table")
|
|
33
|
+
|
|
34
|
+
let weight = 400
|
|
35
|
+
let italic = false
|
|
36
|
+
const os2 = tables.get("OS/2")
|
|
37
|
+
if (os2 && os2.offset + 64 <= bytes.byteLength) {
|
|
38
|
+
weight = dv.getUint16(os2.offset + 4) // usWeightClass
|
|
39
|
+
italic = (dv.getUint16(os2.offset + 62) & 1) === 1 // fsSelection ITALIC
|
|
40
|
+
} else {
|
|
41
|
+
const head = tables.get("head")
|
|
42
|
+
if (head && head.offset + 46 <= bytes.byteLength) {
|
|
43
|
+
const macStyle = dv.getUint16(head.offset + 44)
|
|
44
|
+
if (macStyle & 1) weight = 700
|
|
45
|
+
italic = (macStyle & 2) === 2
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (weight < 1 || weight > 1000) weight = 400
|
|
49
|
+
|
|
50
|
+
return { family, weight, italic }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Best family name: typographic family (nameID 16) over legacy family (1); Windows-English
|
|
54
|
+
* records over the rest. Returns "" when the table yields nothing decodable. */
|
|
55
|
+
const readFamily = (dv: DataView, bytes: Uint8Array, name?: { offset: number, length: number }): string => {
|
|
56
|
+
if (!name || name.offset + 6 > bytes.byteLength) return ""
|
|
57
|
+
const base = name.offset
|
|
58
|
+
const count = dv.getUint16(base + 2)
|
|
59
|
+
const stringOffset = base + dv.getUint16(base + 4)
|
|
60
|
+
|
|
61
|
+
let best = ""
|
|
62
|
+
let bestScore = -1
|
|
63
|
+
for (let i = 0; i < count; i++) {
|
|
64
|
+
const rec = base + 6 + i * 12
|
|
65
|
+
if (rec + 12 > bytes.byteLength) break
|
|
66
|
+
const platformID = dv.getUint16(rec)
|
|
67
|
+
const languageID = dv.getUint16(rec + 4)
|
|
68
|
+
const nameID = dv.getUint16(rec + 6)
|
|
69
|
+
const length = dv.getUint16(rec + 8)
|
|
70
|
+
const offset = stringOffset + dv.getUint16(rec + 10)
|
|
71
|
+
if (nameID !== 16 && nameID !== 1) continue
|
|
72
|
+
if (offset + length > bytes.byteLength) continue
|
|
73
|
+
|
|
74
|
+
// nameID 16 beats 1; within an id, Windows-English > Windows > Unicode > Mac.
|
|
75
|
+
let score = nameID === 16 ? 100 : 0
|
|
76
|
+
if (platformID === 3) score += languageID === 0x409 ? 30 : 20
|
|
77
|
+
else if (platformID === 0) score += 10
|
|
78
|
+
if (score <= bestScore) continue
|
|
79
|
+
|
|
80
|
+
const text = platformID === 1 ? decodeLatin1(bytes, offset, length) : decodeUtf16be(bytes, offset, length)
|
|
81
|
+
if (!text.trim()) continue
|
|
82
|
+
best = text.trim()
|
|
83
|
+
bestScore = score
|
|
84
|
+
}
|
|
85
|
+
return best
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const decodeUtf16be = (bytes: Uint8Array, offset: number, length: number): string => {
|
|
89
|
+
let out = ""
|
|
90
|
+
for (let i = 0; i + 1 < length; i += 2) out += String.fromCharCode((bytes[offset + i] << 8) | bytes[offset + i + 1])
|
|
91
|
+
return out
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const decodeLatin1 = (bytes: Uint8Array, offset: number, length: number): string => {
|
|
95
|
+
let out = ""
|
|
96
|
+
for (let i = 0; i < length; i++) out += String.fromCharCode(bytes[offset + i])
|
|
97
|
+
return out
|
|
98
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Helpers for serving the bundler's source map alongside the bundle. The map's generated positions
|
|
2
|
+
// are relative to the bundle code; when the deploy/preview prepends a comment header, the map must be
|
|
3
|
+
// shifted down by that many lines, then inlined into the bundle as a data URL (the preview executes
|
|
4
|
+
// the bundle via `new Function(string)`, which has no URL identity, so an external `.map` can't be
|
|
5
|
+
// fetched — it must be inline).
|
|
6
|
+
|
|
7
|
+
/** Shift every generated position in `mapJson` down by `lines` (each VLQ line is `;`-separated). */
|
|
8
|
+
export const offsetSourceMap = (mapJson: string, lines: number): string => {
|
|
9
|
+
if (lines <= 0) return mapJson
|
|
10
|
+
const map = JSON.parse(mapJson) as { mappings: string }
|
|
11
|
+
map.mappings = ";".repeat(lines) + map.mappings
|
|
12
|
+
return JSON.stringify(map)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Inline the map into a bundle string under a **non-standard** directive (`lecodesMap`, not
|
|
17
|
+
* `sourceMappingURL`), so the browser does NOT auto-apply it. The bundle runs via `new Function`, and
|
|
18
|
+
* the browser maps the raw stack position *including* the `new Function` wrapper (~2-3 lines) which the
|
|
19
|
+
* map can't subtract — so its native source location would be off by those lines. Our preview mapper
|
|
20
|
+
* reads `lecodesMap` and subtracts the (calibrated) wrapper itself, so its locations are correct.
|
|
21
|
+
*/
|
|
22
|
+
export const inlineSourceMapComment = (mapJson: string): string => {
|
|
23
|
+
const b64 = Buffer.from(mapJson, "utf8").toString("base64")
|
|
24
|
+
return `//# lecodesMap=data:application/json;charset=utf-8;base64,${b64}`
|
|
25
|
+
}
|
package/runtime/sdk/inject.ts
CHANGED
|
@@ -63,6 +63,7 @@ export {
|
|
|
63
63
|
UIInput, UITextArea, type UIInputStyle,
|
|
64
64
|
UIScrollable, type UIScrollableStyle,
|
|
65
65
|
UIPager, type UIPagerStyle,
|
|
66
|
+
UITabs, type UITabDef,
|
|
66
67
|
UIWidget, type UIWidgetStyle,
|
|
67
68
|
UIModal, type UIModalStyle,
|
|
68
69
|
UIBottomSheet, type UIBottomSheetStyle,
|
|
@@ -73,6 +74,7 @@ export {
|
|
|
73
74
|
NativeView, type UINativeViewStyle,
|
|
74
75
|
registerFont,
|
|
75
76
|
__uiMap,
|
|
77
|
+
__UIColumn, __UIRow, __UIBox, __UIButton, __UIScreen, __UIScrollable, __UIWidget,
|
|
76
78
|
} from "./ui/UI"
|
|
77
79
|
// The Presentable navigation contract (docs/navigation-presentable-plan.md). `Presentable` is
|
|
78
80
|
// interface + runtime companion (declaration merging): `Presentable.current` = what's on screen.
|
|
@@ -15,6 +15,9 @@ import { Element, type AnimateStyle, type BaseStyle, type DrawableStyle, type El
|
|
|
15
15
|
|
|
16
16
|
export type UINativeViewStyle = ElementStyle & DrawableStyle
|
|
17
17
|
|
|
18
|
+
/** An instance of a host-registered platform view (map, QR scanner, camera preview, …) —
|
|
19
|
+
* embeddable among a screen's children AND fullscreen-presentable (`open()`); one instance lives
|
|
20
|
+
* in one place at a time. Talk to it via the `call`/`on` channel. */
|
|
18
21
|
export interface NativeView {
|
|
19
22
|
readonly type: "native",
|
|
20
23
|
/** The registered view kind this instance resolves to (the `registerView` name). */
|
package/runtime/sdk/ui/UI.ts
CHANGED
|
@@ -17,6 +17,7 @@ export { UIPopover, type UIPopoverStyle, type UIPopoverAnchor } from './UIPopove
|
|
|
17
17
|
export { UIRow, UIColumn, UIBox, type UIContainerStyle } from './UIContainer'
|
|
18
18
|
export { UIScrollable, type UIScrollableStyle } from './UIScrollable'
|
|
19
19
|
export { UIPager, type UIPagerStyle } from './UIPager'
|
|
20
|
+
export { UITabs, type UITabDef } from './UITabs'
|
|
20
21
|
|
|
21
22
|
export { UIVirtualizedList } from './UIVirtualizedList'
|
|
22
23
|
|
|
@@ -28,4 +29,11 @@ export type { PresentOptions, Transition, TransitionName, TransitionSpec, Transi
|
|
|
28
29
|
|
|
29
30
|
// Internal: memoized map for reactive children — the chisel `reactiveUi` pass rewrites
|
|
30
31
|
// `list.map(fn)` inside children bindings to `__uiMap(list, fn, slot)` calls.
|
|
31
|
-
export { __uiMap } from './UINode'
|
|
32
|
+
export { __uiMap } from './UINode'
|
|
33
|
+
// Compiler fast paths (chisel `flatten_ui` raw lowering): dispatch-free construction for calls
|
|
34
|
+
// whose arguments the compiler proved to be plain children. Never written by hand.
|
|
35
|
+
export { __UIColumn, __UIRow, __UIBox } from './UIContainer'
|
|
36
|
+
export { __UIButton } from './UIButton'
|
|
37
|
+
export { __UIScreen } from './UIScreen'
|
|
38
|
+
export { __UIScrollable } from './UIScrollable'
|
|
39
|
+
export { __UIWidget } from './UIWidget'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ModalElement, type UIModalStyle } from "./UIModal"
|
|
2
2
|
import type { UIWidget } from "./UIWidget"
|
|
3
|
-
import type
|
|
3
|
+
import { buildUI, type ChildrenFn, type UIChildArg, type UINodeChild } from "./UINode"
|
|
4
4
|
|
|
5
5
|
export type UIBottomSheetStyle = UIModalStyle
|
|
6
6
|
|
|
@@ -130,20 +130,14 @@ export class BottomSheetElement extends ModalElement {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
const makeBottomSheet = (style: any, children: any) => new BottomSheetElement(style, children)
|
|
134
|
+
|
|
135
|
+
/** Create a bottom sheet (see {@link UIBottomSheet}) — content-sized by default; call
|
|
136
|
+
* `.detents([...])` for snap positions. Same argument forms as `UIColumn`. */
|
|
137
|
+
export function UIBottomSheet(...children: UIChildArg[]): UIBottomSheet;
|
|
134
138
|
export function UIBottomSheet(children: UINodeChild[] | ChildrenFn): UIBottomSheet;
|
|
135
|
-
|
|
136
|
-
export function UIBottomSheet(style: UIBottomSheetStyle, children
|
|
137
|
-
export function UIBottomSheet(...args: []
|
|
138
|
-
|
|
139
|
-
return new BottomSheetElement({}, [])
|
|
140
|
-
} else if (args.length === 1) {
|
|
141
|
-
if (Array.isArray(args[0]) || typeof args[0] === "function") {
|
|
142
|
-
return new BottomSheetElement({}, args[0])
|
|
143
|
-
} else {
|
|
144
|
-
return new BottomSheetElement(args[0], [])
|
|
145
|
-
}
|
|
146
|
-
} else {
|
|
147
|
-
return new BottomSheetElement(args[0], args[1])
|
|
148
|
-
}
|
|
139
|
+
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
|
|
140
|
+
export function UIBottomSheet(style: UIBottomSheetStyle, children?: UINodeChild[] | ChildrenFn): UIBottomSheet;
|
|
141
|
+
export function UIBottomSheet(...args: any[]): UIBottomSheet {
|
|
142
|
+
return buildUI(args, null, makeBottomSheet)
|
|
149
143
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { ClickEvent, TouchStartEvent, LongPressEvent } from "../runtime/touch"
|
|
2
2
|
import type { ClickEvent as ClickEventType, TouchStartEvent as TouchStartEventType, LongPressEvent as LongPressEventType } from "../runtime/touch"
|
|
3
|
-
import { type ElementStyle, type ContainerStyle, type Classes, type Style, ContainerElement, type DrawableStyle, type UINodeChild, type ChildrenFn, type OnLayoutCallback, type BoundingClientRect, type BaseStyle, type AnimateStyle, type Color } from "./UINode"
|
|
3
|
+
import { type ElementStyle, type ContainerStyle, type Classes, type Style, ContainerElement, buildUI, type DrawableStyle, type UIChildArg, type UINodeChild, type ChildrenFn, type OnLayoutCallback, type BoundingClientRect, type BaseStyle, type AnimateStyle, type Color } from "./UINode"
|
|
4
4
|
|
|
5
5
|
export type UIButtonStyle = ElementStyle & DrawableStyle & ContainerStyle & { rippleColor?: Color | "default" }
|
|
6
6
|
|
|
7
|
+
/** A tappable container — row + centered children by default. Press feedback via the `onPressed`
|
|
8
|
+
* style block, the `$pressed` class, or `rippleColor`. */
|
|
7
9
|
export interface UIButton {
|
|
8
10
|
readonly type: "button",
|
|
9
11
|
|
|
@@ -11,12 +13,15 @@ export interface UIButton {
|
|
|
11
13
|
animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>,
|
|
12
14
|
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
13
15
|
|
|
16
|
+
/** Fired on a completed tap/click. */
|
|
14
17
|
onClick(callback: (ev: ClickEventType<UIButton>) => void): this,
|
|
18
|
+
/** Fired when a touch begins; `ev.track(...)` takes over the rest of the gesture (dragging). */
|
|
15
19
|
onTouchStart(callback: (ev: TouchStartEventType<UIButton>) => void): this,
|
|
16
20
|
/** Fired when a finger is held on the button past the long-press threshold. The event can
|
|
17
21
|
* `ev.track(...)` the rest of the gesture (like `onTouchStart`), so a hold can flow straight into
|
|
18
22
|
* dragging the element. A handled long-press suppresses the click that would follow the release. */
|
|
19
23
|
onLongPress(callback: (ev: LongPressEventType<UIButton>) => void): this,
|
|
24
|
+
/** Is a finger currently down on the button? */
|
|
20
25
|
isPressed(): boolean,
|
|
21
26
|
|
|
22
27
|
append(...nodes: UINodeChild[]): this
|
|
@@ -99,20 +104,17 @@ class ButtonElement extends ContainerElement<"button"> {
|
|
|
99
104
|
// default was a recurring authoring/codegen trap.
|
|
100
105
|
const BUTTON_DEFAULTS = { flexDirection: "row", justifyContent: "center", alignItems: "center" } as const
|
|
101
106
|
|
|
102
|
-
|
|
107
|
+
const makeButton = (style: any, children: any) => new ButtonElement("button", style, children)
|
|
108
|
+
|
|
109
|
+
/** @internal Compiler fast path (chisel `flatten_ui` raw lowering) — see UIContainer. */
|
|
110
|
+
export const __UIButton = (children: UINodeChild[] | ChildrenFn): UIButton => makeButton({ ...BUTTON_DEFAULTS }, children)
|
|
111
|
+
|
|
112
|
+
/** Create a button (row + centered by default — the icon+label shape). Same argument forms as
|
|
113
|
+
* `UIColumn`; attach behavior with `.onClick(...)`. */
|
|
114
|
+
export function UIButton(...children: UIChildArg[]): UIButton;
|
|
103
115
|
export function UIButton(children: UINodeChild[] | ChildrenFn): UIButton;
|
|
104
|
-
|
|
105
|
-
export function UIButton(style: UIButtonStyle, children
|
|
106
|
-
export function UIButton(...args: []
|
|
107
|
-
|
|
108
|
-
return new ButtonElement("button", { ...BUTTON_DEFAULTS }, [])
|
|
109
|
-
} else if (args.length === 1) {
|
|
110
|
-
if (Array.isArray(args[0]) || typeof args[0] === "function") {
|
|
111
|
-
return new ButtonElement("button", { ...BUTTON_DEFAULTS }, args[0])
|
|
112
|
-
} else {
|
|
113
|
-
return new ButtonElement("button", { ...BUTTON_DEFAULTS, ...args[0] }, [])
|
|
114
|
-
}
|
|
115
|
-
} else {
|
|
116
|
-
return new ButtonElement("button", { ...BUTTON_DEFAULTS, ...args[0] }, args[1])
|
|
117
|
-
}
|
|
116
|
+
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
|
|
117
|
+
export function UIButton(style: UIButtonStyle, children?: UINodeChild[] | ChildrenFn): UIButton;
|
|
118
|
+
export function UIButton(...args: any[]): UIButton {
|
|
119
|
+
return buildUI(args, BUTTON_DEFAULTS, makeButton)
|
|
118
120
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { ContainerElement, type AnimateStyle, type AppearStyle, type ChildrenFn, type ContainerStyle, type DisappearStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type BoundingClientRect, type Classes, type Style, type StyleFn, type UINode, type UINodeChild } from "./UINode"
|
|
1
|
+
import { ContainerElement, buildUI, type AnimateStyle, type AppearStyle, type ChildrenFn, type ContainerStyle, type DisappearStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type BoundingClientRect, type Classes, type Style, type StyleFn, type UIChildArg, type UINode, type UINodeChild } from "./UINode"
|
|
2
2
|
|
|
3
3
|
export type UIContainerStyle = ElementStyle & DrawableStyle & ContainerStyle
|
|
4
4
|
|
|
5
|
+
/** A vertical flex container — children stack top to bottom. */
|
|
5
6
|
export interface UIColumn {
|
|
6
7
|
readonly type: "column",
|
|
7
8
|
style: Style<this, UIContainerStyle>,
|
|
@@ -23,24 +24,26 @@ export interface UIColumn {
|
|
|
23
24
|
|
|
24
25
|
readonly children: UINodeChild[]
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
+
const makeColumn = (style: any, children: any) => new ContainerElement("column", style, children)
|
|
28
|
+
|
|
29
|
+
/** @internal Compiler fast path (chisel `flatten_ui` raw lowering): construction with a proven
|
|
30
|
+
* plain-children array — or a reactive ChildrenFn (the Element constructor branches on
|
|
31
|
+
* `typeof === "function"` either way) — skipping the buildUI argument dispatch entirely. */
|
|
32
|
+
export const __UIColumn = (children: UINodeChild[] | ChildrenFn): UIColumn => makeColumn({}, children)
|
|
33
|
+
export const __UIRow = (children: UINodeChild[] | ChildrenFn): UIRow => makeRow({ flexDirection: "row" }, children)
|
|
34
|
+
export const __UIBox = (children: UINodeChild[] | ChildrenFn): UIBox => makeBox({ ...BOX_DEFAULTS }, children)
|
|
35
|
+
|
|
36
|
+
/** Create a vertical flex container. Children are variadic — arrays flatten one level, falsy
|
|
37
|
+
* entries are skipped; a lone function makes the children reactive. */
|
|
38
|
+
export function UIColumn(...children: UIChildArg[]): UIColumn
|
|
27
39
|
export function UIColumn(children: UINodeChild[] | ChildrenFn): UIColumn
|
|
28
|
-
|
|
29
|
-
export function UIColumn(style: UIContainerStyle, children
|
|
30
|
-
export function UIColumn(...args: []
|
|
31
|
-
|
|
32
|
-
return new ContainerElement("column", {}, [])
|
|
33
|
-
} else if (args.length === 1) {
|
|
34
|
-
if (Array.isArray(args[0]) || typeof args[0] === "function") {
|
|
35
|
-
return new ContainerElement("column", {}, args[0])
|
|
36
|
-
} else {
|
|
37
|
-
return new ContainerElement("column", args[0], [])
|
|
38
|
-
}
|
|
39
|
-
} else {
|
|
40
|
-
return new ContainerElement("column", args[0], args[1])
|
|
41
|
-
}
|
|
40
|
+
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
|
|
41
|
+
export function UIColumn(style: UIContainerStyle, children?: UINodeChild[] | ChildrenFn): UIColumn
|
|
42
|
+
export function UIColumn(...args: any[]): UIColumn {
|
|
43
|
+
return buildUI(args, null, makeColumn)
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
/** A horizontal flex container — children flow left to right. */
|
|
44
47
|
export interface UIRow {
|
|
45
48
|
readonly type: "row",
|
|
46
49
|
style: Style<this, UIContainerStyle>,
|
|
@@ -62,24 +65,19 @@ export interface UIRow {
|
|
|
62
65
|
|
|
63
66
|
readonly children: UINodeChild[],
|
|
64
67
|
}
|
|
65
|
-
|
|
68
|
+
const ROW_DEFAULTS = { flexDirection: "row" } as const
|
|
69
|
+
const makeRow = (style: any, children: any) => new ContainerElement("row", style, children)
|
|
70
|
+
|
|
71
|
+
/** Create a horizontal flex container (same argument forms as `UIColumn`). */
|
|
72
|
+
export function UIRow(...children: UIChildArg[]): UIRow
|
|
66
73
|
export function UIRow(children: UINodeChild[] | ChildrenFn): UIRow
|
|
67
|
-
|
|
68
|
-
export function UIRow(style: UIContainerStyle, children
|
|
69
|
-
export function UIRow(...args: []
|
|
70
|
-
|
|
71
|
-
return new ContainerElement("row", { flexDirection: "row" }, [])
|
|
72
|
-
} else if (args.length === 1) {
|
|
73
|
-
if (Array.isArray(args[0]) || typeof args[0] === "function") {
|
|
74
|
-
return new ContainerElement("row", { flexDirection: "row" }, args[0])
|
|
75
|
-
} else {
|
|
76
|
-
return new ContainerElement("row", { flexDirection: "row", ...args[0] }, [])
|
|
77
|
-
}
|
|
78
|
-
} else {
|
|
79
|
-
return new ContainerElement("row", { flexDirection: "row", ...args[0] }, args[1])
|
|
80
|
-
}
|
|
74
|
+
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
|
|
75
|
+
export function UIRow(style: UIContainerStyle, children?: UINodeChild[] | ChildrenFn): UIRow
|
|
76
|
+
export function UIRow(...args: any[]): UIRow {
|
|
77
|
+
return buildUI(args, ROW_DEFAULTS, makeRow)
|
|
81
78
|
}
|
|
82
79
|
|
|
80
|
+
/** A centering container — `justifyContent`/`alignItems` default to center. */
|
|
83
81
|
export interface UIBox {
|
|
84
82
|
readonly type: "box",
|
|
85
83
|
style: Style<this, UIContainerStyle>,
|
|
@@ -103,20 +101,14 @@ export interface UIBox {
|
|
|
103
101
|
}
|
|
104
102
|
const BOX_DEFAULTS = { justifyContent: "center", alignItems: "center" } as const
|
|
105
103
|
|
|
106
|
-
|
|
104
|
+
const makeBox = (style: any, children: any) => new ContainerElement("box", style, children)
|
|
105
|
+
|
|
106
|
+
/** Create a centering container — children are centered on both axes by default (icon badges,
|
|
107
|
+
* avatars, empty states). Same argument forms as `UIColumn`. */
|
|
108
|
+
export function UIBox(...children: UIChildArg[]): UIBox
|
|
107
109
|
export function UIBox(children: UINodeChild[] | ChildrenFn): UIBox
|
|
108
|
-
|
|
109
|
-
export function UIBox(style: UIContainerStyle, children
|
|
110
|
-
export function UIBox(...args: []
|
|
111
|
-
|
|
112
|
-
return new ContainerElement("box", { ...BOX_DEFAULTS }, [])
|
|
113
|
-
} else if (args.length === 1) {
|
|
114
|
-
if (Array.isArray(args[0]) || typeof args[0] === "function") {
|
|
115
|
-
return new ContainerElement("box", { ...BOX_DEFAULTS }, args[0])
|
|
116
|
-
} else {
|
|
117
|
-
return new ContainerElement("box", { ...BOX_DEFAULTS, ...args[0] }, [])
|
|
118
|
-
}
|
|
119
|
-
} else {
|
|
120
|
-
return new ContainerElement("box", { ...BOX_DEFAULTS, ...args[0] }, args[1])
|
|
121
|
-
}
|
|
110
|
+
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
|
|
111
|
+
export function UIBox(style: UIContainerStyle, children?: UINodeChild[] | ChildrenFn): UIBox
|
|
112
|
+
export function UIBox(...args: any[]): UIBox {
|
|
113
|
+
return buildUI(args, BOX_DEFAULTS, makeBox)
|
|
122
114
|
}
|
|
@@ -21,19 +21,20 @@ const toSrc = (src: ImageSource): string | FetchResponse | File | SvgSource | Ca
|
|
|
21
21
|
/** The stored/resolved image source (a Canvas is resolved to a surface marker). */
|
|
22
22
|
export type ResolvedImageSource = string | FetchResponse | File | SvgSource | CanvasImageSource
|
|
23
23
|
|
|
24
|
+
/** An image element. Sources: URL / `asset(...)`, fetch response / File, an SVG source
|
|
25
|
+
* (`assetIcon`), or a live Canvas (`canvas.update()` refreshes the node in place). */
|
|
24
26
|
export interface UIImage {
|
|
25
27
|
readonly type: "image",
|
|
28
|
+
/** The image source — writable to swap the picture in place. */
|
|
26
29
|
get src(): ResolvedImageSource
|
|
27
30
|
set src(value: ImageSource)
|
|
28
31
|
style: Style<this, UIImageStyle>
|
|
29
32
|
animateTo: AnimateStyle<this, UIImageStyle>
|
|
30
33
|
animateFrom: AnimateStyle<this, UIImageStyle>
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// size (one frame, not the whole atlas) and it overrides objectFit — the cropped frame
|
|
36
|
-
// always fills the box.
|
|
35
|
+
/** Render only a sub-rectangle of the source texture (texture pixels) — the atlas/spritesheet
|
|
36
|
+
* primitive: crop once, then swap the rect per frame for sprite animation. The rect's w/h
|
|
37
|
+
* become the node's intrinsic size and override `objectFit` — the cropped frame fills the box. */
|
|
37
38
|
setSourceRect(x: number, y: number, w: number, h: number): this
|
|
38
39
|
|
|
39
40
|
onLayout(onLayout: OnLayoutCallback): this
|
|
@@ -80,7 +81,9 @@ class ImageElement extends Element<"image"> {
|
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
/** Create an image element from a source (see {@link ImageSource}). */
|
|
83
85
|
export function UIImage(src: ImageSource): UIImage
|
|
86
|
+
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
|
|
84
87
|
export function UIImage(style: UIImageStyle, src: ImageSource): UIImage
|
|
85
88
|
export function UIImage(...args: [ImageSource] | [UIImageStyle, ImageSource]): UIImage {
|
|
86
89
|
if (args.length === 1) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ElementStyle, type DrawableStyle, type Classes, type Style,
|
|
1
|
+
import { type ElementStyle, type DrawableStyle, type Classes, type Style, Element, type TextStyle, type Color, type OnLayoutCallback, type BoundingClientRect, type BaseStyle, type AnimateStyle } from "./UINode";
|
|
2
2
|
|
|
3
3
|
type InputStyle = {
|
|
4
4
|
placeholder?: string,
|
|
@@ -49,6 +49,8 @@ type SubmitStyle = {
|
|
|
49
49
|
enterKey?: "done" | "go" | "next" | "search" | "send",
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** A single-line text input. Keyboard/picker kind via the `type` style, return-key behavior via
|
|
53
|
+
* `enterKey` + `onSubmit`; keyboard policy on `keyboardShrink`/`keyboardDismiss`. */
|
|
52
54
|
export interface UIInput {
|
|
53
55
|
readonly type: "input",
|
|
54
56
|
|
|
@@ -83,6 +85,7 @@ export interface UIInput {
|
|
|
83
85
|
readonly class: Classes<this>
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
/** A multi-line text field — see the {@link UITextArea} factory for the auto-grow contract. */
|
|
86
89
|
export interface UITextArea {
|
|
87
90
|
readonly type: "textarea",
|
|
88
91
|
|
|
@@ -90,11 +93,6 @@ export interface UITextArea {
|
|
|
90
93
|
value: string
|
|
91
94
|
|
|
92
95
|
style: Style<this, UIInputStyle & { onFocused?: UIInputStyle & { duration?: number } }>,
|
|
93
|
-
// styleV: StyleFn<this, UIInputStyle>,
|
|
94
|
-
// styleH: StyleFn<this, UIInputStyle>,
|
|
95
|
-
|
|
96
|
-
// appear: AppearStyle<this, UIInputStyle>
|
|
97
|
-
// disappear: DisappearStyle<this, UIInputStyle>
|
|
98
96
|
|
|
99
97
|
onChange(callback: (value: string) => void): UITextArea
|
|
100
98
|
onFocus(callback: () => void): UITextArea
|
|
@@ -175,6 +173,8 @@ class InputElement<T extends "input" | "textarea"> extends Element<T> {
|
|
|
175
173
|
}
|
|
176
174
|
}
|
|
177
175
|
|
|
176
|
+
/** Create a single-line text input. Read/write the text via `.value`, listen with
|
|
177
|
+
* `onChange`/`onSubmit`; style keys `placeholder`, `type`, `enterKey`, … configure it. */
|
|
178
178
|
export function UIInput(): UIInput;
|
|
179
179
|
export function UIInput(style: UIInputStyle): UIInput;
|
|
180
180
|
export function UIInput(style?: UIInputStyle): UIInput {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type UIWidget, type UIWidgetStyle, WidgetElement } from "./UIWidget"
|
|
2
|
-
import type
|
|
2
|
+
import { buildUI, type BaseStyle, type ChildrenFn, type Color, type DrawableStyle, type UIChildArg, type UINodeChild } from "./UINode"
|
|
3
3
|
|
|
4
4
|
export type UIModalStyle = UIWidgetStyle
|
|
5
5
|
|
|
@@ -130,20 +130,14 @@ export class ModalElement extends WidgetElement {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
const makeModal = (style: any, children: any) => new ModalElement(style, children)
|
|
134
|
+
|
|
135
|
+
/** Create a modal dialog (see {@link UIModal}) — create once at module scope, then
|
|
136
|
+
* `show()`/`hide()`. Same argument forms as `UIColumn`. */
|
|
137
|
+
export function UIModal(...children: UIChildArg[]): UIModal;
|
|
134
138
|
export function UIModal(children: UINodeChild[] | ChildrenFn): UIModal;
|
|
135
|
-
|
|
136
|
-
export function UIModal(style: UIModalStyle, children
|
|
137
|
-
export function UIModal(...args: []
|
|
138
|
-
|
|
139
|
-
return new ModalElement({}, [])
|
|
140
|
-
} else if (args.length === 1) {
|
|
141
|
-
if (Array.isArray(args[0]) || typeof args[0] === "function") {
|
|
142
|
-
return new ModalElement({}, args[0])
|
|
143
|
-
} else {
|
|
144
|
-
return new ModalElement(args[0], [])
|
|
145
|
-
}
|
|
146
|
-
} else {
|
|
147
|
-
return new ModalElement(args[0], args[1])
|
|
148
|
-
}
|
|
139
|
+
/** @deprecated Style as the first argument is legacy — use chained `.style({...})`. */
|
|
140
|
+
export function UIModal(style: UIModalStyle, children?: UINodeChild[] | ChildrenFn): UIModal;
|
|
141
|
+
export function UIModal(...args: any[]): UIModal {
|
|
142
|
+
return buildUI(args, null, makeModal)
|
|
149
143
|
}
|