-
Notifications
You must be signed in to change notification settings - Fork 0
fix: route-boundary suspension — router-owned pending state + compound Loader #461
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
c3c5617
feat(ui-kit-system): Loader compound with a swappable indeterminate o…
omridevk 70fc990
feat(app): route-level pending state renders the Loader (#455)
omridevk 3d8fba8
fix(app): element captures no longer blank the whole pane while loadi…
omridevk e2fe6b4
test(app): guard the route Suspense boundary against query suspension…
omridevk c17bfd0
refactor(ui-kit-system): loader styles through the uno preset, drop t…
omridevk e23f53b
refactor(uno-preset): loader visuals as readable preset rules, not br…
omridevk 3dc8ab5
fix(uno-preset): emit loader keyframes from the preflight — theme key…
omridevk e8d79d1
fix(ui-kit-system,app): review follow-ups — enforce indeterminate con…
omridevk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@conciv/ui-kit-system': patch | ||
| --- | ||
|
|
||
| New `Loader` compound (`Loader.Root/Indicator/Text/Label/Description`) built on Ark's indeterminate Progress: a conic-gradient orb whose arcs animate registered `@property` angles rather than rotating a rasterized texture, drawn entirely in `currentColor` so it inherits any surface. Sizes ride a `--pw-loader-size` variable through `data-size`, and `Loader.Indicator` renders whatever children it is given, so a different visual replaces one part instead of the component. Styled entirely through the shared `@conciv/uno-preset` (keyframes, a `data-size` rule and shortcuts), like every other component in the package — no separate stylesheet to `@import`. |
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| import './helpers/utilities.css' | ||
| import {afterEach, expect, test} from 'vitest' | ||
| import {page} from 'vitest/browser' | ||
| import {render} from '@solidjs/testing-library' | ||
| import {RouterProvider, createMemoryHistory} from '@tanstack/solid-router' | ||
| import {makeRpcClient} from '@conciv/contract' | ||
| import {parseConcivSettings} from '../src/data/settings.js' | ||
| import {createConcivRouter, disposeConcivRouter} from '../src/router.js' | ||
| import {CORE_BASE, installFakeCore, sessionRow, type FakeCore} from './helpers/fake-core.js' | ||
|
|
||
| const PANEL_SESSION = 'conciv_1' | ||
| const HELD_ROUTE_MS = 1500 | ||
| const WHILE_HELD = {timeout: 700} | ||
| const disposers: (() => void)[] = [] | ||
| let core: FakeCore | null = null | ||
|
|
||
| afterEach(() => { | ||
| for (const dispose of disposers.splice(0)) dispose() | ||
| core?.restore() | ||
| core = null | ||
| }) | ||
|
|
||
| const PANEL_ENTRY = `/panel/${PANEL_SESSION}?open=true` | ||
| const CLOSED_ENTRY = '/' | ||
|
|
||
| function mountShell(entry: string, config: Parameters<typeof installFakeCore>[0] = {}): void { | ||
| core = installFakeCore({sessions: [sessionRow({id: PANEL_SESSION})], ...config}) | ||
| const router = createConcivRouter({ | ||
| rpc: makeRpcClient(CORE_BASE), | ||
| history: createMemoryHistory({initialEntries: [entry]}), | ||
| environment: {rootNode: document, document}, | ||
| settings: parseConcivSettings(''), | ||
| }) | ||
| const mounted = render(() => <RouterProvider router={router} />) | ||
| disposers.push(() => { | ||
| mounted.unmount() | ||
| disposeConcivRouter(router) | ||
| }) | ||
| } | ||
|
|
||
| const editor = () => page.getByRole('textbox', {name: 'Message the conciv agent'}) | ||
| const launcher = () => page.getByRole('button', {name: 'Open conciv chat'}) | ||
| const routePending = () => page.getByRole('progressbar', {name: 'Loading conciv'}) | ||
|
|
||
| test('the pane paints its composer while the element captures query is still in flight', async () => { | ||
| mountShell(PANEL_ENTRY, {delays: {'/rpc/captures/list': HELD_ROUTE_MS}}) | ||
|
|
||
| await expect.element(editor(), WHILE_HELD).toBeVisible() | ||
| await expect.element(routePending(), WHILE_HELD).not.toBeInTheDocument() | ||
| }) | ||
|
|
||
| test('the shell keeps its launcher while the session list query is still in flight', async () => { | ||
| mountShell(CLOSED_ENTRY, {delays: {'/rpc/sessions/list': HELD_ROUTE_MS}}) | ||
|
|
||
| await expect.element(launcher(), WHILE_HELD).toBeVisible() | ||
| await expect.element(routePending(), WHILE_HELD).not.toBeInTheDocument() | ||
| }) | ||
|
|
||
| test('a pane whose queries all answer immediately never shows the route pending loader', async () => { | ||
| mountShell(PANEL_ENTRY) | ||
|
|
||
| await expect.element(editor(), WHILE_HELD).toBeVisible() | ||
| await core?.idle() | ||
| await expect.element(routePending(), WHILE_HELD).not.toBeInTheDocument() | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| }) | ||
|
|
||
| test('a slow beforeLoad reveals the route pending loader, then hands off to the pane', async () => { | ||
| mountShell('/panel/latest?open=true', {delays: {'/rpc/sessions/resolve': 900}}) | ||
|
|
||
| await expect.element(routePending()).toBeVisible() | ||
| await expect.element(editor(), {timeout: 2000}).toBeVisible() | ||
| await expect.element(routePending()).not.toBeInTheDocument() | ||
| }) | ||
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import {For} from 'solid-js' | ||
| import type {Meta, StoryObj} from 'storybook-solidjs-vite' | ||
| import {Loader, type LoaderSize} from './loader.js' | ||
|
|
||
| const meta: Meta = {title: 'ui-kit-system/Loader'} | ||
| export default meta | ||
| type Story = StoryObj | ||
|
|
||
| const SIZES: LoaderSize[] = ['sm', 'md', 'lg'] | ||
|
|
||
| export const Default: Story = { | ||
| render: () => ( | ||
| <Loader.Root translations={{value: () => 'Loading conciv'}}> | ||
| <Loader.Indicator /> | ||
| <Loader.Text> | ||
| <Loader.Label>Loading conciv…</Loader.Label> | ||
| <Loader.Description>Restoring your session and its transcript.</Loader.Description> | ||
| </Loader.Text> | ||
| </Loader.Root> | ||
| ), | ||
| } | ||
|
|
||
| export const Sizes: Story = { | ||
| render: () => ( | ||
| <div class="flex flex-wrap gap-4 items-start"> | ||
| <For each={SIZES}> | ||
| {(size) => ( | ||
| <Loader.Root size={size} translations={{value: () => `Loading ${size}`}}> | ||
| <Loader.Indicator /> | ||
| <Loader.Text> | ||
| <Loader.Label>Loading conciv…</Loader.Label> | ||
| <Loader.Description>Size {size}</Loader.Description> | ||
| </Loader.Text> | ||
| </Loader.Root> | ||
| )} | ||
| </For> | ||
| </div> | ||
| ), | ||
| } | ||
|
|
||
| export const TitleOnly: Story = { | ||
| render: () => ( | ||
| <Loader.Root size="sm" translations={{value: () => 'Loading conciv'}}> | ||
| <Loader.Indicator /> | ||
| <Loader.Text> | ||
| <Loader.Label>Loading conciv…</Loader.Label> | ||
| </Loader.Text> | ||
| </Loader.Root> | ||
| ), | ||
| } | ||
|
|
||
| export const OnAccent: Story = { | ||
| render: () => ( | ||
| <div class="text-pw-on-accent rounded-pw-lg bg-pw-accent"> | ||
| <Loader.Root translations={{value: () => 'Loading conciv'}}> | ||
| <Loader.Indicator /> | ||
| <Loader.Text> | ||
| <Loader.Label>Loading conciv…</Loader.Label> | ||
| <Loader.Description>The orb is drawn in currentColor, so it inherits any surface.</Loader.Description> | ||
| </Loader.Text> | ||
| </Loader.Root> | ||
| </div> | ||
| ), | ||
| } | ||
|
|
||
| export const SwappedIndicator: Story = { | ||
| render: () => ( | ||
| <Loader.Root translations={{value: () => 'Loading conciv'}}> | ||
| <Loader.Indicator class="grid place-items-center"> | ||
| <span class="border-2 border-pw-line border-t-pw-accent rounded-pw-pill size-6 anim-compact" /> | ||
| </Loader.Indicator> | ||
| <Loader.Text> | ||
| <Loader.Label>Loading conciv…</Loader.Label> | ||
| <Loader.Description>Any children replace the default orb.</Loader.Description> | ||
| </Loader.Text> | ||
| </Loader.Root> | ||
| ), | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import {Show, splitProps, type ComponentProps, type JSX} from 'solid-js' | ||
| import {Progress as Ark} from '@ark-ui/solid/progress' | ||
|
|
||
| export type LoaderSize = 'sm' | 'md' | 'lg' | ||
|
|
||
| const ROOT = 'loader-size flex flex-col items-center justify-center gap-8 p-8' | ||
| const ORB = 'loader-orb' | ||
| const TEXT = 'loader-text' | ||
| const LABEL = 'loader-label' | ||
| const DESCRIPTION = 'loader-description' | ||
|
|
||
| const DEFAULT_TRANSLATIONS = {value: () => 'Loading'} | ||
|
|
||
| function LoaderArcs(): JSX.Element { | ||
| return ( | ||
| <> | ||
| <span class="loader-arc-a loader-arc" /> | ||
| <span class="loader-arc-b loader-arc" /> | ||
| <span class="loader-arc-c loader-arc" /> | ||
| <span class="loader-arc-d loader-arc" /> | ||
| </> | ||
| ) | ||
| } | ||
|
|
||
| function Root( | ||
| props: Omit<ComponentProps<typeof Ark.Root>, 'value' | 'defaultValue'> & {size?: LoaderSize}, | ||
| ): JSX.Element { | ||
| const [local, rest] = splitProps(props, ['class', 'size']) | ||
| return ( | ||
| <Ark.Root | ||
| translations={DEFAULT_TRANSLATIONS} | ||
| {...rest} | ||
| value={null} | ||
| class={`${ROOT} ${local.class ?? ''}`} | ||
| data-size={local.size ?? 'md'} | ||
| /> | ||
| ) | ||
| } | ||
|
|
||
| function Indicator(props: ComponentProps<typeof Ark.Track>): JSX.Element { | ||
| const [local, rest] = splitProps(props, ['class', 'children']) | ||
| return ( | ||
| <Ark.Track {...rest} class={`${ORB} ${local.class ?? ''}`}> | ||
| <Show when={local.children !== undefined} fallback={<LoaderArcs />}> | ||
| {local.children} | ||
| </Show> | ||
| </Ark.Track> | ||
| ) | ||
| } | ||
|
|
||
| function Text(props: ComponentProps<'div'>): JSX.Element { | ||
| const [local, rest] = splitProps(props, ['class']) | ||
| return <div {...rest} class={`${TEXT} ${local.class ?? ''}`} /> | ||
| } | ||
|
|
||
| function Label(props: ComponentProps<typeof Ark.Label>): JSX.Element { | ||
| const [local, rest] = splitProps(props, ['class']) | ||
| return <Ark.Label {...rest} class={`${LABEL} ${local.class ?? ''}`} /> | ||
| } | ||
|
|
||
| function Description(props: ComponentProps<'p'>): JSX.Element { | ||
| const [local, rest] = splitProps(props, ['class']) | ||
| return <p {...rest} class={`${DESCRIPTION} ${local.class ?? ''}`} /> | ||
| } | ||
|
|
||
| export const Loader = Object.assign({}, Ark, {Root, Indicator, Text, Label, Description}) |
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
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added in e8d79d1: fake-core gained a sessions/resolve route, and a new test drives an alias navigation whose beforeLoad is held 900ms — asserting the pending loader appears, hands off to the pane, and leaves. Revert-checked: with defaultPendingComponent removed the test fails on the pending-visible assertion.