@fanvue/ui 3.7.0 → 3.7.2
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/README.md +10 -0
- package/dist/cjs/components/Accordion/AccordionContent.cjs +6 -2
- package/dist/cjs/components/Accordion/AccordionContent.cjs.map +1 -1
- package/dist/cjs/components/Tabs/TabsList.cjs +4 -1
- package/dist/cjs/components/Tabs/TabsList.cjs.map +1 -1
- package/dist/components/Accordion/AccordionContent.mjs +6 -2
- package/dist/components/Accordion/AccordionContent.mjs.map +1 -1
- package/dist/components/Tabs/TabsList.mjs +4 -1
- package/dist/components/Tabs/TabsList.mjs.map +1 -1
- package/dist/styles/base.css +16 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -114,6 +114,7 @@ pnpm storybook
|
|
|
114
114
|
| ------------------------ | ------------------------------------ |
|
|
115
115
|
| **Development** | |
|
|
116
116
|
| `pnpm dev` | Start Vite dev server |
|
|
117
|
+
| `pnpm dev:watch` | Rebuild `dist/` on change (live-reload into apps) |
|
|
117
118
|
| `pnpm build` | Build the library for production |
|
|
118
119
|
| `pnpm preview` | Preview production build |
|
|
119
120
|
| **Testing** | |
|
|
@@ -137,6 +138,15 @@ pnpm storybook
|
|
|
137
138
|
| **Publishing** | |
|
|
138
139
|
| `pnpm publish:dry-run` | Build and dry-run npm publish |
|
|
139
140
|
|
|
141
|
+
### Live-reloading into pandora/eden
|
|
142
|
+
|
|
143
|
+
To iterate on a component and see it live in eden (`local.fanvue.com`) without publishing:
|
|
144
|
+
|
|
145
|
+
- **Easiest:** from the pandora repo root, run `pnpm dev:local-ui`. It runs this library's watch build automatically and points eden at this checkout's `dist/`. See pandora's README ("Live-reloading `@fanvue/ui`") — the pandora wiring ships in a companion PR.
|
|
146
|
+
- **Manual:** run `pnpm dev:watch` here, and start eden with `USE_LOCAL_FANVUE_UI=1` (or `pnpm --filter @pandora/eden start:local-ui`).
|
|
147
|
+
|
|
148
|
+
Requires this repo checked out beside `pandora` (or `FANVUE_UI_PATH` set in pandora). Component markup and Tailwind classes hot-reload; design **tokens** in `theme.css` are loaded by eden from the installed package, so local `theme.css` edits aren't reflected — test those via a published (pre)release.
|
|
149
|
+
|
|
140
150
|
## Figma + Storybook Integration
|
|
141
151
|
|
|
142
152
|
This library is integrated with Figma through Chromatic Connect. View the complete documentation in Storybook:
|
|
@@ -28,13 +28,17 @@ const AccordionContent = React__namespace.forwardRef(({ className, children, noP
|
|
|
28
28
|
{
|
|
29
29
|
ref,
|
|
30
30
|
className: cn.cn(
|
|
31
|
+
// Overflow-clipped panel whose `height` is animated (see `accordion-expand`/
|
|
32
|
+
// `accordion-collapse` in base.css). No `display` override here: a closed item
|
|
33
|
+
// keeps Radix's `hidden` attribute, so `[hidden] { display: none }` removes its
|
|
34
|
+
// footprint entirely (a `display: grid` panel would defeat that and leave a strip).
|
|
31
35
|
"overflow-hidden",
|
|
32
36
|
"motion-safe:data-[state=closed]:animate-accordion-collapse",
|
|
33
37
|
"motion-safe:data-[state=open]:animate-accordion-expand",
|
|
34
38
|
className
|
|
35
39
|
),
|
|
36
40
|
...props,
|
|
37
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
41
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "[contain:layout_paint]", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
38
42
|
"div",
|
|
39
43
|
{
|
|
40
44
|
className: cn.cn(
|
|
@@ -44,7 +48,7 @@ const AccordionContent = React__namespace.forwardRef(({ className, children, noP
|
|
|
44
48
|
),
|
|
45
49
|
children
|
|
46
50
|
}
|
|
47
|
-
)
|
|
51
|
+
) })
|
|
48
52
|
}
|
|
49
53
|
));
|
|
50
54
|
AccordionContent.displayName = "AccordionContent";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AccordionContent.cjs","sources":["../../../../src/components/Accordion/AccordionContent.tsx"],"sourcesContent":["import * as AccordionPrimitive from \"@radix-ui/react-accordion\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Props for the {@link AccordionContent} panel component. */\nexport type AccordionContentProps = React.ComponentPropsWithoutRef<\n typeof AccordionPrimitive.Content\n> & {\n /** Remove the default inner padding (`px-3 pb-3`). Useful when you need custom content layout. */\n noPadding?: boolean;\n};\n\n/** Renders the collapsible content panel for an {@link AccordionItem}. Animates open and closed. */\nexport const AccordionContent = React.forwardRef<\n React.ComponentRef<typeof AccordionPrimitive.Content>,\n AccordionContentProps\n>(({ className, children, noPadding, ...props }, ref) => (\n <AccordionPrimitive.Content\n ref={ref}\n className={cn(\n \"overflow-hidden\",\n \"motion-safe:data-[state=closed]:animate-accordion-collapse\",\n \"motion-safe:data-[state=open]:animate-accordion-expand\",\n className,\n )}\n {...props}\n >\n <div\n className={cn(\n
|
|
1
|
+
{"version":3,"file":"AccordionContent.cjs","sources":["../../../../src/components/Accordion/AccordionContent.tsx"],"sourcesContent":["import * as AccordionPrimitive from \"@radix-ui/react-accordion\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Props for the {@link AccordionContent} panel component. */\nexport type AccordionContentProps = React.ComponentPropsWithoutRef<\n typeof AccordionPrimitive.Content\n> & {\n /** Remove the default inner padding (`px-3 pb-3`). Useful when you need custom content layout. */\n noPadding?: boolean;\n};\n\n/** Renders the collapsible content panel for an {@link AccordionItem}. Animates open and closed. */\nexport const AccordionContent = React.forwardRef<\n React.ComponentRef<typeof AccordionPrimitive.Content>,\n AccordionContentProps\n>(({ className, children, noPadding, ...props }, ref) => (\n <AccordionPrimitive.Content\n ref={ref}\n className={cn(\n // Overflow-clipped panel whose `height` is animated (see `accordion-expand`/\n // `accordion-collapse` in base.css). No `display` override here: a closed item\n // keeps Radix's `hidden` attribute, so `[hidden] { display: none }` removes its\n // footprint entirely (a `display: grid` panel would defeat that and leave a strip).\n \"overflow-hidden\",\n \"motion-safe:data-[state=closed]:animate-accordion-collapse\",\n \"motion-safe:data-[state=open]:animate-accordion-expand\",\n className,\n )}\n {...props}\n >\n {/*\n Isolated layer between the height-animated panel and the content. `contain:\n layout paint` makes this its own layout+paint boundary, so the parent's height\n animation only re-clips this already-laid-out, already-painted layer instead of\n re-flowing and re-painting the padded content on every frame. Without it, the\n padding/content reflow per frame and visibly desync from the clip on mobile.\n */}\n <div className=\"[contain:layout_paint]\">\n <div\n className={cn(\n \"overflow-wrap-anywhere min-w-0\",\n \"typography-body-small-14px-regular text-content-secondary\",\n !noPadding && \"px-3 pt-2 pb-3\",\n )}\n >\n {children}\n </div>\n </div>\n </AccordionPrimitive.Content>\n));\n\nAccordionContent.displayName = \"AccordionContent\";\n"],"names":["React","jsx","AccordionPrimitive","cn"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAaO,MAAM,mBAAmBA,iBAAM,WAGpC,CAAC,EAAE,WAAW,UAAU,WAAW,GAAG,SAAS,QAC/CC,2BAAAA;AAAAA,EAACC,8BAAmB;AAAA,EAAnB;AAAA,IACC;AAAA,IACA,WAAWC,GAAAA;AAAAA;AAAAA;AAAAA;AAAAA;AAAAA,MAKT;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,IAED,GAAG;AAAA,IASJ,UAAAF,2BAAAA,IAAC,OAAA,EAAI,WAAU,0BACb,UAAAA,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAWE,GAAAA;AAAAA,UACT;AAAA,UACA;AAAA,UACA,CAAC,aAAa;AAAA,QAAA;AAAA,QAGf;AAAA,MAAA;AAAA,IAAA,EACH,CACF;AAAA,EAAA;AACF,CACD;AAED,iBAAiB,cAAc;;"}
|
|
@@ -106,7 +106,10 @@ const TabsList = React__namespace.forwardRef(({ className, children, fullWidth =
|
|
|
106
106
|
{
|
|
107
107
|
ref: innerRef,
|
|
108
108
|
className: cn.cn(
|
|
109
|
-
|
|
109
|
+
// `isolate` scopes the active indicator's `z-10` to a local stacking
|
|
110
|
+
// context. Without it, `relative` alone creates no stacking context, so
|
|
111
|
+
// the indicator escapes and paints above page chrome (e.g. the header).
|
|
112
|
+
"relative isolate",
|
|
110
113
|
getLayoutClass(variant, fullWidth, alignLeft),
|
|
111
114
|
"data-[orientation=horizontal]:items-center data-[orientation=horizontal]:shadow-[inset_0_-1px_0_0_var(--color-border-primary)]",
|
|
112
115
|
"data-[orientation=vertical]:flex-col data-[orientation=vertical]:shadow-[inset_-1px_0_0_0_var(--color-border-primary)]",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TabsList.cjs","sources":["../../../../src/components/Tabs/TabsList.tsx"],"sourcesContent":["import * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Breakpoint values for responsive props. */\ntype Breakpoint = \"sm\" | \"md\" | \"lg\" | \"xl\";\n\nconst alignLeftClasses: Record<Breakpoint | \"always\", string> = {\n always: \"[&>[role=tab]]:flex-initial\",\n sm: \"[&>[role=tab]]:sm:flex-initial\",\n md: \"[&>[role=tab]]:md:flex-initial\",\n lg: \"[&>[role=tab]]:lg:flex-initial\",\n xl: \"[&>[role=tab]]:xl:flex-initial\",\n};\n\nfunction getLayoutClass(\n variant: \"fill\" | \"hug\" | undefined,\n fullWidth: boolean,\n alignLeft?: boolean | Breakpoint,\n): string {\n if (variant === \"hug\") return \"inline-flex\";\n if (variant === \"fill\") return \"flex w-full [&>[role=tab]]:flex-1\";\n\n if (!fullWidth) return \"inline-flex\";\n\n const base = \"flex w-full [&>[role=tab]]:flex-1\";\n if (alignLeft === true) return `${base} ${alignLeftClasses.always}`;\n if (typeof alignLeft === \"string\") return `${base} ${alignLeftClasses[alignLeft]}`;\n return base;\n}\n\n/** Props for the {@link TabsList} component. */\nexport type TabsListProps = React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {\n /** When `true` (the default), the tab list spans the full width of its container and each tab grows equally. Set to `false` for inline sizing. */\n fullWidth?: boolean;\n /**\n * Controls tab alignment within a full-width container.\n * - `false` (default): tabs spread evenly\n * - `true`: tabs left-aligned, sized to content\n * - `\"md\"` (breakpoint): spread on mobile, left-aligned at breakpoint and up\n */\n alignLeft?: boolean | Breakpoint;\n /** Explicit layout variant. `\"hug\"` sizes the list to its content; `\"fill\"` stretches it full-width with equal-width tabs. Takes precedence over `fullWidth`/`alignLeft`. */\n variant?: \"fill\" | \"hug\";\n};\n\n/** Container for {@link TabsTrigger} elements. Renders a sliding active-tab indicator that animates between tabs. */\nexport const TabsList = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.List>,\n TabsListProps\n>(({ className, children, fullWidth = true, alignLeft, variant, ...props }, ref) => {\n const innerRef = React.useRef<HTMLDivElement>(null);\n const indicatorRef = React.useRef<HTMLSpanElement>(null);\n\n React.useImperativeHandle(ref, () => innerRef.current as HTMLDivElement);\n\n const updateIndicator = React.useCallback(() => {\n const list = innerRef.current;\n const indicator = indicatorRef.current;\n if (!list || !indicator) return;\n\n const activeTab = list.querySelector<HTMLElement>('[data-state=\"active\"]');\n if (!activeTab) {\n indicator.style.opacity = \"0\";\n return;\n }\n\n const isVertical = list.dataset.orientation === \"vertical\";\n\n indicator.style.opacity = \"1\";\n\n if (isVertical) {\n indicator.style.background =\n \"linear-gradient(180deg, var(--color-buttons-tertiary-default) 0%, var(--color-tab-active) 50%, var(--color-buttons-tertiary-default) 100%)\";\n indicator.style.inset = `0 -1px auto auto`;\n indicator.style.width = \"1px\";\n indicator.style.height = `${activeTab.offsetHeight}px`;\n indicator.style.transform = `translateY(${activeTab.offsetTop}px)`;\n } else {\n indicator.style.background =\n \"linear-gradient(90deg, var(--color-buttons-tertiary-default) 0%, var(--color-tab-active) 50%, var(--color-buttons-tertiary-default) 100%)\";\n const textSpan = activeTab.querySelector(\"span\");\n const textWidth = (textSpan ?? activeTab).getBoundingClientRect().width;\n const tabCenter = activeTab.offsetLeft + activeTab.offsetWidth / 2;\n const indicatorLeft = tabCenter - textWidth / 2;\n indicator.style.inset = \"auto auto 0 0\";\n indicator.style.height = \"1px\";\n indicator.style.width = `${textWidth}px`;\n indicator.style.transform = `translateX(${indicatorLeft}px)`;\n }\n }, []);\n\n React.useLayoutEffect(() => {\n const list = innerRef.current;\n const indicator = indicatorRef.current;\n if (!list || !indicator) return;\n\n indicator.style.transitionDuration = \"0s\";\n updateIndicator();\n indicator.getBoundingClientRect();\n indicator.style.transitionDuration = \"\";\n\n const mutationObserver = new MutationObserver(updateIndicator);\n mutationObserver.observe(list, {\n attributes: true,\n attributeFilter: [\"data-state\"],\n childList: true,\n subtree: true,\n });\n\n const resizeObserver = new ResizeObserver(updateIndicator);\n resizeObserver.observe(list);\n\n let cancelled = false;\n if (document.fonts?.status !== \"loaded\") {\n document.fonts?.ready.then(() => {\n if (!cancelled) updateIndicator();\n });\n }\n\n return () => {\n cancelled = true;\n mutationObserver.disconnect();\n resizeObserver.disconnect();\n };\n }, [updateIndicator]);\n\n return (\n <TabsPrimitive.List\n ref={innerRef}\n className={cn(\n \"relative\",\n getLayoutClass(variant, fullWidth, alignLeft),\n \"data-[orientation=horizontal]:items-center data-[orientation=horizontal]:shadow-[inset_0_-1px_0_0_var(--color-border-primary)]\",\n \"data-[orientation=vertical]:flex-col data-[orientation=vertical]:shadow-[inset_-1px_0_0_0_var(--color-border-primary)]\",\n className,\n )}\n {...props}\n >\n {children}\n <span\n ref={indicatorRef}\n aria-hidden\n className=\"pointer-events-none absolute z-10 rounded-full motion-safe:transition-[transform,width,height] motion-safe:duration-200 motion-safe:ease-in-out\"\n style={{ opacity: 0 }}\n />\n </TabsPrimitive.List>\n );\n});\n\nTabsList.displayName = \"TabsList\";\n"],"names":["React","jsxs","TabsPrimitive","cn","jsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAOA,MAAM,mBAA0D;AAAA,EAC9D,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,SAAS,eACP,SACA,WACA,WACQ;AACR,MAAI,YAAY,MAAO,QAAO;AAC9B,MAAI,YAAY,OAAQ,QAAO;AAE/B,MAAI,CAAC,UAAW,QAAO;AAEvB,QAAM,OAAO;AACb,MAAI,cAAc,KAAM,QAAO,GAAG,IAAI,IAAI,iBAAiB,MAAM;AACjE,MAAI,OAAO,cAAc,SAAU,QAAO,GAAG,IAAI,IAAI,iBAAiB,SAAS,CAAC;AAChF,SAAO;AACT;AAkBO,MAAM,WAAWA,iBAAM,WAG5B,CAAC,EAAE,WAAW,UAAU,YAAY,MAAM,WAAW,SAAS,GAAG,MAAA,GAAS,QAAQ;AAClF,QAAM,WAAWA,iBAAM,OAAuB,IAAI;AAClD,QAAM,eAAeA,iBAAM,OAAwB,IAAI;AAEvDA,mBAAM,oBAAoB,KAAK,MAAM,SAAS,OAAyB;AAEvE,QAAM,kBAAkBA,iBAAM,YAAY,MAAM;AAC9C,UAAM,OAAO,SAAS;AACtB,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,QAAQ,CAAC,UAAW;AAEzB,UAAM,YAAY,KAAK,cAA2B,uBAAuB;AACzE,QAAI,CAAC,WAAW;AACd,gBAAU,MAAM,UAAU;AAC1B;AAAA,IACF;AAEA,UAAM,aAAa,KAAK,QAAQ,gBAAgB;AAEhD,cAAU,MAAM,UAAU;AAE1B,QAAI,YAAY;AACd,gBAAU,MAAM,aACd;AACF,gBAAU,MAAM,QAAQ;AACxB,gBAAU,MAAM,QAAQ;AACxB,gBAAU,MAAM,SAAS,GAAG,UAAU,YAAY;AAClD,gBAAU,MAAM,YAAY,cAAc,UAAU,SAAS;AAAA,IAC/D,OAAO;AACL,gBAAU,MAAM,aACd;AACF,YAAM,WAAW,UAAU,cAAc,MAAM;AAC/C,YAAM,aAAa,YAAY,WAAW,sBAAA,EAAwB;AAClE,YAAM,YAAY,UAAU,aAAa,UAAU,cAAc;AACjE,YAAM,gBAAgB,YAAY,YAAY;AAC9C,gBAAU,MAAM,QAAQ;AACxB,gBAAU,MAAM,SAAS;AACzB,gBAAU,MAAM,QAAQ,GAAG,SAAS;AACpC,gBAAU,MAAM,YAAY,cAAc,aAAa;AAAA,IACzD;AAAA,EACF,GAAG,CAAA,CAAE;AAELA,mBAAM,gBAAgB,MAAM;AAC1B,UAAM,OAAO,SAAS;AACtB,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,QAAQ,CAAC,UAAW;AAEzB,cAAU,MAAM,qBAAqB;AACrC,oBAAA;AACA,cAAU,sBAAA;AACV,cAAU,MAAM,qBAAqB;AAErC,UAAM,mBAAmB,IAAI,iBAAiB,eAAe;AAC7D,qBAAiB,QAAQ,MAAM;AAAA,MAC7B,YAAY;AAAA,MACZ,iBAAiB,CAAC,YAAY;AAAA,MAC9B,WAAW;AAAA,MACX,SAAS;AAAA,IAAA,CACV;AAED,UAAM,iBAAiB,IAAI,eAAe,eAAe;AACzD,mBAAe,QAAQ,IAAI;AAE3B,QAAI,YAAY;AAChB,QAAI,SAAS,OAAO,WAAW,UAAU;AACvC,eAAS,OAAO,MAAM,KAAK,MAAM;AAC/B,YAAI,CAAC,UAAW,iBAAA;AAAA,MAClB,CAAC;AAAA,IACH;AAEA,WAAO,MAAM;AACX,kBAAY;AACZ,uBAAiB,WAAA;AACjB,qBAAe,WAAA;AAAA,IACjB;AAAA,EACF,GAAG,CAAC,eAAe,CAAC;AAEpB,SACEC,2BAAAA;AAAAA,IAACC,yBAAc;AAAA,IAAd;AAAA,MACC,KAAK;AAAA,MACL,WAAWC,GAAAA;AAAAA,
|
|
1
|
+
{"version":3,"file":"TabsList.cjs","sources":["../../../../src/components/Tabs/TabsList.tsx"],"sourcesContent":["import * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Breakpoint values for responsive props. */\ntype Breakpoint = \"sm\" | \"md\" | \"lg\" | \"xl\";\n\nconst alignLeftClasses: Record<Breakpoint | \"always\", string> = {\n always: \"[&>[role=tab]]:flex-initial\",\n sm: \"[&>[role=tab]]:sm:flex-initial\",\n md: \"[&>[role=tab]]:md:flex-initial\",\n lg: \"[&>[role=tab]]:lg:flex-initial\",\n xl: \"[&>[role=tab]]:xl:flex-initial\",\n};\n\nfunction getLayoutClass(\n variant: \"fill\" | \"hug\" | undefined,\n fullWidth: boolean,\n alignLeft?: boolean | Breakpoint,\n): string {\n if (variant === \"hug\") return \"inline-flex\";\n if (variant === \"fill\") return \"flex w-full [&>[role=tab]]:flex-1\";\n\n if (!fullWidth) return \"inline-flex\";\n\n const base = \"flex w-full [&>[role=tab]]:flex-1\";\n if (alignLeft === true) return `${base} ${alignLeftClasses.always}`;\n if (typeof alignLeft === \"string\") return `${base} ${alignLeftClasses[alignLeft]}`;\n return base;\n}\n\n/** Props for the {@link TabsList} component. */\nexport type TabsListProps = React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {\n /** When `true` (the default), the tab list spans the full width of its container and each tab grows equally. Set to `false` for inline sizing. */\n fullWidth?: boolean;\n /**\n * Controls tab alignment within a full-width container.\n * - `false` (default): tabs spread evenly\n * - `true`: tabs left-aligned, sized to content\n * - `\"md\"` (breakpoint): spread on mobile, left-aligned at breakpoint and up\n */\n alignLeft?: boolean | Breakpoint;\n /** Explicit layout variant. `\"hug\"` sizes the list to its content; `\"fill\"` stretches it full-width with equal-width tabs. Takes precedence over `fullWidth`/`alignLeft`. */\n variant?: \"fill\" | \"hug\";\n};\n\n/** Container for {@link TabsTrigger} elements. Renders a sliding active-tab indicator that animates between tabs. */\nexport const TabsList = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.List>,\n TabsListProps\n>(({ className, children, fullWidth = true, alignLeft, variant, ...props }, ref) => {\n const innerRef = React.useRef<HTMLDivElement>(null);\n const indicatorRef = React.useRef<HTMLSpanElement>(null);\n\n React.useImperativeHandle(ref, () => innerRef.current as HTMLDivElement);\n\n const updateIndicator = React.useCallback(() => {\n const list = innerRef.current;\n const indicator = indicatorRef.current;\n if (!list || !indicator) return;\n\n const activeTab = list.querySelector<HTMLElement>('[data-state=\"active\"]');\n if (!activeTab) {\n indicator.style.opacity = \"0\";\n return;\n }\n\n const isVertical = list.dataset.orientation === \"vertical\";\n\n indicator.style.opacity = \"1\";\n\n if (isVertical) {\n indicator.style.background =\n \"linear-gradient(180deg, var(--color-buttons-tertiary-default) 0%, var(--color-tab-active) 50%, var(--color-buttons-tertiary-default) 100%)\";\n indicator.style.inset = `0 -1px auto auto`;\n indicator.style.width = \"1px\";\n indicator.style.height = `${activeTab.offsetHeight}px`;\n indicator.style.transform = `translateY(${activeTab.offsetTop}px)`;\n } else {\n indicator.style.background =\n \"linear-gradient(90deg, var(--color-buttons-tertiary-default) 0%, var(--color-tab-active) 50%, var(--color-buttons-tertiary-default) 100%)\";\n const textSpan = activeTab.querySelector(\"span\");\n const textWidth = (textSpan ?? activeTab).getBoundingClientRect().width;\n const tabCenter = activeTab.offsetLeft + activeTab.offsetWidth / 2;\n const indicatorLeft = tabCenter - textWidth / 2;\n indicator.style.inset = \"auto auto 0 0\";\n indicator.style.height = \"1px\";\n indicator.style.width = `${textWidth}px`;\n indicator.style.transform = `translateX(${indicatorLeft}px)`;\n }\n }, []);\n\n React.useLayoutEffect(() => {\n const list = innerRef.current;\n const indicator = indicatorRef.current;\n if (!list || !indicator) return;\n\n indicator.style.transitionDuration = \"0s\";\n updateIndicator();\n indicator.getBoundingClientRect();\n indicator.style.transitionDuration = \"\";\n\n const mutationObserver = new MutationObserver(updateIndicator);\n mutationObserver.observe(list, {\n attributes: true,\n attributeFilter: [\"data-state\"],\n childList: true,\n subtree: true,\n });\n\n const resizeObserver = new ResizeObserver(updateIndicator);\n resizeObserver.observe(list);\n\n let cancelled = false;\n if (document.fonts?.status !== \"loaded\") {\n document.fonts?.ready.then(() => {\n if (!cancelled) updateIndicator();\n });\n }\n\n return () => {\n cancelled = true;\n mutationObserver.disconnect();\n resizeObserver.disconnect();\n };\n }, [updateIndicator]);\n\n return (\n <TabsPrimitive.List\n ref={innerRef}\n className={cn(\n // `isolate` scopes the active indicator's `z-10` to a local stacking\n // context. Without it, `relative` alone creates no stacking context, so\n // the indicator escapes and paints above page chrome (e.g. the header).\n \"relative isolate\",\n getLayoutClass(variant, fullWidth, alignLeft),\n \"data-[orientation=horizontal]:items-center data-[orientation=horizontal]:shadow-[inset_0_-1px_0_0_var(--color-border-primary)]\",\n \"data-[orientation=vertical]:flex-col data-[orientation=vertical]:shadow-[inset_-1px_0_0_0_var(--color-border-primary)]\",\n className,\n )}\n {...props}\n >\n {children}\n <span\n ref={indicatorRef}\n aria-hidden\n className=\"pointer-events-none absolute z-10 rounded-full motion-safe:transition-[transform,width,height] motion-safe:duration-200 motion-safe:ease-in-out\"\n style={{ opacity: 0 }}\n />\n </TabsPrimitive.List>\n );\n});\n\nTabsList.displayName = \"TabsList\";\n"],"names":["React","jsxs","TabsPrimitive","cn","jsx"],"mappings":"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"}
|
|
@@ -8,13 +8,17 @@ const AccordionContent = React.forwardRef(({ className, children, noPadding, ...
|
|
|
8
8
|
{
|
|
9
9
|
ref,
|
|
10
10
|
className: cn(
|
|
11
|
+
// Overflow-clipped panel whose `height` is animated (see `accordion-expand`/
|
|
12
|
+
// `accordion-collapse` in base.css). No `display` override here: a closed item
|
|
13
|
+
// keeps Radix's `hidden` attribute, so `[hidden] { display: none }` removes its
|
|
14
|
+
// footprint entirely (a `display: grid` panel would defeat that and leave a strip).
|
|
11
15
|
"overflow-hidden",
|
|
12
16
|
"motion-safe:data-[state=closed]:animate-accordion-collapse",
|
|
13
17
|
"motion-safe:data-[state=open]:animate-accordion-expand",
|
|
14
18
|
className
|
|
15
19
|
),
|
|
16
20
|
...props,
|
|
17
|
-
children: /* @__PURE__ */ jsx(
|
|
21
|
+
children: /* @__PURE__ */ jsx("div", { className: "[contain:layout_paint]", children: /* @__PURE__ */ jsx(
|
|
18
22
|
"div",
|
|
19
23
|
{
|
|
20
24
|
className: cn(
|
|
@@ -24,7 +28,7 @@ const AccordionContent = React.forwardRef(({ className, children, noPadding, ...
|
|
|
24
28
|
),
|
|
25
29
|
children
|
|
26
30
|
}
|
|
27
|
-
)
|
|
31
|
+
) })
|
|
28
32
|
}
|
|
29
33
|
));
|
|
30
34
|
AccordionContent.displayName = "AccordionContent";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AccordionContent.mjs","sources":["../../../src/components/Accordion/AccordionContent.tsx"],"sourcesContent":["import * as AccordionPrimitive from \"@radix-ui/react-accordion\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Props for the {@link AccordionContent} panel component. */\nexport type AccordionContentProps = React.ComponentPropsWithoutRef<\n typeof AccordionPrimitive.Content\n> & {\n /** Remove the default inner padding (`px-3 pb-3`). Useful when you need custom content layout. */\n noPadding?: boolean;\n};\n\n/** Renders the collapsible content panel for an {@link AccordionItem}. Animates open and closed. */\nexport const AccordionContent = React.forwardRef<\n React.ComponentRef<typeof AccordionPrimitive.Content>,\n AccordionContentProps\n>(({ className, children, noPadding, ...props }, ref) => (\n <AccordionPrimitive.Content\n ref={ref}\n className={cn(\n \"overflow-hidden\",\n \"motion-safe:data-[state=closed]:animate-accordion-collapse\",\n \"motion-safe:data-[state=open]:animate-accordion-expand\",\n className,\n )}\n {...props}\n >\n <div\n className={cn(\n
|
|
1
|
+
{"version":3,"file":"AccordionContent.mjs","sources":["../../../src/components/Accordion/AccordionContent.tsx"],"sourcesContent":["import * as AccordionPrimitive from \"@radix-ui/react-accordion\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Props for the {@link AccordionContent} panel component. */\nexport type AccordionContentProps = React.ComponentPropsWithoutRef<\n typeof AccordionPrimitive.Content\n> & {\n /** Remove the default inner padding (`px-3 pb-3`). Useful when you need custom content layout. */\n noPadding?: boolean;\n};\n\n/** Renders the collapsible content panel for an {@link AccordionItem}. Animates open and closed. */\nexport const AccordionContent = React.forwardRef<\n React.ComponentRef<typeof AccordionPrimitive.Content>,\n AccordionContentProps\n>(({ className, children, noPadding, ...props }, ref) => (\n <AccordionPrimitive.Content\n ref={ref}\n className={cn(\n // Overflow-clipped panel whose `height` is animated (see `accordion-expand`/\n // `accordion-collapse` in base.css). No `display` override here: a closed item\n // keeps Radix's `hidden` attribute, so `[hidden] { display: none }` removes its\n // footprint entirely (a `display: grid` panel would defeat that and leave a strip).\n \"overflow-hidden\",\n \"motion-safe:data-[state=closed]:animate-accordion-collapse\",\n \"motion-safe:data-[state=open]:animate-accordion-expand\",\n className,\n )}\n {...props}\n >\n {/*\n Isolated layer between the height-animated panel and the content. `contain:\n layout paint` makes this its own layout+paint boundary, so the parent's height\n animation only re-clips this already-laid-out, already-painted layer instead of\n re-flowing and re-painting the padded content on every frame. Without it, the\n padding/content reflow per frame and visibly desync from the clip on mobile.\n */}\n <div className=\"[contain:layout_paint]\">\n <div\n className={cn(\n \"overflow-wrap-anywhere min-w-0\",\n \"typography-body-small-14px-regular text-content-secondary\",\n !noPadding && \"px-3 pt-2 pb-3\",\n )}\n >\n {children}\n </div>\n </div>\n </AccordionPrimitive.Content>\n));\n\nAccordionContent.displayName = \"AccordionContent\";\n"],"names":[],"mappings":";;;;;AAaO,MAAM,mBAAmB,MAAM,WAGpC,CAAC,EAAE,WAAW,UAAU,WAAW,GAAG,SAAS,QAC/C;AAAA,EAAC,mBAAmB;AAAA,EAAnB;AAAA,IACC;AAAA,IACA,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA,MAKT;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,IAED,GAAG;AAAA,IASJ,UAAA,oBAAC,OAAA,EAAI,WAAU,0BACb,UAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA;AAAA,UACA,CAAC,aAAa;AAAA,QAAA;AAAA,QAGf;AAAA,MAAA;AAAA,IAAA,EACH,CACF;AAAA,EAAA;AACF,CACD;AAED,iBAAiB,cAAc;"}
|
|
@@ -86,7 +86,10 @@ const TabsList = React.forwardRef(({ className, children, fullWidth = true, alig
|
|
|
86
86
|
{
|
|
87
87
|
ref: innerRef,
|
|
88
88
|
className: cn(
|
|
89
|
-
|
|
89
|
+
// `isolate` scopes the active indicator's `z-10` to a local stacking
|
|
90
|
+
// context. Without it, `relative` alone creates no stacking context, so
|
|
91
|
+
// the indicator escapes and paints above page chrome (e.g. the header).
|
|
92
|
+
"relative isolate",
|
|
90
93
|
getLayoutClass(variant, fullWidth, alignLeft),
|
|
91
94
|
"data-[orientation=horizontal]:items-center data-[orientation=horizontal]:shadow-[inset_0_-1px_0_0_var(--color-border-primary)]",
|
|
92
95
|
"data-[orientation=vertical]:flex-col data-[orientation=vertical]:shadow-[inset_-1px_0_0_0_var(--color-border-primary)]",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TabsList.mjs","sources":["../../../src/components/Tabs/TabsList.tsx"],"sourcesContent":["import * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Breakpoint values for responsive props. */\ntype Breakpoint = \"sm\" | \"md\" | \"lg\" | \"xl\";\n\nconst alignLeftClasses: Record<Breakpoint | \"always\", string> = {\n always: \"[&>[role=tab]]:flex-initial\",\n sm: \"[&>[role=tab]]:sm:flex-initial\",\n md: \"[&>[role=tab]]:md:flex-initial\",\n lg: \"[&>[role=tab]]:lg:flex-initial\",\n xl: \"[&>[role=tab]]:xl:flex-initial\",\n};\n\nfunction getLayoutClass(\n variant: \"fill\" | \"hug\" | undefined,\n fullWidth: boolean,\n alignLeft?: boolean | Breakpoint,\n): string {\n if (variant === \"hug\") return \"inline-flex\";\n if (variant === \"fill\") return \"flex w-full [&>[role=tab]]:flex-1\";\n\n if (!fullWidth) return \"inline-flex\";\n\n const base = \"flex w-full [&>[role=tab]]:flex-1\";\n if (alignLeft === true) return `${base} ${alignLeftClasses.always}`;\n if (typeof alignLeft === \"string\") return `${base} ${alignLeftClasses[alignLeft]}`;\n return base;\n}\n\n/** Props for the {@link TabsList} component. */\nexport type TabsListProps = React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {\n /** When `true` (the default), the tab list spans the full width of its container and each tab grows equally. Set to `false` for inline sizing. */\n fullWidth?: boolean;\n /**\n * Controls tab alignment within a full-width container.\n * - `false` (default): tabs spread evenly\n * - `true`: tabs left-aligned, sized to content\n * - `\"md\"` (breakpoint): spread on mobile, left-aligned at breakpoint and up\n */\n alignLeft?: boolean | Breakpoint;\n /** Explicit layout variant. `\"hug\"` sizes the list to its content; `\"fill\"` stretches it full-width with equal-width tabs. Takes precedence over `fullWidth`/`alignLeft`. */\n variant?: \"fill\" | \"hug\";\n};\n\n/** Container for {@link TabsTrigger} elements. Renders a sliding active-tab indicator that animates between tabs. */\nexport const TabsList = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.List>,\n TabsListProps\n>(({ className, children, fullWidth = true, alignLeft, variant, ...props }, ref) => {\n const innerRef = React.useRef<HTMLDivElement>(null);\n const indicatorRef = React.useRef<HTMLSpanElement>(null);\n\n React.useImperativeHandle(ref, () => innerRef.current as HTMLDivElement);\n\n const updateIndicator = React.useCallback(() => {\n const list = innerRef.current;\n const indicator = indicatorRef.current;\n if (!list || !indicator) return;\n\n const activeTab = list.querySelector<HTMLElement>('[data-state=\"active\"]');\n if (!activeTab) {\n indicator.style.opacity = \"0\";\n return;\n }\n\n const isVertical = list.dataset.orientation === \"vertical\";\n\n indicator.style.opacity = \"1\";\n\n if (isVertical) {\n indicator.style.background =\n \"linear-gradient(180deg, var(--color-buttons-tertiary-default) 0%, var(--color-tab-active) 50%, var(--color-buttons-tertiary-default) 100%)\";\n indicator.style.inset = `0 -1px auto auto`;\n indicator.style.width = \"1px\";\n indicator.style.height = `${activeTab.offsetHeight}px`;\n indicator.style.transform = `translateY(${activeTab.offsetTop}px)`;\n } else {\n indicator.style.background =\n \"linear-gradient(90deg, var(--color-buttons-tertiary-default) 0%, var(--color-tab-active) 50%, var(--color-buttons-tertiary-default) 100%)\";\n const textSpan = activeTab.querySelector(\"span\");\n const textWidth = (textSpan ?? activeTab).getBoundingClientRect().width;\n const tabCenter = activeTab.offsetLeft + activeTab.offsetWidth / 2;\n const indicatorLeft = tabCenter - textWidth / 2;\n indicator.style.inset = \"auto auto 0 0\";\n indicator.style.height = \"1px\";\n indicator.style.width = `${textWidth}px`;\n indicator.style.transform = `translateX(${indicatorLeft}px)`;\n }\n }, []);\n\n React.useLayoutEffect(() => {\n const list = innerRef.current;\n const indicator = indicatorRef.current;\n if (!list || !indicator) return;\n\n indicator.style.transitionDuration = \"0s\";\n updateIndicator();\n indicator.getBoundingClientRect();\n indicator.style.transitionDuration = \"\";\n\n const mutationObserver = new MutationObserver(updateIndicator);\n mutationObserver.observe(list, {\n attributes: true,\n attributeFilter: [\"data-state\"],\n childList: true,\n subtree: true,\n });\n\n const resizeObserver = new ResizeObserver(updateIndicator);\n resizeObserver.observe(list);\n\n let cancelled = false;\n if (document.fonts?.status !== \"loaded\") {\n document.fonts?.ready.then(() => {\n if (!cancelled) updateIndicator();\n });\n }\n\n return () => {\n cancelled = true;\n mutationObserver.disconnect();\n resizeObserver.disconnect();\n };\n }, [updateIndicator]);\n\n return (\n <TabsPrimitive.List\n ref={innerRef}\n className={cn(\n \"relative\",\n getLayoutClass(variant, fullWidth, alignLeft),\n \"data-[orientation=horizontal]:items-center data-[orientation=horizontal]:shadow-[inset_0_-1px_0_0_var(--color-border-primary)]\",\n \"data-[orientation=vertical]:flex-col data-[orientation=vertical]:shadow-[inset_-1px_0_0_0_var(--color-border-primary)]\",\n className,\n )}\n {...props}\n >\n {children}\n <span\n ref={indicatorRef}\n aria-hidden\n className=\"pointer-events-none absolute z-10 rounded-full motion-safe:transition-[transform,width,height] motion-safe:duration-200 motion-safe:ease-in-out\"\n style={{ opacity: 0 }}\n />\n </TabsPrimitive.List>\n );\n});\n\nTabsList.displayName = \"TabsList\";\n"],"names":[],"mappings":";;;;;AAOA,MAAM,mBAA0D;AAAA,EAC9D,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,SAAS,eACP,SACA,WACA,WACQ;AACR,MAAI,YAAY,MAAO,QAAO;AAC9B,MAAI,YAAY,OAAQ,QAAO;AAE/B,MAAI,CAAC,UAAW,QAAO;AAEvB,QAAM,OAAO;AACb,MAAI,cAAc,KAAM,QAAO,GAAG,IAAI,IAAI,iBAAiB,MAAM;AACjE,MAAI,OAAO,cAAc,SAAU,QAAO,GAAG,IAAI,IAAI,iBAAiB,SAAS,CAAC;AAChF,SAAO;AACT;AAkBO,MAAM,WAAW,MAAM,WAG5B,CAAC,EAAE,WAAW,UAAU,YAAY,MAAM,WAAW,SAAS,GAAG,MAAA,GAAS,QAAQ;AAClF,QAAM,WAAW,MAAM,OAAuB,IAAI;AAClD,QAAM,eAAe,MAAM,OAAwB,IAAI;AAEvD,QAAM,oBAAoB,KAAK,MAAM,SAAS,OAAyB;AAEvE,QAAM,kBAAkB,MAAM,YAAY,MAAM;AAC9C,UAAM,OAAO,SAAS;AACtB,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,QAAQ,CAAC,UAAW;AAEzB,UAAM,YAAY,KAAK,cAA2B,uBAAuB;AACzE,QAAI,CAAC,WAAW;AACd,gBAAU,MAAM,UAAU;AAC1B;AAAA,IACF;AAEA,UAAM,aAAa,KAAK,QAAQ,gBAAgB;AAEhD,cAAU,MAAM,UAAU;AAE1B,QAAI,YAAY;AACd,gBAAU,MAAM,aACd;AACF,gBAAU,MAAM,QAAQ;AACxB,gBAAU,MAAM,QAAQ;AACxB,gBAAU,MAAM,SAAS,GAAG,UAAU,YAAY;AAClD,gBAAU,MAAM,YAAY,cAAc,UAAU,SAAS;AAAA,IAC/D,OAAO;AACL,gBAAU,MAAM,aACd;AACF,YAAM,WAAW,UAAU,cAAc,MAAM;AAC/C,YAAM,aAAa,YAAY,WAAW,sBAAA,EAAwB;AAClE,YAAM,YAAY,UAAU,aAAa,UAAU,cAAc;AACjE,YAAM,gBAAgB,YAAY,YAAY;AAC9C,gBAAU,MAAM,QAAQ;AACxB,gBAAU,MAAM,SAAS;AACzB,gBAAU,MAAM,QAAQ,GAAG,SAAS;AACpC,gBAAU,MAAM,YAAY,cAAc,aAAa;AAAA,IACzD;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,gBAAgB,MAAM;AAC1B,UAAM,OAAO,SAAS;AACtB,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,QAAQ,CAAC,UAAW;AAEzB,cAAU,MAAM,qBAAqB;AACrC,oBAAA;AACA,cAAU,sBAAA;AACV,cAAU,MAAM,qBAAqB;AAErC,UAAM,mBAAmB,IAAI,iBAAiB,eAAe;AAC7D,qBAAiB,QAAQ,MAAM;AAAA,MAC7B,YAAY;AAAA,MACZ,iBAAiB,CAAC,YAAY;AAAA,MAC9B,WAAW;AAAA,MACX,SAAS;AAAA,IAAA,CACV;AAED,UAAM,iBAAiB,IAAI,eAAe,eAAe;AACzD,mBAAe,QAAQ,IAAI;AAE3B,QAAI,YAAY;AAChB,QAAI,SAAS,OAAO,WAAW,UAAU;AACvC,eAAS,OAAO,MAAM,KAAK,MAAM;AAC/B,YAAI,CAAC,UAAW,iBAAA;AAAA,MAClB,CAAC;AAAA,IACH;AAEA,WAAO,MAAM;AACX,kBAAY;AACZ,uBAAiB,WAAA;AACjB,qBAAe,WAAA;AAAA,IACjB;AAAA,EACF,GAAG,CAAC,eAAe,CAAC;AAEpB,SACE;AAAA,IAAC,cAAc;AAAA,IAAd;AAAA,MACC,KAAK;AAAA,MACL,WAAW;AAAA,
|
|
1
|
+
{"version":3,"file":"TabsList.mjs","sources":["../../../src/components/Tabs/TabsList.tsx"],"sourcesContent":["import * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport * as React from \"react\";\nimport { cn } from \"../../utils/cn\";\n\n/** Breakpoint values for responsive props. */\ntype Breakpoint = \"sm\" | \"md\" | \"lg\" | \"xl\";\n\nconst alignLeftClasses: Record<Breakpoint | \"always\", string> = {\n always: \"[&>[role=tab]]:flex-initial\",\n sm: \"[&>[role=tab]]:sm:flex-initial\",\n md: \"[&>[role=tab]]:md:flex-initial\",\n lg: \"[&>[role=tab]]:lg:flex-initial\",\n xl: \"[&>[role=tab]]:xl:flex-initial\",\n};\n\nfunction getLayoutClass(\n variant: \"fill\" | \"hug\" | undefined,\n fullWidth: boolean,\n alignLeft?: boolean | Breakpoint,\n): string {\n if (variant === \"hug\") return \"inline-flex\";\n if (variant === \"fill\") return \"flex w-full [&>[role=tab]]:flex-1\";\n\n if (!fullWidth) return \"inline-flex\";\n\n const base = \"flex w-full [&>[role=tab]]:flex-1\";\n if (alignLeft === true) return `${base} ${alignLeftClasses.always}`;\n if (typeof alignLeft === \"string\") return `${base} ${alignLeftClasses[alignLeft]}`;\n return base;\n}\n\n/** Props for the {@link TabsList} component. */\nexport type TabsListProps = React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> & {\n /** When `true` (the default), the tab list spans the full width of its container and each tab grows equally. Set to `false` for inline sizing. */\n fullWidth?: boolean;\n /**\n * Controls tab alignment within a full-width container.\n * - `false` (default): tabs spread evenly\n * - `true`: tabs left-aligned, sized to content\n * - `\"md\"` (breakpoint): spread on mobile, left-aligned at breakpoint and up\n */\n alignLeft?: boolean | Breakpoint;\n /** Explicit layout variant. `\"hug\"` sizes the list to its content; `\"fill\"` stretches it full-width with equal-width tabs. Takes precedence over `fullWidth`/`alignLeft`. */\n variant?: \"fill\" | \"hug\";\n};\n\n/** Container for {@link TabsTrigger} elements. Renders a sliding active-tab indicator that animates between tabs. */\nexport const TabsList = React.forwardRef<\n React.ComponentRef<typeof TabsPrimitive.List>,\n TabsListProps\n>(({ className, children, fullWidth = true, alignLeft, variant, ...props }, ref) => {\n const innerRef = React.useRef<HTMLDivElement>(null);\n const indicatorRef = React.useRef<HTMLSpanElement>(null);\n\n React.useImperativeHandle(ref, () => innerRef.current as HTMLDivElement);\n\n const updateIndicator = React.useCallback(() => {\n const list = innerRef.current;\n const indicator = indicatorRef.current;\n if (!list || !indicator) return;\n\n const activeTab = list.querySelector<HTMLElement>('[data-state=\"active\"]');\n if (!activeTab) {\n indicator.style.opacity = \"0\";\n return;\n }\n\n const isVertical = list.dataset.orientation === \"vertical\";\n\n indicator.style.opacity = \"1\";\n\n if (isVertical) {\n indicator.style.background =\n \"linear-gradient(180deg, var(--color-buttons-tertiary-default) 0%, var(--color-tab-active) 50%, var(--color-buttons-tertiary-default) 100%)\";\n indicator.style.inset = `0 -1px auto auto`;\n indicator.style.width = \"1px\";\n indicator.style.height = `${activeTab.offsetHeight}px`;\n indicator.style.transform = `translateY(${activeTab.offsetTop}px)`;\n } else {\n indicator.style.background =\n \"linear-gradient(90deg, var(--color-buttons-tertiary-default) 0%, var(--color-tab-active) 50%, var(--color-buttons-tertiary-default) 100%)\";\n const textSpan = activeTab.querySelector(\"span\");\n const textWidth = (textSpan ?? activeTab).getBoundingClientRect().width;\n const tabCenter = activeTab.offsetLeft + activeTab.offsetWidth / 2;\n const indicatorLeft = tabCenter - textWidth / 2;\n indicator.style.inset = \"auto auto 0 0\";\n indicator.style.height = \"1px\";\n indicator.style.width = `${textWidth}px`;\n indicator.style.transform = `translateX(${indicatorLeft}px)`;\n }\n }, []);\n\n React.useLayoutEffect(() => {\n const list = innerRef.current;\n const indicator = indicatorRef.current;\n if (!list || !indicator) return;\n\n indicator.style.transitionDuration = \"0s\";\n updateIndicator();\n indicator.getBoundingClientRect();\n indicator.style.transitionDuration = \"\";\n\n const mutationObserver = new MutationObserver(updateIndicator);\n mutationObserver.observe(list, {\n attributes: true,\n attributeFilter: [\"data-state\"],\n childList: true,\n subtree: true,\n });\n\n const resizeObserver = new ResizeObserver(updateIndicator);\n resizeObserver.observe(list);\n\n let cancelled = false;\n if (document.fonts?.status !== \"loaded\") {\n document.fonts?.ready.then(() => {\n if (!cancelled) updateIndicator();\n });\n }\n\n return () => {\n cancelled = true;\n mutationObserver.disconnect();\n resizeObserver.disconnect();\n };\n }, [updateIndicator]);\n\n return (\n <TabsPrimitive.List\n ref={innerRef}\n className={cn(\n // `isolate` scopes the active indicator's `z-10` to a local stacking\n // context. Without it, `relative` alone creates no stacking context, so\n // the indicator escapes and paints above page chrome (e.g. the header).\n \"relative isolate\",\n getLayoutClass(variant, fullWidth, alignLeft),\n \"data-[orientation=horizontal]:items-center data-[orientation=horizontal]:shadow-[inset_0_-1px_0_0_var(--color-border-primary)]\",\n \"data-[orientation=vertical]:flex-col data-[orientation=vertical]:shadow-[inset_-1px_0_0_0_var(--color-border-primary)]\",\n className,\n )}\n {...props}\n >\n {children}\n <span\n ref={indicatorRef}\n aria-hidden\n className=\"pointer-events-none absolute z-10 rounded-full motion-safe:transition-[transform,width,height] motion-safe:duration-200 motion-safe:ease-in-out\"\n style={{ opacity: 0 }}\n />\n </TabsPrimitive.List>\n );\n});\n\nTabsList.displayName = \"TabsList\";\n"],"names":[],"mappings":";;;;;AAOA,MAAM,mBAA0D;AAAA,EAC9D,QAAQ;AAAA,EACR,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAEA,SAAS,eACP,SACA,WACA,WACQ;AACR,MAAI,YAAY,MAAO,QAAO;AAC9B,MAAI,YAAY,OAAQ,QAAO;AAE/B,MAAI,CAAC,UAAW,QAAO;AAEvB,QAAM,OAAO;AACb,MAAI,cAAc,KAAM,QAAO,GAAG,IAAI,IAAI,iBAAiB,MAAM;AACjE,MAAI,OAAO,cAAc,SAAU,QAAO,GAAG,IAAI,IAAI,iBAAiB,SAAS,CAAC;AAChF,SAAO;AACT;AAkBO,MAAM,WAAW,MAAM,WAG5B,CAAC,EAAE,WAAW,UAAU,YAAY,MAAM,WAAW,SAAS,GAAG,MAAA,GAAS,QAAQ;AAClF,QAAM,WAAW,MAAM,OAAuB,IAAI;AAClD,QAAM,eAAe,MAAM,OAAwB,IAAI;AAEvD,QAAM,oBAAoB,KAAK,MAAM,SAAS,OAAyB;AAEvE,QAAM,kBAAkB,MAAM,YAAY,MAAM;AAC9C,UAAM,OAAO,SAAS;AACtB,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,QAAQ,CAAC,UAAW;AAEzB,UAAM,YAAY,KAAK,cAA2B,uBAAuB;AACzE,QAAI,CAAC,WAAW;AACd,gBAAU,MAAM,UAAU;AAC1B;AAAA,IACF;AAEA,UAAM,aAAa,KAAK,QAAQ,gBAAgB;AAEhD,cAAU,MAAM,UAAU;AAE1B,QAAI,YAAY;AACd,gBAAU,MAAM,aACd;AACF,gBAAU,MAAM,QAAQ;AACxB,gBAAU,MAAM,QAAQ;AACxB,gBAAU,MAAM,SAAS,GAAG,UAAU,YAAY;AAClD,gBAAU,MAAM,YAAY,cAAc,UAAU,SAAS;AAAA,IAC/D,OAAO;AACL,gBAAU,MAAM,aACd;AACF,YAAM,WAAW,UAAU,cAAc,MAAM;AAC/C,YAAM,aAAa,YAAY,WAAW,sBAAA,EAAwB;AAClE,YAAM,YAAY,UAAU,aAAa,UAAU,cAAc;AACjE,YAAM,gBAAgB,YAAY,YAAY;AAC9C,gBAAU,MAAM,QAAQ;AACxB,gBAAU,MAAM,SAAS;AACzB,gBAAU,MAAM,QAAQ,GAAG,SAAS;AACpC,gBAAU,MAAM,YAAY,cAAc,aAAa;AAAA,IACzD;AAAA,EACF,GAAG,CAAA,CAAE;AAEL,QAAM,gBAAgB,MAAM;AAC1B,UAAM,OAAO,SAAS;AACtB,UAAM,YAAY,aAAa;AAC/B,QAAI,CAAC,QAAQ,CAAC,UAAW;AAEzB,cAAU,MAAM,qBAAqB;AACrC,oBAAA;AACA,cAAU,sBAAA;AACV,cAAU,MAAM,qBAAqB;AAErC,UAAM,mBAAmB,IAAI,iBAAiB,eAAe;AAC7D,qBAAiB,QAAQ,MAAM;AAAA,MAC7B,YAAY;AAAA,MACZ,iBAAiB,CAAC,YAAY;AAAA,MAC9B,WAAW;AAAA,MACX,SAAS;AAAA,IAAA,CACV;AAED,UAAM,iBAAiB,IAAI,eAAe,eAAe;AACzD,mBAAe,QAAQ,IAAI;AAE3B,QAAI,YAAY;AAChB,QAAI,SAAS,OAAO,WAAW,UAAU;AACvC,eAAS,OAAO,MAAM,KAAK,MAAM;AAC/B,YAAI,CAAC,UAAW,iBAAA;AAAA,MAClB,CAAC;AAAA,IACH;AAEA,WAAO,MAAM;AACX,kBAAY;AACZ,uBAAiB,WAAA;AACjB,qBAAe,WAAA;AAAA,IACjB;AAAA,EACF,GAAG,CAAC,eAAe,CAAC;AAEpB,SACE;AAAA,IAAC,cAAc;AAAA,IAAd;AAAA,MACC,KAAK;AAAA,MACL,WAAW;AAAA;AAAA;AAAA;AAAA,QAIT;AAAA,QACA,eAAe,SAAS,WAAW,SAAS;AAAA,QAC5C;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,MAED,GAAG;AAAA,MAEH,UAAA;AAAA,QAAA;AAAA,QACD;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,KAAK;AAAA,YACL,eAAW;AAAA,YACX,WAAU;AAAA,YACV,OAAO,EAAE,SAAS,EAAA;AAAA,UAAE;AAAA,QAAA;AAAA,MACtB;AAAA,IAAA;AAAA,EAAA;AAGN,CAAC;AAED,SAAS,cAAc;"}
|
package/dist/styles/base.css
CHANGED
|
@@ -94,6 +94,22 @@
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/*
|
|
98
|
+
* Animate `height` between 0 and the panel's measured height
|
|
99
|
+
* (`--radix-accordion-content-height`, which Radix sets from a getBoundingClientRect
|
|
100
|
+
* in a useLayoutEffect). This is the canonical Radix/Collapsible pattern and keeps the
|
|
101
|
+
* closed state truly hidden: the paired `AccordionContent` adds no `display` override,
|
|
102
|
+
* so a closed panel keeps Radix's `hidden` attribute and `[hidden] { display: none }`
|
|
103
|
+
* removes its footprint entirely.
|
|
104
|
+
*
|
|
105
|
+
* Perf note (ENG-7598 — mobile jank): `height` is layout-animated and is NOT composited
|
|
106
|
+
* (only `transform`/`opacity` run off the main thread). To keep the animation smooth and
|
|
107
|
+
* stop the padded content reflowing/repainting every frame, `AccordionContent` wraps the
|
|
108
|
+
* content in an isolated `contain: layout paint` layer — the parent's height animation then
|
|
109
|
+
* only re-clips that cached layer. We deliberately do NOT animate `grid-template-rows`
|
|
110
|
+
* (also layout-bound, and `display: grid` defeats the `[hidden] { display: none }` closed
|
|
111
|
+
* state) and do NOT add `will-change` (a no-op hint for layout properties that wastes memory).
|
|
112
|
+
*/
|
|
97
113
|
@keyframes accordion-expand {
|
|
98
114
|
from {
|
|
99
115
|
height: 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fanvue/ui",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.2",
|
|
4
4
|
"description": "React component library built with Tailwind CSS for Fanvue ecosystem",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
],
|
|
52
52
|
"scripts": {
|
|
53
53
|
"dev": "vite --host",
|
|
54
|
+
"dev:watch": "vite build --watch",
|
|
54
55
|
"build": "vite build && mkdir -p dist/styles && cp src/styles/theme.css src/styles/base.css dist/styles/",
|
|
55
56
|
"build:showcase": "vite build --config vite.showcase.config.ts",
|
|
56
57
|
"preview": "vite preview",
|