Skip to content

Version Packages - #23

Merged
amritk merged 1 commit into
mainfrom
changeset-release/main
Aug 5, 2026
Merged

Version Packages#23
amritk merged 1 commit into
mainfrom
changeset-release/main

Conversation

@github-actions

@github-actions github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

Releases

@amritk/lynx-deep-linking@0.2.0

Minor Changes

  • c657444: Add @amritk/lynx-deep-linking — deep links for Lynx, as an Android native
    module, an iOS native module and a promise-shaped facade over
    @amritk/mini-lynx-native.

    Lynx ships no linking module, and the two published community packages are parts
    of frameworks rather than libraries: @sigx/lynx-linking needs @sigx/lynx-core
    and a sigx prebuild step, @tamer4lynx/tamer-linking peers on ReactLynx and
    links through the t4l CLI, and neither reaches the main thread a mini-lynx
    tree renders on.

    Inbound, the launch URL is a value (getInitialURL) and every later link is an
    event (onDeepLink), so an app can handle both without handling one tap twice;
    a link that arrives with no view up is held natively and replayed once. Outbound,
    openURL, canOpenURL and openSettings — the last being the missing half of
    the denied states @amritk/lynx-location and @amritk/lynx-notifications
    report. parseURL and createURL are pure and need no bridge.

    Cold start needs no host wiring on either platform; a link into a running app
    needs one forwarded callback, which no library can observe for itself.

Patch Changes

  • 1b6c33d: Bring every package's shipped AI.md back in line with what that package
    actually publishes, and add bun run check:ai-docs so it cannot drift again.

    The files had gone stale in the way generated-and-committed docs always do —
    silently, and only for the audience that cannot file an issue about it.
    @amritk/mini never documented watch, template, the typed matchRoute /
    buildPath re-exports on /router, Field on /forms, or the /vite subpath
    at all; @amritk/mini-lynx was missing computed / effectScope,
    fadeTransition, keepAboveKeyboard and HANDLER_PREFIX;
    @amritk/lynx-notifications documented neither its /testing subpath nor the
    fake behind it. All four native packages exported MODULE and EVENTS with no
    mention of what they are for, and only @amritk/lynx-dialogs showed how to wire
    a fake into installNativeBridge — which is the one thing a consumer testing
    its own screens needs.

    Two accuracy fixes matter more than the additions. Every native package's
    Status section claimed the Objective-C compiles against the real Lynx pod; the
    macOS CI job was disabled on cost, so it now compiles only when somebody runs
    pod lib lint by hand, and the docs say that. And @amritk/lynx-dialogs never
    carried a Status section at all, so nothing in it told a reader that none of
    it has run on a device.

    bun run check:ai-docs reads each package's exports and fails on a runtime
    export, a published subpath, or (for a package shipping native sources) a
    Status section its AI.md never mentions. It runs early in CI, before the
    build. Exports no consumer ever writes — the tree operations the JSX transform
    calls, and the like — are listed in INTERNAL_EXPORTS with the reason.

  • Updated dependencies [e025ac7]

  • Updated dependencies [5101aa7]

  • Updated dependencies [ab6476a]

    • @amritk/mini-lynx-native@0.2.0
    • @amritk/mini-helpers@0.2.0

@amritk/lynx-dialogs@0.2.0

Minor Changes

  • 2b07688: Add @amritk/lynx-dialogs — the platform's own date picker, action sheet and
    alert for Lynx.

    Lynx ships no picker element and no modal module, and neither published option
    fills the gap: @lynx-js/lynx-ui-sheet draws a sheet out of ReactLynx elements
    rather than presenting a native one, and @sigx/lynx-datetime-picker is a real
    UIDatePicker/DatePickerDialog bolted to another framework's bridge. Nothing
    native exists for action sheets at all.

    So this is an Android module, an iOS module, and a promise-shaped facade over
    @amritk/mini-lynx-native:

    const date = await presentDatePicker({ mode: "date", maximum: Date.now() });
    if (date.ok) setDeparture(new Date(date.value));
    
    const choice = await presentActionSheet({
      actions: [{ label: "Replace" }, { label: "Delete", destructive: true }],
    });
    if (choice.ok) apply(choice.index);
    
    const confirm = await presentAlert({
      title: "Delete this photo?",
      buttons: [
        { label: "Cancel", style: "cancel" },
        { label: "Delete", style: "destructive" },
      ],
    });
    if (confirm.ok && confirm.index === 1) remove();

    presentDatePicker covers date, time and datetime with bounds, labels and
    a 12/24-hour override; presentActionSheet covers destructive and disabled rows
    and the iPad popover anchor; presentAlert covers one to three buttons with
    cancel and destructive styling. dismissActiveDialog closes whatever is up so a
    screen can clean up on navigation, and areDialogsAvailable reports whether the
    host app linked the module.

    AlertButtons is a one-to-three tuple rather than an array, because
    AlertDialog has exactly three button slots and there is no fourth — so the
    Android cap is a compile error instead of a button that goes missing on half the
    devices an app runs on.

    Every outcome is a discriminated union rather than a rejection — cancelling is
    the most likely thing a user does with a dialog, so it is a branch. Only one
    presentation is allowed on screen at a time; a second resolves
    { ok: false, reason: 'busy' } rather than stacking on Android or hanging
    forever on iOS.

    Unlike the other two native packages there is no permission, no Info.plist key
    and no manifest entry for the host app to add, and the Android half takes no
    dependencies at all — the framework AlertDialog rather than Material, so no
    Theme.Material3 requirement lands on the host's Activity.

    @amritk/lynx-dialogs/testing ships the native contract as an executable fake.

Patch Changes

  • 1b6c33d: Bring every package's shipped AI.md back in line with what that package
    actually publishes, and add bun run check:ai-docs so it cannot drift again.

    The files had gone stale in the way generated-and-committed docs always do —
    silently, and only for the audience that cannot file an issue about it.
    @amritk/mini never documented watch, template, the typed matchRoute /
    buildPath re-exports on /router, Field on /forms, or the /vite subpath
    at all; @amritk/mini-lynx was missing computed / effectScope,
    fadeTransition, keepAboveKeyboard and HANDLER_PREFIX;
    @amritk/lynx-notifications documented neither its /testing subpath nor the
    fake behind it. All four native packages exported MODULE and EVENTS with no
    mention of what they are for, and only @amritk/lynx-dialogs showed how to wire
    a fake into installNativeBridge — which is the one thing a consumer testing
    its own screens needs.

    Two accuracy fixes matter more than the additions. Every native package's
    Status section claimed the Objective-C compiles against the real Lynx pod; the
    macOS CI job was disabled on cost, so it now compiles only when somebody runs
    pod lib lint by hand, and the docs say that. And @amritk/lynx-dialogs never
    carried a Status section at all, so nothing in it told a reader that none of
    it has run on a device.

    bun run check:ai-docs reads each package's exports and fails on a runtime
    export, a published subpath, or (for a package shipping native sources) a
    Status section its AI.md never mentions. It runs early in CI, before the
    build. Exports no consumer ever writes — the tree operations the JSX transform
    calls, and the like — are listed in INTERNAL_EXPORTS with the reason.

  • Updated dependencies [e025ac7]

  • Updated dependencies [5101aa7]

    • @amritk/mini-lynx-native@0.2.0

@amritk/lynx-location@0.2.0

Minor Changes

  • 293234c: Add @amritk/lynx-location — device location for Lynx.

    Lynx ships no location module, Sparkling's built-ins stop at navigation,
    storage and media, and the one published community package
    (@sigx/lynx-location) calls NativeModules directly from its JavaScript half,
    which is background-thread only — so a main-thread @amritk/mini-lynx component
    importing it gets undefined. This is an Android module, an iOS module, and a
    promise-shaped facade over @amritk/mini-lynx-native, built from the shape
    @amritk/lynx-notifications established.

    getPermissionStatus, requestPermission, getCurrentPosition,
    getLastKnownPosition, isLocationEnabled, isLocationAvailable and
    watchPosition. CLLocationManager on iOS, LocationManager on Android — not
    the fused provider, so no Play Services dependency and no non-GMS devices
    excluded. Foreground only, deliberately: no ACCESS_BACKGROUND_LOCATION and no
    Always authorisation.

    Failures are values rather than rejections — { ok: true, position } or
    { ok: false, error, message } — because Lynx has no error convention for
    bridge callbacks and a missing permission is an ordinary UI branch.

    @amritk/lynx-location/testing ships the native contract as an executable fake.
    Nothing here has run on a device; see the package's AGENTS.md for what the
    three CI checks do and do not cover.

  • 7c690ed: Add reverseGeocode to @amritk/lynx-location — coordinates to a postal
    address, on both platforms.

    CLGeocoder on iOS, android.location.Geocoder on Android. The Android half
    uses the API 33 callback form where it exists and the blocking form on an
    executor below that, because this library supports devices well under 33 and a
    network round trip on the calling thread is an ANR waiting to happen.

    It needs no permission and never prompts. Reverse geocoding reads no device
    location — it geocodes the coordinates it is handed — so an app that has been
    refused location outright can still label a saved venue or a map centre. A
    LocationFix satisfies the new Coordinates type, so pairing it with
    getCurrentPosition needs no mapping step.

    GeocodeResult is a discriminated union like the rest of the package:
    { ok: true, addresses } or { ok: false, error, message }, where error is
    invalidCoordinates, notFound, network or unavailable. Being throttled is
    network rather than a code of its own — Apple rate-limits CLGeocoder and
    reports it that way, and Android has no equivalent to report. notFound is the
    only spelling of "there is no address there"; an empty addresses is never a
    success.

    Every GeocodeAddress field is nullable, and formattedAddress is built by the
    OS — getAddressLine(0) on Android, CNPostalAddressFormatter on iOS — so it
    places each country's postcode where that country places it. isoCountryCode is
    the only field stable across locales.

    The iOS half now links Contacts, for CNPostalAddressFormatter alone. It
    reaches no contact store and needs no permission.

    createFakeLocation gains setNextAddresses, setGeocoderPresent,
    setNetworkAvailable and geocodes(). Nothing here has run on a device; the
    package's AGENTS.md lists what reverse geocoding specifically leaves unproven.

Patch Changes

  • 1b6c33d: Bring every package's shipped AI.md back in line with what that package
    actually publishes, and add bun run check:ai-docs so it cannot drift again.

    The files had gone stale in the way generated-and-committed docs always do —
    silently, and only for the audience that cannot file an issue about it.
    @amritk/mini never documented watch, template, the typed matchRoute /
    buildPath re-exports on /router, Field on /forms, or the /vite subpath
    at all; @amritk/mini-lynx was missing computed / effectScope,
    fadeTransition, keepAboveKeyboard and HANDLER_PREFIX;
    @amritk/lynx-notifications documented neither its /testing subpath nor the
    fake behind it. All four native packages exported MODULE and EVENTS with no
    mention of what they are for, and only @amritk/lynx-dialogs showed how to wire
    a fake into installNativeBridge — which is the one thing a consumer testing
    its own screens needs.

    Two accuracy fixes matter more than the additions. Every native package's
    Status section claimed the Objective-C compiles against the real Lynx pod; the
    macOS CI job was disabled on cost, so it now compiles only when somebody runs
    pod lib lint by hand, and the docs say that. And @amritk/lynx-dialogs never
    carried a Status section at all, so nothing in it told a reader that none of
    it has run on a device.

    bun run check:ai-docs reads each package's exports and fails on a runtime
    export, a published subpath, or (for a package shipping native sources) a
    Status section its AI.md never mentions. It runs early in CI, before the
    build. Exports no consumer ever writes — the tree operations the JSX transform
    calls, and the like — are listed in INTERNAL_EXPORTS with the reason.

  • Updated dependencies [e025ac7]

  • Updated dependencies [5101aa7]

    • @amritk/mini-lynx-native@0.2.0

@amritk/lynx-notifications@0.2.0

Minor Changes

  • e025ac7: Add native modules for Lynx, starting with push notifications.

    Lynx ships no notifications module, and Sparkling's sparkling-notifications is
    a reserved npm name with no implementation behind it — so the official answer is
    still "write native code and send it into your Lynx code". These two packages
    are that, plus the piece it needs first.

    @amritk/mini-lynx-native is the wire. Lynx's NativeModules and
    GlobalEventEmitter are background-thread globals, while @amritk/mini-lynx
    renders on the main thread because the Element PAPI is a main-thread API — so a
    component reaching for a native module finds undefined, with nothing to read.
    The package carries calls one way and events the other: callNative and
    callNativeAsync for the two shapes a Lynx native method comes in,
    isNativeModuleAvailable for feature detection, and onNativeEvent for
    sendGlobalEvent. Calls made before the background half is installed are queued
    rather than lost, because the main-thread chunk usually runs first. The
    /background subpath is the half an app installs in its background chunk, in
    one line; /testing ships the fakes both halves run against.

    @amritk/lynx-notifications is the first module built on it: local and
    remote push, with an Android implementation (NotificationManager,
    AlarmManager, FCM) and an iOS one (UNUserNotificationCenter, APNs) declared
    to Lynx's autolinker through lynx.lib.json. The JavaScript surface is promises
    and subscriptions rather than signals, deliberately — a second edge onto the
    signal engine is how a consumer ends up with two reactive graphs that cannot see
    each other's writes.

    Both native halves are compiled in CI — Gradle against the real
    org.lynxsdk.lynx:lynx AAR for Android, pod lib lint against the real Lynx pod
    on a macOS runner for iOS — and src/native-contract.test.ts pins their method
    names, arities and event strings against the TypeScript, which is the one class
    of drift no compiler on either side can catch. bun run check:android runs the
    Android compile locally and skips with an explanation when there is no SDK.

    None of that has run on a device. Permission flows, AlarmManager under
    Doze, APNs registration and FCM delivery are unverified. The caveat is carried in
    the package's README.md, AI.md and AGENTS.md.

Patch Changes

  • 1b6c33d: Bring every package's shipped AI.md back in line with what that package
    actually publishes, and add bun run check:ai-docs so it cannot drift again.

    The files had gone stale in the way generated-and-committed docs always do —
    silently, and only for the audience that cannot file an issue about it.
    @amritk/mini never documented watch, template, the typed matchRoute /
    buildPath re-exports on /router, Field on /forms, or the /vite subpath
    at all; @amritk/mini-lynx was missing computed / effectScope,
    fadeTransition, keepAboveKeyboard and HANDLER_PREFIX;
    @amritk/lynx-notifications documented neither its /testing subpath nor the
    fake behind it. All four native packages exported MODULE and EVENTS with no
    mention of what they are for, and only @amritk/lynx-dialogs showed how to wire
    a fake into installNativeBridge — which is the one thing a consumer testing
    its own screens needs.

    Two accuracy fixes matter more than the additions. Every native package's
    Status section claimed the Objective-C compiles against the real Lynx pod; the
    macOS CI job was disabled on cost, so it now compiles only when somebody runs
    pod lib lint by hand, and the docs say that. And @amritk/lynx-dialogs never
    carried a Status section at all, so nothing in it told a reader that none of
    it has run on a device.

    bun run check:ai-docs reads each package's exports and fails on a runtime
    export, a published subpath, or (for a package shipping native sources) a
    Status section its AI.md never mentions. It runs early in CI, before the
    build. Exports no consumer ever writes — the tree operations the JSX transform
    calls, and the like — are listed in INTERNAL_EXPORTS with the reason.

  • 4423822: Backport the dangling-selector check from @amritk/lynx-location's parity
    suite: native-contract.test.ts now asserts that every selector named in the
    Objective-C methodLookup table has a method implementing it.

    That is the one cross-language failure nothing else here could see. A selector
    string pointing at no method is not a build error on iOS — pod lib lint
    passes — and fails only when Lynx tries to dispatch through it, on a device, as
    a promise that never settles. All eleven of the package's selectors resolve
    today; the check is mutation-verified.

    No runtime change.

  • Updated dependencies [e025ac7]

  • Updated dependencies [5101aa7]

    • @amritk/mini-lynx-native@0.2.0

@amritk/mini@0.7.0

Minor Changes

  • ab6476a: Type route params from the pattern, and give @amritk/mini-lynx a browser
    history again.

    The pattern is now read at the type level. PathParams<'/users/:id'> is
    { id: string }, and matchRoute is generic over its pattern, so
    matchRoute('/users/:id', path) hands back { id: string } | null instead of a
    record that answers string for every key including the misspelt ones. It lives
    in @amritk/mini-helpers beside the matcher it mirrors, because the grammar has
    to be one definition or the value and type worlds drift; path-params.test.ts
    pins the two together, down to /v:major being a literal segment on both sides.

    PathParams<string> is RouteParams, so this is additive: a table annotated
    Route[], or built at runtime, compiles exactly as before and gets exactly what
    it got before.

    buildPath is the inverse. buildPath('/users/:id', { id }) cannot spell
    the pattern wrong, forget a param or be left behind when a route is renamed, and
    its values are encoded to round-trip back through matchRoute. A * wildcard
    is encoded per segment, because rest is a path and its slashes are structure.
    navigate still takes a plain string, deliberately — a router navigates to
    concrete paths, and one that matches nothing is what a fallback screen is for —
    so the check sits where the string is assembled instead.

    @amritk/mini-lynx/router gains route() and AnyRoute.
    route('/users/:id', (params) => …, meta) keeps the pattern's literal type
    alive so the view is handed a () => { id: string }. Its three arguments are
    not a style choice: folded into one object literal, the pattern and the metadata
    compete for inference against an intersection, and the metadata loses — a tab
    bar reading route.label would get unknown back.

    Route is now Route<P extends string = string>, and the router's generics are
    constrained by the new AnyRoute. Route<'/users/:id'> is deliberately not
    assignable to Route<string> — its view demands the narrow getter, and the
    widened form can only promise the flat record — so a table of mixed patterns
    needs a constraint that admits all of them. render-route.ts owns the single
    cast that reconciles the two. Existing tables keep working: Route with no type
    argument is what it always was.

    createBrowserHistory is back, on @amritk/mini-lynx/router/browser. It is
    a RouterHistory and nothing else, because the rest of routing is already
    target-free — a device build swaps createMemoryHistory() back in and changes
    nothing else. It takes a base prefix, and it stamps its depth into
    history.state rather than reading window.history.length, which counts the
    whole tab and is wrong in both directions once the user has gone back. That
    stamp is what survives the two things a memory stack never faces: a reload
    mid-stack and a forward button.

    It is the only module in the package that names window, and it is quarantined
    accordingly. src/router/browser/ is excluded from the main compiler pass and
    covered by tsconfig.dom.json, so the platform-free rule the rest of the
    package keeps stays something the compiler enforces rather than a convention.
    Hash mode is not included: configuring a host for an SPA fallback is one line,
    where a second URL grammar would be one the device build can never use.

    stripBase moved into @amritk/mini-helpers, since both routers now read a
    browser pathname and a base that meant something slightly different on each side
    is exactly the drift that package exists to prevent. @amritk/mini's router
    surface is unchanged apart from re-exporting buildPath and PathParams.

Patch Changes

  • 1b6c33d: Bring every package's shipped AI.md back in line with what that package
    actually publishes, and add bun run check:ai-docs so it cannot drift again.

    The files had gone stale in the way generated-and-committed docs always do —
    silently, and only for the audience that cannot file an issue about it.
    @amritk/mini never documented watch, template, the typed matchRoute /
    buildPath re-exports on /router, Field on /forms, or the /vite subpath
    at all; @amritk/mini-lynx was missing computed / effectScope,
    fadeTransition, keepAboveKeyboard and HANDLER_PREFIX;
    @amritk/lynx-notifications documented neither its /testing subpath nor the
    fake behind it. All four native packages exported MODULE and EVENTS with no
    mention of what they are for, and only @amritk/lynx-dialogs showed how to wire
    a fake into installNativeBridge — which is the one thing a consumer testing
    its own screens needs.

    Two accuracy fixes matter more than the additions. Every native package's
    Status section claimed the Objective-C compiles against the real Lynx pod; the
    macOS CI job was disabled on cost, so it now compiles only when somebody runs
    pod lib lint by hand, and the docs say that. And @amritk/lynx-dialogs never
    carried a Status section at all, so nothing in it told a reader that none of
    it has run on a device.

    bun run check:ai-docs reads each package's exports and fails on a runtime
    export, a published subpath, or (for a package shipping native sources) a
    Status section its AI.md never mentions. It runs early in CI, before the
    build. Exports no consumer ever writes — the tree operations the JSX transform
    calls, and the like — are listed in INTERNAL_EXPORTS with the reason.

  • Updated dependencies [ab6476a]

    • @amritk/mini-helpers@0.2.0

@amritk/mini-helpers@0.2.0

Minor Changes

  • ab6476a: Type route params from the pattern, and give @amritk/mini-lynx a browser
    history again.

    The pattern is now read at the type level. PathParams<'/users/:id'> is
    { id: string }, and matchRoute is generic over its pattern, so
    matchRoute('/users/:id', path) hands back { id: string } | null instead of a
    record that answers string for every key including the misspelt ones. It lives
    in @amritk/mini-helpers beside the matcher it mirrors, because the grammar has
    to be one definition or the value and type worlds drift; path-params.test.ts
    pins the two together, down to /v:major being a literal segment on both sides.

    PathParams<string> is RouteParams, so this is additive: a table annotated
    Route[], or built at runtime, compiles exactly as before and gets exactly what
    it got before.

    buildPath is the inverse. buildPath('/users/:id', { id }) cannot spell
    the pattern wrong, forget a param or be left behind when a route is renamed, and
    its values are encoded to round-trip back through matchRoute. A * wildcard
    is encoded per segment, because rest is a path and its slashes are structure.
    navigate still takes a plain string, deliberately — a router navigates to
    concrete paths, and one that matches nothing is what a fallback screen is for —
    so the check sits where the string is assembled instead.

    @amritk/mini-lynx/router gains route() and AnyRoute.
    route('/users/:id', (params) => …, meta) keeps the pattern's literal type
    alive so the view is handed a () => { id: string }. Its three arguments are
    not a style choice: folded into one object literal, the pattern and the metadata
    compete for inference against an intersection, and the metadata loses — a tab
    bar reading route.label would get unknown back.

    Route is now Route<P extends string = string>, and the router's generics are
    constrained by the new AnyRoute. Route<'/users/:id'> is deliberately not
    assignable to Route<string> — its view demands the narrow getter, and the
    widened form can only promise the flat record — so a table of mixed patterns
    needs a constraint that admits all of them. render-route.ts owns the single
    cast that reconciles the two. Existing tables keep working: Route with no type
    argument is what it always was.

    createBrowserHistory is back, on @amritk/mini-lynx/router/browser. It is
    a RouterHistory and nothing else, because the rest of routing is already
    target-free — a device build swaps createMemoryHistory() back in and changes
    nothing else. It takes a base prefix, and it stamps its depth into
    history.state rather than reading window.history.length, which counts the
    whole tab and is wrong in both directions once the user has gone back. That
    stamp is what survives the two things a memory stack never faces: a reload
    mid-stack and a forward button.

    It is the only module in the package that names window, and it is quarantined
    accordingly. src/router/browser/ is excluded from the main compiler pass and
    covered by tsconfig.dom.json, so the platform-free rule the rest of the
    package keeps stays something the compiler enforces rather than a convention.
    Hash mode is not included: configuring a host for an SPA fallback is one line,
    where a second URL grammar would be one the device build can never use.

    stripBase moved into @amritk/mini-helpers, since both routers now read a
    browser pathname and a base that meant something slightly different on each side
    is exactly the drift that package exists to prevent. @amritk/mini's router
    surface is unchanged apart from re-exporting buildPath and PathParams.

@amritk/mini-lynx@0.4.0

Minor Changes

  • 59983c4: Add @amritk/mini-lynx/keyboard — the soft keyboard as a signal, and the
    layouts that move out of its way.

    Lynx does not avoid the keyboard for you. <input> does not do it, the docs say
    so outright, and what the engine offers is a single global event —
    keyboardstatuschanged, carrying 'on' | 'off' and a height. Every Lynx app
    with a form writes the layout on top of that event, and the three mistakes are
    always the same: lifting by the whole keyboard rather than by the overlap,
    adding the bottom safe-area inset on top of a keyboard that already covers it,
    and clearing on blur — which makes the screen flinch when focus moves between
    two adjacent fields.

    The wiring is two lines and no provider:

    trackKeyboard() // once, next to setEngine
    
    <KeyboardAvoiding>
      <input ref={avoidKeyboard} placeholder="email" />
      <input ref={avoidKeyboard} type="password" />
    </KeyboardAvoiding>

    ref is this runtime's element-extension seam, so a control reports its own
    focus and the container reads it — nothing is threaded between them.

    • trackKeyboard(options?) subscribes to the engine's GlobalEventEmitter
      and feeds keyboardHeight(). The emitter is an option because Lynx's own
      compatibility data lists keyboardstatuschanged as unsupported on the web:
      a DOM build reports the keyboard from visualViewport and passes it in, and
      everything downstream is the same code.
    • keyboardLift({ inset, offset }) is max(0, height - inset) + offset
      while open and exactly zero while closed — the offset included, because a gap
      above a keyboard that is not there is a hole in the layout.
    • keepAboveKeyboard() measures the focused field against a bounds element
      and answers how far a container has to rise, feeding the rise already applied
      back into the next measurement so moving between fields cannot drop the
      container back to rest. Both rects come from one coordinate space, so nothing
      here needs pixel ratios or a status-bar height — which is the part lynx-ui
      pays for with an androidStatusBarPlusBottomBarHeight prop.
    • <KeyboardAvoiding> is that wired to a style binding, with
      behavior='translate' (rise by the measured overlap) or 'padding' (reserve
      the keyboard's height for a scroller). It writes only the declaration it owns
      and imposes no layout of its own, and it honours reducedMotion() the way
      RouteStack does.

    /keyboard is opt-in and its own module graph, reaching sideways into exactly
    one file — elements/invoke.ts, for the rect — which import-boundary.test.ts
    now pins as an exact list.

    The playground gains a /keyboard screen, a visualViewport emitter, and
    __InvokeUIMethod on its DOM Element PAPI (boundingClientRect and
    scrollIntoView answered honestly, everything else reported as unimplemented
    rather than invented).

    Not verified on a device. The arithmetic and the components are covered
    against the fake engine, but the engine event itself, the units it reports, and
    how a real IME animation interacts with the transition are unconfirmed on
    hardware.

  • ab6476a: Type route params from the pattern, and give @amritk/mini-lynx a browser
    history again.

    The pattern is now read at the type level. PathParams<'/users/:id'> is
    { id: string }, and matchRoute is generic over its pattern, so
    matchRoute('/users/:id', path) hands back { id: string } | null instead of a
    record that answers string for every key including the misspelt ones. It lives
    in @amritk/mini-helpers beside the matcher it mirrors, because the grammar has
    to be one definition or the value and type worlds drift; path-params.test.ts
    pins the two together, down to /v:major being a literal segment on both sides.

    PathParams<string> is RouteParams, so this is additive: a table annotated
    Route[], or built at runtime, compiles exactly as before and gets exactly what
    it got before.

    buildPath is the inverse. buildPath('/users/:id', { id }) cannot spell
    the pattern wrong, forget a param or be left behind when a route is renamed, and
    its values are encoded to round-trip back through matchRoute. A * wildcard
    is encoded per segment, because rest is a path and its slashes are structure.
    navigate still takes a plain string, deliberately — a router navigates to
    concrete paths, and one that matches nothing is what a fallback screen is for —
    so the check sits where the string is assembled instead.

    @amritk/mini-lynx/router gains route() and AnyRoute.
    route('/users/:id', (params) => …, meta) keeps the pattern's literal type
    alive so the view is handed a () => { id: string }. Its three arguments are
    not a style choice: folded into one object literal, the pattern and the metadata
    compete for inference against an intersection, and the metadata loses — a tab
    bar reading route.label would get unknown back.

    Route is now Route<P extends string = string>, and the router's generics are
    constrained by the new AnyRoute. Route<'/users/:id'> is deliberately not
    assignable to Route<string> — its view demands the narrow getter, and the
    widened form can only promise the flat record — so a table of mixed patterns
    needs a constraint that admits all of them. render-route.ts owns the single
    cast that reconciles the two. Existing tables keep working: Route with no type
    argument is what it always was.

    createBrowserHistory is back, on @amritk/mini-lynx/router/browser. It is
    a RouterHistory and nothing else, because the rest of routing is already
    target-free — a device build swaps createMemoryHistory() back in and changes
    nothing else. It takes a base prefix, and it stamps its depth into
    history.state rather than reading window.history.length, which counts the
    whole tab and is wrong in both directions once the user has gone back. That
    stamp is what survives the two things a memory stack never faces: a reload
    mid-stack and a forward button.

    It is the only module in the package that names window, and it is quarantined
    accordingly. src/router/browser/ is excluded from the main compiler pass and
    covered by tsconfig.dom.json, so the platform-free rule the rest of the
    package keeps stays something the compiler enforces rather than a convention.
    Hash mode is not included: configuring a host for an SPA fallback is one line,
    where a second URL grammar would be one the device build can never use.

    stripBase moved into @amritk/mini-helpers, since both routers now read a
    browser pathname and a base that meant something slightly different on each side
    is exactly the drift that package exists to prevent. @amritk/mini's router
    surface is unchanged apart from re-exporting buildPath and PathParams.

Patch Changes

  • 1b6c33d: Bring every package's shipped AI.md back in line with what that package
    actually publishes, and add bun run check:ai-docs so it cannot drift again.

    The files had gone stale in the way generated-and-committed docs always do —
    silently, and only for the audience that cannot file an issue about it.
    @amritk/mini never documented watch, template, the typed matchRoute /
    buildPath re-exports on /router, Field on /forms, or the /vite subpath
    at all; @amritk/mini-lynx was missing computed / effectScope,
    fadeTransition, keepAboveKeyboard and HANDLER_PREFIX;
    @amritk/lynx-notifications documented neither its /testing subpath nor the
    fake behind it. All four native packages exported MODULE and EVENTS with no
    mention of what they are for, and only @amritk/lynx-dialogs showed how to wire
    a fake into installNativeBridge — which is the one thing a consumer testing
    its own screens needs.

    Two accuracy fixes matter more than the additions. Every native package's
    Status section claimed the Objective-C compiles against the real Lynx pod; the
    macOS CI job was disabled on cost, so it now compiles only when somebody runs
    pod lib lint by hand, and the docs say that. And @amritk/lynx-dialogs never
    carried a Status section at all, so nothing in it told a reader that none of
    it has run on a device.

    bun run check:ai-docs reads each package's exports and fails on a runtime
    export, a published subpath, or (for a package shipping native sources) a
    Status section its AI.md never mentions. It runs early in CI, before the
    build. Exports no consumer ever writes — the tree operations the JSX transform
    calls, and the like — are listed in INTERNAL_EXPORTS with the reason.

  • d11610b: Cut main-thread work out of the paths every screen runs.

    This runtime is main-thread because the Element PAPI is, so the work it does
    building and updating a tree is not work a background thread can absorb — it is
    work the frame has to fit around. Five changes, all in the hot paths, none of
    them changing an API:

    • jsx-runtime.ts walks props with for…in instead of Object.entries, which
      was allocating an array plus a pair per prop, per element. It was the single
      hottest function in a CPU profile of building a thousand-row list.
    • apply-prop.ts remembers the event-prefix parse per prop name. Every prop
      used to pay up to six startsWith scans on its way to __SetAttribute; a
      given spelling is now scanned once for the life of the app.
    • style/to-css-name.ts and style/to-style-text.ts do the same for the CSS
      spelling of a style key and the unitless verdict on it. Both ran a regex per
      key per write, which for a reactive style bag — style={() => ({ paddingBottom: keyboardHeight() })} — meant rediscovering the same answers every frame.
    • add-event.ts no longer copies a handler set of one on every delivered event,
      and builds that set empty rather than from an iterable it had to allocate. The
      dispatcher runs on every frame of a scroll, where its garbage competes with
      the layout it is scrolling.
    • resolve-class.ts collects into one accumulator instead of
      map/filter/join, which allocated three arrays per array level and four
      per toggle map to produce one string. A reactive class is the binding an app
      re-runs most.

    Measured against an engine that does nothing but keep the tree — so the number
    is the runtime's own cost and not a host's — building a thousand rows went from
    ~9.5ms to ~7ms, about a quarter.

    Separately, testing/create-fake-engine.ts trims its call log in batches
    instead of on every call. It was doing calls.slice(-1000) per PAPI call once
    past the first thousand, so a suite that builds a thousand rows paid twenty-five
    million array writes for a log it keeps a thousand lines of — which also meant
    the package's own reconciler benchmark was mostly measuring the fake. calls()
    slices on the way out, so the window a caller sees is unchanged. End to end that
    takes create 10,000 rows in bun run bench:reconciler from ~840ms to ~170ms,
    and create 1,000 rows from ~105ms to ~20ms, with the engine-call counts
    identical.

    The core's gzipped byte budget moves 5449 → 5559 to pay for the lookup tables.
    See the note in core-size-budget.test.ts for the reasoning; it is the third
    deliberate move.

  • Updated dependencies [ab6476a]

    • @amritk/mini-helpers@0.2.0

@amritk/mini-lynx-native@0.2.0

Minor Changes

  • e025ac7: Add native modules for Lynx, starting with push notifications.

    Lynx ships no notifications module, and Sparkling's sparkling-notifications is
    a reserved npm name with no implementation behind it — so the official answer is
    still "write native code and send it into your Lynx code". These two packages
    are that, plus the piece it needs first.

    @amritk/mini-lynx-native is the wire. Lynx's NativeModules and
    GlobalEventEmitter are background-thread globals, while @amritk/mini-lynx
    renders on the main thread because the Element PAPI is a main-thread API — so a
    component reaching for a native module finds undefined, with nothing to read.
    The package carries calls one way and events the other: callNative and
    callNativeAsync for the two shapes a Lynx native method comes in,
    isNativeModuleAvailable for feature detection, and onNativeEvent for
    sendGlobalEvent. Calls made before the background half is installed are queued
    rather than lost, because the main-thread chunk usually runs first. The
    /background subpath is the half an app installs in its background chunk, in
    one line; /testing ships the fakes both halves run against.

    @amritk/lynx-notifications is the first module built on it: local and
    remote push, with an Android implementation (NotificationManager,
    AlarmManager, FCM) and an iOS one (UNUserNotificationCenter, APNs) declared
    to Lynx's autolinker through lynx.lib.json. The JavaScript surface is promises
    and subscriptions rather than signals, deliberately — a second edge onto the
    signal engine is how a consumer ends up with two reactive graphs that cannot see
    each other's writes.

    Both native halves are compiled in CI — Gradle against the real
    org.lynxsdk.lynx:lynx AAR for Android, pod lib lint against the real Lynx pod
    on a macOS runner for iOS — and src/native-contract.test.ts pins their method
    names, arities and event strings against the TypeScript, which is the one class
    of drift no compiler on either side can catch. bun run check:android runs the
    Android compile locally and skips with an explanation when there is no SDK.

    None of that has run on a device. Permission flows, AlarmManager under
    Doze, APNs registration and FCM delivery are unverified. The caveat is carried in
    the package's README.md, AI.md and AGENTS.md.

Patch Changes

  • 5101aa7: Correct the two mis-call failure modes in AI.md, and pin them with tests.

    The gotchas list had them the wrong way round: it said callNative on a
    callback method never settles and callNativeAsync on a returning one gives
    you undefined. The bridge does the opposite, and it is not a close call —
    the return form always replies, so callNative always settles, while the
    callback form appends a callback that a returning method ignores, so nothing
    ever replies at all.

    // settles: with `undefined` for a method that stores the callback, or as a
    // rejection when the method reaches for the argument it was not handed
    await callNative("StorageModule", "loadValue", "profile");
    
    // never settles: the appended callback is an argument the method ignores
    await callNativeAsync("StorageModule", "getValue", "token");

    Both are now cases in channel.test.ts, because the symmetric-sounding summary
    is the half that sends people looking in the wrong place — a promise that never
    settles reads as a thread problem, and it is a one-word choice at the call site.

    Found by wiring the package into apps/playground-mini-lynx, which now has a
    screen per native package driving them through each package's own published
    fake.

@amritk/playground-mini@0.0.2

Patch Changes

  • Updated dependencies [1b6c33d]
  • Updated dependencies [ab6476a]
    • @amritk/mini@0.7.0

@amritk/playground-mini-lynx@0.0.4

Patch Changes

  • Updated dependencies [1b6c33d]
  • Updated dependencies [293234c]
  • Updated dependencies [59983c4]
  • Updated dependencies [7c690ed]
  • Updated dependencies [2b07688]
  • Updated dependencies [e025ac7]
  • Updated dependencies [4423822]
  • Updated dependencies [5101aa7]
  • Updated dependencies [d11610b]
  • Updated dependencies [c657444]
  • Updated dependencies [ab6476a]
    • @amritk/lynx-deep-linking@0.2.0
    • @amritk/lynx-notifications@0.2.0
    • @amritk/lynx-dialogs@0.2.0
    • @amritk/lynx-location@0.2.0
    • @amritk/mini-lynx@0.4.0
    • @amritk/mini-lynx-native@0.2.0

@github-actions
github-actions Bot force-pushed the changeset-release/main branch 9 times, most recently from 58fa38c to b55ecbf Compare August 5, 2026 04:05
@github-actions
github-actions Bot force-pushed the changeset-release/main branch from b55ecbf to d63c718 Compare August 5, 2026 04:23
@amritk
amritk merged commit 9c6b6bc into main Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant