cherry-styled-components 0.2.15 → 0.2.17

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/README.md CHANGED
@@ -10,7 +10,7 @@ Cherry Design System is a versatile foundation for projects. It offers a white l
10
10
  Install the package along with its peer dependencies:
11
11
 
12
12
  ```bash
13
- npm install cherry-styled-components react react-dom styled-components
13
+ npm install cherry-styled-components react react-dom styled-components lucide-react
14
14
  ```
15
15
 
16
16
  ## Quick Start
@@ -38,6 +38,8 @@ The built-in `theme` and `themeDark` objects are a starting point; both are plai
38
38
 
39
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), interactive components (Accordion, Modal, Toast, ThemeToggle, Icon, IconButton), content components (Prose, Callout, Avatar), and a chat kit (see below). See the [component docs](https://cherry.al/) for the full API.
40
40
 
41
+ Icons come from [lucide](https://lucide.dev/icons/), a peer dependency. Pass an imported component, `<Icon icon={ArrowRight} />`, which ships only that glyph and renders on the server. Component icon props such as Avatar's `$icon` take the same component, or a rendered element. For an icon chosen at runtime, import `DynamicIcon` from `cherry-styled-components/dynamic`: it resolves a name on demand, and only apps that import that subpath carry lucide's dynamic import map.
42
+
41
43
  ## Chat kit
42
44
 
43
45
  Cherry ships everything needed to build an AI-assistant chat UI, split into a headless provider and presentational components. The provider owns panel state, focus management, the transcript, and streaming bookkeeping; **the app owns the transport** — Cherry never fetches, parses SSE, or assumes an endpoint.
@@ -100,11 +102,10 @@ A Next.js App Router setup looks like this:
100
102
  ```tsx
101
103
  // app/layout.tsx (server component)
102
104
  import { cookies } from "next/headers";
103
- import {
104
- ClientThemeProvider,
105
- StyledComponentsRegistry,
106
- themeInitScript,
107
- } from "cherry-styled-components";
105
+ // StyledComponentsRegistry is the one export that depends on Next, so it ships
106
+ // from the `/next` subpath instead of the package root.
107
+ import { StyledComponentsRegistry } from "cherry-styled-components/next";
108
+ import { ClientThemeProvider, themeInitScript } from "cherry-styled-components";
108
109
  import { theme, themeDark } from "./theme";
109
110
 
110
111
  export default async function RootLayout({ children }) {
package/dist/accordion.js CHANGED
@@ -5,10 +5,11 @@ import { Icon } from "./icon.js";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
6
  import { useState } from "react";
7
7
  import styled, { css } from "styled-components";
8
+ import { ChevronDown } from "lucide-react";
8
9
  //#region src/lib/accordion.tsx
9
10
  var StyledAccordion = styled.div.withConfig({
10
11
  displayName: "accordion__StyledAccordion",
11
- componentId: "sc-20594240-0"
12
+ componentId: "sc-bafb2fc7-0"
12
13
  })([
13
14
  `text-align:left;border-radius:`,
14
15
  `;margin:0;`,
@@ -18,7 +19,7 @@ var StyledAccordion = styled.div.withConfig({
18
19
  ], ({ theme, $inline }) => $inline ? "0" : theme.spacing.radius.lg, ({ theme }) => styledText(theme), ({ theme }) => theme.colors.light, ({ theme, $inline }) => !$inline && css([`border:solid 1px `, `;`], theme.colors.grayLight));
19
20
  var StyledAccordionTitle = styled.button.withConfig({
20
21
  displayName: "accordion__StyledAccordionTitle",
21
- componentId: "sc-20594240-1"
22
+ componentId: "sc-bafb2fc7-1"
22
23
  })([
23
24
  `appearance:none;border:none;background:none;font-family:inherit;text-align:left;width:100%;box-sizing:border-box;font-weight:700;cursor:pointer;margin:0;`,
24
25
  `;color:`,
@@ -32,7 +33,7 @@ var StyledAccordionTitle = styled.button.withConfig({
32
33
  ], ({ theme }) => styledText(theme), ({ theme }) => theme.colors.primary, ({ $inline }) => $inline ? "8px 40px 8px 0" : "20px 50px 20px 20px", ({ theme }) => theme.colors.primaryDark, ({ theme }) => theme.spacing.radius.xs, ({ theme }) => theme.colors.primary, ({ $inline }) => $inline ? "0" : "20px", ({ $isOpen }) => $isOpen && css([`transform:translateY(-50%) rotate(180deg);`]));
33
34
  var StyledAccordionContent = styled.div.withConfig({
34
35
  displayName: "accordion__StyledAccordionContent",
35
- componentId: "sc-20594240-2"
36
+ componentId: "sc-bafb2fc7-2"
36
37
  })([
37
38
  `display:grid;grid-template-rows:`,
38
39
  `;visibility:`,
@@ -40,11 +41,11 @@ var StyledAccordionContent = styled.div.withConfig({
40
41
  ], ({ $isOpen }) => $isOpen ? "1fr" : "0fr", ({ $isOpen }) => $isOpen ? "visible" : "hidden");
41
42
  var StyledAccordionInner = styled.div.withConfig({
42
43
  displayName: "accordion__StyledAccordionInner",
43
- componentId: "sc-20594240-3"
44
+ componentId: "sc-bafb2fc7-3"
44
45
  })([`min-height:0;overflow:clip;`]);
45
46
  var StyledAccordionBody = styled.div.withConfig({
46
47
  displayName: "accordion__StyledAccordionBody",
47
- componentId: "sc-20594240-4"
48
+ componentId: "sc-bafb2fc7-4"
48
49
  })([
49
50
  ``,
50
51
  `;color:`,
@@ -74,7 +75,7 @@ function Accordion({ children, title, onToggle, $inline, defaultOpen, open, ...r
74
75
  children: [
75
76
  title,
76
77
  " ",
77
- /*#__PURE__*/ jsx(Icon, { name: "ChevronDown" })
78
+ /*#__PURE__*/ jsx(Icon, { icon: ChevronDown })
78
79
  ]
79
80
  }), /*#__PURE__*/ jsx(StyledAccordionContent, {
80
81
  $isOpen: isOpen,
@@ -1,11 +1,11 @@
1
1
  import { default as React } from 'react';
2
2
  import { DropzoneRejection } from './dropzone';
3
- import { Icon } from './icon';
3
+ import { IconSource } from './icon';
4
4
  export interface AvatarDropzoneProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange" | "children" | "multiple"> {
5
5
  /** Visual scale, following the IconButton size scale. */
6
6
  $size?: "default" | "big" | "small";
7
- /** Lucide icon shown while no picture is selected. */
8
- $icon?: React.ComponentProps<typeof Icon>["name"];
7
+ /** Icon shown while no picture is selected: an imported lucide component, or a rendered element. */
8
+ $icon?: IconSource;
9
9
  /** Maximum file size in bytes. */
10
10
  $maxBytes?: number;
11
11
  /** Accessible label for the drop target. */
@@ -1,11 +1,12 @@
1
1
  "use client";
2
2
  "use client";
3
3
  import { alpha, resetButton } from "./utils/mixins.js";
4
- import { Icon } from "./icon.js";
4
+ import { Icon, renderIcon } from "./icon.js";
5
5
  import { matchesAccept } from "./dropzone.js";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
7
  import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
8
8
  import styled, { css } from "styled-components";
9
+ import { User, X } from "lucide-react";
9
10
  //#region src/lib/avatar-dropzone.tsx
10
11
  var avatarSizes = {
11
12
  small: {
@@ -21,7 +22,7 @@ var avatarSizes = {
21
22
  icon: 28
22
23
  }
23
24
  };
24
- function LocalAvatarDropzone({ $size, $icon = "User", $maxBytes, "aria-label": ariaLabel = "Upload profile picture", onFileChange, onFileRejected, accept = "image/*", disabled, ...rest }, ref) {
25
+ function LocalAvatarDropzone({ $size, $icon = User, $maxBytes, "aria-label": ariaLabel = "Upload profile picture", onFileChange, onFileRejected, accept = "image/*", disabled, ...rest }, ref) {
25
26
  const [previewUrl, setPreviewUrl] = useState(null);
26
27
  const [dragOver, setDragOver] = useState(false);
27
28
  const inputRef = useRef(null);
@@ -87,14 +88,14 @@ function LocalAvatarDropzone({ $size, $icon = "User", $maxBytes, "aria-label": a
87
88
  children: previewUrl ? /*#__PURE__*/ jsx("img", {
88
89
  src: previewUrl,
89
90
  alt: ""
90
- }) : /*#__PURE__*/ jsx(Icon, { name: $icon })
91
+ }) : renderIcon($icon)
91
92
  }),
92
93
  previewUrl && /*#__PURE__*/ jsx(StyledAvatarRemove, {
93
94
  type: "button",
94
95
  "aria-label": "Remove profile picture",
95
96
  onClick: removeFile,
96
97
  disabled,
97
- children: /*#__PURE__*/ jsx(Icon, { name: "X" })
98
+ children: /*#__PURE__*/ jsx(Icon, { icon: X })
98
99
  }),
99
100
  /*#__PURE__*/ jsx("input", {
100
101
  ref: inputRef,
@@ -114,7 +115,7 @@ function LocalAvatarDropzone({ $size, $icon = "User", $maxBytes, "aria-label": a
114
115
  var AvatarDropzone = /*#__PURE__*/ forwardRef(LocalAvatarDropzone);
115
116
  var StyledAvatarDropzoneWrapper = styled.span.withConfig({
116
117
  displayName: "avatar-dropzone__StyledAvatarDropzoneWrapper",
117
- componentId: "sc-758cb640-0"
118
+ componentId: "sc-b516c8e0-0"
118
119
  })([
119
120
  `position:relative;display:inline-flex;width:`,
120
121
  `px;height:`,
@@ -122,7 +123,7 @@ var StyledAvatarDropzoneWrapper = styled.span.withConfig({
122
123
  ], ({ $size }) => avatarSizes[$size ?? "default"].box, ({ $size }) => avatarSizes[$size ?? "default"].box);
123
124
  var StyledAvatarDropzone = styled.button.withConfig({
124
125
  displayName: "avatar-dropzone__StyledAvatarDropzone",
125
- componentId: "sc-758cb640-1"
126
+ componentId: "sc-b516c8e0-1"
126
127
  })([
127
128
  ``,
128
129
  `;font-family:inherit;display:inline-flex;align-items:center;justify-content:center;width:100%;height:100%;border-radius:50%;overflow:hidden;border:2px `,
@@ -146,7 +147,7 @@ var StyledAvatarDropzone = styled.button.withConfig({
146
147
  ], theme.colors.primary, alpha(theme.colors.primary, theme.isDark ? 12 : 5), alpha(theme.colors.primary, 12)), ({ theme }) => theme.colors.primary, ({ theme }) => alpha(theme.colors.primary, theme.isDark ? 10 : 4), ({ theme }) => theme.colors.primary, ({ theme }) => theme.colors.primaryLight);
147
148
  var StyledAvatarRemove = styled.button.withConfig({
148
149
  displayName: "avatar-dropzone__StyledAvatarRemove",
149
- componentId: "sc-758cb640-2"
150
+ componentId: "sc-b516c8e0-2"
150
151
  })([
151
152
  ``,
152
153
  `;position:absolute;top:0;right:0;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;color:#ffffff;background:`,
package/dist/avatar.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { default as React } from 'react';
2
- import { IconProps } from './icon';
2
+ import { IconSource } from './icon';
3
3
  export type AvatarSize = "small" | "default" | "big";
4
4
  export type AvatarColor = "primary" | "secondary" | "tertiary" | "gray";
5
5
  export interface AvatarProps extends React.HTMLAttributes<HTMLSpanElement> {
@@ -9,8 +9,8 @@ export interface AvatarProps extends React.HTMLAttributes<HTMLSpanElement> {
9
9
  $alt?: string;
10
10
  /** Full name or label; the first letters of its first two words are shown. */
11
11
  $name?: string;
12
- /** Fallback icon used when there is no image and no name. */
13
- $icon?: IconProps;
12
+ /** Fallback icon used when there is no image and no name: an imported lucide component, or a rendered element. */
13
+ $icon?: IconSource;
14
14
  $color?: AvatarColor;
15
15
  }
16
16
  declare const Avatar: React.ForwardRefExoticComponent<AvatarProps & React.RefAttributes<HTMLSpanElement>>;
package/dist/avatar.js CHANGED
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
  "use client";
3
3
  import { darkFilledTextRule, filledTextColor } from "./utils/mixins.js";
4
- import { Icon } from "./icon.js";
4
+ import { renderIcon } from "./icon.js";
5
5
  import { jsx } from "react/jsx-runtime";
6
6
  import { forwardRef } from "react";
7
7
  import styled, { css } from "styled-components";
8
+ import { User } from "lucide-react";
8
9
  //#region src/lib/avatar.tsx
9
10
  var avatarSizes = {
10
11
  small: {
@@ -31,7 +32,7 @@ function toInitials(name) {
31
32
  }
32
33
  var StyledAvatar = styled.span.withConfig({
33
34
  displayName: "avatar__StyledAvatar",
34
- componentId: "sc-d3a5da79-0"
35
+ componentId: "sc-3c0e28ca-0"
35
36
  })([``, `;`], ({ theme, $size = "default", $color = "primary" }) => {
36
37
  const { box } = avatarSizes[$size];
37
38
  return css([
@@ -44,7 +45,7 @@ var StyledAvatar = styled.span.withConfig({
44
45
  `;& img{width:100%;height:100%;object-fit:cover;display:block;}`
45
46
  ], box, box, avatarColors[$color](theme), filledTextColor(theme), Math.round(box * .38), darkFilledTextRule(theme));
46
47
  });
47
- function LocalAvatar({ $size = "default", $src, $alt, $name, $icon = "User", $color, ...props }, ref) {
48
+ function LocalAvatar({ $size = "default", $src, $alt, $name, $icon = User, $color, ...props }, ref) {
48
49
  const initials = $name ? toInitials($name) : "";
49
50
  const fallbackLabel = $src || initials ? void 0 : $alt ?? $name;
50
51
  return /*#__PURE__*/ jsx(StyledAvatar, {
@@ -57,10 +58,7 @@ function LocalAvatar({ $size = "default", $src, $alt, $name, $icon = "User", $co
57
58
  children: $src ? /*#__PURE__*/ jsx("img", {
58
59
  src: $src,
59
60
  alt: $alt ?? $name ?? ""
60
- }) : initials ? initials : /*#__PURE__*/ jsx(Icon, {
61
- name: $icon,
62
- size: avatarSizes[$size].icon
63
- })
61
+ }) : initials ? initials : renderIcon($icon, { size: avatarSizes[$size].icon })
64
62
  });
65
63
  }
66
64
  var Avatar = /*#__PURE__*/ forwardRef(LocalAvatar);
package/dist/callout.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { default as React } from 'react';
2
- import { IconProps } from './icon';
2
+ import { IconSource } from './icon';
3
3
  export type CalloutType = "note" | "info" | "warning" | "danger" | "success";
4
4
  export interface CalloutProps extends React.HTMLAttributes<HTMLDivElement> {
5
5
  children: React.ReactNode;
6
6
  $type?: CalloutType;
7
- /** Overrides the icon the type would pick, or supplies one when untyped. */
8
- $icon?: IconProps;
7
+ /** Overrides the icon the type would pick, or supplies one when untyped: an imported lucide component, or a rendered element. */
8
+ $icon?: IconSource;
9
9
  /** Renders without an icon. */
10
10
  $hideIcon?: boolean;
11
11
  }
package/dist/callout.js CHANGED
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
  "use client";
3
3
  import { styledSmall } from "./utils/typography.js";
4
- import { Icon } from "./icon.js";
4
+ import { renderIcon } from "./icon.js";
5
5
  import { jsx, jsxs } from "react/jsx-runtime";
6
6
  import { forwardRef } from "react";
7
7
  import styled, { css } from "styled-components";
8
+ import { Check, CircleAlert, Info, OctagonAlert, TriangleAlert } from "lucide-react";
8
9
  //#region src/lib/callout.tsx
9
10
  var calloutTints = {
10
11
  note: {
@@ -69,11 +70,11 @@ var calloutTints = {
69
70
  }
70
71
  };
71
72
  var calloutIcons = {
72
- note: "CircleAlert",
73
- info: "Info",
74
- warning: "TriangleAlert",
75
- danger: "OctagonAlert",
76
- success: "Check"
73
+ note: CircleAlert,
74
+ info: Info,
75
+ warning: TriangleAlert,
76
+ danger: OctagonAlert,
77
+ success: Check
77
78
  };
78
79
  var tintStyles = (tint) => css([
79
80
  `border-color:`,
@@ -83,7 +84,7 @@ var tintStyles = (tint) => css([
83
84
  ], tint.border, tint.background, tint.text);
84
85
  var StyledCallout = styled.div.withConfig({
85
86
  displayName: "callout__StyledCallout",
86
- componentId: "sc-84e10f5e-0"
87
+ componentId: "sc-46cf6e91-0"
87
88
  })([
88
89
  `background:`,
89
90
  `;border:solid 1px `,
@@ -99,17 +100,14 @@ var StyledCallout = styled.div.withConfig({
99
100
  `;}`
100
101
  ], tintStyles(calloutTints[$type].light), theme.isDark && tintStyles(calloutTints[$type].dark), tintStyles(calloutTints[$type].dark)));
101
102
  function LocalCallout({ children, $type, $icon, $hideIcon, ...props }, ref) {
102
- const iconName = $icon ?? ($type ? calloutIcons[$type] : void 0);
103
+ const icon = $icon ?? ($type ? calloutIcons[$type] : void 0);
103
104
  return /*#__PURE__*/ jsxs(StyledCallout, {
104
105
  $type,
105
106
  ...props,
106
107
  ref,
107
- children: [!$hideIcon && iconName && /*#__PURE__*/ jsx("span", {
108
+ children: [!$hideIcon && icon && /*#__PURE__*/ jsx("span", {
108
109
  className: "callout-icon",
109
- children: /*#__PURE__*/ jsx(Icon, {
110
- name: iconName,
111
- size: 16
112
- })
110
+ children: renderIcon(icon, { size: 16 })
113
111
  }), /*#__PURE__*/ jsx("span", {
114
112
  className: "callout-content",
115
113
  children
@@ -8,6 +8,7 @@ import { Spinner } from "./spinner.js";
8
8
  import { jsx, jsxs } from "react/jsx-runtime";
9
9
  import { forwardRef, useContext, useEffect, useLayoutEffect, useRef, useState } from "react";
10
10
  import styled, { createGlobalStyle, css, keyframes } from "styled-components";
11
+ import { ArrowUp } from "lucide-react";
11
12
  //#region src/lib/chat-input.tsx
12
13
  var GLOW_COLORS = [
13
14
  "#cc5555",
@@ -75,7 +76,7 @@ var sparkleFloat = keyframes([`0%,100%{opacity:0;transform:translateY(0) scale(0
75
76
  var conicStops = (colors) => [...colors, colors[0]].join(", ");
76
77
  var StyledChatForm = styled.form.withConfig({
77
78
  displayName: "chat-input__StyledChatForm",
78
- componentId: "sc-d9366e15-0"
79
+ componentId: "sc-88c29886-0"
79
80
  })([
80
81
  `display:flex;gap:10px;justify-content:center;align-items:flex-end;background:`,
81
82
  `;padding:20px;width:100%;flex-shrink:0;border-top:solid 1px `,
@@ -83,7 +84,7 @@ var StyledChatForm = styled.form.withConfig({
83
84
  ], ({ theme }) => theme.colors.light, ({ theme }) => theme.colors.grayLight);
84
85
  var StyledInputWrapper = styled.div.withConfig({
85
86
  displayName: "chat-input__StyledInputWrapper",
86
- componentId: "sc-d9366e15-1"
87
+ componentId: "sc-88c29886-1"
87
88
  })([`position:relative;flex:1;min-width:0;`, ``], ({ $glow, $glowColors = GLOW_COLORS, theme }) => $glow && css([
88
89
  `isolation:isolate;&::before{content:"";position:absolute;inset:0;border-radius:`,
89
90
  `;background:conic-gradient( from var(--cherry-gradient-angle),`,
@@ -97,7 +98,7 @@ var StyledInputWrapper = styled.div.withConfig({
97
98
  ], theme.spacing.radius.xs, conicStops($glowColors), rotateGradient, theme.spacing.radius.xs, conicStops($glowColors.map((color) => alpha(color, 40))), rotateGradient, theme.spacing.radius.xs, theme.spacing.radius.xs));
98
99
  var StyledRadiantGlow = styled.div.withConfig({
99
100
  displayName: "chat-input__StyledRadiantGlow",
100
- componentId: "sc-d9366e15-2"
101
+ componentId: "sc-88c29886-2"
101
102
  })([
102
103
  `position:absolute;inset:-10px;border-radius:20px;pointer-events:none;z-index:-2;opacity:0.6;transition:all 0.3s ease;&::before{content:"";position:absolute;inset:0;border-radius:inherit;background:conic-gradient( from var(--cherry-gradient-angle),`,
103
104
  ` );animation:`,
@@ -107,11 +108,11 @@ var StyledRadiantGlow = styled.div.withConfig({
107
108
  ], ({ $glowColors = GLOW_COLORS }) => conicStops($glowColors.map((color) => alpha(color, 40))), rotateGradient, pulseGlow, ({ $isActive }) => $isActive && css([`opacity:1;`]));
108
109
  var StyledSparkleContainer = styled.div.withConfig({
109
110
  displayName: "chat-input__StyledSparkleContainer",
110
- componentId: "sc-d9366e15-3"
111
+ componentId: "sc-88c29886-3"
111
112
  })([`position:absolute;inset:-30px;pointer-events:none;overflow:hidden;border-radius:30px;z-index:-3;opacity:0;transition:all 0.3s ease;`, ` @media (prefers-reduced-motion:reduce){display:none;}`], ({ $isActive }) => $isActive && css([`opacity:1;`]));
112
113
  var StyledSparkle = styled.div.withConfig({
113
114
  displayName: "chat-input__StyledSparkle",
114
- componentId: "sc-d9366e15-4"
115
+ componentId: "sc-88c29886-4"
115
116
  })([
116
117
  `position:absolute;width:4px;height:4px;border-radius:50%;background:`,
117
118
  `;box-shadow:0 0 6px `,
@@ -123,7 +124,7 @@ var StyledSparkle = styled.div.withConfig({
123
124
  ], ({ $color }) => $color, ({ $color }) => $color, ({ $left }) => $left, ({ $top }) => $top, sparkleFloat, ({ $delay }) => $delay);
124
125
  var StyledTextarea = styled.textarea.withConfig({
125
126
  displayName: "chat-input__StyledTextarea",
126
- componentId: "sc-d9366e15-5"
127
+ componentId: "sc-88c29886-5"
127
128
  })([
128
129
  `position:relative;z-index:1;display:block;box-sizing:border-box;width:100%;resize:none;font-family:inherit;line-height:1.5;color:`,
129
130
  `;background:`,
@@ -147,7 +148,7 @@ var StyledTextarea = styled.textarea.withConfig({
147
148
  ], theme.colors.primaryLight, theme.colors.primary, theme.colors.primaryLight, theme.colors.primary, theme.colors.primaryLight));
148
149
  var StyledSendButton = styled(Button).withConfig({
149
150
  displayName: "chat-input__StyledSendButton",
150
- componentId: "sc-d9366e15-6"
151
+ componentId: "sc-88c29886-6"
151
152
  })([`flex-shrink:0;`, `;min-height:0;position:relative;& svg,& .lucide{transition:none;position:absolute;inset:0;margin:auto;}`], formElementHeightStyles());
152
153
  var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
153
154
  function LocalChatInput({ value, onValueChange, onSend, $loading, $glow = false, $glowColors, $maxRows, placeholder = "Ask AI Assistant...", "aria-label": ariaLabel = "Ask a question", ...props }, ref) {
@@ -247,7 +248,7 @@ function LocalChatInput({ value, onValueChange, onSend, $loading, $glow = false,
247
248
  type: "submit",
248
249
  disabled: loading || !hasContent,
249
250
  "aria-label": loading ? "Loading response" : "Submit question",
250
- children: loading ? /*#__PURE__*/ jsx(Spinner, {}) : /*#__PURE__*/ jsx(Icon, { name: "ArrowUp" })
251
+ children: loading ? /*#__PURE__*/ jsx(Spinner, {}) : /*#__PURE__*/ jsx(Icon, { icon: ArrowUp })
251
252
  })
252
253
  ]
253
254
  });
@@ -6,6 +6,7 @@ import { ChatContext } from "./chat-provider.js";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
  import { forwardRef, useContext } from "react";
8
8
  import styled, { createGlobalStyle, css, keyframes } from "styled-components";
9
+ import { Sparkles } from "lucide-react";
9
10
  //#region src/lib/chat-launcher.tsx
10
11
  var GLOW_COLORS = [
11
12
  "#cc5555",
@@ -21,7 +22,7 @@ var pulseGlow = keyframes([`0%,100%{opacity:0.5;filter:blur(16px);}50%{opacity:1
21
22
  var conicStops = (colors) => [...colors, colors[0]].join(", ");
22
23
  var StyledChatLauncher = styled.button.withConfig({
23
24
  displayName: "chat-launcher__StyledChatLauncher",
24
- componentId: "sc-64539cc0-0"
25
+ componentId: "sc-61e3ed78-0"
25
26
  })([
26
27
  ``,
27
28
  `;appearance:none;outline:none;cursor:pointer;box-sizing:border-box;height:30px;background:`,
@@ -68,7 +69,7 @@ function LocalChatLauncher({ children, $glow = false, $glowColors, onClick, "ari
68
69
  }),
69
70
  /*#__PURE__*/ jsx("span", {
70
71
  className: "chat-launcher-inner",
71
- children: children ?? /*#__PURE__*/ jsxs(Fragment, { children: [/*#__PURE__*/ jsx(Icon, { name: "Sparkles" }), "Ask AI"] })
72
+ children: children ?? /*#__PURE__*/ jsxs(Fragment, { children: [/*#__PURE__*/ jsx(Icon, { icon: Sparkles }), "Ask AI"] })
72
73
  })
73
74
  ]
74
75
  })] });
@@ -1,5 +1,5 @@
1
1
  import { default as React } from 'react';
2
- import { IconProps } from './icon';
2
+ import { IconSource } from './icon';
3
3
  export type ChatPanelVariant = "drawer" | "inline" | "fullscreen";
4
4
  export interface ChatPanelProps extends React.HTMLAttributes<HTMLDivElement> {
5
5
  children: React.ReactNode;
@@ -15,7 +15,8 @@ export interface ChatPanelProps extends React.HTMLAttributes<HTMLDivElement> {
15
15
  $width?: number;
16
16
  /** Header title. Pass null to render no header text. */
17
17
  $title?: React.ReactNode;
18
- $titleIcon?: IconProps | null;
18
+ /** Header icon: an imported lucide component, or a rendered element. Pass null for none. */
19
+ $titleIcon?: IconSource | null;
19
20
  /** Extra header controls (reset button, …), placed before the close button. */
20
21
  $actions?: React.ReactNode;
21
22
  $hideCloseButton?: boolean;
@@ -3,18 +3,19 @@
3
3
  import { mq } from "./utils/theme.js";
4
4
  import { useLockBodyScroll } from "./utils/use-lock-body-scroll.js";
5
5
  import { useBelowBreakpoint } from "./utils/use-media-query.js";
6
- import { Icon } from "./icon.js";
6
+ import { Icon, renderIcon } from "./icon.js";
7
7
  import { ChatContext } from "./chat-provider.js";
8
8
  import { IconButton } from "./icon-button.js";
9
9
  import { jsx, jsxs } from "react/jsx-runtime";
10
10
  import { forwardRef, useContext, useEffect, useRef } from "react";
11
11
  import styled, { css } from "styled-components";
12
+ import { Sparkles, X } from "lucide-react";
12
13
  //#region src/lib/chat-panel.tsx
13
14
  var DEFAULT_WIDTH = 420;
14
15
  var FOCUSABLE_SELECTOR = "a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
15
16
  var StyledChatPanel = styled.div.withConfig({
16
17
  displayName: "chat-panel__StyledChatPanel",
17
- componentId: "sc-701cc462-0"
18
+ componentId: "sc-82bb81a5-0"
18
19
  })([
19
20
  `display:flex;flex-direction:column;background:`,
20
21
  `;color:`,
@@ -40,7 +41,7 @@ var StyledChatPanel = styled.div.withConfig({
40
41
  `;}`
41
42
  ], $side, !$isVisible && css([`transform:translateX(`, `);opacity:0;visibility:hidden;transition-delay:0s,0s,0.3s;`], $side === "left" ? "-100%" : "100%"), mq("lg"), $width, $side === "left" ? "right" : "left", theme.colors.grayLight);
42
43
  });
43
- function LocalChatPanel({ children, $variant = "drawer", $side = "right", $width = DEFAULT_WIDTH, $title = "AI Assistant", $titleIcon = "Sparkles", $actions, $hideCloseButton, "aria-label": ariaLabel, ...props }, ref) {
44
+ function LocalChatPanel({ children, $variant = "drawer", $side = "right", $width = DEFAULT_WIDTH, $title = "AI Assistant", $titleIcon = Sparkles, $actions, $hideCloseButton, "aria-label": ariaLabel, ...props }, ref) {
44
45
  const { isOpen, close, inputRef } = useContext(ChatContext);
45
46
  const panelRef = useRef(null);
46
47
  const isBelowLg = useBelowBreakpoint("lg");
@@ -145,14 +146,14 @@ function LocalChatPanel({ children, $variant = "drawer", $side = "right", $width
145
146
  className: "chat-header",
146
147
  children: [/*#__PURE__*/ jsxs("span", {
147
148
  className: "chat-header-title",
148
- children: [$titleIcon && /*#__PURE__*/ jsx(Icon, { name: $titleIcon }), $title !== null && /*#__PURE__*/ jsx("h3", { children: $title })]
149
+ children: [$titleIcon && renderIcon($titleIcon), $title !== null && /*#__PURE__*/ jsx("h3", { children: $title })]
149
150
  }), /*#__PURE__*/ jsxs("span", {
150
151
  className: "chat-header-actions",
151
152
  children: [$actions, isOverlay && !$hideCloseButton && /*#__PURE__*/ jsx(IconButton, {
152
153
  onClick: () => close(),
153
154
  "aria-label": "Close chat",
154
155
  title: "Close chat",
155
- children: /*#__PURE__*/ jsx(Icon, { name: "X" })
156
+ children: /*#__PURE__*/ jsx(Icon, { icon: X })
156
157
  })]
157
158
  })]
158
159
  }), children]
@@ -1,5 +1,5 @@
1
1
  import { default as React } from 'react';
2
- import { Icon } from './icon';
2
+ import { IconSource } from './icon';
3
3
  export type DropzoneRejectionReason = "type" | "size" | "count";
4
4
  export interface DropzoneRejection {
5
5
  file: File;
@@ -12,8 +12,8 @@ export interface DropzoneProps extends Omit<React.InputHTMLAttributes<HTMLInputE
12
12
  $browse?: string;
13
13
  /** Optional smaller hint line (accepted types / limits). */
14
14
  $hint?: string;
15
- /** Lucide icon shown in the rounded-square tile. */
16
- $icon?: React.ComponentProps<typeof Icon>["name"];
15
+ /** Icon shown in the rounded-square tile: an imported lucide component, or a rendered element. */
16
+ $icon?: IconSource;
17
17
  /** Compact, flush variant: horizontal layout, tighter padding, and a
18
18
  transparent background so it sits on whatever surface it's embedded in
19
19
  (a form row, modal, or list) next to regular inputs. */
package/dist/dropzone.js CHANGED
@@ -2,10 +2,11 @@
2
2
  "use client";
3
3
  import { mq } from "./utils/theme.js";
4
4
  import { alpha, resetButton } from "./utils/mixins.js";
5
- import { Icon } from "./icon.js";
5
+ import { Icon, renderIcon } from "./icon.js";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
7
7
  import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
8
8
  import styled, { css } from "styled-components";
9
+ import { File, FileUp, X } from "lucide-react";
9
10
  //#region src/lib/dropzone.tsx
10
11
  /** Matches a file against an `accept` string (".pdf,image/*,text/plain"),
11
12
  mirroring the native file-picker filter for dropped files. */ var matchesAccept = (file, accept) => {
@@ -18,7 +19,7 @@ mirroring the native file-picker filter for dropped files. */ var matchesAccept
18
19
  return file.type.toLowerCase() === entry;
19
20
  });
20
21
  };
21
- function LocalDropzone({ $prompt = "Drag & drop files here", $browse, $hint, $icon = "FileUp", $inline, $maxFiles, $maxBytes, onFilesChange, onFilesRejected, accept, multiple, disabled, ...rest }, ref) {
22
+ function LocalDropzone({ $prompt = "Drag & drop files here", $browse, $hint, $icon = FileUp, $inline, $maxFiles, $maxBytes, onFilesChange, onFilesRejected, accept, multiple, disabled, ...rest }, ref) {
22
23
  const [items, setItems] = useState([]);
23
24
  const [dragOver, setDragOver] = useState(false);
24
25
  const inputRef = useRef(null);
@@ -111,7 +112,7 @@ function LocalDropzone({ $prompt = "Drag & drop files here", $browse, $hint, $ic
111
112
  children: [
112
113
  /*#__PURE__*/ jsx(StyledDropIcon, {
113
114
  $inline,
114
- children: /*#__PURE__*/ jsx(Icon, { name: $icon })
115
+ children: renderIcon($icon)
115
116
  }),
116
117
  /*#__PURE__*/ jsxs(StyledDropPrompt, {
117
118
  $inline,
@@ -141,12 +142,12 @@ function LocalDropzone({ $prompt = "Drag & drop files here", $browse, $hint, $ic
141
142
  children: [item.previewUrl ? /*#__PURE__*/ jsx("img", {
142
143
  src: item.previewUrl,
143
144
  alt: item.file.name
144
- }) : /*#__PURE__*/ jsxs(StyledThumbFile, { children: [/*#__PURE__*/ jsx(Icon, { name: "File" }), /*#__PURE__*/ jsx("span", { children: item.file.name })] }), /*#__PURE__*/ jsx(StyledThumbRemove, {
145
+ }) : /*#__PURE__*/ jsxs(StyledThumbFile, { children: [/*#__PURE__*/ jsx(Icon, { icon: File }), /*#__PURE__*/ jsx("span", { children: item.file.name })] }), /*#__PURE__*/ jsx(StyledThumbRemove, {
145
146
  type: "button",
146
147
  "aria-label": `Remove ${item.file.name}`,
147
148
  onClick: () => removeItem(item.key),
148
149
  disabled,
149
- children: /*#__PURE__*/ jsx(Icon, { name: "X" })
150
+ children: /*#__PURE__*/ jsx(Icon, { icon: X })
150
151
  })]
151
152
  }, item.key)) })
152
153
  ] });
@@ -154,11 +155,11 @@ function LocalDropzone({ $prompt = "Drag & drop files here", $browse, $hint, $ic
154
155
  var Dropzone = /*#__PURE__*/ forwardRef(LocalDropzone);
155
156
  var StyledDropzoneWrapper = styled.div.withConfig({
156
157
  displayName: "dropzone__StyledDropzoneWrapper",
157
- componentId: "sc-a5a0875f-0"
158
+ componentId: "sc-1391bc72-0"
158
159
  })([`display:flex;flex-direction:column;gap:12px;width:100%;`]);
159
160
  var StyledDropzone = styled.button.withConfig({
160
161
  displayName: "dropzone__StyledDropzone",
161
- componentId: "sc-a5a0875f-1"
162
+ componentId: "sc-1391bc72-1"
162
163
  })([
163
164
  ``,
164
165
  `;font-family:inherit;display:flex;flex-direction:`,
@@ -183,7 +184,7 @@ var StyledDropzone = styled.button.withConfig({
183
184
  ], theme.colors.primary, alpha(theme.colors.primary, theme.isDark ? 12 : 5), alpha(theme.colors.primary, 12)), ({ theme }) => theme.colors.primary, ({ theme }) => alpha(theme.colors.primary, theme.isDark ? 10 : 4), ({ theme }) => theme.colors.primary, ({ theme }) => theme.colors.primaryLight);
184
185
  var StyledDropIcon = styled.span.withConfig({
185
186
  displayName: "dropzone__StyledDropIcon",
186
- componentId: "sc-a5a0875f-2"
187
+ componentId: "sc-1391bc72-2"
187
188
  })([
188
189
  `display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:`,
189
190
  `;height:`,
@@ -197,7 +198,7 @@ var StyledDropIcon = styled.span.withConfig({
197
198
  ], ({ $inline }) => $inline ? "32px" : "44px", ({ $inline }) => $inline ? "32px" : "44px", ({ theme }) => theme.spacing.radius.lg, ({ theme }) => theme.colors.grayLight, ({ theme }) => theme.colors.grayLight, ({ theme }) => theme.colors.primary, ({ $inline }) => $inline ? "18px" : "22px", ({ $inline }) => $inline ? "18px" : "22px");
198
199
  var StyledDropPrompt = styled.span.withConfig({
199
200
  displayName: "dropzone__StyledDropPrompt",
200
- componentId: "sc-a5a0875f-3"
201
+ componentId: "sc-1391bc72-3"
201
202
  })([
202
203
  `display:flex;flex-direction:`,
203
204
  `;flex-wrap:wrap;align-items:baseline;gap:`,
@@ -209,7 +210,7 @@ var StyledDropPrompt = styled.span.withConfig({
209
210
  ], ({ $inline }) => $inline ? "row" : "column", ({ $inline }) => $inline ? "0 6px" : "2px", ({ theme }) => theme.fontSizes.text.xs, ({ theme }) => theme.lineHeights.text.xs, ({ theme }) => theme.colors.dark, ({ theme }) => theme.colors.grayDark);
210
211
  var StyledDropHint = styled.span.withConfig({
211
212
  displayName: "dropzone__StyledDropHint",
212
- componentId: "sc-a5a0875f-4"
213
+ componentId: "sc-1391bc72-4"
213
214
  })([
214
215
  `font-family:inherit;font-size:`,
215
216
  `;line-height:`,
@@ -219,11 +220,11 @@ var StyledDropHint = styled.span.withConfig({
219
220
  ], ({ theme }) => theme.fontSizes.small.xs, ({ theme }) => theme.lineHeights.small.xs, ({ theme }) => theme.colors.gray, ({ $inline }) => $inline && css([`margin-left:auto;`]));
220
221
  var StyledThumbs = styled.div.withConfig({
221
222
  displayName: "dropzone__StyledThumbs",
222
- componentId: "sc-a5a0875f-5"
223
+ componentId: "sc-1391bc72-5"
223
224
  })([`display:grid;grid-template-columns:repeat(auto-fill,72px);gap:10px;`, `{grid-template-columns:repeat(auto-fill,84px);}`], mq("sm"));
224
225
  var StyledThumb = styled.div.withConfig({
225
226
  displayName: "dropzone__StyledThumb",
226
- componentId: "sc-a5a0875f-6"
227
+ componentId: "sc-1391bc72-6"
227
228
  })([
228
229
  `position:relative;width:72px;height:72px;border-radius:`,
229
230
  `;overflow:hidden;border:solid 1px `,
@@ -233,7 +234,7 @@ var StyledThumb = styled.div.withConfig({
233
234
  ], ({ theme }) => theme.spacing.radius.lg, ({ theme }) => theme.colors.grayLight, ({ theme }) => theme.colors.light, mq("sm"));
234
235
  var StyledThumbFile = styled.span.withConfig({
235
236
  displayName: "dropzone__StyledThumbFile",
236
- componentId: "sc-a5a0875f-7"
237
+ componentId: "sc-1391bc72-7"
237
238
  })([
238
239
  `display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;width:100%;height:100%;padding:6px;color:`,
239
240
  `;& svg{width:20px;height:20px;color:`,
@@ -241,7 +242,7 @@ var StyledThumbFile = styled.span.withConfig({
241
242
  ], ({ theme }) => theme.colors.grayDark, ({ theme }) => theme.colors.primary);
242
243
  var StyledThumbRemove = styled.button.withConfig({
243
244
  displayName: "dropzone__StyledThumbRemove",
244
- componentId: "sc-a5a0875f-8"
245
+ componentId: "sc-1391bc72-8"
245
246
  })([
246
247
  ``,
247
248
  `;position:absolute;top:4px;right:4px;display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;color:#ffffff;background:`,
@@ -0,0 +1,24 @@
1
+ import { IconName as LucideDynamicName } from 'lucide-react/dynamic';
2
+ import { icons } from 'lucide-react';
3
+ /**
4
+ * A lucide icon referenced by name: the PascalCase export name
5
+ * (`"ArrowRight"`) or lucide's own kebab-case id (`"arrow-right"`).
6
+ */
7
+ export type IconName = keyof typeof icons | LucideDynamicName;
8
+ export interface DynamicIconProps {
9
+ /** The icon, by name. An unknown name renders nothing. */
10
+ name: IconName;
11
+ color?: string;
12
+ size?: number;
13
+ className?: string;
14
+ "aria-label"?: string;
15
+ }
16
+ /**
17
+ * Renders a lucide icon chosen at runtime (CMS content, user settings). The
18
+ * glyph is fetched on demand through `lucide-react/dynamic`, so it appears
19
+ * after hydration once its chunk has loaded, and never in server-rendered
20
+ * HTML. For any icon known when the code is written, use
21
+ * `<Icon icon={Glyph} />` from the main entry instead. In a `$icon`-style
22
+ * prop, pass it as an element: `$icon={<DynamicIcon name={n} />}`.
23
+ */
24
+ export declare function DynamicIcon({ name, "aria-label": ariaLabel, ...props }: DynamicIconProps): import("react").JSX.Element | null;