@ship-it-ui/next 0.0.7 → 0.0.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/theme-cookie.ts","../src/ThemeBootstrap.tsx","../src/ThemeToggle.tsx"],"sourcesContent":["/**\n * @ship-it-ui/next — Next.js (App Router) helpers for the Ship-It design\n * system. SSR-safe theme persistence with FOUC prevention, cookie helpers,\n * and a token-styled theme toggle that reuses `useTheme` from\n * `@ship-it-ui/ui`.\n */\n\nexport { ThemeBootstrap, buildBootstrapScript } from './ThemeBootstrap';\nexport { ThemeToggle, type ThemeToggleProps } from './ThemeToggle';\nexport {\n getThemeFromCookies,\n parseThemeCookie,\n readThemeCookie,\n writeThemeCookie,\n THEME_COOKIE_NAME,\n THEME_COOKIE_MAX_AGE,\n type CookieGetter,\n} from './theme-cookie';\n","/**\n * Theme cookie helpers shared between the server-side `getThemeFromCookies`\n * and the client-side `ThemeBootstrap` / `ThemeToggle`. The cookie value is\n * the literal string `'dark'` or `'light'`; anything else falls back to\n * undefined and the consumer's default (typically dark) applies.\n */\n\nimport type { Theme } from '@ship-it-ui/ui';\n\nexport const THEME_COOKIE_NAME = 'ship-it-theme';\n\n/** One year. The cookie is non-sensitive, so a long lifetime is fine. */\nexport const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;\n\n/** Parse a raw cookie string value into a `Theme` or `undefined`. */\nexport function parseThemeCookie(value: string | null | undefined): Theme | undefined {\n if (value === 'dark' || value === 'light') return value;\n return undefined;\n}\n\n/**\n * Server helper for the App Router `cookies()` API. Pass `cookies()` (or any\n * object with `.get(name)` that returns `{ value: string }`) and receive the\n * stored theme.\n *\n * ```ts\n * import { cookies } from 'next/headers';\n * const theme = getThemeFromCookies(await cookies());\n * ```\n */\nexport interface CookieGetter {\n get(name: string): { value: string } | undefined;\n}\n\nexport function getThemeFromCookies(cookieStore: CookieGetter): Theme | undefined {\n return parseThemeCookie(cookieStore.get(THEME_COOKIE_NAME)?.value);\n}\n\n/**\n * Client-side cookie writer. Sets a path-`/` cookie with a year-long TTL and\n * `SameSite=Lax`, which is safe for theme preferences (non-sensitive, not\n * cross-site). No-op on the server.\n */\nexport function writeThemeCookie(theme: Theme): void {\n if (typeof document === 'undefined') return;\n document.cookie = `${THEME_COOKIE_NAME}=${theme}; path=/; max-age=${THEME_COOKIE_MAX_AGE}; samesite=lax`;\n}\n\n/** Read the theme cookie from `document.cookie`. No-op on the server. */\nexport function readThemeCookie(): Theme | undefined {\n if (typeof document === 'undefined') return undefined;\n const match = document.cookie\n .split(';')\n .map((part) => part.trim())\n .find((part) => part.startsWith(`${THEME_COOKIE_NAME}=`));\n if (!match) return undefined;\n return parseThemeCookie(match.slice(THEME_COOKIE_NAME.length + 1));\n}\n","import { THEME_COOKIE_NAME } from './theme-cookie';\n\n/**\n * ThemeBootstrap — render inside the `<head>` of your App Router root layout.\n * Injects a synchronous inline script that reads the theme cookie and sets\n * `<html data-theme>` *before* the first paint, which kills the dark→light\n * flash of unstyled content for users on the light theme.\n *\n * Pair with `getThemeFromCookies(cookies())` to seed the server-rendered\n * `data-theme` on `<html>` for the very first frame.\n *\n * ```tsx\n * import { cookies } from 'next/headers';\n * import { ThemeBootstrap, getThemeFromCookies } from '@ship-it-ui/next';\n *\n * export default function RootLayout({ children }) {\n * const theme = getThemeFromCookies(cookies());\n * return (\n * <html lang=\"en\" data-theme={theme}>\n * <head><ThemeBootstrap /></head>\n * <body>{children}</body>\n * </html>\n * );\n * }\n * ```\n */\nexport function ThemeBootstrap(): JSX.Element {\n return (\n <script\n // The script must be synchronous and inline — there's no place else to\n // intercept the document before paint. Build the cookie name into the\n // script so consumers don't have to mirror it.\n dangerouslySetInnerHTML={{ __html: buildBootstrapScript() }}\n />\n );\n}\n\nThemeBootstrap.displayName = 'ThemeBootstrap';\n\n/**\n * Returns the inline-script body that `ThemeBootstrap` injects. Exported so\n * tests (and consumers with a custom layout) can inspect or reuse the source.\n */\nexport function buildBootstrapScript(): string {\n return `(function(){try{var m=document.cookie.match(/(?:^|; )${THEME_COOKIE_NAME}=(dark|light)/);var t=m&&m[1];if(t==='light'){document.documentElement.setAttribute('data-theme','light');}else if(t==='dark'){document.documentElement.removeAttribute('data-theme');}}catch(_){}})();`;\n}\n","'use client';\n\nimport { Switch, useTheme, type Theme } from '@ship-it-ui/ui';\nimport { useCallback, type ReactNode } from 'react';\n\nimport { writeThemeCookie } from './theme-cookie';\n\n/**\n * ThemeToggle — token-styled `Switch` bound to the active theme. Reuses the\n * shared `useTheme` hook from `@ship-it-ui/ui` for in-page state and writes\n * a year-long cookie so the next request's `ThemeBootstrap` can render the\n * correct theme synchronously.\n *\n * Pass `label` for a visible labelled control, or supply your own wrapper if\n * you want a chip / menu-item layout.\n */\n\nexport interface ThemeToggleProps {\n /** Visible label rendered next to the switch. */\n label?: ReactNode;\n /** Override the accessible name. Falls back to `label` when it is a string. */\n 'aria-label'?: string;\n /** Fires after the theme has been persisted. Useful for analytics. */\n onThemeChange?: (next: Theme) => void;\n}\n\nexport function ThemeToggle({\n label,\n 'aria-label': ariaLabel,\n onThemeChange,\n}: ThemeToggleProps): JSX.Element {\n const { theme, setTheme } = useTheme();\n\n const handleChange = useCallback(\n (next: boolean) => {\n const target: Theme = next ? 'light' : 'dark';\n setTheme(target);\n writeThemeCookie(target);\n onThemeChange?.(target);\n },\n [setTheme, onThemeChange],\n );\n\n const accessibleName = ariaLabel ?? (typeof label === 'string' ? label : 'Toggle theme');\n\n return (\n <span className=\"inline-flex items-center gap-2\">\n <Switch\n checked={theme === 'light'}\n onCheckedChange={handleChange}\n aria-label={accessibleName}\n />\n {label && <span className=\"text-text-muted text-[12px]\">{label}</span>}\n </span>\n );\n}\n\nThemeToggle.displayName = 'ThemeToggle';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACSO,IAAM,oBAAoB;AAG1B,IAAM,uBAAuB,KAAK,KAAK,KAAK;AAG5C,SAAS,iBAAiB,OAAqD;AACpF,MAAI,UAAU,UAAU,UAAU,QAAS,QAAO;AAClD,SAAO;AACT;AAgBO,SAAS,oBAAoB,aAA8C;AAChF,SAAO,iBAAiB,YAAY,IAAI,iBAAiB,GAAG,KAAK;AACnE;AAOO,SAAS,iBAAiB,OAAoB;AACnD,MAAI,OAAO,aAAa,YAAa;AACrC,WAAS,SAAS,GAAG,iBAAiB,IAAI,KAAK,qBAAqB,oBAAoB;AAC1F;AAGO,SAAS,kBAAqC;AACnD,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,QAAM,QAAQ,SAAS,OACpB,MAAM,GAAG,EACT,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,EACzB,KAAK,CAAC,SAAS,KAAK,WAAW,GAAG,iBAAiB,GAAG,CAAC;AAC1D,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,iBAAiB,MAAM,MAAM,kBAAkB,SAAS,CAAC,CAAC;AACnE;;;AC7BI;AAFG,SAAS,iBAA8B;AAC5C,SACE;AAAA,IAAC;AAAA;AAAA,MAIC,yBAAyB,EAAE,QAAQ,qBAAqB,EAAE;AAAA;AAAA,EAC5D;AAEJ;AAEA,eAAe,cAAc;AAMtB,SAAS,uBAA+B;AAC7C,SAAO,wDAAwD,iBAAiB;AAClF;;;AC3CA,gBAA6C;AAC7C,mBAA4C;AA2CxC,IAAAA,sBAAA;AApBG,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,cAAc;AAAA,EACd;AACF,GAAkC;AAChC,QAAM,EAAE,OAAO,SAAS,QAAI,oBAAS;AAErC,QAAM,mBAAe;AAAA,IACnB,CAAC,SAAkB;AACjB,YAAM,SAAgB,OAAO,UAAU;AACvC,eAAS,MAAM;AACf,uBAAiB,MAAM;AACvB,sBAAgB,MAAM;AAAA,IACxB;AAAA,IACA,CAAC,UAAU,aAAa;AAAA,EAC1B;AAEA,QAAM,iBAAiB,cAAc,OAAO,UAAU,WAAW,QAAQ;AAEzE,SACE,8CAAC,UAAK,WAAU,kCACd;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,SAAS,UAAU;AAAA,QACnB,iBAAiB;AAAA,QACjB,cAAY;AAAA;AAAA,IACd;AAAA,IACC,SAAS,6CAAC,UAAK,WAAU,+BAA+B,iBAAM;AAAA,KACjE;AAEJ;AAEA,YAAY,cAAc;","names":["import_jsx_runtime"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/theme-cookie.ts","../src/ThemeBootstrap.tsx","../src/ThemeToggle.tsx"],"sourcesContent":["/**\n * @ship-it-ui/next — Next.js (App Router) helpers for the Ship-It design\n * system. SSR-safe theme persistence with FOUC prevention, cookie helpers,\n * and a token-styled theme toggle that reuses `useTheme` from\n * `@ship-it-ui/ui`.\n */\n\nexport { ThemeBootstrap, buildBootstrapScript } from './ThemeBootstrap';\nexport { ThemeToggle, type ThemeToggleProps } from './ThemeToggle';\nexport {\n getThemeFromCookies,\n parseThemeCookie,\n readThemeCookie,\n writeThemeCookie,\n THEME_COOKIE_NAME,\n THEME_COOKIE_MAX_AGE,\n type CookieGetter,\n} from './theme-cookie';\n","/**\n * Theme cookie helpers shared between the server-side `getThemeFromCookies`\n * and the client-side `ThemeBootstrap` / `ThemeToggle`. The cookie value is\n * the literal string `'dark'` or `'light'`; anything else falls back to\n * undefined and the consumer's default (typically dark) applies.\n */\n\nimport type { Theme } from '@ship-it-ui/ui';\n\nexport const THEME_COOKIE_NAME = 'ship-it-theme';\n\n/** One year. The cookie is non-sensitive, so a long lifetime is fine. */\nexport const THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;\n\n/** Parse a raw cookie string value into a `Theme` or `undefined`. */\nexport function parseThemeCookie(value: string | null | undefined): Theme | undefined {\n if (value === 'dark' || value === 'light') return value;\n return undefined;\n}\n\n/**\n * Server helper for the App Router `cookies()` API. Pass `cookies()` (or any\n * object with `.get(name)` that returns `{ value: string }`) and receive the\n * stored theme.\n *\n * ```ts\n * import { cookies } from 'next/headers';\n * const theme = getThemeFromCookies(await cookies());\n * ```\n */\nexport interface CookieGetter {\n get(name: string): { value: string } | undefined;\n}\n\nexport function getThemeFromCookies(cookieStore: CookieGetter): Theme | undefined {\n return parseThemeCookie(cookieStore.get(THEME_COOKIE_NAME)?.value);\n}\n\n/**\n * Client-side cookie writer. Sets a path-`/` cookie with a year-long TTL and\n * `SameSite=Lax`, which is safe for theme preferences (non-sensitive, not\n * cross-site). No-op on the server.\n */\nexport function writeThemeCookie(theme: Theme): void {\n if (typeof document === 'undefined') return;\n document.cookie = `${THEME_COOKIE_NAME}=${theme}; path=/; max-age=${THEME_COOKIE_MAX_AGE}; samesite=lax`;\n}\n\n/** Read the theme cookie from `document.cookie`. No-op on the server. */\nexport function readThemeCookie(): Theme | undefined {\n if (typeof document === 'undefined') return undefined;\n const match = document.cookie\n .split(';')\n .map((part) => part.trim())\n .find((part) => part.startsWith(`${THEME_COOKIE_NAME}=`));\n if (!match) return undefined;\n return parseThemeCookie(match.slice(THEME_COOKIE_NAME.length + 1));\n}\n","import type { JSX } from 'react';\n\nimport { THEME_COOKIE_NAME } from './theme-cookie';\n\n/**\n * ThemeBootstrap — render inside the `<head>` of your App Router root layout.\n * Injects a synchronous inline script that reads the theme cookie and sets\n * `<html data-theme>` *before* the first paint, which kills the dark→light\n * flash of unstyled content for users on the light theme.\n *\n * Pair with `getThemeFromCookies(cookies())` to seed the server-rendered\n * `data-theme` on `<html>` for the very first frame.\n *\n * ```tsx\n * import { cookies } from 'next/headers';\n * import { ThemeBootstrap, getThemeFromCookies } from '@ship-it-ui/next';\n *\n * export default function RootLayout({ children }) {\n * const theme = getThemeFromCookies(cookies());\n * return (\n * <html lang=\"en\" data-theme={theme}>\n * <head><ThemeBootstrap /></head>\n * <body>{children}</body>\n * </html>\n * );\n * }\n * ```\n */\nexport function ThemeBootstrap(): JSX.Element {\n return (\n <script\n // The script must be synchronous and inline — there's no place else to\n // intercept the document before paint. Build the cookie name into the\n // script so consumers don't have to mirror it.\n dangerouslySetInnerHTML={{ __html: buildBootstrapScript() }}\n />\n );\n}\n\nThemeBootstrap.displayName = 'ThemeBootstrap';\n\n/**\n * Returns the inline-script body that `ThemeBootstrap` injects. Exported so\n * tests (and consumers with a custom layout) can inspect or reuse the source.\n */\nexport function buildBootstrapScript(): string {\n return `(function(){try{var m=document.cookie.match(/(?:^|; )${THEME_COOKIE_NAME}=(dark|light)/);var t=m&&m[1];if(t==='light'){document.documentElement.setAttribute('data-theme','light');}else if(t==='dark'){document.documentElement.removeAttribute('data-theme');}}catch(_){}})();`;\n}\n","'use client';\n\nimport { Switch, useTheme, type Theme } from '@ship-it-ui/ui';\nimport { useCallback, type JSX, type ReactNode } from 'react';\n\nimport { writeThemeCookie } from './theme-cookie';\n\n/**\n * ThemeToggle — token-styled `Switch` bound to the active theme. Reuses the\n * shared `useTheme` hook from `@ship-it-ui/ui` for in-page state and writes\n * a year-long cookie so the next request's `ThemeBootstrap` can render the\n * correct theme synchronously.\n *\n * Pass `label` for a visible labelled control, or supply your own wrapper if\n * you want a chip / menu-item layout.\n */\n\nexport interface ThemeToggleProps {\n /** Visible label rendered next to the switch. */\n label?: ReactNode;\n /** Override the accessible name. Falls back to `label` when it is a string. */\n 'aria-label'?: string;\n /** Fires after the theme has been persisted. Useful for analytics. */\n onThemeChange?: (next: Theme) => void;\n}\n\nexport function ThemeToggle({\n label,\n 'aria-label': ariaLabel,\n onThemeChange,\n}: ThemeToggleProps): JSX.Element {\n const { theme, setTheme } = useTheme();\n\n const handleChange = useCallback(\n (next: boolean) => {\n const target: Theme = next ? 'light' : 'dark';\n setTheme(target);\n writeThemeCookie(target);\n onThemeChange?.(target);\n },\n [setTheme, onThemeChange],\n );\n\n const accessibleName = ariaLabel ?? (typeof label === 'string' ? label : 'Toggle theme');\n\n return (\n <span className=\"inline-flex items-center gap-2\">\n <Switch\n checked={theme === 'light'}\n onCheckedChange={handleChange}\n aria-label={accessibleName}\n />\n {label && <span className=\"text-text-muted text-[12px]\">{label}</span>}\n </span>\n );\n}\n\nThemeToggle.displayName = 'ThemeToggle';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACSO,IAAM,oBAAoB;AAG1B,IAAM,uBAAuB,KAAK,KAAK,KAAK;AAG5C,SAAS,iBAAiB,OAAqD;AACpF,MAAI,UAAU,UAAU,UAAU,QAAS,QAAO;AAClD,SAAO;AACT;AAgBO,SAAS,oBAAoB,aAA8C;AAChF,SAAO,iBAAiB,YAAY,IAAI,iBAAiB,GAAG,KAAK;AACnE;AAOO,SAAS,iBAAiB,OAAoB;AACnD,MAAI,OAAO,aAAa,YAAa;AACrC,WAAS,SAAS,GAAG,iBAAiB,IAAI,KAAK,qBAAqB,oBAAoB;AAC1F;AAGO,SAAS,kBAAqC;AACnD,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,QAAM,QAAQ,SAAS,OACpB,MAAM,GAAG,EACT,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,EACzB,KAAK,CAAC,SAAS,KAAK,WAAW,GAAG,iBAAiB,GAAG,CAAC;AAC1D,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,iBAAiB,MAAM,MAAM,kBAAkB,SAAS,CAAC,CAAC;AACnE;;;AC3BI;AAFG,SAAS,iBAA8B;AAC5C,SACE;AAAA,IAAC;AAAA;AAAA,MAIC,yBAAyB,EAAE,QAAQ,qBAAqB,EAAE;AAAA;AAAA,EAC5D;AAEJ;AAEA,eAAe,cAAc;AAMtB,SAAS,uBAA+B;AAC7C,SAAO,wDAAwD,iBAAiB;AAClF;;;AC7CA,gBAA6C;AAC7C,mBAAsD;AA2ClD,IAAAA,sBAAA;AApBG,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,cAAc;AAAA,EACd;AACF,GAAkC;AAChC,QAAM,EAAE,OAAO,SAAS,QAAI,oBAAS;AAErC,QAAM,mBAAe;AAAA,IACnB,CAAC,SAAkB;AACjB,YAAM,SAAgB,OAAO,UAAU;AACvC,eAAS,MAAM;AACf,uBAAiB,MAAM;AACvB,sBAAgB,MAAM;AAAA,IACxB;AAAA,IACA,CAAC,UAAU,aAAa;AAAA,EAC1B;AAEA,QAAM,iBAAiB,cAAc,OAAO,UAAU,WAAW,QAAQ;AAEzE,SACE,8CAAC,UAAK,WAAU,kCACd;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,SAAS,UAAU;AAAA,QACnB,iBAAiB;AAAA,QACjB,cAAY;AAAA;AAAA,IACd;AAAA,IACC,SAAS,6CAAC,UAAK,WAAU,+BAA+B,iBAAM;AAAA,KACjE;AAEJ;AAEA,YAAY,cAAc;","names":["import_jsx_runtime"]}
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
+ import { JSX, ReactNode } from 'react';
1
2
  import { Theme } from '@ship-it-ui/ui';
2
- import { ReactNode } from 'react';
3
3
  export { C as CookieGetter, T as THEME_COOKIE_MAX_AGE, a as THEME_COOKIE_NAME, g as getThemeFromCookies, p as parseThemeCookie, r as readThemeCookie, w as writeThemeCookie } from './server-DE2Fqp12.cjs';
4
4
 
5
5
  /**
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
+ import { JSX, ReactNode } from 'react';
1
2
  import { Theme } from '@ship-it-ui/ui';
2
- import { ReactNode } from 'react';
3
3
  export { C as CookieGetter, T as THEME_COOKIE_MAX_AGE, a as THEME_COOKIE_NAME, g as getThemeFromCookies, p as parseThemeCookie, r as readThemeCookie, w as writeThemeCookie } from './server-DE2Fqp12.js';
4
4
 
5
5
  /**
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ThemeBootstrap.tsx","../src/ThemeToggle.tsx"],"sourcesContent":["import { THEME_COOKIE_NAME } from './theme-cookie';\n\n/**\n * ThemeBootstrap — render inside the `<head>` of your App Router root layout.\n * Injects a synchronous inline script that reads the theme cookie and sets\n * `<html data-theme>` *before* the first paint, which kills the dark→light\n * flash of unstyled content for users on the light theme.\n *\n * Pair with `getThemeFromCookies(cookies())` to seed the server-rendered\n * `data-theme` on `<html>` for the very first frame.\n *\n * ```tsx\n * import { cookies } from 'next/headers';\n * import { ThemeBootstrap, getThemeFromCookies } from '@ship-it-ui/next';\n *\n * export default function RootLayout({ children }) {\n * const theme = getThemeFromCookies(cookies());\n * return (\n * <html lang=\"en\" data-theme={theme}>\n * <head><ThemeBootstrap /></head>\n * <body>{children}</body>\n * </html>\n * );\n * }\n * ```\n */\nexport function ThemeBootstrap(): JSX.Element {\n return (\n <script\n // The script must be synchronous and inline — there's no place else to\n // intercept the document before paint. Build the cookie name into the\n // script so consumers don't have to mirror it.\n dangerouslySetInnerHTML={{ __html: buildBootstrapScript() }}\n />\n );\n}\n\nThemeBootstrap.displayName = 'ThemeBootstrap';\n\n/**\n * Returns the inline-script body that `ThemeBootstrap` injects. Exported so\n * tests (and consumers with a custom layout) can inspect or reuse the source.\n */\nexport function buildBootstrapScript(): string {\n return `(function(){try{var m=document.cookie.match(/(?:^|; )${THEME_COOKIE_NAME}=(dark|light)/);var t=m&&m[1];if(t==='light'){document.documentElement.setAttribute('data-theme','light');}else if(t==='dark'){document.documentElement.removeAttribute('data-theme');}}catch(_){}})();`;\n}\n","'use client';\n\nimport { Switch, useTheme, type Theme } from '@ship-it-ui/ui';\nimport { useCallback, type ReactNode } from 'react';\n\nimport { writeThemeCookie } from './theme-cookie';\n\n/**\n * ThemeToggle — token-styled `Switch` bound to the active theme. Reuses the\n * shared `useTheme` hook from `@ship-it-ui/ui` for in-page state and writes\n * a year-long cookie so the next request's `ThemeBootstrap` can render the\n * correct theme synchronously.\n *\n * Pass `label` for a visible labelled control, or supply your own wrapper if\n * you want a chip / menu-item layout.\n */\n\nexport interface ThemeToggleProps {\n /** Visible label rendered next to the switch. */\n label?: ReactNode;\n /** Override the accessible name. Falls back to `label` when it is a string. */\n 'aria-label'?: string;\n /** Fires after the theme has been persisted. Useful for analytics. */\n onThemeChange?: (next: Theme) => void;\n}\n\nexport function ThemeToggle({\n label,\n 'aria-label': ariaLabel,\n onThemeChange,\n}: ThemeToggleProps): JSX.Element {\n const { theme, setTheme } = useTheme();\n\n const handleChange = useCallback(\n (next: boolean) => {\n const target: Theme = next ? 'light' : 'dark';\n setTheme(target);\n writeThemeCookie(target);\n onThemeChange?.(target);\n },\n [setTheme, onThemeChange],\n );\n\n const accessibleName = ariaLabel ?? (typeof label === 'string' ? label : 'Toggle theme');\n\n return (\n <span className=\"inline-flex items-center gap-2\">\n <Switch\n checked={theme === 'light'}\n onCheckedChange={handleChange}\n aria-label={accessibleName}\n />\n {label && <span className=\"text-text-muted text-[12px]\">{label}</span>}\n </span>\n );\n}\n\nThemeToggle.displayName = 'ThemeToggle';\n"],"mappings":";;;;;;;;;;AA4BI;AAFG,SAAS,iBAA8B;AAC5C,SACE;AAAA,IAAC;AAAA;AAAA,MAIC,yBAAyB,EAAE,QAAQ,qBAAqB,EAAE;AAAA;AAAA,EAC5D;AAEJ;AAEA,eAAe,cAAc;AAMtB,SAAS,uBAA+B;AAC7C,SAAO,wDAAwD,iBAAiB;AAClF;;;AC3CA,SAAS,QAAQ,gBAA4B;AAC7C,SAAS,mBAAmC;AA2CxC,SACE,OAAAA,MADF;AApBG,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,cAAc;AAAA,EACd;AACF,GAAkC;AAChC,QAAM,EAAE,OAAO,SAAS,IAAI,SAAS;AAErC,QAAM,eAAe;AAAA,IACnB,CAAC,SAAkB;AACjB,YAAM,SAAgB,OAAO,UAAU;AACvC,eAAS,MAAM;AACf,uBAAiB,MAAM;AACvB,sBAAgB,MAAM;AAAA,IACxB;AAAA,IACA,CAAC,UAAU,aAAa;AAAA,EAC1B;AAEA,QAAM,iBAAiB,cAAc,OAAO,UAAU,WAAW,QAAQ;AAEzE,SACE,qBAAC,UAAK,WAAU,kCACd;AAAA,oBAAAA;AAAA,MAAC;AAAA;AAAA,QACC,SAAS,UAAU;AAAA,QACnB,iBAAiB;AAAA,QACjB,cAAY;AAAA;AAAA,IACd;AAAA,IACC,SAAS,gBAAAA,KAAC,UAAK,WAAU,+BAA+B,iBAAM;AAAA,KACjE;AAEJ;AAEA,YAAY,cAAc;","names":["jsx"]}
1
+ {"version":3,"sources":["../src/ThemeBootstrap.tsx","../src/ThemeToggle.tsx"],"sourcesContent":["import type { JSX } from 'react';\n\nimport { THEME_COOKIE_NAME } from './theme-cookie';\n\n/**\n * ThemeBootstrap — render inside the `<head>` of your App Router root layout.\n * Injects a synchronous inline script that reads the theme cookie and sets\n * `<html data-theme>` *before* the first paint, which kills the dark→light\n * flash of unstyled content for users on the light theme.\n *\n * Pair with `getThemeFromCookies(cookies())` to seed the server-rendered\n * `data-theme` on `<html>` for the very first frame.\n *\n * ```tsx\n * import { cookies } from 'next/headers';\n * import { ThemeBootstrap, getThemeFromCookies } from '@ship-it-ui/next';\n *\n * export default function RootLayout({ children }) {\n * const theme = getThemeFromCookies(cookies());\n * return (\n * <html lang=\"en\" data-theme={theme}>\n * <head><ThemeBootstrap /></head>\n * <body>{children}</body>\n * </html>\n * );\n * }\n * ```\n */\nexport function ThemeBootstrap(): JSX.Element {\n return (\n <script\n // The script must be synchronous and inline — there's no place else to\n // intercept the document before paint. Build the cookie name into the\n // script so consumers don't have to mirror it.\n dangerouslySetInnerHTML={{ __html: buildBootstrapScript() }}\n />\n );\n}\n\nThemeBootstrap.displayName = 'ThemeBootstrap';\n\n/**\n * Returns the inline-script body that `ThemeBootstrap` injects. Exported so\n * tests (and consumers with a custom layout) can inspect or reuse the source.\n */\nexport function buildBootstrapScript(): string {\n return `(function(){try{var m=document.cookie.match(/(?:^|; )${THEME_COOKIE_NAME}=(dark|light)/);var t=m&&m[1];if(t==='light'){document.documentElement.setAttribute('data-theme','light');}else if(t==='dark'){document.documentElement.removeAttribute('data-theme');}}catch(_){}})();`;\n}\n","'use client';\n\nimport { Switch, useTheme, type Theme } from '@ship-it-ui/ui';\nimport { useCallback, type JSX, type ReactNode } from 'react';\n\nimport { writeThemeCookie } from './theme-cookie';\n\n/**\n * ThemeToggle — token-styled `Switch` bound to the active theme. Reuses the\n * shared `useTheme` hook from `@ship-it-ui/ui` for in-page state and writes\n * a year-long cookie so the next request's `ThemeBootstrap` can render the\n * correct theme synchronously.\n *\n * Pass `label` for a visible labelled control, or supply your own wrapper if\n * you want a chip / menu-item layout.\n */\n\nexport interface ThemeToggleProps {\n /** Visible label rendered next to the switch. */\n label?: ReactNode;\n /** Override the accessible name. Falls back to `label` when it is a string. */\n 'aria-label'?: string;\n /** Fires after the theme has been persisted. Useful for analytics. */\n onThemeChange?: (next: Theme) => void;\n}\n\nexport function ThemeToggle({\n label,\n 'aria-label': ariaLabel,\n onThemeChange,\n}: ThemeToggleProps): JSX.Element {\n const { theme, setTheme } = useTheme();\n\n const handleChange = useCallback(\n (next: boolean) => {\n const target: Theme = next ? 'light' : 'dark';\n setTheme(target);\n writeThemeCookie(target);\n onThemeChange?.(target);\n },\n [setTheme, onThemeChange],\n );\n\n const accessibleName = ariaLabel ?? (typeof label === 'string' ? label : 'Toggle theme');\n\n return (\n <span className=\"inline-flex items-center gap-2\">\n <Switch\n checked={theme === 'light'}\n onCheckedChange={handleChange}\n aria-label={accessibleName}\n />\n {label && <span className=\"text-text-muted text-[12px]\">{label}</span>}\n </span>\n );\n}\n\nThemeToggle.displayName = 'ThemeToggle';\n"],"mappings":";;;;;;;;;;AA8BI;AAFG,SAAS,iBAA8B;AAC5C,SACE;AAAA,IAAC;AAAA;AAAA,MAIC,yBAAyB,EAAE,QAAQ,qBAAqB,EAAE;AAAA;AAAA,EAC5D;AAEJ;AAEA,eAAe,cAAc;AAMtB,SAAS,uBAA+B;AAC7C,SAAO,wDAAwD,iBAAiB;AAClF;;;AC7CA,SAAS,QAAQ,gBAA4B;AAC7C,SAAS,mBAA6C;AA2ClD,SACE,OAAAA,MADF;AApBG,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA,cAAc;AAAA,EACd;AACF,GAAkC;AAChC,QAAM,EAAE,OAAO,SAAS,IAAI,SAAS;AAErC,QAAM,eAAe;AAAA,IACnB,CAAC,SAAkB;AACjB,YAAM,SAAgB,OAAO,UAAU;AACvC,eAAS,MAAM;AACf,uBAAiB,MAAM;AACvB,sBAAgB,MAAM;AAAA,IACxB;AAAA,IACA,CAAC,UAAU,aAAa;AAAA,EAC1B;AAEA,QAAM,iBAAiB,cAAc,OAAO,UAAU,WAAW,QAAQ;AAEzE,SACE,qBAAC,UAAK,WAAU,kCACd;AAAA,oBAAAA;AAAA,MAAC;AAAA;AAAA,QACC,SAAS,UAAU;AAAA,QACnB,iBAAiB;AAAA,QACjB,cAAY;AAAA;AAAA,IACd;AAAA,IACC,SAAS,gBAAAA,KAAC,UAAK,WAAU,+BAA+B,iBAAM;AAAA,KACjE;AAEJ;AAEA,YAAY,cAAc;","names":["jsx"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ship-it-ui/next",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Next.js (App Router) helpers for the Ship-It design system: SSR-safe theme persistence with FOUC prevention, cookie helpers, and a token-styled theme toggle.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://ship-it-ops.github.io/ship-it-design/",
@@ -47,10 +47,10 @@
47
47
  "provenance": true
48
48
  },
49
49
  "peerDependencies": {
50
- "next": "^14.0.0 || ^15.0.0",
51
- "react": "^18.0.0 || ^19.0.0",
52
- "react-dom": "^18.0.0 || ^19.0.0",
53
- "@ship-it-ui/ui": "0.0.9"
50
+ "next": "^15.0.0 || ^16.0.0",
51
+ "react": "^19.0.0",
52
+ "react-dom": "^19.0.0",
53
+ "@ship-it-ui/ui": "0.0.11"
54
54
  },
55
55
  "peerDependenciesMeta": {
56
56
  "next": {
@@ -61,21 +61,21 @@
61
61
  "@testing-library/jest-dom": "^6.6.3",
62
62
  "@testing-library/react": "^16.0.1",
63
63
  "@testing-library/user-event": "^14.5.2",
64
- "@types/react": "^18.3.12",
65
- "@types/react-dom": "^18.3.1",
64
+ "@types/react": "^19.2.0",
65
+ "@types/react-dom": "^19.2.0",
66
66
  "axe-core": "^4.10.2",
67
67
  "esbuild-plugin-preserve-directives": "^0.0.11",
68
68
  "jsdom": "^29.1.1",
69
- "react": "^18.3.1",
70
- "react-dom": "^18.3.1",
69
+ "react": "^19.2.0",
70
+ "react-dom": "^19.2.0",
71
71
  "tsup": "^8.3.0",
72
72
  "typescript": "^5.6.3",
73
73
  "vitest": "^2.1.3",
74
74
  "vitest-axe": "^0.1.0",
75
75
  "@ship-it-ui/eslint-config": "0.0.1",
76
- "@ship-it-ui/tokens": "0.0.7",
77
- "@ship-it-ui/ui": "0.0.9",
78
- "@ship-it-ui/tsconfig": "0.0.1"
76
+ "@ship-it-ui/tsconfig": "0.0.1",
77
+ "@ship-it-ui/ui": "0.0.11",
78
+ "@ship-it-ui/tokens": "0.0.7"
79
79
  },
80
80
  "scripts": {
81
81
  "build": "tsup",