cherry-styled-components 0.1.18 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 };
@@ -0,0 +1,40 @@
1
+ import { default as React } from 'react';
2
+ import { Theme } from './utils';
3
+ export type ToastColor = "info" | "success" | "error" | "warning";
4
+ export interface ToastConfig {
5
+ color?: ToastColor;
6
+ autoHide?: number;
7
+ }
8
+ type Toast = {
9
+ text: string;
10
+ status: "hidden" | "visible";
11
+ color: ToastColor;
12
+ autoHide: number;
13
+ };
14
+ declare const ToastNotificationsContext: React.Context<{
15
+ notifications: Toast[];
16
+ addNotification: (text: string, config?: ToastConfig) => void;
17
+ }>;
18
+ declare function useToastNotifications(): {
19
+ notifications: Toast[];
20
+ addNotification: (text: string, config?: ToastConfig) => void;
21
+ };
22
+ interface ToastNotificationsProviderProps {
23
+ children: React.ReactNode;
24
+ }
25
+ declare function ToastNotificationsProvider({ children, }: ToastNotificationsProviderProps): React.JSX.Element;
26
+ 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
+ theme: Theme;
28
+ $align?: "center" | "left" | "right";
29
+ $bottom?: boolean;
30
+ }, never> & Partial<Pick<import('styled-components').FastOmit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLUListElement>, HTMLUListElement>, "theme" | "$align" | "$bottom"> & {
31
+ theme: Theme;
32
+ $align?: "center" | "left" | "right";
33
+ $bottom?: boolean;
34
+ }, never>>> & string;
35
+ export interface ToastNotificationsProps {
36
+ $align?: "center" | "left" | "right";
37
+ $bottom?: boolean;
38
+ }
39
+ declare function ToastNotifications({ $align, $bottom, }: ToastNotificationsProps): React.JSX.Element;
40
+ export { ToastNotifications, ToastNotificationsContext, ToastNotificationsProvider, useToastNotifications, };
package/dist/toast.js ADDED
@@ -0,0 +1,115 @@
1
+ "use client";
2
+ "use client";
3
+ import { Icon } from "./icon.js";
4
+ import { IconButton } from "./icon-button.js";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { createContext, useContext, useEffect, useState } from "react";
7
+ import styled, { css } from "styled-components";
8
+ //#region src/lib/toast.tsx
9
+ var statusIcons = {
10
+ success: "CircleCheck",
11
+ error: "CircleX",
12
+ warning: "TriangleAlert",
13
+ info: "Info"
14
+ };
15
+ var ToastNotificationsContext = /*#__PURE__*/ createContext({
16
+ notifications: [],
17
+ addNotification: () => null
18
+ });
19
+ function useToastNotifications() {
20
+ return useContext(ToastNotificationsContext);
21
+ }
22
+ function ToastNotificationsProvider({ children }) {
23
+ const [notifications, setNotifications] = useState([]);
24
+ const addNotification = (text, config) => {
25
+ setNotifications((prev) => [...prev, {
26
+ text,
27
+ status: "hidden",
28
+ color: config?.color || "info",
29
+ autoHide: config?.autoHide || 0
30
+ }]);
31
+ setTimeout(() => {
32
+ setNotifications((prev) => prev.map((toast) => toast.status === "hidden" ? {
33
+ ...toast,
34
+ status: "visible"
35
+ } : toast));
36
+ }, 50);
37
+ };
38
+ return /*#__PURE__*/ jsx(ToastNotificationsContext.Provider, {
39
+ value: {
40
+ notifications,
41
+ addNotification
42
+ },
43
+ children
44
+ });
45
+ }
46
+ var StyledNotifications = styled.ul.withConfig({
47
+ displayName: "toast__StyledNotifications",
48
+ componentId: "sc-1dfe4a8-0"
49
+ })([
50
+ `position:fixed;z-index:99999;margin:0;padding:0;list-style:none;`,
51
+ ` `,
52
+ ` `,
53
+ ` `,
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;`,
55
+ ` `,
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:`,
57
+ `;background:`,
58
+ `;border:solid 1px `,
59
+ `;box-shadow:`,
60
+ `;color:`,
61
+ `;font-size:`,
62
+ `;line-height:`,
63
+ `;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;`]));
71
+ function ToastNotifications({ $align = "center", $bottom }) {
72
+ const { notifications } = useToastNotifications();
73
+ return /*#__PURE__*/ jsx(StyledNotifications, {
74
+ $align,
75
+ $bottom,
76
+ children: notifications.map((notification, i) => /*#__PURE__*/ jsx(NotificationItem, { ...notification }, i))
77
+ });
78
+ }
79
+ function NotificationItem(notification) {
80
+ const [visible, setVisible] = useState(true);
81
+ useEffect(() => {
82
+ if (!notification.autoHide) return;
83
+ const timeout = setTimeout(() => setVisible(false), notification.autoHide);
84
+ return () => clearTimeout(timeout);
85
+ }, [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
+ ]
111
+ })
112
+ });
113
+ }
114
+ //#endregion
115
+ export { StyledNotifications, ToastNotifications, ToastNotificationsContext, ToastNotificationsProvider, useToastNotifications };
@@ -3,3 +3,4 @@ export * from './icons';
3
3
  export * from './mixins';
4
4
  export * from './theme';
5
5
  export * from './typography';
6
+ export * from './use-on-click-outside';
@@ -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 };
@@ -0,0 +1,2 @@
1
+ import { RefObject } from 'react';
2
+ export declare function useOnClickOutside(refs: RefObject<HTMLElement | null>[], cb: () => void): void;
@@ -0,0 +1,17 @@
1
+ "use client";
2
+ "use client";
3
+ import { useEffect } from "react";
4
+ //#region src/lib/utils/use-on-click-outside.tsx
5
+ function useOnClickOutside(refs, cb) {
6
+ useEffect(() => {
7
+ function handleClickOutside(event) {
8
+ if (refs && refs.map((ref) => ref && ref.current && ref.current.contains(event.target)).every((i) => i === false)) cb();
9
+ }
10
+ document.addEventListener("mousedown", handleClickOutside);
11
+ return () => {
12
+ document.removeEventListener("mousedown", handleClickOutside);
13
+ };
14
+ }, [refs, cb]);
15
+ }
16
+ //#endregion
17
+ export { useOnClickOutside };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cherry-styled-components",
3
- "version": "0.1.18",
3
+ "version": "0.2.0",
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,10 +38,11 @@
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
- "lucide-react": "^1.21.0",
45
+ "lucide-react": "^1.23.0",
45
46
  "polished": "^4.3.1"
46
47
  },
47
48
  "peerDependencies": {
@@ -50,21 +51,23 @@
50
51
  "styled-components": "^6.0.0"
51
52
  },
52
53
  "devDependencies": {
53
- "@swc/plugin-styled-components": "^12.14.0",
54
- "@types/node": "^26.0.1",
54
+ "@swc/plugin-styled-components": "^12.14.1",
55
+ "@types/node": "^26.1.0",
55
56
  "@types/react": "^19.2.17",
56
57
  "@types/react-dom": "^19.2.3",
57
58
  "@vitejs/plugin-react-swc": "^4.3.1",
58
59
  "eslint-plugin-react-hooks": "^7.1.1",
59
60
  "eslint-plugin-react-refresh": "^0.5.3",
60
- "next": "^16.2.9",
61
- "prettier": "^3.9.1",
61
+ "next": "^16.2.10",
62
+ "playwright-core": "^1.61.1",
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
- "vite": "^8.1.0",
70
+ "vite": "^8.1.3",
68
71
  "vite-plugin-dts": "^5.0.3"
69
72
  }
70
73
  }