@hyddenlabs/hydn-ui 0.3.8 → 0.3.9

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.
@@ -10,7 +10,7 @@ export type Tab = {
10
10
  export type TabsProps = {
11
11
  /** Array of tab configurations with id, label, and content */
12
12
  tabs: Tab[];
13
- /** ID of the tab to display initially (defaults to first tab) */
13
+ /** ID of the tab to display initially when no URL param is present (defaults to first tab) */
14
14
  defaultTab?: string;
15
15
  /** Additional CSS classes for the tabs container */
16
16
  className?: string;
@@ -18,13 +18,22 @@ export type TabsProps = {
18
18
  ariaLabel?: string;
19
19
  /** Remove the bottom border from the tab list */
20
20
  noBorder?: boolean;
21
- /** Optional localStorage key to persist the active tab */
22
- storageKey?: string;
21
+ /**
22
+ * URL search-param key used to persist the active tab (defaults to `"tab"`).
23
+ * e.g. `urlParam="section"` → `?section=details`
24
+ */
25
+ urlParam?: string;
26
+ /** Called every time the active tab changes. */
27
+ onTabChange?: (tabId: string) => void;
23
28
  };
24
29
  /**
25
- * Accessible Tabs component with animated tab switching using PageTransition
30
+ * Accessible Tabs component with animated tab switching using PageTransition.
31
+ *
32
+ * Active tab state is automatically persisted in the URL as a search param
33
+ * (default key: `"tab"`). Browser back/forward navigation is supported.
34
+ * Falls back to `defaultTab` or the first tab when the param is absent or invalid.
26
35
  */
27
- declare function Tabs({ tabs, defaultTab, className, ariaLabel, noBorder, storageKey }: Readonly<TabsProps>): import("react/jsx-runtime").JSX.Element;
36
+ declare function Tabs({ tabs, defaultTab, className, ariaLabel, noBorder, urlParam, onTabChange }: Readonly<TabsProps>): import("react/jsx-runtime").JSX.Element;
28
37
  declare namespace Tabs {
29
38
  var displayName: string;
30
39
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["../../../../src/components/navigation/tabs/tabs.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmB,MAAM,OAAO,CAAC;AAGxC,MAAM,MAAM,GAAG,GAAG;IAChB,oCAAoC;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,8DAA8D;IAC9D,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;GAEG;AACH,iBAAS,IAAI,CAAC,EACZ,IAAI,EACJ,UAAU,EACV,SAAc,EACd,SAAkB,EAClB,QAAgB,EAChB,UAAU,EACX,EAAE,QAAQ,CAAC,SAAS,CAAC,2CA6DrB;kBApEQ,IAAI;;;AAwEb,eAAe,IAAI,CAAC"}
1
+ {"version":3,"file":"tabs.d.ts","sourceRoot":"","sources":["../../../../src/components/navigation/tabs/tabs.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAC;AAGnD,MAAM,MAAM,GAAG,GAAG;IAChB,oCAAoC;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,8DAA8D;IAC9D,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,8FAA8F;IAC9F,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC,CAAC;AAEF;;;;;;GAMG;AACH,iBAAS,IAAI,CAAC,EACZ,IAAI,EACJ,UAAU,EACV,SAAc,EACd,SAAkB,EAClB,QAAgB,EAChB,QAAgB,EAChB,WAAW,EACZ,EAAE,QAAQ,CAAC,SAAS,CAAC,2CAoErB;kBA5EQ,IAAI;;;AAgFb,eAAe,IAAI,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
- import { useState } from "react";
2
+ import { useState, useEffect } from "react";
3
3
  import { PageTransition } from "../../layout/page-transition/page-transition.js";
4
4
  function Tabs({
5
5
  tabs,
@@ -7,25 +7,30 @@ function Tabs({
7
7
  className = "",
8
8
  ariaLabel = "Tabs",
9
9
  noBorder = false,
10
- storageKey
10
+ urlParam = "tab",
11
+ onTabChange
11
12
  }) {
12
- const [activeTab, setActiveTab] = useState(() => {
13
- if (storageKey) {
14
- const stored = localStorage.getItem(storageKey);
15
- if (stored && tabs.some((tab) => tab.id === stored)) {
16
- return stored;
17
- }
18
- }
19
- return defaultTab || tabs[0]?.id;
20
- });
13
+ const resolveTab = (id) => {
14
+ if (id && tabs.some((tab) => tab.id === id)) return id;
15
+ if (defaultTab && tabs.some((tab) => tab.id === defaultTab)) return defaultTab;
16
+ return tabs[0]?.id ?? "";
17
+ };
18
+ const getTabFromUrl = () => {
19
+ const params = new URLSearchParams(window.location.search);
20
+ return resolveTab(params.get(urlParam));
21
+ };
22
+ const [activeTab, setActiveTab] = useState(getTabFromUrl);
23
+ useEffect(() => {
24
+ const handlePopState = () => setActiveTab(getTabFromUrl());
25
+ window.addEventListener("popstate", handlePopState);
26
+ return () => window.removeEventListener("popstate", handlePopState);
27
+ }, [urlParam, tabs, defaultTab]);
21
28
  const handleSetActiveTab = (tabId) => {
22
29
  setActiveTab(tabId);
23
- if (storageKey) {
24
- try {
25
- localStorage.setItem(storageKey, tabId);
26
- } catch {
27
- }
28
- }
30
+ const params = new URLSearchParams(window.location.search);
31
+ params.set(urlParam, tabId);
32
+ window.history.replaceState(null, "", `?${params.toString()}`);
33
+ onTabChange?.(tabId);
29
34
  };
30
35
  return /* @__PURE__ */ jsxs("div", { className, children: [
31
36
  /* @__PURE__ */ jsx("div", { role: "tablist", "aria-label": ariaLabel, className: `flex ${noBorder ? "" : "border-b-2 border-border/50"}`, children: tabs.map((tab) => /* @__PURE__ */ jsx(
@@ -1 +1 @@
1
- {"version":3,"file":"tabs.js","sources":["../../../../src/components/navigation/tabs/tabs.tsx"],"sourcesContent":["import React, { useState } from 'react';\nimport PageTransition from '../../layout/page-transition/page-transition';\n\nexport type Tab = {\n /** Unique identifier for the tab */\n id: string;\n /** Display text shown in the tab button */\n label: string;\n /** Content to render when this tab is active */\n content: React.ReactNode;\n};\n\nexport type TabsProps = {\n /** Array of tab configurations with id, label, and content */\n tabs: Tab[];\n /** ID of the tab to display initially (defaults to first tab) */\n defaultTab?: string;\n /** Additional CSS classes for the tabs container */\n className?: string;\n /** Accessibility label for the tab list */\n ariaLabel?: string;\n /** Remove the bottom border from the tab list */\n noBorder?: boolean;\n /** Optional localStorage key to persist the active tab */\n storageKey?: string;\n};\n\n/**\n * Accessible Tabs component with animated tab switching using PageTransition\n */\nfunction Tabs({\n tabs,\n defaultTab,\n className = '',\n ariaLabel = 'Tabs',\n noBorder = false,\n storageKey\n}: Readonly<TabsProps>) {\n const [activeTab, setActiveTab] = useState(() => {\n if (storageKey) {\n const stored = localStorage.getItem(storageKey);\n if (stored && tabs.some((tab) => tab.id === stored)) {\n return stored;\n }\n }\n return defaultTab || tabs[0]?.id;\n });\n\n // Save to localStorage whenever activeTab changes\n const handleSetActiveTab = (tabId: string) => {\n setActiveTab(tabId);\n if (storageKey) {\n try {\n localStorage.setItem(storageKey, tabId);\n } catch {\n // Persistence is best-effort; ignore storage errors\n }\n }\n };\n\n return (\n <div className={className}>\n <div role=\"tablist\" aria-label={ariaLabel} className={`flex ${noBorder ? '' : 'border-b-2 border-border/50'}`}>\n {tabs.map((tab) => (\n <button\n key={tab.id}\n role=\"tab\"\n aria-selected={activeTab === tab.id}\n aria-controls={`panel-${tab.id}`}\n id={`tab-${tab.id}`}\n onClick={() => handleSetActiveTab(tab.id)}\n className={`px-4 py-3 font-medium cursor-pointer transition-all duration-200 focus:outline-none relative hover:text-foreground hover:bg-muted/50 ${\n activeTab === tab.id\n ? 'text-primary hover:text-primary after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-primary after:rounded-t after:transition-all after:duration-200'\n : 'text-muted-foreground'\n }`}\n >\n {tab.label}\n </button>\n ))}\n </div>\n {tabs.map((tab) => (\n <div\n key={tab.id}\n role=\"tabpanel\"\n id={`panel-${tab.id}`}\n aria-labelledby={`tab-${tab.id}`}\n hidden={activeTab !== tab.id}\n >\n {activeTab === tab.id && (\n <PageTransition type=\"fade\" duration={150}>\n <div className=\"pt-6\">{tab.content}</div>\n </PageTransition>\n )}\n </div>\n ))}\n </div>\n );\n}\n\nTabs.displayName = 'Tabs';\n\nexport default Tabs;\n"],"names":[],"mappings":";;;AA8BA,SAAS,KAAK;AAAA,EACZ;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,WAAW;AAAA,EACX;AACF,GAAwB;AACtB,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,MAAM;AAC/C,QAAI,YAAY;AACd,YAAM,SAAS,aAAa,QAAQ,UAAU;AAC9C,UAAI,UAAU,KAAK,KAAK,CAAC,QAAQ,IAAI,OAAO,MAAM,GAAG;AACnD,eAAO;AAAA,MACT;AAAA,IACF;AACA,WAAO,cAAc,KAAK,CAAC,GAAG;AAAA,EAChC,CAAC;AAGD,QAAM,qBAAqB,CAAC,UAAkB;AAC5C,iBAAa,KAAK;AAClB,QAAI,YAAY;AACd,UAAI;AACF,qBAAa,QAAQ,YAAY,KAAK;AAAA,MACxC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AAEA,SACE,qBAAC,SAAI,WACH,UAAA;AAAA,IAAA,oBAAC,OAAA,EAAI,MAAK,WAAU,cAAY,WAAW,WAAW,QAAQ,WAAW,KAAK,6BAA6B,IACxG,UAAA,KAAK,IAAI,CAAC,QACT;AAAA,MAAC;AAAA,MAAA;AAAA,QAEC,MAAK;AAAA,QACL,iBAAe,cAAc,IAAI;AAAA,QACjC,iBAAe,SAAS,IAAI,EAAE;AAAA,QAC9B,IAAI,OAAO,IAAI,EAAE;AAAA,QACjB,SAAS,MAAM,mBAAmB,IAAI,EAAE;AAAA,QACxC,WAAW,wIACT,cAAc,IAAI,KACd,kLACA,uBACN;AAAA,QAEC,UAAA,IAAI;AAAA,MAAA;AAAA,MAZA,IAAI;AAAA,IAAA,CAcZ,GACH;AAAA,IACC,KAAK,IAAI,CAAC,QACT;AAAA,MAAC;AAAA,MAAA;AAAA,QAEC,MAAK;AAAA,QACL,IAAI,SAAS,IAAI,EAAE;AAAA,QACnB,mBAAiB,OAAO,IAAI,EAAE;AAAA,QAC9B,QAAQ,cAAc,IAAI;AAAA,QAEzB,UAAA,cAAc,IAAI,MACjB,oBAAC,kBAAe,MAAK,QAAO,UAAU,KACpC,8BAAC,OAAA,EAAI,WAAU,QAAQ,UAAA,IAAI,SAAQ,EAAA,CACrC;AAAA,MAAA;AAAA,MATG,IAAI;AAAA,IAAA,CAYZ;AAAA,EAAA,GACH;AAEJ;AAEA,KAAK,cAAc;"}
1
+ {"version":3,"file":"tabs.js","sources":["../../../../src/components/navigation/tabs/tabs.tsx"],"sourcesContent":["import React, { useEffect, useState } from 'react';\nimport PageTransition from '../../layout/page-transition/page-transition';\n\nexport type Tab = {\n /** Unique identifier for the tab */\n id: string;\n /** Display text shown in the tab button */\n label: string;\n /** Content to render when this tab is active */\n content: React.ReactNode;\n};\n\nexport type TabsProps = {\n /** Array of tab configurations with id, label, and content */\n tabs: Tab[];\n /** ID of the tab to display initially when no URL param is present (defaults to first tab) */\n defaultTab?: string;\n /** Additional CSS classes for the tabs container */\n className?: string;\n /** Accessibility label for the tab list */\n ariaLabel?: string;\n /** Remove the bottom border from the tab list */\n noBorder?: boolean;\n /**\n * URL search-param key used to persist the active tab (defaults to `\"tab\"`).\n * e.g. `urlParam=\"section\"` → `?section=details`\n */\n urlParam?: string;\n /** Called every time the active tab changes. */\n onTabChange?: (tabId: string) => void;\n};\n\n/**\n * Accessible Tabs component with animated tab switching using PageTransition.\n *\n * Active tab state is automatically persisted in the URL as a search param\n * (default key: `\"tab\"`). Browser back/forward navigation is supported.\n * Falls back to `defaultTab` or the first tab when the param is absent or invalid.\n */\nfunction Tabs({\n tabs,\n defaultTab,\n className = '',\n ariaLabel = 'Tabs',\n noBorder = false,\n urlParam = 'tab',\n onTabChange\n}: Readonly<TabsProps>) {\n const resolveTab = (id: string | null | undefined): string => {\n if (id && tabs.some((tab) => tab.id === id)) return id;\n if (defaultTab && tabs.some((tab) => tab.id === defaultTab)) return defaultTab;\n return tabs[0]?.id ?? '';\n };\n\n const getTabFromUrl = (): string => {\n const params = new URLSearchParams(window.location.search);\n return resolveTab(params.get(urlParam));\n };\n\n const [activeTab, setActiveTab] = useState<string>(getTabFromUrl);\n\n // Keep state in sync when the user navigates back/forward.\n useEffect(() => {\n const handlePopState = () => setActiveTab(getTabFromUrl());\n window.addEventListener('popstate', handlePopState);\n return () => window.removeEventListener('popstate', handlePopState);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [urlParam, tabs, defaultTab]);\n\n const handleSetActiveTab = (tabId: string) => {\n setActiveTab(tabId);\n const params = new URLSearchParams(window.location.search);\n params.set(urlParam, tabId);\n window.history.replaceState(null, '', `?${params.toString()}`);\n onTabChange?.(tabId);\n };\n\n return (\n <div className={className}>\n <div role=\"tablist\" aria-label={ariaLabel} className={`flex ${noBorder ? '' : 'border-b-2 border-border/50'}`}>\n {tabs.map((tab) => (\n <button\n key={tab.id}\n role=\"tab\"\n aria-selected={activeTab === tab.id}\n aria-controls={`panel-${tab.id}`}\n id={`tab-${tab.id}`}\n onClick={() => handleSetActiveTab(tab.id)}\n className={`px-4 py-3 font-medium cursor-pointer transition-all duration-200 focus:outline-none relative hover:text-foreground hover:bg-muted/50 ${\n activeTab === tab.id\n ? 'text-primary hover:text-primary after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-primary after:rounded-t after:transition-all after:duration-200'\n : 'text-muted-foreground'\n }`}\n >\n {tab.label}\n </button>\n ))}\n </div>\n {tabs.map((tab) => (\n <div\n key={tab.id}\n role=\"tabpanel\"\n id={`panel-${tab.id}`}\n aria-labelledby={`tab-${tab.id}`}\n hidden={activeTab !== tab.id}\n >\n {activeTab === tab.id && (\n <PageTransition type=\"fade\" duration={150}>\n <div className=\"pt-6\">{tab.content}</div>\n </PageTransition>\n )}\n </div>\n ))}\n </div>\n );\n}\n\nTabs.displayName = 'Tabs';\n\nexport default Tabs;\n"],"names":[],"mappings":";;;AAuCA,SAAS,KAAK;AAAA,EACZ;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,WAAW;AAAA,EACX;AACF,GAAwB;AACtB,QAAM,aAAa,CAAC,OAA0C;AAC5D,QAAI,MAAM,KAAK,KAAK,CAAC,QAAQ,IAAI,OAAO,EAAE,EAAG,QAAO;AACpD,QAAI,cAAc,KAAK,KAAK,CAAC,QAAQ,IAAI,OAAO,UAAU,EAAG,QAAO;AACpE,WAAO,KAAK,CAAC,GAAG,MAAM;AAAA,EACxB;AAEA,QAAM,gBAAgB,MAAc;AAClC,UAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,WAAO,WAAW,OAAO,IAAI,QAAQ,CAAC;AAAA,EACxC;AAEA,QAAM,CAAC,WAAW,YAAY,IAAI,SAAiB,aAAa;AAGhE,YAAU,MAAM;AACd,UAAM,iBAAiB,MAAM,aAAa,eAAe;AACzD,WAAO,iBAAiB,YAAY,cAAc;AAClD,WAAO,MAAM,OAAO,oBAAoB,YAAY,cAAc;AAAA,EAEpE,GAAG,CAAC,UAAU,MAAM,UAAU,CAAC;AAE/B,QAAM,qBAAqB,CAAC,UAAkB;AAC5C,iBAAa,KAAK;AAClB,UAAM,SAAS,IAAI,gBAAgB,OAAO,SAAS,MAAM;AACzD,WAAO,IAAI,UAAU,KAAK;AAC1B,WAAO,QAAQ,aAAa,MAAM,IAAI,IAAI,OAAO,SAAA,CAAU,EAAE;AAC7D,kBAAc,KAAK;AAAA,EACrB;AAEA,SACE,qBAAC,SAAI,WACH,UAAA;AAAA,IAAA,oBAAC,OAAA,EAAI,MAAK,WAAU,cAAY,WAAW,WAAW,QAAQ,WAAW,KAAK,6BAA6B,IACxG,UAAA,KAAK,IAAI,CAAC,QACT;AAAA,MAAC;AAAA,MAAA;AAAA,QAEC,MAAK;AAAA,QACL,iBAAe,cAAc,IAAI;AAAA,QACjC,iBAAe,SAAS,IAAI,EAAE;AAAA,QAC9B,IAAI,OAAO,IAAI,EAAE;AAAA,QACjB,SAAS,MAAM,mBAAmB,IAAI,EAAE;AAAA,QACxC,WAAW,wIACT,cAAc,IAAI,KACd,kLACA,uBACN;AAAA,QAEC,UAAA,IAAI;AAAA,MAAA;AAAA,MAZA,IAAI;AAAA,IAAA,CAcZ,GACH;AAAA,IACC,KAAK,IAAI,CAAC,QACT;AAAA,MAAC;AAAA,MAAA;AAAA,QAEC,MAAK;AAAA,QACL,IAAI,SAAS,IAAI,EAAE;AAAA,QACnB,mBAAiB,OAAO,IAAI,EAAE;AAAA,QAC9B,QAAQ,cAAc,IAAI;AAAA,QAEzB,UAAA,cAAc,IAAI,MACjB,oBAAC,kBAAe,MAAK,QAAO,UAAU,KACpC,8BAAC,OAAA,EAAI,WAAU,QAAQ,UAAA,IAAI,SAAQ,EAAA,CACrC;AAAA,MAAA;AAAA,MATG,IAAI;AAAA,IAAA,CAYZ;AAAA,EAAA,GACH;AAEJ;AAEA,KAAK,cAAc;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyddenlabs/hydn-ui",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "private": false,
5
5
  "description": "A modern React component library built with TypeScript and Tailwind CSS",
6
6
  "main": "./dist/index.js",