Version Packages - #23
Merged
Merged
Conversation
github-actions
Bot
force-pushed
the
changeset-release/main
branch
9 times, most recently
from
August 5, 2026 04:05
58fa38c to
b55ecbf
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
August 5, 2026 04:23
b55ecbf to
d63c718
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 nativemodule, 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-linkingneeds@sigx/lynx-coreand a
sigx prebuildstep,@tamer4lynx/tamer-linkingpeers on ReactLynx andlinks through the
t4lCLI, and neither reaches the main thread amini-lynxtree renders on.
Inbound, the launch URL is a value (
getInitialURL) and every later link is anevent (
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,canOpenURLandopenSettings— the last being the missing half ofthe
deniedstates@amritk/lynx-locationand@amritk/lynx-notificationsreport.
parseURLandcreateURLare 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.mdback in line with what that packageactually publishes, and add
bun run check:ai-docsso 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/mininever documentedwatch,template, the typedmatchRoute/buildPathre-exports on/router,Fieldon/forms, or the/vitesubpathat all;
@amritk/mini-lynxwas missingcomputed/effectScope,fadeTransition,keepAboveKeyboardandHANDLER_PREFIX;@amritk/lynx-notificationsdocumented neither its/testingsubpath nor thefake behind it. All four native packages exported
MODULEandEVENTSwith nomention of what they are for, and only
@amritk/lynx-dialogsshowed how to wirea fake into
installNativeBridge— which is the one thing a consumer testingits 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 lintby hand, and the docs say that. And@amritk/lynx-dialogsnevercarried 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-docsreads each package'sexportsand fails on a runtimeexport, a published subpath, or (for a package shipping native sources) a
Status section its
AI.mdnever mentions. It runs early in CI, before thebuild. Exports no consumer ever writes — the tree operations the JSX transform
calls, and the like — are listed in
INTERNAL_EXPORTSwith the reason.Updated dependencies [e025ac7]
Updated dependencies [5101aa7]
Updated dependencies [ab6476a]
@amritk/lynx-dialogs@0.2.0
Minor Changes
2b07688: Add
@amritk/lynx-dialogs— the platform's own date picker, action sheet andalert for Lynx.
Lynx ships no picker element and no modal module, and neither published option
fills the gap:
@lynx-js/lynx-ui-sheetdraws a sheet out of ReactLynx elementsrather than presenting a native one, and
@sigx/lynx-datetime-pickeris a realUIDatePicker/DatePickerDialogbolted to another framework's bridge. Nothingnative 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:presentDatePickercoversdate,timeanddatetimewith bounds, labels anda 12/24-hour override;
presentActionSheetcovers destructive and disabled rowsand the iPad popover anchor;
presentAlertcovers one to three buttons withcancel and destructive styling.
dismissActiveDialogcloses whatever is up so ascreen can clean up on navigation, and
areDialogsAvailablereports whether thehost app linked the module.
AlertButtonsis a one-to-three tuple rather than an array, becauseAlertDialoghas exactly three button slots and there is no fourth — so theAndroid 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 hangingforever on iOS.
Unlike the other two native packages there is no permission, no
Info.plistkeyand no manifest entry for the host app to add, and the Android half takes no
dependencies at all — the framework
AlertDialograther than Material, so noTheme.Material3requirement lands on the host's Activity.@amritk/lynx-dialogs/testingships the native contract as an executable fake.Patch Changes
1b6c33d: Bring every package's shipped
AI.mdback in line with what that packageactually publishes, and add
bun run check:ai-docsso 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/mininever documentedwatch,template, the typedmatchRoute/buildPathre-exports on/router,Fieldon/forms, or the/vitesubpathat all;
@amritk/mini-lynxwas missingcomputed/effectScope,fadeTransition,keepAboveKeyboardandHANDLER_PREFIX;@amritk/lynx-notificationsdocumented neither its/testingsubpath nor thefake behind it. All four native packages exported
MODULEandEVENTSwith nomention of what they are for, and only
@amritk/lynx-dialogsshowed how to wirea fake into
installNativeBridge— which is the one thing a consumer testingits 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 lintby hand, and the docs say that. And@amritk/lynx-dialogsnevercarried 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-docsreads each package'sexportsand fails on a runtimeexport, a published subpath, or (for a package shipping native sources) a
Status section its
AI.mdnever mentions. It runs early in CI, before thebuild. Exports no consumer ever writes — the tree operations the JSX transform
calls, and the like — are listed in
INTERNAL_EXPORTSwith the reason.Updated dependencies [e025ac7]
Updated dependencies [5101aa7]
@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) callsNativeModulesdirectly from its JavaScript half,which is background-thread only — so a main-thread
@amritk/mini-lynxcomponentimporting it gets
undefined. This is an Android module, an iOS module, and apromise-shaped facade over
@amritk/mini-lynx-native, built from the shape@amritk/lynx-notificationsestablished.getPermissionStatus,requestPermission,getCurrentPosition,getLastKnownPosition,isLocationEnabled,isLocationAvailableandwatchPosition.CLLocationManageron iOS,LocationManageron Android — notthe fused provider, so no Play Services dependency and no non-GMS devices
excluded. Foreground only, deliberately: no
ACCESS_BACKGROUND_LOCATIONand noAlwaysauthorisation.Failures are values rather than rejections —
{ ok: true, position }or{ ok: false, error, message }— because Lynx has no error convention forbridge callbacks and a missing permission is an ordinary UI branch.
@amritk/lynx-location/testingships the native contract as an executable fake.Nothing here has run on a device; see the package's
AGENTS.mdfor what thethree CI checks do and do not cover.
7c690ed: Add
reverseGeocodeto@amritk/lynx-location— coordinates to a postaladdress, on both platforms.
CLGeocoderon iOS,android.location.Geocoderon Android. The Android halfuses 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
LocationFixsatisfies the newCoordinatestype, so pairing it withgetCurrentPositionneeds no mapping step.GeocodeResultis a discriminated union like the rest of the package:{ ok: true, addresses }or{ ok: false, error, message }, whereerrorisinvalidCoordinates,notFound,networkorunavailable. Being throttled isnetworkrather than a code of its own — Apple rate-limitsCLGeocoderandreports it that way, and Android has no equivalent to report.
notFoundis theonly spelling of "there is no address there"; an empty
addressesis never asuccess.
Every
GeocodeAddressfield is nullable, andformattedAddressis built by theOS —
getAddressLine(0)on Android,CNPostalAddressFormatteron iOS — so itplaces each country's postcode where that country places it.
isoCountryCodeisthe only field stable across locales.
The iOS half now links
Contacts, forCNPostalAddressFormatteralone. Itreaches no contact store and needs no permission.
createFakeLocationgainssetNextAddresses,setGeocoderPresent,setNetworkAvailableandgeocodes(). Nothing here has run on a device; thepackage's
AGENTS.mdlists what reverse geocoding specifically leaves unproven.Patch Changes
1b6c33d: Bring every package's shipped
AI.mdback in line with what that packageactually publishes, and add
bun run check:ai-docsso 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/mininever documentedwatch,template, the typedmatchRoute/buildPathre-exports on/router,Fieldon/forms, or the/vitesubpathat all;
@amritk/mini-lynxwas missingcomputed/effectScope,fadeTransition,keepAboveKeyboardandHANDLER_PREFIX;@amritk/lynx-notificationsdocumented neither its/testingsubpath nor thefake behind it. All four native packages exported
MODULEandEVENTSwith nomention of what they are for, and only
@amritk/lynx-dialogsshowed how to wirea fake into
installNativeBridge— which is the one thing a consumer testingits 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 lintby hand, and the docs say that. And@amritk/lynx-dialogsnevercarried 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-docsreads each package'sexportsand fails on a runtimeexport, a published subpath, or (for a package shipping native sources) a
Status section its
AI.mdnever mentions. It runs early in CI, before thebuild. Exports no consumer ever writes — the tree operations the JSX transform
calls, and the like — are listed in
INTERNAL_EXPORTSwith the reason.Updated dependencies [e025ac7]
Updated dependencies [5101aa7]
@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-notificationsisa 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-nativeis the wire. Lynx'sNativeModulesandGlobalEventEmitterare background-thread globals, while@amritk/mini-lynxrenders 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:
callNativeandcallNativeAsyncfor the two shapes a Lynx native method comes in,isNativeModuleAvailablefor feature detection, andonNativeEventforsendGlobalEvent. Calls made before the background half is installed are queuedrather than lost, because the main-thread chunk usually runs first. The
/backgroundsubpath is the half an app installs in its background chunk, inone line;
/testingships the fakes both halves run against.@amritk/lynx-notificationsis the first module built on it: local andremote push, with an Android implementation (
NotificationManager,AlarmManager, FCM) and an iOS one (UNUserNotificationCenter, APNs) declaredto Lynx's autolinker through
lynx.lib.json. The JavaScript surface is promisesand 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:lynxAAR for Android,pod lib lintagainst the real Lynx podon a macOS runner for iOS — and
src/native-contract.test.tspins their methodnames, arities and event strings against the TypeScript, which is the one class
of drift no compiler on either side can catch.
bun run check:androidruns theAndroid compile locally and skips with an explanation when there is no SDK.
None of that has run on a device. Permission flows,
AlarmManagerunderDoze, APNs registration and FCM delivery are unverified. The caveat is carried in
the package's
README.md,AI.mdandAGENTS.md.Patch Changes
1b6c33d: Bring every package's shipped
AI.mdback in line with what that packageactually publishes, and add
bun run check:ai-docsso 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/mininever documentedwatch,template, the typedmatchRoute/buildPathre-exports on/router,Fieldon/forms, or the/vitesubpathat all;
@amritk/mini-lynxwas missingcomputed/effectScope,fadeTransition,keepAboveKeyboardandHANDLER_PREFIX;@amritk/lynx-notificationsdocumented neither its/testingsubpath nor thefake behind it. All four native packages exported
MODULEandEVENTSwith nomention of what they are for, and only
@amritk/lynx-dialogsshowed how to wirea fake into
installNativeBridge— which is the one thing a consumer testingits 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 lintby hand, and the docs say that. And@amritk/lynx-dialogsnevercarried 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-docsreads each package'sexportsand fails on a runtimeexport, a published subpath, or (for a package shipping native sources) a
Status section its
AI.mdnever mentions. It runs early in CI, before thebuild. Exports no consumer ever writes — the tree operations the JSX transform
calls, and the like — are listed in
INTERNAL_EXPORTSwith the reason.4423822: Backport the dangling-selector check from
@amritk/lynx-location's paritysuite:
native-contract.test.tsnow asserts that every selector named in theObjective-C
methodLookuptable 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 lintpasses — 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@0.7.0
Minor Changes
ab6476a: Type route params from the pattern, and give
@amritk/mini-lynxa browserhistory again.
The pattern is now read at the type level.
PathParams<'/users/:id'>is{ id: string }, andmatchRouteis generic over its pattern, somatchRoute('/users/:id', path)hands back{ id: string } | nullinstead of arecord that answers
stringfor every key including the misspelt ones. It livesin
@amritk/mini-helpersbeside the matcher it mirrors, because the grammar hasto be one definition or the value and type worlds drift;
path-params.test.tspins the two together, down to
/v:majorbeing a literal segment on both sides.PathParams<string>isRouteParams, so this is additive: a table annotatedRoute[], or built at runtime, compiles exactly as before and gets exactly whatit got before.
buildPathis the inverse.buildPath('/users/:id', { id })cannot spellthe 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*wildcardis encoded per segment, because
restis a path and its slashes are structure.navigatestill takes a plain string, deliberately — a router navigates toconcrete 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/routergainsroute()andAnyRoute.route('/users/:id', (params) => …, meta)keeps the pattern's literal typealive so the view is handed a
() => { id: string }. Its three arguments arenot 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.labelwould getunknownback.Routeis nowRoute<P extends string = string>, and the router's generics areconstrained by the new
AnyRoute.Route<'/users/:id'>is deliberately notassignable to
Route<string>— itsviewdemands the narrow getter, and thewidened form can only promise the flat record — so a table of mixed patterns
needs a constraint that admits all of them.
render-route.tsowns the singlecast that reconciles the two. Existing tables keep working:
Routewith no typeargument is what it always was.
createBrowserHistoryis back, on@amritk/mini-lynx/router/browser. It isa
RouterHistoryand nothing else, because the rest of routing is alreadytarget-free — a device build swaps
createMemoryHistory()back in and changesnothing else. It takes a
baseprefix, and it stamps its depth intohistory.staterather than readingwindow.history.length, which counts thewhole 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 quarantinedaccordingly.
src/router/browser/is excluded from the main compiler pass andcovered by
tsconfig.dom.json, so the platform-free rule the rest of thepackage 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.
stripBasemoved into@amritk/mini-helpers, since both routers now read abrowser pathname and a base that meant something slightly different on each side
is exactly the drift that package exists to prevent.
@amritk/mini's routersurface is unchanged apart from re-exporting
buildPathandPathParams.Patch Changes
1b6c33d: Bring every package's shipped
AI.mdback in line with what that packageactually publishes, and add
bun run check:ai-docsso 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/mininever documentedwatch,template, the typedmatchRoute/buildPathre-exports on/router,Fieldon/forms, or the/vitesubpathat all;
@amritk/mini-lynxwas missingcomputed/effectScope,fadeTransition,keepAboveKeyboardandHANDLER_PREFIX;@amritk/lynx-notificationsdocumented neither its/testingsubpath nor thefake behind it. All four native packages exported
MODULEandEVENTSwith nomention of what they are for, and only
@amritk/lynx-dialogsshowed how to wirea fake into
installNativeBridge— which is the one thing a consumer testingits 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 lintby hand, and the docs say that. And@amritk/lynx-dialogsnevercarried 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-docsreads each package'sexportsand fails on a runtimeexport, a published subpath, or (for a package shipping native sources) a
Status section its
AI.mdnever mentions. It runs early in CI, before thebuild. Exports no consumer ever writes — the tree operations the JSX transform
calls, and the like — are listed in
INTERNAL_EXPORTSwith the reason.Updated dependencies [ab6476a]
@amritk/mini-helpers@0.2.0
Minor Changes
ab6476a: Type route params from the pattern, and give
@amritk/mini-lynxa browserhistory again.
The pattern is now read at the type level.
PathParams<'/users/:id'>is{ id: string }, andmatchRouteis generic over its pattern, somatchRoute('/users/:id', path)hands back{ id: string } | nullinstead of arecord that answers
stringfor every key including the misspelt ones. It livesin
@amritk/mini-helpersbeside the matcher it mirrors, because the grammar hasto be one definition or the value and type worlds drift;
path-params.test.tspins the two together, down to
/v:majorbeing a literal segment on both sides.PathParams<string>isRouteParams, so this is additive: a table annotatedRoute[], or built at runtime, compiles exactly as before and gets exactly whatit got before.
buildPathis the inverse.buildPath('/users/:id', { id })cannot spellthe 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*wildcardis encoded per segment, because
restis a path and its slashes are structure.navigatestill takes a plain string, deliberately — a router navigates toconcrete 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/routergainsroute()andAnyRoute.route('/users/:id', (params) => …, meta)keeps the pattern's literal typealive so the view is handed a
() => { id: string }. Its three arguments arenot 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.labelwould getunknownback.Routeis nowRoute<P extends string = string>, and the router's generics areconstrained by the new
AnyRoute.Route<'/users/:id'>is deliberately notassignable to
Route<string>— itsviewdemands the narrow getter, and thewidened form can only promise the flat record — so a table of mixed patterns
needs a constraint that admits all of them.
render-route.tsowns the singlecast that reconciles the two. Existing tables keep working:
Routewith no typeargument is what it always was.
createBrowserHistoryis back, on@amritk/mini-lynx/router/browser. It isa
RouterHistoryand nothing else, because the rest of routing is alreadytarget-free — a device build swaps
createMemoryHistory()back in and changesnothing else. It takes a
baseprefix, and it stamps its depth intohistory.staterather than readingwindow.history.length, which counts thewhole 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 quarantinedaccordingly.
src/router/browser/is excluded from the main compiler pass andcovered by
tsconfig.dom.json, so the platform-free rule the rest of thepackage 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.
stripBasemoved into@amritk/mini-helpers, since both routers now read abrowser pathname and a base that meant something slightly different on each side
is exactly the drift that package exists to prevent.
@amritk/mini's routersurface is unchanged apart from re-exporting
buildPathandPathParams.@amritk/mini-lynx@0.4.0
Minor Changes
59983c4: Add
@amritk/mini-lynx/keyboard— the soft keyboard as a signal, and thelayouts that move out of its way.
Lynx does not avoid the keyboard for you.
<input>does not do it, the docs sayso outright, and what the engine offers is a single global event —
keyboardstatuschanged, carrying'on' | 'off'and a height. Every Lynx appwith 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 betweentwo adjacent fields.
The wiring is two lines and no provider:
refis this runtime's element-extension seam, so a control reports its ownfocus and the container reads it — nothing is threaded between them.
trackKeyboard(options?)subscribes to the engine'sGlobalEventEmitterand feeds
keyboardHeight(). The emitter is an option because Lynx's owncompatibility data lists
keyboardstatuschangedas unsupported on the web:a DOM build reports the keyboard from
visualViewportand passes it in, andeverything downstream is the same code.
keyboardLift({ inset, offset })ismax(0, height - inset) + offsetwhile 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 elementand 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-uipays for with an
androidStatusBarPlusBottomBarHeightprop.<KeyboardAvoiding>is that wired to a style binding, withbehavior='translate'(rise by the measured overlap) or'padding'(reservethe 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 wayRouteStackdoes./keyboardis opt-in and its own module graph, reaching sideways into exactlyone file —
elements/invoke.ts, for the rect — whichimport-boundary.test.tsnow pins as an exact list.
The playground gains a
/keyboardscreen, avisualViewportemitter, and__InvokeUIMethodon its DOM Element PAPI (boundingClientRectandscrollIntoViewanswered honestly, everything else reported as unimplementedrather 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-lynxa browserhistory again.
The pattern is now read at the type level.
PathParams<'/users/:id'>is{ id: string }, andmatchRouteis generic over its pattern, somatchRoute('/users/:id', path)hands back{ id: string } | nullinstead of arecord that answers
stringfor every key including the misspelt ones. It livesin
@amritk/mini-helpersbeside the matcher it mirrors, because the grammar hasto be one definition or the value and type worlds drift;
path-params.test.tspins the two together, down to
/v:majorbeing a literal segment on both sides.PathParams<string>isRouteParams, so this is additive: a table annotatedRoute[], or built at runtime, compiles exactly as before and gets exactly whatit got before.
buildPathis the inverse.buildPath('/users/:id', { id })cannot spellthe 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*wildcardis encoded per segment, because
restis a path and its slashes are structure.navigatestill takes a plain string, deliberately — a router navigates toconcrete 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/routergainsroute()andAnyRoute.route('/users/:id', (params) => …, meta)keeps the pattern's literal typealive so the view is handed a
() => { id: string }. Its three arguments arenot 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.labelwould getunknownback.Routeis nowRoute<P extends string = string>, and the router's generics areconstrained by the new
AnyRoute.Route<'/users/:id'>is deliberately notassignable to
Route<string>— itsviewdemands the narrow getter, and thewidened form can only promise the flat record — so a table of mixed patterns
needs a constraint that admits all of them.
render-route.tsowns the singlecast that reconciles the two. Existing tables keep working:
Routewith no typeargument is what it always was.
createBrowserHistoryis back, on@amritk/mini-lynx/router/browser. It isa
RouterHistoryand nothing else, because the rest of routing is alreadytarget-free — a device build swaps
createMemoryHistory()back in and changesnothing else. It takes a
baseprefix, and it stamps its depth intohistory.staterather than readingwindow.history.length, which counts thewhole 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 quarantinedaccordingly.
src/router/browser/is excluded from the main compiler pass andcovered by
tsconfig.dom.json, so the platform-free rule the rest of thepackage 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.
stripBasemoved into@amritk/mini-helpers, since both routers now read abrowser pathname and a base that meant something slightly different on each side
is exactly the drift that package exists to prevent.
@amritk/mini's routersurface is unchanged apart from re-exporting
buildPathandPathParams.Patch Changes
1b6c33d: Bring every package's shipped
AI.mdback in line with what that packageactually publishes, and add
bun run check:ai-docsso 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/mininever documentedwatch,template, the typedmatchRoute/buildPathre-exports on/router,Fieldon/forms, or the/vitesubpathat all;
@amritk/mini-lynxwas missingcomputed/effectScope,fadeTransition,keepAboveKeyboardandHANDLER_PREFIX;@amritk/lynx-notificationsdocumented neither its/testingsubpath nor thefake behind it. All four native packages exported
MODULEandEVENTSwith nomention of what they are for, and only
@amritk/lynx-dialogsshowed how to wirea fake into
installNativeBridge— which is the one thing a consumer testingits 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 lintby hand, and the docs say that. And@amritk/lynx-dialogsnevercarried 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-docsreads each package'sexportsand fails on a runtimeexport, a published subpath, or (for a package shipping native sources) a
Status section its
AI.mdnever mentions. It runs early in CI, before thebuild. Exports no consumer ever writes — the tree operations the JSX transform
calls, and the like — are listed in
INTERNAL_EXPORTSwith 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.tswalks props withfor…ininstead ofObject.entries, whichwas 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.tsremembers the event-prefix parse per prop name. Every propused to pay up to six
startsWithscans on its way to__SetAttribute; agiven spelling is now scanned once for the life of the app.
style/to-css-name.tsandstyle/to-style-text.tsdo the same for the CSSspelling 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.tsno 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.tscollects into one accumulator instead ofmap/filter/join, which allocated three arrays per array level and fourper toggle map to produce one string. A reactive
classis the binding an appre-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.tstrims its call log in batchesinstead of on every call. It was doing
calls.slice(-1000)per PAPI call oncepast 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 rowsinbun run bench:reconcilerfrom ~840ms to ~170ms,and
create 1,000 rowsfrom ~105ms to ~20ms, with the engine-call countsidentical.
The core's gzipped byte budget moves 5449 → 5559 to pay for the lookup tables.
See the note in
core-size-budget.test.tsfor the reasoning; it is the thirddeliberate move.
Updated dependencies [ab6476a]
@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-notificationsisa 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-nativeis the wire. Lynx'sNativeModulesandGlobalEventEmitterare background-thread globals, while@amritk/mini-lynxrenders 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:
callNativeandcallNativeAsyncfor the two shapes a Lynx native method comes in,isNativeModuleAvailablefor feature detection, andonNativeEventforsendGlobalEvent. Calls made before the background half is installed are queuedrather than lost, because the main-thread chunk usually runs first. The
/backgroundsubpath is the half an app installs in its background chunk, inone line;
/testingships the fakes both halves run against.@amritk/lynx-notificationsis the first module built on it: local andremote push, with an Android implementation (
NotificationManager,AlarmManager, FCM) and an iOS one (UNUserNotificationCenter, APNs) declaredto Lynx's autolinker through
lynx.lib.json. The JavaScript surface is promisesand 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:lynxAAR for Android,pod lib lintagainst the real Lynx podon a macOS runner for iOS — and
src/native-contract.test.tspins their methodnames, arities and event strings against the TypeScript, which is the one class
of drift no compiler on either side can catch.
bun run check:androidruns theAndroid compile locally and skips with an explanation when there is no SDK.
None of that has run on a device. Permission flows,
AlarmManagerunderDoze, APNs registration and FCM delivery are unverified. The caveat is carried in
the package's
README.md,AI.mdandAGENTS.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
callNativeon acallback method never settles and
callNativeAsyncon a returning one givesyou
undefined. The bridge does the opposite, and it is not a close call —the
returnform always replies, socallNativealways settles, while thecallbackform appends a callback that a returning method ignores, so nothingever replies at all.
Both are now cases in
channel.test.ts, because the symmetric-sounding summaryis 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 ascreen per native package driving them through each package's own published
fake.
@amritk/playground-mini@0.0.2
Patch Changes
@amritk/playground-mini-lynx@0.0.4
Patch Changes