@godxjp/ui 28.6.0 → 28.8.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 (99) hide show
  1. package/dist/components/data-display/data-table.d.ts +10 -2
  2. package/dist/components/data-display/data-table.js +12 -5
  3. package/dist/components/data-display/index.d.ts +2 -0
  4. package/dist/components/data-display/index.js +2 -0
  5. package/dist/components/data-display/marquee.d.ts +16 -0
  6. package/dist/components/data-display/marquee.js +155 -0
  7. package/dist/components/data-display/scroll-area.js +16 -2
  8. package/dist/components/data-display/service-launcher-card.d.ts +25 -0
  9. package/dist/components/data-display/table.d.ts +38 -1
  10. package/dist/components/data-display/table.js +57 -39
  11. package/dist/components/data-entry/form-field.js +29 -12
  12. package/dist/components/general/reveal.d.ts +23 -2
  13. package/dist/components/general/reveal.js +37 -7
  14. package/dist/components/general/typography.d.ts +4 -1
  15. package/dist/components/general/typography.js +14 -1
  16. package/dist/components/layout/affix.d.ts +86 -0
  17. package/dist/components/layout/affix.js +187 -0
  18. package/dist/components/layout/index.d.ts +4 -0
  19. package/dist/components/layout/index.js +4 -0
  20. package/dist/components/layout/legal-document-shell.js +4 -3
  21. package/dist/components/layout/masonry.d.ts +74 -0
  22. package/dist/components/layout/masonry.js +214 -0
  23. package/dist/components/layout/page-container.js +5 -20
  24. package/dist/components/navigation/anchor.d.ts +64 -0
  25. package/dist/components/navigation/anchor.js +284 -0
  26. package/dist/components/navigation/app-setting-picker.js +20 -3
  27. package/dist/components/navigation/index.d.ts +4 -0
  28. package/dist/components/navigation/index.js +4 -0
  29. package/dist/components/navigation/mega-menu.d.ts +21 -0
  30. package/dist/components/navigation/mega-menu.js +526 -0
  31. package/dist/components/ui/table.js +1 -0
  32. package/dist/contracts/measurement.json +1 -1
  33. package/dist/i18n/messages/en.json +522 -1
  34. package/dist/i18n/messages/ja.json +518 -1
  35. package/dist/i18n/messages/vi.json +518 -1
  36. package/dist/lib/hooks.d.ts +91 -0
  37. package/dist/lib/hooks.js +79 -0
  38. package/dist/lib/platform.d.ts +14 -0
  39. package/dist/lib/platform.js +10 -1
  40. package/dist/lib/utils.d.ts +1 -1
  41. package/dist/lib/utils.js +3 -2
  42. package/dist/props/components/data-display.prop.d.ts +109 -1
  43. package/dist/props/components/general.prop.d.ts +47 -3
  44. package/dist/props/components/layout.prop.d.ts +194 -0
  45. package/dist/props/components/navigation.prop.d.ts +263 -0
  46. package/dist/props/registry.d.ts +360 -5
  47. package/dist/props/registry.js +474 -4
  48. package/dist/props/vocabulary/index.d.ts +1 -1
  49. package/dist/props/vocabulary/interaction.prop.d.ts +39 -2
  50. package/dist/styles/control.css +5 -6
  51. package/dist/styles/data-display-layout.css +2 -1
  52. package/dist/styles/density.css +4 -0
  53. package/dist/styles/form-layout.css +22 -0
  54. package/dist/styles/layout.css +79 -1
  55. package/dist/styles/motion.css +121 -1
  56. package/dist/styles/navigation-layout.css +415 -20
  57. package/dist/styles/shell-layout.css +3 -0
  58. package/dist/styles/text-layout.css +52 -4
  59. package/dist/tokens/base.css +5 -0
  60. package/dist/tokens/components/affix.css +7 -0
  61. package/dist/tokens/components/anchor.css +17 -0
  62. package/dist/tokens/components/control.css +3 -3
  63. package/dist/tokens/components/form.css +3 -1
  64. package/dist/tokens/components/marquee.css +7 -0
  65. package/dist/tokens/components/masonry.css +6 -0
  66. package/dist/tokens/components/mega-menu.css +62 -0
  67. package/dist/tokens/components/navigation.css +2 -0
  68. package/dist/tokens/components/shell.css +3 -0
  69. package/dist/tokens/foundation.css +11 -0
  70. package/dist/tokens/semantic/layout.css +7 -0
  71. package/docs/COMPOSITION-VS-COMPONENT.md +21 -0
  72. package/docs/DESIGN-AUTHORITY.md +124 -23
  73. package/docs/FRAME-COVERAGE-REPORT.md +7 -2
  74. package/docs/data-display/marquee.tsx +254 -0
  75. package/docs/data-display/scroll-area.tsx +31 -0
  76. package/docs/data-display/service-launcher-card.tsx +275 -115
  77. package/docs/foundation/_theme-editor-scope.ts +222 -0
  78. package/docs/foundation/density.tsx +12 -2
  79. package/docs/foundation/spacing.tsx +5 -0
  80. package/docs/foundation/theme-editor.tsx +645 -0
  81. package/docs/general/activity.tsx +65 -0
  82. package/docs/general/reveal.tsx +290 -22
  83. package/docs/general/typography.tsx +91 -1
  84. package/docs/layout/affix.tsx +209 -0
  85. package/docs/layout/masonry.tsx +291 -0
  86. package/docs/navigation/anchor.tsx +285 -0
  87. package/docs/navigation/mega-menu-panel.tsx +86 -0
  88. package/docs/navigation/mega-menu.tsx +254 -0
  89. package/docs/roadmap/website-components.md +779 -0
  90. package/docs/showcase/acme-website.tsx +75 -39
  91. package/docs/showcase/futurelastic-web.tsx +91 -49
  92. package/docs/showcase/marketing-page.tsx +885 -0
  93. package/docs/showcase/table-footer-totals.tsx +12 -2
  94. package/docs/showcase/theme-customization.tsx +1259 -0
  95. package/package.json +8 -5
  96. package/scripts/brand-accent.generated.mjs +27 -0
  97. package/scripts/ui-audit.mjs +66 -0
  98. package/scripts/visual-audit-rules.mjs +46 -2
  99. package/scripts/visual-audit.mjs +29 -1
@@ -0,0 +1,214 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { isDevelopment } from "../../lib/dev.js";
5
+ import { useMediaQuery } from "../../lib/hooks.js";
6
+ import { cn } from "../../lib/utils.js";
7
+ const BREAKPOINT_QUERY = {
8
+ sm: "(min-width: 40rem)",
9
+ md: "(min-width: 48rem)",
10
+ lg: "(min-width: 64rem)",
11
+ xl: "(min-width: 80rem)"
12
+ };
13
+ const BREAKPOINT_ORDER = ["xl", "lg", "md", "sm"];
14
+ const COLUMN_STEPS = /* @__PURE__ */ new Set(["base", ...BREAKPOINT_ORDER]);
15
+ const DEFAULT_COLUMNS = 3;
16
+ function gapToken(step, axis) {
17
+ if (step === "none" || step === 0) return "0px";
18
+ if (typeof step === "number") return `var(--space-${step})`;
19
+ return axis === "inline" ? `var(--space-inline-${step})` : `var(--space-stack-${step})`;
20
+ }
21
+ const EMPTY_METRICS = { rowGap: 0, items: [] };
22
+ function sameMetrics(a, b) {
23
+ if (a.rowGap !== b.rowGap || a.items.length !== b.items.length) return false;
24
+ return a.items.every((metric, index) => {
25
+ const other = b.items[index];
26
+ return metric[0] === other[0] && metric[1] === other[1] && metric[2] === other[2];
27
+ });
28
+ }
29
+ function packColumns(metrics, columnCount, rowGap) {
30
+ const columnHeights = new Array(columnCount).fill(0);
31
+ const positions = /* @__PURE__ */ new Map();
32
+ for (const [key, height, pinned] of metrics) {
33
+ const shortest = columnHeights.indexOf(Math.min(...columnHeights));
34
+ const target = pinned === void 0 ? shortest : Math.min(Math.max(Math.trunc(pinned), 0), columnCount - 1);
35
+ positions.set(key, { column: target, top: columnHeights[target] });
36
+ columnHeights[target] += height + rowGap;
37
+ }
38
+ return { positions, totalHeight: Math.max(0, Math.max(...columnHeights, 0) - rowGap) };
39
+ }
40
+ const Masonry = React.forwardRef(function Masonry2({
41
+ items,
42
+ itemRender,
43
+ columns = DEFAULT_COLUMNS,
44
+ gap,
45
+ gutter,
46
+ fresh = false,
47
+ onLayoutChange,
48
+ id,
49
+ className,
50
+ ...rest
51
+ }, ref) {
52
+ const rootRef = React.useRef(null);
53
+ const itemRefs = React.useRef(/* @__PURE__ */ new Map());
54
+ const matched = {
55
+ sm: useMediaQuery(BREAKPOINT_QUERY.sm),
56
+ md: useMediaQuery(BREAKPOINT_QUERY.md),
57
+ lg: useMediaQuery(BREAKPOINT_QUERY.lg),
58
+ xl: useMediaQuery(BREAKPOINT_QUERY.xl)
59
+ };
60
+ if (isDevelopment()) {
61
+ if (gutter !== void 0) {
62
+ console.warn(
63
+ '[@godxjp/ui] Masonry: `gutter` is Ant Design\'s name for this axis; here it is `gap`, and it takes a GapProp token step (`"sm"`, `4`, `["md", "lg"]`) rather than a pixel number. The value was IGNORED.'
64
+ );
65
+ }
66
+ if (columns !== null && typeof columns === "object") {
67
+ const unknown = Object.keys(columns).filter((step) => !COLUMN_STEPS.has(step));
68
+ if (unknown.length > 0) {
69
+ console.warn(
70
+ `[@godxjp/ui] Masonry: \`columns\` has no step ${unknown.join(", ")} \u2014 the steps here are base | sm | md | lg | xl. Ant Design's \`xs\` is \`base\`, and there is no \`xxl\`. The key(s) were IGNORED.`
71
+ );
72
+ }
73
+ }
74
+ }
75
+ const columnCount = React.useMemo(() => {
76
+ const resolved = (() => {
77
+ if (typeof columns === "number") return columns;
78
+ for (const step of BREAKPOINT_ORDER) {
79
+ if (matched[step] && columns[step] !== void 0) return columns[step];
80
+ }
81
+ return columns.base ?? 1;
82
+ })();
83
+ return Math.max(1, Math.trunc(resolved) || 1);
84
+ }, [columns, matched.sm, matched.md, matched.lg, matched.xl]);
85
+ const [inlineGap, blockGap] = React.useMemo(() => {
86
+ if (gap === void 0) return [void 0, void 0];
87
+ return Array.isArray(gap) ? [gap[0], gap[1]] : [gap, gap];
88
+ }, [gap]);
89
+ const [metrics, setMetrics] = React.useState(EMPTY_METRICS);
90
+ const measure = React.useCallback(() => {
91
+ const root = rootRef.current;
92
+ if (!root) return;
93
+ const rowGap = Number.parseFloat(window.getComputedStyle(root).rowGap) || 0;
94
+ const next = (items ?? []).map((item, index) => {
95
+ const key = item.key ?? index;
96
+ if (typeof item.height === "number" && Number.isFinite(item.height)) {
97
+ return [key, item.height, item.column];
98
+ }
99
+ const element = itemRefs.current.get(key);
100
+ return [key, element ? element.getBoundingClientRect().height : 0, item.column];
101
+ });
102
+ setMetrics((previous) => {
103
+ const candidate = { rowGap, items: next };
104
+ return sameMetrics(previous, candidate) ? previous : candidate;
105
+ });
106
+ }, [items]);
107
+ const frameRef = React.useRef(null);
108
+ const scheduleMeasure = React.useCallback(() => {
109
+ if (typeof window === "undefined") return;
110
+ if (frameRef.current !== null) window.cancelAnimationFrame(frameRef.current);
111
+ frameRef.current = window.requestAnimationFrame(() => {
112
+ frameRef.current = null;
113
+ measure();
114
+ });
115
+ }, [measure]);
116
+ React.useEffect(
117
+ () => () => {
118
+ if (frameRef.current !== null && typeof window !== "undefined") {
119
+ window.cancelAnimationFrame(frameRef.current);
120
+ }
121
+ },
122
+ []
123
+ );
124
+ React.useLayoutEffect(() => {
125
+ measure();
126
+ }, [measure, columnCount, inlineGap, blockGap]);
127
+ React.useEffect(() => {
128
+ const root = rootRef.current;
129
+ if (!root) return void 0;
130
+ root.addEventListener("load", scheduleMeasure, true);
131
+ root.addEventListener("error", scheduleMeasure, true);
132
+ let observer;
133
+ if (typeof ResizeObserver !== "undefined") {
134
+ observer = new ResizeObserver(scheduleMeasure);
135
+ observer.observe(root);
136
+ if (fresh) {
137
+ for (const element of itemRefs.current.values()) {
138
+ if (element) observer.observe(element);
139
+ }
140
+ }
141
+ }
142
+ return () => {
143
+ root.removeEventListener("load", scheduleMeasure, true);
144
+ root.removeEventListener("error", scheduleMeasure, true);
145
+ observer?.disconnect();
146
+ };
147
+ }, [scheduleMeasure, fresh, items]);
148
+ const { positions, totalHeight } = React.useMemo(
149
+ () => packColumns(metrics.items, columnCount, metrics.rowGap),
150
+ [metrics, columnCount]
151
+ );
152
+ const lastLayoutRef = React.useRef(null);
153
+ React.useEffect(() => {
154
+ if (!onLayoutChange || !items || items.length === 0) return;
155
+ if (items.length !== positions.size) return;
156
+ if (!items.every((item, index) => positions.has(item.key ?? index))) return;
157
+ const layout = items.map((item, index) => ({
158
+ ...item,
159
+ column: positions.get(item.key ?? index).column
160
+ }));
161
+ const signature = layout.map((entry) => `${String(entry.key)}:${entry.column}`).join("|");
162
+ if (signature === lastLayoutRef.current) return;
163
+ lastLayoutRef.current = signature;
164
+ onLayoutChange(layout);
165
+ }, [items, positions, onLayoutChange]);
166
+ const style = {
167
+ "--masonry-column-count": columnCount,
168
+ ...inlineGap === void 0 ? void 0 : { "--masonry-gap-inline": gapToken(inlineGap, "inline") },
169
+ ...blockGap === void 0 ? void 0 : { "--masonry-gap-block": gapToken(blockGap, "block") },
170
+ blockSize: totalHeight
171
+ };
172
+ return /* @__PURE__ */ jsx(
173
+ "div",
174
+ {
175
+ ref: (element) => {
176
+ rootRef.current = element;
177
+ if (typeof ref === "function") ref(element);
178
+ else if (ref) ref.current = element;
179
+ },
180
+ id,
181
+ "data-slot": "masonry",
182
+ className: cn("ui-masonry", className),
183
+ style,
184
+ ...rest,
185
+ children: (items ?? []).map((item, index) => {
186
+ const key = item.key ?? index;
187
+ const position = positions.get(key);
188
+ const itemStyle = {
189
+ "--masonry-item-column": position?.column ?? 0,
190
+ insetBlockStart: position?.top ?? 0,
191
+ ...typeof item.height === "number" && Number.isFinite(item.height) ? { blockSize: item.height } : void 0
192
+ };
193
+ return /* @__PURE__ */ jsx(
194
+ "div",
195
+ {
196
+ ref: (element) => {
197
+ itemRefs.current.set(key, element);
198
+ },
199
+ "data-slot": "masonry-item",
200
+ "data-positioned": position ? "" : void 0,
201
+ className: "ui-masonry-item",
202
+ style: itemStyle,
203
+ children: item.children ?? itemRender?.({ ...item, index, column: position?.column ?? 0 })
204
+ },
205
+ key
206
+ );
207
+ })
208
+ }
209
+ );
210
+ });
211
+ Masonry.displayName = "Masonry";
212
+ export {
213
+ Masonry
214
+ };
@@ -3,32 +3,17 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { isValidElement, useEffect, useRef, useState } from "react";
4
4
  import { ChevronRight } from "lucide-react";
5
5
  import { useTranslation } from "../../i18n/use-translation.js";
6
+ import { scrollParent, useInView } from "../../lib/hooks.js";
6
7
  import { cn } from "../../lib/utils.js";
7
8
  import { densityClass, pageContainerVariantClass, padStyle } from "../../lib/variants.js";
8
- function scrollParent(el) {
9
- let node = el?.parentElement ?? null;
10
- while (node) {
11
- const overflowY = getComputedStyle(node).overflowY;
12
- if (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") return node;
13
- node = node.parentElement;
14
- }
15
- return null;
16
- }
17
9
  function useFooterReveal(enabled) {
18
10
  const headerRef = useRef(null);
19
- const [revealed, setRevealed] = useState(false);
11
+ const [root, setRoot] = useState(null);
20
12
  useEffect(() => {
21
- if (!enabled) return;
22
- const el = headerRef.current;
23
- if (!el || typeof IntersectionObserver === "undefined") return;
24
- const observer = new IntersectionObserver(([entry]) => setRevealed(!entry.isIntersecting), {
25
- root: scrollParent(el),
26
- threshold: 0
27
- });
28
- observer.observe(el);
29
- return () => observer.disconnect();
13
+ setRoot(scrollParent(headerRef.current));
30
14
  }, [enabled]);
31
- return { headerRef, revealed: enabled && revealed };
15
+ const inView = useInView(headerRef, { enabled, root, assumeInView: true });
16
+ return { headerRef, revealed: enabled && !inView };
32
17
  }
33
18
  function PageContainerInset({ className, children, ...props }) {
34
19
  return /* @__PURE__ */ jsx("div", { className: cn("ui-page-container-inset", className), ...props, children });
@@ -0,0 +1,64 @@
1
+ import * as React from "react";
2
+ import type { AnchorProp } from "../../props/components/navigation.prop.js";
3
+ export type { AnchorContainerProp, AnchorDirectionProp, AnchorItemProp, AnchorProp, AnchorProp as AnchorProps, } from "../../props/components/navigation.prop.js";
4
+ /**
5
+ * Anchor — Ant Design `Anchor` (6.6.5): the in-page section navigation, and — the part that makes
6
+ * it a component rather than a composition — the thing that WORKS OUT which section is current.
7
+ *
8
+ * `NavList activeId` takes that answer as a prop. Nothing else in this library computes it.
9
+ *
10
+ * ## Why this resolves by measurement and not by `IntersectionObserver`
11
+ *
12
+ * The obvious build is an observer over every section with a thin `rootMargin` band, and it is
13
+ * what Bootstrap's Scrollspy does (`rootMargin: "0px 0px -25%"`, `threshold: [0.1, 0.5, 1]`) and
14
+ * what this repo's own `LegalDocumentShell` does (`"-10% 0px -75% 0px"`, threshold 0). It has two
15
+ * failure modes that no choice of margin fixes, because they are properties of the question rather
16
+ * than of the numbers:
17
+ *
18
+ * 1. **A section taller than the band reports nothing.** Scroll into the middle of a long
19
+ * section and no element intersects, so the answer is empty and the implementation has to fall
20
+ * back to "keep whatever was active" — which is a guess, and is wrong after any jump.
21
+ * 2. **The answer depends on scroll DIRECTION.** With several short sections inside the band at
22
+ * once, "first intersecting" and "last intersecting" disagree, and which is right depends on
23
+ * which way the reader is moving. That is where scrollspy flicker comes from, and it is why
24
+ * the good implementations end up bolting hysteresis onto the observer.
25
+ *
26
+ * Ant Design resolves it the other way and gets neither problem: on each scroll, take every
27
+ * section whose block-start edge has crossed a single decision line, and pick the LAST one. It is
28
+ * a pure function of scroll position, so it cannot oscillate at a fixed position, it needs no
29
+ * hysteresis, and a section a mile tall is still the current one all the way down. That rule is
30
+ * what is ported here, `bounds` and all.
31
+ *
32
+ * ## The three traps, and where each is handled
33
+ *
34
+ * **A click must not fight the resolver.** A click scrolls, the scroll fires the resolver, and the
35
+ * resolver re-picks every section the page passes on the way — so the item you clicked lights up,
36
+ * goes out, and comes back. `suppressedRef` holds the resolver off from the moment the
37
+ * programmatic scroll starts until the container stops emitting `scroll` (see
38
+ * `SCROLL_SETTLE_MS`), and a real `wheel`/`touchstart` releases it early.
39
+ *
40
+ * **The hash is state too.** Landing on `/pricing#enterprise` must select that entry, and must do
41
+ * it without waiting for a scroll event that may never come — the browser's own hash jump does not
42
+ * fire one when the section is already in view. It is therefore read in the state INITIALISER, so
43
+ * the very first render is already correct, and the mount-time resolution is skipped when it
44
+ * matched, rather than immediately overwriting it with a scroll position nobody has reached yet.
45
+ *
46
+ * **Reduced motion jumps.** `behavior: "smooth"` becomes `"auto"` under
47
+ * `prefers-reduced-motion: reduce` (WCAG 2.3.3) — it still lands on the section, instantly. The
48
+ * ink rail transitions on `--duration-fast` and snaps under the same query; the current item is
49
+ * never conveyed by motion alone, it is conveyed by `aria-current`.
50
+ *
51
+ * ## Accessibility
52
+ *
53
+ * A named `<nav>` landmark (a page routinely carries a breadcrumb, a rail and this one), a real
54
+ * `<ul>`/`<li>` list of real `<a href="#…">` — middle-clickable, deep-linkable, and working before
55
+ * JavaScript boots — and `aria-current="location"` on the active one. `"location"` and not
56
+ * `"page"`: the entry points at a FRAGMENT of the page being read, not at a different page, and
57
+ * `"page"` is what a breadcrumb's or a nav rail's current item uses. A click also moves FOCUS to
58
+ * the target section, not just the scroll position, so the next Tab continues from where the
59
+ * reader was sent instead of from the nav.
60
+ */
61
+ export declare function Anchor({ items, direction, affix, bounds, getContainer, getCurrentAnchor, offsetBlockStart, targetOffsetBlockStart, showInkInFixed, replace, value, defaultValue, onValueChange, onChange, offsetTop, targetOffset, onClick, label, id, className, ...rest }: AnchorProp): React.JSX.Element;
62
+ export declare namespace Anchor {
63
+ var displayName: string;
64
+ }
@@ -0,0 +1,284 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { useTranslation } from "../../i18n/use-translation.js";
5
+ import { isDevelopment } from "../../lib/dev.js";
6
+ import { useControlledLatch } from "../../lib/hooks.js";
7
+ import { cn, prefersReducedMotion } from "../../lib/utils.js";
8
+ import { Affix } from "../layout/affix.js";
9
+ const SHARP_MATCHER = /#([^\t\r\n\f\v]+)$/;
10
+ const DEFAULT_BOUNDS = 5;
11
+ const SCROLL_SETTLE_MS = 120;
12
+ function hashOf(href) {
13
+ return SHARP_MATCHER.exec(href)?.[1] ?? "";
14
+ }
15
+ function flatten(items, out = []) {
16
+ for (const item of items ?? []) {
17
+ out.push(item);
18
+ if (item.children?.length) flatten(item.children, out);
19
+ }
20
+ return out;
21
+ }
22
+ function offsetTopWithin(element, container) {
23
+ if (!element.getClientRects().length) return 0;
24
+ const rect = element.getBoundingClientRect();
25
+ if (rect.width || rect.height) {
26
+ if (container === window) return rect.top - element.ownerDocument.documentElement.clientTop;
27
+ return rect.top - container.getBoundingClientRect().top;
28
+ }
29
+ return rect.top;
30
+ }
31
+ function scrollTopOf(container) {
32
+ return container === window ? window.scrollY ?? document.documentElement.scrollTop : container.scrollTop;
33
+ }
34
+ function Anchor({
35
+ items,
36
+ direction = "vertical",
37
+ affix = true,
38
+ bounds = DEFAULT_BOUNDS,
39
+ getContainer,
40
+ getCurrentAnchor,
41
+ offsetBlockStart,
42
+ targetOffsetBlockStart,
43
+ showInkInFixed = false,
44
+ replace = false,
45
+ value,
46
+ defaultValue,
47
+ onValueChange,
48
+ onChange,
49
+ offsetTop,
50
+ targetOffset,
51
+ onClick,
52
+ label,
53
+ id,
54
+ className,
55
+ ...rest
56
+ }) {
57
+ const { t } = useTranslation();
58
+ const horizontal = direction === "horizontal";
59
+ if (isDevelopment()) {
60
+ if (onChange !== void 0) {
61
+ console.warn(
62
+ "[@godxjp/ui] Anchor: `onChange` is Ant Design's name for this callback; here the active href is a controlled value, so it is `onValueChange` (beside `value` / `defaultValue`). The handler was IGNORED."
63
+ );
64
+ }
65
+ if (offsetTop !== void 0) {
66
+ console.warn(
67
+ "[@godxjp/ui] Anchor: `offsetTop` is Ant Design's name for this axis; here it is `offsetBlockStart` \u2014 the logical axis, per check:rtl. The value was IGNORED."
68
+ );
69
+ }
70
+ if (targetOffset !== void 0) {
71
+ console.warn(
72
+ "[@godxjp/ui] Anchor: `targetOffset` is Ant Design's name for this axis; here it is `targetOffsetBlockStart`. The value was IGNORED."
73
+ );
74
+ }
75
+ if (horizontal && items?.some((item) => item.children?.length)) {
76
+ console.warn(
77
+ '[@godxjp/ui] Anchor: `items[].children` is not supported when `direction="horizontal"` \u2014 a nested list has nowhere to go on one row. The nested entries were DROPPED. (Ant Design warns about the same combination.)'
78
+ );
79
+ }
80
+ }
81
+ const links = React.useMemo(
82
+ () => horizontal ? items ?? [] : flatten(items),
83
+ [items, horizontal]
84
+ );
85
+ const linksKey = links.map((item) => item.href).join("\n");
86
+ const navRef = React.useRef(null);
87
+ const listRef = React.useRef(null);
88
+ const suppressedRef = React.useRef(false);
89
+ const settleTimerRef = React.useRef(null);
90
+ const hashOnMountRef = React.useRef(null);
91
+ const [internal, setInternal] = React.useState(() => {
92
+ if (typeof window === "undefined") return defaultValue ?? "";
93
+ const hash = window.location.hash;
94
+ if (!hash) return defaultValue ?? "";
95
+ const decoded = decodeURIComponent(hash.slice(1));
96
+ const matched = links.find((item) => hashOf(item.href) === decoded);
97
+ if (!matched) return defaultValue ?? "";
98
+ hashOnMountRef.current = matched.href;
99
+ return matched.href;
100
+ });
101
+ const controlled = useControlledLatch(value !== void 0);
102
+ const resolved = controlled ? value ?? "" : internal;
103
+ const active = controlled ? resolved : getCurrentAnchor?.(resolved) ?? resolved;
104
+ const activeRef = React.useRef(active);
105
+ const controlledRef = React.useRef(controlled);
106
+ const onValueChangeRef = React.useRef(onValueChange);
107
+ const getCurrentAnchorRef = React.useRef(getCurrentAnchor);
108
+ React.useEffect(() => {
109
+ activeRef.current = active;
110
+ controlledRef.current = controlled;
111
+ onValueChangeRef.current = onValueChange;
112
+ getCurrentAnchorRef.current = getCurrentAnchor;
113
+ });
114
+ const commit = React.useCallback((href, force = false) => {
115
+ const mapped = getCurrentAnchorRef.current?.(href) ?? href;
116
+ if (!force && mapped === activeRef.current) return;
117
+ if (!controlledRef.current) setInternal(href);
118
+ onValueChangeRef.current?.(href);
119
+ }, []);
120
+ const container = React.useCallback(
121
+ () => getContainer?.() ?? window,
122
+ [getContainer]
123
+ );
124
+ const line = targetOffsetBlockStart ?? offsetBlockStart ?? 0;
125
+ const resolveFromScroll = React.useCallback(() => {
126
+ if (typeof document === "undefined") return "";
127
+ const box = container();
128
+ let best = "";
129
+ let bestTop = Number.NEGATIVE_INFINITY;
130
+ for (const item of links) {
131
+ const hash = hashOf(item.href);
132
+ if (!hash) continue;
133
+ const element = document.getElementById(hash);
134
+ if (!element) continue;
135
+ const top = offsetTopWithin(element, box);
136
+ const itemLine = item.targetOffsetBlockStart ?? line;
137
+ if (top <= itemLine + bounds && top > bestTop) {
138
+ best = item.href;
139
+ bestTop = top;
140
+ }
141
+ }
142
+ return best;
143
+ }, [container, links, line, bounds]);
144
+ const release = React.useCallback(() => {
145
+ if (settleTimerRef.current) clearTimeout(settleTimerRef.current);
146
+ settleTimerRef.current = null;
147
+ suppressedRef.current = false;
148
+ }, []);
149
+ React.useEffect(() => {
150
+ if (typeof window === "undefined") return void 0;
151
+ const box = container();
152
+ const handleScroll = () => {
153
+ if (suppressedRef.current) {
154
+ if (settleTimerRef.current) clearTimeout(settleTimerRef.current);
155
+ settleTimerRef.current = setTimeout(release, SCROLL_SETTLE_MS);
156
+ return;
157
+ }
158
+ commit(resolveFromScroll());
159
+ };
160
+ const handleGesture = () => {
161
+ if (suppressedRef.current) release();
162
+ };
163
+ if (hashOnMountRef.current === null) commit(resolveFromScroll());
164
+ hashOnMountRef.current = null;
165
+ box.addEventListener("scroll", handleScroll, { passive: true });
166
+ window.addEventListener("resize", handleScroll);
167
+ window.addEventListener("wheel", handleGesture, { passive: true });
168
+ window.addEventListener("touchstart", handleGesture, { passive: true });
169
+ return () => {
170
+ box.removeEventListener("scroll", handleScroll);
171
+ window.removeEventListener("resize", handleScroll);
172
+ window.removeEventListener("wheel", handleGesture);
173
+ window.removeEventListener("touchstart", handleGesture);
174
+ if (settleTimerRef.current) clearTimeout(settleTimerRef.current);
175
+ };
176
+ }, [container, resolveFromScroll, commit, release, linksKey]);
177
+ const [ink, setInk] = React.useState(null);
178
+ React.useEffect(() => {
179
+ const list = listRef.current;
180
+ if (!list) return;
181
+ const current = list.querySelector('[data-slot="anchor-link"][data-active]');
182
+ if (!current) {
183
+ setInk(null);
184
+ return;
185
+ }
186
+ const listRect = list.getBoundingClientRect();
187
+ const rect = current.getBoundingClientRect();
188
+ if (!horizontal) {
189
+ setInk({ offset: rect.top - listRect.top, size: rect.height });
190
+ return;
191
+ }
192
+ const rtl = getComputedStyle(list).direction === "rtl";
193
+ setInk({
194
+ offset: rtl ? listRect.right - rect.right : rect.left - listRect.left,
195
+ size: rect.width
196
+ });
197
+ }, [active, horizontal, linksKey]);
198
+ const handleLinkClick = (event, item) => {
199
+ onClick?.(event, item);
200
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
201
+ return;
202
+ }
203
+ const hash = hashOf(item.href);
204
+ const targetElement = hash && typeof document !== "undefined" ? document.getElementById(hash) : null;
205
+ if (!targetElement) return;
206
+ event.preventDefault();
207
+ commit(item.href, true);
208
+ const useReplace = item.replace ?? replace;
209
+ window.history[useReplace ? "replaceState" : "pushState"](null, "", item.href);
210
+ const box = container();
211
+ const landing = item.targetOffsetBlockStart ?? targetOffsetBlockStart ?? offsetBlockStart ?? 0;
212
+ const top = scrollTopOf(box) + offsetTopWithin(targetElement, box) - landing;
213
+ const behavior = prefersReducedMotion() ? "auto" : "smooth";
214
+ suppressedRef.current = true;
215
+ if (settleTimerRef.current) clearTimeout(settleTimerRef.current);
216
+ settleTimerRef.current = setTimeout(release, SCROLL_SETTLE_MS);
217
+ if (box === window) window.scrollTo({ top, behavior });
218
+ else box.scrollTo({ top, behavior });
219
+ if (!targetElement.hasAttribute("tabindex") && targetElement.tabIndex < 0) {
220
+ targetElement.setAttribute("tabindex", "-1");
221
+ }
222
+ targetElement.focus({ preventScroll: true });
223
+ };
224
+ const renderItems = (list, nested) => list.map((item) => /* @__PURE__ */ jsxs("li", { "data-slot": "anchor-item", "data-nested": nested ? "" : void 0, children: [
225
+ /* @__PURE__ */ jsx(
226
+ "a",
227
+ {
228
+ href: item.href,
229
+ target: item.target,
230
+ "data-slot": "anchor-link",
231
+ "data-active": item.href === active ? "" : void 0,
232
+ "aria-current": item.href === active ? "location" : void 0,
233
+ onClick: (event) => {
234
+ handleLinkClick(event, item);
235
+ },
236
+ children: item.title
237
+ }
238
+ ),
239
+ !horizontal && item.children?.length ? /* @__PURE__ */ jsx("ul", { "data-slot": "anchor-list", "data-nested": "", children: renderItems(item.children, true) }) : null
240
+ ] }, item.key));
241
+ const content = /* @__PURE__ */ jsx(
242
+ "nav",
243
+ {
244
+ ref: navRef,
245
+ id,
246
+ "data-slot": "anchor",
247
+ "data-direction": direction,
248
+ "data-ink": affix === false && !showInkInFixed ? "hidden" : void 0,
249
+ "aria-label": label ?? t("navigation.anchor.ariaLabel"),
250
+ className: cn("ui-anchor", className),
251
+ ...rest,
252
+ children: /* @__PURE__ */ jsxs("div", { className: "ui-anchor-track", children: [
253
+ /* @__PURE__ */ jsx(
254
+ "span",
255
+ {
256
+ "data-slot": "anchor-ink",
257
+ "data-visible": ink ? "" : void 0,
258
+ "aria-hidden": "true",
259
+ className: "ui-anchor-ink",
260
+ style: ink ? {
261
+ "--anchor-ink-offset": `${ink.offset}px`,
262
+ "--anchor-ink-extent": `${ink.size}px`
263
+ } : void 0
264
+ }
265
+ ),
266
+ /* @__PURE__ */ jsx("ul", { ref: listRef, "data-slot": "anchor-list", className: "ui-anchor-list", children: renderItems(items ?? [], false) })
267
+ ] })
268
+ }
269
+ );
270
+ if (!affix) return content;
271
+ return /* @__PURE__ */ jsx(
272
+ Affix,
273
+ {
274
+ offsetBlockStart,
275
+ target: container,
276
+ ...typeof affix === "object" ? affix : void 0,
277
+ children: content
278
+ }
279
+ );
280
+ }
281
+ Anchor.displayName = "Anchor";
282
+ export {
283
+ Anchor
284
+ };
@@ -186,10 +186,27 @@ const AppSettingPicker = React.forwardRef(
186
186
  )
187
187
  ) : (
188
188
  // Labeled: sized to a per-kind width from `sm` up; below `sm` it hugs its content and
189
- // A form field that wants a full-width control passes
190
- // `className="w-full"`, which wins over `w-auto`.
191
189
  // `compact` drops the per-kind width entirely so the trigger hugs its value.
192
- cn("w-auto max-w-full", !isCompact && "ui-app-setting-picker-trigger")
190
+ //
191
+ // The width is a UTILITY READING A TOKEN, for the third time in this file's
192
+ // history and for the same reason (gh#366 icon, gh#375 Select, gh#819 here):
193
+ // @layer utilities is ordered after @layer components, so the `w-auto` that
194
+ // stood here outranked `.ui-app-setting-picker-trigger[data-kind]`'s
195
+ // `inline-size` and every one of the eight per-kind tokens was inert — a
196
+ // consumer who retuned --app-setting-picker-density-width saw nothing move.
197
+ // `--app-setting-picker-trigger-width` is what `data-kind` resolves in
198
+ // navigation-layout.css (a custom property, which no utility competes with);
199
+ // it defaults to `auto`, so below the breakpoint and under `compact` this is
200
+ // byte-for-byte the old `w-auto` behaviour.
201
+ //
202
+ // It stays a SINGLE unprefixed `w-*` (not `sm:w-[…]` per kind): `cn` is
203
+ // tailwind-merge, so a form field that wants a full-width control still passes
204
+ // `className="w-full"` and REPLACES this one. A `sm:`-variant utility would
205
+ // survive that merge and quietly ignore the consumer above 40rem.
206
+ cn(
207
+ "w-[length:var(--app-setting-picker-trigger-width)] max-w-full",
208
+ !isCompact && "ui-app-setting-picker-trigger"
209
+ )
193
210
  ),
194
211
  // Compact re-tiers the box through tokens (--app-setting-picker-compact-*); the height
195
212
  // still comes from the official --control-height-sm tier, never a literal. The gap is a
@@ -9,5 +9,9 @@ export { AppSettingToggle } from "./app-setting-toggle.js";
9
9
  export type { AppSettingToggleProp, AppSettingToggleProps, AppSettingToggleKind, } from "./app-setting-toggle.js";
10
10
  export type { BreadcrumbItemProp as BreadcrumbItem } from "../../props/vocabulary/navigation.prop.js";
11
11
  export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "./dropdown-menu.js";
12
+ export { MegaMenu } from "./mega-menu.js";
13
+ export type { MegaMenuProp, MegaMenuProps, MegaMenuItemProp, MegaMenuItemProps, MegaMenuPanelProp, MegaMenuPanelProps, MegaMenuGroupProp, MegaMenuGroupProps, MegaMenuLinkProp, MegaMenuLinkProps, MegaMenuLinkComponentProp, MegaMenuLinkComponentProps, MegaMenuTriggerActionProp, MegaMenuTriggerActionProps, } from "./mega-menu.js";
12
14
  export { Conversations } from "./conversations.js";
13
15
  export type { ConversationsProp, ConversationsProps, ConversationsItemProp, ConversationsDividerProp, ConversationsEntryProp, ConversationsMenuProp, ConversationsMenuItemProp, ConversationsGroupableProp, ConversationsCreationProp, } from "./conversations.js";
16
+ export { Anchor } from "./anchor.js";
17
+ export type { AnchorContainerProp, AnchorDirectionProp, AnchorItemProp, AnchorProp, AnchorProps, } from "./anchor.js";
@@ -29,8 +29,11 @@ import {
29
29
  DropdownMenuSubTrigger,
30
30
  DropdownMenuTrigger
31
31
  } from "./dropdown-menu.js";
32
+ import { MegaMenu } from "./mega-menu.js";
32
33
  import { Conversations } from "./conversations.js";
34
+ import { Anchor } from "./anchor.js";
33
35
  export {
36
+ Anchor,
34
37
  AppSettingPicker,
35
38
  AppSettingToggle,
36
39
  Conversations,
@@ -51,6 +54,7 @@ export {
51
54
  DropdownMenuTrigger,
52
55
  FilterBar,
53
56
  FilterBarGroup,
57
+ MegaMenu,
54
58
  Pagination,
55
59
  PaginationContent,
56
60
  PaginationEllipsis,