cherry-styled-components 0.1.18 → 0.1.19

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,11 @@
1
+ import { default as React } from 'react';
2
+ export interface AccordionProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title" | "onToggle"> {
3
+ children: React.ReactNode;
4
+ title: React.ReactNode;
5
+ onToggle?: (isOpen: boolean) => void;
6
+ $inline?: boolean;
7
+ defaultOpen?: boolean;
8
+ open?: boolean;
9
+ }
10
+ declare function Accordion({ children, title, onToggle, $inline, defaultOpen, open, ...rest }: AccordionProps): React.JSX.Element;
11
+ export { Accordion };
@@ -0,0 +1,88 @@
1
+ "use client";
2
+ "use client";
3
+ import { styledText } from "./utils/typography.js";
4
+ import { Icon } from "./icon.js";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { useState } from "react";
7
+ import styled, { css } from "styled-components";
8
+ //#region src/lib/accordion.tsx
9
+ var StyledAccordion = styled.div.withConfig({
10
+ displayName: "accordion__StyledAccordion",
11
+ componentId: "sc-4007375b-0"
12
+ })([
13
+ `text-align:left;border-radius:`,
14
+ `;margin:0;`,
15
+ `;width:100%;background:`,
16
+ `;`,
17
+ ``
18
+ ], ({ 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
+ var StyledAccordionTitle = styled.h3.withConfig({
20
+ displayName: "accordion__StyledAccordionTitle",
21
+ componentId: "sc-4007375b-1"
22
+ })([
23
+ `cursor:pointer;margin:0;`,
24
+ `;color:`,
25
+ `;transition:color 0.3s ease;position:relative;padding:`,
26
+ `;&:hover{color:`,
27
+ `;}& .lucide-chevron-down{position:absolute;top:50%;transform:translateY(-50%);right:`,
28
+ `;transition:transform 0.3s ease;`,
29
+ `}`
30
+ ], ({ theme }) => styledText(theme), ({ theme }) => theme.colors.primary, ({ $inline }) => $inline ? "8px 40px 8px 0" : "20px 50px 20px 20px", ({ theme }) => theme.colors.primaryDark, ({ $inline }) => $inline ? "0" : "20px", ({ $isOpen }) => $isOpen && css([`transform:translateY(-50%) rotate(180deg);`]));
31
+ var StyledAccordionContent = styled.div.withConfig({
32
+ displayName: "accordion__StyledAccordionContent",
33
+ componentId: "sc-4007375b-2"
34
+ })([
35
+ `display:grid;grid-template-rows:`,
36
+ `;visibility:`,
37
+ `;transition:grid-template-rows 0.3s cubic-bezier(0.4,0,0.2,1),visibility 0.3s;@media (prefers-reduced-motion:reduce){transition:none;}`
38
+ ], ({ $isOpen }) => $isOpen ? "1fr" : "0fr", ({ $isOpen }) => $isOpen ? "visible" : "hidden");
39
+ var StyledAccordionInner = styled.div.withConfig({
40
+ displayName: "accordion__StyledAccordionInner",
41
+ componentId: "sc-4007375b-3"
42
+ })([`min-height:0;overflow:clip;`]);
43
+ var StyledAccordionBody = styled.div.withConfig({
44
+ displayName: "accordion__StyledAccordionBody",
45
+ componentId: "sc-4007375b-4"
46
+ })([
47
+ ``,
48
+ `;color:`,
49
+ `;padding:`,
50
+ `;opacity:`,
51
+ `;transform:translateY(`,
52
+ `);transition:opacity 0.3s ease,transform 0.3s cubic-bezier(0.4,0,0.2,1);@media (prefers-reduced-motion:reduce){transform:none;transition:none;}`
53
+ ], ({ theme }) => styledText(theme), ({ theme }) => theme.colors.grayDark, ({ $inline }) => $inline ? "0 0 12px" : "0 20px 20px", ({ $isOpen }) => $isOpen ? 1 : 0, ({ $isOpen }) => $isOpen ? 0 : "-6px");
54
+ function Accordion({ children, title, onToggle, $inline, defaultOpen, open, ...rest }) {
55
+ const [internalOpen, setInternalOpen] = useState(defaultOpen ?? false);
56
+ const isControlled = open !== void 0;
57
+ const isOpen = isControlled ? open : internalOpen;
58
+ const handleToggle = () => {
59
+ const next = !isOpen;
60
+ if (!isControlled) setInternalOpen(next);
61
+ onToggle?.(next);
62
+ };
63
+ return /*#__PURE__*/ jsxs(StyledAccordion, {
64
+ $inline,
65
+ ...rest,
66
+ children: [/*#__PURE__*/ jsxs(StyledAccordionTitle, {
67
+ onClick: handleToggle,
68
+ $isOpen: isOpen,
69
+ $inline,
70
+ role: "button",
71
+ "aria-expanded": isOpen,
72
+ children: [
73
+ title,
74
+ " ",
75
+ /*#__PURE__*/ jsx(Icon, { name: "ChevronDown" })
76
+ ]
77
+ }), /*#__PURE__*/ jsx(StyledAccordionContent, {
78
+ $isOpen: isOpen,
79
+ children: /*#__PURE__*/ jsx(StyledAccordionInner, { children: /*#__PURE__*/ jsx(StyledAccordionBody, {
80
+ $isOpen: isOpen,
81
+ $inline,
82
+ children
83
+ }) })
84
+ })]
85
+ });
86
+ }
87
+ //#endregion
88
+ export { Accordion };
@@ -0,0 +1,19 @@
1
+ import { default as React } from 'react';
2
+ import { DropzoneRejection } from './dropzone';
3
+ import { Icon } from './icon';
4
+ export interface AvatarDropzoneProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange" | "children" | "multiple"> {
5
+ /** Visual scale, following the IconButton size scale. */
6
+ $size?: "default" | "big" | "small";
7
+ /** Lucide icon shown while no picture is selected. */
8
+ $icon?: React.ComponentProps<typeof Icon>["name"];
9
+ /** Maximum file size in bytes. */
10
+ $maxBytes?: number;
11
+ /** Accessible label for the drop target. */
12
+ "aria-label"?: string;
13
+ /** Called with the selected picture, or null when it is removed. */
14
+ onFileChange?: (file: File | null) => void;
15
+ /** Called when a file is rejected (wrong type or too big). */
16
+ onFileRejected?: (rejection: DropzoneRejection) => void;
17
+ }
18
+ declare const AvatarDropzone: React.ForwardRefExoticComponent<AvatarDropzoneProps & React.RefAttributes<HTMLInputElement>>;
19
+ export { AvatarDropzone };
@@ -0,0 +1,157 @@
1
+ "use client";
2
+ "use client";
3
+ import { resetButton } from "./utils/mixins.js";
4
+ import { Icon } from "./icon.js";
5
+ import { matchesAccept } from "./dropzone.js";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
8
+ import styled, { css } from "styled-components";
9
+ import { rgba } from "polished";
10
+ //#region src/lib/avatar-dropzone.tsx
11
+ var avatarSizes = {
12
+ small: {
13
+ box: 64,
14
+ icon: 20
15
+ },
16
+ default: {
17
+ box: 96,
18
+ icon: 24
19
+ },
20
+ big: {
21
+ box: 128,
22
+ icon: 28
23
+ }
24
+ };
25
+ function LocalAvatarDropzone({ $size, $icon = "User", $maxBytes, "aria-label": ariaLabel = "Upload profile picture", onFileChange, onFileRejected, accept = "image/*", disabled, ...rest }, ref) {
26
+ const [previewUrl, setPreviewUrl] = useState(null);
27
+ const [dragOver, setDragOver] = useState(false);
28
+ const inputRef = useRef(null);
29
+ useImperativeHandle(ref, () => inputRef.current);
30
+ const previewUrlRef = useRef(null);
31
+ previewUrlRef.current = previewUrl;
32
+ useEffect(() => {
33
+ return () => {
34
+ if (previewUrlRef.current) URL.revokeObjectURL(previewUrlRef.current);
35
+ };
36
+ }, []);
37
+ const setFile = (fileList) => {
38
+ if (!fileList || fileList.length === 0 || disabled) return;
39
+ const file = fileList[0];
40
+ if (!matchesAccept(file, accept)) {
41
+ onFileRejected?.({
42
+ file,
43
+ reason: "type"
44
+ });
45
+ return;
46
+ }
47
+ if ($maxBytes && file.size > $maxBytes) {
48
+ onFileRejected?.({
49
+ file,
50
+ reason: "size"
51
+ });
52
+ return;
53
+ }
54
+ setPreviewUrl((current) => {
55
+ if (current) URL.revokeObjectURL(current);
56
+ return URL.createObjectURL(file);
57
+ });
58
+ onFileChange?.(file);
59
+ };
60
+ const removeFile = () => {
61
+ setPreviewUrl((current) => {
62
+ if (current) URL.revokeObjectURL(current);
63
+ return null;
64
+ });
65
+ onFileChange?.(null);
66
+ };
67
+ return /*#__PURE__*/ jsxs(StyledAvatarDropzoneWrapper, {
68
+ $size,
69
+ children: [
70
+ /*#__PURE__*/ jsx(StyledAvatarDropzone, {
71
+ type: "button",
72
+ "aria-label": ariaLabel,
73
+ $size,
74
+ $dragOver: dragOver,
75
+ $hasImage: !!previewUrl,
76
+ disabled,
77
+ onClick: () => !disabled && inputRef.current?.click(),
78
+ onDragOver: (e) => {
79
+ e.preventDefault();
80
+ if (!disabled) setDragOver(true);
81
+ },
82
+ onDragLeave: () => setDragOver(false),
83
+ onDrop: (e) => {
84
+ e.preventDefault();
85
+ setDragOver(false);
86
+ setFile(e.dataTransfer.files);
87
+ },
88
+ children: previewUrl ? /*#__PURE__*/ jsx("img", {
89
+ src: previewUrl,
90
+ alt: ""
91
+ }) : /*#__PURE__*/ jsx(Icon, { name: $icon })
92
+ }),
93
+ previewUrl && /*#__PURE__*/ jsx(StyledAvatarRemove, {
94
+ type: "button",
95
+ "aria-label": "Remove profile picture",
96
+ onClick: removeFile,
97
+ disabled,
98
+ children: /*#__PURE__*/ jsx(Icon, { name: "X" })
99
+ }),
100
+ /*#__PURE__*/ jsx("input", {
101
+ ref: inputRef,
102
+ type: "file",
103
+ accept,
104
+ disabled,
105
+ hidden: true,
106
+ onChange: (e) => {
107
+ setFile(e.target.files);
108
+ e.target.value = "";
109
+ },
110
+ ...rest
111
+ })
112
+ ]
113
+ });
114
+ }
115
+ var AvatarDropzone = /*#__PURE__*/ forwardRef(LocalAvatarDropzone);
116
+ var StyledAvatarDropzoneWrapper = styled.span.withConfig({
117
+ displayName: "avatar-dropzone__StyledAvatarDropzoneWrapper",
118
+ componentId: "sc-e18fe5e0-0"
119
+ })([
120
+ `position:relative;display:inline-flex;width:`,
121
+ `px;height:`,
122
+ `px;`
123
+ ], ({ $size }) => avatarSizes[$size ?? "default"].box, ({ $size }) => avatarSizes[$size ?? "default"].box);
124
+ var StyledAvatarDropzone = styled.button.withConfig({
125
+ displayName: "avatar-dropzone__StyledAvatarDropzone",
126
+ componentId: "sc-e18fe5e0-1"
127
+ })([
128
+ ``,
129
+ `;display:inline-flex;align-items:center;justify-content:center;width:100%;height:100%;border-radius:50%;overflow:hidden;border:2px `,
130
+ ` `,
131
+ `;background:`,
132
+ `;color:`,
133
+ `;transition:all 0.3s ease;& svg{width:`,
134
+ `px;height:`,
135
+ `px;color:`,
136
+ `;}& img{width:100%;height:100%;object-fit:cover;display:block;}`,
137
+ ` @media (hover:hover){&:hover:not(:disabled){border-color:`,
138
+ `;background:`,
139
+ `;}}&:focus{border-color:`,
140
+ `;box-shadow:0 0 0 4px `,
141
+ `;}&:disabled{cursor:not-allowed;opacity:0.6;}`
142
+ ], resetButton, ({ $hasImage }) => $hasImage ? "solid" : "dashed", ({ theme }) => theme.colors.grayLight, ({ theme }) => theme.colors.light, ({ theme }) => theme.colors.grayDark, ({ $size }) => avatarSizes[$size ?? "default"].icon, ({ $size }) => avatarSizes[$size ?? "default"].icon, ({ theme }) => theme.colors.primary, ({ theme, $dragOver }) => $dragOver && css([
143
+ `border-color:`,
144
+ `;background:`,
145
+ `;`
146
+ ], theme.colors.primary, rgba(theme.colors.primary, theme.isDark ? .12 : .05)), ({ theme }) => theme.colors.primary, ({ theme }) => rgba(theme.colors.primary, theme.isDark ? .1 : .04), ({ theme }) => theme.colors.primary, ({ theme }) => theme.colors.primaryLight);
147
+ var StyledAvatarRemove = styled.button.withConfig({
148
+ displayName: "avatar-dropzone__StyledAvatarRemove",
149
+ componentId: "sc-e18fe5e0-2"
150
+ })([
151
+ ``,
152
+ `;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:`,
153
+ `;transition:background 0.3s ease;& svg{width:14px;height:14px;}@media (hover:hover){&:hover:not(:disabled){background:`,
154
+ `;}}&:disabled{cursor:not-allowed;opacity:0.6;}`
155
+ ], resetButton, ({ theme }) => rgba(theme.colors.dark, .6), ({ theme }) => theme.colors.error);
156
+ //#endregion
157
+ export { AvatarDropzone };
package/dist/button.js CHANGED
@@ -77,7 +77,7 @@ var buttonStyles = (theme, $variant, $size, $outline, $fullWidth, $error, disabl
77
77
  `, $fullWidth && `width: 100%;`);
78
78
  var StyledButton = styled.button.withConfig({
79
79
  displayName: "button__StyledButton",
80
- componentId: "sc-86ae8105-0"
80
+ componentId: "sc-2f3ba034-0"
81
81
  })([``, ``], ({ theme, $variant, $error, $size, $outline, $fullWidth, disabled }) => buttonStyles(theme, $variant, $size, $outline, $fullWidth, $error, disabled));
82
82
  function LocalButton({ $variant = "primary", ...props }, ref) {
83
83
  return /*#__PURE__*/ jsxs(StyledButton, {
@@ -0,0 +1,35 @@
1
+ import { default as React } from 'react';
2
+ import { Icon } from './icon';
3
+ export type DropzoneRejectionReason = "type" | "size" | "count";
4
+ export interface DropzoneRejection {
5
+ file: File;
6
+ reason: DropzoneRejectionReason;
7
+ }
8
+ export interface DropzoneProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "value" | "onChange" | "children"> {
9
+ /** Bold primary prompt line. */
10
+ $prompt?: string;
11
+ /** Optional "or click to browse" sub-line beneath the prompt. */
12
+ $browse?: string;
13
+ /** Optional smaller hint line (accepted types / limits). */
14
+ $hint?: string;
15
+ /** Lucide icon shown in the rounded-square tile. */
16
+ $icon?: React.ComponentProps<typeof Icon>["name"];
17
+ /** Compact, flush variant: horizontal layout, tighter padding, and a
18
+ transparent background so it sits on whatever surface it's embedded in
19
+ (a form row, modal, or list) next to regular inputs. */
20
+ $inline?: boolean;
21
+ /** Maximum number of files kept at once. */
22
+ $maxFiles?: number;
23
+ /** Maximum size per file in bytes. */
24
+ $maxBytes?: number;
25
+ /** Called with the full current file list whenever it changes. */
26
+ onFilesChange?: (files: File[]) => void;
27
+ /** Called with the files that were rejected (wrong type, too big, over the
28
+ count limit) so the caller can surface an error however it likes. */
29
+ onFilesRejected?: (rejections: DropzoneRejection[]) => void;
30
+ }
31
+ /** Matches a file against an `accept` string (".pdf,image/*,text/plain"),
32
+ mirroring the native file-picker filter for dropped files. */
33
+ export declare const matchesAccept: (file: File, accept?: string) => boolean;
34
+ declare const Dropzone: React.ForwardRefExoticComponent<DropzoneProps & React.RefAttributes<HTMLInputElement>>;
35
+ export { Dropzone };
@@ -0,0 +1,255 @@
1
+ "use client";
2
+ "use client";
3
+ import { mq } from "./utils/theme.js";
4
+ import { resetButton } from "./utils/mixins.js";
5
+ import { Icon } from "./icon.js";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
8
+ import styled, { css } from "styled-components";
9
+ import { rgba } from "polished";
10
+ //#region src/lib/dropzone.tsx
11
+ /** Matches a file against an `accept` string (".pdf,image/*,text/plain"),
12
+ mirroring the native file-picker filter for dropped files. */ var matchesAccept = (file, accept) => {
13
+ if (!accept) return true;
14
+ return accept.split(",").some((raw) => {
15
+ const entry = raw.trim().toLowerCase();
16
+ if (!entry) return false;
17
+ if (entry.startsWith(".")) return file.name.toLowerCase().endsWith(entry);
18
+ if (entry.endsWith("/*")) return file.type.toLowerCase().startsWith(entry.slice(0, -1));
19
+ return file.type.toLowerCase() === entry;
20
+ });
21
+ };
22
+ function LocalDropzone({ $prompt = "Drag & drop files here", $browse, $hint, $icon = "FileUp", $inline, $maxFiles, $maxBytes, onFilesChange, onFilesRejected, accept, multiple, disabled, ...rest }, ref) {
23
+ const [items, setItems] = useState([]);
24
+ const [dragOver, setDragOver] = useState(false);
25
+ const inputRef = useRef(null);
26
+ useImperativeHandle(ref, () => inputRef.current);
27
+ const itemsRef = useRef([]);
28
+ itemsRef.current = items;
29
+ useEffect(() => {
30
+ return () => {
31
+ itemsRef.current.forEach((item) => item.previewUrl && URL.revokeObjectURL(item.previewUrl));
32
+ };
33
+ }, []);
34
+ const limit = multiple ? $maxFiles ?? Infinity : 1;
35
+ const atLimit = items.length >= limit;
36
+ const addFiles = (fileList) => {
37
+ if (!fileList || disabled) return;
38
+ const files = Array.from(fileList);
39
+ const rejections = [];
40
+ const accepted = [];
41
+ const replacing = !multiple && files.length > 0;
42
+ const remaining = replacing ? 1 : limit - items.length;
43
+ for (const file of files) {
44
+ if (accepted.length >= remaining) {
45
+ rejections.push({
46
+ file,
47
+ reason: "count"
48
+ });
49
+ continue;
50
+ }
51
+ if (!matchesAccept(file, accept)) {
52
+ rejections.push({
53
+ file,
54
+ reason: "type"
55
+ });
56
+ continue;
57
+ }
58
+ if ($maxBytes && file.size > $maxBytes) {
59
+ rejections.push({
60
+ file,
61
+ reason: "size"
62
+ });
63
+ continue;
64
+ }
65
+ accepted.push({
66
+ key: `${file.name}-${file.size}-${Date.now()}-${accepted.length}`,
67
+ file,
68
+ previewUrl: file.type.startsWith("image/") ? URL.createObjectURL(file) : null
69
+ });
70
+ }
71
+ if (rejections.length > 0) onFilesRejected?.(rejections);
72
+ if (accepted.length === 0) return;
73
+ setItems((current) => {
74
+ const kept = replacing ? [] : current;
75
+ if (replacing) current.forEach((item) => item.previewUrl && URL.revokeObjectURL(item.previewUrl));
76
+ const next = [...kept, ...accepted];
77
+ onFilesChange?.(next.map((item) => item.file));
78
+ return next;
79
+ });
80
+ };
81
+ const removeItem = (key) => {
82
+ setItems((current) => {
83
+ const item = current.find((it) => it.key === key);
84
+ if (item?.previewUrl) URL.revokeObjectURL(item.previewUrl);
85
+ const next = current.filter((it) => it.key !== key);
86
+ onFilesChange?.(next.map((it) => it.file));
87
+ return next;
88
+ });
89
+ };
90
+ const openPicker = () => {
91
+ if (disabled || atLimit) return;
92
+ inputRef.current?.click();
93
+ };
94
+ return /*#__PURE__*/ jsxs(StyledDropzoneWrapper, { children: [
95
+ /*#__PURE__*/ jsxs(StyledDropzone, {
96
+ type: "button",
97
+ "aria-label": $prompt,
98
+ $dragOver: dragOver,
99
+ $inline,
100
+ disabled: disabled || atLimit,
101
+ onClick: openPicker,
102
+ onDragOver: (e) => {
103
+ e.preventDefault();
104
+ if (!disabled && !atLimit) setDragOver(true);
105
+ },
106
+ onDragLeave: () => setDragOver(false),
107
+ onDrop: (e) => {
108
+ e.preventDefault();
109
+ setDragOver(false);
110
+ addFiles(e.dataTransfer.files);
111
+ },
112
+ children: [
113
+ /*#__PURE__*/ jsx(StyledDropIcon, {
114
+ $inline,
115
+ children: /*#__PURE__*/ jsx(Icon, { name: $icon })
116
+ }),
117
+ /*#__PURE__*/ jsxs(StyledDropPrompt, {
118
+ $inline,
119
+ children: [$prompt, $browse ? /*#__PURE__*/ jsx("span", { children: $browse }) : null]
120
+ }),
121
+ $hint ? /*#__PURE__*/ jsx(StyledDropHint, {
122
+ $inline,
123
+ children: $hint
124
+ }) : null
125
+ ]
126
+ }),
127
+ /*#__PURE__*/ jsx("input", {
128
+ ref: inputRef,
129
+ type: "file",
130
+ accept,
131
+ multiple,
132
+ disabled,
133
+ hidden: true,
134
+ onChange: (e) => {
135
+ addFiles(e.target.files);
136
+ e.target.value = "";
137
+ },
138
+ ...rest
139
+ }),
140
+ items.length > 0 && /*#__PURE__*/ jsx(StyledThumbs, { children: items.map((item) => /*#__PURE__*/ jsxs(StyledThumb, {
141
+ title: item.file.name,
142
+ children: [item.previewUrl ? /*#__PURE__*/ jsx("img", {
143
+ src: item.previewUrl,
144
+ alt: item.file.name
145
+ }) : /*#__PURE__*/ jsxs(StyledThumbFile, { children: [/*#__PURE__*/ jsx(Icon, { name: "File" }), /*#__PURE__*/ jsx("span", { children: item.file.name })] }), /*#__PURE__*/ jsx(StyledThumbRemove, {
146
+ type: "button",
147
+ "aria-label": `Remove ${item.file.name}`,
148
+ onClick: () => removeItem(item.key),
149
+ disabled,
150
+ children: /*#__PURE__*/ jsx(Icon, { name: "X" })
151
+ })]
152
+ }, item.key)) })
153
+ ] });
154
+ }
155
+ var Dropzone = /*#__PURE__*/ forwardRef(LocalDropzone);
156
+ var StyledDropzoneWrapper = styled.div.withConfig({
157
+ displayName: "dropzone__StyledDropzoneWrapper",
158
+ componentId: "sc-cf1c3c01-0"
159
+ })([`display:flex;flex-direction:column;gap:12px;width:100%;`]);
160
+ var StyledDropzone = styled.button.withConfig({
161
+ displayName: "dropzone__StyledDropzone",
162
+ componentId: "sc-cf1c3c01-1"
163
+ })([
164
+ ``,
165
+ `;display:flex;flex-direction:`,
166
+ `;align-items:center;justify-content:center;gap:`,
167
+ `;width:100%;padding:`,
168
+ `;border-radius:`,
169
+ `;border:2px dashed `,
170
+ `;background:`,
171
+ `;color:`,
172
+ `;text-align:`,
173
+ `;transition:all 0.3s ease;`,
174
+ ` @media (hover:hover){&:hover:not(:disabled){border-color:`,
175
+ `;background:`,
176
+ `;}}&:focus{border-color:`,
177
+ `;box-shadow:0 0 0 4px `,
178
+ `;}&:disabled{cursor:not-allowed;opacity:0.6;}`
179
+ ], resetButton, ({ $inline }) => $inline ? "row" : "column", ({ $inline }) => $inline ? "10px" : "8px", ({ $inline }) => $inline ? "10px 14px" : "24px 20px", ({ theme }) => theme.spacing.radius.lg, ({ theme }) => theme.colors.grayLight, ({ theme, $inline }) => $inline ? "transparent" : theme.colors.light, ({ theme }) => theme.colors.grayDark, ({ $inline }) => $inline ? "left" : "center", ({ theme, $dragOver }) => $dragOver && css([
180
+ `border-color:`,
181
+ `;background:`,
182
+ `;`
183
+ ], theme.colors.primary, rgba(theme.colors.primary, theme.isDark ? .12 : .05)), ({ theme }) => theme.colors.primary, ({ theme }) => rgba(theme.colors.primary, theme.isDark ? .1 : .04), ({ theme }) => theme.colors.primary, ({ theme }) => theme.colors.primaryLight);
184
+ var StyledDropIcon = styled.span.withConfig({
185
+ displayName: "dropzone__StyledDropIcon",
186
+ componentId: "sc-cf1c3c01-2"
187
+ })([
188
+ `display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:`,
189
+ `;height:`,
190
+ `;border-radius:`,
191
+ `;background:`,
192
+ `;border:solid 1px `,
193
+ `;color:`,
194
+ `;transition:all 0.3s ease;& svg{width:`,
195
+ `;height:`,
196
+ `;}`
197
+ ], ({ $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
+ var StyledDropPrompt = styled.span.withConfig({
199
+ displayName: "dropzone__StyledDropPrompt",
200
+ componentId: "sc-cf1c3c01-3"
201
+ })([
202
+ `display:flex;flex-direction:`,
203
+ `;flex-wrap:wrap;align-items:baseline;gap:`,
204
+ `;font-family:`,
205
+ `;font-size:`,
206
+ `;line-height:`,
207
+ `;font-weight:600;color:`,
208
+ `;& span{font-weight:500;color:`,
209
+ `;}`
210
+ ], ({ $inline }) => $inline ? "row" : "column", ({ $inline }) => $inline ? "0 6px" : "2px", ({ theme }) => theme.fonts.text, ({ theme }) => theme.fontSizes.text.xs, ({ theme }) => theme.lineHeights.text.xs, ({ theme }) => theme.colors.dark, ({ theme }) => theme.colors.grayDark);
211
+ var StyledDropHint = styled.span.withConfig({
212
+ displayName: "dropzone__StyledDropHint",
213
+ componentId: "sc-cf1c3c01-4"
214
+ })([
215
+ `font-family:`,
216
+ `;font-size:`,
217
+ `;line-height:`,
218
+ `;color:`,
219
+ `;`,
220
+ ``
221
+ ], ({ theme }) => theme.fonts.text, ({ theme }) => theme.fontSizes.small.xs, ({ theme }) => theme.lineHeights.small.xs, ({ theme }) => theme.colors.gray, ({ $inline }) => $inline && css([`margin-left:auto;`]));
222
+ var StyledThumbs = styled.div.withConfig({
223
+ displayName: "dropzone__StyledThumbs",
224
+ componentId: "sc-cf1c3c01-5"
225
+ })([`display:grid;grid-template-columns:repeat(auto-fill,72px);gap:10px;`, `{grid-template-columns:repeat(auto-fill,84px);}`], mq("sm"));
226
+ var StyledThumb = styled.div.withConfig({
227
+ displayName: "dropzone__StyledThumb",
228
+ componentId: "sc-cf1c3c01-6"
229
+ })([
230
+ `position:relative;width:72px;height:72px;border-radius:`,
231
+ `;overflow:hidden;border:solid 1px `,
232
+ `;background:`,
233
+ `;& img{width:100%;height:100%;object-fit:cover;display:block;}`,
234
+ `{width:84px;height:84px;}`
235
+ ], ({ theme }) => theme.spacing.radius.lg, ({ theme }) => theme.colors.grayLight, ({ theme }) => theme.colors.light, mq("sm"));
236
+ var StyledThumbFile = styled.span.withConfig({
237
+ displayName: "dropzone__StyledThumbFile",
238
+ componentId: "sc-cf1c3c01-7"
239
+ })([
240
+ `display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;width:100%;height:100%;padding:6px;color:`,
241
+ `;& svg{width:20px;height:20px;color:`,
242
+ `;}& span{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:`,
243
+ `;font-size:10px;line-height:1.2;}`
244
+ ], ({ theme }) => theme.colors.grayDark, ({ theme }) => theme.colors.primary, ({ theme }) => theme.fonts.text);
245
+ var StyledThumbRemove = styled.button.withConfig({
246
+ displayName: "dropzone__StyledThumbRemove",
247
+ componentId: "sc-cf1c3c01-8"
248
+ })([
249
+ ``,
250
+ `;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:`,
251
+ `;transition:background 0.3s ease;& svg{width:14px;height:14px;}@media (hover:hover){&:hover:not(:disabled){background:`,
252
+ `;}}&:disabled{cursor:not-allowed;opacity:0.6;}`
253
+ ], resetButton, ({ theme }) => rgba(theme.colors.dark, .6), ({ theme }) => theme.colors.error);
254
+ //#endregion
255
+ export { Dropzone, matchesAccept };
@@ -0,0 +1,11 @@
1
+ import { default as React } from 'react';
2
+ import { Theme } from './utils';
3
+ export interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
4
+ children?: React.ReactNode;
5
+ "aria-label": string;
6
+ $size?: "default" | "big" | "small";
7
+ $error?: boolean;
8
+ }
9
+ export declare const iconButtonStyles: (theme: Theme, $size?: "default" | "big" | "small", $error?: boolean, disabled?: boolean) => import('styled-components').RuleSet<object>;
10
+ declare const IconButton: React.ForwardRefExoticComponent<IconButtonProps & React.RefAttributes<HTMLButtonElement>>;
11
+ export { IconButton };
@@ -0,0 +1,72 @@
1
+ "use client";
2
+ "use client";
3
+ import { resetButton } from "./utils/mixins.js";
4
+ import { jsx } from "react/jsx-runtime";
5
+ import { forwardRef } from "react";
6
+ import styled, { css } from "styled-components";
7
+ import { darken, lighten } from "polished";
8
+ //#region src/lib/icon-button.tsx
9
+ var iconButtonSizes = {
10
+ small: {
11
+ box: 24,
12
+ icon: 12
13
+ },
14
+ default: {
15
+ box: 28,
16
+ icon: 14
17
+ },
18
+ big: {
19
+ box: 32,
20
+ icon: 16
21
+ }
22
+ };
23
+ var iconButtonStyles = (theme, $size, $error, disabled) => {
24
+ const { box, icon } = iconButtonSizes[$size ?? "default"];
25
+ return css([
26
+ ``,
27
+ `;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;width:`,
28
+ `px;height:`,
29
+ `px;border-radius:50%;background:`,
30
+ `;border:solid 2px `,
31
+ `;color:`,
32
+ `;box-shadow:0 0 0 0px `,
33
+ `;transition:all 0.3s ease;& svg{width:`,
34
+ `px;height:`,
35
+ `px;}`,
36
+ ` `,
37
+ ``
38
+ ], resetButton, box, box, theme.colors.light, theme.colors.grayLight, $error ? theme.colors.error : theme.colors.grayDark, $error ? lighten(.1, theme.colors.error) : theme.colors.primaryLight, icon, icon, !disabled && ($error ? css([
39
+ `border-color:`,
40
+ `;&:hover{border-color:`,
41
+ `;color:`,
42
+ `;}&:focus{box-shadow:0 0 0 4px `,
43
+ `;}&:active{box-shadow:0 0 0 2px `,
44
+ `;}`
45
+ ], theme.colors.error, darken(.1, theme.colors.error), darken(.1, theme.colors.error), lighten(.1, theme.colors.error), lighten(.1, theme.colors.error)) : css([
46
+ `&:hover{border-color:`,
47
+ `;color:`,
48
+ `;}&:focus{box-shadow:0 0 0 4px `,
49
+ `;border-color:`,
50
+ `;}&:active{box-shadow:0 0 0 2px `,
51
+ `;}`
52
+ ], theme.colors.primary, theme.colors.primary, theme.colors.primaryLight, theme.colors.primary, theme.colors.primaryLight)), disabled && css([
53
+ `cursor:not-allowed;background:`,
54
+ `;border-color:`,
55
+ `;color:`,
56
+ `;`
57
+ ], theme.colors.grayLight, theme.colors.grayLight, theme.colors.gray));
58
+ };
59
+ var StyledIconButton = styled.button.withConfig({
60
+ displayName: "icon-button__StyledIconButton",
61
+ componentId: "sc-def35ae6-0"
62
+ })([``, ``], ({ theme, $size, $error, disabled }) => iconButtonStyles(theme, $size, $error, disabled));
63
+ function LocalIconButton({ ...props }, ref) {
64
+ return /*#__PURE__*/ jsx(StyledIconButton, {
65
+ type: "button",
66
+ ...props,
67
+ ref
68
+ });
69
+ }
70
+ var IconButton = /*#__PURE__*/ forwardRef(LocalIconButton);
71
+ //#endregion
72
+ export { IconButton, iconButtonStyles };
package/dist/index.d.ts CHANGED
@@ -1,16 +1,23 @@
1
1
  export * from './styled-components';
2
2
  export * from './utils';
3
+ export * from './accordion';
4
+ export * from './avatar-dropzone';
3
5
  export * from './box';
4
6
  export * from './button';
5
7
  export * from './col';
6
8
  export * from './container';
9
+ export * from './dropzone';
7
10
  export * from './flex';
8
11
  export * from './grid';
9
12
  export * from './icon';
13
+ export * from './icon-button';
10
14
  export * from './input';
11
15
  export * from './max-width';
16
+ export * from './modal';
17
+ export * from './password';
12
18
  export * from './range';
13
19
  export * from './select';
14
20
  export * from './space';
15
21
  export * from './textarea';
22
+ export * from './toast';
16
23
  export * from './toggle';
package/dist/index.js CHANGED
@@ -4,19 +4,27 @@ import { IconArrow, IconCalendar, IconCheck } from "./utils/icons.js";
4
4
  import { breakpoints, colors, colorsDark, fontSizes, fonts, lineHeights, mq, shadows, shadowsDark, spacing, theme, themeDark } from "./utils/theme.js";
5
5
  import { formElementHeightStyles, fullWidthStyles, generateAlignContentStyles, generateAlignItemsStyles, generateColSpanStyles, generateColsStyles, generateDirectionStyles, generateGapStyles, generateJustifyContentStyles, generatePaddingStyles, resetButton, resetInput, statusBorderStyles } from "./utils/mixins.js";
6
6
  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
+ import { useOnClickOutside } from "./utils/use-on-click-outside.js";
7
8
  import { CherryThemeProvider, ThemeContext } from "./styled-components/theme-provider.js";
9
+ import { Icon } from "./icon.js";
10
+ import { Accordion } from "./accordion.js";
11
+ import { Dropzone, matchesAccept } from "./dropzone.js";
12
+ import { AvatarDropzone } from "./avatar-dropzone.js";
8
13
  import { Container } from "./container.js";
9
14
  import { Box } from "./box.js";
10
15
  import { Button, buttonStyles } from "./button.js";
11
16
  import { Col } from "./col.js";
12
17
  import { Flex } from "./flex.js";
13
18
  import { Grid } from "./grid.js";
14
- import { Icon } from "./icon.js";
19
+ import { IconButton, iconButtonStyles } from "./icon-button.js";
15
20
  import { Input, StyledInputWrapper, StyledLabel } from "./input.js";
16
21
  import { MaxWidth } from "./max-width.js";
22
+ import { Modal } from "./modal.js";
23
+ import { Password } from "./password.js";
17
24
  import { Range } from "./range.js";
18
25
  import { Select, StyledIconWrapper } from "./select.js";
19
26
  import { Space } from "./space.js";
20
27
  import { Textarea } from "./textarea.js";
28
+ import { StyledNotifications, ToastNotifications, ToastNotificationsContext, ToastNotificationsProvider, useToastNotifications } from "./toast.js";
21
29
  import { Toggle } from "./toggle.js";
22
- export { Box, Button, CherryThemeProvider, Col, Container, Flex, GlobalStyles, Grid, Icon, IconArrow, IconCalendar, IconCheck, Input, MaxWidth, Range, Select, Space, StyledComponentsRegistry, StyledIconWrapper, StyledInputWrapper, StyledLabel, Textarea, ThemeContext, Toggle, breakpoints, buttonStyles, colors, colorsDark, createTypographyStyle, fontSizes, fonts, formElementHeightStyles, fullWidthStyles, generateAlignContentStyles, generateAlignItemsStyles, generateColSpanStyles, generateColsStyles, generateDirectionStyles, generateGapStyles, generateJustifyContentStyles, generatePaddingStyles, lineHeights, 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 };
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 };
package/dist/input.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { default as React } from 'react';
2
2
  import { IStyledComponent } from 'styled-components';
3
- interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
3
+ export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
4
4
  children?: React.ReactNode;
5
5
  $wrapperClassName?: string;
6
6
  $label?: string;
package/dist/input.js CHANGED
@@ -8,7 +8,7 @@ import styled, { css } from "styled-components";
8
8
  //#region src/lib/input.tsx
9
9
  var StyledInputWrapper = styled.span.withConfig({
10
10
  displayName: "input__StyledInputWrapper",
11
- componentId: "sc-df1aa532-0"
11
+ componentId: "sc-f9d22714-0"
12
12
  })([
13
13
  `display:inline-flex;flex-wrap:`,
14
14
  `;align-items:center;`,
@@ -17,7 +17,7 @@ var StyledInputWrapper = styled.span.withConfig({
17
17
  ], ({ type }) => type === "checkbox" || type === "radio" ? "nowrap" : "wrap", ({ $label }) => $label && css([`gap:5px;align-items:flex-start;align-content:flex-start;`]), ({ $fullWidth }) => fullWidthStyles($fullWidth));
18
18
  var StyledLabel = styled.label.withConfig({
19
19
  displayName: "input__StyledLabel",
20
- componentId: "sc-df1aa532-1"
20
+ componentId: "sc-f9d22714-1"
21
21
  })([
22
22
  `display:inline-block;color:`,
23
23
  `;font-size:`,
@@ -26,7 +26,7 @@ var StyledLabel = styled.label.withConfig({
26
26
  ], ({ theme }) => theme.colors.grayDark, ({ theme }) => theme.fontSizes.small.lg, ({ theme }) => theme.lineHeights.small.lg);
27
27
  var StyledInput = styled.input.withConfig({
28
28
  displayName: "input__StyledInput",
29
- componentId: "sc-df1aa532-2"
29
+ componentId: "sc-f9d22714-2"
30
30
  })([
31
31
  ``,
32
32
  `;`,
@@ -76,11 +76,11 @@ var StyledInput = styled.input.withConfig({
76
76
  `, ({ $fullWidth }) => fullWidthStyles($fullWidth));
77
77
  var StyledIconWrapper = styled.span.withConfig({
78
78
  displayName: "input__StyledIconWrapper",
79
- componentId: "sc-df1aa532-3"
79
+ componentId: "sc-f9d22714-3"
80
80
  })([`display:inline-flex;position:relative;line-height:0;min-width:fit-content;& em{display:block;border-radius:50%;background:`, `;}& svg,& em{object-fit:contain;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%) scale(0.7);opacity:0;pointer-events:none;transition:all 0.3s ease;}`], ({ theme }) => theme.colors.primary);
81
81
  var StyledRadioCheckboxInput = styled.input.withConfig({
82
82
  displayName: "input__StyledRadioCheckboxInput",
83
- componentId: "sc-df1aa532-4"
83
+ componentId: "sc-f9d22714-4"
84
84
  })([
85
85
  ``,
86
86
  `;display:inline-block;background:`,
@@ -165,7 +165,7 @@ var StyledRadioCheckboxInput = styled.input.withConfig({
165
165
  });
166
166
  var StyledCustomIconWrapper = styled.span.withConfig({
167
167
  displayName: "input__StyledCustomIconWrapper",
168
- componentId: "sc-df1aa532-5"
168
+ componentId: "sc-f9d22714-5"
169
169
  })([
170
170
  `position:relative;`,
171
171
  `;& svg{position:absolute;top:50%;transform:translateY(-50%);pointer-events:none;width:24px;height:24px;object-fit:contain;color:`,
@@ -0,0 +1,10 @@
1
+ import { default as React } from 'react';
2
+ export interface ModalProps {
3
+ children: React.ReactNode;
4
+ $isOpen: boolean;
5
+ $onClose: () => void;
6
+ $title?: string;
7
+ $width?: number;
8
+ }
9
+ declare function Modal({ children, $isOpen, $onClose, $title, $width }: ModalProps): React.ReactPortal | null;
10
+ export { Modal };
package/dist/modal.js ADDED
@@ -0,0 +1,96 @@
1
+ "use client";
2
+ "use client";
3
+ import { mq } from "./utils/theme.js";
4
+ import { styledH5, styledText } from "./utils/typography.js";
5
+ import { useOnClickOutside } from "./utils/use-on-click-outside.js";
6
+ import { Icon } from "./icon.js";
7
+ import { IconButton } from "./icon-button.js";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
10
+ import styled, { css } from "styled-components";
11
+ import { rgba } from "polished";
12
+ import { createPortal } from "react-dom";
13
+ //#region src/lib/modal.tsx
14
+ var emptySubscribe = () => () => {};
15
+ function useIsClient() {
16
+ return useSyncExternalStore(emptySubscribe, () => true, () => false);
17
+ }
18
+ var StyledModal = styled.div.withConfig({
19
+ displayName: "modal__StyledModal",
20
+ componentId: "sc-43ca5273-0"
21
+ })([
22
+ `position:fixed;top:0;left:0;width:100%;height:100%;background:`,
23
+ `;display:flex;justify-content:center;align-items:center;z-index:1010;pointer-events:none;opacity:0;transition:all 0.3s ease;`,
24
+ ` & .modal-inner{background:`,
25
+ `;border-radius:`,
26
+ `;padding:20px;max-width:calc(100% - 40px);width:100%;margin:auto;position:relative;transform:translateY(40px);transition:all 0.3s ease;`,
27
+ ` `,
28
+ `{max-width:500px;`,
29
+ `}}`
30
+ ], ({ theme }) => rgba(theme.colors.primary, .5), ({ $isOpen }) => $isOpen && css([`opacity:1;pointer-events:all;`]), ({ theme }) => theme.colors.light, ({ theme }) => theme.spacing.radius.lg, ({ $isOpen }) => $isOpen && css([`transform:translateY(0);`]), mq("lg"), ({ $width }) => $width && css([`max-width:`, `px;`], $width));
31
+ var StyledModalClose = styled(IconButton).withConfig({
32
+ displayName: "modal__StyledModalClose",
33
+ componentId: "sc-43ca5273-1"
34
+ })([`position:absolute;top:20px;right:20px;z-index:10;`]);
35
+ var StyledModalTitle = styled.h2.withConfig({
36
+ displayName: "modal__StyledModalTitle",
37
+ componentId: "sc-43ca5273-2"
38
+ })([
39
+ `--divider-color:`,
40
+ `;margin:0 0 15px 0;padding:0 0 15px 0;color:`,
41
+ `;position:relative;`,
42
+ `;&::after{content:"";position:absolute;bottom:0;left:0;width:100%;height:1px;background:var(--divider-color);}`
43
+ ], ({ theme }) => theme.colors.grayLight, ({ theme }) => theme.colors.dark, ({ theme }) => styledH5(theme));
44
+ var StyledModalContent = styled.div.withConfig({
45
+ displayName: "modal__StyledModalContent",
46
+ componentId: "sc-43ca5273-3"
47
+ })([
48
+ `max-height:calc(100svh - 200px);overflow-y:auto;padding:5px;margin:-5px;--divider-color:`,
49
+ `;`,
50
+ `;& hr{margin:20px 0;border:none;border-bottom:solid 1px var(--divider-color);}`
51
+ ], ({ theme }) => theme.colors.grayLight, ({ theme }) => styledText(theme));
52
+ function Modal({ children, $isOpen, $onClose, $title, $width }) {
53
+ const wrapperRef = useRef(null);
54
+ const elmRef = useRef(null);
55
+ const isClient = useIsClient();
56
+ const closeModal = useCallback(() => {
57
+ $onClose();
58
+ }, [$onClose]);
59
+ useEffect(() => {
60
+ if (!$isOpen) return;
61
+ function handleKeydown(event) {
62
+ if (event.key === "Escape") {
63
+ event.preventDefault();
64
+ closeModal();
65
+ }
66
+ }
67
+ document.addEventListener("keydown", handleKeydown);
68
+ return () => document.removeEventListener("keydown", handleKeydown);
69
+ }, [$isOpen, closeModal]);
70
+ useOnClickOutside([elmRef, wrapperRef], $isOpen ? closeModal : () => {});
71
+ if (!isClient) return null;
72
+ return /*#__PURE__*/ createPortal(/*#__PURE__*/ jsx(StyledModal, {
73
+ $isOpen,
74
+ $width,
75
+ children: /*#__PURE__*/ jsxs("div", {
76
+ className: "modal-inner",
77
+ ref: wrapperRef,
78
+ children: [
79
+ /*#__PURE__*/ jsx("span", {
80
+ ref: elmRef,
81
+ children: /*#__PURE__*/ jsx(StyledModalClose, {
82
+ $size: "small",
83
+ onClick: closeModal,
84
+ className: "modal-close",
85
+ "aria-label": "Close Modal",
86
+ children: /*#__PURE__*/ jsx(Icon, { name: "X" })
87
+ })
88
+ }),
89
+ $title && /*#__PURE__*/ jsx(StyledModalTitle, { children: $title }),
90
+ /*#__PURE__*/ jsx(StyledModalContent, { children })
91
+ ]
92
+ })
93
+ }), document.body);
94
+ }
95
+ //#endregion
96
+ export { Modal };
@@ -0,0 +1,5 @@
1
+ import { default as React } from 'react';
2
+ import { InputProps } from './input';
3
+ export type PasswordProps = Omit<InputProps, "type" | "$icon" | "$iconPosition">;
4
+ declare const Password: React.ForwardRefExoticComponent<PasswordProps & React.RefAttributes<HTMLInputElement>>;
5
+ export { Password };
@@ -0,0 +1,75 @@
1
+ "use client";
2
+ "use client";
3
+ import { fullWidthStyles } from "./utils/mixins.js";
4
+ import { Icon } from "./icon.js";
5
+ import { IconButton } from "./icon-button.js";
6
+ import { Input } from "./input.js";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ import { forwardRef, useState } from "react";
9
+ import styled, { css } from "styled-components";
10
+ //#region src/lib/password.tsx
11
+ var passwordSizes = {
12
+ small: {
13
+ height: 40,
14
+ inset: 8,
15
+ button: 24
16
+ },
17
+ default: {
18
+ height: 50,
19
+ inset: 11,
20
+ button: 28
21
+ },
22
+ big: {
23
+ height: 60,
24
+ inset: 16,
25
+ button: 28
26
+ }
27
+ };
28
+ var StyledPassword = styled.span.withConfig({
29
+ displayName: "password__StyledPassword",
30
+ componentId: "sc-b3beabaa-0"
31
+ })([
32
+ `position:relative;display:inline-flex;& input{padding-right:`,
33
+ `;}`,
34
+ ``
35
+ ], ({ $size }) => {
36
+ const { inset, button } = passwordSizes[$size ?? "default"];
37
+ return `${inset + button + 10}px`;
38
+ }, ({ $fullWidth }) => fullWidthStyles($fullWidth));
39
+ var StyledPasswordReveal = styled.span.withConfig({
40
+ displayName: "password__StyledPasswordReveal",
41
+ componentId: "sc-b3beabaa-1"
42
+ })([`position:absolute;bottom:0;display:flex;align-items:center;`, ``], ({ $size }) => {
43
+ const { height, inset } = passwordSizes[$size ?? "default"];
44
+ return css([
45
+ `right:`,
46
+ `px;height:`,
47
+ `px;`
48
+ ], inset, height);
49
+ });
50
+ function LocalPassword({ ...props }, ref) {
51
+ const [show, setShow] = useState(false);
52
+ return /*#__PURE__*/ jsxs(StyledPassword, {
53
+ $size: props.$size,
54
+ $fullWidth: props.$fullWidth,
55
+ children: [/*#__PURE__*/ jsx(Input, {
56
+ ...props,
57
+ type: show ? "text" : "password",
58
+ ref
59
+ }), /*#__PURE__*/ jsx(StyledPasswordReveal, {
60
+ $size: props.$size,
61
+ children: /*#__PURE__*/ jsx(IconButton, {
62
+ $size: props.$size === "small" ? "small" : "default",
63
+ $error: props.$error,
64
+ "aria-label": show ? "Hide password" : "Show password",
65
+ "aria-pressed": show,
66
+ title: show ? "Hide password" : "Show password",
67
+ onClick: () => setShow((prev) => !prev),
68
+ children: /*#__PURE__*/ jsx(Icon, { name: show ? "EyeOff" : "Eye" })
69
+ })
70
+ })]
71
+ });
72
+ }
73
+ var Password = /*#__PURE__*/ forwardRef(LocalPassword);
74
+ //#endregion
75
+ export { Password };
@@ -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';
@@ -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.1.19",
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",
@@ -41,7 +41,7 @@
41
41
  "format": "prettier --write ."
42
42
  },
43
43
  "dependencies": {
44
- "lucide-react": "^1.21.0",
44
+ "lucide-react": "^1.23.0",
45
45
  "polished": "^4.3.1"
46
46
  },
47
47
  "peerDependencies": {
@@ -50,21 +50,21 @@
50
50
  "styled-components": "^6.0.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@swc/plugin-styled-components": "^12.14.0",
54
- "@types/node": "^26.0.1",
53
+ "@swc/plugin-styled-components": "^12.14.1",
54
+ "@types/node": "^26.1.0",
55
55
  "@types/react": "^19.2.17",
56
56
  "@types/react-dom": "^19.2.3",
57
57
  "@vitejs/plugin-react-swc": "^4.3.1",
58
58
  "eslint-plugin-react-hooks": "^7.1.1",
59
59
  "eslint-plugin-react-refresh": "^0.5.3",
60
- "next": "^16.2.9",
61
- "prettier": "^3.9.1",
60
+ "next": "^16.2.10",
61
+ "prettier": "^3.9.4",
62
62
  "react": "^19.2.7",
63
63
  "react-dom": "^19.2.7",
64
64
  "rollup-plugin-preserve-directives": "^0.4.0",
65
65
  "styled-components": "^6.4.3",
66
66
  "typescript": "^6.0.3",
67
- "vite": "^8.1.0",
67
+ "vite": "^8.1.3",
68
68
  "vite-plugin-dts": "^5.0.3"
69
69
  }
70
70
  }