@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 +9 -0
- package/DESIGN.md +5 -1
- package/DESIGN_SYSTEM.md +27 -2
- package/package.json +1 -1
- package/src/components/Layout.tsx +17 -13
- package/src/components/SearchInput.tsx +13 -0
- package/src/components/SiteHeader.tsx +2 -2
- package/src/index.ts +1 -0
- package/styles/components.css +47 -13
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
|
-
-
|
|
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
|
|
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,
|
|
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
|
@@ -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
|
|
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=
|
|
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
|
-
/**
|
|
26
|
-
|
|
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="
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<
|
|
32
|
-
|
|
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).
|
|
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
|
|
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
package/styles/components.css
CHANGED
|
@@ -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:
|
|
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:
|
|
50
|
-
.ds-btn-lg { padding:
|
|
51
|
-
.ds-btn-icon { padding:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
341
|
-
.ds-btn { min-height: 44px; }
|
|
342
|
-
.ds-tab { min-height:
|
|
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
|
}
|