styleguide: the pager, two chevron icons, and two pattern sentences #314

Closed
opened 2026-10-19 16:22:26 +02:00 by dominik.polakovics · 0 comments

Summary

The dashboard (deckle-hosted) gets a Builds page listing every push of a site, fifty a page, and needs a shared way to move between pages. The styleguide has none: today's lists offer a "Newer" and an "Older" link in a panel foot. This adds .pager to components.css with its sample and rules in styleguide.html, two Lucide chevron icons for its ends, and two sentences to the Patterns section that record decisions the products already follow. No new token, no new colour, no new tone; the contrast table is unchanged.

The design it serves is deckle-hosted's Overview redesign, https://claude.ai/artifact/DWvnF6xuivH8o4EFPr2qEi (section 5, "The builds pages", with the pager at three and at forty pages, desktop and phone), and deckle-hosted issue #146, which is blocked on this one. The editor gains the component for free through its import of components.css and need not use it.

Current behavior

docs/styleguide/components.css has no pagination component; the dashboard draws Newer and Older as plain .links in a .panel__foot. styleguide.html section 5 (Components) has no sample for it, section 6 (Patterns) says nothing about where a pager sits, nothing about a destructive action that needs no confirmation, and its "Responsive" article does not mention paging. The sample page's icon set (PATHS in the inline script) has lock, ext, copy, x, mail, pencil; no chevrons.

Desired behavior

components.css: the pager

A new block, /* ---- pager: the pages under a long list ---- */, placed after the list and table block and before /* ---- forms ---- */, every value a token:

/* ---- pager: the pages under a long list, in a panel foot ---- */
.pager { flex: 1 1 100%; display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--space-1); }
.pager__pages { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--space-1); padding: 0 var(--space-1); }
.pager [aria-current="page"] { border-color: var(--color-border-control); background: var(--tone-neutral-bg); color: var(--color-heading); cursor: default; }
.pager [aria-disabled="true"] { opacity: 0.45; cursor: default; }
.pager__gap { padding: 0 2px; color: var(--color-text-muted); }
.pager__meta { margin-right: auto; font: var(--type-hint); color: var(--color-text-muted); white-space: nowrap; }
.pager__current { display: none; }

and, inside the existing @media (max-width: 720px) block:

  .pager { justify-content: space-between; }
  .pager__pages { display: none; }
  .pager__current { display: inline; }
  .pager__meta { flex-basis: 100%; order: 1; margin: 0; white-space: normal; }

The buttons inside are the existing .btn.btn--sm (the two ends) and .btn.btn--sm.btn--ghost (the page numbers); the pager adds no button variant. flex: 1 1 100% is what makes "count left, controls right" happen inside a .panel__foot, which is itself a flex row. The opacity rule is on a selector that names a disabled state, as the drift rule requires.

Markup the sample and both products use (the dashboard renders it server-side; the current page is a span, the ends are links or, with nothing beyond them, spans):

<nav class="pager" aria-label="Pages of builds">
  <span class="pager__meta"><span class="pager__current">Page 17 of 40 · </span>801–850 of 1,973 builds</span>
  <a class="btn btn--sm" href="?page=16"><i data-icon="prev"></i>Newer</a>
  <span class="pager__pages">
    <a class="btn btn--sm btn--ghost" href="?page=1" aria-label="Page 1">1</a>
    <span class="pager__gap" aria-hidden="true">…</span>
    <a class="btn btn--sm btn--ghost" href="?page=15" aria-label="Page 15">15</a>
    <a class="btn btn--sm btn--ghost" href="?page=16" aria-label="Page 16">16</a>
    <span class="btn btn--sm btn--ghost" aria-current="page" aria-label="Page 17, current">17</span>
    <a class="btn btn--sm btn--ghost" href="?page=18" aria-label="Page 18">18</a>
    <a class="btn btn--sm btn--ghost" href="?page=19" aria-label="Page 19">19</a>
    <span class="pager__gap" aria-hidden="true">…</span>
    <a class="btn btn--sm btn--ghost" href="?page=40" aria-label="Page 40">40</a>
  </span>
  <a class="btn btn--sm" href="?page=18">Older<i data-icon="next"></i></a>
</nav>

styleguide.html, section 5 (Components): the sample and its rules

A new board after the "list · table · empty" board, labelled pager · the pages under a long list, holding two .panels, each a short .table (two or three rows of the builds table already used in the list board) with the pager in its .panel__foot:

  • three pages, on page 2: 51–100 of 128 builds · Newer · 1 [2] 3 · Older;
  • forty pages, on page 17: 801–850 of 1,973 builds · Newer · 1 … 15 16 [17] 18 19 … 40 · Older;

and a third panel showing page 1 of 3, where Newer is a span with aria-disabled="true", so the disabled end is seen once.

Below the board, four articles in the .grid, in the voice of the existing ones:

  • Pages. The numbers are the first page, the last, and the current with two neighbours each side; a gap mark where pages are skipped. Nine numbers at most, whatever the count. The current page is held on the neutral tone and is not a link.
  • Ends. Newer and Older are the two bordered small buttons, with the chevron icons; an end with nothing beyond it is drawn disabled, not dropped, so the row keeps its shape.
  • Count. pager__meta says the range and the total ("51–100 of 128 builds"), at the left; pager__current ("Page 2 of 3 · ") is hidden on desktop and shown on a phone, where the numbers are not.
  • Place. A panel foot below a table or a list, the count at the left, the controls at the right, mirroring the panel head (text left, tools right). On a phone Newer and Older take the two edges of the first line and the count the line below. A window of a long list (the newest few on an overview) is not paged; it links to the paged page instead.

styleguide.html, section 6 (Patterns): two sentences and one addition

  • Page anatomy: append "A pager sits in a panel foot, right-aligned, with the count at the left."
  • Destructive flows: append "An action that ends a capability someone else holds and that can be reissued (a share link, an access token) is ghost red and needs no confirmation; the filled red confirmation stays for what cannot be undone." This records what deckle-hosted's Account page and preview share links already do.
  • Responsive: after "tab meta wraps to its own line", add "a pager's numbers give way to its page count and its two ends move to the edges".

styleguide.html: the icons

PATHS in the inline script gains the two Lucide chevrons, so <i data-icon="prev"> and <i data-icon="next"> draw in the sample:

prev:'<path d="m15 18-6-6 6-6"/>',
next:'<path d="m9 18 6-6-6-6"/>'

The "Shape, controls, icons, measure" article (section 4) already says icons are Lucide at 16px and 14px inside controls; nothing to change there.

README.md (docs/styleguide)

No change: the file table and "Keeping it current" already cover a new shared component.

Decisions

  • A component, not a product class. deckle-hosted's CLAUDE.md puts a piece the vocabulary lacks in the styleguide first, never in dashboard.css under a dk- name; the pager is shared vocabulary even while only one product uses it.
  • Count left, controls right. The foot mirrors the panel head; a centered pager would be the products' one centered control outside the empty state.
  • Disabled ends stay. The row keeps its shape between page 1 and page 2; opacity on a disabled control is the one use the styleguide allows.
  • No "go to page" input, no page-size control. Fifty a page and the window rule cover the histories the products have.
  • Phone: ends at the edges, count below. Two thumbs find Newer and Older; the numbers are not tap targets at that width.

Key interfaces

  • docs/styleguide/components.css: the new block and the four lines in the narrow block.
  • docs/styleguide/styleguide.html: the board and four articles in section 5; the three sentences in section 6; PATHS in the script.
  • docs/styleguide/check-contrast.py and tokens.css: untouched; the pager uses --tone-neutral-bg, --color-border-control, --color-heading, --color-text-muted, --type-hint, --space-1, all listed already.
  • web/src/styles.css: untouched; it imports components.css and inherits the component.
  • deckle-hosted follows with dev/sync-styleguide.sh (its drift test compares the two files verbatim) and its own icon table; that is #146 there, not this issue.

Acceptance criteria

  • components.css has the .pager block and the four narrow-screen lines, every value a token, no colour literal, opacity only on [aria-disabled="true"].
  • styleguide.html section 5 shows the pager at three pages, at forty pages, and at page 1 with a disabled end; the current page is held, the numbers follow the window rule, the count sits at the left and the controls at the right at full width; narrowing the browser below 720px hides the numbers, shows "Page n of m · ", and puts Newer and Older at the two edges with the count below.
  • The four articles under the board and the three sentences in section 6 are present as worded above.
  • <i data-icon="prev"> and <i data-icon="next"> render as chevrons in the sample.
  • go test ./docs/ (check-contrast) passes; the web/ drift test passes unchanged; styleguide.html opens from the folder with no server and no build step, as before.

How to verify

  • python3 docs/styleguide/check-contrast.py exits 0; grep -nE '#[0-9a-fA-F]{3,8}|rgba?\(' docs/styleguide/components.css finds nothing.
  • Open docs/styleguide/styleguide.html, go to section 5, and compare the three pager samples with the artifact's section 5 at desktop width, then narrow the window under 720px and compare with its phone frame.
## Summary The dashboard (deckle-hosted) gets a Builds page listing every push of a site, fifty a page, and needs a shared way to move between pages. The styleguide has none: today's lists offer a "Newer" and an "Older" link in a panel foot. This adds **`.pager`** to `components.css` with its sample and rules in `styleguide.html`, two Lucide chevron icons for its ends, and two sentences to the Patterns section that record decisions the products already follow. No new token, no new colour, no new tone; the contrast table is unchanged. The design it serves is deckle-hosted's Overview redesign, <https://claude.ai/artifact/DWvnF6xuivH8o4EFPr2qEi> (section 5, "The builds pages", with the pager at three and at forty pages, desktop and phone), and deckle-hosted issue #146, which is blocked on this one. The editor gains the component for free through its import of `components.css` and need not use it. ## Current behavior `docs/styleguide/components.css` has no pagination component; the dashboard draws Newer and Older as plain `.link`s in a `.panel__foot`. `styleguide.html` section 5 (Components) has no sample for it, section 6 (Patterns) says nothing about where a pager sits, nothing about a destructive action that needs no confirmation, and its "Responsive" article does not mention paging. The sample page's icon set (`PATHS` in the inline script) has `lock`, `ext`, `copy`, `x`, `mail`, `pencil`; no chevrons. ## Desired behavior ### components.css: the pager A new block, `/* ---- pager: the pages under a long list ---- */`, placed after the list and table block and before `/* ---- forms ---- */`, every value a token: ```css /* ---- pager: the pages under a long list, in a panel foot ---- */ .pager { flex: 1 1 100%; display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: var(--space-1); } .pager__pages { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--space-1); padding: 0 var(--space-1); } .pager [aria-current="page"] { border-color: var(--color-border-control); background: var(--tone-neutral-bg); color: var(--color-heading); cursor: default; } .pager [aria-disabled="true"] { opacity: 0.45; cursor: default; } .pager__gap { padding: 0 2px; color: var(--color-text-muted); } .pager__meta { margin-right: auto; font: var(--type-hint); color: var(--color-text-muted); white-space: nowrap; } .pager__current { display: none; } ``` and, inside the existing `@media (max-width: 720px)` block: ```css .pager { justify-content: space-between; } .pager__pages { display: none; } .pager__current { display: inline; } .pager__meta { flex-basis: 100%; order: 1; margin: 0; white-space: normal; } ``` The buttons inside are the existing `.btn.btn--sm` (the two ends) and `.btn.btn--sm.btn--ghost` (the page numbers); the pager adds no button variant. `flex: 1 1 100%` is what makes "count left, controls right" happen inside a `.panel__foot`, which is itself a flex row. The `opacity` rule is on a selector that names a disabled state, as the drift rule requires. Markup the sample and both products use (the dashboard renders it server-side; the current page is a `span`, the ends are links or, with nothing beyond them, `span`s): ```html <nav class="pager" aria-label="Pages of builds"> <span class="pager__meta"><span class="pager__current">Page 17 of 40 · </span>801–850 of 1,973 builds</span> <a class="btn btn--sm" href="?page=16"><i data-icon="prev"></i>Newer</a> <span class="pager__pages"> <a class="btn btn--sm btn--ghost" href="?page=1" aria-label="Page 1">1</a> <span class="pager__gap" aria-hidden="true">…</span> <a class="btn btn--sm btn--ghost" href="?page=15" aria-label="Page 15">15</a> <a class="btn btn--sm btn--ghost" href="?page=16" aria-label="Page 16">16</a> <span class="btn btn--sm btn--ghost" aria-current="page" aria-label="Page 17, current">17</span> <a class="btn btn--sm btn--ghost" href="?page=18" aria-label="Page 18">18</a> <a class="btn btn--sm btn--ghost" href="?page=19" aria-label="Page 19">19</a> <span class="pager__gap" aria-hidden="true">…</span> <a class="btn btn--sm btn--ghost" href="?page=40" aria-label="Page 40">40</a> </span> <a class="btn btn--sm" href="?page=18">Older<i data-icon="next"></i></a> </nav> ``` ### styleguide.html, section 5 (Components): the sample and its rules A new board after the "list · table · empty" board, labelled `pager · the pages under a long list`, holding two `.panel`s, each a short `.table` (two or three rows of the builds table already used in the list board) with the pager in its `.panel__foot`: - **three pages, on page 2**: `51–100 of 128 builds` · Newer · 1 [2] 3 · Older; - **forty pages, on page 17**: `801–850 of 1,973 builds` · Newer · 1 … 15 16 [17] 18 19 … 40 · Older; and a third panel showing **page 1 of 3**, where Newer is a `span` with `aria-disabled="true"`, so the disabled end is seen once. Below the board, four articles in the `.grid`, in the voice of the existing ones: - **Pages.** The numbers are the first page, the last, and the current with two neighbours each side; a gap mark where pages are skipped. Nine numbers at most, whatever the count. The current page is held on the neutral tone and is not a link. - **Ends.** Newer and Older are the two bordered small buttons, with the chevron icons; an end with nothing beyond it is drawn disabled, not dropped, so the row keeps its shape. - **Count.** `pager__meta` says the range and the total ("51–100 of 128 builds"), at the left; `pager__current` ("Page 2 of 3 · ") is hidden on desktop and shown on a phone, where the numbers are not. - **Place.** A panel foot below a table or a list, the count at the left, the controls at the right, mirroring the panel head (text left, tools right). On a phone Newer and Older take the two edges of the first line and the count the line below. A window of a long list (the newest few on an overview) is not paged; it links to the paged page instead. ### styleguide.html, section 6 (Patterns): two sentences and one addition - **Page anatomy**: append "A pager sits in a panel foot, right-aligned, with the count at the left." - **Destructive flows**: append "An action that ends a capability someone else holds and that can be reissued (a share link, an access token) is ghost red and needs no confirmation; the filled red confirmation stays for what cannot be undone." This records what deckle-hosted's Account page and preview share links already do. - **Responsive**: after "tab meta wraps to its own line", add "a pager's numbers give way to its page count and its two ends move to the edges". ### styleguide.html: the icons `PATHS` in the inline script gains the two Lucide chevrons, so `<i data-icon="prev">` and `<i data-icon="next">` draw in the sample: ```js prev:'<path d="m15 18-6-6 6-6"/>', next:'<path d="m9 18 6-6-6-6"/>' ``` The "Shape, controls, icons, measure" article (section 4) already says icons are Lucide at 16px and 14px inside controls; nothing to change there. ### README.md (docs/styleguide) No change: the file table and "Keeping it current" already cover a new shared component. ## Decisions - **A component, not a product class.** deckle-hosted's CLAUDE.md puts a piece the vocabulary lacks in the styleguide first, never in `dashboard.css` under a `dk-` name; the pager is shared vocabulary even while only one product uses it. - **Count left, controls right.** The foot mirrors the panel head; a centered pager would be the products' one centered control outside the empty state. - **Disabled ends stay.** The row keeps its shape between page 1 and page 2; `opacity` on a disabled control is the one use the styleguide allows. - **No "go to page" input, no page-size control.** Fifty a page and the window rule cover the histories the products have. - **Phone: ends at the edges, count below.** Two thumbs find Newer and Older; the numbers are not tap targets at that width. ## Key interfaces - `docs/styleguide/components.css`: the new block and the four lines in the narrow block. - `docs/styleguide/styleguide.html`: the board and four articles in section 5; the three sentences in section 6; `PATHS` in the script. - `docs/styleguide/check-contrast.py` and `tokens.css`: untouched; the pager uses `--tone-neutral-bg`, `--color-border-control`, `--color-heading`, `--color-text-muted`, `--type-hint`, `--space-1`, all listed already. - `web/src/styles.css`: untouched; it imports `components.css` and inherits the component. - deckle-hosted follows with `dev/sync-styleguide.sh` (its drift test compares the two files verbatim) and its own icon table; that is #146 there, not this issue. ## Acceptance criteria - [ ] `components.css` has the `.pager` block and the four narrow-screen lines, every value a token, no colour literal, `opacity` only on `[aria-disabled="true"]`. - [ ] `styleguide.html` section 5 shows the pager at three pages, at forty pages, and at page 1 with a disabled end; the current page is held, the numbers follow the window rule, the count sits at the left and the controls at the right at full width; narrowing the browser below 720px hides the numbers, shows "Page n of m · ", and puts Newer and Older at the two edges with the count below. - [ ] The four articles under the board and the three sentences in section 6 are present as worded above. - [ ] `<i data-icon="prev">` and `<i data-icon="next">` render as chevrons in the sample. - [ ] `go test ./docs/` (check-contrast) passes; the `web/` drift test passes unchanged; `styleguide.html` opens from the folder with no server and no build step, as before. ## How to verify - `python3 docs/styleguide/check-contrast.py` exits 0; `grep -nE '#[0-9a-fA-F]{3,8}|rgba?\(' docs/styleguide/components.css` finds nothing. - Open `docs/styleguide/styleguide.html`, go to section 5, and compare the three pager samples with the artifact's section 5 at desktop width, then narrow the window under 720px and compare with its phone frame.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle#314
No description provided.