mac-human-design 0.1.21 → 0.1.23

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,40 @@
2
2
 
3
3
  This file tracks changes between published npm versions of `mac-human-design`.
4
4
 
5
+ ## 0.1.23
6
+
7
+ ### Added
8
+
9
+ - Added `grid`/`table` semantics, row and column metadata, busy state, selected
10
+ row state, keyboard row activation, and optional accessible labels to
11
+ `MacDataTable`.
12
+ - Added focus-visible styling for interactive `MacDataTable` rows.
13
+ - Extended the macOS design verifier to protect interactive table/grid
14
+ accessibility invariants.
15
+
16
+ ### Changed
17
+
18
+ - Interactive table rows can now be selected with Space and opened/activated
19
+ with Enter when row handlers are provided.
20
+ - Bumped the package to `0.1.23`.
21
+
22
+ ## 0.1.22
23
+
24
+ ### Added
25
+
26
+ - Added a centered loading spinner overlay for `SymbolIconButton` that shares
27
+ the icon button grid cell and respects reduced-motion preferences.
28
+ - Added `aria-busy` to loading `SymbolIconButton` instances.
29
+ - Extended the macOS design verifier to protect the loading spinner's centered
30
+ geometry, animation keyframes, reduced-motion behavior, and `aria-busy`
31
+ loading state.
32
+
33
+ ### Changed
34
+
35
+ - Kept the underlying symbol visible at reduced opacity while loading so the
36
+ button remains spatially stable.
37
+ - Bumped the package to `0.1.22`.
38
+
5
39
  ## 0.1.21
6
40
 
7
41
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mac-human-design",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "Reusable macOS-oriented UI primitives and SF Symbols bridge for Tauri apps.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -8,6 +8,7 @@ const packageJsonPath = path.join(repoRoot, "package.json");
8
8
  const wrapperPath = path.join(repoRoot, "src", "components", "MacBaseUI.tsx");
9
9
  const symbolIconButtonPath = path.join(repoRoot, "src", "components", "SymbolIconButton.tsx");
10
10
  const galleryPath = path.join(repoRoot, "src", "components", "MacBaseUIGallery.tsx");
11
+ const dataTablePath = path.join(repoRoot, "src", "components", "MacDataTable.tsx");
11
12
  const symbolServicePath = path.join(repoRoot, "src", "symbols", "systemSymbolService.ts");
12
13
 
13
14
  function readText(filePath) {
@@ -48,6 +49,7 @@ const packageJson = JSON.parse(readText(packageJsonPath));
48
49
  const wrapperSource = readText(wrapperPath);
49
50
  const symbolIconButtonSource = readText(symbolIconButtonPath);
50
51
  const gallerySource = readText(galleryPath);
52
+ const dataTableSource = readText(dataTablePath);
51
53
  const symbolServiceSource = readText(symbolServicePath);
52
54
 
53
55
  function getCssRuleBody(selector) {
@@ -138,6 +140,10 @@ requireCssDeclarations(".hd-mac-symbol-icon-button", [
138
140
  ["fixed 30px min-height", /min-height\s*:\s*30px\s*;/],
139
141
  ]);
140
142
 
143
+ requireCssDeclarations(".hd-mac-symbol-icon-button::after", [
144
+ ["loading overlay grid placement", /grid-area\s*:\s*1\s*\/\s*1\s*;/],
145
+ ]);
146
+
141
147
  requireCssDeclarations(".hd-mac-symbol-icon", [
142
148
  ["single grid cell placement", /grid-area\s*:\s*1\s*\/\s*1\s*;/],
143
149
  ["flex centering display", /display\s*:\s*inline-flex\s*;/],
@@ -184,6 +190,7 @@ const symbolIconRequirements = [
184
190
  ["SymbolIconButton imports MacIconButton", /import\s+\{\s*MacIconButton,\s*MacTooltip\s*\}\s+from\s+["']\.\/MacBaseUI["']/],
185
191
  ["SymbolIconButton render element is MacIconButton", /render=\{\s*<MacIconButton\b/],
186
192
  ["SymbolIconButton applies hd-mac-symbol-icon-button class", /className=["']hd-mac-symbol-icon-button["']/],
193
+ ["SymbolIconButton exposes aria-busy loading state", /aria-busy=\{isLoading \|\| undefined\}/],
187
194
  ["masked symbol class path", /className=["']hd-mac-symbol-icon hd-mac-symbol-icon-mask["']/],
188
195
  ["fallback symbol class path", /className=["']hd-mac-symbol-icon hd-mac-symbol-icon-fallback["']/],
189
196
  ["mask centering", /maskPosition:\s*["']center["']/],
@@ -213,6 +220,63 @@ if (missingGalleryStateCoverage.length > 0) {
213
220
  );
214
221
  }
215
222
 
223
+ const loadingIconRule = getCssRuleBody(".hd-mac-symbol-icon-button[data-loading]::after");
224
+ const missingLoadingIconDeclarations = [
225
+ ["centered loading spinner content", /content\s*:\s*""\s*;/],
226
+ ["spinner width", /width\s*:\s*15px\s*;/],
227
+ ["spinner height", /height\s*:\s*15px\s*;/],
228
+ ["spinner automatic centering margin", /margin\s*:\s*auto\s*;/],
229
+ ["spinner center transform origin", /transform-origin\s*:\s*center\s*;/],
230
+ ["spinner animation", /animation\s*:\s*mac-symbol-icon-spin\s+720ms\s+linear\s+infinite\s*;/],
231
+ ].filter(([, pattern]) => !pattern.test(loadingIconRule));
232
+
233
+ if (missingLoadingIconDeclarations.length > 0) {
234
+ fail(
235
+ "Missing centered loading affordance declarations for SymbolIconButton.",
236
+ missingLoadingIconDeclarations.map(([label]) => label),
237
+ );
238
+ }
239
+
240
+ if (!/@keyframes\s+mac-symbol-icon-spin/.test(stylesSource)) {
241
+ fail("Missing SymbolIconButton loading spinner keyframes.", ["hd-mac-symbol-icon-spin"]);
242
+ }
243
+
244
+ if (
245
+ !/@media\s*\(prefers-reduced-motion:\s*reduce\)[\s\S]*?\.hd-mac-symbol-icon-button\[data-loading\]::after[\s\S]*?animation-name\s*:\s*none\s*!important/.test(
246
+ stylesSource,
247
+ )
248
+ ) {
249
+ fail("Missing reduced-motion handling for SymbolIconButton loading spinner.", [
250
+ ".hd-mac-symbol-icon-button[data-loading]::after",
251
+ ]);
252
+ }
253
+
254
+ const missingDataTableRequirements = [
255
+ ["interactive grid role", /const tableRole = hasInteractiveRows \? "grid" : "table"/],
256
+ ["aria busy state", /aria-busy=\{loading \|\| undefined\}/],
257
+ ["row count metadata", /aria-rowcount=\{loading \? undefined : rows\.length \+ 1\}/],
258
+ ["column count metadata", /aria-colcount=\{columns\.length\}/],
259
+ ["header row index", /aria-rowindex=\{1\}/],
260
+ ["cell column index", /aria-colindex=\{columnIndex \+ 1\}/],
261
+ ["interactive row selected state", /aria-selected=\{hasInteractiveRows \? selected : undefined\}/],
262
+ ["interactive row tab stop", /tabIndex=\{hasInteractiveRows \? 0 : undefined\}/],
263
+ ["keyboard row handler", /handleRowKeyDown/],
264
+ ["enter key activation", /event\.key === "Enter"/],
265
+ ["space key activation", /event\.key === " "/],
266
+ ].filter(([, pattern]) => !pattern.test(dataTableSource));
267
+
268
+ if (missingDataTableRequirements.length > 0) {
269
+ fail(
270
+ "Missing MacDataTable interactive grid accessibility invariants.",
271
+ missingDataTableRequirements.map(([label]) => label),
272
+ );
273
+ }
274
+
275
+ requireCssDeclarations(".hd-mac-data-table-row-interactive:focus-visible", [
276
+ ["focus outline reset", /outline\s*:\s*none\s*;/],
277
+ ["focus ring", /var\(--hd-mac-focus-ring\)/],
278
+ ]);
279
+
216
280
  const missingSymbolServiceRequirements = [
217
281
  ["transparent symbol padding trim", /trimTransparentSymbolPadding/],
218
282
  ["alpha threshold trim", /TRANSPARENT_ALPHA_THRESHOLD/],
@@ -17,6 +17,8 @@ export type MacDataTableProps<Row> = {
17
17
  columns: readonly MacDataTableColumn<Row>[];
18
18
  rows: readonly Row[];
19
19
  getRowKey: (row: Row, index: number) => React.Key;
20
+ ariaLabel?: string;
21
+ ariaLabelledBy?: string;
20
22
  isRowSelected?: (row: Row, index: number) => boolean;
21
23
  onRowClick?: (row: Row, index: number) => void;
22
24
  onRowDoubleClick?: (row: Row, index: number) => void;
@@ -53,6 +55,8 @@ export function MacDataTable<Row>({
53
55
  columns,
54
56
  rows,
55
57
  getRowKey,
58
+ ariaLabel,
59
+ ariaLabelledBy,
56
60
  isRowSelected,
57
61
  onRowClick,
58
62
  onRowDoubleClick,
@@ -67,6 +71,30 @@ export function MacDataTable<Row>({
67
71
  }: MacDataTableProps<Row>) {
68
72
  const gridTemplateColumns = columns.map((column) => column.width).join(" ");
69
73
  const hasInteractiveRows = Boolean(onRowClick || onRowDoubleClick);
74
+ const tableRole = hasInteractiveRows ? "grid" : "table";
75
+
76
+ const handleRowKeyDown = React.useCallback(
77
+ (event: React.KeyboardEvent, row: Row, index: number) => {
78
+ if (!hasInteractiveRows) {
79
+ return;
80
+ }
81
+
82
+ if (event.key === "Enter") {
83
+ event.preventDefault();
84
+ if (onRowDoubleClick) {
85
+ onRowDoubleClick(row, index);
86
+ return;
87
+ }
88
+ onRowClick?.(row, index);
89
+ }
90
+
91
+ if (event.key === " ") {
92
+ event.preventDefault();
93
+ onRowClick?.(row, index);
94
+ }
95
+ },
96
+ [hasInteractiveRows, onRowClick, onRowDoubleClick],
97
+ );
70
98
 
71
99
  return (
72
100
  <motion.div
@@ -74,12 +102,19 @@ export function MacDataTable<Row>({
74
102
  animate={{ opacity: 1 }}
75
103
  transition={tableTransition}
76
104
  className={joinClasses("hd-mac-data-table", bordered && "hd-mac-data-table-bordered", className)}
105
+ role={tableRole}
106
+ aria-label={ariaLabel}
107
+ aria-labelledby={ariaLabelledBy}
108
+ aria-busy={loading || undefined}
109
+ aria-rowcount={loading ? undefined : rows.length + 1}
110
+ aria-colcount={columns.length}
77
111
  style={style}
78
112
  >
79
113
  <motion.div
80
114
  layout
81
115
  className="hd-mac-data-table-header"
82
116
  role="row"
117
+ aria-rowindex={1}
83
118
  style={{ gridTemplateColumns }}
84
119
  transition={tableTransition}
85
120
  >
@@ -92,6 +127,7 @@ export function MacDataTable<Row>({
92
127
  column.headerClassName,
93
128
  )}
94
129
  role="columnheader"
130
+ aria-colindex={columns.indexOf(column) + 1}
95
131
  style={column.headerStyle}
96
132
  >
97
133
  {column.header}
@@ -139,10 +175,14 @@ export function MacDataTable<Row>({
139
175
  data-selected={selected || undefined}
140
176
  onClick={onRowClick ? () => onRowClick(row, index) : undefined}
141
177
  onDoubleClick={onRowDoubleClick ? () => onRowDoubleClick(row, index) : undefined}
178
+ onKeyDown={hasInteractiveRows ? (event) => handleRowKeyDown(event, row, index) : undefined}
142
179
  role="row"
180
+ aria-rowindex={index + 2}
181
+ aria-selected={hasInteractiveRows ? selected : undefined}
182
+ tabIndex={hasInteractiveRows ? 0 : undefined}
143
183
  style={{ gridTemplateColumns, ...rowStyle?.(row, index) }}
144
184
  >
145
- {columns.map((column) => (
185
+ {columns.map((column, columnIndex) => (
146
186
  <div
147
187
  key={column.id}
148
188
  className={joinClasses(
@@ -151,6 +191,7 @@ export function MacDataTable<Row>({
151
191
  column.className,
152
192
  )}
153
193
  role="cell"
194
+ aria-colindex={columnIndex + 1}
154
195
  style={column.style}
155
196
  >
156
197
  {column.render(row, index)}
@@ -38,6 +38,7 @@ export function SymbolIconButton({
38
38
  render={
39
39
  <MacIconButton
40
40
  aria-label={label}
41
+ aria-busy={isLoading || undefined}
41
42
  title={label}
42
43
  disabled={disabled || isLoading}
43
44
  onClick={onClick}
@@ -76,6 +76,7 @@
76
76
  .hd-mac-avatar,
77
77
  .hd-mac-scroll-thumb,
78
78
  .hd-mac-status-message,
79
+ .hd-mac-symbol-icon-button[data-loading]::after,
79
80
  .hd-mac-window-toolbar,
80
81
  .hd-mac-data-table,
81
82
  .hd-mac-data-table-row {
@@ -84,6 +85,10 @@
84
85
  animation-iteration-count: 1 !important;
85
86
  scroll-behavior: auto !important;
86
87
  }
88
+
89
+ .hd-mac-symbol-icon-button[data-loading]::after {
90
+ animation-name: none !important;
91
+ }
87
92
  }
88
93
 
89
94
  .hd-mac,
@@ -279,6 +284,10 @@
279
284
  border-radius: 7px;
280
285
  }
281
286
 
287
+ .hd-mac-symbol-icon-button::after {
288
+ grid-area: 1 / 1;
289
+ }
290
+
282
291
  .hd-mac-symbol-icon {
283
292
  grid-area: 1 / 1;
284
293
  display: inline-flex;
@@ -955,7 +964,31 @@
955
964
  }
956
965
 
957
966
  .hd-mac-symbol-icon-button[data-loading] {
958
- opacity: 0.62;
967
+ opacity: 1;
968
+ color: var(--hd-mac-blue);
969
+ }
970
+
971
+ .hd-mac-symbol-icon-button[data-loading] .hd-mac-symbol-icon {
972
+ opacity: 0.22;
973
+ }
974
+
975
+ .hd-mac-symbol-icon-button[data-loading]::after {
976
+ content: "";
977
+ width: 15px;
978
+ height: 15px;
979
+ border: 1.5px solid color-mix(in srgb, currentColor 28%, transparent);
980
+ border-top-color: currentColor;
981
+ border-radius: 999px;
982
+ box-sizing: border-box;
983
+ margin: auto;
984
+ transform-origin: center;
985
+ animation: mac-symbol-icon-spin 720ms linear infinite;
986
+ }
987
+
988
+ @keyframes mac-symbol-icon-spin {
989
+ to {
990
+ transform: rotate(360deg);
991
+ }
959
992
  }
960
993
 
961
994
  .hd-mac-toggle[data-pressed] {
@@ -1448,6 +1481,13 @@
1448
1481
  cursor: default;
1449
1482
  }
1450
1483
 
1484
+ .hd-mac-data-table-row-interactive:focus-visible {
1485
+ outline: none;
1486
+ box-shadow:
1487
+ var(--hd-mac-focus-ring),
1488
+ inset 0 0 0 1px color-mix(in srgb, var(--hd-mac-blue) 24%, transparent);
1489
+ }
1490
+
1451
1491
  .hd-mac-data-table-row-interactive:hover {
1452
1492
  background: var(--hd-mac-fill-hover);
1453
1493
  }
@@ -1461,6 +1501,12 @@
1461
1501
  box-shadow: inset 3px 0 0 var(--hd-mac-blue);
1462
1502
  }
1463
1503
 
1504
+ .hd-mac-data-table-row[data-selected]:focus-visible {
1505
+ box-shadow:
1506
+ var(--hd-mac-focus-ring),
1507
+ inset 3px 0 0 var(--hd-mac-blue);
1508
+ }
1509
+
1464
1510
  .hd-mac-data-table-row[data-selected]:hover {
1465
1511
  background: color-mix(in srgb, var(--hd-mac-blue) 24%, transparent);
1466
1512
  }