Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/tidy-pots-smell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
---

Make both playgrounds work on a phone. `playground-mini-lynx`'s page element now
takes the screen's height in the browser preview, so the app scrolls instead of
running off the bottom of its frame, and rows wrap rather than clipping their
controls at the edge. `playground-mini` is now written small-screen first: the
sidebar becomes a drawer behind a top bar below 760px, readouts scroll instead
of losing their alignment to wrapping, and controls take a touch-sized target.

Playgrounds only — no published package changed.
7 changes: 7 additions & 0 deletions apps/playground-mini-lynx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ ship to a device.
`prefers-color-scheme`. The platform's colour scheme arrives as data and the app
decides what to do with it, which is what the header switch stands in for.

**Layouts fit rather than respond.** The corollary of no `@media` is that there
is no breakpoint to fall back on: a screen either works at whatever width the
device hands it or it does not. So `<Row>` wraps by default and the controls in
one may shrink — a run that does not fit continues on the next line instead of
being clipped at the edge, which is what Lynx does to it otherwise, taking the
tap targets with it.

## Deploying

An assets-only Worker: no `main`, so Cloudflare serves `dist/` from its edge
Expand Down
9 changes: 7 additions & 2 deletions apps/playground-mini-lynx/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,18 @@
}

/* A device-shaped viewport on a desktop, and the whole window on a phone
— the app inside is identical either way. */
— the app inside is identical either way.

The height stops short of the bottom inset because `viewport-fit=cover`
above means the page extends under the home indicator: without it the
tab bar is the thing that ends up under there. `env()` is 0 wherever
there is no inset, which is every other case. */
#app {
display: flex;
flex-direction: column;
width: 100%;
max-width: 480px;
height: 100dvh;
height: calc(100dvh - env(safe-area-inset-bottom));
margin: 0 auto;
overflow: hidden;
}
Expand Down
8 changes: 7 additions & 1 deletion apps/playground-mini-lynx/src/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,19 @@ export const Chip = (props: ChipProps): LynxElement => {

export type RowProps = {
gap?: 'xs' | 'sm' | 'md'
/**
* Whether a run too wide for the screen continues on the next line. Defaults
* to `true`, because the alternative is not "a tidy single line" — it is a
* row clipped at the edge of a phone, with the buttons past the fold
* unreachable. Pass `false` for a run that genuinely is one line.
*/
wrap?: boolean
children?: ContainerChildren
}

/** A horizontal run of children. Lynx lays out in a column by default. */
export const Row = (props: RowProps): LynxElement => (
<view class={['row', `gap-${props.gap ?? 'sm'}`, props.wrap === true && 'wrap']}>{props.children}</view>
<view class={['row', `gap-${props.gap ?? 'sm'}`, props.wrap !== false && 'wrap']}>{props.children}</view>
)

export type StackProps = {
Expand Down
17 changes: 17 additions & 0 deletions apps/playground-mini-lynx/src/lib/install-lynx-reset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,23 @@ ${SCOPE} :where(*) {
text-align: start;
}

/* On a device the page element IS the screen: it is sized by the viewport, and
a tree taller than it scrolls inside a scroll-view rather than making the
page itself grow. In a browser it is an ordinary element that sizes to its
content, so the height has to be handed back — without this the app runs
past the bottom of its host box, every scroll-view inside it is unbounded,
and nothing scrolls anywhere.

Both declarations are here on purpose, for the two shapes a host box takes:
\`height\` covers a block container with a definite height, and the flex
pair covers a flex container — which is what \`index.html\` uses, and where
the reset's own \`flex-shrink: 0\` would otherwise let the page overflow. */
${SCOPE} {
height: 100%;
flex: 1 1 0%;
align-self: stretch;
}

/* Inside a <text>, Lynx does keep the cascade — an inline run and a nested
<text> both take their colour and font from the text around them. Those
three tags plus a nested <text> are the whole of it; every other element is
Expand Down
4 changes: 2 additions & 2 deletions apps/playground-mini-lynx/src/screens/data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const Basics = (): LynxElement => {
title="createQuery"
blurb="`nobody` always fails and retries once, `empty` resolves to zero rows, and the other two stay cached for five seconds — switch back and forth to watch a warm key render with no spinner at all."
>
<Row gap="xs" wrap>
<Row gap="xs">
<For each={SCOPES}>
{(name) => (
<Action onTap={() => scope(name)} disabled={() => scope() === name}>
Expand Down Expand Up @@ -179,7 +179,7 @@ const Invalidation = (): LynxElement => (
title="Invalidation"
blurb="createQuery returns signals and nothing else. The QueryClient stays yours, so invalidating, prefetching and writing optimistically are plain query-core calls rather than a second API this package would have to keep in step with it."
>
<Row gap="sm" wrap>
<Row gap="sm">
<Action onTap={() => void client.invalidateQueries({ queryKey: ['packages'] })}>invalidate every scope</Action>
<Action onTap={() => client.clear()}>clear the cache</Action>
</Row>
Expand Down
10 changes: 5 additions & 5 deletions apps/playground-mini-lynx/src/screens/elements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ const ImagePanel = (): LynxElement => {
</view>
</Row>

<Row gap="xs" wrap={true}>
<Row gap="xs">
<Chip tone="good">mode is real here</Chip>
<Chip tone="bad">blur-radius is not</Chip>
<Chip>auto-size is the browser's own intrinsic sizing, not the engine's</Chip>
Expand Down Expand Up @@ -235,7 +235,7 @@ const ScrollPanel = (): LynxElement => {
))}
</scroll-view>

<Row gap="xs" wrap={true}>
<Row gap="xs">
<Chip tone="good">scroll fires, with the engine's field names</Chip>
<Chip tone="bad">scrolltoupper / scrolltolower are device-only</Chip>
</Row>
Expand Down Expand Up @@ -327,7 +327,7 @@ const ScrollCoordinatorPanel = (): LynxElement => {
<TextLine class="card-meta">the draggable slot variant</TextLine>
</scroll-coordinator-slot-drag>

<Row gap="xs" wrap={true}>
<Row gap="xs">
<Chip tone="good">the tree, the nesting and the attribute names</Chip>
<Chip tone="bad">no coordination — three stacked boxes here</Chip>
</Row>
Expand Down Expand Up @@ -356,7 +356,7 @@ const FramePanel = (): LynxElement => (
style={{ width: '100%', height: 96, borderRadius: 8, backgroundColor: '#eef1f8' }}
/>

<Row gap="xs" wrap={true}>
<Row gap="xs">
<Chip tone="bad">nothing renders here</Chip>
<Chip>Android and iOS only, and there is no second bundle to load</Chip>
</Row>
Expand Down Expand Up @@ -386,7 +386,7 @@ type GalleryItemProps = {

const GalleryItem = (props: GalleryItemProps): LynxElement => (
<view class="card">
<Row gap="xs" wrap={true}>
<Row gap="xs">
<TextLine>{props.title}</TextLine>
<Chip tone={props.tone}>{props.note}</Chip>
</Row>
Expand Down
8 changes: 4 additions & 4 deletions apps/playground-mini-lynx/src/screens/engine.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ const TheTree = (): LynxElement => {
>
<Readout>{tree}</Readout>

<Row gap="sm" wrap>
<Row gap="sm">
<Action onTap={() => act(() => name(name() === 'Ada Lovelace' ? 'Grace Hopper' : 'Ada Lovelace'))}>
rename
</Action>
Expand Down Expand Up @@ -209,7 +209,7 @@ const CallLog = (): LynxElement => {
title="Every call the runtime makes"
blurb="`list` is the only reconciler in the package, it is keyed, and it is move-minimal. Moving the last row to the front is the interesting button: a moved row costs one __RemoveElement and one __InsertElementBefore, because an insert detaches first and therefore doubles as a move, and nothing is rebuilt."
>
<Row gap="sm" wrap>
<Row gap="sm">
<Action
onTap={() =>
act(() => {
Expand Down Expand Up @@ -413,7 +413,7 @@ const Flushing = (): LynxElement => {
title="A tick costs exactly one commit"
blurb="Nothing appears on a Lynx screen until __FlushElementTree runs, and flushing is not free — so however many attributes a tick wrote, the runtime asks the engine to commit once. Mutate twenty-five times and watch the flush count stay where it is."
>
<Row gap="sm" wrap>
<Row gap="sm">
<Action onTap={() => mutate(1)}>mutate ×1</Action>
<Action onTap={() => mutate(25)}>mutate ×25</Action>
<Action onTap={commit} disabled={() => writes() === 0}>
Expand Down Expand Up @@ -500,7 +500,7 @@ const Events = (): LynxElement => {
title="Events, end to end"
blurb="Press a button and the engine dispatches at its own element, exactly as a device would — through the global runWorklet rather than by reaching for the closure. The payload is the engine's shape, so a handler reading event.detail.x is reading the real thing."
>
<Row gap="sm" wrap>
<Row gap="sm">
<Action onTap={() => fire('tap', { detail: { x: 24, y: 91 } })}>engine: tap</Action>
<Action onTap={() => fire('tap', { detail: { x: 180, y: 12 } })}>engine: tap elsewhere</Action>
<Action onTap={() => fire('longpress', { detail: { x: 24, y: 91 } })}>engine: longpress</Action>
Expand Down
2 changes: 1 addition & 1 deletion apps/playground-mini-lynx/src/screens/flow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ const SwitchDemo = (): LynxElement => {
title="Switch and Match"
blurb="Branches are tested in source order and only the winner is ever built, so the three you are not showing create no elements and no bindings at all. On a device that is three view trees not crossing into the platform."
>
<Row gap="xs" wrap={true}>
<Row gap="xs">
<For each={STATES}>
{(name) => (
<Action onTap={() => state(name)} disabled={() => state() === name}>
Expand Down
2 changes: 1 addition & 1 deletion apps/playground-mini-lynx/src/screens/forms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ const Gating = (): LynxElement => {
>
<Field form={form} name="email" label="Email" type="email" placeholder="[email protected]" {...FIELD_STYLING} />

<Row gap="sm" wrap>
<Row gap="sm">
<Action onTap={() => field.setTouched()} disabled={field.touched}>
mark it touched
</Action>
Expand Down
8 changes: 4 additions & 4 deletions apps/playground-mini-lynx/src/screens/list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const SinglePanel = (): LynxElement => {
)}
</For>

<Row gap="sm" wrap={true}>
<Row gap="sm">
<Action onTap={() => rows([...rows(), ...makeRows(rows().length, 50)])}>append 50</Action>
<Action onTap={() => rows(INITIAL_ROWS)}>reset</Action>
<Chip>{() => `${rows().length} rows`}</Chip>
Expand Down Expand Up @@ -174,7 +174,7 @@ const LayoutPanel = (): LynxElement => {
)}
</For>

<Row gap="sm" wrap={true}>
<Row gap="sm">
<Action onTap={() => step(step() + 1)}>next layout</Action>
<Chip>{() => `list-type="${layout()}" span-count={2}`}</Chip>
</Row>
Expand Down Expand Up @@ -330,7 +330,7 @@ const RowGroupingPanel = (): LynxElement => {
)}
</list>

<Row gap="xs" wrap={true}>
<Row gap="xs">
<Chip tone="good">the nesting the engine accepts</Chip>
<Chip tone="bad">no grouping, no columns, no measurement</Chip>
</Row>
Expand Down Expand Up @@ -374,7 +374,7 @@ const HonestyPanel = (): LynxElement => (
estimate for.
</Prose>

<Row gap="xs" wrap={true}>
<Row gap="xs">
<Chip tone="good">the tree and the attribute names are real</Chip>
<Chip tone="good">keyed reconciliation is real</Chip>
<Chip tone="bad">every behaviour above is not</Chip>
Expand Down
14 changes: 7 additions & 7 deletions apps/playground-mini-lynx/src/screens/routing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const LiveState = (): LynxElement => (
title="route()"
blurb="One signal carrying the path, the raw search string, the parsed query, the captured params, and the matched route definition — or null, which is your not-found screen. Read it inside a binding and everything downstream follows navigation."
>
<Row gap="xs" wrap={true}>
<Row gap="xs">
<Link to="/routing?tab=state&amp;page=2">?tab=state&amp;page=2</Link>
<Link to="/routing/amritk">/routing/amritk</Link>
<Link to="/routing">/routing</Link>
Expand Down Expand Up @@ -104,7 +104,7 @@ const Params = (props: RoutingScreenProps & { build: number }): LynxElement => {
title="Params, and what survives"
blurb="RouteView swaps on the matched ROUTE, not on every change to what it read. Move between the two owners below and the screen is kept while params() reports new values; go back to the bare /routing and the subtree is torn down, because that is a different route definition."
>
<Row gap="xs" wrap={true}>
<Row gap="xs">
<Link to="/routing/amritk">amritk</Link>
<Link to="/routing/acme">acme</Link>
<Link to="/routing/lynx">lynx</Link>
Expand Down Expand Up @@ -149,7 +149,7 @@ const Navigation = (): LynxElement => {
title="navigate · replace · back · canGoBack"
blurb="canGoBack is a signal, so a back control can be shown only where it would do something. It counts the entries THIS router pushed, which is the only question a back chevron actually asks."
>
<Row gap="sm" wrap={true}>
<Row gap="sm">
<Action onTap={() => router.navigate('/routing/amritk')}>navigate</Action>
<Action onTap={() => router.navigate('/routing/acme', { replace: true })}>replace</Action>
<Action onTap={() => router.back()} disabled={() => !router.canGoBack()}>
Expand Down Expand Up @@ -177,7 +177,7 @@ const Query = (): LynxElement => (
title="parseQuery"
blurb="The search string is parsed once per navigation and handed over as a flat record, so a screen reads route().query.page rather than doing its own string surgery."
>
<Row gap="xs" wrap={true}>
<Row gap="xs">
<Link to="/routing?page=2&amp;sort=downloads">page and sort</Link>
<Link to="/routing?q=hello+world">a plus-encoded space</Link>
<Link to="/routing?flag">a key with no value</Link>
Expand Down Expand Up @@ -216,7 +216,7 @@ const Matching = (): LynxElement => {
title="matchRoute"
blurb="String arithmetic with no platform in it. A colon captures one segment into params, a trailing star captures the rest into params.rest, and everything else must match literally. Leading and trailing slashes are normalised, and captured segments are decoded."
>
<Row gap="xs" wrap={true}>
<Row gap="xs">
<For each={SAMPLES}>
{(sample) => (
<Action onTap={() => path(sample)} disabled={() => path() === sample}>
Expand Down Expand Up @@ -266,7 +266,7 @@ const SecondRouter = (): LynxElement => {
title="createMemoryHistory"
blurb="A second, completely independent router in the same app. The two cannot interfere with each other because the history is a parameter rather than an assumption — which is also what lets an app embedded in a native shell hand that shell's navigation in behind the same interface."
>
<Row gap="xs" wrap={true}>
<Row gap="xs">
<Action onTap={() => local.navigate('/')}>/</Action>
<Action onTap={() => local.navigate('/settings')}>/settings</Action>
<Action onTap={() => local.navigate('/users/42')}>/users/42</Action>
Expand Down Expand Up @@ -359,7 +359,7 @@ const Stack = (): LynxElement => {
title="RouteStack"
blurb="Push twice and go back: the screen underneath was never torn down, so it comes back with whatever state it had. A single-slot RouteView would have rebuilt it."
>
<Row gap="sm" wrap>
<Row gap="sm">
<Action onTap={() => stack.navigate('/one')}>push /one</Action>
<Action onTap={() => stack.navigate('/two')}>push /two</Action>
<Action onTap={() => stack.navigate('/two', { replace: true })}>replace /two</Action>
Expand Down
10 changes: 5 additions & 5 deletions apps/playground-mini-lynx/src/screens/styling.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const ChannelsPanel = (): LynxElement => {
title="two channels, two jobs"
blurb="A class is resolved once by the engine; an inline declaration is written on every change. So classes carry what is static and style carries what is genuinely dynamic."
>
<Row gap="sm" wrap={true}>
<Row gap="sm">
<view class="card">
<TextLine class="card-meta">class="card" — one __SetClasses call, ever</TextLine>
</view>
Expand Down Expand Up @@ -77,7 +77,7 @@ const UnitsPanel = (): LynxElement => {
title="there is no implicit unit"
blurb="In Lynx's CSS a bare number is not a length. width: 100 is invalid and silently does nothing; 0 is the only number a length takes bare."
>
<Row gap="sm" wrap={true}>
<Row gap="sm">
{/*
* Three spellings of one declaration set, side by side and identical on
* screen. The bag is what an app writes; the getter is the same bag
Expand All @@ -90,7 +90,7 @@ const UnitsPanel = (): LynxElement => {
<view style="width:72px;height:44px;border-radius:8px;background-color:#2f5bd7" />
</Row>

<Row gap="sm" wrap={true}>
<Row gap="sm">
<Action onTap={() => step(step() + 1)}>{() => `explain the ${form()}`}</Action>
<Chip>{() => FORM_NOTES[form()]}</Chip>
</Row>
Expand Down Expand Up @@ -195,7 +195,7 @@ const MotionPanel = (): LynxElement => {
<Readout>{'@keyframes pulse { from { opacity: 0.4 } to { opacity: 1 } }'}</Readout>
<Readout>{'.pulsing { animation: pulse 1.2s ease-in-out infinite alternate }'}</Readout>

<Row gap="xs" wrap={true}>
<Row gap="xs">
<Chip tone="good">the transition is real in both targets</Chip>
<Chip tone="bad">the keyframes above are text, not a running animation</Chip>
</Row>
Expand Down Expand Up @@ -245,7 +245,7 @@ const LinearPanel = (): LynxElement => (
<view style={{ flexGrow: 1, backgroundColor: '#dde2ee', borderRadius: 6 }} />
</view>

<Row gap="xs" wrap={true}>
<Row gap="xs">
<Chip tone="bad">the first row is approximated with flex here</Chip>
<Chip tone="good">the second is the real thing</Chip>
</Row>
Expand Down
8 changes: 4 additions & 4 deletions apps/playground-mini-lynx/src/screens/text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const MaxlinePanel = (): LynxElement => {

<Readout>{layout}</Readout>

<Row gap="xs" wrap={true}>
<Row gap="xs">
<Chip tone="good">the clamp is approximated</Chip>
<Chip tone="bad">the layout event is device-only</Chip>
</Row>
Expand Down Expand Up @@ -281,7 +281,7 @@ const MarkdownPanel = (): LynxElement => {
binddrawEnd={() => parsed(`drew ${content().length} characters`)}
/>

<Row gap="xs" wrap={true}>
<Row gap="xs">
<Action
onTap={() => {
step(step() >= PARTS.length ? 1 : step() + 1)
Expand All @@ -297,7 +297,7 @@ const MarkdownPanel = (): LynxElement => {

<Readout>{parsed}</Readout>

<Row gap="xs" wrap={true}>
<Row gap="xs">
<Chip tone="good">the attribute names and the reactive content</Chip>
<Chip tone="bad">no parser here — the raw string is what you see</Chip>
</Row>
Expand Down Expand Up @@ -342,7 +342,7 @@ const InheritancePanel = (): LynxElement => (
</text>
</text>

<Row gap="xs" wrap={true}>
<Row gap="xs">
<Chip tone="good">the preview reproduces this one</Chip>
<Chip>because the reset re-asserts font and colour on every element</Chip>
</Row>
Expand Down
Loading
Loading