lecodes-sdk 2.2.0 → 2.2.1

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/package.json CHANGED
@@ -45,7 +45,7 @@
45
45
  "marcidb-embedded": "^0.14.1",
46
46
  "sharp": "^0.35.2"
47
47
  },
48
- "version": "2.2.0",
48
+ "version": "2.2.1",
49
49
  "files": [
50
50
  "src",
51
51
  "dist",
package/prompts/ui.md CHANGED
@@ -191,6 +191,7 @@ UIInput() / UITextArea()
191
191
  // type: "text"|"password"|"search"|"phone"|"email"|"number"|"decimal"|"url"|"date"|"time"
192
192
  // enterKey: "done"|"go"|"next"|"search"|"send" (UIInput only) — return-key label
193
193
  // maxLength: number; autocapitalize: "none"|"words"|"sentences"|"characters"; autocorrect: boolean
194
+ // autofocus: boolean — takes the focus as its screen opens (a composer, a chat, a search; once, one per screen)
194
195
  // keyboardShrink: boolean (default true) — false: keyboard OVERLAYS the UI, no relayout (chat composers)
195
196
  // keyboardDismiss: boolean (default true) — false: taps never dismiss the keyboard (chat composers)
196
197
  // $focused: { borderColor, ..., duration? } — style while focused (reserved class)
package/src/ui/UIInput.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type ElementStyle, type DrawableStyle, Element, type TextStyle, type Color, type BaseStyle, type UIElementBase } from "./UINode";
2
2
  import { TREE_FLAG_BLUR, TREE_FLAG_CHANGE, TREE_FLAG_FOCUS, TREE_FLAG_SUBMIT, tree } from "./tree"
3
+ import { registerAutofocus } from "./autofocus"
3
4
 
4
5
  type InputStyle = {
5
6
  placeholder?: string,
@@ -32,6 +33,13 @@ type InputStyle = {
32
33
  * still work in both modes. Scrolling never dismisses the keyboard on any setting.
33
34
  */
34
35
  keyboardDismiss?: boolean,
36
+ /**
37
+ * Takes the focus when its screen opens — the keyboard is up from the first moment. For a
38
+ * screen made for typing: a composer, a chat, a search, a one-field form; not the first field
39
+ * of a long form. Once per input (a screen shown again does not take the keyboard back); one
40
+ * per screen. The SDK's own behaviour over `focus()`: a design tool shows the screen as it opens.
41
+ */
42
+ autofocus?: boolean,
35
43
  /** Hard cap on the text length — enforced by the host while typing/pasting. */
36
44
  maxLength?: number,
37
45
  /** Auto-capitalization hint; omitted = platform default. */
@@ -110,9 +118,11 @@ class InputElement<T extends "input" | "textarea"> extends Element<T> {
110
118
  }
111
119
 
112
120
  // `phone` is SDK-surface only — hosts (and the web's verbatim `el.type`) keep HTML's `tel` as
113
- // the wire format, so the rename never reaches them.
121
+ // the wire format, so the rename never reaches them. `autofocus` is the SDK's too (./autofocus.ts):
122
+ // it never reaches a host.
114
123
  _mapStyle(style: any): void {
115
124
  if (style.type === "phone") style.type = "tel"
125
+ if ("autofocus" in style) { registerAutofocus(this, !!style.autofocus); delete style.autofocus }
116
126
  }
117
127
 
118
128
  onChange (callback: (value: string) => void): this {
package/src/ui/UINode.ts CHANGED
@@ -395,6 +395,7 @@ const handler: ProxyHandler<any> = {
395
395
  if (el._mapStyle !== undefined) {
396
396
  const mapped = { [key]: value }
397
397
  el._mapStyle(mapped)
398
+ if (!(key in mapped)) return true // the SDK's own key (an input's autofocus): nothing for the host
398
399
  value = mapped[key]
399
400
  }
400
401
  const wire = wireValue(key, value)
@@ -3,6 +3,7 @@ import { _bumpNavEpoch, _setCurrent, Presentable, type DestTuple, type DismissOp
3
3
  import { _transitionId, TRANSITION_NONE } from "./transitions"
4
4
  import { ContainerElement, buildUI, type BaseStyle, type ContainerStyle, type DrawableStyle, type PaddingStyle, type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, } from "./UINode"
5
5
  import { TREE_DEST_SCREEN, TREE_FLAG_BACK, TREE_FLAG_INTERACTIVE, TREE_FLAG_KEEP_ALIVE, pin, tree } from "./tree"
6
+ import { autofocusIn } from "./autofocus"
6
7
 
7
8
  export type UIScreenStyle = ContainerStyle & DrawableStyle & PaddingStyle & BaseStyle
8
9
 
@@ -92,7 +93,7 @@ export class ScreenElement extends ContainerElement<"screen"> {
92
93
  return this
93
94
  }
94
95
  /** @internal presentation lifecycle from the runtime (also a pager's page open / close). */
95
- _emitOpen(): void { for (const cb of this.ol.slice()) cb() }
96
+ _emitOpen(): void { autofocusIn(this._h.id); for (const cb of this.ol.slice()) cb() }
96
97
  /** @internal */
97
98
  _emitClose(): void { for (const cb of this.cl.slice()) cb() }
98
99
 
@@ -0,0 +1,54 @@
1
+ // Autofocus: an input that takes the focus when its screen opens (`autofocus: true` in its style)
2
+ // — a composer, a chat, a search, a one-field form open with the keyboard. The behaviour is the
3
+ // SDK's over the hosts' `focus()`: no host knows the key. An input registers here when the key is
4
+ // set; a screen's open (UIScreen._emitOpen, a pager's page too) asks for the one inside it. Once
5
+ // per input: a screen shown again (uncovered, its tab chosen again) does not take the keyboard
6
+ // back. Two on one screen is a mistake the console says; the first keeps it. The registry holds
7
+ // the inputs weakly: a screen that was dropped goes with its input.
8
+ import { tree } from "./tree"
9
+
10
+ interface WeakRefLike<T extends object> { deref(): T | undefined }
11
+ const WeakRefImpl: new <T extends object>(target: T) => WeakRefLike<T> =
12
+ typeof WeakRef !== "undefined" ? WeakRef : class StrongRef<T extends object> {
13
+ private _t: T
14
+ constructor(target: T) { this._t = target }
15
+ deref(): T | undefined { return this._t }
16
+ }
17
+
18
+ type Focusable = { readonly _h: { readonly id: number }, focus(): void, _autofocusDone?: boolean }
19
+
20
+ const registry = new Set<WeakRefLike<Focusable>>()
21
+ const refOf = (el: Focusable): WeakRefLike<Focusable> | undefined => { for (const ref of registry) if (ref.deref() === el) return ref; return undefined }
22
+
23
+ /** @internal The input asks for (or gives up) the focus at its screen's open. */
24
+ export const registerAutofocus = (el: Focusable, on: boolean): void => {
25
+ const ref = refOf(el)
26
+ if (on) { if (!ref) registry.add(new WeakRefImpl(el)) }
27
+ else if (ref) registry.delete(ref)
28
+ }
29
+
30
+ const within = (id: number, rootId: number): boolean => {
31
+ const t = tree()
32
+ let p = t.parent(id)
33
+ for (let guard = 0; p !== 0 && guard < 10_000; guard++) {
34
+ if (p === rootId) return true
35
+ p = t.parent(p)
36
+ }
37
+ return false
38
+ }
39
+
40
+ /** @internal A screen (or page) opened: the autofocus input inside it takes the focus, once. */
41
+ export const autofocusIn = (rootId: number): void => {
42
+ if (registry.size === 0) return
43
+ let taken: Focusable | null = null
44
+ for (const ref of [...registry]) {
45
+ const el = ref.deref()
46
+ if (!el || el._h.id === 0) { registry.delete(ref); continue }
47
+ if (el._autofocusDone || !within(el._h.id, rootId)) continue
48
+ if (taken) { console.warn("autofocus: two inputs on one screen ask for it — the first keeps it"); continue }
49
+ taken = el
50
+ }
51
+ if (!taken) return
52
+ taken._autofocusDone = true
53
+ taken.focus()
54
+ }
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.2.0"
7
+ export const SDK_VERSION = "2.2.1"
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 => {