@q1travel/design-system 0.8.0 → 0.9.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.
@@ -33,6 +33,50 @@ The `0.8.0` candidate freezes two independent product models:
33
33
 
34
34
  TypeScript is the public input boundary for the mutually exclusive models. The package does not own q1travel-web data fetching, query-string state, router navigation, or cursor token parsing.
35
35
 
36
+ ## #111 Shared slots and loading accessibility
37
+
38
+ Status: accepted locally
39
+
40
+ Decision date: 2026-08-07
41
+
42
+ This contract applies to the current known-total and unknown-tail models. A future true-cursor model must reuse these shared semantics, but this decision does not add a cursor prop or infer cursor ownership before #110 supplies a real consumer and request boundary.
43
+
44
+ ### Shared public types
45
+
46
+ `PaginationCommonProps` is the named public type for props shared by every product-pagination model. `PaginationSummaryArgs` remains the public callback input type. The model-specific types extend the common type and remain mutually exclusive.
47
+
48
+ ### Summary
49
+
50
+ - The summary is rendered before the controls inside the named pagination `nav`.
51
+ - The summary uses `aria-live="polite"` and `aria-atomic="true"`. Page changes and loading completion may update this region; consumer-provided text must be concise and stable enough to announce.
52
+ - A summary callback receives `page`, `pageSize`, `total`, `totalPages`, `start`, and `end`. Unknown-tail supplies no fabricated `total` or `totalPages` and uses `start: 0` and `end: 0` unless the consumer supplies a different summary node.
53
+ - The package owns the default summary formatting, but does not own data-fetching status or consumer-specific result wording.
54
+
55
+ ### `pageSizeSlot`
56
+
57
+ - `pageSizeSlot` is a consumer-owned `ReactNode`, rendered before the pagination buttons in the controls region.
58
+ - The package positions the node but does not create, control, label, or synchronize a page-size control.
59
+ - If the slot contains an interactive control, the consumer must provide a programmatic label association, such as a `label`/`htmlFor` pair or `aria-label`/`aria-labelledby`. A bare text wrapper is not an accessible label contract.
60
+ - The slot may wrap in compact or narrow layouts; the consumer remains responsible for the control's value, state, and update handling.
61
+
62
+ ### Loading, focus, and completion
63
+
64
+ - While `loading` is true, the pagination `nav` exposes `aria-busy="true"` and the existing Spinner exposes the polite status text `加载中…`.
65
+ - Structural boundary controls and empty-state controls remain natively disabled.
66
+ - Actionable pagination controls remain focusable while loading and expose `aria-disabled="true"`. Their click and keyboard handlers are inert until loading ends. This preserves the user's current focus rather than moving focus to a newly rendered control.
67
+ - The current-page marker and `aria-current="page"` remain stable while loading.
68
+ - After a real loading-to-idle transition, a separate polite, atomic completion region announces `分页已更新`. The summary may additionally announce the new range or page.
69
+
70
+ ### Responsive and unavailable states
71
+
72
+ - `compact` and the existing tokenized responsive layout own dense and narrow presentation; the shared semantics do not change at smaller widths.
73
+ - `total={0}` exposes the existing `暂无数据` summary and does not fabricate page numbers.
74
+ - Error or unavailable presentation is consumer-owned composition around `Pagination` (for example, an adjacent alert and a disabled/empty pagination state); #111 does not add an `error` or `unavailable` prop.
75
+
76
+ ### Required evidence
77
+
78
+ The package contract must be backed by generated public declarations, a TypeScript consumer fixture, static markup assertions for summary/slot/busy semantics, and browser assertions for native keyboard activation, visible focus, loading `aria-disabled`, focus retention, completion announcement, empty state, and labelled page-size controls. Web migration, true-cursor adoption, publication, and issue closure remain independent gates.
79
+
36
80
  ## Route and link ownership
37
81
 
38
82
  The documentation route `/components/pagination` is retained. It documents both the product `Pagination` boundary and the separate `DocsPagination` sequence-navigation boundary.
@@ -0,0 +1,168 @@
1
+ # Table 公共合同
2
+
3
+ Status: accepted locally
4
+
5
+ Decision date: 2026-08-08
6
+
7
+ Related: q1travel-design-system#25
8
+
9
+ This contract freezes the first public boundary for a shared `Table`. It records
10
+ the behavior and ownership decisions needed before implementation; it does not
11
+ implement the component, change q1travel-web, create migration tickets, or
12
+ claim consumer adoption.
13
+
14
+ ## Public boundary
15
+
16
+ `Table` is a consumer-controlled, native-semantic table. It renders the rows and
17
+ columns supplied by the consumer, but it does not fetch data, own router state,
18
+ infer a data-set selection, or implement pagination.
19
+
20
+ The first implementation is split into three responsibilities:
21
+
22
+ | Responsibility | Frozen public boundary | Ownership |
23
+ | --- | --- | --- |
24
+ | Core | `columns`, `data`, stable `rowKey`, native `table` / `thead` / `tbody` / `th` semantics, composed rows, and consumer-owned loading/empty/error content | Table owns structure and semantics; the consumer owns data and state content |
25
+ | Interaction | Controlled selection, current-page select-all, bulk actions, action columns, and keyboard-operable row activation | The consumer owns selected state and action effects; Table owns selection wiring and accessible interaction states |
26
+ | Layout and data behavior | Controlled sorting with `aria-sort`, `stickyHeader`, `maxBodyHeight`, explicit density, `minWidth`, column-group composition, `renderRow`, and `showHeader` | Table owns layout wiring and state attributes; the consumer owns sort state, render decisions, and data ordering |
27
+
28
+ The public component must not require a business-specific workflow, request
29
+ library, router, or product-side data model.
30
+
31
+ ## Core contract
32
+
33
+ - `columns` and `data` are consumer-owned inputs. A column identifies its
34
+ heading and cell rendering without requiring Table to know the domain model.
35
+ - `rowKey` is a stable consumer-owned key. Table must not use the array index as
36
+ the identity for selection, updates, or row activation.
37
+ - The default structure uses native table elements and `th scope="col"`.
38
+ Table does not promote the surface to `role="grid"` or add arrow-key grid
39
+ navigation in this contract.
40
+ - `renderRow` and composed cells may extend row presentation, but they must
41
+ preserve the table's heading, cell, focus, and selection semantics.
42
+ - Loading, empty, and error states are explicit consumer-owned surfaces. Table
43
+ must not invent fetch status, error copy, recovery actions, or business
44
+ permissions. An empty result may be rendered as an adjacent Empty State
45
+ rather than as an artificial table row.
46
+ - `showHeader` is a presentation option, not permission to remove the
47
+ accessible relationship between cells and their column headings. A consumer
48
+ that hides the visual header remains responsible for an accessible table
49
+ name and header semantics.
50
+
51
+ The current documentation-only `striped`, `bordered`, `divided`, `interactive`,
52
+ and virtualized specimens do not automatically become public variant props.
53
+ They remain evidence and comparison material until a consumer-backed contract
54
+ requires each capability.
55
+
56
+ ## Controlled interaction contract
57
+
58
+ ### Selection
59
+
60
+ - Selection is controlled by the consumer. Table receives the selected row keys
61
+ and reports the next selected-key collection through its change callback; it
62
+ does not keep an independent authoritative selection store.
63
+ - A row is selectable only when the consumer exposes it as selectable. Selection
64
+ controls use native checkboxes with accessible names and a visible
65
+ `:focus-visible` state.
66
+ - The header select-all control operates on the currently rendered/selectable
67
+ page only.
68
+ - Selecting all adds the selectable keys visible on the current page to the
69
+ consumer's existing selected keys. Clearing all removes only those current
70
+ page keys. Keys belonging to other pages are preserved in both operations.
71
+ - The header state is derived from current-page selectable rows: unchecked when
72
+ none are selected, mixed when only some are selected, and checked when all
73
+ current-page selectable rows are selected.
74
+ - Table must not infer that the entire data set is selected, emit a hidden
75
+ "select all pages" sentinel, fetch other pages, or rewrite keys it cannot see.
76
+ - Bulk actions and action-column content are consumer-provided. Table may
77
+ expose the current selection boundary to the action surface, but it does not
78
+ execute business mutations or decide confirmation, retry, or success copy.
79
+
80
+ ### Row activation
81
+
82
+ - A consumer may provide row activation. The activation target must be
83
+ keyboard-operable with the same outcome as pointer activation and must expose
84
+ a visible focus state.
85
+ - Enter and Space activation must not be implemented by turning the entire
86
+ table into a grid. Arrow-key row navigation, roving focus, and cell editing
87
+ are outside this first contract.
88
+ - A row action must not conflict with a nested native link, button, checkbox,
89
+ or other interactive cell control; nested controls retain their native
90
+ behavior.
91
+
92
+ ### Sorting
93
+
94
+ - Sorting is controlled: the consumer owns the active sort key and direction,
95
+ receives the sort-change event, and owns data ordering or server requests.
96
+ - A sortable heading is a native button inside the heading cell. The heading
97
+ exposes `aria-sort` with the applicable `none`, `ascending`, or `descending`
98
+ value. The sort icon is decorative and is not the only state cue.
99
+ - Table does not silently sort data, fetch a new page, mutate URL/query state,
100
+ or fabricate a total while sorting.
101
+
102
+ ## Layout and responsive contract
103
+
104
+ - `density` is explicit consumer input; Table must not infer density from a
105
+ parent page or from being rendered inside a data surface.
106
+ - `stickyHeader` and `maxBodyHeight` are layout capabilities. When they create a
107
+ scroll region, the region must remain usable with keyboard focus and must not
108
+ create an unnecessary second scrollbar.
109
+ - `minWidth` and column-group composition are consumer-visible layout inputs;
110
+ intrinsic table sizing remains the default and JavaScript measurement is not
111
+ required for normal layout.
112
+ - The implementation must remain usable at small, laptop, desktop, and wide
113
+ widths, with horizontal overflow owned by the table surface when columns
114
+ cannot fit.
115
+ - Light and dark themes use canonical q1travel tokens. No Web page-local
116
+ `--ds-*` token becomes a new public Table token.
117
+
118
+ ## Accessibility and state evidence
119
+
120
+ The component follows the existing native-semantic accessibility bar:
121
+
122
+ - native table structure and column-heading relationships;
123
+ - native buttons, links, and checkboxes for interactive cells;
124
+ - visible focus for every focusable control;
125
+ - accessible names for select-all, row, and action controls;
126
+ - redundant selected, sorted, loading, empty, and error cues;
127
+ - consumer-owned async announcements and recovery actions where state changes.
128
+
129
+ The implementation gate must add generated public declarations, a TypeScript
130
+ consumer fixture, package-level markup assertions, and browser evidence for:
131
+
132
+ 1. controlled selection and current-page select-all while preserving off-page
133
+ keys;
134
+ 2. indeterminate and checked header states;
135
+ 3. sortable headings and `aria-sort` transitions through native keyboard input;
136
+ 4. row activation, nested-control isolation, focus visibility, and disabled
137
+ rows/controls where applicable;
138
+ 5. loading, empty, error, dense, narrow, light, and dark states; and
139
+ 6. sticky/max-height behavior without losing focus or introducing an unwanted
140
+ scroll trap.
141
+
142
+ The existing Table documentation page and its browser checks prove only the
143
+ page-local specimen. They are not package or consumer evidence for this
144
+ contract.
145
+
146
+ ## Pagination and migration boundary
147
+
148
+ - `Table` receives the visible data it must render and does not own `page`,
149
+ `pageSize`, `total`, cursor tokens, query-string state, or data fetching.
150
+ - Dataset pagination is composed with the public `Pagination` component;
151
+ documentation sequence navigation remains the separate `DocsPagination`
152
+ boundary.
153
+ - The current-page select-all rule remains valid when the consumer changes
154
+ pages: off-page selected keys stay in the consumer's controlled collection,
155
+ while the header state is recalculated from the newly visible page.
156
+ - q1travel-web migration is a separate gate. It may begin only after the
157
+ package export, generated types, focused tests, published package identity,
158
+ and consumer adoption have each been independently verified.
159
+ - This contract does not modify q1travel-web, publish the package, migrate a
160
+ consumer, close #25, or create implementation issues.
161
+
162
+ ## Implementation sequence
163
+
164
+ `Table core` → `Interaction` → `Layout` → `export/types/tests` → `published
165
+ package verification` → `q1travel-web migration`
166
+
167
+ Each arrow is a separate acceptance gate. A passing documentation specimen,
168
+ local package build, or source-level assertion does not establish the next gate.
@@ -1,6 +1,7 @@
1
1
  import { AnchorHTMLAttributes } from 'react';
2
2
  import { ButtonHTMLAttributes } from 'react';
3
3
  import { ComponentPropsWithoutRef } from 'react';
4
+ import { CSSProperties } from 'react';
4
5
  import { FocusEventHandler } from 'react';
5
6
  import { HTMLAttributes } from 'react';
6
7
  import { InputHTMLAttributes } from 'react';
@@ -12,6 +13,7 @@ import { ReactNode } from 'react';
12
13
  import { Ref } from 'react';
13
14
  import { RefObject } from 'react';
14
15
  import { SelectHTMLAttributes } from 'react';
16
+ import { TableHTMLAttributes } from 'react';
15
17
  import { TextareaHTMLAttributes } from 'react';
16
18
 
17
19
  export declare function addDays(date: Date, amount: number): Date;
@@ -1023,7 +1025,7 @@ export declare type NoteTone = 'info' | 'success' | 'warning' | 'error' | 'secon
1023
1025
 
1024
1026
  export declare function Pagination({ page, pageSize: pageSizeProp, total, totalPages: totalPagesProp, hasNextPage, onChange, loading, showPageNumbers, summary, pageSizeSlot, prevLabel, nextLabel, compact, className, }: PaginationProps): JSX.Element;
1025
1027
 
1026
- declare type PaginationCommonProps = {
1028
+ export declare type PaginationCommonProps = {
1027
1029
  page: number;
1028
1030
  pageSize?: number;
1029
1031
  onChange: (page: number) => void;
@@ -1359,6 +1361,70 @@ export declare type TabItem = {
1359
1361
  tooltip?: string;
1360
1362
  };
1361
1363
 
1364
+ export declare function Table<T>({ data, columns, rowKey, caption, density, stickyHeader, maxBodyHeight, minWidth, colGroup, renderRow, showHeader, sortKey, sortDirection, onSortChange, selection, actions, bulkActions, bulkActionsLabel, rowActivation, className: consumerClassName, style: consumerStyle, ...nativeProps }: TableProps<T>): JSX.Element;
1365
+
1366
+ export declare type TableActions<T> = {
1367
+ header: ReactNode;
1368
+ render: (row: T, rowIndex: number) => ReactNode;
1369
+ };
1370
+
1371
+ export declare type TableColumn<T> = {
1372
+ key: string;
1373
+ header: ReactNode;
1374
+ render: (row: T, rowIndex: number) => ReactNode;
1375
+ sortable?: boolean;
1376
+ sortLabel?: string;
1377
+ };
1378
+
1379
+ export declare type TableDensity = 'comfortable' | 'compact';
1380
+
1381
+ export declare type TableProps<T> = {
1382
+ data: readonly T[];
1383
+ columns: readonly TableColumn<T>[];
1384
+ rowKey: (row: T, rowIndex: number) => TableRowKey;
1385
+ caption?: ReactNode;
1386
+ density?: TableDensity;
1387
+ stickyHeader?: boolean;
1388
+ maxBodyHeight?: CSSProperties['maxHeight'];
1389
+ minWidth?: CSSProperties['minWidth'];
1390
+ colGroup?: ReactNode;
1391
+ renderRow?: (row: T, context: TableRowRenderContext<T>) => ReactNode;
1392
+ showHeader?: boolean;
1393
+ sortKey?: string;
1394
+ sortDirection?: TableSortDirection;
1395
+ onSortChange?: (key: string, direction: TableSortDirection) => void;
1396
+ selection?: TableSelection<T>;
1397
+ actions?: TableActions<T>;
1398
+ bulkActions?: ReactNode;
1399
+ bulkActionsLabel?: string;
1400
+ rowActivation?: TableRowActivation<T>;
1401
+ } & Omit<TableHTMLAttributes<HTMLTableElement>, 'children'>;
1402
+
1403
+ export declare type TableRowActivation<T> = {
1404
+ getRowLabel: (row: T, rowIndex: number) => string;
1405
+ onActivate: (row: T, rowIndex: number) => void;
1406
+ };
1407
+
1408
+ export declare type TableRowKey = string | number;
1409
+
1410
+ export declare type TableRowRenderContext<T> = {
1411
+ row: T;
1412
+ rowIndex: number;
1413
+ rowKey: TableRowKey;
1414
+ rowProps: TableHTMLAttributes<HTMLTableRowElement>;
1415
+ cells: ReactNode;
1416
+ };
1417
+
1418
+ export declare type TableSelection<T> = {
1419
+ selectedKeys: readonly TableRowKey[];
1420
+ onSelectionChange: (selectedKeys: TableRowKey[]) => void;
1421
+ getRowLabel: (row: T, rowIndex: number) => string;
1422
+ getRowDisabled?: (row: T, rowIndex: number) => boolean;
1423
+ selectAllLabel?: string;
1424
+ };
1425
+
1426
+ export declare type TableSortDirection = 'asc' | 'desc';
1427
+
1362
1428
  export declare function Tabs({ tabs, ariaLabel, variant, }: {
1363
1429
  tabs: TabItem[];
1364
1430
  ariaLabel: string;