@ds-mo/ui 1.0.1 → 1.1.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.
Files changed (179) hide show
  1. package/dist/components/ds-bar-nav-action.js +1 -1
  2. package/dist/components/ds-bar-nav.js +1 -1
  3. package/dist/components/ds-bar-nav.js.map +1 -1
  4. package/dist/components/ds-breadcrumb.js +1 -1
  5. package/dist/components/ds-button-group.js +1 -1
  6. package/dist/components/ds-button.js +1 -1
  7. package/dist/components/ds-card.js +1 -1
  8. package/dist/components/ds-card.js.map +1 -1
  9. package/dist/components/ds-checkbox.js +1 -1
  10. package/dist/components/ds-divider.js +1 -1
  11. package/dist/components/ds-empty-state.js +1 -1
  12. package/dist/components/ds-empty-state.js.map +1 -1
  13. package/dist/components/ds-fade.js +1 -1
  14. package/dist/components/ds-field.js +1 -1
  15. package/dist/components/ds-header.js +1 -1
  16. package/dist/components/ds-header.js.map +1 -1
  17. package/dist/components/ds-icon.js +1 -1
  18. package/dist/components/ds-input.js +1 -1
  19. package/dist/components/ds-label-wrap.js +1 -1
  20. package/dist/components/ds-menu.js +1 -1
  21. package/dist/components/ds-modal.js +1 -1
  22. package/dist/components/ds-panel-nav.js +1 -1
  23. package/dist/components/ds-radio-group.js +1 -1
  24. package/dist/components/ds-radio-group.js.map +1 -1
  25. package/dist/components/ds-scrollbar.js +1 -1
  26. package/dist/components/ds-select.js +1 -1
  27. package/dist/components/ds-slider.js +1 -1
  28. package/dist/components/ds-slider.js.map +1 -1
  29. package/dist/components/ds-surface.js +1 -1
  30. package/dist/components/ds-tab-group-nav.d.ts +11 -0
  31. package/dist/components/ds-tab-group-nav.js +2 -0
  32. package/dist/components/ds-tab-group-nav.js.map +1 -0
  33. package/dist/components/ds-tab-group.js +1 -1
  34. package/dist/components/ds-tab-group.js.map +1 -1
  35. package/dist/components/ds-table.js +1 -1
  36. package/dist/components/ds-table.js.map +1 -1
  37. package/dist/components/ds-tag.js +1 -1
  38. package/dist/components/ds-text.js +1 -1
  39. package/dist/components/ds-toast-provider.js +1 -1
  40. package/dist/components/ds-toggle-button-group.js +1 -1
  41. package/dist/components/ds-toggle-button.js +1 -1
  42. package/dist/components/ds-toggle.js +1 -1
  43. package/dist/components/ds-tooltip.js +1 -1
  44. package/dist/components/p-BQq26pt9.js +2 -0
  45. package/dist/components/p-BQq26pt9.js.map +1 -0
  46. package/dist/components/p-CMyQsk0K.js +2 -0
  47. package/dist/components/p-CMyQsk0K.js.map +1 -0
  48. package/dist/components/{p-D3CwGoIu.js → p-COsKOwZR.js} +2 -2
  49. package/dist/components/p-CpbnmEu1.js +2 -0
  50. package/dist/components/p-CpbnmEu1.js.map +1 -0
  51. package/dist/components/{p-DHF5BQbO.js → p-Dd0GrKaF.js} +2 -2
  52. package/dist/components/p-H89PzqGU.js +2 -0
  53. package/dist/components/p-H89PzqGU.js.map +1 -0
  54. package/dist/components/{p-Dfx0StHj.js → p-Vwlmhl4E.js} +2 -2
  55. package/dist/components/{p-C4p1fG67.js → p-woooYulR.js} +2 -2
  56. package/dist/types/.stencil/BarNav/BarNav.d.ts +7 -2
  57. package/dist/types/.stencil/BarNav/bar-nav-dom-utils.d.ts +1 -2
  58. package/dist/types/.stencil/TabGroupNav/TabGroupNav.d.ts +27 -0
  59. package/dist/types/components.d.ts +68 -0
  60. package/package.json +39 -9
  61. package/src/angular/index.ts +1 -0
  62. package/src/angular/proxies.ts +28 -0
  63. package/src/react/components.ts +46 -0
  64. package/src/react/ds-accordion.ts +27 -0
  65. package/src/react/ds-badge.ts +26 -0
  66. package/src/react/ds-banner.ts +27 -0
  67. package/src/react/ds-bar-nav-action.ts +27 -0
  68. package/src/react/ds-bar-nav.ts +33 -0
  69. package/src/react/ds-breadcrumb.ts +27 -0
  70. package/src/react/ds-button-group.ts +27 -0
  71. package/src/react/ds-button.ts +35 -0
  72. package/src/react/ds-card.ts +26 -0
  73. package/src/react/ds-checkbox.ts +27 -0
  74. package/src/react/ds-divider.ts +26 -0
  75. package/src/react/ds-empty-state.ts +26 -0
  76. package/src/react/ds-fade.ts +26 -0
  77. package/src/react/ds-field.ts +26 -0
  78. package/src/react/ds-header.ts +26 -0
  79. package/src/react/ds-icon.ts +26 -0
  80. package/src/react/ds-input.ts +33 -0
  81. package/src/react/ds-label-wrap.ts +26 -0
  82. package/src/react/ds-loader.ts +26 -0
  83. package/src/react/ds-menu.ts +33 -0
  84. package/src/react/ds-modal.ts +27 -0
  85. package/src/react/ds-pagination.ts +27 -0
  86. package/src/react/ds-panel-nav.ts +37 -0
  87. package/src/react/ds-radio-group.ts +27 -0
  88. package/src/react/ds-scrollbar.ts +26 -0
  89. package/src/react/ds-select.ts +27 -0
  90. package/src/react/ds-sidebar.ts +33 -0
  91. package/src/react/ds-skeleton.ts +26 -0
  92. package/src/react/ds-slider.ts +27 -0
  93. package/src/react/ds-surface.ts +26 -0
  94. package/src/react/ds-tab-group-nav.ts +27 -0
  95. package/src/react/ds-tab-group.ts +27 -0
  96. package/src/react/ds-table.ts +35 -0
  97. package/src/react/ds-tag.ts +35 -0
  98. package/src/react/ds-text.ts +26 -0
  99. package/src/react/ds-toast-provider.ts +26 -0
  100. package/src/react/ds-toggle-button-group.ts +27 -0
  101. package/src/react/ds-toggle-button.ts +27 -0
  102. package/src/react/ds-toggle.ts +27 -0
  103. package/src/react/ds-tooltip.ts +26 -0
  104. package/src/wc/components/Accordion/Accordion.tsx +144 -0
  105. package/src/wc/components/Badge/Badge.tsx +34 -0
  106. package/src/wc/components/Badge/index.ts +1 -0
  107. package/src/wc/components/Banner/Banner.tsx +148 -0
  108. package/src/wc/components/BarNav/BarNav.tsx +605 -0
  109. package/src/wc/components/BarNav/bar-nav-dom-utils.ts +23 -0
  110. package/src/wc/components/BarNav/bar-nav-tabs-menu-utils.ts +59 -0
  111. package/src/wc/components/BarNav/bar-nav-utils.ts +76 -0
  112. package/src/wc/components/BarNav/index.ts +1 -0
  113. package/src/wc/components/BarNavAction/BarNavAction.tsx +67 -0
  114. package/src/wc/components/BarNavAction/index.ts +1 -0
  115. package/src/wc/components/Breadcrumb/Breadcrumb.tsx +61 -0
  116. package/src/wc/components/Button/Button.tsx +158 -0
  117. package/src/wc/components/Button/index.ts +1 -0
  118. package/src/wc/components/ButtonGroup/ButtonGroup.tsx +110 -0
  119. package/src/wc/components/Card/Card.tsx +26 -0
  120. package/src/wc/components/Checkbox/Checkbox.tsx +60 -0
  121. package/src/wc/components/Checkbox/index.ts +1 -0
  122. package/src/wc/components/Divider/Divider.tsx +23 -0
  123. package/src/wc/components/Divider/index.ts +2 -0
  124. package/src/wc/components/EmptyState/EmptyState.tsx +32 -0
  125. package/src/wc/components/Fade/Fade.tsx +32 -0
  126. package/src/wc/components/Fade/index.ts +1 -0
  127. package/src/wc/components/Field/Field.tsx +28 -0
  128. package/src/wc/components/Header/Header.tsx +33 -0
  129. package/src/wc/components/Icon/Icon.tsx +112 -0
  130. package/src/wc/components/Icon/flag-icon-catalog.ts +68 -0
  131. package/src/wc/components/Icon/index.ts +1 -0
  132. package/src/wc/components/Icon/system-icon-catalog.ts +798 -0
  133. package/src/wc/components/Input/Input.tsx +100 -0
  134. package/src/wc/components/Input/index.ts +1 -0
  135. package/src/wc/components/LabelWrap/LabelWrap.tsx +38 -0
  136. package/src/wc/components/Loader/Loader.tsx +47 -0
  137. package/src/wc/components/Loader/index.ts +1 -0
  138. package/src/wc/components/Menu/Menu.tsx +340 -0
  139. package/src/wc/components/Modal/Modal.tsx +197 -0
  140. package/src/wc/components/Pagination/Pagination.tsx +97 -0
  141. package/src/wc/components/PanelNav/PanelNav.tsx +666 -0
  142. package/src/wc/components/PanelNav/index.ts +7 -0
  143. package/src/wc/components/PanelNav/panel-nav-utils.ts +124 -0
  144. package/src/wc/components/RadioGroup/RadioGroup.tsx +128 -0
  145. package/src/wc/components/RadioGroup/index.ts +1 -0
  146. package/src/wc/components/Scrollbar/Scrollbar.tsx +189 -0
  147. package/src/wc/components/Select/Select.tsx +195 -0
  148. package/src/wc/components/Sidebar/Sidebar.tsx +245 -0
  149. package/src/wc/components/Skeleton/Skeleton.tsx +64 -0
  150. package/src/wc/components/Skeleton/index.ts +1 -0
  151. package/src/wc/components/Slider/Slider.tsx +68 -0
  152. package/src/wc/components/Slider/index.ts +1 -0
  153. package/src/wc/components/Surface/Surface.tsx +81 -0
  154. package/src/wc/components/TabGroup/TabGroup.tsx +188 -0
  155. package/src/wc/components/TabGroup/tab-item-utils.ts +24 -0
  156. package/src/wc/components/TabGroupNav/TabGroupNav.tsx +188 -0
  157. package/src/wc/components/Table/Table.tsx +268 -0
  158. package/src/wc/components/Tag/Tag.tsx +135 -0
  159. package/src/wc/components/Tag/index.ts +1 -0
  160. package/src/wc/components/Text/Text.tsx +100 -0
  161. package/src/wc/components/Text/index.ts +1 -0
  162. package/src/wc/components/Toast/Toast.tsx +174 -0
  163. package/src/wc/components/Toast/toast-service.ts +60 -0
  164. package/src/wc/components/Toggle/Toggle.tsx +43 -0
  165. package/src/wc/components/Toggle/index.ts +1 -0
  166. package/src/wc/components/ToggleButton/ToggleButton.tsx +120 -0
  167. package/src/wc/components/ToggleButtonGroup/ToggleButtonGroup.tsx +118 -0
  168. package/src/wc/components/Tooltip/Tooltip.tsx +196 -0
  169. package/src/wc/components.d.ts +3028 -0
  170. package/dist/components/p-DV8pmt1m.js +0 -2
  171. package/dist/components/p-DV8pmt1m.js.map +0 -1
  172. package/dist/components/p-Dqga4VCz.js +0 -2
  173. package/dist/components/p-Dqga4VCz.js.map +0 -1
  174. package/dist/components/p-STnE_bmQ.js +0 -2
  175. package/dist/components/p-STnE_bmQ.js.map +0 -1
  176. /package/dist/components/{p-D3CwGoIu.js.map → p-COsKOwZR.js.map} +0 -0
  177. /package/dist/components/{p-DHF5BQbO.js.map → p-Dd0GrKaF.js.map} +0 -0
  178. /package/dist/components/{p-Dfx0StHj.js.map → p-Vwlmhl4E.js.map} +0 -0
  179. /package/dist/components/{p-C4p1fG67.js.map → p-woooYulR.js.map} +0 -0
@@ -0,0 +1,188 @@
1
+ import { Component, Prop, Event, EventEmitter, Element, Listen, Watch, h, Host } from '@stencil/core';
2
+ import {
3
+ getSelectableTabs,
4
+ isTabDivider,
5
+ type TabItem,
6
+ type TabItemTab,
7
+ } from '../TabGroup/tab-item-utils';
8
+
9
+ export type TabGroupNavBackground = 'faint' | 'medium' | 'bold' | 'strong' | 'always-dark';
10
+
11
+ @Component({
12
+ tag: 'ds-tab-group-nav',
13
+ styleUrl: 'TabGroupNav.css',
14
+ scoped: true,
15
+ })
16
+ export class TabGroupNav {
17
+ @Element() el!: HTMLElement;
18
+
19
+ @Prop({ mutable: true }) value: string = '';
20
+ @Prop() tabs: TabItem[] = [];
21
+ @Prop() background: TabGroupNavBackground | undefined;
22
+ @Prop({ attribute: 'aria-label' }) ariaLabel: string | undefined;
23
+ @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;
24
+ @Prop() orientation: 'horizontal' | 'vertical' = 'horizontal';
25
+
26
+ @Event() dsChange!: EventEmitter<string>;
27
+
28
+ private get selectableTabs(): TabItemTab[] {
29
+ return getSelectableTabs(this.tabs);
30
+ }
31
+
32
+ private selectTab(id: string, options?: { focus?: boolean }) {
33
+ this.value = id;
34
+ this.dsChange.emit(id);
35
+ if (options?.focus !== false) {
36
+ this.focusTab(id);
37
+ }
38
+ }
39
+
40
+ private focusTab(id: string) {
41
+ const btn = this.el.querySelector(`[data-tab-id="${id}"]`) as HTMLElement | null;
42
+ btn?.focus({ preventScroll: true });
43
+ }
44
+
45
+ /** Keep focus on the selected tab when value changes externally (e.g. BarNav menu). */
46
+ @Watch('value')
47
+ onValueChange(next: string, prev: string | undefined) {
48
+ if (prev === undefined || next === prev) return;
49
+
50
+ requestAnimationFrame(() => {
51
+ const active = document.activeElement as HTMLElement | null;
52
+ if (!active || !this.el.contains(active)) return;
53
+
54
+ const activeId = active.getAttribute('data-tab-id');
55
+ if (activeId === next) return;
56
+
57
+ const nextTab = this.el.querySelector(`[data-tab-id="${next}"]`) as HTMLElement | null;
58
+ if (nextTab) {
59
+ nextTab.focus({ preventScroll: true });
60
+ } else {
61
+ active.blur();
62
+ }
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Find the next non-disabled tab index relative to `from`, stepping by `step`.
68
+ * Returns `from` unchanged if no other enabled tab exists.
69
+ */
70
+ private findEnabled(from: number, step: 1 | -1): number {
71
+ const tabs = this.selectableTabs;
72
+ const len = tabs.length;
73
+ for (let n = 0; n < len; n++) {
74
+ const i = (from + step * (n + 1) + len * (n + 1)) % len;
75
+ if (!tabs[i]?.disabled) return i;
76
+ }
77
+ return from;
78
+ }
79
+
80
+ @Listen('keydown')
81
+ handleKeyDown(e: KeyboardEvent) {
82
+ const tabs = this.selectableTabs;
83
+ if (!tabs.length) return;
84
+
85
+ const currentIdx = tabs.findIndex(t => t.id === this.value);
86
+ const isVertical = this.orientation === 'vertical';
87
+
88
+ let nextIdx: number | null = null;
89
+
90
+ if (!isVertical) {
91
+ if (e.key === 'ArrowRight') {
92
+ nextIdx = this.findEnabled(currentIdx, 1);
93
+ } else if (e.key === 'ArrowLeft') {
94
+ nextIdx = this.findEnabled(currentIdx, -1);
95
+ }
96
+ } else {
97
+ if (e.key === 'ArrowDown') {
98
+ nextIdx = this.findEnabled(currentIdx, 1);
99
+ } else if (e.key === 'ArrowUp') {
100
+ nextIdx = this.findEnabled(currentIdx, -1);
101
+ }
102
+ }
103
+
104
+ if (e.key === 'Home') {
105
+ const first = tabs.findIndex(t => !t.disabled);
106
+ nextIdx = first === -1 ? null : first;
107
+ } else if (e.key === 'End') {
108
+ const lastEnabled = [...tabs].reverse().findIndex(t => !t.disabled);
109
+ nextIdx = lastEnabled === -1 ? null : tabs.length - 1 - lastEnabled;
110
+ }
111
+
112
+ if (nextIdx !== null) {
113
+ e.preventDefault();
114
+ const next = tabs[nextIdx];
115
+ this.selectTab(next.id);
116
+ }
117
+ }
118
+
119
+ private getBgClass(): string {
120
+ if (!this.background || this.background === 'faint') return '';
121
+ if (this.background === 'always-dark') return 'on-always-dark';
122
+ return `on-${this.background}`;
123
+ }
124
+
125
+ render() {
126
+ const bgClass = this.getBgClass();
127
+ const isVertical = this.orientation === 'vertical';
128
+
129
+ return (
130
+ <Host class="tab-group-nav-host">
131
+ <div
132
+ role="tablist"
133
+ class={{ 'tab-list': true, 'tab-list--vertical': isVertical }}
134
+ aria-label={this.ariaLabel}
135
+ aria-labelledby={this.ariaLabelledby}
136
+ aria-orientation={isVertical ? 'vertical' : undefined}
137
+ >
138
+ {this.tabs.map((tab, index) => {
139
+ if (isTabDivider(tab)) {
140
+ return (
141
+ <div
142
+ key={`divider-${index}`}
143
+ class={{
144
+ 'tab-divider': true,
145
+ 'tab-divider--vertical': isVertical,
146
+ }}
147
+ aria-hidden="true"
148
+ >
149
+ <div class="tab-divider__line" />
150
+ </div>
151
+ );
152
+ }
153
+
154
+ const isSelected = tab.id === this.value;
155
+ return (
156
+ <button
157
+ key={tab.id}
158
+ type="button"
159
+ role="tab"
160
+ data-tab-id={tab.id}
161
+ class={{
162
+ tab: true,
163
+ 'tab--selected': isSelected,
164
+ [bgClass]: !!bgClass,
165
+ }}
166
+ aria-selected={isSelected}
167
+ aria-disabled={tab.disabled || undefined}
168
+ aria-controls={tab.panelId ?? undefined}
169
+ disabled={tab.disabled}
170
+ tabIndex={isSelected ? 0 : -1}
171
+ onClick={() => !tab.disabled && this.selectTab(tab.id)}
172
+ >
173
+ <span class={{
174
+ tab__label: true,
175
+ 'tab__label--dot': !!tab.dot,
176
+ [isSelected ? 'text-body-medium-emphasis' : 'text-body-medium']: true,
177
+ }}>
178
+ {tab.label}
179
+ {tab.dot && <span class="tab__dot" aria-hidden="true" />}
180
+ </span>
181
+ </button>
182
+ );
183
+ })}
184
+ </div>
185
+ </Host>
186
+ );
187
+ }
188
+ }
@@ -0,0 +1,268 @@
1
+ import { Component, Prop, State, Event, EventEmitter, h, Host } from '@stencil/core';
2
+
3
+ export type SortDirection = 'asc' | 'desc';
4
+
5
+ export interface SortState {
6
+ columnId: string;
7
+ direction: SortDirection;
8
+ }
9
+
10
+ export interface TableColumn<T = unknown> {
11
+ id: string;
12
+ header: string;
13
+ accessorKey?: string;
14
+ accessor?: (row: T) => unknown;
15
+ sortable?: boolean;
16
+ align?: 'left' | 'right' | 'center';
17
+ width?: string | number;
18
+ minWidth?: string | number;
19
+ hide?: boolean;
20
+ }
21
+
22
+ function getCellValue<T>(col: TableColumn<T>, row: T): unknown {
23
+ if (col.accessorKey) return (row as Record<string, unknown>)[col.accessorKey];
24
+ if (col.accessor) return col.accessor(row);
25
+ return undefined;
26
+ }
27
+
28
+ function compareValues(a: unknown, b: unknown, dir: SortDirection): number {
29
+ const m = dir === 'asc' ? 1 : -1;
30
+ if (a == null && b == null) return 0;
31
+ if (a == null) return m;
32
+ if (b == null) return -m;
33
+ if (typeof a === 'number' && typeof b === 'number') return (a - b) * m;
34
+ return String(a).localeCompare(String(b)) * m;
35
+ }
36
+
37
+ @Component({
38
+ tag: 'ds-table',
39
+ styleUrl: 'Table.css',
40
+ scoped: true,
41
+ })
42
+ export class Table {
43
+ @Prop() columns: TableColumn[] = [];
44
+ @Prop() data: unknown[] = [];
45
+ @Prop() sortState: SortState | undefined;
46
+ @Prop() loading: boolean = false;
47
+ @Prop() emptyMessage: string = 'No results found.';
48
+ /** Comma-separated zero-based row indices that are selected. */
49
+ @Prop() selectedRows: string = '';
50
+ /** Page index (0-based). Enables built-in pagination when set alongside pageSize. */
51
+ @Prop({ mutable: true }) pageIndex: number | undefined;
52
+ @Prop() pageSize: number = 20;
53
+
54
+ @State() private internalSort: SortState | undefined;
55
+
56
+ @Event() dsSort!: EventEmitter<{ columnId: string }>;
57
+ @Event() dsRowClick!: EventEmitter<{ row: unknown; rowIndex: number }>;
58
+ @Event() dsPageChange!: EventEmitter<{ pageIndex: number }>;
59
+
60
+ private get activeSort(): SortState | undefined {
61
+ return this.sortState ?? this.internalSort;
62
+ }
63
+
64
+ private get visibleCols(): TableColumn[] {
65
+ return this.columns.filter(c => !c.hide);
66
+ }
67
+
68
+ private get gridTemplate(): string {
69
+ return this.visibleCols.map(col => {
70
+ if (col.width) return typeof col.width === 'number' ? `${col.width}px` : col.width;
71
+ return 'minmax(120px, 1fr)';
72
+ }).join(' ');
73
+ }
74
+
75
+ private get sortedData(): unknown[] {
76
+ const sort = this.activeSort;
77
+ if (!sort) return this.data;
78
+ const col = this.columns.find(c => c.id === sort.columnId);
79
+ if (!col) return this.data;
80
+ return [...this.data].sort((a, b) =>
81
+ compareValues(getCellValue(col, a), getCellValue(col, b), sort.direction)
82
+ );
83
+ }
84
+
85
+ private get pagedData(): unknown[] {
86
+ if (this.pageIndex === undefined) return this.sortedData;
87
+ const start = this.pageIndex * this.pageSize;
88
+ return this.sortedData.slice(start, start + this.pageSize);
89
+ }
90
+
91
+ private get totalPages(): number {
92
+ if (this.pageIndex === undefined) return 1;
93
+ return Math.ceil(this.sortedData.length / this.pageSize);
94
+ }
95
+
96
+ private get selectedSet(): Set<number> {
97
+ if (!this.selectedRows) return new Set();
98
+ return new Set(this.selectedRows.split(',').map(Number).filter(n => !isNaN(n)));
99
+ }
100
+
101
+ private handleSort(columnId: string) {
102
+ this.dsSort.emit({ columnId });
103
+ // Toggle internal sort if consumer hasn't provided sortState
104
+ if (!this.sortState) {
105
+ const cur = this.internalSort;
106
+ this.internalSort = cur?.columnId === columnId
107
+ ? { columnId, direction: cur.direction === 'asc' ? 'desc' : 'asc' }
108
+ : { columnId, direction: 'asc' };
109
+ }
110
+ }
111
+
112
+ render() {
113
+ const { visibleCols, pagedData, totalPages, loading } = this;
114
+ const isEmpty = !loading && pagedData.length === 0;
115
+ const selectedSet = this.selectedSet;
116
+ const sort = this.activeSort;
117
+ const hasPagination = this.pageIndex !== undefined && totalPages > 1;
118
+
119
+ return (
120
+ <Host>
121
+ <div class="table-wrapper" aria-busy={loading || undefined}>
122
+ <div class="table-scroll">
123
+ <div class="table" style={{ gridTemplateColumns: this.gridTemplate }} role="table">
124
+
125
+ {/* Header row */}
126
+ <div class="header-row" role="row">
127
+ {visibleCols.map(col => {
128
+ const isSorted = sort?.columnId === col.id;
129
+ const ariaSort = col.sortable
130
+ ? (isSorted ? (sort.direction === 'asc' ? 'ascending' : 'descending') : 'none')
131
+ : undefined;
132
+ return (
133
+ <div
134
+ key={col.id}
135
+ class={{
136
+ 'header-cell': true,
137
+ 'header-cell--sortable': !!col.sortable,
138
+ 'align-right': col.align === 'right',
139
+ 'align-center': col.align === 'center',
140
+ }}
141
+ role="columnheader"
142
+ aria-sort={ariaSort}
143
+ tabIndex={col.sortable ? 0 : undefined}
144
+ onClick={() => col.sortable && this.handleSort(col.id)}
145
+ onKeyDown={(e: KeyboardEvent) => {
146
+ if (col.sortable && (e.key === 'Enter' || e.key === ' ')) {
147
+ e.preventDefault();
148
+ this.handleSort(col.id);
149
+ }
150
+ }}
151
+ style={col.minWidth ? { minWidth: typeof col.minWidth === 'number' ? `${col.minWidth}px` : col.minWidth as string } : undefined}
152
+ >
153
+ <span class="text-body-small-emphasis" style={{ color: 'var(--color-foreground-secondary)' }}>
154
+ {col.header}
155
+ </span>
156
+ {isSorted && (
157
+ <span class="sort-indicator" aria-hidden="true">
158
+ {sort.direction === 'asc' ? '↑' : '↓'}
159
+ </span>
160
+ )}
161
+ </div>
162
+ );
163
+ })}
164
+ </div>
165
+
166
+ {/* Loading skeleton */}
167
+ {loading && Array.from({ length: 5 }).map((_, i) => (
168
+ <div
169
+ key={`sk-${i}`}
170
+ class="row"
171
+ role={i === 0 ? 'status' : 'row'}
172
+ aria-label={i === 0 ? 'Loading' : undefined}
173
+ >
174
+ {visibleCols.map(col => (
175
+ <div key={col.id} class="cell" role="cell">
176
+ <div class="skeleton" />
177
+ </div>
178
+ ))}
179
+ </div>
180
+ ))}
181
+
182
+ {/* Empty state */}
183
+ {isEmpty && (
184
+ <div class="empty-row" style={{ gridColumn: '1 / -1' }}>
185
+ <p class="text-body-medium" style={{ margin: '0', color: 'var(--color-foreground-secondary)' }}>
186
+ {this.emptyMessage}
187
+ </p>
188
+ </div>
189
+ )}
190
+
191
+ {/* Data rows */}
192
+ {!loading && pagedData.map((row, rowIndex) => {
193
+ const isSelected = selectedSet.has(rowIndex);
194
+ return (
195
+ <div
196
+ key={rowIndex}
197
+ class={{
198
+ 'row': true,
199
+ 'row--selected': isSelected,
200
+ 'row--clickable': true,
201
+ }}
202
+ role="row"
203
+ tabIndex={0}
204
+ aria-selected={this.selectedRows ? isSelected : undefined}
205
+ onClick={() => this.dsRowClick.emit({ row, rowIndex })}
206
+ onKeyDown={(e: KeyboardEvent) => {
207
+ if (e.key === 'Enter' || e.key === ' ') {
208
+ e.preventDefault();
209
+ this.dsRowClick.emit({ row, rowIndex });
210
+ }
211
+ }}
212
+ >
213
+ {visibleCols.map(col => (
214
+ <div
215
+ key={col.id}
216
+ class={{
217
+ 'cell': true,
218
+ 'align-right': col.align === 'right',
219
+ 'align-center': col.align === 'center',
220
+ }}
221
+ role="cell"
222
+ >
223
+ <span class="text-body-medium cell-value">
224
+ {String(getCellValue(col, row) ?? '')}
225
+ </span>
226
+ </div>
227
+ ))}
228
+ </div>
229
+ );
230
+ })}
231
+ </div>
232
+ </div>
233
+
234
+ {/* Built-in pagination */}
235
+ {hasPagination && (
236
+ <div class="table-pagination">
237
+ <button
238
+ type="button"
239
+ class="page-button"
240
+ disabled={this.pageIndex === 0}
241
+ onClick={() => {
242
+ this.pageIndex = this.pageIndex! - 1;
243
+ this.dsPageChange.emit({ pageIndex: this.pageIndex });
244
+ }}
245
+ >
246
+ <span class="text-body-small">Previous</span>
247
+ </button>
248
+ <span class="text-body-small" style={{ color: 'var(--color-foreground-secondary)' }}>
249
+ Page {this.pageIndex! + 1} of {totalPages}
250
+ </span>
251
+ <button
252
+ type="button"
253
+ class="page-button"
254
+ disabled={this.pageIndex! >= totalPages - 1}
255
+ onClick={() => {
256
+ this.pageIndex = this.pageIndex! + 1;
257
+ this.dsPageChange.emit({ pageIndex: this.pageIndex });
258
+ }}
259
+ >
260
+ <span class="text-body-small">Next</span>
261
+ </button>
262
+ </div>
263
+ )}
264
+ </div>
265
+ </Host>
266
+ );
267
+ }
268
+ }
@@ -0,0 +1,135 @@
1
+ import { Component, Prop, Event, EventEmitter, h, Host } from '@stencil/core';
2
+
3
+ export type TagIntent = 'neutral' | 'brand' | 'ai' | 'negative' | 'warning' | 'caution' | 'positive';
4
+ export type TagContrast = 'strong' | 'bold' | 'medium' | 'faint';
5
+ export type TagElevation = 'none' | 'flat' | 'elevated';
6
+ export type TagSize = 'md' | 'sm' | 'xs';
7
+ export type TagBackground = 'faint' | 'medium' | 'bold' | 'strong' | 'always-dark';
8
+
9
+ const TEXT_VARIANT: Record<TagSize, string> = {
10
+ md: 'text-body-medium',
11
+ sm: 'text-body-small',
12
+ xs: 'text-caption',
13
+ };
14
+
15
+ const ICON_SIZE: Record<TagSize, number> = { md: 20, sm: 16, xs: 12 };
16
+
17
+ @Component({
18
+ tag: 'ds-tag',
19
+ styleUrl: 'Tag.css',
20
+ scoped: true,
21
+ })
22
+ export class Tag {
23
+ @Prop() label!: string;
24
+ @Prop() intent: TagIntent = 'neutral';
25
+ @Prop() contrast: TagContrast = 'faint';
26
+ @Prop() elevation: TagElevation = 'none';
27
+ @Prop() size: TagSize = 'md';
28
+ @Prop() rounded: boolean = false;
29
+ @Prop() removable: boolean = false;
30
+ @Prop() maxWidth: string | number | undefined;
31
+ @Prop() inactive: boolean = false;
32
+ @Prop() background: TagBackground | undefined;
33
+ @Prop({ mutable: true }) pressed: boolean = false;
34
+ /** Whether the tag has a click/toggle behavior. */
35
+ @Prop() interactive: boolean = false;
36
+
37
+ /** Fired when the remove button is clicked. */
38
+ @Event() dsRemove!: EventEmitter<void>;
39
+ /** Fired when an interactive tag is clicked. */
40
+ @Event() dsClick!: EventEmitter<void>;
41
+ /** Fired when the pressed state toggles. */
42
+ @Event() dsPressedChange!: EventEmitter<boolean>;
43
+
44
+ private handleClick = () => {
45
+ if (this.inactive) return;
46
+ this.pressed = !this.pressed;
47
+ this.dsPressedChange.emit(this.pressed);
48
+ this.dsClick.emit();
49
+ };
50
+
51
+ private handleKeyDown = (e: KeyboardEvent) => {
52
+ if (this.inactive) return;
53
+ if (e.key === 'Enter' || e.key === ' ') {
54
+ e.preventDefault();
55
+ this.handleClick();
56
+ }
57
+ };
58
+
59
+ private handleRemove = (e: MouseEvent) => {
60
+ e.stopPropagation();
61
+ this.dsRemove.emit();
62
+ };
63
+
64
+ render() {
65
+ const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
66
+ const sz = this.size.toUpperCase() as 'MD' | 'SM' | 'XS';
67
+ const hasIconSlot = true; // always render slot, consumer decides whether to fill it
68
+ const textVariant = TEXT_VARIANT[this.size];
69
+ const iconSize = ICON_SIZE[this.size];
70
+
71
+ const maxWidthStyle = this.maxWidth != null
72
+ ? { maxWidth: typeof this.maxWidth === 'number' ? `${this.maxWidth}px` : this.maxWidth }
73
+ : undefined;
74
+
75
+ const hostCls: Record<string, boolean> = {
76
+ 'tag': true,
77
+ [`tag--intent-${this.intent}`]: true,
78
+ [`tag--contrast-${this.contrast}`]: true,
79
+ [`tag--elevation-${this.elevation}`]: true,
80
+ [`tag--size-${this.size}`]: true,
81
+ 'tag--rounded': this.rounded,
82
+ 'tag--rounded-no-icon-left': this.rounded, // consumer overrides via slot presence hack
83
+ 'tag--rounded-no-remove-right': this.rounded && !this.removable,
84
+ [`tag--icon-left-${sz}`]: false, // toggled by slotchange
85
+ [`tag--icon-right-${sz}`]: this.removable,
86
+ 'tag--removable': this.removable,
87
+ 'tag--interactive': this.interactive,
88
+ 'tag--inactive': this.inactive,
89
+ 'tag--pressed': this.pressed,
90
+ 'tag--on-medium': this.background === 'medium',
91
+ 'tag--on-bold': this.background === 'bold',
92
+ 'tag--on-strong': this.background === 'strong',
93
+ 'tag--on-always-dark': this.background === 'always-dark',
94
+ };
95
+
96
+ return (
97
+ <Host
98
+ class={hostCls}
99
+ style={maxWidthStyle}
100
+ onClick={this.interactive ? this.handleClick : undefined}
101
+ onKeyDown={this.interactive ? this.handleKeyDown : undefined}
102
+ role={this.interactive ? 'button' : undefined}
103
+ tabIndex={this.interactive ? (this.inactive ? -1 : 0) : undefined}
104
+ aria-pressed={this.interactive && this.pressed !== undefined ? String(this.pressed) : undefined}
105
+ aria-disabled={this.inactive || undefined}
106
+ >
107
+ {/* Leading icon slot — consumer provides an icon element */}
108
+ <span class="tag__icon-slot" style={{ fontSize: `${iconSize}px`, lineHeight: '0' }}>
109
+ <slot name="icon" />
110
+ </span>
111
+ {/* Label */}
112
+ <span class={{ 'tag__label': true, [textVariant]: true }}>
113
+ {this.label}
114
+ </span>
115
+ {/* Remove button */}
116
+ {this.removable && (
117
+ <button
118
+ type="button"
119
+ class="tag__remove"
120
+ onClick={this.handleRemove}
121
+ aria-label={`Remove ${this.label}`}
122
+ tabIndex={-1}
123
+ >
124
+ {/* Remove icon slot — defaults to × glyph */}
125
+ <slot name="remove-icon">
126
+ <span class="tag__remove-x" style={{ fontSize: `${iconSize}px`, lineHeight: '0' }}>
127
+
128
+ </span>
129
+ </slot>
130
+ </button>
131
+ )}
132
+ </Host>
133
+ );
134
+ }
135
+ }
@@ -0,0 +1 @@
1
+ export type { TagIntent, TagContrast, TagElevation, TagSize, TagBackground } from './Tag';
@@ -0,0 +1,100 @@
1
+ import { Component, Prop, h, Host } from '@stencil/core';
2
+
3
+ export type TextVariant =
4
+ | 'text-display-medium'
5
+ | 'text-display-small'
6
+ | 'text-title-large'
7
+ | 'text-title-medium'
8
+ | 'text-title-small'
9
+ | 'text-body-large'
10
+ | 'text-body-large-emphasis'
11
+ | 'text-body-medium'
12
+ | 'text-body-medium-emphasis'
13
+ | 'text-body-small'
14
+ | 'text-body-small-emphasis'
15
+ | 'text-caption'
16
+ | 'text-caption-emphasis';
17
+
18
+ export type TextColorToken =
19
+ | 'primary' | 'secondary' | 'tertiary'
20
+ | 'brand' | 'negative' | 'positive' | 'warning' | 'caution' | 'ai'
21
+ | 'on-strong' | 'on-bold' | 'inherit';
22
+
23
+ export type TextColor = TextColorToken | `var(--${string})`;
24
+ export type TextDecoration = 'none' | 'underline' | 'dotted-underline';
25
+ export type TextAlign = 'left' | 'center' | 'right';
26
+ export type LineTruncation = 1 | 2 | 3 | 4 | 5 | 'none';
27
+ export type TextWrap = 'wrap' | 'nowrap' | 'balance' | 'pretty';
28
+ export type TextElement = 'p' | 'span' | 'div' | 'label' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
29
+
30
+ @Component({
31
+ tag: 'ds-text',
32
+ styleUrl: 'Text.css',
33
+ scoped: true,
34
+ })
35
+ export class Text {
36
+ @Prop() variant: TextVariant = 'text-body-medium';
37
+ @Prop() color: TextColor | undefined;
38
+ @Prop() decoration: TextDecoration | undefined;
39
+ @Prop() italic: boolean = false;
40
+ @Prop() align: TextAlign | undefined;
41
+ @Prop() lineTruncation: LineTruncation = 'none';
42
+ @Prop() wrap: TextWrap | undefined;
43
+ @Prop() as: TextElement = 'p';
44
+ /** Maps to `for` attribute on <label> elements. */
45
+ @Prop() for: string | undefined;
46
+
47
+ render() {
48
+ // Polymorphic element: TSX treats a variable tag as a component, so a
49
+ // dynamic intrinsic element name must be cast to `any` to render.
50
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
51
+ const Tag = this.as as any;
52
+ const isCustomColor = (c: TextColor) => typeof c === 'string' && c.startsWith('var(--');
53
+
54
+ const cls: Record<string, boolean> = {
55
+ [this.variant]: true,
56
+ 'text': true,
57
+ 'text--nowrap': this.wrap === 'nowrap',
58
+ 'text--balance': this.wrap === 'balance',
59
+ 'text--pretty': this.wrap === 'pretty',
60
+ 'text--truncate-1': this.lineTruncation === 1 && this.wrap !== 'nowrap',
61
+ 'text--truncate-2': this.lineTruncation === 2 && this.wrap !== 'nowrap',
62
+ 'text--truncate-3': this.lineTruncation === 3 && this.wrap !== 'nowrap',
63
+ 'text--truncate-4': this.lineTruncation === 4 && this.wrap !== 'nowrap',
64
+ 'text--truncate-5': this.lineTruncation === 5 && this.wrap !== 'nowrap',
65
+ 'text--color-primary': this.color === 'primary',
66
+ 'text--color-secondary': this.color === 'secondary',
67
+ 'text--color-tertiary': this.color === 'tertiary',
68
+ 'text--color-brand': this.color === 'brand',
69
+ 'text--color-negative': this.color === 'negative',
70
+ 'text--color-positive': this.color === 'positive',
71
+ 'text--color-warning': this.color === 'warning',
72
+ 'text--color-caution': this.color === 'caution',
73
+ 'text--color-ai': this.color === 'ai',
74
+ 'text--color-on-strong': this.color === 'on-strong',
75
+ 'text--color-on-bold': this.color === 'on-bold',
76
+ 'text--color-inherit': this.color === 'inherit',
77
+ 'text--decoration-underline': this.decoration === 'underline',
78
+ 'text--decoration-dotted-underline': this.decoration === 'dotted-underline',
79
+ 'text--italic': this.italic,
80
+ 'text--align-left': this.align === 'left',
81
+ 'text--align-center': this.align === 'center',
82
+ 'text--align-right': this.align === 'right',
83
+ };
84
+
85
+ const style = this.color && isCustomColor(this.color)
86
+ ? { color: this.color as string }
87
+ : undefined;
88
+
89
+ const extraProps: Record<string, string> = {};
90
+ if (this.as === 'label' && this.for) extraProps['for'] = this.for;
91
+
92
+ return (
93
+ <Host style={{ display: 'contents' }}>
94
+ <Tag class={cls} style={style} {...extraProps}>
95
+ <slot />
96
+ </Tag>
97
+ </Host>
98
+ );
99
+ }
100
+ }