cherry-styled-components 0.1.17 → 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.
Files changed (61) hide show
  1. package/dist/accordion.d.ts +11 -0
  2. package/dist/accordion.js +88 -0
  3. package/dist/avatar-dropzone.d.ts +19 -0
  4. package/dist/avatar-dropzone.js +157 -0
  5. package/dist/box.js +2 -2
  6. package/dist/button.js +3 -3
  7. package/dist/col.js +2 -2
  8. package/dist/container.js +2 -2
  9. package/dist/dropzone.d.ts +35 -0
  10. package/dist/dropzone.js +255 -0
  11. package/dist/flex.d.ts +53 -0
  12. package/dist/flex.js +28 -5
  13. package/dist/grid.js +2 -2
  14. package/dist/icon-button.d.ts +11 -0
  15. package/dist/icon-button.js +72 -0
  16. package/dist/icon.js +1 -1
  17. package/dist/{src/lib/index.d.ts → index.d.ts} +7 -0
  18. package/dist/index.js +11 -3
  19. package/dist/{src/lib/input.d.ts → input.d.ts} +1 -1
  20. package/dist/input.js +15 -15
  21. package/dist/max-width.js +2 -2
  22. package/dist/modal.d.ts +10 -0
  23. package/dist/modal.js +96 -0
  24. package/dist/password.d.ts +5 -0
  25. package/dist/password.js +75 -0
  26. package/dist/range.js +4 -4
  27. package/dist/select.js +6 -6
  28. package/dist/space.js +2 -2
  29. package/dist/styled-components/registry.js +2 -2
  30. package/dist/styled-components/theme-provider.js +4 -4
  31. package/dist/textarea.js +4 -4
  32. package/dist/toast.d.ts +40 -0
  33. package/dist/toast.js +115 -0
  34. package/dist/toggle.js +5 -5
  35. package/dist/utils/icons.js +15 -15
  36. package/dist/{src/lib/utils → utils}/index.d.ts +1 -0
  37. package/dist/{src/lib/utils → utils}/mixins.d.ts +3 -0
  38. package/dist/utils/mixins.js +16 -1
  39. package/dist/utils/use-on-click-outside.d.ts +2 -0
  40. package/dist/utils/use-on-click-outside.js +17 -0
  41. package/package.json +16 -16
  42. package/dist/src/lib/flex.d.ts +0 -27
  43. /package/dist/{src/lib/box.d.ts → box.d.ts} +0 -0
  44. /package/dist/{src/lib/button.d.ts → button.d.ts} +0 -0
  45. /package/dist/{src/lib/col.d.ts → col.d.ts} +0 -0
  46. /package/dist/{src/lib/container.d.ts → container.d.ts} +0 -0
  47. /package/dist/{src/lib/grid.d.ts → grid.d.ts} +0 -0
  48. /package/dist/{src/lib/icon.d.ts → icon.d.ts} +0 -0
  49. /package/dist/{src/lib/max-width.d.ts → max-width.d.ts} +0 -0
  50. /package/dist/{src/lib/range.d.ts → range.d.ts} +0 -0
  51. /package/dist/{src/lib/select.d.ts → select.d.ts} +0 -0
  52. /package/dist/{src/lib/space.d.ts → space.d.ts} +0 -0
  53. /package/dist/{src/lib/styled-components → styled-components}/index.d.ts +0 -0
  54. /package/dist/{src/lib/styled-components → styled-components}/registry.d.ts +0 -0
  55. /package/dist/{src/lib/styled-components → styled-components}/theme-provider.d.ts +0 -0
  56. /package/dist/{src/lib/textarea.d.ts → textarea.d.ts} +0 -0
  57. /package/dist/{src/lib/toggle.d.ts → toggle.d.ts} +0 -0
  58. /package/dist/{src/lib/utils → utils}/global.d.ts +0 -0
  59. /package/dist/{src/lib/utils → utils}/icons.d.ts +0 -0
  60. /package/dist/{src/lib/utils → utils}/theme.d.ts +0 -0
  61. /package/dist/{src/lib/utils → utils}/typography.d.ts +0 -0
@@ -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/box.js CHANGED
@@ -15,12 +15,12 @@ var StyledBox = styled(Container).withConfig({
15
15
  `;`
16
16
  ], ({ theme }) => theme.colors.light, ({ theme }) => theme.spacing.radius.lg, ({ theme }) => theme.colors.grayLight);
17
17
  function LocalBox({ ...props }, ref) {
18
- return /* @__PURE__ */ jsx(StyledBox, {
18
+ return /*#__PURE__*/ jsx(StyledBox, {
19
19
  ...props,
20
20
  ref,
21
21
  children: props.children
22
22
  });
23
23
  }
24
- var Box = /* @__PURE__ */ forwardRef(LocalBox);
24
+ var Box = /*#__PURE__*/ forwardRef(LocalBox);
25
25
  //#endregion
26
26
  export { Box };
package/dist/button.js CHANGED
@@ -77,10 +77,10 @@ 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-f54d90ca-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
- return /* @__PURE__ */ jsxs(StyledButton, {
83
+ return /*#__PURE__*/ jsxs(StyledButton, {
84
84
  $variant,
85
85
  $error: props.$error,
86
86
  ...props,
@@ -92,6 +92,6 @@ function LocalButton({ $variant = "primary", ...props }, ref) {
92
92
  ]
93
93
  });
94
94
  }
95
- var Button = /* @__PURE__ */ forwardRef(LocalButton);
95
+ var Button = /*#__PURE__*/ forwardRef(LocalButton);
96
96
  //#endregion
97
97
  export { Button, buttonStyles };
package/dist/col.js CHANGED
@@ -22,12 +22,12 @@ var StyledCol = styled.div.withConfig({
22
22
  grid-column: span ${$span};
23
23
  `, ({ $xsSpan }) => $xsSpan && generateColSpanStyles("xs", $xsSpan), ({ $smSpan }) => $smSpan && generateColSpanStyles("sm", $smSpan), ({ $mdSpan }) => $mdSpan && generateColSpanStyles("md", $mdSpan), ({ $lgSpan }) => $lgSpan && generateColSpanStyles("lg", $lgSpan), ({ $xlSpan }) => $xlSpan && generateColSpanStyles("xl", $xlSpan), ({ $xxlSpan }) => $xxlSpan && generateColSpanStyles("xxl", $xxlSpan), ({ $xxxlSpan }) => $xxxlSpan && generateColSpanStyles("xxxl", $xxxlSpan));
24
24
  function LocalCol({ ...props }, ref) {
25
- return /* @__PURE__ */ jsx(StyledCol, {
25
+ return /*#__PURE__*/ jsx(StyledCol, {
26
26
  ...props,
27
27
  ref,
28
28
  children: props.children
29
29
  });
30
30
  }
31
- var Col = /* @__PURE__ */ forwardRef(LocalCol);
31
+ var Col = /*#__PURE__*/ forwardRef(LocalCol);
32
32
  //#endregion
33
33
  export { Col };
package/dist/container.js CHANGED
@@ -27,12 +27,12 @@ var StyledContainer = styled.div.withConfig({
27
27
  ``
28
28
  ], ({ theme, $fluid }) => $fluid ? `100%` : theme.spacing.maxWidth.xs, ({ $textAlign }) => $textAlign && `text-align: ${$textAlign}`, ({ $padding, theme }) => $padding && `${$padding}px` || theme.spacing.padding.xs, mq("lg"), ({ $padding, theme }) => $padding && `${$padding}px` || theme.spacing.padding.lg, mq("xxxl"), ({ theme, $fluid }) => $fluid ? `100%` : theme.spacing.maxWidth.xxxl, ({ $xsPadding }) => $xsPadding && generatePaddingStyles("xs", $xsPadding), ({ $smPadding }) => $smPadding && generatePaddingStyles("sm", $smPadding), ({ $mdPadding }) => $mdPadding && generatePaddingStyles("md", $mdPadding), ({ $lgPadding }) => $lgPadding && generatePaddingStyles("lg", $lgPadding), ({ $xlPadding }) => $xlPadding && generatePaddingStyles("xl", $xlPadding), ({ $xxlPadding }) => $xxlPadding && generatePaddingStyles("xxl", $xxlPadding), ({ $xxxlPadding }) => $xxxlPadding && generatePaddingStyles("xxxl", $xxxlPadding));
29
29
  function LocalContainer({ ...props }, ref) {
30
- return /* @__PURE__ */ jsx(StyledContainer, {
30
+ return /*#__PURE__*/ jsx(StyledContainer, {
31
31
  ...props,
32
32
  ref,
33
33
  children: props.children
34
34
  });
35
35
  }
36
- var Container = /* @__PURE__ */ forwardRef(LocalContainer);
36
+ var Container = /*#__PURE__*/ forwardRef(LocalContainer);
37
37
  //#endregion
38
38
  export { Container };
@@ -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 };