lecodes-cli 0.9.0 → 0.9.2
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/index.js +327 -80
- package/package.json +36 -36
- package/runtime/sdk/inject.ts +1 -0
- package/runtime/sdk/ui/UI.ts +1 -0
- package/runtime/sdk/ui/UIBottomSheet.ts +149 -0
- package/runtime/sdk/ui/UIInput.ts +24 -1
- package/runtime/sdk/ui/UIScrollable.ts +9 -1
- package/runtime/sdk/ui/UIVirtualizedList.ts +4 -1
- package/runtime/sdk/ui/UIWidget.ts +8 -2
- package/runtime/sdk-types.json +1 -1
package/package.json
CHANGED
|
@@ -1,42 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lecodes-cli",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
"lecodes",
|
|
9
|
-
"
|
|
10
|
-
|
|
3
|
+
"version": "0.9.2",
|
|
4
|
+
"dependencies": {
|
|
5
|
+
"@letary/chisel": "^0.5.1"
|
|
6
|
+
},
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"lecodes-design": "workspace:*",
|
|
9
|
+
"lecodes-renderer": "workspace:*",
|
|
10
|
+
"qrcode-terminal": "^0.12.0",
|
|
11
|
+
"lecodes-3d-editor": "workspace:*",
|
|
12
|
+
"sdk": "workspace:*"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"lecodes-design": "^0.3.2",
|
|
16
|
+
"lecodes-renderer": "^0.7.1",
|
|
17
|
+
"lecodes-3d-editor": "^0.1.0"
|
|
18
|
+
},
|
|
11
19
|
"bin": {
|
|
12
20
|
"lecodes": "dist/index.js"
|
|
13
21
|
},
|
|
22
|
+
"description": "Clone, edit and push LeCodes projects from your machine.",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
14
26
|
"files": [
|
|
15
27
|
"dist",
|
|
16
28
|
"runtime",
|
|
17
29
|
"README.md"
|
|
18
30
|
],
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"vendor-runtime": "bun scripts/vendor-runtime.ts",
|
|
25
|
-
"sync-peers": "bun scripts/sync-peer-versions.ts",
|
|
26
|
-
"build": "bun scripts/vendor-runtime.ts && bun build src/index.ts --target=node --outfile dist/index.js --external @letary/chisel --external lecodes-renderer --external lecodes-design --external lecodes-3d-editor --banner=\"#!/usr/bin/env node\"",
|
|
27
|
-
"build:bin": "bun scripts/build-bin.ts",
|
|
28
|
-
"typecheck": "tsc --noEmit",
|
|
29
|
-
"dev": "bun run src/index.ts",
|
|
30
|
-
"prepublishOnly": "bun run sync-peers && bun run build"
|
|
31
|
-
},
|
|
32
|
-
"dependencies": {
|
|
33
|
-
"@letary/chisel": "^0.4.2"
|
|
34
|
-
},
|
|
35
|
-
"peerDependencies": {
|
|
36
|
-
"lecodes-design": "^0.3.2",
|
|
37
|
-
"lecodes-renderer": "^0.7.0",
|
|
38
|
-
"lecodes-3d-editor": "^0.1.0"
|
|
39
|
-
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"lecodes",
|
|
33
|
+
"cli"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
40
36
|
"peerDependenciesMeta": {
|
|
41
37
|
"lecodes-design": {
|
|
42
38
|
"optional": true
|
|
@@ -48,11 +44,15 @@
|
|
|
48
44
|
"optional": true
|
|
49
45
|
}
|
|
50
46
|
},
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"lecodes-3d-editor"
|
|
56
|
-
"
|
|
57
|
-
|
|
47
|
+
"scripts": {
|
|
48
|
+
"icon": "bun scripts/make-icon.ts",
|
|
49
|
+
"vendor-runtime": "bun scripts/vendor-runtime.ts",
|
|
50
|
+
"sync-peers": "bun scripts/sync-peer-versions.ts",
|
|
51
|
+
"build": "bun scripts/vendor-runtime.ts && bun build src/index.ts --target=node --outfile dist/index.js --external @letary/chisel --external lecodes-renderer --external lecodes-design --external lecodes-3d-editor --banner=\"#!/usr/bin/env node\"",
|
|
52
|
+
"build:bin": "bun scripts/build-bin.ts",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"dev": "bun run src/index.ts",
|
|
55
|
+
"prepublishOnly": "bun run sync-peers && bun run build"
|
|
56
|
+
},
|
|
57
|
+
"type": "module"
|
|
58
58
|
}
|
package/runtime/sdk/inject.ts
CHANGED
package/runtime/sdk/ui/UI.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { UIInput, UITextArea, type UIInputStyle } from './UIInput'
|
|
|
11
11
|
|
|
12
12
|
export { UIWidget, type UIWidgetStyle } from './UIWidget'
|
|
13
13
|
export { UIModal, type UIModalStyle } from './UIModal'
|
|
14
|
+
export { UIBottomSheet, type UIBottomSheetStyle } from './UIBottomSheet'
|
|
14
15
|
export { UIPopover, type UIPopoverStyle, type UIPopoverAnchor } from './UIPopover'
|
|
15
16
|
|
|
16
17
|
export { UIRow, UIColumn, UIBox, type UIContainerStyle } from './UIContainer'
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { ModalElement, type UIModalStyle } from "./UIModal"
|
|
2
|
+
import type { UIWidget } from "./UIWidget"
|
|
3
|
+
import type { ChildrenFn, UINodeChild } from "./UINode"
|
|
4
|
+
|
|
5
|
+
export type UIBottomSheetStyle = UIModalStyle
|
|
6
|
+
|
|
7
|
+
const SHEET_ANIM_MS = 250
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A bottom sheet: a `UIWidget` pinned to the bottom edge. By default it is **content-sized** —
|
|
11
|
+
* as tall as what's inside (capped at the screen), one position, drag down to dismiss (the
|
|
12
|
+
* action-sheet shape). Call `detents()` for the multi-position map-app model instead: snap
|
|
13
|
+
* positions the user drags between on native hosts. Ships as a widget node on the wire (no new
|
|
14
|
+
* node type — docs/bottom-sheet-plan.md): hosts without sheet support show a static bottom panel.
|
|
15
|
+
*
|
|
16
|
+
* With detents, content lays out at the HIGHEST detent's size; lower detents show the top
|
|
17
|
+
* slice. The scrim, `onOpen`/`onClose`, back-button and scrim-tap dismissal all come from the
|
|
18
|
+
* modal machinery.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* const actions = UIBottomSheet({ bgColor: "#1c1c1e", borderRadius: 16 }, [rows]) // content-sized
|
|
22
|
+
*
|
|
23
|
+
* const mapSheet = UIBottomSheet({ bgColor: "#1c1c1e", borderRadius: 16 }, [results])
|
|
24
|
+
* .detents([0.3, 0.6, 1])
|
|
25
|
+
* .onDetentChange(i => map.setPadding(...))
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export interface UIBottomSheet extends UIWidget {
|
|
29
|
+
/** Snap positions as fractions of screen height, ascending. Also sizes the sheet: its box is
|
|
30
|
+
* the last (highest) detent; set this before `show()`. Without a `detents()` call the sheet
|
|
31
|
+
* is CONTENT-SIZED instead — one position, as tall as its children (up to the screen). */
|
|
32
|
+
detents(list: number[]): this,
|
|
33
|
+
/** Snap to a detent (animated). Clamped to the detents range; fires `onDetentChange`. */
|
|
34
|
+
setDetent(index: number): void,
|
|
35
|
+
/** Current detent index. */
|
|
36
|
+
readonly detent: number,
|
|
37
|
+
/** Fires on every settle: a finger snap on native hosts, or a `setDetent()` call. */
|
|
38
|
+
onDetentChange(callback: (index: number) => void): this,
|
|
39
|
+
|
|
40
|
+
/** Mount and slide in to the current detent. No-op while already open. */
|
|
41
|
+
show(): void,
|
|
42
|
+
/** Slide out, fade the scrim, then unmount. No-op while closed. */
|
|
43
|
+
hide(): void,
|
|
44
|
+
/** True from `show()` until `hide()` starts. */
|
|
45
|
+
readonly isOpen: boolean,
|
|
46
|
+
|
|
47
|
+
/** Called when `show()` mounts the sheet. */
|
|
48
|
+
onOpen(callback: () => void): this,
|
|
49
|
+
/** Called when the sheet starts closing — scrim tap, back button, drag-dismiss, or `hide()`. */
|
|
50
|
+
onClose(callback: () => void): this,
|
|
51
|
+
/** `dismissible(false)`: scrim taps, the back button and dragging below the lowest detent no
|
|
52
|
+
* longer close the sheet (a drag collapses to the lowest detent instead). Default `true`. */
|
|
53
|
+
dismissible(enabled: boolean): this,
|
|
54
|
+
|
|
55
|
+
append(...nodes: UINodeChild[]): this
|
|
56
|
+
insert(index: number, ...nodes: UINodeChild[]): this
|
|
57
|
+
remove(...nodes: UINodeChild[]): this
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class BottomSheetElement extends ModalElement {
|
|
61
|
+
_detents: number[] = [1]
|
|
62
|
+
_detent = 0
|
|
63
|
+
/** Host-facing (the `sl`/`otl` contract): hosts read `dtl` at mount and call it with the
|
|
64
|
+
* settled detent index for GESTURE settles only — programmatic `setDetent` fires JS-side.
|
|
65
|
+
* `-1` = dragged below the lowest detent while dismissible. */
|
|
66
|
+
readonly dtl: any[] = []
|
|
67
|
+
readonly _detentListeners: ((index: number) => void)[] = []
|
|
68
|
+
|
|
69
|
+
constructor(style: UIBottomSheetStyle, children: UINodeChild[] | ChildrenFn) {
|
|
70
|
+
// No default height: an absolute node with only bottom set is CONTENT-sized by yoga. A
|
|
71
|
+
// single detent makes the geometry height-independent on every host (shown = offset 0,
|
|
72
|
+
// hidden = own height), so content mode needs no host support beyond the detents path.
|
|
73
|
+
super({ left: 0, right: 0, bottom: 0, maxHeight: "100%", sheetDetents: "1", ...style } as UIModalStyle, children)
|
|
74
|
+
// The inherited show/hide pose animates ONLY the scrim: sheet geometry rides the
|
|
75
|
+
// sheetDetent style key, never the pose system.
|
|
76
|
+
this._transition = { duration: SHEET_ANIM_MS }
|
|
77
|
+
this.dtl.push((index: number) => {
|
|
78
|
+
if (index < 0) { this.hide(); return }
|
|
79
|
+
this._detent = index
|
|
80
|
+
for (const callback of this._detentListeners) callback(index)
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
detents(list: number[]): this {
|
|
85
|
+
const sorted = [...list].filter(d => d > 0).sort((a, b) => a - b)
|
|
86
|
+
if (sorted.length === 0) return this
|
|
87
|
+
this._detents = sorted
|
|
88
|
+
if (this._detent >= sorted.length) this._detent = sorted.length - 1
|
|
89
|
+
this.style({
|
|
90
|
+
height: `${sorted[sorted.length - 1] * 100}%`,
|
|
91
|
+
sheetDetents: sorted.join(","),
|
|
92
|
+
} as UIBottomSheetStyle)
|
|
93
|
+
return this
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get detent(): number {
|
|
97
|
+
return this._detent
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
setDetent(index: number): void {
|
|
101
|
+
const clamped = Math.max(0, Math.min(Math.floor(index), this._detents.length - 1))
|
|
102
|
+
this._detent = clamped
|
|
103
|
+
if (!this._open) return // applied by the next show()
|
|
104
|
+
this.style({ sheetDetent: clamped } as UIBottomSheetStyle)
|
|
105
|
+
for (const callback of this._detentListeners) callback(clamped)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
onDetentChange(callback: (index: number) => void): this {
|
|
109
|
+
this._detentListeners.push(callback)
|
|
110
|
+
return this
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
show(): void {
|
|
114
|
+
if (this._open) return
|
|
115
|
+
// Before the mount, so the built node carries the target and hosts play the entrance to it.
|
|
116
|
+
this.style({ sheetDetent: this._detent } as UIBottomSheetStyle)
|
|
117
|
+
super.show()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
hide(): void {
|
|
121
|
+
if (!this._open) return
|
|
122
|
+
this.style({ sheetDetent: -1 } as UIBottomSheetStyle) // hosts slide out; unmount follows
|
|
123
|
+
super.hide()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
dismissible(enabled: boolean): this {
|
|
127
|
+
super.dismissible(enabled)
|
|
128
|
+
this.style({ sheetDismissible: enabled ? 1 : 0 } as UIBottomSheetStyle)
|
|
129
|
+
return this
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function UIBottomSheet(): UIBottomSheet;
|
|
134
|
+
export function UIBottomSheet(children: UINodeChild[] | ChildrenFn): UIBottomSheet;
|
|
135
|
+
export function UIBottomSheet(style: UIBottomSheetStyle): UIBottomSheet;
|
|
136
|
+
export function UIBottomSheet(style: UIBottomSheetStyle, children: UINodeChild[] | ChildrenFn): UIBottomSheet;
|
|
137
|
+
export function UIBottomSheet(...args: [] | [UINodeChild[] | ChildrenFn | UIBottomSheetStyle] | [UIBottomSheetStyle, children: UINodeChild[] | ChildrenFn]): UIBottomSheet {
|
|
138
|
+
if (args.length === 0) {
|
|
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
|
+
}
|
|
149
|
+
}
|
|
@@ -6,8 +6,14 @@ type InputStyle = {
|
|
|
6
6
|
/**
|
|
7
7
|
* Keyboard hint, NOT a validator — `number` does not block pasted letters; validate in your own
|
|
8
8
|
* onChange. The phone-pad value is `phone` (LeCodes has no `tel`).
|
|
9
|
+
*
|
|
10
|
+
* `date` / `time` are PICKER kinds, not keyboards: the host opens its native picker in the
|
|
11
|
+
* keyboard slot (iOS wheels / browser calendar) and free typing is disabled. `value` is always
|
|
12
|
+
* canonical — `YYYY-MM-DD` for `date`, `HH:MM` (24h) for `time` — while the field displays a
|
|
13
|
+
* localized string; set/compare values in the canonical form only.
|
|
9
14
|
*/
|
|
10
|
-
type?: "text" | "password" | "search" | "phone" | "email" | "number" | "decimal" | "url"
|
|
15
|
+
type?: "text" | "password" | "search" | "phone" | "email" | "number" | "decimal" | "url"
|
|
16
|
+
| "date" | "time",
|
|
11
17
|
/**
|
|
12
18
|
* What the layout does while the keyboard is up (decided by the FOCUSED input). `true` (default):
|
|
13
19
|
* the layout viewport shrinks to the area above the keyboard — one relayout. `false`: the keyboard
|
|
@@ -16,6 +22,15 @@ type InputStyle = {
|
|
|
16
22
|
* reflow is worse than overlap.
|
|
17
23
|
*/
|
|
18
24
|
keyboardShrink?: boolean,
|
|
25
|
+
/**
|
|
26
|
+
* Whether a tap on a non-interactive area dismisses the keyboard while THIS input is focused
|
|
27
|
+
* (like `keyboardShrink`, the policy of the focused input). `true` (default): tapping dead
|
|
28
|
+
* space blurs — but a tap on a button or another input NEVER dismisses; the control acts and
|
|
29
|
+
* focus stays. `false`: taps never dismiss — the chat-composer policy. `blur()`, the return
|
|
30
|
+
* key (per `enterKey`), and the platform's own gestures (iOS drag-down over the keyboard)
|
|
31
|
+
* still work in both modes. Scrolling never dismisses the keyboard on any setting.
|
|
32
|
+
*/
|
|
33
|
+
keyboardDismiss?: boolean,
|
|
19
34
|
/** Hard cap on the text length — enforced by the host while typing/pasting. */
|
|
20
35
|
maxLength?: number,
|
|
21
36
|
/** Auto-capitalization hint; omitted = platform default. */
|
|
@@ -148,6 +163,14 @@ export function UIInput(style?: UIInputStyle): UIInput {
|
|
|
148
163
|
return new InputElement("input", { minHeight: "1.2em", ...style })
|
|
149
164
|
}
|
|
150
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Multi-line text field. Enter is a newline (submit lives on UIInput / your own button).
|
|
168
|
+
*
|
|
169
|
+
* Auto-grow: with no explicit `height` the field measures its own content and grows line by
|
|
170
|
+
* line — clamp the range with `minHeight` / `maxHeight` (past `maxHeight` the text scrolls
|
|
171
|
+
* inside). A fixed `height` opts out and scrolls internally from the start. Chat composers are
|
|
172
|
+
* the canonical use: a bare textarea in a padded pill, `maxHeight` at ~5 lines.
|
|
173
|
+
*/
|
|
151
174
|
export function UITextArea(): UITextArea;
|
|
152
175
|
export function UITextArea(style: UIInputStyle): UITextArea;
|
|
153
176
|
export function UITextArea(style?: UIInputStyle): UITextArea {
|
|
@@ -2,7 +2,15 @@ import type { UIContainerStyle } from "./UIContainer"
|
|
|
2
2
|
import { type Style, type AnimateStyle, type UINodeChild, type ChildrenFn, type OnLayoutCallback, type BoundingClientRect, type Color, ContainerElement } from "./UINode"
|
|
3
3
|
|
|
4
4
|
// refreshControlColor tints the native pull-to-refresh spinner (pairs with onRefresh).
|
|
5
|
-
|
|
5
|
+
// keyboardDismissMode: how a scroll gesture in THIS scrollable dismisses the keyboard.
|
|
6
|
+
// "interactive" (default) — dragging over the keyboard slides it away (chat composer feel);
|
|
7
|
+
// "scroll" — any drag dismisses the moment it starts (search-results feel);
|
|
8
|
+
// "none" — scrolling never dismisses (tap-outside policy still applies).
|
|
9
|
+
// snap: paging — when the drag ends, rest on a direct child's boundary along the scroll axis
|
|
10
|
+
// (a carousel/pager is a horizontal scrollable of full-width children + snap:"start"). The value
|
|
11
|
+
// picks where the child sits in the viewport; "none" (default) is free scrolling. Snap targets
|
|
12
|
+
// are the children themselves, so item widths can differ (card-carousel-with-peek).
|
|
13
|
+
export type ScrollStyle = { showScrollbar?: boolean, overscrollMode?: "none" | "absorb" | "default", refreshControlColor?: Color, keyboardDismissMode?: "interactive" | "scroll" | "none", snap?: "none" | "start" | "center" | "end" }
|
|
6
14
|
|
|
7
15
|
export type UIScrollableStyle = UIContainerStyle & ScrollStyle & { scrollDirection?: "horizontal" | "vertical" }
|
|
8
16
|
|
|
@@ -80,7 +80,10 @@ export class VListElement<T> extends Element<"vlist"> implements UIVirtualizedLi
|
|
|
80
80
|
private estimate: (item: T) => number
|
|
81
81
|
|
|
82
82
|
constructor(config: VListConfig<T>) {
|
|
83
|
-
|
|
83
|
+
// flexShrink: 1 default (SDK-owned, same as UIScrollable): an explicit height/flexBasis
|
|
84
|
+
// larger than the available space shrinks to fit instead of overflowing. The vlist's own
|
|
85
|
+
// yoga basis is 0 (items live in detached roots), so this is inert until a size is set.
|
|
86
|
+
super("vlist", { flexShrink: 1 })
|
|
84
87
|
this.keyOf = config.keyOf
|
|
85
88
|
this.renderItem = config.render
|
|
86
89
|
this.estimate = typeof config.estimatedHeight === "function"
|
|
@@ -20,7 +20,10 @@ export interface UIWidget {
|
|
|
20
20
|
* Attach this widget to a destination (a `Scene`, a `UIScreen`, …): the widget lives on that
|
|
21
21
|
* page's layer — it shows/hides with the page and rides its transition, and a screen pushed
|
|
22
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.
|
|
23
|
+
* (= back to a global overlay). Set it while the widget is hidden. The widget is always its own
|
|
24
|
+
* overlay root mounted into the owner's page view — it never becomes a child of the owner, so it
|
|
25
|
+
* stays topmost and its updates never trigger the owner's relayout. Works with a `UIScreen` that
|
|
26
|
+
* is a page inside a `UIPager`: the widget mounts into that tab and rides its swipe.
|
|
24
27
|
*/
|
|
25
28
|
attachTo(owner: Presentable | null): this,
|
|
26
29
|
|
|
@@ -66,7 +69,10 @@ export class WidgetElement extends ContainerElement<"widget"> {
|
|
|
66
69
|
return this
|
|
67
70
|
}
|
|
68
71
|
show(): void {
|
|
69
|
-
//
|
|
72
|
+
// A widget is always its OWN overlay root (a separate Yoga tree mounted above the owner's
|
|
73
|
+
// content) — never a child of the owner, so it stays topmost, its updates never relayout the
|
|
74
|
+
// owner, and it doesn't pollute the owner's children. `attachTo(owner)` just tells the host
|
|
75
|
+
// which page view to mount that root into; ownerless widgets are global overlays.
|
|
70
76
|
if (this._owner && _navSupported()) {
|
|
71
77
|
_creatorUI.showWidget(this, _descOf(this._owner))
|
|
72
78
|
} else {
|