@ship-it-ui/next 0.0.2 → 0.0.3

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.
@@ -0,0 +1,30 @@
1
+ // src/theme-cookie.ts
2
+ var THEME_COOKIE_NAME = "ship-it-theme";
3
+ var THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
4
+ function parseThemeCookie(value) {
5
+ if (value === "dark" || value === "light") return value;
6
+ return void 0;
7
+ }
8
+ function getThemeFromCookies(cookieStore) {
9
+ return parseThemeCookie(cookieStore.get(THEME_COOKIE_NAME)?.value);
10
+ }
11
+ function writeThemeCookie(theme) {
12
+ if (typeof document === "undefined") return;
13
+ document.cookie = `${THEME_COOKIE_NAME}=${theme}; path=/; max-age=${THEME_COOKIE_MAX_AGE}; samesite=lax`;
14
+ }
15
+ function readThemeCookie() {
16
+ if (typeof document === "undefined") return void 0;
17
+ const match = document.cookie.split(";").map((part) => part.trim()).find((part) => part.startsWith(`${THEME_COOKIE_NAME}=`));
18
+ if (!match) return void 0;
19
+ return parseThemeCookie(match.slice(THEME_COOKIE_NAME.length + 1));
20
+ }
21
+
22
+ export {
23
+ THEME_COOKIE_NAME,
24
+ THEME_COOKIE_MAX_AGE,
25
+ parseThemeCookie,
26
+ getThemeFromCookies,
27
+ writeThemeCookie,
28
+ readThemeCookie
29
+ };
30
+ //# sourceMappingURL=chunk-K3H6CPFL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/theme-cookie.ts"],"sourcesContent":["/**\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"],"mappings":";AASO,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;","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Theme } from '@ship-it-ui/ui';
2
2
  import { ReactNode } from 'react';
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';
3
4
 
4
5
  /**
5
6
  * ThemeBootstrap — render inside the `<head>` of your App Router root layout.
@@ -57,41 +58,4 @@ declare namespace ThemeToggle {
57
58
  var displayName: string;
58
59
  }
59
60
 
60
- /**
61
- * Theme cookie helpers shared between the server-side `getThemeFromCookies`
62
- * and the client-side `ThemeBootstrap` / `ThemeToggle`. The cookie value is
63
- * the literal string `'dark'` or `'light'`; anything else falls back to
64
- * undefined and the consumer's default (typically dark) applies.
65
- */
66
-
67
- declare const THEME_COOKIE_NAME = "ship-it-theme";
68
- /** One year. The cookie is non-sensitive, so a long lifetime is fine. */
69
- declare const THEME_COOKIE_MAX_AGE: number;
70
- /** Parse a raw cookie string value into a `Theme` or `undefined`. */
71
- declare function parseThemeCookie(value: string | null | undefined): Theme | undefined;
72
- /**
73
- * Server helper for the App Router `cookies()` API. Pass `cookies()` (or any
74
- * object with `.get(name)` that returns `{ value: string }`) and receive the
75
- * stored theme.
76
- *
77
- * ```ts
78
- * import { cookies } from 'next/headers';
79
- * const theme = getThemeFromCookies(await cookies());
80
- * ```
81
- */
82
- interface CookieGetter {
83
- get(name: string): {
84
- value: string;
85
- } | undefined;
86
- }
87
- declare function getThemeFromCookies(cookieStore: CookieGetter): Theme | undefined;
88
- /**
89
- * Client-side cookie writer. Sets a path-`/` cookie with a year-long TTL and
90
- * `SameSite=Lax`, which is safe for theme preferences (non-sensitive, not
91
- * cross-site). No-op on the server.
92
- */
93
- declare function writeThemeCookie(theme: Theme): void;
94
- /** Read the theme cookie from `document.cookie`. No-op on the server. */
95
- declare function readThemeCookie(): Theme | undefined;
96
-
97
- export { type CookieGetter, THEME_COOKIE_MAX_AGE, THEME_COOKIE_NAME, ThemeBootstrap, ThemeToggle, type ThemeToggleProps, buildBootstrapScript, getThemeFromCookies, parseThemeCookie, readThemeCookie, writeThemeCookie };
61
+ export { ThemeBootstrap, ThemeToggle, type ThemeToggleProps, buildBootstrapScript };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Theme } from '@ship-it-ui/ui';
2
2
  import { ReactNode } from 'react';
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';
3
4
 
4
5
  /**
5
6
  * ThemeBootstrap — render inside the `<head>` of your App Router root layout.
@@ -57,41 +58,4 @@ declare namespace ThemeToggle {
57
58
  var displayName: string;
58
59
  }
59
60
 
60
- /**
61
- * Theme cookie helpers shared between the server-side `getThemeFromCookies`
62
- * and the client-side `ThemeBootstrap` / `ThemeToggle`. The cookie value is
63
- * the literal string `'dark'` or `'light'`; anything else falls back to
64
- * undefined and the consumer's default (typically dark) applies.
65
- */
66
-
67
- declare const THEME_COOKIE_NAME = "ship-it-theme";
68
- /** One year. The cookie is non-sensitive, so a long lifetime is fine. */
69
- declare const THEME_COOKIE_MAX_AGE: number;
70
- /** Parse a raw cookie string value into a `Theme` or `undefined`. */
71
- declare function parseThemeCookie(value: string | null | undefined): Theme | undefined;
72
- /**
73
- * Server helper for the App Router `cookies()` API. Pass `cookies()` (or any
74
- * object with `.get(name)` that returns `{ value: string }`) and receive the
75
- * stored theme.
76
- *
77
- * ```ts
78
- * import { cookies } from 'next/headers';
79
- * const theme = getThemeFromCookies(await cookies());
80
- * ```
81
- */
82
- interface CookieGetter {
83
- get(name: string): {
84
- value: string;
85
- } | undefined;
86
- }
87
- declare function getThemeFromCookies(cookieStore: CookieGetter): Theme | undefined;
88
- /**
89
- * Client-side cookie writer. Sets a path-`/` cookie with a year-long TTL and
90
- * `SameSite=Lax`, which is safe for theme preferences (non-sensitive, not
91
- * cross-site). No-op on the server.
92
- */
93
- declare function writeThemeCookie(theme: Theme): void;
94
- /** Read the theme cookie from `document.cookie`. No-op on the server. */
95
- declare function readThemeCookie(): Theme | undefined;
96
-
97
- export { type CookieGetter, THEME_COOKIE_MAX_AGE, THEME_COOKIE_NAME, ThemeBootstrap, ThemeToggle, type ThemeToggleProps, buildBootstrapScript, getThemeFromCookies, parseThemeCookie, readThemeCookie, writeThemeCookie };
61
+ export { ThemeBootstrap, ThemeToggle, type ThemeToggleProps, buildBootstrapScript };
package/dist/index.js CHANGED
@@ -1,25 +1,13 @@
1
1
  'use client';
2
2
 
3
- // src/theme-cookie.ts
4
- var THEME_COOKIE_NAME = "ship-it-theme";
5
- var THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
6
- function parseThemeCookie(value) {
7
- if (value === "dark" || value === "light") return value;
8
- return void 0;
9
- }
10
- function getThemeFromCookies(cookieStore) {
11
- return parseThemeCookie(cookieStore.get(THEME_COOKIE_NAME)?.value);
12
- }
13
- function writeThemeCookie(theme) {
14
- if (typeof document === "undefined") return;
15
- document.cookie = `${THEME_COOKIE_NAME}=${theme}; path=/; max-age=${THEME_COOKIE_MAX_AGE}; samesite=lax`;
16
- }
17
- function readThemeCookie() {
18
- if (typeof document === "undefined") return void 0;
19
- const match = document.cookie.split(";").map((part) => part.trim()).find((part) => part.startsWith(`${THEME_COOKIE_NAME}=`));
20
- if (!match) return void 0;
21
- return parseThemeCookie(match.slice(THEME_COOKIE_NAME.length + 1));
22
- }
3
+ import {
4
+ THEME_COOKIE_MAX_AGE,
5
+ THEME_COOKIE_NAME,
6
+ getThemeFromCookies,
7
+ parseThemeCookie,
8
+ readThemeCookie,
9
+ writeThemeCookie
10
+ } from "./chunk-K3H6CPFL.js";
23
11
 
24
12
  // src/ThemeBootstrap.tsx
25
13
  import { jsx } from "react/jsx-runtime";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/theme-cookie.ts","../src/ThemeBootstrap.tsx","../src/ThemeToggle.tsx"],"sourcesContent":["/**\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":";AASO,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,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 { 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"]}
@@ -0,0 +1,40 @@
1
+ import { Theme } from '@ship-it-ui/ui';
2
+
3
+ /**
4
+ * Theme cookie helpers shared between the server-side `getThemeFromCookies`
5
+ * and the client-side `ThemeBootstrap` / `ThemeToggle`. The cookie value is
6
+ * the literal string `'dark'` or `'light'`; anything else falls back to
7
+ * undefined and the consumer's default (typically dark) applies.
8
+ */
9
+
10
+ declare const THEME_COOKIE_NAME = "ship-it-theme";
11
+ /** One year. The cookie is non-sensitive, so a long lifetime is fine. */
12
+ declare const THEME_COOKIE_MAX_AGE: number;
13
+ /** Parse a raw cookie string value into a `Theme` or `undefined`. */
14
+ declare function parseThemeCookie(value: string | null | undefined): Theme | undefined;
15
+ /**
16
+ * Server helper for the App Router `cookies()` API. Pass `cookies()` (or any
17
+ * object with `.get(name)` that returns `{ value: string }`) and receive the
18
+ * stored theme.
19
+ *
20
+ * ```ts
21
+ * import { cookies } from 'next/headers';
22
+ * const theme = getThemeFromCookies(await cookies());
23
+ * ```
24
+ */
25
+ interface CookieGetter {
26
+ get(name: string): {
27
+ value: string;
28
+ } | undefined;
29
+ }
30
+ declare function getThemeFromCookies(cookieStore: CookieGetter): Theme | undefined;
31
+ /**
32
+ * Client-side cookie writer. Sets a path-`/` cookie with a year-long TTL and
33
+ * `SameSite=Lax`, which is safe for theme preferences (non-sensitive, not
34
+ * cross-site). No-op on the server.
35
+ */
36
+ declare function writeThemeCookie(theme: Theme): void;
37
+ /** Read the theme cookie from `document.cookie`. No-op on the server. */
38
+ declare function readThemeCookie(): Theme | undefined;
39
+
40
+ export { type CookieGetter as C, THEME_COOKIE_MAX_AGE as T, THEME_COOKIE_NAME as a, getThemeFromCookies as g, parseThemeCookie as p, readThemeCookie as r, writeThemeCookie as w };
@@ -0,0 +1,40 @@
1
+ import { Theme } from '@ship-it-ui/ui';
2
+
3
+ /**
4
+ * Theme cookie helpers shared between the server-side `getThemeFromCookies`
5
+ * and the client-side `ThemeBootstrap` / `ThemeToggle`. The cookie value is
6
+ * the literal string `'dark'` or `'light'`; anything else falls back to
7
+ * undefined and the consumer's default (typically dark) applies.
8
+ */
9
+
10
+ declare const THEME_COOKIE_NAME = "ship-it-theme";
11
+ /** One year. The cookie is non-sensitive, so a long lifetime is fine. */
12
+ declare const THEME_COOKIE_MAX_AGE: number;
13
+ /** Parse a raw cookie string value into a `Theme` or `undefined`. */
14
+ declare function parseThemeCookie(value: string | null | undefined): Theme | undefined;
15
+ /**
16
+ * Server helper for the App Router `cookies()` API. Pass `cookies()` (or any
17
+ * object with `.get(name)` that returns `{ value: string }`) and receive the
18
+ * stored theme.
19
+ *
20
+ * ```ts
21
+ * import { cookies } from 'next/headers';
22
+ * const theme = getThemeFromCookies(await cookies());
23
+ * ```
24
+ */
25
+ interface CookieGetter {
26
+ get(name: string): {
27
+ value: string;
28
+ } | undefined;
29
+ }
30
+ declare function getThemeFromCookies(cookieStore: CookieGetter): Theme | undefined;
31
+ /**
32
+ * Client-side cookie writer. Sets a path-`/` cookie with a year-long TTL and
33
+ * `SameSite=Lax`, which is safe for theme preferences (non-sensitive, not
34
+ * cross-site). No-op on the server.
35
+ */
36
+ declare function writeThemeCookie(theme: Theme): void;
37
+ /** Read the theme cookie from `document.cookie`. No-op on the server. */
38
+ declare function readThemeCookie(): Theme | undefined;
39
+
40
+ export { type CookieGetter as C, THEME_COOKIE_MAX_AGE as T, THEME_COOKIE_NAME as a, getThemeFromCookies as g, parseThemeCookie as p, readThemeCookie as r, writeThemeCookie as w };
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/server.ts
21
+ var server_exports = {};
22
+ __export(server_exports, {
23
+ THEME_COOKIE_MAX_AGE: () => THEME_COOKIE_MAX_AGE,
24
+ THEME_COOKIE_NAME: () => THEME_COOKIE_NAME,
25
+ getThemeFromCookies: () => getThemeFromCookies,
26
+ parseThemeCookie: () => parseThemeCookie
27
+ });
28
+ module.exports = __toCommonJS(server_exports);
29
+
30
+ // src/theme-cookie.ts
31
+ var THEME_COOKIE_NAME = "ship-it-theme";
32
+ var THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
33
+ function parseThemeCookie(value) {
34
+ if (value === "dark" || value === "light") return value;
35
+ return void 0;
36
+ }
37
+ function getThemeFromCookies(cookieStore) {
38
+ return parseThemeCookie(cookieStore.get(THEME_COOKIE_NAME)?.value);
39
+ }
40
+ // Annotate the CommonJS export names for ESM import in node:
41
+ 0 && (module.exports = {
42
+ THEME_COOKIE_MAX_AGE,
43
+ THEME_COOKIE_NAME,
44
+ getThemeFromCookies,
45
+ parseThemeCookie
46
+ });
47
+ //# sourceMappingURL=server.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts","../src/theme-cookie.ts"],"sourcesContent":["/**\n * @ship-it-ui/next/server — server-safe subset of `@ship-it-ui/next`.\n *\n * Re-exports only the cookie helpers that have no React dependency, so they\n * can be imported from a server `layout.tsx` without `tsup`'s preserved\n * `'use client'` directive (hoisted from `ThemeToggle.tsx` in the root entry)\n * tagging the whole module as a client reference.\n */\n\nexport {\n getThemeFromCookies,\n parseThemeCookie,\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"],"mappings":";;;;;;;;;;;;;;;;;;;;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;","names":[]}
@@ -0,0 +1,2 @@
1
+ export { C as CookieGetter, T as THEME_COOKIE_MAX_AGE, a as THEME_COOKIE_NAME, g as getThemeFromCookies, p as parseThemeCookie } from './server-DE2Fqp12.cjs';
2
+ import '@ship-it-ui/ui';
@@ -0,0 +1,2 @@
1
+ export { C as CookieGetter, T as THEME_COOKIE_MAX_AGE, a as THEME_COOKIE_NAME, g as getThemeFromCookies, p as parseThemeCookie } from './server-DE2Fqp12.js';
2
+ import '@ship-it-ui/ui';
package/dist/server.js ADDED
@@ -0,0 +1,13 @@
1
+ import {
2
+ THEME_COOKIE_MAX_AGE,
3
+ THEME_COOKIE_NAME,
4
+ getThemeFromCookies,
5
+ parseThemeCookie
6
+ } from "./chunk-K3H6CPFL.js";
7
+ export {
8
+ THEME_COOKIE_MAX_AGE,
9
+ THEME_COOKIE_NAME,
10
+ getThemeFromCookies,
11
+ parseThemeCookie
12
+ };
13
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ship-it-ui/next",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
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/",
@@ -30,6 +30,11 @@
30
30
  "types": "./dist/index.d.ts",
31
31
  "import": "./dist/index.js",
32
32
  "require": "./dist/index.cjs"
33
+ },
34
+ "./server": {
35
+ "types": "./dist/server.d.ts",
36
+ "import": "./dist/server.js",
37
+ "require": "./dist/server.cjs"
33
38
  }
34
39
  },
35
40
  "files": [
@@ -45,7 +50,7 @@
45
50
  "next": "^14.0.0 || ^15.0.0",
46
51
  "react": "^18.0.0 || ^19.0.0",
47
52
  "react-dom": "^18.0.0 || ^19.0.0",
48
- "@ship-it-ui/ui": "0.0.4"
53
+ "@ship-it-ui/ui": "0.0.5"
49
54
  },
50
55
  "peerDependenciesMeta": {
51
56
  "next": {
@@ -68,9 +73,9 @@
68
73
  "vitest": "^2.1.3",
69
74
  "vitest-axe": "^0.1.0",
70
75
  "@ship-it-ui/eslint-config": "0.0.1",
71
- "@ship-it-ui/tokens": "0.0.4",
76
+ "@ship-it-ui/tokens": "0.0.5",
72
77
  "@ship-it-ui/tsconfig": "0.0.1",
73
- "@ship-it-ui/ui": "0.0.4"
78
+ "@ship-it-ui/ui": "0.0.5"
74
79
  },
75
80
  "scripts": {
76
81
  "build": "tsup",