@vibeuncle/gpgb-ui 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  Versions follow semver while 0.x: **minor** = new components or token changes that may shift appearance, **patch** = fixes. Apps pin with `^0.x.0`.
4
4
 
5
+ ## 0.4.0
6
+ - **Standard sizes:** header row is a fixed 72px (`ds-header-row`, built into `SiteHeader`) so the logo and page sit at the same height in every app; exact control heights, sm 32 / md 40 / lg 48px for buttons and inputs (`ds-input-lg` added), tabs 40px; page head classes `ds-page-head`, `ds-title` (36/48px), `ds-title-sm`, `ds-lede`; `ds-search` and `<SearchInput>`; `<PageTitle small>`. `ds-page-title` removed (use `ds-page-head`).
7
+ - The browser test suite now measures these sizes in the style guide.
8
+ - Appearance: buttons that were 46px (lg) are now 48px; inputs 39px are 40px; small inputs 34px are 32px; headers that depended on their content are now 72px.
9
+
10
+ ## 0.3.0
11
+ - **Page width standard:** the 72rem (1152px) frame of draw.gpgb.app, now with responsive gutters (16px, 24px from 640px) so desktop content is 1104px. `ds-container-wide` (80rem), `ds-measure` (42rem reading column inside the frame), `ds-split` / `<SplitLayout>` (content + 22rem side panel), `ds-grid-cards` (auto-fill card grid). `<PageShell wide>`.
12
+ - Docs: Tailwind's `rounded-sm/md/lg/xl` and `shadow-sm/md/lg` follow the system scale (they are defined in `@theme`), and the page-width rules.
13
+
5
14
  ## 0.2.1
6
15
  - Release pipeline only: publishes through npm trusted publishing (no token). No code or appearance changes.
7
16
 
package/DESIGN.md CHANGED
@@ -187,7 +187,11 @@ Rule: no raw hex in app code; no Tailwind red/gray/orange.
187
187
 
188
188
  ## Layout
189
189
 
190
- - 4px base. Page gutter 24px; `ds-container` max 72rem. Card padding 16 to 24px; field gap 16px; button icon gap 8px.
190
+ - **Standard sizes (every app):** header 72px (+1px border) with the 24px (`h-6`) graphic logo; page title 36px (48px from 640px), 40/48px below the header; buttons and inputs 32 / 40 / 48px (page action = lg; toolbar and search = md; row actions = sm); segmented tabs and the language toggle 40px. Fixed heights, not by-products of padding.
191
+
192
+ - **Page frame:** 72rem (1152px) centered, as draw.gpgb.app; gutters 16px, then 24px from 640px (1104px of content on desktop). The header and the content share it. Editors and big tables use the 80rem wide frame. Never a narrower frame: narrow content is a 42rem column (`ds-measure`) inside it; sign-in, invite and not-found are a 400px centered card.
193
+ - **Using the width:** content plus a 22rem side panel (`ds-split`, from 1024px, sticky; stacks on smaller screens), responsive card grids (`ds-grid-cards`, columns of at least 18rem), or a 240px left navigation column for editors and admin.
194
+ - 4px spacing base. Card padding 16 to 24px; field gap 16px; button icon gap 8px.
191
195
  - Mobile-first; `sm` (640px) is the phone/tablet switch. On touch (`pointer: coarse`) inputs are 16px (no iOS zoom) and buttons are 44px tall; switches get a 44px hit area; tabs 40px.
192
196
  - Sections separate with generous vertical space (40px) and a hairline; groups inside stay tight.
193
197
 
package/DESIGN_SYSTEM.md CHANGED
@@ -89,7 +89,7 @@ Rules: one flourish per view; everything else is quiet feedback that answers an
89
89
  ## Navigation
90
90
  | Piece | React | Notes |
91
91
  |---|---|---|
92
- | Header | `<SiteHeader logoSrc title nav actions menuButton containerClassName homeLabel>` | Graphic logo left; `nav` shows from `md`; `menuButton` shows below `md`. Includes a skip link to `#main` (give your `<main>` that id). `containerClassName` sets the content width (default 72rem `ds-container`; narrow apps pass e.g. `mx-auto max-w-2xl px-5`). Put short always-visible links in `actions` instead of `nav`. |
92
+ | Header | `<SiteHeader logoSrc title nav actions menuButton containerClassName homeLabel>` | Graphic logo left; `nav` shows from `md`; `menuButton` shows below `md`. Includes a skip link to `#main` (give your `<main>` that id). `containerClassName` sets the content width (default the 72rem `ds-container` frame; pass `ds-container ds-container-wide` on wide pages; don't narrow it). Put short always-visible links in `actions` instead of `nav`. |
93
93
  | Nav links | `<NavLinks items>` / `ds-nav-link` | Pill links; current = accent-soft wash + `aria-current="page"`. |
94
94
  | Phone nav | `<MenuButton>` + `<NavDrawer items open onOpenChange>` | Left drawer (`Sheet side="left"`). Closes on link click, Esc, scrim. |
95
95
  | Account | `<UserMenu>` / `<SignInButton>` | Avatar + menu: settings, extras, sign out. Signed out: outline 登录 button. |
@@ -105,8 +105,33 @@ Pick by job: **Menu** = list of actions; **Popover** = a little extra content or
105
105
  - Modal, ConfirmDialog and Sheet trap focus, close on Esc, lock scroll and restore focus to the opener. Footer order: outline cancel, then the primary or danger action, whose label names the action ("删除海报", not "确定").
106
106
  - Positioning is simple (below the trigger, start or end aligned); there is no collision flipping yet. Keep triggers away from the bottom edge, or use a Sheet on phones.
107
107
 
108
+ ## Standard sizes (identical in every app)
109
+ | Thing | Size | Class |
110
+ |---|---|---|
111
+ | Header | **72px** tall (+1px border), content centred, whatever is inside (language toggle, buttons, nothing); inner content at most 48px | `ds-header-row` (built into `SiteHeader`) |
112
+ | Logo | **24px** high (`h-6`), always the graphic mark | |
113
+ | Page title | **36px**, 48px from 640px, serif 500, tracking -0.025em. Detail pages with long titles: 30px / 36px | `ds-title`, `ds-title-sm` |
114
+ | Page head | title + lede on the left, actions right; **40px** below the header (48px from 640px) | `ds-page-head`, `ds-lede`, `<PageTitle>` |
115
+ | Buttons | **sm 32 / md 40 / lg 48px**; icon buttons are square at the same size | `ds-btn` `-sm` `-lg` `-icon` |
116
+ | Inputs | **sm 32 / md 40 / lg 48px** | `ds-input` `-sm` `-lg` |
117
+ | Search | the 40px input with a leading icon | `ds-search` / `<SearchInput>` |
118
+ | Segmented tabs / language toggle | **40px** | `ds-tabs` |
119
+ | Touch (`pointer: coarse`) | controls 44px, input text 16px | automatic |
120
+
121
+ Use: page-level action (new poster, new album, create group) = **lg**; toolbar and in-card actions, search and filter tabs = **md** (they line up in one 40px row); compact row actions = **sm**. Don't set your own heights or text sizes on these. The style guide's "Sizes & page head" section measures them live, and `npm test` asserts them.
122
+
123
+ ## Page width
124
+ One width across all 大道大商 apps: the **72rem frame** (1152px, centered), as on draw.gpgb.app. Gutters are 16px, then 24px from 640px, so desktop content is **1104px** wide. The header and the page content share the frame.
125
+ - **Standard:** `ds-container` (72rem). Galleries, dashboards, chat, lists, forms-with-context.
126
+ - **Wide:** `ds-container ds-container-wide` (80rem). Editors and big tables only. Pass the same to `SiteHeader`'s `containerClassName`.
127
+ - **Never narrow the frame.** Narrow content is a column *inside* it: `ds-measure` (42rem) for reading text or a single form. Sign-in, invite and not-found pages are a `CenteredPage` card (400px).
128
+ - **Use the width:** `ds-split` / `<SplitLayout aside>` puts content on the left and a 22rem side panel (share link, QR, info, actions) on the right from 1024px, stacking below on smaller screens; `ds-grid-cards` is a responsive card grid (columns of at least 18rem); `<SidebarLayout>` is a left navigation column (240px) for editors, settings and admin.
129
+ - **Phones:** everything is one column with 16px gutters; the frame only matters from tablet up.
130
+
131
+ Tailwind note: `rounded-sm/md/lg/xl` (6/8/12/16px) and `shadow-sm/md/lg` are the system scale (they are defined in `@theme`), so they differ slightly from Tailwind's defaults in every app that imports the package. Use them rather than arbitrary radii.
132
+
108
133
  ## Page layouts
109
- `<PageShell header footer>` (header, 72rem container, footer: lists, galleries, dashboards) · `<CenteredPage>` (one 400px card: sign-in, invite, not-found) · `<SidebarLayout aside>` (240px sticky aside from `lg`; on phones move the aside into a bottom Sheet: editors, settings, admin) · `<PageTitle title description actions breadcrumbs>` (serif title, primary action first on phones).
134
+ `<PageShell header footer wide>` (header, frame, footer: lists, galleries, dashboards) · `<CenteredPage>` (one 400px card: sign-in, invite, not-found) · `<SplitLayout aside>` (content + right side panel) · `<SidebarLayout aside>` (left navigation column: editors, settings, admin; on phones move it into a bottom Sheet) · `<PageTitle title description actions breadcrumbs>` (serif title, primary action first on phones).
110
135
 
111
136
  ## Content & voice
112
137
  Warm, plain, respectful: a helpful neighbour at the community centre. Chinese (简体) first, English beside or beneath. Say what happened and what to do next; never blame the person.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeuncle/gpgb-ui",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "大道大商 shared design system: tokens, CSS components, React components, style guide",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -6,12 +6,12 @@ import { t } from '../copy'
6
6
  import type { Lang } from '../copy'
7
7
  import { useLink } from '../link'
8
8
 
9
- /** Standard page: header, main, footer. Content width via `ds-container`. */
10
- export function PageShell({ header, children, footer }: { header?: ReactNode; children: ReactNode; footer?: ReactNode }) {
9
+ /** Standard page: header, main, footer, all on the 72rem frame (`wide` = 80rem for editors and big tables). */
10
+ export function PageShell({ header, children, footer, wide }: { header?: ReactNode; children: ReactNode; footer?: ReactNode; wide?: boolean }) {
11
11
  return (
12
12
  <div className="ds-page">
13
13
  {header}
14
- <main id="main" className="ds-container py-10">{children}</main>
14
+ <main id="main" className={cn('ds-container py-10', wide && 'ds-container-wide')}>{children}</main>
15
15
  {footer}
16
16
  </div>
17
17
  )
@@ -22,22 +22,26 @@ export function CenteredPage({ children }: { children: ReactNode }) {
22
22
  return <div className="ds-page-centered"><main id="main" className="ds-card">{children}</main></div>
23
23
  }
24
24
 
25
- /** Page title row: serif title + description on the left, actions on the right (primary action first on phones). */
26
- export function PageTitle({ title, description, actions, breadcrumbs }: { title: string; description?: string; actions?: ReactNode; breadcrumbs?: ReactNode }) {
25
+ /** The page head, identical in position and type on every page: serif title (36px, 48px from 640px; `small` = 30/36px for detail pages
26
+ * with long titles), optional lede, actions on the right. Sits 40/48px below the header. */
27
+ export function PageTitle({ title, description, actions, breadcrumbs, small }: { title: string; description?: string; actions?: ReactNode; breadcrumbs?: ReactNode; small?: boolean }) {
27
28
  return (
28
- <div className="mb-9">
29
- {breadcrumbs && <div className="mb-3">{breadcrumbs}</div>}
30
- <div className="ds-page-title !mb-0">
31
- <div className="min-w-0">
32
- <h1 className="font-display text-4xl font-medium sm:text-5xl">{title}</h1>
33
- {description && <p className="mt-3 max-w-[44em] text-[13px] leading-[1.85] text-ink-soft">{description}</p>}
34
- </div>
35
- {actions && <div className="flex flex-wrap items-center gap-2">{actions}</div>}
29
+ <div className="ds-page-head">
30
+ <div className="min-w-0">
31
+ {breadcrumbs && <div className="mb-3">{breadcrumbs}</div>}
32
+ <h1 className={small ? 'ds-title-sm' : 'ds-title'}>{title}</h1>
33
+ {description && <p className="ds-lede">{description}</p>}
36
34
  </div>
35
+ {actions && <div className="flex flex-wrap items-center gap-2">{actions}</div>}
37
36
  </div>
38
37
  )
39
38
  }
40
39
 
40
+ /** Content + side panel (share link, QR, info, actions). Panel is on the right from 1024px (sticky) and stacks below the content on smaller screens. */
41
+ export function SplitLayout({ aside, children }: { aside: ReactNode; children: ReactNode }) {
42
+ return <div className="ds-split"><div className="min-w-0">{children}</div><aside>{aside}</aside></div>
43
+ }
44
+
41
45
  /** Sidebar + content (editor, settings, admin). Sidebar is sticky from `lg`; on phones put its content in a Sheet instead. */
42
46
  export function SidebarLayout({ aside, children, wide }: { aside: ReactNode; children: ReactNode; wide?: boolean }) {
43
47
  return <div className={cn('ds-shell', wide && 'ds-shell-wide')}><aside>{aside}</aside><div className="min-w-0">{children}</div></div>
@@ -0,0 +1,13 @@
1
+ import { Search } from 'lucide-react'
2
+ import type { InputHTMLAttributes } from 'react'
3
+ import { cn } from '../cn'
4
+
5
+ /** Search field: the standard 40px `ds-input` with a leading icon. Pass `label` (the accessible name) and a placeholder. */
6
+ export function SearchInput({ label, className, wrapperClassName, ...rest }: InputHTMLAttributes<HTMLInputElement> & { label: string; wrapperClassName?: string }) {
7
+ return (
8
+ <div className={cn('ds-search', wrapperClassName)}>
9
+ <Search aria-hidden />
10
+ <input type="search" aria-label={label} className={cn('ds-input', className)} {...rest} />
11
+ </div>
12
+ )
13
+ }
@@ -7,7 +7,7 @@ import { useLink } from '../link'
7
7
  * Phones: pass `menuButton` (see MenuButton + NavDrawer) and hide `nav` below `md` (NavLinks className="hidden md:flex"). */
8
8
  export function SiteHeader({ logoSrc, logoAlt = '大道大商', title, nav, actions, menuButton, href = '/', containerClassName = 'ds-container', homeLabel }: {
9
9
  logoSrc: string; logoAlt?: string; title?: string; nav?: ReactNode; actions?: ReactNode; menuButton?: ReactNode; href?: string
10
- /** Width/gutter of the header content. Default `ds-container` (72rem). Narrow apps pass e.g. 'mx-auto max-w-2xl px-5' to line up with their page column. */
10
+ /** Width/gutter of the header content. Default `ds-container` (the 72rem frame). Keep it equal to the page's frame: pass 'ds-container ds-container-wide' on wide pages. Don't narrow it; put narrow content in a column inside the frame. */
11
11
  containerClassName?: string
12
12
  /** aria-label for the logo link when there is no title (e.g. "接龙 首页"). */
13
13
  homeLabel?: string
@@ -16,7 +16,7 @@ export function SiteHeader({ logoSrc, logoAlt = '大道大商', title, nav, acti
16
16
  return (
17
17
  <header className="ds-header">
18
18
  <a href="#main" className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-50 ds-btn ds-btn-primary">Skip to content</a>
19
- <div className={`${containerClassName} flex items-center justify-between gap-x-4 gap-y-2 py-4`}>
19
+ <div className={`${containerClassName} ds-header-row flex items-center justify-between gap-x-4 gap-y-2`}>
20
20
  <div className="flex min-w-0 items-center gap-3">
21
21
  {menuButton && <div className="md:hidden">{menuButton}</div>}
22
22
  <Link href={href} aria-label={homeLabel} className="flex min-w-0 items-center gap-4">
package/src/index.ts CHANGED
@@ -17,3 +17,4 @@ export * from './copy'
17
17
  export * from './hooks'
18
18
  export * from './components/Stamp'
19
19
  export * from './link'
20
+ export * from './components/SearchInput'
@@ -36,9 +36,12 @@
36
36
  .ds-hint { font-size: 12px; color: var(--color-ink-faint); }
37
37
 
38
38
  /* ---- Buttons: pills. One primary per view. ---- */
39
+ /* Control heights are a standard, not a by-product of padding: sm 32px, md 40px, lg 48px (buttons, inputs, tabs). Page-level
40
+ action = lg; toolbar and in-card actions = md; compact row actions = sm. */
39
41
  .ds-btn {
42
+ --ds-h: 40px; min-height: var(--ds-h);
40
43
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
41
- padding: 10px 20px; font-size: 14px; font-weight: 500; line-height: 1.3;
44
+ padding: 0 20px; font-size: 14px; font-weight: 500; line-height: 1.3;
42
45
  border: 1px solid transparent; border-radius: var(--radius-pill); cursor: pointer;
43
46
  transition: background-color var(--duration-fast) ease-out, color var(--duration-fast) ease-out,
44
47
  border-color var(--duration-fast) ease-out, box-shadow var(--duration-fast) ease-out,
@@ -46,9 +49,9 @@
46
49
  }
47
50
  .ds-btn:active:not(:disabled) { transform: scale(0.97); }
48
51
  .ds-btn:disabled, .ds-btn[aria-disabled='true'] { opacity: 0.45; cursor: not-allowed; }
49
- .ds-btn-sm { padding: 6px 14px; font-size: 13px; }
50
- .ds-btn-lg { padding: 12px 24px; font-size: 15px; }
51
- .ds-btn-icon { padding: 8px; aspect-ratio: 1; }
52
+ .ds-btn-sm { --ds-h: 32px; padding: 0 14px; font-size: 13px; }
53
+ .ds-btn-lg { --ds-h: 48px; padding: 0 24px; font-size: 15px; }
54
+ .ds-btn-icon { padding: 0; width: var(--ds-h); }
52
55
  .ds-btn-primary { background: var(--color-accent); color: var(--color-on-accent); box-shadow: var(--shadow-sm); }
53
56
  .ds-btn-primary:hover:not(:disabled) { background: var(--color-accent-hover); box-shadow: var(--shadow-md); }
54
57
  .ds-btn-outline { background: var(--color-surface); color: var(--color-ink); border-color: var(--color-border-soft); }
@@ -70,7 +73,7 @@
70
73
  /* ---- Forms ---- */
71
74
  .ds-label { display: block; margin-bottom: 6px; font-size: 13px; font-weight: 500; color: var(--color-ink); }
72
75
  .ds-input {
73
- width: 100%; padding: 10px 14px; font-size: 14px; color: var(--color-ink);
76
+ width: 100%; min-height: 40px; padding: 9px 14px; font-size: 14px; line-height: 1.4; color: var(--color-ink);
74
77
  background: var(--color-surface); border: 1px solid var(--color-border-strong);
75
78
  border-radius: var(--radius-md); transition: border-color var(--duration-fast) ease-out;
76
79
  }
@@ -80,7 +83,9 @@
80
83
  .ds-input:disabled { opacity: 0.55; cursor: not-allowed; }
81
84
  .ds-input[aria-invalid='true'] { border-color: var(--color-danger); }
82
85
  .ds-input[aria-invalid='true']:focus-visible { outline-color: var(--color-danger); border-color: var(--color-danger); }
83
- .ds-input-sm { padding: 7px 12px; font-size: 13px; }
86
+ .ds-input-sm { min-height: 32px; padding: 5px 12px; font-size: 13px; }
87
+ .ds-input-lg { min-height: 48px; padding: 12px 16px; font-size: 15px; }
88
+ textarea.ds-input, select.ds-input-sm, textarea.ds-input-sm { min-height: 0; }
84
89
  textarea.ds-input { resize: vertical; min-height: 88px; line-height: 1.6; }
85
90
  .ds-error { margin-top: 6px; font-size: 12px; color: var(--color-danger); }
86
91
  /* Checkbox / radio: native control, brand-coloured, with a comfortable hit target. */
@@ -126,11 +131,11 @@
126
131
 
127
132
  /* ---- Segmented control / tabs (role=tablist) ---- */
128
133
  .ds-tabs {
129
- display: flex; gap: 6px; padding: 4px; width: fit-content; background: var(--color-surface);
134
+ display: flex; gap: 4px; padding: 3px; width: fit-content; background: var(--color-surface); /* 3 + 32 + 3 + 2 border = 40px */
130
135
  border: 1px solid var(--color-border-soft); border-radius: var(--radius-md);
131
136
  }
132
137
  .ds-tab {
133
- padding: 6px 12px; font-size: 12px; font-weight: 500; color: var(--color-ink-soft); cursor: pointer;
138
+ display: inline-flex; align-items: center; min-height: 32px; padding: 0 12px; font-size: 12px; font-weight: 500; color: var(--color-ink-soft); cursor: pointer;
134
139
  border: 0; background: transparent; border-radius: var(--radius-sm);
135
140
  transition: background-color var(--duration-fast) ease-out, color var(--duration-fast) ease-out;
136
141
  }
@@ -282,13 +287,42 @@
282
287
  @media (min-width: 1024px) { .ds-shell { grid-template-columns: 240px minmax(0, 1fr); } .ds-shell-wide { grid-template-columns: 280px minmax(0, 1fr); } }
283
288
  .ds-shell > aside { align-self: start; }
284
289
  @media (min-width: 1024px) { .ds-shell > aside { position: sticky; top: 24px; } }
285
- .ds-page-title { display: flex; flex-wrap: wrap; align-items: end; justify-content: space-between; gap: 16px; margin-bottom: 32px; }
290
+ /* Page head: the same position and type on every page. Title 36px (48px from 640px), 40px (48px) above it, optional lede and
291
+ actions on the right. Detail pages with long titles use ds-title-sm (30px / 36px). */
292
+ .ds-page-head { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 16px 24px; padding-top: 40px; padding-bottom: 24px; }
293
+ @media (min-width: 640px) { .ds-page-head { padding-top: 48px; padding-bottom: 32px; } }
294
+ .ds-title { font-family: var(--font-display); font-weight: 500; font-size: 36px; line-height: 1.15; letter-spacing: -0.025em; text-wrap: balance; }
295
+ .ds-title-sm { font-family: var(--font-display); font-weight: 500; font-size: 30px; line-height: 1.2; letter-spacing: -0.02em; text-wrap: balance; }
296
+ @media (min-width: 640px) { .ds-title { font-size: 48px; } .ds-title-sm { font-size: 36px; } }
297
+ .ds-lede { margin-top: 12px; max-width: 48rem; font-size: 14px; line-height: 1.7; color: var(--color-ink-soft); }
298
+ @media (min-width: 640px) { .ds-lede { font-size: 15px; } }
299
+ /* Search field: a ds-input with a leading icon. <div class="ds-search"><svg/><input class="ds-input" type="search"></div> */
300
+ .ds-search { position: relative; display: block; width: 100%; }
301
+ .ds-search > svg { position: absolute; left: 12px; top: 50%; width: 16px; height: 16px; transform: translateY(-50%); color: var(--color-ink-faint); pointer-events: none; }
302
+ .ds-search > .ds-input { padding-left: 38px; }
303
+ .ds-search > .ds-input::-webkit-search-cancel-button { display: none; }
286
304
 
287
305
  /* ---- Header / layout ---- */
288
306
  .ds-header { border-bottom: 1px solid var(--color-border-soft); }
307
+ /* Every header is the same height whatever sits in it (language toggle, buttons, nothing): 72px + 1px border, content centred.
308
+ The logo is always h-6 (24px). Content must not be taller than 48px. */
309
+ .ds-header-row { min-height: 72px; padding-block: 12px; }
289
310
  /* Hairline between the 大道大商 mark and the app name, so the graphic logo and the app title don't read as one phrase. */
290
311
  .ds-brand-divider { width: 1px; height: 20px; flex-shrink: 0; background: var(--color-border-strong); opacity: 0.55; }
291
- .ds-container { margin-inline: auto; width: 100%; max-width: 72rem; padding-inline: 24px; }
312
+ /* Page width: the 72rem frame (1152px, = draw.gpgb.app), gutters 16px then 24px from 640px, so 1104px of content on desktop.
313
+ Header and page content share it. Never use a narrower frame: put narrow content in a column INSIDE it (ds-measure). */
314
+ .ds-container { margin-inline: auto; width: 100%; max-width: 72rem; padding-inline: 16px; }
315
+ @media (min-width: 640px) { .ds-container { padding-inline: 24px; } }
316
+ .ds-container-wide { max-width: 80rem; } /* editors, big tables: add to ds-container */
317
+ .ds-measure { max-width: 42rem; } /* reading text / single-column form inside the frame */
318
+ /* Content + side panel. From 1024px the panel sits to the right (22rem, sticky); below that it stacks under the content. */
319
+ .ds-split { display: grid; gap: 24px; }
320
+ @media (min-width: 1024px) {
321
+ .ds-split { grid-template-columns: minmax(0, 1fr) 22rem; gap: 32px; }
322
+ .ds-split > aside { position: sticky; top: 24px; align-self: start; }
323
+ }
324
+ /* Responsive card grid: as many columns as fit at >=18rem. */
325
+ .ds-grid-cards { display: grid; gap: 16px; grid-template-columns: repeat(auto-fill, minmax(min(100%, 18rem), 1fr)); }
292
326
  /* Underline draws in from the left on hover. */
293
327
  .ds-link {
294
328
  color: var(--color-ink-soft); text-decoration: none; background: linear-gradient(currentColor, currentColor) 0 100% / 0 1px no-repeat;
@@ -337,9 +371,9 @@
337
371
 
338
372
  /* Touch: >=16px inputs (iOS zoom) and 44px tap targets; fine pointers keep compact sizes. */
339
373
  @media (pointer: coarse) {
340
- .ds-input, .ds-input-sm { font-size: 16px; padding-top: 11px; padding-bottom: 11px; }
341
- .ds-btn { min-height: 44px; }
342
- .ds-tab { min-height: 40px; }
374
+ .ds-input, .ds-input-sm { font-size: 16px; min-height: 44px; }
375
+ .ds-btn { min-height: max(44px, var(--ds-h)); }
376
+ .ds-tab { min-height: 36px; }
343
377
  .ds-menu-item { min-height: 44px; }
344
378
  .ds-nav-link { min-height: 40px; }
345
379
  }