@comitor/ui 1.0.0 → 1.1.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.
@@ -14,6 +14,13 @@ interface ContrastProviderProps {
14
14
  defaultContrast?: ContrastMode;
15
15
  /** Khoá localStorage. Đổi khi một app muốn tách lựa chọn khỏi các app khác cùng tên miền. */
16
16
  storageKey?: string;
17
+ /**
18
+ * Nonce của CSP, cho `<ContrastScript>` mà provider này TỰ render.
19
+ *
20
+ * Bỏ trống nếu app không ép `script-src` — `undefined` nghĩa là thẻ không mang thuộc tính `nonce`,
21
+ * đúng hành vi trước đây. Xem `ContrastScript` để biết vì sao nó chỉ phát ở phía máy chủ.
22
+ */
23
+ nonce?: string;
17
24
  }
18
25
  /**
19
26
  * Trục thứ HAI của giao diện, độc lập với sáng/tối: chọn BẢNG MÀU.
@@ -40,7 +47,7 @@ interface ContrastProviderProps {
40
47
  * `<html>` phải có `suppressHydrationWarning` (cùng lý do với ThemeProvider: script sửa DOM
41
48
  * trước khi React hydrate nên HTML client luôn khác HTML server).
42
49
  */
43
- declare function ContrastProvider({ children, defaultContrast, storageKey }: ContrastProviderProps): React$1.JSX.Element;
50
+ declare function ContrastProvider({ children, defaultContrast, storageKey, nonce }: ContrastProviderProps): React$1.JSX.Element;
44
51
  /**
45
52
  * Script chống nháy. `<ContrastProvider>` đã render sẵn nó nên **app thường không cần gọi tay** —
46
53
  * export ra chỉ cho trường hợp app tự dựng provider hoặc cần đặt nó ở một vị trí khác trong body.
@@ -48,8 +55,9 @@ declare function ContrastProvider({ children, defaultContrast, storageKey }: Con
48
55
  * ⚠ ĐỪNG đặt vào `<head>`: React 19 báo lỗi console ở mọi lần render client (xem chú thích của
49
56
  * `ContrastProvider`). Gọi hai lần cũng vô hại — script idempotent — chỉ tốn một thẻ.
50
57
  */
51
- declare function ContrastScript({ storageKey }: {
58
+ declare function ContrastScript({ storageKey, nonce }: {
52
59
  storageKey?: string;
60
+ nonce?: string;
53
61
  }): React$1.JSX.Element;
54
62
  /**
55
63
  * Đọc/đổi bảng màu. Ném lỗi nếu gọi ngoài `<ContrastProvider>` — im lặng trả mặc định sẽ khiến
@@ -6,7 +6,8 @@ const ContrastContext = createContext(null);
6
6
  function ContrastProvider({
7
7
  children,
8
8
  defaultContrast = "normal",
9
- storageKey = CONTRAST_STORAGE_KEY
9
+ storageKey = CONTRAST_STORAGE_KEY,
10
+ nonce
10
11
  }) {
11
12
  const [contrast, setContrastState] = useState(defaultContrast);
12
13
  const [mounted, setMounted] = useState(false);
@@ -40,13 +41,20 @@ function ContrastProvider({
40
41
  [contrast, setContrast, mounted]
41
42
  );
42
43
  return /* @__PURE__ */ jsxs(ContrastContext.Provider, { value, children: [
43
- /* @__PURE__ */ jsx(ContrastScript, { storageKey }),
44
+ /* @__PURE__ */ jsx(ContrastScript, { storageKey, nonce }),
44
45
  children
45
46
  ] });
46
47
  }
47
- function ContrastScript({ storageKey = CONTRAST_STORAGE_KEY }) {
48
+ function ContrastScript({ storageKey = CONTRAST_STORAGE_KEY, nonce }) {
48
49
  const script = `(function(){try{var v=localStorage.getItem(${JSON.stringify(storageKey)});if(v==="high")document.documentElement.setAttribute(${JSON.stringify(CONTRAST_ATTRIBUTE)},"high")}catch(e){}})();`;
49
- return /* @__PURE__ */ jsx("script", { suppressHydrationWarning: true, dangerouslySetInnerHTML: { __html: script } });
50
+ return /* @__PURE__ */ jsx(
51
+ "script",
52
+ {
53
+ suppressHydrationWarning: true,
54
+ nonce: typeof window === "undefined" ? nonce : "",
55
+ dangerouslySetInnerHTML: { __html: script }
56
+ }
57
+ );
50
58
  }
51
59
  function useContrast() {
52
60
  const ctx = useContext(ContrastContext);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/contrast-provider.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport { CONTRAST_ATTRIBUTE, CONTRAST_STORAGE_KEY, type ContrastMode, isContrastMode } from \"./contrast-constants.js\";\n\nexport interface ContrastContextValue {\n /** Chế độ đang áp dụng. Trước khi mount luôn là giá trị mặc định, không phải giá trị đã lưu. */\n contrast: ContrastMode;\n setContrast: (mode: ContrastMode) => void;\n /** `false` cho tới khi effect đầu tiên chạy — dùng để tránh lệch hydration, xem ContrastToggle. */\n mounted: boolean;\n}\n\nconst ContrastContext = createContext<ContrastContextValue | null>(null);\n\nexport interface ContrastProviderProps {\n children: React.ReactNode;\n /** Bảng màu khi người dùng chưa chọn gì. Mặc định `normal` (comitor-ds). */\n defaultContrast?: ContrastMode;\n /** Khoá localStorage. Đổi khi một app muốn tách lựa chọn khỏi các app khác cùng tên miền. */\n storageKey?: string;\n}\n\n/**\n * Trục thứ HAI của giao diện, độc lập với sáng/tối: chọn BẢNG MÀU.\n *\n * ─ VÌ SAO KHÔNG DÙNG `next-themes` CHO TRỤC NÀY ─\n * `next-themes` giữ **một context singleton** cho toàn cây. Lồng hai `<ThemeProvider>` với\n * `attribute`/`storageKey` khác nhau thì provider trong cùng thắng và `useTheme()` của app đọc\n * nhầm trục — không có lỗi biên dịch nào, chỉ có nút đổi sáng/tối lặng lẽ ngừng ăn. Trục tương\n * phản đơn giản hơn theme (không có \"system\", không đọc `prefers-color-scheme`) nên tự viết\n * ~60 dòng là đủ và không phải trả cái giá đó.\n *\n * ─ CHỐNG NHÁY ─\n * Bảng màu phải có mặt **trước** lần sơn đầu tiên, nếu không người bật tương phản cao sẽ thấy một\n * khung hình bảng mặc định rồi mới đổi. Effect của React chạy SAU khi sơn, nên phải là một script\n * đồng bộ — provider TỰ render nó (`<ContrastScript />`) làm con đầu tiên, app không phải làm gì.\n *\n * ⚠ Script nằm trong `<body>`, KHÔNG phải `<head>` — và đó là chỗ duy nhất đúng. Đặt vào `<head>`\n * thì React 19 kêu \"Encountered a script tag while rendering React component\" ở mỗi lần render\n * client (script inline không thuộc nhóm phần tử React hoisted được), tức console đỏ ở mọi trang.\n * Trong `<body>` thì không, và về chống nháy vẫn tương đương: trình duyệt chạy script ngay khi\n * parser đi tới nó, mà nó đứng TRƯỚC toàn bộ markup của app. Đây cũng đúng chỗ next-themes đặt\n * script của nó.\n *\n * `<html>` phải có `suppressHydrationWarning` (cùng lý do với ThemeProvider: script sửa DOM\n * trước khi React hydrate nên HTML client luôn khác HTML server).\n */\nexport function ContrastProvider({\n children,\n defaultContrast = \"normal\",\n storageKey = CONTRAST_STORAGE_KEY\n}: ContrastProviderProps) {\n const [contrast, setContrastState] = useState<ContrastMode>(defaultContrast);\n const [mounted, setMounted] = useState(false);\n\n // Đọc lựa chọn đã lưu SAU khi mount. Trước đó state phải bằng đúng giá trị server đã render,\n // nếu không React báo lệch hydration — DOM thật đã được ContrastScript đặt đúng từ trước rồi.\n useEffect(() => {\n let stored: string | null = null;\n try {\n stored = window.localStorage.getItem(storageKey);\n } catch {\n // Safari chế độ riêng tư / localStorage bị chặn: rơi về mặc định, không phải lỗi.\n }\n if (isContrastMode(stored)) setContrastState(stored);\n setMounted(true);\n }, [storageKey]);\n\n useEffect(() => {\n if (!mounted) return;\n const root = document.documentElement;\n if (contrast === \"normal\") root.removeAttribute(CONTRAST_ATTRIBUTE);\n else root.setAttribute(CONTRAST_ATTRIBUTE, contrast);\n }, [contrast, mounted]);\n\n const setContrast = useCallback(\n (mode: ContrastMode) => {\n setContrastState(mode);\n try {\n window.localStorage.setItem(storageKey, mode);\n } catch {\n // Không lưu được thì lựa chọn chỉ sống trong phiên này — vẫn tốt hơn là ném lỗi.\n }\n },\n [storageKey]\n );\n\n const value = useMemo<ContrastContextValue>(\n () => ({ contrast, setContrast, mounted }),\n [contrast, setContrast, mounted]\n );\n\n return (\n <ContrastContext.Provider value={value}>\n <ContrastScript storageKey={storageKey} />\n {children}\n </ContrastContext.Provider>\n );\n}\n\n/**\n * Script chống nháy. `<ContrastProvider>` đã render sẵn nó nên **app thường không cần gọi tay** —\n * export ra chỉ cho trường hợp app tự dựng provider hoặc cần đặt nó ở một vị trí khác trong body.\n *\n * ⚠ ĐỪNG đặt vào `<head>`: React 19 báo lỗi console ở mọi lần render client (xem chú thích của\n * `ContrastProvider`). Gọi hai lần cũng vô hại — script idempotent — chỉ tốn một thẻ.\n */\nexport function ContrastScript({ storageKey = CONTRAST_STORAGE_KEY }: { storageKey?: string }) {\n // Chuỗi tự gọi, không phụ thuộc React: chạy ngay lúc trình duyệt gặp thẻ <script>.\n const script = `(function(){try{var v=localStorage.getItem(${JSON.stringify(storageKey)});if(v===\"high\")document.documentElement.setAttribute(${JSON.stringify(CONTRAST_ATTRIBUTE)},\"high\")}catch(e){}})();`;\n // biome-ignore lint/security/noDangerouslySetInnerHtml: chuỗi hằng do chính package sinh, không có dữ liệu người dùng.\n return <script suppressHydrationWarning dangerouslySetInnerHTML={{ __html: script }} />;\n}\n\n/**\n * Đọc/đổi bảng màu. Ném lỗi nếu gọi ngoài `<ContrastProvider>` — im lặng trả mặc định sẽ khiến\n * nút gạt trông như hỏng mà không có gì chỉ ra nguyên nhân.\n */\nexport function useContrast(): ContrastContextValue {\n const ctx = useContext(ContrastContext);\n if (!ctx) throw new Error(\"useContrast() phải nằm trong <ContrastProvider>. Bọc nó ở root layout.\");\n return ctx;\n}\n"],"mappings":";AA8FI,SACE,KADF;AA5FJ,SAAS,eAAe,aAAa,YAAY,WAAW,SAAS,gBAAgB;AACrF,SAAS,oBAAoB,sBAAyC,sBAAsB;AAU5F,MAAM,kBAAkB,cAA2C,IAAI;AAmChE,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA,kBAAkB;AAAA,EAClB,aAAa;AACf,GAA0B;AACxB,QAAM,CAAC,UAAU,gBAAgB,IAAI,SAAuB,eAAe;AAC3E,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAI5C,YAAU,MAAM;AACd,QAAI,SAAwB;AAC5B,QAAI;AACF,eAAS,OAAO,aAAa,QAAQ,UAAU;AAAA,IACjD,QAAQ;AAAA,IAER;AACA,QAAI,eAAe,MAAM,EAAG,kBAAiB,MAAM;AACnD,eAAW,IAAI;AAAA,EACjB,GAAG,CAAC,UAAU,CAAC;AAEf,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AACd,UAAM,OAAO,SAAS;AACtB,QAAI,aAAa,SAAU,MAAK,gBAAgB,kBAAkB;AAAA,QAC7D,MAAK,aAAa,oBAAoB,QAAQ;AAAA,EACrD,GAAG,CAAC,UAAU,OAAO,CAAC;AAEtB,QAAM,cAAc;AAAA,IAClB,CAAC,SAAuB;AACtB,uBAAiB,IAAI;AACrB,UAAI;AACF,eAAO,aAAa,QAAQ,YAAY,IAAI;AAAA,MAC9C,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AAEA,QAAM,QAAQ;AAAA,IACZ,OAAO,EAAE,UAAU,aAAa,QAAQ;AAAA,IACxC,CAAC,UAAU,aAAa,OAAO;AAAA,EACjC;AAEA,SACE,qBAAC,gBAAgB,UAAhB,EAAyB,OACxB;AAAA,wBAAC,kBAAe,YAAwB;AAAA,IACvC;AAAA,KACH;AAEJ;AASO,SAAS,eAAe,EAAE,aAAa,qBAAqB,GAA4B;AAE7F,QAAM,SAAS,8CAA8C,KAAK,UAAU,UAAU,CAAC,yDAAyD,KAAK,UAAU,kBAAkB,CAAC;AAElL,SAAO,oBAAC,YAAO,0BAAwB,MAAC,yBAAyB,EAAE,QAAQ,OAAO,GAAG;AACvF;AAMO,SAAS,cAAoC;AAClD,QAAM,MAAM,WAAW,eAAe;AACtC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,+FAAwE;AAClG,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../src/shell/contrast-provider.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport { CONTRAST_ATTRIBUTE, CONTRAST_STORAGE_KEY, type ContrastMode, isContrastMode } from \"./contrast-constants.js\";\n\nexport interface ContrastContextValue {\n /** Chế độ đang áp dụng. Trước khi mount luôn là giá trị mặc định, không phải giá trị đã lưu. */\n contrast: ContrastMode;\n setContrast: (mode: ContrastMode) => void;\n /** `false` cho tới khi effect đầu tiên chạy — dùng để tránh lệch hydration, xem ContrastToggle. */\n mounted: boolean;\n}\n\nconst ContrastContext = createContext<ContrastContextValue | null>(null);\n\nexport interface ContrastProviderProps {\n children: React.ReactNode;\n /** Bảng màu khi người dùng chưa chọn gì. Mặc định `normal` (comitor-ds). */\n defaultContrast?: ContrastMode;\n /** Khoá localStorage. Đổi khi một app muốn tách lựa chọn khỏi các app khác cùng tên miền. */\n storageKey?: string;\n /**\n * Nonce của CSP, cho `<ContrastScript>` mà provider này TỰ render.\n *\n * Bỏ trống nếu app không ép `script-src` — `undefined` nghĩa là thẻ không mang thuộc tính `nonce`,\n * đúng hành vi trước đây. Xem `ContrastScript` để biết vì sao nó chỉ phát ở phía máy chủ.\n */\n nonce?: string;\n}\n\n/**\n * Trục thứ HAI của giao diện, độc lập với sáng/tối: chọn BẢNG MÀU.\n *\n * ─ VÌ SAO KHÔNG DÙNG `next-themes` CHO TRỤC NÀY ─\n * `next-themes` giữ **một context singleton** cho toàn cây. Lồng hai `<ThemeProvider>` với\n * `attribute`/`storageKey` khác nhau thì provider trong cùng thắng và `useTheme()` của app đọc\n * nhầm trục — không có lỗi biên dịch nào, chỉ có nút đổi sáng/tối lặng lẽ ngừng ăn. Trục tương\n * phản đơn giản hơn theme (không có \"system\", không đọc `prefers-color-scheme`) nên tự viết\n * ~60 dòng là đủ và không phải trả cái giá đó.\n *\n * ─ CHỐNG NHÁY ─\n * Bảng màu phải có mặt **trước** lần sơn đầu tiên, nếu không người bật tương phản cao sẽ thấy một\n * khung hình bảng mặc định rồi mới đổi. Effect của React chạy SAU khi sơn, nên phải là một script\n * đồng bộ — provider TỰ render nó (`<ContrastScript />`) làm con đầu tiên, app không phải làm gì.\n *\n * ⚠ Script nằm trong `<body>`, KHÔNG phải `<head>` — và đó là chỗ duy nhất đúng. Đặt vào `<head>`\n * thì React 19 kêu \"Encountered a script tag while rendering React component\" ở mỗi lần render\n * client (script inline không thuộc nhóm phần tử React hoisted được), tức console đỏ ở mọi trang.\n * Trong `<body>` thì không, và về chống nháy vẫn tương đương: trình duyệt chạy script ngay khi\n * parser đi tới nó, mà nó đứng TRƯỚC toàn bộ markup của app. Đây cũng đúng chỗ next-themes đặt\n * script của nó.\n *\n * `<html>` phải có `suppressHydrationWarning` (cùng lý do với ThemeProvider: script sửa DOM\n * trước khi React hydrate nên HTML client luôn khác HTML server).\n */\nexport function ContrastProvider({\n children,\n defaultContrast = \"normal\",\n storageKey = CONTRAST_STORAGE_KEY,\n nonce\n}: ContrastProviderProps) {\n const [contrast, setContrastState] = useState<ContrastMode>(defaultContrast);\n const [mounted, setMounted] = useState(false);\n\n // Đọc lựa chọn đã lưu SAU khi mount. Trước đó state phải bằng đúng giá trị server đã render,\n // nếu không React báo lệch hydration — DOM thật đã được ContrastScript đặt đúng từ trước rồi.\n useEffect(() => {\n let stored: string | null = null;\n try {\n stored = window.localStorage.getItem(storageKey);\n } catch {\n // Safari chế độ riêng tư / localStorage bị chặn: rơi về mặc định, không phải lỗi.\n }\n if (isContrastMode(stored)) setContrastState(stored);\n setMounted(true);\n }, [storageKey]);\n\n useEffect(() => {\n if (!mounted) return;\n const root = document.documentElement;\n if (contrast === \"normal\") root.removeAttribute(CONTRAST_ATTRIBUTE);\n else root.setAttribute(CONTRAST_ATTRIBUTE, contrast);\n }, [contrast, mounted]);\n\n const setContrast = useCallback(\n (mode: ContrastMode) => {\n setContrastState(mode);\n try {\n window.localStorage.setItem(storageKey, mode);\n } catch {\n // Không lưu được thì lựa chọn chỉ sống trong phiên này — vẫn tốt hơn là ném lỗi.\n }\n },\n [storageKey]\n );\n\n const value = useMemo<ContrastContextValue>(\n () => ({ contrast, setContrast, mounted }),\n [contrast, setContrast, mounted]\n );\n\n return (\n <ContrastContext.Provider value={value}>\n <ContrastScript storageKey={storageKey} nonce={nonce} />\n {children}\n </ContrastContext.Provider>\n );\n}\n\n/**\n * Script chống nháy. `<ContrastProvider>` đã render sẵn nó nên **app thường không cần gọi tay** —\n * export ra chỉ cho trường hợp app tự dựng provider hoặc cần đặt nó ở một vị trí khác trong body.\n *\n * ⚠ ĐỪNG đặt vào `<head>`: React 19 báo lỗi console ở mọi lần render client (xem chú thích của\n * `ContrastProvider`). Gọi hai lần cũng vô hại — script idempotent — chỉ tốn một thẻ.\n */\nexport function ContrastScript({ storageKey = CONTRAST_STORAGE_KEY, nonce }: { storageKey?: string; nonce?: string }) {\n // Chuỗi tự gọi, không phụ thuộc React: chạy ngay lúc trình duyệt gặp thẻ <script>.\n const script = `(function(){try{var v=localStorage.getItem(${JSON.stringify(storageKey)});if(v===\"high\")document.documentElement.setAttribute(${JSON.stringify(CONTRAST_ATTRIBUTE)},\"high\")}catch(e){}})();`;\n return (\n <script\n suppressHydrationWarning\n /*\n * ⚠ `nonce` CHỈ phát ở phía MÁY CHỦ. Khuôn chép của `next-themes`, hai lý do thật:\n * 1. Trình duyệt CỐ Ý giấu giá trị `nonce` — `getAttribute(\"nonce\")` trả chuỗi RỖNG sau khi\n * trang tải xong, để một XSS đọc được DOM cũng không lấy được nonce. Render nonce thật ở\n * client thì React so với chuỗi rỗng đó và báo lệch hydration ở MỌI trang.\n * 2. Nonce chỉ có tác dụng ở lượt HTML đầu tiên, mà script này cũng chỉ chạy ở lượt đó.\n */\n nonce={typeof window === \"undefined\" ? nonce : \"\"}\n // biome-ignore lint/security/noDangerouslySetInnerHtml: chuỗi hằng do chính package sinh, không có dữ liệu người dùng.\n dangerouslySetInnerHTML={{ __html: script }}\n />\n );\n}\n\n/**\n * Đọc/đổi bảng màu. Ném lỗi nếu gọi ngoài `<ContrastProvider>` — im lặng trả mặc định sẽ khiến\n * nút gạt trông như hỏng mà không có gì chỉ ra nguyên nhân.\n */\nexport function useContrast(): ContrastContextValue {\n const ctx = useContext(ContrastContext);\n if (!ctx) throw new Error(\"useContrast() phải nằm trong <ContrastProvider>. Bọc nó ở root layout.\");\n return ctx;\n}\n"],"mappings":";AAsGI,SACE,KADF;AApGJ,SAAS,eAAe,aAAa,YAAY,WAAW,SAAS,gBAAgB;AACrF,SAAS,oBAAoB,sBAAyC,sBAAsB;AAU5F,MAAM,kBAAkB,cAA2C,IAAI;AA0ChE,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA,kBAAkB;AAAA,EAClB,aAAa;AAAA,EACb;AACF,GAA0B;AACxB,QAAM,CAAC,UAAU,gBAAgB,IAAI,SAAuB,eAAe;AAC3E,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAI5C,YAAU,MAAM;AACd,QAAI,SAAwB;AAC5B,QAAI;AACF,eAAS,OAAO,aAAa,QAAQ,UAAU;AAAA,IACjD,QAAQ;AAAA,IAER;AACA,QAAI,eAAe,MAAM,EAAG,kBAAiB,MAAM;AACnD,eAAW,IAAI;AAAA,EACjB,GAAG,CAAC,UAAU,CAAC;AAEf,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AACd,UAAM,OAAO,SAAS;AACtB,QAAI,aAAa,SAAU,MAAK,gBAAgB,kBAAkB;AAAA,QAC7D,MAAK,aAAa,oBAAoB,QAAQ;AAAA,EACrD,GAAG,CAAC,UAAU,OAAO,CAAC;AAEtB,QAAM,cAAc;AAAA,IAClB,CAAC,SAAuB;AACtB,uBAAiB,IAAI;AACrB,UAAI;AACF,eAAO,aAAa,QAAQ,YAAY,IAAI;AAAA,MAC9C,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AAEA,QAAM,QAAQ;AAAA,IACZ,OAAO,EAAE,UAAU,aAAa,QAAQ;AAAA,IACxC,CAAC,UAAU,aAAa,OAAO;AAAA,EACjC;AAEA,SACE,qBAAC,gBAAgB,UAAhB,EAAyB,OACxB;AAAA,wBAAC,kBAAe,YAAwB,OAAc;AAAA,IACrD;AAAA,KACH;AAEJ;AASO,SAAS,eAAe,EAAE,aAAa,sBAAsB,MAAM,GAA4C;AAEpH,QAAM,SAAS,8CAA8C,KAAK,UAAU,UAAU,CAAC,yDAAyD,KAAK,UAAU,kBAAkB,CAAC;AAClL,SACE;AAAA,IAAC;AAAA;AAAA,MACC,0BAAwB;AAAA,MAQxB,OAAO,OAAO,WAAW,cAAc,QAAQ;AAAA,MAE/C,yBAAyB,EAAE,QAAQ,OAAO;AAAA;AAAA,EAC5C;AAEJ;AAMO,SAAS,cAAoC;AAClD,QAAM,MAAM,WAAW,eAAe;AACtC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,+FAAwE;AAClG,SAAO;AACT;","names":[]}
@@ -8,13 +8,26 @@ interface DisplayAxisValue<T extends string> {
8
8
  /** `false` cho tới khi effect đầu tiên chạy — dùng để tránh lệch hydration ở control. */
9
9
  mounted: boolean;
10
10
  }
11
- declare const DensityProvider: ({ children, defaultValue, storageKey }: {
11
+ declare const DensityProvider: ({ children, defaultValue, storageKey, nonce }: {
12
12
  children: ReactNode;
13
13
  defaultValue?: "compact" | "comfortable" | undefined;
14
14
  storageKey?: string;
15
+ /**
16
+ * Nonce của CSP, cho script chống nháy mà provider này TỰ render.
17
+ *
18
+ * Bỏ trống nếu app không ép `script-src` — `undefined` nghĩa là thẻ `<script>` không mang thuộc
19
+ * tính `nonce`, đúng hành vi trước đây.
20
+ *
21
+ * Đường lấy giá trị (Next 16): `proxy.ts` sinh nonce và đặt nó lên header REQUEST cùng chính
22
+ * sách CSP; root layout đọc bằng `(await headers()).get("x-nonce")`. Next TỰ gắn nonce cho mọi
23
+ * script của CHÍNH NÓ, nhưng script do gói này render thì React 19 **không** gắn hộ — nên nó
24
+ * bắt buộc phải đi qua prop.
25
+ */
26
+ nonce?: string;
15
27
  }) => React.JSX.Element;
16
- declare const DensityScript: ({ storageKey }: {
28
+ declare const DensityScript: ({ storageKey, nonce }: {
17
29
  storageKey?: string;
30
+ nonce?: string;
18
31
  }) => React.JSX.Element;
19
32
  /** Đọc/đổi mật độ bố cục. */
20
33
  declare function useDensity(): {
@@ -22,13 +35,26 @@ declare function useDensity(): {
22
35
  setDensity: (m: DensityMode) => void;
23
36
  mounted: boolean;
24
37
  };
25
- declare const FontSizeProvider: ({ children, defaultValue, storageKey }: {
38
+ declare const FontSizeProvider: ({ children, defaultValue, storageKey, nonce }: {
26
39
  children: ReactNode;
27
40
  defaultValue?: "sm" | "md" | "lg" | undefined;
28
41
  storageKey?: string;
42
+ /**
43
+ * Nonce của CSP, cho script chống nháy mà provider này TỰ render.
44
+ *
45
+ * Bỏ trống nếu app không ép `script-src` — `undefined` nghĩa là thẻ `<script>` không mang thuộc
46
+ * tính `nonce`, đúng hành vi trước đây.
47
+ *
48
+ * Đường lấy giá trị (Next 16): `proxy.ts` sinh nonce và đặt nó lên header REQUEST cùng chính
49
+ * sách CSP; root layout đọc bằng `(await headers()).get("x-nonce")`. Next TỰ gắn nonce cho mọi
50
+ * script của CHÍNH NÓ, nhưng script do gói này render thì React 19 **không** gắn hộ — nên nó
51
+ * bắt buộc phải đi qua prop.
52
+ */
53
+ nonce?: string;
29
54
  }) => React.JSX.Element;
30
- declare const FontSizeScript: ({ storageKey }: {
55
+ declare const FontSizeScript: ({ storageKey, nonce }: {
31
56
  storageKey?: string;
57
+ nonce?: string;
32
58
  }) => React.JSX.Element;
33
59
  /** Đọc/đổi cỡ chữ. */
34
60
  declare function useFontSize(): {
@@ -13,14 +13,22 @@ import {
13
13
  } from "./display-axis-constants.js";
14
14
  function createDisplayAxis(config) {
15
15
  const Context = createContext(null);
16
- function AxisScript({ storageKey = config.storageKey }) {
16
+ function AxisScript({ storageKey = config.storageKey, nonce }) {
17
17
  const script = `(function(){try{var v=localStorage.getItem(${JSON.stringify(storageKey)});if(v&&v!==${JSON.stringify(config.defaultValue)})document.documentElement.setAttribute(${JSON.stringify(config.attribute)},v)}catch(e){}})();`;
18
- return /* @__PURE__ */ jsx("script", { suppressHydrationWarning: true, dangerouslySetInnerHTML: { __html: script } });
18
+ return /* @__PURE__ */ jsx(
19
+ "script",
20
+ {
21
+ suppressHydrationWarning: true,
22
+ nonce: typeof window === "undefined" ? nonce : "",
23
+ dangerouslySetInnerHTML: { __html: script }
24
+ }
25
+ );
19
26
  }
20
27
  function Provider({
21
28
  children,
22
29
  defaultValue = config.defaultValue,
23
- storageKey = config.storageKey
30
+ storageKey = config.storageKey,
31
+ nonce
24
32
  }) {
25
33
  const [value, setValueState] = useState(defaultValue);
26
34
  const [mounted, setMounted] = useState(false);
@@ -51,7 +59,7 @@ function createDisplayAxis(config) {
51
59
  );
52
60
  const ctx = useMemo(() => ({ value, setValue, mounted }), [value, setValue, mounted]);
53
61
  return /* @__PURE__ */ jsxs(Context.Provider, { value: ctx, children: [
54
- /* @__PURE__ */ jsx(AxisScript, { storageKey }),
62
+ /* @__PURE__ */ jsx(AxisScript, { storageKey, nonce }),
55
63
  children
56
64
  ] });
57
65
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/shell/display-axis-provider.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport {\n DEFAULT_DENSITY,\n DEFAULT_FONT_SIZE,\n DENSITY_ATTRIBUTE,\n DENSITY_STORAGE_KEY,\n type DensityMode,\n FONT_SIZE_ATTRIBUTE,\n FONT_SIZE_STORAGE_KEY,\n type FontSizeMode,\n isDensityMode,\n isFontSizeMode\n} from \"./display-axis-constants.js\";\n\n/**\n * Xưởng dựng TRỤC HIỂN THỊ — dùng chung cho mật độ và cỡ chữ.\n *\n * `ContrastProvider` viết tay ~60 dòng cho trục thứ hai. Trục thứ ba và thứ tư sẽ là hai bản chép\n * gần như y hệt, nên phần chung tách ra đây: cùng cơ chế đọc localStorage sau khi mount, cùng cờ\n * `mounted` chống lệch hydration, cùng script chống nháy trong `<body>`, cùng quy ước GỠ thuộc\n * tính ở giá trị mặc định.\n *\n * ⚠ `ContrastProvider` CỐ Ý chưa chuyển sang xưởng này. Nó là API đã phát hành và có hành vi\n * riêng đã được ghi chép; chuyển nó là một thay đổi có rủi ro mà lượt này không cần tới. Nếu sau\n * này chuyển thì chuyển một mình nó, có bản so đầu ra trước/sau.\n *\n * ─ VÌ SAO KHÔNG DÙNG `next-themes` ─\n * Nó giữ MỘT context singleton cho cả cây. Lồng thêm provider với `attribute`/`storageKey` khác\n * thì provider trong cùng thắng và `useTheme()` của app đọc nhầm trục — không lỗi biên dịch, chỉ\n * là nút đổi sáng/tối lặng lẽ ngừng ăn. `contrast-provider.tsx` đã ghi lại đúng cạm bẫy này.\n */\ninterface AxisConfig<T extends string> {\n attribute: string;\n storageKey: string;\n defaultValue: T;\n isValue: (v: unknown) => v is T;\n hookName: string;\n providerName: string;\n}\n\nexport interface DisplayAxisValue<T extends string> {\n value: T;\n setValue: (next: T) => void;\n /** `false` cho tới khi effect đầu tiên chạy — dùng để tránh lệch hydration ở control. */\n mounted: boolean;\n}\n\nfunction createDisplayAxis<T extends string>(config: AxisConfig<T>) {\n const Context = createContext<DisplayAxisValue<T> | null>(null);\n\n /**\n * Script chống nháy. Phải chạy TRƯỚC lần sơn đầu tiên, nếu không người đã chọn `lg` sẽ thấy một\n * khung hình cỡ mặc định rồi mới nhảy. Effect của React chạy SAU khi sơn nên không dùng được.\n *\n * ⚠ Nằm trong `<body>`, KHÔNG phải `<head>`: React 19 kêu \"Encountered a script tag while\n * rendering React component\" ở mọi lần render client nếu đặt vào head. Trong body thì tương\n * đương về chống nháy vì parser chạy script ngay khi gặp, mà nó đứng trước toàn bộ markup.\n */\n function AxisScript({ storageKey = config.storageKey }: { storageKey?: string }) {\n const script = `(function(){try{var v=localStorage.getItem(${JSON.stringify(storageKey)});if(v&&v!==${JSON.stringify(config.defaultValue)})document.documentElement.setAttribute(${JSON.stringify(config.attribute)},v)}catch(e){}})();`;\n // biome-ignore lint/security/noDangerouslySetInnerHtml: chuỗi hằng do chính package sinh, không có dữ liệu người dùng.\n return <script suppressHydrationWarning dangerouslySetInnerHTML={{ __html: script }} />;\n }\n\n function Provider({\n children,\n defaultValue = config.defaultValue,\n storageKey = config.storageKey\n }: {\n children: ReactNode;\n defaultValue?: T;\n storageKey?: string;\n }) {\n const [value, setValueState] = useState<T>(defaultValue);\n const [mounted, setMounted] = useState(false);\n\n // Đọc lựa chọn đã lưu SAU khi mount. Trước đó state phải bằng đúng giá trị server đã render,\n // nếu không React báo lệch hydration — DOM thật đã được AxisScript đặt đúng từ trước rồi.\n useEffect(() => {\n let stored: string | null = null;\n try {\n stored = window.localStorage.getItem(storageKey);\n } catch {\n // Safari chế độ riêng tư / localStorage bị chặn: rơi về mặc định, không phải lỗi.\n }\n if (config.isValue(stored)) setValueState(stored);\n setMounted(true);\n }, [storageKey]);\n\n useEffect(() => {\n if (!mounted) return;\n const root = document.documentElement;\n // Mặc định thì GỠ thuộc tính, không ghi giá trị — xem ghi chú cuối display-axis-constants.ts.\n if (value === config.defaultValue) root.removeAttribute(config.attribute);\n else root.setAttribute(config.attribute, value);\n }, [value, mounted]);\n\n const setValue = useCallback(\n (next: T) => {\n setValueState(next);\n try {\n window.localStorage.setItem(storageKey, next);\n } catch {\n // Không lưu được thì lựa chọn chỉ sống trong phiên này — vẫn tốt hơn là ném lỗi.\n }\n },\n [storageKey]\n );\n\n const ctx = useMemo<DisplayAxisValue<T>>(() => ({ value, setValue, mounted }), [value, setValue, mounted]);\n\n return (\n <Context.Provider value={ctx}>\n <AxisScript storageKey={storageKey} />\n {children}\n </Context.Provider>\n );\n }\n\n function useAxis(): DisplayAxisValue<T> {\n const ctx = useContext(Context);\n // Ném lỗi thay vì trả mặc định: im lặng thì control trông như hỏng mà không chỉ ra nguyên nhân.\n if (!ctx) throw new Error(`${config.hookName}() phải nằm trong <${config.providerName}>. Bọc nó ở root layout.`);\n return ctx;\n }\n\n return { Provider, useAxis, AxisScript };\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Trục 3 — MẬT ĐỘ\n ──────────────────────────────────────────────────────────────────────────── */\n\nconst densityAxis = createDisplayAxis<DensityMode>({\n attribute: DENSITY_ATTRIBUTE,\n storageKey: DENSITY_STORAGE_KEY,\n defaultValue: DEFAULT_DENSITY,\n isValue: isDensityMode,\n hookName: \"useDensity\",\n providerName: \"DensityProvider\"\n});\n\nexport const DensityProvider = densityAxis.Provider;\nexport const DensityScript = densityAxis.AxisScript;\n\n/** Đọc/đổi mật độ bố cục. */\nexport function useDensity(): { density: DensityMode; setDensity: (m: DensityMode) => void; mounted: boolean } {\n const { value, setValue, mounted } = densityAxis.useAxis();\n return { density: value, setDensity: setValue, mounted };\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Trục 4 — CỠ CHỮ\n ──────────────────────────────────────────────────────────────────────────── */\n\nconst fontSizeAxis = createDisplayAxis<FontSizeMode>({\n attribute: FONT_SIZE_ATTRIBUTE,\n storageKey: FONT_SIZE_STORAGE_KEY,\n defaultValue: DEFAULT_FONT_SIZE,\n isValue: isFontSizeMode,\n hookName: \"useFontSize\",\n providerName: \"FontSizeProvider\"\n});\n\nexport const FontSizeProvider = fontSizeAxis.Provider;\nexport const FontSizeScript = fontSizeAxis.AxisScript;\n\n/** Đọc/đổi cỡ chữ. */\nexport function useFontSize(): { fontSize: FontSizeMode; setFontSize: (m: FontSizeMode) => void; mounted: boolean } {\n const { value, setValue, mounted } = fontSizeAxis.useAxis();\n return { fontSize: value, setFontSize: setValue, mounted };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/shell/display-axis-provider.tsx"],"sourcesContent":["\"use client\";\n\nimport { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from \"react\";\nimport {\n DEFAULT_DENSITY,\n DEFAULT_FONT_SIZE,\n DENSITY_ATTRIBUTE,\n DENSITY_STORAGE_KEY,\n type DensityMode,\n FONT_SIZE_ATTRIBUTE,\n FONT_SIZE_STORAGE_KEY,\n type FontSizeMode,\n isDensityMode,\n isFontSizeMode\n} from \"./display-axis-constants.js\";\n\n/**\n * Xưởng dựng TRỤC HIỂN THỊ — dùng chung cho mật độ và cỡ chữ.\n *\n * `ContrastProvider` viết tay ~60 dòng cho trục thứ hai. Trục thứ ba và thứ tư sẽ là hai bản chép\n * gần như y hệt, nên phần chung tách ra đây: cùng cơ chế đọc localStorage sau khi mount, cùng cờ\n * `mounted` chống lệch hydration, cùng script chống nháy trong `<body>`, cùng quy ước GỠ thuộc\n * tính ở giá trị mặc định.\n *\n * ⚠ `ContrastProvider` CỐ Ý chưa chuyển sang xưởng này. Nó là API đã phát hành và có hành vi\n * riêng đã được ghi chép; chuyển nó là một thay đổi có rủi ro mà lượt này không cần tới. Nếu sau\n * này chuyển thì chuyển một mình nó, có bản so đầu ra trước/sau.\n *\n * ─ VÌ SAO KHÔNG DÙNG `next-themes` ─\n * Nó giữ MỘT context singleton cho cả cây. Lồng thêm provider với `attribute`/`storageKey` khác\n * thì provider trong cùng thắng và `useTheme()` của app đọc nhầm trục — không lỗi biên dịch, chỉ\n * là nút đổi sáng/tối lặng lẽ ngừng ăn. `contrast-provider.tsx` đã ghi lại đúng cạm bẫy này.\n */\ninterface AxisConfig<T extends string> {\n attribute: string;\n storageKey: string;\n defaultValue: T;\n isValue: (v: unknown) => v is T;\n hookName: string;\n providerName: string;\n}\n\nexport interface DisplayAxisValue<T extends string> {\n value: T;\n setValue: (next: T) => void;\n /** `false` cho tới khi effect đầu tiên chạy — dùng để tránh lệch hydration ở control. */\n mounted: boolean;\n}\n\nfunction createDisplayAxis<T extends string>(config: AxisConfig<T>) {\n const Context = createContext<DisplayAxisValue<T> | null>(null);\n\n /**\n * Script chống nháy. Phải chạy TRƯỚC lần sơn đầu tiên, nếu không người đã chọn `lg` sẽ thấy một\n * khung hình cỡ mặc định rồi mới nhảy. Effect của React chạy SAU khi sơn nên không dùng được.\n *\n * ⚠ Nằm trong `<body>`, KHÔNG phải `<head>`: React 19 kêu \"Encountered a script tag while\n * rendering React component\" ở mọi lần render client nếu đặt vào head. Trong body thì tương\n * đương về chống nháy vì parser chạy script ngay khi gặp, mà nó đứng trước toàn bộ markup.\n */\n function AxisScript({ storageKey = config.storageKey, nonce }: { storageKey?: string; nonce?: string }) {\n const script = `(function(){try{var v=localStorage.getItem(${JSON.stringify(storageKey)});if(v&&v!==${JSON.stringify(config.defaultValue)})document.documentElement.setAttribute(${JSON.stringify(config.attribute)},v)}catch(e){}})();`;\n return (\n <script\n suppressHydrationWarning\n /*\n * ⚠ `nonce` CHỈ phát ở phía MÁY CHỦ — `typeof window === \"undefined\" ? nonce : \"\"`.\n *\n * Khuôn này chép nguyên của `next-themes`, và nó có hai lý do thật chứ không phải thẩm mỹ:\n *\n * 1. **Trình duyệt CỐ Ý giấu giá trị `nonce`**: `el.getAttribute(\"nonce\")` trả về chuỗi\n * RỖNG sau khi trang tải xong. Đó là một chốt của chính CSP — để một XSS đọc được DOM\n * cũng không lấy được nonce rồi tự tiêm script hợp lệ. Render nonce thật ở client thì\n * React so nó với chuỗi rỗng của DOM và báo lệch hydration ở MỌI trang.\n * 2. Nonce chỉ có tác dụng ở lượt phản hồi HTML đầu tiên, mà script này cũng chỉ chạy ở\n * đúng lượt đó — sau khi hydrate xong nó không còn việc gì.\n */\n nonce={typeof window === \"undefined\" ? nonce : \"\"}\n // biome-ignore lint/security/noDangerouslySetInnerHtml: chuỗi hằng do chính package sinh, không có dữ liệu người dùng.\n dangerouslySetInnerHTML={{ __html: script }}\n />\n );\n }\n\n function Provider({\n children,\n defaultValue = config.defaultValue,\n storageKey = config.storageKey,\n nonce\n }: {\n children: ReactNode;\n defaultValue?: T;\n storageKey?: string;\n /**\n * Nonce của CSP, cho script chống nháy mà provider này TỰ render.\n *\n * Bỏ trống nếu app không ép `script-src` — `undefined` nghĩa là thẻ `<script>` không mang thuộc\n * tính `nonce`, đúng hành vi trước đây.\n *\n * Đường lấy giá trị (Next 16): `proxy.ts` sinh nonce và đặt nó lên header REQUEST cùng chính\n * sách CSP; root layout đọc bằng `(await headers()).get(\"x-nonce\")`. Next TỰ gắn nonce cho mọi\n * script của CHÍNH NÓ, nhưng script do gói này render thì React 19 **không** gắn hộ — nên nó\n * bắt buộc phải đi qua prop.\n */\n nonce?: string;\n }) {\n const [value, setValueState] = useState<T>(defaultValue);\n const [mounted, setMounted] = useState(false);\n\n // Đọc lựa chọn đã lưu SAU khi mount. Trước đó state phải bằng đúng giá trị server đã render,\n // nếu không React báo lệch hydration — DOM thật đã được AxisScript đặt đúng từ trước rồi.\n useEffect(() => {\n let stored: string | null = null;\n try {\n stored = window.localStorage.getItem(storageKey);\n } catch {\n // Safari chế độ riêng tư / localStorage bị chặn: rơi về mặc định, không phải lỗi.\n }\n if (config.isValue(stored)) setValueState(stored);\n setMounted(true);\n }, [storageKey]);\n\n useEffect(() => {\n if (!mounted) return;\n const root = document.documentElement;\n // Mặc định thì GỠ thuộc tính, không ghi giá trị — xem ghi chú cuối display-axis-constants.ts.\n if (value === config.defaultValue) root.removeAttribute(config.attribute);\n else root.setAttribute(config.attribute, value);\n }, [value, mounted]);\n\n const setValue = useCallback(\n (next: T) => {\n setValueState(next);\n try {\n window.localStorage.setItem(storageKey, next);\n } catch {\n // Không lưu được thì lựa chọn chỉ sống trong phiên này — vẫn tốt hơn là ném lỗi.\n }\n },\n [storageKey]\n );\n\n const ctx = useMemo<DisplayAxisValue<T>>(() => ({ value, setValue, mounted }), [value, setValue, mounted]);\n\n return (\n <Context.Provider value={ctx}>\n <AxisScript storageKey={storageKey} nonce={nonce} />\n {children}\n </Context.Provider>\n );\n }\n\n function useAxis(): DisplayAxisValue<T> {\n const ctx = useContext(Context);\n // Ném lỗi thay vì trả mặc định: im lặng thì control trông như hỏng mà không chỉ ra nguyên nhân.\n if (!ctx) throw new Error(`${config.hookName}() phải nằm trong <${config.providerName}>. Bọc nó ở root layout.`);\n return ctx;\n }\n\n return { Provider, useAxis, AxisScript };\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Trục 3 — MẬT ĐỘ\n ──────────────────────────────────────────────────────────────────────────── */\n\nconst densityAxis = createDisplayAxis<DensityMode>({\n attribute: DENSITY_ATTRIBUTE,\n storageKey: DENSITY_STORAGE_KEY,\n defaultValue: DEFAULT_DENSITY,\n isValue: isDensityMode,\n hookName: \"useDensity\",\n providerName: \"DensityProvider\"\n});\n\nexport const DensityProvider = densityAxis.Provider;\nexport const DensityScript = densityAxis.AxisScript;\n\n/** Đọc/đổi mật độ bố cục. */\nexport function useDensity(): { density: DensityMode; setDensity: (m: DensityMode) => void; mounted: boolean } {\n const { value, setValue, mounted } = densityAxis.useAxis();\n return { density: value, setDensity: setValue, mounted };\n}\n\n/* ────────────────────────────────────────────────────────────────────────────\n Trục 4 — CỠ CHỮ\n ──────────────────────────────────────────────────────────────────────────── */\n\nconst fontSizeAxis = createDisplayAxis<FontSizeMode>({\n attribute: FONT_SIZE_ATTRIBUTE,\n storageKey: FONT_SIZE_STORAGE_KEY,\n defaultValue: DEFAULT_FONT_SIZE,\n isValue: isFontSizeMode,\n hookName: \"useFontSize\",\n providerName: \"FontSizeProvider\"\n});\n\nexport const FontSizeProvider = fontSizeAxis.Provider;\nexport const FontSizeScript = fontSizeAxis.AxisScript;\n\n/** Đọc/đổi cỡ chữ. */\nexport function useFontSize(): { fontSize: FontSizeMode; setFontSize: (m: FontSizeMode) => void; mounted: boolean } {\n const { value, setValue, mounted } = fontSizeAxis.useAxis();\n return { fontSize: value, setFontSize: setValue, mounted };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comitor/ui",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Comitor design system — Shadcn/UI tùy biến theo brand Comitor + component dùng chung cho hệ sinh thái SaaS (Tasks, Chat, CRM, HR…)",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -53,19 +53,6 @@
53
53
  "./styles.css": "./styles.css",
54
54
  "./package.json": "./package.json"
55
55
  },
56
- "scripts": {
57
- "build": "tsup",
58
- "typecheck": "tsc --noEmit",
59
- "lint": "biome check .",
60
- "lint:fix": "biome check --write .",
61
- "lint:a11y": "node scripts/check-contrast.mjs",
62
- "test:a11y-gate": "node scripts/a11y/self-test.mjs",
63
- "test:nav-active": "node scripts/check-nav-active.mjs",
64
- "verify:directives": "node scripts/verify-directives.mjs",
65
- "smoke:entries": "node scripts/smoke-entries.mjs",
66
- "check:exports": "publint --strict && attw --pack . --profile esm-only --exclude-entrypoints styles.css",
67
- "prepublishOnly": "pnpm run typecheck && pnpm run build && pnpm run verify:directives && pnpm run smoke:entries && pnpm run check:exports && pnpm run test:nav-active && pnpm run test:a11y-gate && pnpm run lint:a11y"
68
- },
69
56
  "peerDependencies": {
70
57
  "lucide-react": ">=0.540.0",
71
58
  "next": ">=15",
@@ -147,5 +134,17 @@
147
134
  "tailwindcss": "^4.2.0",
148
135
  "tsup": "^8.3.5",
149
136
  "typescript": "^5.7.0"
137
+ },
138
+ "scripts": {
139
+ "build": "tsup",
140
+ "typecheck": "tsc --noEmit",
141
+ "lint": "biome check .",
142
+ "lint:fix": "biome check --write .",
143
+ "lint:a11y": "node scripts/check-contrast.mjs",
144
+ "test:a11y-gate": "node scripts/a11y/self-test.mjs",
145
+ "test:nav-active": "node scripts/check-nav-active.mjs",
146
+ "verify:directives": "node scripts/verify-directives.mjs",
147
+ "smoke:entries": "node scripts/smoke-entries.mjs",
148
+ "check:exports": "publint --strict && attw --pack . --profile esm-only --exclude-entrypoints styles.css"
150
149
  }
151
- }
150
+ }