@mindlogic-ai/logician-ui 5.0.0-alpha.12 → 5.0.0-alpha.13

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 (77) hide show
  1. package/README.md +1 -0
  2. package/dist/components/NavList/NavList.d.ts +34 -0
  3. package/dist/components/NavList/NavList.d.ts.map +1 -0
  4. package/dist/components/NavList/NavList.js +113 -0
  5. package/dist/components/NavList/NavList.js.map +1 -0
  6. package/dist/components/NavList/NavList.mjs +111 -0
  7. package/dist/components/NavList/NavList.mjs.map +1 -0
  8. package/dist/components/NavList/NavList.styles.d.ts +90 -0
  9. package/dist/components/NavList/NavList.styles.d.ts.map +1 -0
  10. package/dist/components/NavList/NavList.styles.js +104 -0
  11. package/dist/components/NavList/NavList.styles.js.map +1 -0
  12. package/dist/components/NavList/NavList.styles.mjs +97 -0
  13. package/dist/components/NavList/NavList.styles.mjs.map +1 -0
  14. package/dist/components/NavList/NavList.translations.json.js +19 -0
  15. package/dist/components/NavList/NavList.translations.json.js.map +1 -0
  16. package/dist/components/NavList/NavList.translations.json.mjs +14 -0
  17. package/dist/components/NavList/NavList.translations.json.mjs.map +1 -0
  18. package/dist/components/NavList/NavList.types.d.ts +131 -0
  19. package/dist/components/NavList/NavList.types.d.ts.map +1 -0
  20. package/dist/components/NavList/NavListContext.d.ts +30 -0
  21. package/dist/components/NavList/NavListContext.d.ts.map +1 -0
  22. package/dist/components/NavList/NavListContext.js +16 -0
  23. package/dist/components/NavList/NavListContext.js.map +1 -0
  24. package/dist/components/NavList/NavListContext.mjs +13 -0
  25. package/dist/components/NavList/NavListContext.mjs.map +1 -0
  26. package/dist/components/NavList/NavListGroup.d.ts +31 -0
  27. package/dist/components/NavList/NavListGroup.d.ts.map +1 -0
  28. package/dist/components/NavList/NavListGroup.js +115 -0
  29. package/dist/components/NavList/NavListGroup.js.map +1 -0
  30. package/dist/components/NavList/NavListGroup.mjs +113 -0
  31. package/dist/components/NavList/NavListGroup.mjs.map +1 -0
  32. package/dist/components/NavList/NavListItem.d.ts +15 -0
  33. package/dist/components/NavList/NavListItem.d.ts.map +1 -0
  34. package/dist/components/NavList/NavListItem.js +269 -0
  35. package/dist/components/NavList/NavListItem.js.map +1 -0
  36. package/dist/components/NavList/NavListItem.mjs +267 -0
  37. package/dist/components/NavList/NavListItem.mjs.map +1 -0
  38. package/dist/components/NavList/NavListSkeleton.d.ts +17 -0
  39. package/dist/components/NavList/NavListSkeleton.d.ts.map +1 -0
  40. package/dist/components/NavList/NavListSkeleton.js +33 -0
  41. package/dist/components/NavList/NavListSkeleton.js.map +1 -0
  42. package/dist/components/NavList/NavListSkeleton.mjs +31 -0
  43. package/dist/components/NavList/NavListSkeleton.mjs.map +1 -0
  44. package/dist/components/NavList/index.d.ts +7 -0
  45. package/dist/components/NavList/index.d.ts.map +1 -0
  46. package/dist/index.d.ts +2 -1
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +12 -0
  49. package/dist/index.js.map +1 -1
  50. package/dist/index.mjs +6 -1
  51. package/dist/index.mjs.map +1 -1
  52. package/dist/utils/focusRing.d.ts +22 -0
  53. package/dist/utils/focusRing.d.ts.map +1 -1
  54. package/dist/utils/focusRing.js +22 -0
  55. package/dist/utils/focusRing.js.map +1 -1
  56. package/dist/utils/focusRing.mjs +21 -1
  57. package/dist/utils/focusRing.mjs.map +1 -1
  58. package/package.json +2 -1
  59. package/src/components/Menu/Menu.usage.mdx +8 -0
  60. package/src/components/NavList/NavList.stories.tsx +895 -0
  61. package/src/components/NavList/NavList.styles.ts +114 -0
  62. package/src/components/NavList/NavList.test.tsx +1254 -0
  63. package/src/components/NavList/NavList.translations.json +9 -0
  64. package/src/components/NavList/NavList.tsx +160 -0
  65. package/src/components/NavList/NavList.types.ts +145 -0
  66. package/src/components/NavList/NavList.usage.mdx +540 -0
  67. package/src/components/NavList/NavListContext.ts +25 -0
  68. package/src/components/NavList/NavListGroup.tsx +243 -0
  69. package/src/components/NavList/NavListItem.tsx +525 -0
  70. package/src/components/NavList/NavListSkeleton.tsx +57 -0
  71. package/src/components/NavList/index.ts +14 -0
  72. package/src/components/Tabs/Tabs.usage.mdx +58 -14
  73. package/src/components/Tooltip/Tooltip.usage.mdx +8 -0
  74. package/src/components/a11y.kwcag.test.tsx +66 -0
  75. package/src/components/claude.md +1 -1
  76. package/src/index.ts +7 -1
  77. package/src/utils/focusRing.ts +23 -0
@@ -0,0 +1,9 @@
1
+ {
2
+ "nav_list_opens_new_window": {
3
+ "ko": "새 창 열림",
4
+ "en": "opens in a new window",
5
+ "zh": "在新窗口中打开",
6
+ "ja": "新しいウィンドウで開く",
7
+ "es": "se abre en una ventana nueva"
8
+ }
9
+ }
@@ -0,0 +1,160 @@
1
+ import {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useLayoutEffect,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from 'react';
10
+ import { chakra, mergeRefs } from '@chakra-ui/react';
11
+
12
+ import { byOrientation, listStyles } from './NavList.styles';
13
+ import { NavListProps } from './NavList.types';
14
+ import { NavListContext } from './NavListContext';
15
+ import { NavListGroup } from './NavListGroup';
16
+ import { NavListItem } from './NavListItem';
17
+ import { NavListSkeleton } from './NavListSkeleton';
18
+
19
+ const useIsomorphicLayoutEffect =
20
+ typeof window === 'undefined' ? useEffect : useLayoutEffect;
21
+
22
+ /**
23
+ * Bring the current row into a sideways-scrolling list's visible range, by
24
+ * moving that list's own `scrollLeft` only — never `scrollIntoView`, which
25
+ * also scrolls the page and every scrolling ancestor. A no-op when the list
26
+ * does not scroll sideways (a vertical rail) or the row is already in view.
27
+ */
28
+ const revealCurrentHorizontally = (
29
+ list: HTMLUListElement,
30
+ current: Element
31
+ ) => {
32
+ if (list.scrollWidth <= list.clientWidth) return;
33
+ const listBox = list.getBoundingClientRect();
34
+ const rowBox = (current.closest('li') ?? current).getBoundingClientRect();
35
+ // Inside a closed group the row has no box; there is nothing to show.
36
+ if (rowBox.width === 0) return;
37
+ if (rowBox.left < listBox.left) {
38
+ list.scrollLeft -= listBox.left - rowBox.left;
39
+ } else if (rowBox.right > listBox.right) {
40
+ list.scrollLeft += rowBox.right - listBox.right;
41
+ }
42
+ };
43
+
44
+ /**
45
+ * A vertical list of rows for side navigation and in-page filter rails.
46
+ *
47
+ * Not tabs: there is no tablist, no `aria-selected` and no roving tabindex.
48
+ * Every row is its own link or button in the tab order, and the current one
49
+ * says so with `aria-current`.
50
+ *
51
+ * Named (`aria-label` / `aria-labelledby`), it is a `<nav>` landmark around
52
+ * the list; unnamed, it is just the list. A landmark should have a name once a
53
+ * page has more than one `nav`, and a list inside the app sidebar is already
54
+ * inside one — so the name is what decides whether this is a landmark of its
55
+ * own, rather than a flag the caller has to remember next to it.
56
+ *
57
+ * The ref and every other prop land on the `<ul>` either way — it is the flex
58
+ * and scroll container, so `gap`, `onScroll` and `ref.current.scrollLeft` mean
59
+ * the same thing whether or not the list is named.
60
+ */
61
+ const NavListRoot = forwardRef<HTMLUListElement, NavListProps>(
62
+ (
63
+ {
64
+ orientation = 'vertical',
65
+ isCollapsed = false,
66
+ children,
67
+ 'aria-label': ariaLabel,
68
+ 'aria-labelledby': ariaLabelledBy,
69
+ ...rest
70
+ },
71
+ ref
72
+ ) => {
73
+ const isLandmark = ariaLabel != null || ariaLabelledBy != null;
74
+ // A collapsed rail is a column of icons; it has no sideways form.
75
+ const axis = isCollapsed ? 'vertical' : orientation;
76
+ const listRef = useRef<HTMLUListElement>(null);
77
+ const mergedRef = useMemo(() => mergeRefs(ref, listRef), [ref]);
78
+
79
+ // Reveal the current row when it CHANGES (a row reports it from its own
80
+ // effect, so this works even when only that row re-rendered) and when the
81
+ // list is resized — a strip that just laid down at a breakpoint. Not on
82
+ // every render: a re-render must not snap back a strip the user scrolled.
83
+ const lastRevealed = useRef<Element | null>(null);
84
+ const reveal = useCallback((force = false) => {
85
+ const list = listRef.current;
86
+ const current = list?.querySelector('[data-current]');
87
+ if (!list || !current) return;
88
+ if (!force && current === lastRevealed.current) return;
89
+ lastRevealed.current = current;
90
+ revealCurrentHorizontally(list, current);
91
+ }, []);
92
+ useEffect(() => {
93
+ const list = listRef.current;
94
+ reveal();
95
+ if (!list || typeof ResizeObserver === 'undefined') return;
96
+ const observer = new ResizeObserver(() => reveal(true));
97
+ observer.observe(list);
98
+ return () => observer.disconnect();
99
+ }, [reveal]);
100
+
101
+ // Layout never reads this: rows, groups and the list inherit `direction`
102
+ // in CSS, so a right-to-left server render is right on its first paint.
103
+ // Only the rows' TOOLTIPS need it in JS — zag stamps `dir` on their
104
+ // portalled content from Ark's locale, and a popper placement is
105
+ // physical — and a tooltip only opens after hydration, when this is known.
106
+ const [tooltipDir, setTooltipDir] = useState<'ltr' | 'rtl'>('ltr');
107
+ useIsomorphicLayoutEffect(() => {
108
+ const list = listRef.current;
109
+ if (!list) return;
110
+ const next = getComputedStyle(list).direction === 'rtl' ? 'rtl' : 'ltr';
111
+ if (next !== tooltipDir) setTooltipDir(next);
112
+ });
113
+
114
+ const context = useMemo(
115
+ () => ({
116
+ orientation: axis,
117
+ isCollapsed,
118
+ tooltipDir,
119
+ revealCurrent: reveal,
120
+ }),
121
+ [axis, isCollapsed, tooltipDir, reveal]
122
+ );
123
+
124
+ const list = (
125
+ <chakra.ul
126
+ ref={mergedRef}
127
+ {...listStyles(axis)}
128
+ overflowX={byOrientation(axis, 'visible', 'auto')}
129
+ overflowY={byOrientation(axis, 'visible', 'hidden')}
130
+ {...rest}
131
+ >
132
+ {children}
133
+ </chakra.ul>
134
+ );
135
+
136
+ return (
137
+ <NavListContext.Provider value={context}>
138
+ {isLandmark ? (
139
+ <chakra.nav
140
+ aria-label={ariaLabel}
141
+ aria-labelledby={ariaLabelledBy}
142
+ minW={0}
143
+ >
144
+ {list}
145
+ </chakra.nav>
146
+ ) : (
147
+ list
148
+ )}
149
+ </NavListContext.Provider>
150
+ );
151
+ }
152
+ );
153
+
154
+ NavListRoot.displayName = 'NavList';
155
+
156
+ export const NavList = Object.assign(NavListRoot, {
157
+ Item: NavListItem,
158
+ Group: NavListGroup,
159
+ Skeleton: NavListSkeleton,
160
+ });
@@ -0,0 +1,145 @@
1
+ import type { MouseEventHandler, ReactNode } from 'react';
2
+ import type { HTMLChakraProps } from '@chakra-ui/react';
3
+
4
+ export type NavListOrientation = 'vertical' | 'horizontal';
5
+
6
+ type NavListBreakpoint = 'base' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
7
+
8
+ /**
9
+ * One orientation, or one per breakpoint. The media-template rails and the
10
+ * Drive source rail lie down into a sideways-scrolling row below `md`:
11
+ *
12
+ * ```tsx
13
+ * <NavList orientation={{ base: 'horizontal', md: 'vertical' }} />
14
+ * ```
15
+ *
16
+ * The switch is pure CSS (one rule per breakpoint), so it never renders the
17
+ * wrong axis first and corrects itself after hydration.
18
+ */
19
+ export type NavListResponsiveOrientation =
20
+ | NavListOrientation
21
+ | Partial<Record<NavListBreakpoint, NavListOrientation>>;
22
+
23
+ export type NavListProps = Omit<HTMLChakraProps<'ul'>, 'children'> & {
24
+ children?: ReactNode;
25
+ /**
26
+ * `vertical` is a rail; `horizontal` is a single row that scrolls sideways
27
+ * instead of wrapping. Accepts a per-breakpoint object.
28
+ * @default 'vertical'
29
+ */
30
+ orientation?: NavListResponsiveOrientation;
31
+ /**
32
+ * Naming the list makes it a `<nav>` landmark around the list. Leave it off
33
+ * when the list already sits inside a named landmark (the app sidebar) — a
34
+ * second, unnamed `nav` is noise in the landmark menu.
35
+ */
36
+ 'aria-label'?: string;
37
+ /** As `aria-label`, pointing at a visible heading. Also makes it a `<nav>`. */
38
+ 'aria-labelledby'?: string;
39
+ /**
40
+ * The collapsed rail: every row is a 40px icon-only square whose label is
41
+ * still its accessible name (visually hidden) and shows as a tooltip on
42
+ * hover and keyboard focus. `trailingVisual`, `trailingAction` and group
43
+ * titles are hidden; a collapsible group shows its rows. Every row needs a
44
+ * `leadingVisual` in this mode. Vertical only.
45
+ * @default false
46
+ */
47
+ isCollapsed?: boolean;
48
+ };
49
+
50
+ export type NavListTrailingActionVisibility = 'always' | 'hover';
51
+
52
+ export type NavListItemProps = Omit<
53
+ HTMLChakraProps<'a'>,
54
+ 'children' | 'onClick' | 'aria-current' | 'asChild'
55
+ > & {
56
+ /**
57
+ * The label. With `asChild`, the single link element (e.g. `next/link`)
58
+ * whose own children are the label.
59
+ */
60
+ children?: ReactNode;
61
+ /** Renders an `<a>`. Without `href` (and without `asChild`) the row is a `<button>`. */
62
+ href?: string;
63
+ /**
64
+ * Merge the row onto the one child element instead of rendering an `<a>` —
65
+ * for a router link: `<NavList.Item asChild><NextLink href="/x">X</NextLink></NavList.Item>`.
66
+ * The row is treated as a link.
67
+ */
68
+ asChild?: boolean;
69
+ onClick?: MouseEventHandler<HTMLElement>;
70
+ /**
71
+ * This row is where the user is: `aria-current="page"` on a link,
72
+ * `aria-current="true"` on a button (the active filter of a rail). Shown as
73
+ * an ink label, bold, on a `bg.emphasized` fill.
74
+ */
75
+ isCurrent?: boolean;
76
+ /**
77
+ * A button becomes `disabled`. A link — `asChild` included — is rendered
78
+ * as an `<a role="link" aria-disabled="true">` with no `href`: still
79
+ * announced as a link, out of the tab order, and nothing to open in a new tab.
80
+ */
81
+ isDisabled?: boolean;
82
+ /** Icon before the label. Decorative — the label names the row. */
83
+ leadingVisual?: ReactNode;
84
+ /** Count or badge after the label, in muted text. Read as part of the row's name. */
85
+ trailingVisual?: ReactNode;
86
+ /**
87
+ * A separate control beside the row (more-menu, remove), e.g. an
88
+ * `IconButton` with its own `aria-label`. It is a sibling of the row, never
89
+ * inside it, so there is no interactive element nested in another.
90
+ */
91
+ trailingAction?: ReactNode;
92
+ /**
93
+ * `hover`: the trailing action is transparent until the row is hovered, or
94
+ * focus is anywhere in the row, or the action's own menu is open
95
+ * (`aria-expanded="true"` / `data-state="open"` inside it). It is never
96
+ * removed, so Tab still reaches it — focusing it is what reveals it. Always
97
+ * shown on touch screens (`hover: none`), where there is no hover to reveal it.
98
+ * @default 'always'
99
+ */
100
+ trailingActionVisibility?: NavListTrailingActionVisibility;
101
+ /**
102
+ * A tooltip that adds to the label — a description, or why a row is
103
+ * disabled. It is also the row's accessible DESCRIPTION, through a hidden
104
+ * element that is referenced whether or not the tooltip is open — so a
105
+ * disabled row, which cannot take focus, still says why. It must not repeat
106
+ * the label. In a collapsed list it shows under the label.
107
+ *
108
+ * Label tooltips need no prop: a label cut off by the ellipsis shows itself
109
+ * in full on hover/focus, and every row of a collapsed list shows its label.
110
+ * Those are not wired as a description, because they would read the name twice.
111
+ */
112
+ tooltip?: ReactNode;
113
+ };
114
+
115
+ export type NavListGroupProps = Omit<
116
+ HTMLChakraProps<'li'>,
117
+ 'children' | 'title'
118
+ > & {
119
+ children?: ReactNode;
120
+ /**
121
+ * Text above the group's rows, in overline style. Not interactive and not a
122
+ * heading element — it names the group's list (`aria-labelledby`), so it
123
+ * does not add a level to the page outline.
124
+ */
125
+ title?: ReactNode;
126
+ /**
127
+ * The title becomes a `<button aria-expanded>` that shows and hides the
128
+ * group's rows. Needs a `title`. Persisting the state (local storage, a
129
+ * store) is the caller's: pass `isOpen` + `onOpenChange`.
130
+ * @default false
131
+ */
132
+ isCollapsible?: boolean;
133
+ /** Controlled open state of a collapsible group. */
134
+ isOpen?: boolean;
135
+ /** Uncontrolled initial state of a collapsible group. @default true */
136
+ defaultOpen?: boolean;
137
+ onOpenChange?: (isOpen: boolean) => void;
138
+ };
139
+
140
+ export type NavListSkeletonProps = Omit<HTMLChakraProps<'ul'>, 'children'> & {
141
+ /** How many placeholder rows. @default 5 */
142
+ count?: number;
143
+ /** Lay the placeholders out like the list they stand in for. @default 'vertical' */
144
+ orientation?: NavListResponsiveOrientation;
145
+ };