@eifi1/ui-kit 0.51.2 → 0.51.4

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 (96) hide show
  1. package/README.md +5 -5
  2. package/dist/components/combobox-core.js +4 -5
  3. package/dist/components/combobox-core.js.map +1 -1
  4. package/dist/components/currency-option-list.js +3 -3
  5. package/dist/components/currency-option-list.js.map +1 -1
  6. package/dist/components/data-table-fit.js +7 -5
  7. package/dist/components/data-table-fit.js.map +1 -1
  8. package/dist/components/data-table-head.js +5 -1
  9. package/dist/components/data-table-head.js.map +1 -1
  10. package/dist/components/data-table.d.ts +8 -0
  11. package/dist/components/data-table.js.map +1 -1
  12. package/dist/components/grouped-picker.js +5 -4
  13. package/dist/components/grouped-picker.js.map +1 -1
  14. package/dist/components/icon-picker.js +5 -4
  15. package/dist/components/icon-picker.js.map +1 -1
  16. package/dist/components/inline-entity-combobox.js +4 -4
  17. package/dist/components/inline-entity-combobox.js.map +1 -1
  18. package/dist/components/legal-labels.js +9 -2
  19. package/dist/components/legal-labels.js.map +1 -1
  20. package/dist/components/legal-page.d.ts +35 -3
  21. package/dist/components/legal-page.js +22 -0
  22. package/dist/components/legal-page.js.map +1 -1
  23. package/dist/components/legal.d.ts +49 -5
  24. package/dist/components/legal.js +31 -3
  25. package/dist/components/legal.js.map +1 -1
  26. package/dist/components/list-labels.js +3 -1
  27. package/dist/components/list-labels.js.map +1 -1
  28. package/dist/components/list.d.ts +18 -1
  29. package/dist/components/list.js +32 -3
  30. package/dist/components/list.js.map +1 -1
  31. package/dist/eslint/index.js +1 -1
  32. package/dist/feedback/feedback-table.d.ts +3 -1
  33. package/dist/feedback/feedback-table.js +18 -4
  34. package/dist/feedback/feedback-table.js.map +1 -1
  35. package/dist/i18n/german.js +12 -3
  36. package/dist/i18n/german.js.map +1 -1
  37. package/dist/i18n/locales/en.js +16 -3
  38. package/dist/i18n/locales/en.js.map +2 -2
  39. package/dist/i18n/locales/es.js +12 -3
  40. package/dist/i18n/locales/es.js.map +1 -1
  41. package/dist/i18n/locales/fr.js +12 -3
  42. package/dist/i18n/locales/fr.js.map +1 -1
  43. package/dist/i18n/locales/hu.js +12 -3
  44. package/dist/i18n/locales/hu.js.map +1 -1
  45. package/dist/i18n/locales/it.js +12 -3
  46. package/dist/i18n/locales/it.js.map +1 -1
  47. package/dist/i18n/locales/zh.js +12 -3
  48. package/dist/i18n/locales/zh.js.map +1 -1
  49. package/dist/i18n/review.js +4 -0
  50. package/dist/i18n/review.js.map +1 -1
  51. package/dist/index.d.ts +1 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/lib/text-match.d.ts +16 -0
  54. package/dist/lib/text-match.js +11 -1
  55. package/dist/lib/text-match.js.map +1 -1
  56. package/dist/settings/use-settings-focus.d.ts +7 -0
  57. package/dist/settings/use-settings-focus.js +2 -0
  58. package/dist/settings/use-settings-focus.js.map +1 -1
  59. package/dist/shell/dev-menu.d.ts +5 -0
  60. package/dist/shell/dev-menu.js +2 -1
  61. package/dist/shell/dev-menu.js.map +1 -1
  62. package/dist/shell/topbar-action-menu.d.ts +14 -1
  63. package/dist/shell/topbar-action-menu.js +85 -34
  64. package/dist/shell/topbar-action-menu.js.map +1 -1
  65. package/dist/shell.d.ts +1 -1
  66. package/dist/shell.js +2 -1
  67. package/dist/shell.js.map +1 -1
  68. package/package.json +1 -1
  69. package/src/components/combobox-core.tsx +5 -5
  70. package/src/components/currency-option-list.tsx +4 -5
  71. package/src/components/data-table-fit.ts +7 -5
  72. package/src/components/data-table-head.tsx +5 -1
  73. package/src/components/data-table.tsx +8 -0
  74. package/src/components/grouped-picker.tsx +15 -8
  75. package/src/components/icon-picker.tsx +6 -4
  76. package/src/components/inline-entity-combobox.tsx +5 -4
  77. package/src/components/legal-labels.ts +8 -1
  78. package/src/components/legal-page.tsx +72 -2
  79. package/src/components/legal.tsx +80 -5
  80. package/src/components/list-labels.ts +2 -0
  81. package/src/components/list.tsx +52 -2
  82. package/src/feedback/feedback-table.tsx +17 -1
  83. package/src/i18n/german.ts +10 -1
  84. package/src/i18n/locales/en.ts +4 -0
  85. package/src/i18n/locales/es.ts +10 -1
  86. package/src/i18n/locales/fr.ts +10 -1
  87. package/src/i18n/locales/hu.ts +10 -1
  88. package/src/i18n/locales/it.ts +10 -1
  89. package/src/i18n/locales/zh.ts +10 -1
  90. package/src/i18n/review.ts +4 -0
  91. package/src/index.ts +1 -0
  92. package/src/lib/text-match.ts +30 -0
  93. package/src/settings/use-settings-focus.ts +8 -0
  94. package/src/shell/dev-menu.tsx +6 -0
  95. package/src/shell/topbar-action-menu.tsx +122 -38
  96. package/src/shell.ts +1 -1
@@ -1,5 +1,5 @@
1
1
  import { useId, useLayoutEffect, useRef } from "react";
2
- import type { KeyboardEvent as ReactKeyboardEvent, ReactNode } from "react";
2
+ import type { CSSProperties, KeyboardEvent as ReactKeyboardEvent, ReactNode } from "react";
3
3
  import { Link } from "react-router";
4
4
  import { cn } from "../lib/cn";
5
5
  import { HoverMenu, type HoverMenuProps } from "../components/hover-menu";
@@ -109,6 +109,40 @@ function toBlocks(entries: readonly TopBarMenuEntry[]): Block[] {
109
109
  return blocks.filter((block) => block.kind === "loose" || block.rows.length > 0);
110
110
  }
111
111
 
112
+ /** One column of `columns` (0.51.4): a group and the loose entries after it, up to the
113
+ * next group — so a group's own way out behind a rule (DevMenu's "Back to {role}") stays
114
+ * under it instead of running across the panel. */
115
+ type LooseBlock = Extract<Block, { kind: "loose" }>;
116
+ type Column = { group: Extract<Block, { kind: "group" }>; after: Array<RowEntry | DividerEntry>; index: number };
117
+
118
+ /** `blocks` as the columns layout draws them: what comes before the first group, then a
119
+ * column per group. A rule that would end a column is dropped: the column's edge is the
120
+ * separation there. */
121
+ function toColumns(blocks: readonly Block[]): { lead: LooseBlock[]; columns: Column[] } {
122
+ const lead: LooseBlock[] = [];
123
+ const columns: Column[] = [];
124
+ blocks.forEach((block, index) => {
125
+ if (block.kind === "group") columns.push({ group: block, after: [], index });
126
+ else if (columns.length === 0) lead.push(block);
127
+ else columns[columns.length - 1]!.after.push(block.entry);
128
+ });
129
+ for (const column of columns) {
130
+ while (column.after.at(-1)?.kind === "divider") column.after.pop();
131
+ }
132
+ return { lead, columns };
133
+ }
134
+
135
+ /**
136
+ * `columns`' panel: each column 14rem, the panel as wide as its columns (a CSS variable
137
+ * the list sets) but never wider than the glass, and from `md` up only. The grid places
138
+ * as many 14rem columns as that width holds, so a narrow desktop wraps the groups onto a
139
+ * second row instead of scrolling sideways. The header, the title and the footer span
140
+ * the row.
141
+ */
142
+ const COLUMNS_PANEL =
143
+ "md:grid md:grid-cols-[repeat(auto-fill,minmax(14rem,1fr))] md:items-start md:gap-y-2 md:w-[min(calc(100vw-2rem),var(--menu-columns-width))]";
144
+ const SPAN_ALL = "md:col-span-full";
145
+
112
146
  /**
113
147
  * The non-interactive identity block at the top of an account menu — kastlan's name +
114
148
  * role, keksdose's display name + email + role chip. It is text, not a `menuitem`: the
@@ -225,6 +259,7 @@ export function TopBarActionMenu({
225
259
  align,
226
260
  panelClassName,
227
261
  scrollable = false,
262
+ columns = false,
228
263
  triggerAttributes,
229
264
  }: {
230
265
  /** The trigger's face for the usual icon button. Ignored when `trigger` is given. */
@@ -308,6 +343,19 @@ export function TopBarActionMenu({
308
343
  * header and the footer are inside the list and scroll with the rows.
309
344
  */
310
345
  scrollable?: boolean;
346
+ /**
347
+ * From `md` up, lay the heading groups side by side, one column per group (0.51.4,
348
+ * `DevMenu`; keksdose dev #640, Marcel: "Each group gets a column. That way it would be
349
+ * much better visible for desktop users"). Below `md`, the one column as before.
350
+ *
351
+ * A column is a group and the loose rows after it up to the next heading, so a row
352
+ * behind a rule at the end of a group (DevMenu's "Back to {role}") stays in that
353
+ * group's column. Loose rows before the first heading, the header, the menu-wide
354
+ * `heading` and the footer span the panel. The DOM order is unchanged, so ↓/↑ walk the
355
+ * rows down one column and on into the next, as a reader reads them. `leading` and
356
+ * `children` rows take a cell each; add `md:col-span-full` to one that should span.
357
+ */
358
+ columns?: boolean;
311
359
  /** `data-*` attributes for the trigger itself — a tour's anchor (`data-tour`), a test
312
360
  * hook. The parts built on this menu (`TourMenu`) pass theirs through here. */
313
361
  triggerAttributes?: { [key: `data-${string}`]: string | number | boolean | undefined };
@@ -316,6 +364,10 @@ export function TopBarActionMenu({
316
364
  // entry's `key`, which is the caller's text and may hold a space — and a space in
317
365
  // `aria-labelledby` is a list separator.
318
366
  const uid = useId();
367
+ const blocks = toBlocks(entries);
368
+ const grouped = columns ? toColumns(blocks) : null;
369
+ // A menu with no group has nothing to put side by side: the list as always.
370
+ const laidOut = grouped && grouped.columns.length > 0 ? grouped : null;
319
371
  return (
320
372
  <HoverMenu
321
373
  aria-label={ariaLabel}
@@ -380,8 +432,8 @@ export function TopBarActionMenu({
380
432
  const footerClass =
381
433
  "flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted [&_a:hover]:text-primary [&_a]:rounded-sm [&_a:focus-visible]:outline-none [&_a:focus-visible]:ring-focus [&_a:focus-visible]:ring-brand";
382
434
  /** One action or link row — loose, or inside a heading's group. */
383
- const row = (entry: RowEntry) => (
384
- <li key={entry.key}>
435
+ const row = (entry: RowEntry, className?: string) => (
436
+ <li key={entry.key} className={className}>
385
437
  {entry.kind === "link" ? (
386
438
  <MenuItem
387
439
  href={entry.to}
@@ -413,11 +465,46 @@ export function TopBarActionMenu({
413
465
  )}
414
466
  </li>
415
467
  );
468
+ /** A rule or a row outside any group; `span` stretches it across the columns. */
469
+ const loose = (block: LooseBlock, span: boolean) => {
470
+ const entry = block.entry;
471
+ if (entry.kind === "divider") {
472
+ return <li key={entry.key} className={cn("my-1 border-t border-subtle", span && SPAN_ALL)} />;
473
+ }
474
+ return row(entry, span ? SPAN_ALL : undefined);
475
+ };
476
+ /** A heading's group: its name, then its rows in a named `role="group"`. */
477
+ const group = (block: Extract<Block, { kind: "group" }>, i: number, tight: boolean, inColumn = false) => {
478
+ const headingId = `${uid}-group-${i}`;
479
+ return (
480
+ <li key={block.heading.key} role="none">
481
+ <div
482
+ id={headingId}
483
+ role="presentation"
484
+ className={cn(
485
+ "px-3 pb-0.5 text-caption font-semibold uppercase tracking-wide text-muted [overflow-wrap:anywhere]",
486
+ // A column's heading lines up with its neighbours' at the top of the
487
+ // grid row; under `md` the columns stack, and it spaces like the list's.
488
+ tight ? "pt-1" : cn("pt-2.5", inColumn && "md:pt-1"),
489
+ )}
490
+ >
491
+ {block.heading.label}
492
+ </div>
493
+ <ul role="group" aria-labelledby={headingId}>
494
+ {block.rows.map((entry) => row(entry))}
495
+ </ul>
496
+ </li>
497
+ );
498
+ };
416
499
  return (
417
500
  // eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- a Tab bubbling up from a row or footer link, see keepTabInsidePanel
418
- <ul className={cn("py-1", scrollable && SCROLLABLE_PANEL, panelClassName)} onKeyDown={keepTabInsidePanel}>
501
+ <ul
502
+ className={cn("py-1", scrollable && SCROLLABLE_PANEL, laidOut && COLUMNS_PANEL, panelClassName)}
503
+ style={laidOut ? ({ "--menu-columns-width": `${laidOut.columns.length * 14}rem` } as CSSProperties) : undefined}
504
+ onKeyDown={keepTabInsidePanel}
505
+ >
419
506
  {header && (
420
- <MenuSection className="mb-1 border-b border-subtle px-3 py-2">
507
+ <MenuSection className={cn("mb-1 border-b border-subtle px-3 py-2", laidOut && SPAN_ALL)}>
421
508
  <div className="truncate text-sm font-medium text-primary">{header.title}</div>
422
509
  {header.subtitle != null && (
423
510
  <div className="truncate text-xs text-muted">{header.subtitle}</div>
@@ -426,45 +513,42 @@ export function TopBarActionMenu({
426
513
  </MenuSection>
427
514
  )}
428
515
  {heading && (
429
- <li className="px-3 py-1 text-micro font-semibold uppercase tracking-wide text-placeholder">
516
+ <li
517
+ className={cn(
518
+ "px-3 py-1 text-micro font-semibold uppercase tracking-wide text-placeholder",
519
+ laidOut && SPAN_ALL,
520
+ )}
521
+ >
430
522
  {heading}
431
523
  </li>
432
524
  )}
433
525
  {leading?.(close)}
434
- {toBlocks(entries).map((block, i, blocks) => {
435
- if (block.kind === "loose") {
436
- const entry = block.entry;
437
- if (entry.kind === "divider") {
438
- return <li key={entry.key} className="my-1 border-t border-subtle" />;
439
- }
440
- return row(entry);
441
- }
442
- const headingId = `${uid}-group-${i}`;
443
- const before = blocks[i - 1];
444
- // Under a rule (or at the top of the rows) the rule's margin is the gap;
445
- // straight under another group's last row the heading makes its own.
446
- const tight = !before || (before.kind === "loose" && before.entry.kind === "divider");
447
- return (
448
- <li key={block.heading.key} role="none">
449
- <div
450
- id={headingId}
451
- role="presentation"
452
- className={cn(
453
- "px-3 pb-0.5 text-caption font-semibold uppercase tracking-wide text-muted [overflow-wrap:anywhere]",
454
- tight ? "pt-1" : "pt-2.5",
455
- )}
456
- >
457
- {block.heading.label}
458
- </div>
459
- <ul role="group" aria-labelledby={headingId}>
460
- {block.rows.map(row)}
461
- </ul>
462
- </li>
463
- );
464
- })}
526
+ {laidOut ? (
527
+ <>
528
+ {laidOut.lead.map((block) => loose(block, true))}
529
+ {laidOut.columns.map((column, c) => (
530
+ // A column of the grid; under `md` a group as in the list, its rows
531
+ // following.
532
+ <li key={column.group.heading.key} role="none" data-menu-column="" className="min-w-0">
533
+ <ul role="none">
534
+ {group(column.group, column.index, c === 0 && laidOut.lead.length === 0, true)}
535
+ {column.after.map((entry) => loose({ kind: "loose", entry }, false))}
536
+ </ul>
537
+ </li>
538
+ ))}
539
+ </>
540
+ ) : (
541
+ blocks.map((block, i) => {
542
+ if (block.kind === "loose") return loose(block, false);
543
+ const before = blocks[i - 1];
544
+ // Under a rule (or at the top of the rows) the rule's margin is the gap;
545
+ // straight under another group's last row the heading makes its own.
546
+ return group(block, i, !before || (before.kind === "loose" && before.entry.kind === "divider"));
547
+ })
548
+ )}
465
549
  {children?.(close)}
466
550
  {footerBody != null && footerBody !== false && (
467
- <MenuSection className="mt-1 border-t border-subtle px-3 py-2">
551
+ <MenuSection className={cn("mt-1 border-t border-subtle px-3 py-2", laidOut && SPAN_ALL)}>
468
552
  {footerLabel ? (
469
553
  <nav aria-label={footerLabel} className={footerClass}>
470
554
  {footerBody}
package/src/shell.ts CHANGED
@@ -41,7 +41,7 @@ export type {
41
41
  SettingsLocationRules,
42
42
  SettingsLocationResult,
43
43
  } from "./settings/use-settings-route";
44
- export { useSettingsFocus, SETTINGS_FOCUS_RING, SETTINGS_FOCUS_MS } from "./settings/use-settings-focus";
44
+ export { useSettingsFocus, useFocusParam, SETTINGS_FOCUS_RING, SETTINGS_FOCUS_MS } from "./settings/use-settings-focus";
45
45
  export { SettingsSection } from "./settings/settings-section";
46
46
  export type { SettingsSectionProps } from "./settings/settings-section";
47
47
  export { useSettingsLayout } from "./settings/settings-context";