cherry-styled-components 0.1.19 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Luan Gjokaj
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,31 +1,221 @@
1
1
  # Cherry React Library
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/cherry-styled-components.svg?style=flat)](https://www.npmjs.com/package/cherry-styled-components)
4
-
5
- ## Introduction
4
+ [![license](https://img.shields.io/npm/l/cherry-styled-components.svg?style=flat)](https://github.com/cherry-design-system/styled-components/blob/main/LICENSE)
6
5
 
7
6
  Cherry Design System is a versatile foundation for projects. It offers a white label base, ready-to-use Figma designs, open-source React components, built-in support for theming and dark mode. Explore the [docs](https://cherry.al/) to create delightful user interfaces.
8
7
 
9
- ---
8
+ ## Installation
10
9
 
11
- # Installation
10
+ Install the package along with its peer dependencies:
12
11
 
13
- This project requires Node.js v20+ installed.
12
+ ```bash
13
+ npm install cherry-styled-components react react-dom styled-components
14
+ ```
15
+
16
+ ## Quick Start
17
+
18
+ Wrap your app in `CherryThemeProvider` and start using components. The provider injects the global styles and, when you pass `themeDark`, handles dark mode automatically:
19
+
20
+ ```tsx
21
+ import {
22
+ Button,
23
+ CherryThemeProvider,
24
+ theme,
25
+ themeDark,
26
+ } from "cherry-styled-components";
27
+
28
+ export default function App() {
29
+ return (
30
+ <CherryThemeProvider theme={theme} themeDark={themeDark}>
31
+ <Button $variant="primary">Hello Cherry</Button>
32
+ </CherryThemeProvider>
33
+ );
34
+ }
35
+ ```
36
+
37
+ The built-in `theme` and `themeDark` objects are a starting point; both are plain objects you can extend or replace to white-label the system. Styled props use a `$` prefix (`$variant`, `$size`, `$fullWidth`) so they never leak into the DOM.
38
+
39
+ The library ships form components (Button, Input, Select, Textarea, Toggle, Range, Password, Checkbox, Radio, Dropzone, AvatarDropzone), layout primitives (Container, Grid, Col, Flex, Box, MaxWidth, Space), and interactive components (Accordion, Modal, Toast, ThemeToggle, Icon, IconButton). See the [component docs](https://cherry.al/) for the full API.
40
+
41
+ ## Theming
42
+
43
+ Cherry ships two theme providers:
44
+
45
+ - `CherryThemeProvider` - the original client-only provider. It resolves the theme from `localStorage` and the OS preference after mount. Simple, but in a server-rendered app the first paint is always the light theme.
46
+ - `ClientThemeProvider` - the SSR-aware provider for flash-free dark mode. The server resolves the `theme` cookie and passes `$initial`, so the first paint is already correct. On mount it reconciles against the cookie and OS preference (Safari and Firefox don't send color-scheme client hints, so their first server render can guess wrong) and swaps in place. Theme changes persist to the `theme` cookie and `localStorage` directly; no API route is needed.
47
+
48
+ Both providers expose `setTheme` and `toggleTheme` through `ThemeContext`, and the `ThemeToggle` component works under either.
49
+
50
+ A Next.js App Router setup looks like this:
51
+
52
+ ```tsx
53
+ // app/layout.tsx (server component)
54
+ import { cookies } from "next/headers";
55
+ import {
56
+ ClientThemeProvider,
57
+ StyledComponentsRegistry,
58
+ themeInitScript,
59
+ } from "cherry-styled-components";
60
+ import { theme, themeDark } from "./theme";
61
+
62
+ export default async function RootLayout({ children }) {
63
+ const cookieTheme = (await cookies()).get("theme")?.value;
64
+
65
+ return (
66
+ <html lang="en">
67
+ <head>
68
+ {/* Seeds the theme cookie and prevents the dark-mode flash in
69
+ browsers without color-scheme client hints. */}
70
+ <script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
71
+ </head>
72
+ <body>
73
+ <StyledComponentsRegistry>
74
+ <ClientThemeProvider
75
+ theme={theme}
76
+ themeDark={themeDark}
77
+ $initial={cookieTheme === "dark" ? "dark" : "light"}
78
+ >
79
+ {children}
80
+ </ClientThemeProvider>
81
+ </StyledComponentsRegistry>
82
+ </body>
83
+ </html>
84
+ );
85
+ }
86
+ ```
87
+
88
+ For the best first-visit experience in Chrome, also opt into color-scheme client hints in your middleware so the very first server render matches the OS preference:
89
+
90
+ ```ts
91
+ // middleware.ts
92
+ res.headers.set("Accept-CH", "Sec-CH-Prefers-Color-Scheme");
93
+ res.headers.set("Vary", "Sec-CH-Prefers-Color-Scheme");
94
+ res.headers.set("Critical-CH", "Sec-CH-Prefers-Color-Scheme");
95
+
96
+ const hint = req.headers.get("Sec-CH-Prefers-Color-Scheme");
97
+ if (!req.cookies.get("theme")?.value && hint) {
98
+ res.cookies.set("theme", hint === "dark" ? "dark" : "light", {
99
+ path: "/",
100
+ maxAge: 60 * 60 * 24 * 365,
101
+ sameSite: "lax",
102
+ });
103
+ }
104
+ ```
105
+
106
+ `resolveTheme(cookieValue, theme, themeDark)` is exported for other server code that needs the active theme object, e.g. `generateViewport` for the initial `theme-color`.
107
+
108
+ In a client-only app (like this repo's demo, see `src/main.tsx`), resolve the initial theme synchronously from the cookie, `localStorage`, or `matchMedia` before rendering and pass it as `$initial`.
109
+
110
+ ## Development
111
+
112
+ To work on the library itself, clone the repo and use pnpm. Node.js v20+ is required for the dev tooling.
14
113
 
15
114
  ```bash
16
- npm install
115
+ pnpm install # Install dependencies
116
+ pnpm run dev # Start the Vite dev server (demo app + previews)
117
+ pnpm run build # Build the library to dist/
118
+ pnpm run format # Format with Prettier
119
+ ```
120
+
121
+ ## Component Previews
122
+
123
+ The dev server ships with a preview route that renders a single component in isolation, centered on the page. It is meant for visual inspection and for taking automated screenshots (e.g. with Playwright).
124
+
125
+ With `pnpm run dev` running:
126
+
127
+ - `/preview` shows an index page linking to every available preview.
128
+ - `/preview/<name>` renders one component centered inside a wrapper with the stable selector `#preview-box`.
129
+
130
+ The route is handled in `src/main.tsx` and the previews live in `src/preview.tsx`. Both are part of the demo app only and are not included in the library build.
131
+
132
+ ### Available previews
133
+
134
+ | Name | Renders |
135
+ | ------------------ | ------------------------------------------------- |
136
+ | `accordion` | Card accordion, open by default |
137
+ | `accordion-inline` | Inline accordion variant, open by default |
138
+ | `avatar-dropzone` | Empty avatar dropzone, default size |
139
+ | `box` | Box surface with a placeholder block |
140
+ | `button` | Primary filled button |
141
+ | `button-secondary` | Secondary filled button |
142
+ | `button-tertiary` | Tertiary filled button |
143
+ | `button-outline` | Primary outline button |
144
+ | `checkbox` | Checked checkbox |
145
+ | `dropzone` | Block dropzone with prompt, browse, and hint text |
146
+ | `dropzone-inline` | Inline dropzone variant |
147
+ | `flex` | Flex row with three placeholder blocks |
148
+ | `grid` | Three-column grid with a full-width row |
149
+ | `icon` | Cherry icon at 48px |
150
+ | `icon-button` | Icon button with a settings icon |
151
+ | `input` | Text input with label and placeholder |
152
+ | `modal` | Modal, already open on a backdrop |
153
+ | `password` | Password field with label and placeholder |
154
+ | `radio` | Checked radio button |
155
+ | `range` | Range slider |
156
+ | `select` | Select with label |
157
+ | `textarea` | Textarea with label and value |
158
+ | `theme-toggle` | Pill-shaped sun/moon theme switch |
159
+ | `toast` | Success, error, and info toasts, fired on mount |
160
+ | `toggle` | Checked toggle |
161
+
162
+ Pure layout primitives with no visual output of their own (Container, Col, MaxWidth, Space) have no preview entry.
163
+
164
+ ### Forcing a theme
165
+
166
+ Append `?theme=dark` or `?theme=light` to any preview URL to force a theme:
167
+
168
+ ```
169
+ /preview/button?theme=dark
17
170
  ```
18
171
 
19
- To run the development environment, use the following command:
172
+ The value is persisted to the `theme` cookie and `localStorage`, exactly like a `ThemeToggle` click, and the demo resolves it synchronously before the first render. It persists for subsequent navigations in the same browser context, so always pass the parameter explicitly when comparing themes to avoid leakage from a previous visit.
173
+
174
+ ### Custom theme colors
175
+
176
+ Append `?colors=<URI-encoded JSON>` to any preview URL to override theme colors. The JSON has the shape `{ "default": { "primary": "#BE123C", ... }, "dark": { ... } }` where `default` patches the light theme's colors and `dark` patches the dark theme's. Unknown keys and values that are not valid CSS colors are ignored, and the parameter only applies to preview routes.
177
+
178
+ ### Taking screenshots
179
+
180
+ `scripts/screenshot-previews.cjs` captures every preview in both themes and writes the framed images used by the documentation site. With the dev server running in another terminal:
20
181
 
21
182
  ```bash
22
- npm run dev
183
+ pnpm run dev
184
+ pnpm run screenshots
23
185
  ```
24
186
 
25
- To build the library for production, use the following command:
187
+ For each component it produces `<name>-light.png` and `<name>-dark.png` at 2x resolution (`deviceScaleFactor: 2`), then post-processes every image with sharp:
188
+
189
+ - Corners are rounded with the theme's `radius.lg` (12px, so 24px at 2x) and made transparent outside the radius.
190
+ - A 1px solid border is drawn in the theme's `grayLight` color: `#e5e7eb` for light, `#1a1a1a` for dark.
191
+
192
+ The border and radius are baked into the PNG rather than applied in CSS so that all images get the identical frame, including the two special captures:
193
+
194
+ - `modal` overlays the whole viewport, so it is captured as a full-page screenshot to include the backdrop. It is shot on a desktop-size viewport (1100px wide) so the modal's built-in `max-width: 500px` (active from the `lg` breakpoint) applies.
195
+ - `toast` renders in a corner outside `#preview-box`, so the toast list element is captured instead, with 40px of padding injected at capture time for breathing room.
196
+
197
+ Everything else is cropped to the `#preview-box` element. Animated previews (accordion, modal, toast) get a one-second settle delay before capture.
198
+
199
+ The script drives your installed Chrome through `playwright-core` (no browser download needed) and defaults can be overridden with env vars:
200
+
201
+ | Variable | Default |
202
+ | ------------- | -------------------------------------------------------------- |
203
+ | `PREVIEW_URL` | `http://localhost:5173/preview` |
204
+ | `OUT_DIR` | `../cherry-documentation/public/components` |
205
+ | `CHROME_PATH` | `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome` |
206
+ | `THEME_JSON` | none (path to a JSON file with `default`/`dark` color maps) |
207
+
208
+ `THEME_JSON` points at a file like the documentation site's `theme.json`; its colors are passed to the preview via `?colors=` and also drive the baked-in border color (`grayLight`). Example:
26
209
 
27
210
  ```bash
28
- npm run build
211
+ THEME_JSON=../cherry-documentation/theme.json pnpm run screenshots
212
+ ```
213
+
214
+ For one-off manual screenshots, target the `#preview-box` selector:
215
+
216
+ ```js
217
+ await page.goto("http://localhost:5173/preview/button?theme=light");
218
+ await page.locator("#preview-box").screenshot({ path: "button-light.png" });
29
219
  ```
30
220
 
31
221
  ## Community
@@ -37,3 +227,7 @@ For help, discussion about best practices, or any other conversation that would
37
227
  For casual chit-chat with others using Cherry:
38
228
 
39
229
  [Join the Discord Server](https://discord.gg/6JvcWU5bke)
230
+
231
+ ## License
232
+
233
+ [MIT](https://github.com/cherry-design-system/styled-components/blob/main/LICENSE)
package/dist/index.d.ts CHANGED
@@ -19,5 +19,6 @@ export * from './range';
19
19
  export * from './select';
20
20
  export * from './space';
21
21
  export * from './textarea';
22
+ export * from './theme-toggle';
22
23
  export * from './toast';
23
24
  export * from './toggle';
package/dist/index.js CHANGED
@@ -1,11 +1,13 @@
1
- import { StyledComponentsRegistry } from "./styled-components/registry.js";
2
1
  import { GlobalStyles } from "./utils/global.js";
3
2
  import { IconArrow, IconCalendar, IconCheck } from "./utils/icons.js";
4
3
  import { breakpoints, colors, colorsDark, fontSizes, fonts, lineHeights, mq, shadows, shadowsDark, spacing, theme, themeDark } from "./utils/theme.js";
5
- import { formElementHeightStyles, fullWidthStyles, generateAlignContentStyles, generateAlignItemsStyles, generateColSpanStyles, generateColsStyles, generateDirectionStyles, generateGapStyles, generateJustifyContentStyles, generatePaddingStyles, resetButton, resetInput, statusBorderStyles } from "./utils/mixins.js";
4
+ import { errorInteractiveStyles, formElementHeightStyles, fullWidthStyles, generateAlignContentStyles, generateAlignItemsStyles, generateColSpanStyles, generateColsStyles, generateDirectionStyles, generateGapStyles, generateJustifyContentStyles, generatePaddingStyles, interactiveStyles, resetButton, resetInput, statusBorderStyles } from "./utils/mixins.js";
6
5
  import { createTypographyStyle, styledBlockquote, styledButton, styledButtonBig, styledCode, styledH1, styledH2, styledH3, styledH4, styledH5, styledH6, styledHero1, styledHero2, styledHero3, styledInput, styledInputBig, styledSmall, styledStrong, styledText } from "./utils/typography.js";
7
6
  import { useOnClickOutside } from "./utils/use-on-click-outside.js";
8
7
  import { CherryThemeProvider, ThemeContext } from "./styled-components/theme-provider.js";
8
+ import { ClientThemeProvider } from "./styled-components/client-theme-provider.js";
9
+ import { StyledComponentsRegistry } from "./styled-components/registry.js";
10
+ import { createThemeInitScript, resolveTheme, themeInitScript } from "./styled-components/theme-init.js";
9
11
  import { Icon } from "./icon.js";
10
12
  import { Accordion } from "./accordion.js";
11
13
  import { Dropzone, matchesAccept } from "./dropzone.js";
@@ -25,6 +27,7 @@ import { Range } from "./range.js";
25
27
  import { Select, StyledIconWrapper } from "./select.js";
26
28
  import { Space } from "./space.js";
27
29
  import { Textarea } from "./textarea.js";
28
- import { StyledNotifications, ToastNotifications, ToastNotificationsContext, ToastNotificationsProvider, useToastNotifications } from "./toast.js";
30
+ import { ThemeToggle } from "./theme-toggle.js";
31
+ import { StyledNotificationItem, StyledNotifications, ToastNotifications, ToastNotificationsContext, ToastNotificationsProvider, useToastNotifications } from "./toast.js";
29
32
  import { Toggle } from "./toggle.js";
30
- export { Accordion, AvatarDropzone, Box, Button, CherryThemeProvider, Col, Container, Dropzone, Flex, GlobalStyles, Grid, Icon, IconArrow, IconButton, IconCalendar, IconCheck, Input, MaxWidth, Modal, Password, Range, Select, Space, StyledComponentsRegistry, StyledIconWrapper, StyledInputWrapper, StyledLabel, StyledNotifications, Textarea, ThemeContext, ToastNotifications, ToastNotificationsContext, ToastNotificationsProvider, Toggle, breakpoints, buttonStyles, colors, colorsDark, createTypographyStyle, fontSizes, fonts, formElementHeightStyles, fullWidthStyles, generateAlignContentStyles, generateAlignItemsStyles, generateColSpanStyles, generateColsStyles, generateDirectionStyles, generateGapStyles, generateJustifyContentStyles, generatePaddingStyles, iconButtonStyles, lineHeights, matchesAccept, mq, resetButton, resetInput, shadows, shadowsDark, spacing, statusBorderStyles, styledBlockquote, styledButton, styledButtonBig, styledCode, styledH1, styledH2, styledH3, styledH4, styledH5, styledH6, styledHero1, styledHero2, styledHero3, styledInput, styledInputBig, styledSmall, styledStrong, styledText, theme, themeDark, useOnClickOutside, useToastNotifications };
33
+ export { Accordion, AvatarDropzone, Box, Button, CherryThemeProvider, ClientThemeProvider, Col, Container, Dropzone, Flex, GlobalStyles, Grid, Icon, IconArrow, IconButton, IconCalendar, IconCheck, Input, MaxWidth, Modal, Password, Range, Select, Space, StyledComponentsRegistry, StyledIconWrapper, StyledInputWrapper, StyledLabel, StyledNotificationItem, StyledNotifications, Textarea, ThemeContext, ThemeToggle, ToastNotifications, ToastNotificationsContext, ToastNotificationsProvider, Toggle, breakpoints, buttonStyles, colors, colorsDark, createThemeInitScript, createTypographyStyle, errorInteractiveStyles, fontSizes, fonts, formElementHeightStyles, fullWidthStyles, generateAlignContentStyles, generateAlignItemsStyles, generateColSpanStyles, generateColsStyles, generateDirectionStyles, generateGapStyles, generateJustifyContentStyles, generatePaddingStyles, iconButtonStyles, interactiveStyles, lineHeights, matchesAccept, mq, resetButton, resetInput, resolveTheme, shadows, shadowsDark, spacing, statusBorderStyles, styledBlockquote, styledButton, styledButtonBig, styledCode, styledH1, styledH2, styledH3, styledH4, styledH5, styledH6, styledHero1, styledHero2, styledHero3, styledInput, styledInputBig, styledSmall, styledStrong, styledText, theme, themeDark, themeInitScript, useOnClickOutside, useToastNotifications };
@@ -0,0 +1,35 @@
1
+ import { default as React } from 'react';
2
+ import { Theme } from '../utils';
3
+ export interface ClientThemeProviderProps {
4
+ children: React.ReactNode;
5
+ theme: Theme;
6
+ themeDark?: Theme;
7
+ /**
8
+ * The theme the server resolved from the `theme` cookie for the first
9
+ * render. Omit in client-only apps and resolve it before mounting instead.
10
+ */
11
+ $initial?: "light" | "dark";
12
+ /**
13
+ * Keeps a `theme-color` meta tag in sync with the active theme so browser
14
+ * chrome (e.g. the iOS Safari status bar) matches the page. Pass the name
15
+ * of the theme color to track, or false to disable. A key (not a function)
16
+ * so it stays serializable across the server/client component boundary.
17
+ * Defaults to "light".
18
+ */
19
+ $themeColor?: keyof Theme["colors"] | false;
20
+ /**
21
+ * Renders Cherry's GlobalStyles for the active theme. Pass false when the
22
+ * app provides its own global styles.
23
+ */
24
+ $globalStyles?: boolean;
25
+ }
26
+ /**
27
+ * SSR-aware theme provider. Renders with the server-resolved theme on the
28
+ * first paint (no dark-mode flash), reconciles it against the `theme` cookie
29
+ * and OS preference on mount (Safari/Firefox don't send client hints, so
30
+ * their first server render can guess wrong), and exposes setTheme /
31
+ * toggleTheme through ThemeContext. Theme changes persist to the `theme`
32
+ * cookie and localStorage; no API route is needed.
33
+ */
34
+ declare function ClientThemeProvider({ children, theme, themeDark, $initial, $themeColor, $globalStyles, }: ClientThemeProviderProps): React.JSX.Element;
35
+ export { ClientThemeProvider };
@@ -0,0 +1,103 @@
1
+ "use client";
2
+ "use client";
3
+ import { GlobalStyles } from "../utils/global.js";
4
+ import { ThemeContext } from "./theme-provider.js";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { useEffect, useMemo, useState } from "react";
7
+ import { ThemeProvider } from "styled-components";
8
+ //#region src/lib/styled-components/client-theme-provider.tsx
9
+ var THEME_COOKIE_MAX_AGE = 3600 * 24 * 365;
10
+ function readThemeCookie() {
11
+ const cookie = document.cookie.split(";").map((c) => c.trim()).find((c) => c.startsWith("theme="));
12
+ const value = cookie ? cookie.split("=")[1] : void 0;
13
+ return value === "dark" ? "dark" : value === "light" ? "light" : void 0;
14
+ }
15
+ function persistTheme(value) {
16
+ document.cookie = `theme=${value};path=/;max-age=${THEME_COOKIE_MAX_AGE};SameSite=Lax`;
17
+ try {
18
+ if (typeof localStorage !== "undefined") localStorage.theme = value;
19
+ } catch {}
20
+ }
21
+ function removeThemeInitStyle() {
22
+ const el = document.getElementById("__theme-init");
23
+ if (el) el.remove();
24
+ }
25
+ /**
26
+ * SSR-aware theme provider. Renders with the server-resolved theme on the
27
+ * first paint (no dark-mode flash), reconciles it against the `theme` cookie
28
+ * and OS preference on mount (Safari/Firefox don't send client hints, so
29
+ * their first server render can guess wrong), and exposes setTheme /
30
+ * toggleTheme through ThemeContext. Theme changes persist to the `theme`
31
+ * cookie and localStorage; no API route is needed.
32
+ */ function ClientThemeProvider({ children, theme, themeDark, $initial = "light", $themeColor = "light", $globalStyles = true }) {
33
+ const initialTheme = $initial === "dark" && themeDark ? themeDark : theme;
34
+ const [overrideTheme, setOverrideTheme] = useState(null);
35
+ const effectiveTheme = useMemo(() => overrideTheme ?? initialTheme, [overrideTheme, initialTheme]);
36
+ useEffect(() => {
37
+ if (!themeDark) {
38
+ removeThemeInitStyle();
39
+ return;
40
+ }
41
+ try {
42
+ let cookieValue = readThemeCookie();
43
+ if (!cookieValue && typeof localStorage !== "undefined") {
44
+ const stored = localStorage.theme;
45
+ if (stored === "dark" || stored === "light") {
46
+ persistTheme(stored);
47
+ cookieValue = stored;
48
+ }
49
+ }
50
+ if (!cookieValue) {
51
+ cookieValue = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
52
+ persistTheme(cookieValue);
53
+ }
54
+ const wantDark = cookieValue === "dark";
55
+ if (initialTheme.isDark !== wantDark) {
56
+ setOverrideTheme(wantDark ? themeDark : theme);
57
+ return;
58
+ }
59
+ } catch {}
60
+ removeThemeInitStyle();
61
+ }, []);
62
+ useEffect(() => {
63
+ if (overrideTheme) removeThemeInitStyle();
64
+ }, [overrideTheme]);
65
+ useEffect(() => {
66
+ document.documentElement.classList.toggle("dark", effectiveTheme.isDark);
67
+ }, [effectiveTheme.isDark]);
68
+ useEffect(() => {
69
+ if (!$themeColor) return;
70
+ const background = effectiveTheme.colors[$themeColor];
71
+ let meta = document.querySelector("meta[name=\"theme-color\"]");
72
+ if (!meta) {
73
+ meta = document.createElement("meta");
74
+ meta.name = "theme-color";
75
+ document.head.appendChild(meta);
76
+ }
77
+ meta.removeAttribute("media");
78
+ meta.content = background;
79
+ document.querySelectorAll("meta[name=\"theme-color\"][media]").forEach((m) => m.remove());
80
+ }, [effectiveTheme, $themeColor]);
81
+ const setTheme = (action) => {
82
+ const next = typeof action === "function" ? action(effectiveTheme) : action;
83
+ setOverrideTheme(next);
84
+ persistTheme(next.isDark ? "dark" : "light");
85
+ };
86
+ const toggleTheme = () => {
87
+ if (!themeDark) return;
88
+ setTheme(effectiveTheme.isDark ? theme : themeDark);
89
+ };
90
+ const GlobalStylesComponent = $globalStyles ? GlobalStyles(effectiveTheme) : null;
91
+ return /*#__PURE__*/ jsx(ThemeProvider, {
92
+ theme: effectiveTheme,
93
+ children: /*#__PURE__*/ jsxs(ThemeContext.Provider, {
94
+ value: {
95
+ setTheme,
96
+ toggleTheme
97
+ },
98
+ children: [GlobalStylesComponent && /*#__PURE__*/ jsx(GlobalStylesComponent, {}), children]
99
+ })
100
+ });
101
+ }
102
+ //#endregion
103
+ export { ClientThemeProvider };
@@ -1,2 +1,4 @@
1
+ export * from './client-theme-provider';
1
2
  export * from './registry';
3
+ export * from './theme-init';
2
4
  export * from './theme-provider';
@@ -2,8 +2,8 @@
2
2
  "use client";
3
3
  import { Fragment, jsx } from "react/jsx-runtime";
4
4
  import { useState } from "react";
5
- import { useServerInsertedHTML } from "next/navigation";
6
5
  import { ServerStyleSheet, StyleSheetManager } from "styled-components";
6
+ import { useServerInsertedHTML } from "next/navigation";
7
7
  //#region src/lib/styled-components/registry.tsx
8
8
  function StyledComponentsRegistry({ children }) {
9
9
  const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());
@@ -0,0 +1,19 @@
1
+ import { Theme } from '../utils';
2
+ /**
3
+ * Blocking script for the document <head>. On a first visit from a browser
4
+ * that doesn't send Sec-CH-Prefers-Color-Scheme (Safari, Firefox), it seeds
5
+ * the `theme` cookie from the OS preference and, when dark, hides the body
6
+ * behind a `#__theme-init` style so the light server render never flashes.
7
+ * ClientThemeProvider removes that style once the corrected theme has
8
+ * committed on the client. Pass your dark theme's page background so the
9
+ * brief pre-hydration frame matches it.
10
+ */
11
+ export declare function createThemeInitScript(darkBackground?: string): string;
12
+ /** createThemeInitScript() with the default dark background (#000). */
13
+ export declare const themeInitScript: string;
14
+ /**
15
+ * Resolves a `theme` cookie value ("light" | "dark") to a theme object.
16
+ * Meant for server code: read the cookie, resolve, pass the result to
17
+ * ClientThemeProvider via $initial (or use it for viewport theme-color).
18
+ */
19
+ export declare function resolveTheme(cookieValue: string | undefined, theme: Theme, themeDark?: Theme): Theme;
@@ -0,0 +1,22 @@
1
+ //#region src/lib/styled-components/theme-init.ts
2
+ /**
3
+ * Blocking script for the document <head>. On a first visit from a browser
4
+ * that doesn't send Sec-CH-Prefers-Color-Scheme (Safari, Firefox), it seeds
5
+ * the `theme` cookie from the OS preference and, when dark, hides the body
6
+ * behind a `#__theme-init` style so the light server render never flashes.
7
+ * ClientThemeProvider removes that style once the corrected theme has
8
+ * committed on the client. Pass your dark theme's page background so the
9
+ * brief pre-hydration frame matches it.
10
+ */ function createThemeInitScript(darkBackground = "#000") {
11
+ return `(function(){try{var c=document.cookie.split(";").find(function(s){return s.trim().startsWith("theme=")});if(!c){var d=window.matchMedia&&window.matchMedia("(prefers-color-scheme:dark)").matches;document.cookie="theme="+(d?"dark":"light")+";path=/;max-age=31536000;SameSite=Lax";if(d){var s=document.createElement("style");s.id="__theme-init";s.textContent="html{background:${darkBackground}!important;color-scheme:dark}body{visibility:hidden}";document.head.appendChild(s)}}}catch(e){}})();`;
12
+ }
13
+ /** createThemeInitScript() with the default dark background (#000). */ var themeInitScript = createThemeInitScript();
14
+ /**
15
+ * Resolves a `theme` cookie value ("light" | "dark") to a theme object.
16
+ * Meant for server code: read the cookie, resolve, pass the result to
17
+ * ClientThemeProvider via $initial (or use it for viewport theme-color).
18
+ */ function resolveTheme(cookieValue, theme, themeDark) {
19
+ return cookieValue === "dark" && themeDark ? themeDark : theme;
20
+ }
21
+ //#endregion
22
+ export { createThemeInitScript, resolveTheme, themeInitScript };
@@ -2,6 +2,7 @@ import { default as React } from 'react';
2
2
  import { Theme } from '../utils';
3
3
  interface ThemeContextProps {
4
4
  setTheme: React.Dispatch<React.SetStateAction<Theme>>;
5
+ toggleTheme: () => void;
5
6
  }
6
7
  export declare const ThemeContext: React.Context<ThemeContextProps>;
7
8
  declare function CherryThemeProvider({ children, theme, themeDark, }: {
@@ -5,7 +5,10 @@ import { jsx, jsxs } from "react/jsx-runtime";
5
5
  import { createContext, useEffect, useState } from "react";
6
6
  import { ThemeProvider } from "styled-components";
7
7
  //#region src/lib/styled-components/theme-provider.tsx
8
- var ThemeContext = /*#__PURE__*/ createContext({ setTheme: () => {} });
8
+ var ThemeContext = /*#__PURE__*/ createContext({
9
+ setTheme: () => {},
10
+ toggleTheme: () => {}
11
+ });
9
12
  function CherryThemeProvider({ children, theme, themeDark }) {
10
13
  const [currentTheme, setTheme] = useState(theme);
11
14
  useEffect(() => {
@@ -18,11 +21,21 @@ function CherryThemeProvider({ children, theme, themeDark }) {
18
21
  setTheme(theme);
19
22
  }
20
23
  }, [theme, themeDark]);
24
+ const toggleTheme = () => {
25
+ if (!themeDark) return;
26
+ const next = currentTheme.isDark ? theme : themeDark;
27
+ setTheme(next);
28
+ localStorage.theme = next.isDark ? "dark" : "light";
29
+ document.documentElement.classList.toggle("dark", next.isDark);
30
+ };
21
31
  const GlobalStylesComponent = GlobalStyles(currentTheme);
22
32
  return /*#__PURE__*/ jsx(ThemeProvider, {
23
33
  theme: currentTheme,
24
34
  children: /*#__PURE__*/ jsxs(ThemeContext.Provider, {
25
- value: { setTheme },
35
+ value: {
36
+ setTheme,
37
+ toggleTheme
38
+ },
26
39
  children: [/*#__PURE__*/ jsx(GlobalStylesComponent, {}), children]
27
40
  })
28
41
  });
@@ -0,0 +1,6 @@
1
+ import { default as React } from 'react';
2
+ export interface ThemeToggleProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
3
+ $hidden?: boolean;
4
+ }
5
+ declare const ThemeToggle: React.ForwardRefExoticComponent<ThemeToggleProps & React.RefAttributes<HTMLButtonElement>>;
6
+ export { ThemeToggle };
@@ -0,0 +1,42 @@
1
+ "use client";
2
+ "use client";
3
+ import { resetButton } from "./utils/mixins.js";
4
+ import { ThemeContext } from "./styled-components/theme-provider.js";
5
+ import { Icon } from "./icon.js";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { forwardRef, useContext } from "react";
8
+ import styled, { css } from "styled-components";
9
+ import { rgba } from "polished";
10
+ //#region src/lib/theme-toggle.tsx
11
+ var StyledThemeToggle = styled.button.withConfig({
12
+ displayName: "theme-toggle__StyledThemeToggle",
13
+ componentId: "sc-cc0c1ca5-0"
14
+ })([
15
+ ``,
16
+ ` width:56px;height:30px;border-radius:30px;display:flex;position:relative;margin:auto 0;transform:scale(1);background:`,
17
+ `;border:1px solid `,
18
+ `;transition:all 0.3s ease;&::after{content:"";position:absolute;top:2px;left:2px;width:24px;height:24px;border-radius:50%;background:`,
19
+ `;transition:all 0.3s ease;z-index:1;`,
20
+ `}`,
21
+ ` & svg{width:16px;height:16px;object-fit:contain;margin:auto;transition:all 0.3s ease;position:relative;z-index:2;}& .lucide-sun{transform:translateX(1px);}& svg[stroke]{stroke:`,
22
+ `;}&:hover{transform:scale(1.05);color:`,
23
+ `;& svg[stroke]{stroke:`,
24
+ `;}}&:active{transform:scale(0.97);}`
25
+ ], resetButton, ({ theme }) => theme.colors.light, ({ theme }) => theme.colors.grayLight, ({ theme }) => rgba(theme.colors.primaryLight, .2), ({ theme }) => theme.isDark && css([`transform:translateX(26px);`]), ({ $hidden }) => $hidden && css([`display:none;`]), ({ theme }) => theme.colors.primary, ({ theme }) => theme.isDark ? theme.colors.primaryLight : theme.colors.primaryDark, ({ theme }) => theme.isDark ? theme.colors.primaryLight : theme.colors.primaryDark);
26
+ function LocalThemeToggle({ onClick, ...props }, ref) {
27
+ const { toggleTheme } = useContext(ThemeContext);
28
+ return /*#__PURE__*/ jsxs(StyledThemeToggle, {
29
+ type: "button",
30
+ "aria-label": "Toggle Theme",
31
+ ...props,
32
+ onClick: (event) => {
33
+ toggleTheme();
34
+ onClick?.(event);
35
+ },
36
+ ref,
37
+ children: [/*#__PURE__*/ jsx(Icon, { name: "Sun" }), /*#__PURE__*/ jsx(Icon, { name: "MoonStar" })]
38
+ });
39
+ }
40
+ var ThemeToggle = /*#__PURE__*/ forwardRef(LocalThemeToggle);
41
+ //#endregion
42
+ export { ThemeToggle };
package/dist/toast.d.ts CHANGED
@@ -1,23 +1,26 @@
1
1
  import { default as React } from 'react';
2
2
  import { Theme } from './utils';
3
3
  export type ToastColor = "info" | "success" | "error" | "warning";
4
+ export type ToastAlign = "center" | "left" | "right";
4
5
  export interface ToastConfig {
5
6
  color?: ToastColor;
6
7
  autoHide?: number;
7
8
  }
8
9
  type Toast = {
10
+ id: number;
9
11
  text: string;
10
- status: "hidden" | "visible";
11
12
  color: ToastColor;
12
13
  autoHide: number;
13
14
  };
14
15
  declare const ToastNotificationsContext: React.Context<{
15
16
  notifications: Toast[];
16
17
  addNotification: (text: string, config?: ToastConfig) => void;
18
+ removeNotification: (id: number) => void;
17
19
  }>;
18
20
  declare function useToastNotifications(): {
19
21
  notifications: Toast[];
20
22
  addNotification: (text: string, config?: ToastConfig) => void;
23
+ removeNotification: (id: number) => void;
21
24
  };
22
25
  interface ToastNotificationsProviderProps {
23
26
  children: React.ReactNode;
@@ -25,15 +28,30 @@ interface ToastNotificationsProviderProps {
25
28
  declare function ToastNotificationsProvider({ children, }: ToastNotificationsProviderProps): React.JSX.Element;
26
29
  export declare const StyledNotifications: import('styled-components/dist/types').IStyledComponentBase<"web", import('styled-components').FastOmit<import('styled-components').FastOmit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLUListElement>, HTMLUListElement>, "theme" | "$align" | "$bottom"> & {
27
30
  theme: Theme;
28
- $align?: "center" | "left" | "right";
31
+ $align?: ToastAlign;
29
32
  $bottom?: boolean;
30
33
  }, never> & Partial<Pick<import('styled-components').FastOmit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLUListElement>, HTMLUListElement>, "theme" | "$align" | "$bottom"> & {
31
34
  theme: Theme;
32
- $align?: "center" | "left" | "right";
35
+ $align?: ToastAlign;
36
+ $bottom?: boolean;
37
+ }, never>>> & string;
38
+ export declare const StyledNotificationItem: import('styled-components/dist/types').IStyledComponentBase<"web", import('styled-components').FastOmit<import('styled-components').FastOmit<React.DetailedHTMLProps<React.LiHTMLAttributes<HTMLLIElement>, HTMLLIElement>, "theme" | "$align" | "$bottom" | "$color" | "$visible" | "$static"> & {
39
+ theme: Theme;
40
+ $color: ToastColor;
41
+ $visible: boolean;
42
+ $static: boolean;
43
+ $align?: ToastAlign;
44
+ $bottom?: boolean;
45
+ }, never> & Partial<Pick<import('styled-components').FastOmit<React.DetailedHTMLProps<React.LiHTMLAttributes<HTMLLIElement>, HTMLLIElement>, "theme" | "$align" | "$bottom" | "$color" | "$visible" | "$static"> & {
46
+ theme: Theme;
47
+ $color: ToastColor;
48
+ $visible: boolean;
49
+ $static: boolean;
50
+ $align?: ToastAlign;
33
51
  $bottom?: boolean;
34
52
  }, never>>> & string;
35
53
  export interface ToastNotificationsProps {
36
- $align?: "center" | "left" | "right";
54
+ $align?: ToastAlign;
37
55
  $bottom?: boolean;
38
56
  }
39
57
  declare function ToastNotifications({ $align, $bottom, }: ToastNotificationsProps): React.JSX.Element;
package/dist/toast.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import { Icon } from "./icon.js";
4
4
  import { IconButton } from "./icon-button.js";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
- import { createContext, useContext, useEffect, useState } from "react";
6
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
7
7
  import styled, { css } from "styled-components";
8
8
  //#region src/lib/toast.tsx
9
9
  var statusIcons = {
@@ -12,48 +12,62 @@ var statusIcons = {
12
12
  warning: "TriangleAlert",
13
13
  info: "Info"
14
14
  };
15
+ var EXIT_DURATION_MS = 400;
15
16
  var ToastNotificationsContext = /*#__PURE__*/ createContext({
16
17
  notifications: [],
17
- addNotification: () => null
18
+ addNotification: () => null,
19
+ removeNotification: () => null
18
20
  });
19
21
  function useToastNotifications() {
20
22
  return useContext(ToastNotificationsContext);
21
23
  }
22
24
  function ToastNotificationsProvider({ children }) {
23
25
  const [notifications, setNotifications] = useState([]);
24
- const addNotification = (text, config) => {
26
+ const nextId = useRef(0);
27
+ const addNotification = useCallback((text, config) => {
28
+ const id = nextId.current++;
25
29
  setNotifications((prev) => [...prev, {
30
+ id,
26
31
  text,
27
- status: "hidden",
28
32
  color: config?.color || "info",
29
33
  autoHide: config?.autoHide || 0
30
34
  }]);
31
- setTimeout(() => {
32
- setNotifications((prev) => prev.map((toast) => toast.status === "hidden" ? {
33
- ...toast,
34
- status: "visible"
35
- } : toast));
36
- }, 50);
37
- };
35
+ }, []);
36
+ const removeNotification = useCallback((id) => {
37
+ setNotifications((prev) => prev.filter((toast) => toast.id !== id));
38
+ }, []);
39
+ const value = useMemo(() => ({
40
+ notifications,
41
+ addNotification,
42
+ removeNotification
43
+ }), [
44
+ notifications,
45
+ addNotification,
46
+ removeNotification
47
+ ]);
38
48
  return /*#__PURE__*/ jsx(ToastNotificationsContext.Provider, {
39
- value: {
40
- notifications,
41
- addNotification
42
- },
49
+ value,
43
50
  children
44
51
  });
45
52
  }
46
53
  var StyledNotifications = styled.ul.withConfig({
47
54
  displayName: "toast__StyledNotifications",
48
- componentId: "sc-1dfe4a8-0"
55
+ componentId: "sc-1f36b7f7-0"
49
56
  })([
50
57
  `position:fixed;z-index:99999;margin:0;padding:0;list-style:none;`,
51
58
  ` `,
52
59
  ` `,
53
60
  ` `,
54
- ` & li{justify-content:center;display:flex;margin:0;opacity:0;pointer-events:none;transform:translateY(-20px);padding:0;max-height:0;transition:opacity 0.2s ease,transform 0.2s ease,max-height 0.25s ease 0.15s,margin 0.25s ease 0.15s;`,
61
+ ``
62
+ ], ({ $align }) => $align === "center" && css([`left:50%;transform:translateX(-50%);`]), ({ $align }) => $align === "right" && css([`right:20px;`]), ({ $align }) => $align === "left" && css([`left:20px;`]), ({ $bottom }) => $bottom ? css([`bottom:20px;`]) : css([`top:20px;`]));
63
+ var StyledNotificationItem = styled.li.withConfig({
64
+ displayName: "toast__StyledNotificationItem",
65
+ componentId: "sc-1f36b7f7-1"
66
+ })([
67
+ `display:grid;grid-template-rows:0fr;justify-items:center;margin:0;padding:0;opacity:0;pointer-events:none;transform:translateY(`,
68
+ `);transition:opacity 0.2s ease,transform 0.2s ease,grid-template-rows 0.25s ease 0.15s,margin 0.25s ease 0.15s;`,
55
69
  ` `,
56
- ` & .item{display:inline-flex;align-items:center;gap:10px;max-width:min(420px,calc(100vw - 40px));padding:10px 10px 10px 18px;margin:0;border-radius:`,
70
+ ` & .item-wrapper{min-height:0;}& .item{display:inline-flex;align-items:center;gap:10px;max-width:min(420px,calc(100vw - 40px));padding:10px 10px 10px 18px;margin:0;border-radius:`,
57
71
  `;background:`,
58
72
  `;border:solid 1px `,
59
73
  `;box-shadow:`,
@@ -61,55 +75,80 @@ var StyledNotifications = styled.ul.withConfig({
61
75
  `;font-size:`,
62
76
  `;line-height:`,
63
77
  `;font-weight:500;& .status-icon{display:inline-flex;flex-shrink:0;& svg{width:20px;height:20px;color:`,
64
- `;}}& .message{flex:1;min-width:0;line-height:1.5;overflow-wrap:anywhere;}& .close-button{flex-shrink:0;}}&.error{& .item .status-icon svg{color:`,
65
- `;}}&.success{& .item .status-icon svg{color:`,
66
- `;}}&.warning{& .item .status-icon svg{color:`,
67
- `;}}&.info{& .item .status-icon svg{color:`,
68
- `;}}&.visible{opacity:1;pointer-events:all;transform:translateY(0);max-height:320px;transition:max-height 0.25s ease,margin 0.25s ease,opacity 0.2s ease 0.15s,transform 0.2s ease 0.15s;`,
69
- `}&.static{position:relative;z-index:10;}}`
70
- ], ({ $align }) => $align === "center" && css([`left:50%;transform:translateX(-50%);`]), ({ $align }) => $align === "right" && css([`right:20px;`]), ({ $align }) => $align === "left" && css([`left:20px;`]), ({ $bottom }) => $bottom ? css([`bottom:20px;`]) : css([`top:20px;`]), ({ $align }) => $align === "right" && css([`justify-content:flex-end;`]), ({ $align }) => $align === "left" && css([`justify-content:flex-start;`]), ({ theme }) => theme.spacing.radius.xl, ({ theme }) => theme.colors.light, ({ theme }) => theme.colors.grayLight, ({ theme }) => theme.isDark ? theme.shadows.sm : theme.shadows.xs, ({ theme }) => theme.colors.dark, ({ theme }) => theme.fontSizes.small.lg, ({ theme }) => theme.lineHeights.small.lg, ({ theme }) => theme.colors.info, ({ theme }) => theme.colors.error, ({ theme }) => theme.colors.success, ({ theme }) => theme.colors.warning, ({ theme }) => theme.colors.info, ({ $bottom }) => $bottom ? css([`margin-top:10px;`]) : css([`margin-bottom:10px;`]));
78
+ `;}}& .message{flex:1;min-width:0;line-height:1.5;overflow-wrap:anywhere;}& .close-button{flex-shrink:0;}}`,
79
+ ` `,
80
+ ``
81
+ ], ({ $bottom }) => $bottom ? "20px" : "-20px", ({ $align }) => $align === "right" && css([`justify-items:end;`]), ({ $align }) => $align === "left" && css([`justify-items:start;`]), ({ theme }) => theme.spacing.radius.xl, ({ theme }) => theme.colors.light, ({ theme }) => theme.colors.grayLight, ({ theme }) => theme.isDark ? theme.shadows.sm : theme.shadows.xs, ({ theme }) => theme.colors.dark, ({ theme }) => theme.fontSizes.small.lg, ({ theme }) => theme.lineHeights.small.lg, ({ theme, $color }) => theme.colors[$color], ({ $visible, $bottom }) => $visible && css([`opacity:1;pointer-events:auto;transform:translateY(0);grid-template-rows:1fr;transition:grid-template-rows 0.25s ease,margin 0.25s ease,opacity 0.2s ease 0.15s,transform 0.2s ease 0.15s;`, ``], $bottom ? css([`margin-top:10px;`]) : css([`margin-bottom:10px;`])), ({ $static }) => $static && css([`position:relative;z-index:10;`]));
71
82
  function ToastNotifications({ $align = "center", $bottom }) {
72
83
  const { notifications } = useToastNotifications();
73
84
  return /*#__PURE__*/ jsx(StyledNotifications, {
74
85
  $align,
75
86
  $bottom,
76
- children: notifications.map((notification, i) => /*#__PURE__*/ jsx(NotificationItem, { ...notification }, i))
87
+ "aria-live": "polite",
88
+ children: notifications.map((notification) => /*#__PURE__*/ jsx(NotificationItem, {
89
+ notification,
90
+ $align,
91
+ $bottom
92
+ }, notification.id))
77
93
  });
78
94
  }
79
- function NotificationItem(notification) {
80
- const [visible, setVisible] = useState(true);
95
+ function NotificationItem({ notification, $align, $bottom }) {
96
+ const { removeNotification } = useToastNotifications();
97
+ const [phase, setPhase] = useState("entering");
98
+ useEffect(() => {
99
+ let inner;
100
+ const outer = requestAnimationFrame(() => {
101
+ inner = requestAnimationFrame(() => setPhase((prev) => prev === "entering" ? "visible" : prev));
102
+ });
103
+ return () => {
104
+ cancelAnimationFrame(outer);
105
+ cancelAnimationFrame(inner);
106
+ };
107
+ }, []);
81
108
  useEffect(() => {
82
109
  if (!notification.autoHide) return;
83
- const timeout = setTimeout(() => setVisible(false), notification.autoHide);
110
+ const timeout = setTimeout(() => setPhase("exiting"), notification.autoHide);
84
111
  return () => clearTimeout(timeout);
85
112
  }, [notification.autoHide]);
86
- return /*#__PURE__*/ jsx("li", {
87
- className: [
88
- visible ? notification.status : "hidden",
89
- notification.color,
90
- !notification.autoHide && "static"
91
- ].filter(Boolean).join(" "),
92
- children: /*#__PURE__*/ jsxs("span", {
93
- className: "item",
94
- children: [
95
- /*#__PURE__*/ jsx("span", {
96
- className: "status-icon",
97
- children: /*#__PURE__*/ jsx(Icon, { name: statusIcons[notification.color] || statusIcons.info })
98
- }),
99
- /*#__PURE__*/ jsx("span", {
100
- className: "message",
101
- children: notification.text
102
- }),
103
- /*#__PURE__*/ jsx(IconButton, {
104
- $size: "small",
105
- className: "close-button",
106
- "aria-label": "Dismiss notification",
107
- onClick: () => setVisible(false),
108
- children: /*#__PURE__*/ jsx(Icon, { name: "X" })
109
- })
110
- ]
113
+ useEffect(() => {
114
+ if (phase !== "exiting") return;
115
+ const timeout = setTimeout(() => removeNotification(notification.id), EXIT_DURATION_MS);
116
+ return () => clearTimeout(timeout);
117
+ }, [
118
+ phase,
119
+ notification.id,
120
+ removeNotification
121
+ ]);
122
+ return /*#__PURE__*/ jsx(StyledNotificationItem, {
123
+ $visible: phase === "visible",
124
+ $color: notification.color,
125
+ $static: !notification.autoHide,
126
+ $align,
127
+ $bottom,
128
+ children: /*#__PURE__*/ jsx("span", {
129
+ className: "item-wrapper",
130
+ children: /*#__PURE__*/ jsxs("span", {
131
+ className: "item",
132
+ children: [
133
+ /*#__PURE__*/ jsx("span", {
134
+ className: "status-icon",
135
+ children: /*#__PURE__*/ jsx(Icon, { name: statusIcons[notification.color] || statusIcons.info })
136
+ }),
137
+ /*#__PURE__*/ jsx("span", {
138
+ className: "message",
139
+ children: notification.text
140
+ }),
141
+ /*#__PURE__*/ jsx(IconButton, {
142
+ $size: "small",
143
+ className: "close-button",
144
+ "aria-label": "Dismiss notification",
145
+ onClick: () => setPhase("exiting"),
146
+ children: /*#__PURE__*/ jsx(Icon, { name: "X" })
147
+ })
148
+ ]
149
+ })
111
150
  })
112
151
  });
113
152
  }
114
153
  //#endregion
115
- export { StyledNotifications, ToastNotifications, ToastNotificationsContext, ToastNotificationsProvider, useToastNotifications };
154
+ export { StyledNotificationItem, StyledNotifications, ToastNotifications, ToastNotificationsContext, ToastNotificationsProvider, useToastNotifications };
@@ -1,4 +1,21 @@
1
1
  import { Breakpoints, Theme } from './theme';
2
+ /**
3
+ * Hover/focus/active affordance for interactive surfaces (cards, tiles,
4
+ * links-as-blocks): a transparent 1px border that picks up the primary color
5
+ * on hover, with a soft focus ring. Pair with resetButton for clickable
6
+ * elements that aren't Buttons.
7
+ */
8
+ export declare const interactiveStyles: import('styled-components').RuleSet<NoInfer<{
9
+ theme: Theme;
10
+ }>>;
11
+ /**
12
+ * The destructive-action sibling of interactiveStyles: identical
13
+ * hover/focus/active border + ring behavior, but in the theme error red (the
14
+ * same red the $error Button uses). Use for delete/remove affordances.
15
+ */
16
+ export declare const errorInteractiveStyles: import('styled-components').RuleSet<NoInfer<{
17
+ theme: Theme;
18
+ }>>;
2
19
  export declare const resetButton: import('styled-components').RuleSet<object>;
3
20
  export declare const resetInput: import('styled-components').RuleSet<object>;
4
21
  export declare const fullWidthStyles: (fullWidth?: boolean) => import('styled-components').RuleSet<object> | undefined;
@@ -2,7 +2,33 @@
2
2
  "use client";
3
3
  import { mq } from "./theme.js";
4
4
  import { css } from "styled-components";
5
+ import { rgba } from "polished";
5
6
  //#region src/lib/utils/mixins.tsx
7
+ /**
8
+ * Hover/focus/active affordance for interactive surfaces (cards, tiles,
9
+ * links-as-blocks): a transparent 1px border that picks up the primary color
10
+ * on hover, with a soft focus ring. Pair with resetButton for clickable
11
+ * elements that aren't Buttons.
12
+ */ var interactiveStyles = css([
13
+ `transition:all 0.3s ease;border:solid 1px transparent;box-shadow:0 0 0 0px `,
14
+ `;@media (hover:hover){&:hover{border-color:`,
15
+ `;}}&:focus{border-color:`,
16
+ `;box-shadow:0 0 0 4px `,
17
+ `;}&:active{box-shadow:0 0 0 2px `,
18
+ `;}`
19
+ ], ({ theme }) => theme.colors.primary, ({ theme }) => theme.colors.primary, ({ theme }) => theme.colors.primary, ({ theme }) => theme.colors.primaryLight, ({ theme }) => theme.colors.primaryLight);
20
+ /**
21
+ * The destructive-action sibling of interactiveStyles: identical
22
+ * hover/focus/active border + ring behavior, but in the theme error red (the
23
+ * same red the $error Button uses). Use for delete/remove affordances.
24
+ */ var errorInteractiveStyles = css([
25
+ `transition:all 0.3s ease;border:solid 1px transparent;box-shadow:0 0 0 0px `,
26
+ `;@media (hover:hover){&:hover{border-color:`,
27
+ `;}}&:focus{border-color:`,
28
+ `;box-shadow:0 0 0 4px `,
29
+ `;}&:active{box-shadow:0 0 0 2px `,
30
+ `;}`
31
+ ], ({ theme }) => theme.colors.error, ({ theme }) => theme.colors.error, ({ theme }) => theme.colors.error, ({ theme }) => rgba(theme.colors.error, .3), ({ theme }) => rgba(theme.colors.error, .3));
6
32
  var resetButton = css([`box-sizing:border-box;appearance:none;border:none;background:none;padding:0;margin:0;cursor:pointer;outline:none;`]);
7
33
  var resetInput = css([`cursor:text;min-width:100px;`]);
8
34
  var fullWidthStyles = (fullWidth) => {
@@ -58,4 +84,4 @@ var generateDirectionStyles = (size, direction) => css([
58
84
  `;}`
59
85
  ], mq(size), direction && `${direction}`);
60
86
  //#endregion
61
- export { formElementHeightStyles, fullWidthStyles, generateAlignContentStyles, generateAlignItemsStyles, generateColSpanStyles, generateColsStyles, generateDirectionStyles, generateGapStyles, generateJustifyContentStyles, generatePaddingStyles, resetButton, resetInput, statusBorderStyles };
87
+ export { errorInteractiveStyles, formElementHeightStyles, fullWidthStyles, generateAlignContentStyles, generateAlignItemsStyles, generateColSpanStyles, generateColsStyles, generateDirectionStyles, generateGapStyles, generateJustifyContentStyles, generatePaddingStyles, interactiveStyles, resetButton, resetInput, statusBorderStyles };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cherry-styled-components",
3
- "version": "0.1.19",
3
+ "version": "0.2.1",
4
4
  "description": "Cherry is a design system for the modern web. Designed in Figma, built in React using Typescript.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -38,7 +38,8 @@
38
38
  "scripts": {
39
39
  "dev": "vite dev",
40
40
  "build": "vite build",
41
- "format": "prettier --write ."
41
+ "format": "prettier --write .",
42
+ "screenshots": "node scripts/screenshot-previews.cjs"
42
43
  },
43
44
  "dependencies": {
44
45
  "lucide-react": "^1.23.0",
@@ -58,10 +59,12 @@
58
59
  "eslint-plugin-react-hooks": "^7.1.1",
59
60
  "eslint-plugin-react-refresh": "^0.5.3",
60
61
  "next": "^16.2.10",
62
+ "playwright-core": "^1.61.1",
61
63
  "prettier": "^3.9.4",
62
64
  "react": "^19.2.7",
63
65
  "react-dom": "^19.2.7",
64
66
  "rollup-plugin-preserve-directives": "^0.4.0",
67
+ "sharp": "^0.35.3",
65
68
  "styled-components": "^6.4.3",
66
69
  "typescript": "^6.0.3",
67
70
  "vite": "^8.1.3",