@vibeuncle/gpgb-ui 0.3.0 → 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,11 @@
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
+
5
10
  ## 0.3.0
6
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>`.
7
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.
package/DESIGN.md CHANGED
@@ -187,6 +187,8 @@ Rule: no raw hex in app code; no Tailwind red/gray/orange.
187
187
 
188
188
  ## Layout
189
189
 
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
+
190
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.
191
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.
192
194
  - 4px spacing base. Card padding 16 to 24px; field gap 16px; button icon gap 8px.
package/DESIGN_SYSTEM.md CHANGED
@@ -105,6 +105,21 @@ 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
+
108
123
  ## Page width
109
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.
110
125
  - **Standard:** `ds-container` (72rem). Galleries, dashboards, chat, lists, forms-with-context.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeuncle/gpgb-ui",
3
- "version": "0.3.0",
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": {
@@ -22,18 +22,17 @@ 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
  }
@@ -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
+ }
@@ -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,10 +287,26 @@
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
312
  /* Page width: the 72rem frame (1152px, = draw.gpgb.app), gutters 16px then 24px from 640px, so 1104px of content on desktop.
@@ -350,9 +371,9 @@
350
371
 
351
372
  /* Touch: >=16px inputs (iOS zoom) and 44px tap targets; fine pointers keep compact sizes. */
352
373
  @media (pointer: coarse) {
353
- .ds-input, .ds-input-sm { font-size: 16px; padding-top: 11px; padding-bottom: 11px; }
354
- .ds-btn { min-height: 44px; }
355
- .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; }
356
377
  .ds-menu-item { min-height: 44px; }
357
378
  .ds-nav-link { min-height: 40px; }
358
379
  }