@q1travel/design-system 0.28.1 → 0.29.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.
@@ -105,7 +105,7 @@ Timeline / TimelineItem (ol/li, 8px token dot or Avatar), Marker (quiet actor/ti
105
105
 
106
106
  ## ToggleGroup selection ownership
107
107
 
108
- Accepted 2026-10-07: public `ToggleGroup` uses the shadcn/Radix name (no corresponding industry-common component). Its `items` API exposes `value`, `label`, optional `icon`, `disabled`, and `ariaLabel`; icon-only items require an icon and `ariaLabel`. `type="single"` accepts string `value` / `defaultValue` and `onValueChange`; `allowEmpty` defaults to false. `type="multiple"` accepts string arrays. Controlled values remain caller-owned; uncontrolled single selection falls back to the first enabled item when its value is absent or unknown and empty selection is disallowed. Disabling an existing selected item retains its pressed state and moves only the tabstop. No existing Switch consumer is migrated.
108
+ Accepted 2026-10-07: public `ToggleGroup` names a group of pressable controls with single or multiple selection. Its `items` API exposes `value`, `label`, optional `icon`, `disabled`, and `ariaLabel`; icon-only items require an icon and `ariaLabel`. `type="single"` accepts string `value` / `defaultValue` and `onValueChange`; `allowEmpty` defaults to false. `type="multiple"` accepts string arrays. Controlled values remain caller-owned; uncontrolled single selection falls back to the first enabled item when its value is absent or unknown and empty selection is disallowed. Disabling an existing selected item retains its pressed state and moves only the tabstop. No existing Switch consumer is migrated.
109
109
 
110
110
  Native buttons expose `aria-pressed` within `role="group"`. Roving tabindex and Arrow / Home / End move focus without selecting; Enter / Space activate through native clicks. Disabled items are skipped. Button-aligned sm32 / md40 / lg48 sizes use existing tokens, quiet selected background and primary text without a selected ring, shared focus shadow, and the shared coarse-pointer 44px TouchTarget with reserved neighbor space. The Tier A keyboard contract lives on `/components/toggle-group`; Switch retains radio semantics and Tabs retains panel semantics.
111
111
  ## Checkbox task completion circle
@@ -113,7 +113,7 @@ Native buttons expose `aria-pressed` within `role="group"`. Roving tabindex and
113
113
  - Documentation: `src/pages/CheckboxPage.tsx`, `CheckboxCircleDemo.tsx`, `src/styles/checkbox-circle-specimen.css`; circle completion is restricted to tasks/checklists and row completion treatment stays consumer-owned.
114
114
  - Verification: `scripts/check-checkbox-circle-contract.test.mjs`, `scripts/fixtures/checkbox-circle-api-consumer.tsx`, `scripts/lib/checkbox-circle-contrast.mjs`, `scripts/lib/check-checkbox-circle-browser.mjs`. Package gate covers API/default/native markup/readonly semantics/token existence and both-theme contrast; browser gate covers native toggles, focus without preview, readonly, tone fill, reduced motion and coarse hit targets.
115
115
  ## Chat messages
116
- Bubble is separate from Message: chat has optional identity and direction, while Message retains its required author, comment attachments and receipt API unchanged. MessageScroller is separate from Scroller: keyed prepend anchors and message following need ownership of the viewport; the general Scroller API remains unchanged. The shadcn chat reference supplies conversation intent only; quiet chat surfaces map to existing q1travel panel/selected backgrounds, borders, spacing, radii and Button. No reference CSS, motion background or private tokens are shipped.
116
+ Bubble is separate from Message: chat has optional identity and direction, while Message retains its required author, comment attachments and receipt API unchanged. MessageScroller is separate from Scroller: keyed prepend anchors and message following need ownership of the viewport; the general Scroller API remains unchanged. Quiet chat surfaces use existing q1travel panel/selected backgrounds, borders, spacing, radii and Button. No reference CSS, motion background or private tokens are shipped.
117
117
  The consumer mapping was checked against downstream chat-style implementations (read-only, no downstream edits).
118
118
  | Web capability | Package support / caller boundary |
119
119
  | --- | --- |
@@ -26,7 +26,7 @@ Adding a clause therefore means answering two questions before the wording: who
26
26
  - [both] MUST keep mobile control targets at least 44px where touch is expected.
27
27
  - [both] MUST use `--q1-touch-target-min` (44px) for the shared centered invisible child-span hit region of Button (all sizes, including compact consumer overrides and icon-only), CopyButton, Menu/DotsMenu triggers, Description info/hint buttons, and the independent FilterChip picker/clear buttons only under `(pointer: coarse), (any-pointer: coarse)`. Preserve every existing coarse-pointer visual-size rule. Only axes still below the minimum receive extra hit area; existing 44px targets do not expand outside their visual frame. The region covers the complete border box: canonical Button/CopyButton/Menu/DotsMenu borders are 1px, while Description borders are none. Consumers overriding a control border width must also set its private `--touch-target-border-width` to the actual per-side width (including zero for `border: none`); asymmetric borders need their own hit-region sizing. FilterChip keeps its default sm32 visual height and existing gaps/margins; its picker and clear hit regions must not overlap. Menu item rows use a real `min-height: var(--q1-touch-target-min)` under the same coarse-pointer query so neighboring rows remain independent. Focus rings stay on the visual control; child hit-region events activate that native control without replacing its pseudo-elements or overflow rules.
28
28
  - [both] MUST leave enough space for expanded targets: dense neighbors must not overlap each other's visual frames. Consumers must reserve the expansion on each side and avoid clipping ancestors; if this is impossible, regroup controls or use larger visual controls. The shared region cannot infer neighbor gaps or escape control/ancestor clipping; clipped compact controls must use a visual size of at least 44px.
29
- - [both] SHOULD NOT ship dense measured 24–32px controls to coarse pointers without enlargement: Apple HIG's 44pt guidance and WCAG 2.5.5 motivate q1travel's 44×44 CSS-pixel mobile usability target; pt and CSS pixels are different units.
29
+ - [both] SHOULD NOT ship dense measured 24–32px controls to coarse pointers without enlargement: q1travel uses a 44×44 CSS-pixel mobile usability target, informed by WCAG 2.5.5.
30
30
  - [both] NEVER disable browser zoom.
31
31
  - [both] MUST avoid dead zones: if a region looks clickable, the clickable element must cover it.
32
32
  - [both] MUST show a loading indicator on in-flight buttons while keeping the original label.
@@ -191,7 +191,7 @@ Theming owns tokens and `color-scheme`; the interaction-contrast rule lives in D
191
191
  - [both] MUST separate Chinese characters from adjacent Latin letters or digits with a half-width space in authored prose; code contents, URLs, literal UI quotations, and code samples are excluded.
192
192
  - [both] MUST use full-width `,。:;` immediately after Chinese characters in authored Chinese sentences; code contents, URLs, literal UI quotations, and code samples are excluded.
193
193
  - [both] SHOULD use full-width `,。:;` throughout Chinese sentences, including after Latin words or inline code; identifying sentence language across mixed JSX nodes remains an editorial review item.
194
- - [both] SHOULD follow common Chinese technical-writing practice for mixed-language wording; Microsoft's Chinese localization guidance is a reference example, not a proprietary q1travel rule. Semantic choices and text whose rendered role cannot be reliably inferred remain editorial review items rather than machine-enforced requirements.
194
+ - [both] SHOULD follow common Chinese technical-writing practice for mixed-language wording. Semantic choices and text whose rendered role cannot be reliably inferred remain editorial review items rather than machine-enforced requirements.
195
195
  - [both] SHOULD prefer short action-oriented labels.
196
196
  - [both] MUST use active voice.
197
197
  - [both] MUST use unambiguous action labels (「保存 API Key」, not 「继续」).
@@ -213,7 +213,7 @@ These copy clauses keep reader address and empty states consistent with a formal
213
213
 
214
214
  ### Component Naming
215
215
 
216
- - [repo] SHOULD use the industry-common component name when one exists, preferring shadcn/ui, then Radix. Invent a name only when neither has one. Name what the component is, not how it is built.
216
+ - [repo] SHOULD use the industry-common component name when one exists. Invent a name only when no established name fits. Name what the component is, not how it is built.
217
217
  - [repo] MUST switch public names directly to the new export without deprecated aliases or old documentation route redirects; record the removal as a breaking change in CHANGELOG.
218
218
 
219
219
  ### Component Documentation Pages
@@ -1578,7 +1578,7 @@ export declare type MultiSelectProps = {
1578
1578
  preserveSelectionOrder?: boolean;
1579
1579
  };
1580
1580
 
1581
- export declare function Navigation({ className, orientation, ...props }: NavigationProps): JSX.Element;
1581
+ export declare function Navigation({ className, orientation, variant, ...props }: NavigationProps): JSX.Element;
1582
1582
 
1583
1583
  export declare function NavigationGroup({ label, icon, expanded, defaultExpanded, onExpandedChange, children, }: NavigationGroupProps): JSX.Element;
1584
1584
 
@@ -1617,6 +1617,8 @@ export declare type NavigationItemProps = NavigationItemAsLink | NavigationItemA
1617
1617
  export declare type NavigationProps = HTMLAttributes<HTMLElement> & {
1618
1618
  'aria-label': string;
1619
1619
  orientation?: 'vertical' | 'horizontal';
1620
+ /** Line styling applies only to horizontal navigation; vertical navigation ignores it. */
1621
+ variant?: 'default' | 'line';
1620
1622
  };
1621
1623
 
1622
1624
  export declare function NavigationSection({ label, action, children, className, ...props }: NavigationSectionProps): JSX.Element;
@@ -5987,16 +5987,17 @@ function cu({
5987
5987
  ) : null
5988
5988
  ] });
5989
5989
  }
5990
- function lu({ className: e, orientation: t = "vertical", ...r }) {
5990
+ function lu({ className: e, orientation: t = "vertical", variant: r = "default", ...o }) {
5991
5991
  return /* @__PURE__ */ n(
5992
5992
  "nav",
5993
5993
  {
5994
5994
  className: se(
5995
5995
  "q1-navigation",
5996
5996
  t === "horizontal" && "q1-navigation--horizontal",
5997
+ t === "horizontal" && r === "line" && "q1-navigation--line",
5997
5998
  e
5998
5999
  ),
5999
- ...r
6000
+ ...o
6000
6001
  }
6001
6002
  );
6002
6003
  }
@@ -9845,7 +9845,7 @@ button.q1-navigation__item:not(:disabled),
9845
9845
  }
9846
9846
 
9847
9847
  .q1-navigation__item--active {
9848
- background: var(--q1-color-bg-hover);
9848
+ background: var(--q1-color-gray-alpha-200);
9849
9849
  color: var(--q1-color-text-primary);
9850
9850
  font-weight: var(--q1-font-weight-medium);
9851
9851
  }
@@ -10034,15 +10034,20 @@ a.q1-navigation__item[aria-disabled='true'] {
10034
10034
  background-color var(--q1-duration-press) var(--q1-ease-state);
10035
10035
  }
10036
10036
 
10037
+ /* 滚动容器内沿用 Table 的内嵌焦点环,避免四边被 overflow 裁切。 */
10038
+ .q1-navigation--horizontal .q1-navigation__item:focus-visible {
10039
+ box-shadow: inset 0 0 0 calc(var(--q1-space-1) / 2) var(--q1-color-focus);
10040
+ }
10041
+
10037
10042
  .q1-navigation--horizontal .q1-navigation__item--active {
10038
- background: var(--q1-color-bg-hover);
10043
+ background: var(--q1-color-gray-alpha-200);
10039
10044
  color: var(--q1-color-text-primary);
10040
10045
  }
10041
10046
 
10042
10047
  @media (hover: hover) and (pointer: fine) {
10043
- .q1-navigation__item:hover:not(:active):not([aria-disabled='true']):not(:disabled),
10048
+ .q1-navigation__item:hover:not(.q1-navigation__item--active):not(:active):not([aria-disabled='true']):not(:disabled),
10044
10049
  .q1-navigation__group-trigger:hover:not(:active) {
10045
- background: var(--q1-color-bg-subtle);
10050
+ background: var(--q1-color-bg-hover);
10046
10051
  }
10047
10052
 
10048
10053
  .q1-navigation__item-row:hover .q1-navigation__item-action {
@@ -10053,11 +10058,12 @@ a.q1-navigation__item[aria-disabled='true'] {
10053
10058
 
10054
10059
  .q1-navigation--horizontal .q1-navigation__item:hover:not(:active):not([aria-disabled='true']):not(:disabled) {
10055
10060
  color: var(--q1-color-text-primary);
10056
- background: var(--q1-color-bg-subtle);
10061
+ background: var(--q1-color-bg-hover);
10057
10062
  }
10058
10063
 
10059
10064
  .q1-navigation--horizontal .q1-navigation__item--active:hover:not(:active):not([aria-disabled='true']):not(:disabled) {
10060
10065
  color: var(--q1-color-text-primary);
10066
+ background: var(--q1-color-gray-alpha-200);
10061
10067
  }
10062
10068
  }
10063
10069
 
@@ -11266,6 +11272,71 @@ a.q1-navigation__item[aria-disabled='true'] {
11266
11272
  display: none;
11267
11273
  }
11268
11274
 
11275
+ .q1-navigation--line {
11276
+ --q1-navigation-row-min-height: calc(var(--q1-space-10) + var(--q1-space-1));
11277
+
11278
+ scrollbar-width: none;
11279
+ align-items: stretch;
11280
+ gap: var(--q1-space-6);
11281
+ /* 基线不占布局空间,绘于子项下方;滚动时下划线仍压在同一底边。 */
11282
+ box-shadow: inset 0 calc(var(--q1-space-1) / -4) 0 var(--q1-color-border);
11283
+ }
11284
+
11285
+ .q1-navigation--line::-webkit-scrollbar {
11286
+ display: none;
11287
+ }
11288
+
11289
+ .q1-navigation--line .q1-navigation__item {
11290
+ position: relative;
11291
+ border-bottom: calc(var(--q1-space-1) / 2) solid transparent;
11292
+ border-radius: 0;
11293
+ padding: var(--q1-space-3) var(--q1-space-2);
11294
+ background: none;
11295
+ color: var(--q1-color-text-secondary);
11296
+ font-weight: var(--q1-font-weight-regular);
11297
+ transition: color var(--q1-duration-press) var(--q1-ease-state);
11298
+ }
11299
+
11300
+ /* 焦点环仅纵向收进项内,底边与选中下划线保留一档间距。 */
11301
+ .q1-navigation--horizontal.q1-navigation--line .q1-navigation__item:focus-visible {
11302
+ box-shadow: none;
11303
+ }
11304
+
11305
+ .q1-navigation--line .q1-navigation__item:focus-visible::after {
11306
+ content: '';
11307
+ position: absolute;
11308
+ inset: var(--q1-space-1) 0;
11309
+ border-radius: var(--q1-radius-2);
11310
+ box-shadow: inset 0 0 0 calc(var(--q1-space-1) / 2) var(--q1-color-focus);
11311
+ pointer-events: none;
11312
+ }
11313
+
11314
+ .q1-navigation--line .q1-navigation__item:is(.q1-navigation__item--active, [aria-current='page']) {
11315
+ border-bottom-color: var(--q1-color-text-primary);
11316
+ color: var(--q1-color-text-primary);
11317
+ }
11318
+
11319
+ .q1-navigation--line .q1-navigation__item--disabled {
11320
+ color: var(--q1-color-text-muted);
11321
+ }
11322
+
11323
+ @media (hover: hover) and (pointer: fine) {
11324
+ .q1-navigation--horizontal.q1-navigation--line .q1-navigation__item:hover:not(:active):not([aria-disabled='true']):not(:disabled) {
11325
+ background: none;
11326
+ color: var(--q1-color-text-primary);
11327
+ }
11328
+ }
11329
+
11330
+ .q1-navigation--line .q1-navigation__item:active:not([aria-disabled='true']):not(:disabled) {
11331
+ background: none;
11332
+ }
11333
+
11334
+ @media (prefers-reduced-motion: reduce) {
11335
+ .q1-navigation--line .q1-navigation__item {
11336
+ transition: none;
11337
+ }
11338
+ }
11339
+
11269
11340
  /* Calendar grid rows, focus treatment, touch targets, and RangeField entrance motion. */
11270
11341
  .calendar__weekdays,
11271
11342
  .calendar__row {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@q1travel/design-system",
3
- "version": "0.28.1",
3
+ "version": "0.29.0",
4
4
  "description": "React components, design tokens, and interface contracts for q1travel products.",
5
5
  "keywords": [
6
6
  "react",