cherry-styled-components 0.1.18 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -0
- package/dist/accordion.d.ts +11 -0
- package/dist/accordion.js +88 -0
- package/dist/avatar-dropzone.d.ts +19 -0
- package/dist/avatar-dropzone.js +157 -0
- package/dist/button.js +1 -1
- package/dist/dropzone.d.ts +35 -0
- package/dist/dropzone.js +255 -0
- package/dist/icon-button.d.ts +11 -0
- package/dist/icon-button.js +72 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +15 -4
- package/dist/input.d.ts +1 -1
- package/dist/input.js +6 -6
- package/dist/modal.d.ts +10 -0
- package/dist/modal.js +96 -0
- package/dist/password.d.ts +5 -0
- package/dist/password.js +75 -0
- package/dist/styled-components/client-theme-provider.d.ts +35 -0
- package/dist/styled-components/client-theme-provider.js +103 -0
- package/dist/styled-components/index.d.ts +2 -0
- package/dist/styled-components/registry.js +1 -1
- package/dist/styled-components/theme-init.d.ts +19 -0
- package/dist/styled-components/theme-init.js +22 -0
- package/dist/styled-components/theme-provider.d.ts +1 -0
- package/dist/styled-components/theme-provider.js +15 -2
- package/dist/theme-toggle.d.ts +6 -0
- package/dist/theme-toggle.js +42 -0
- package/dist/toast.d.ts +40 -0
- package/dist/toast.js +115 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/mixins.d.ts +17 -0
- package/dist/utils/mixins.js +27 -1
- package/dist/utils/use-on-click-outside.d.ts +2 -0
- package/dist/utils/use-on-click-outside.js +17 -0
- package/package.json +11 -8
package/README.md
CHANGED
|
@@ -28,6 +28,164 @@ To build the library for production, use the following command:
|
|
|
28
28
|
npm run build
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
## Theming
|
|
32
|
+
|
|
33
|
+
Cherry ships two theme providers:
|
|
34
|
+
|
|
35
|
+
- `CherryThemeProvider` - the original client-only provider. It resolves the theme from `localStorage` and the OS preference after mount. Simple, but in a server-rendered app the first paint is always the light theme.
|
|
36
|
+
- `ClientThemeProvider` - the SSR-aware provider for flash-free dark mode. The server resolves the `theme` cookie and passes `$initial`, so the first paint is already correct. On mount it reconciles against the cookie and OS preference (Safari and Firefox don't send color-scheme client hints, so their first server render can guess wrong) and swaps in place. Theme changes persist to the `theme` cookie and `localStorage` directly; no API route is needed.
|
|
37
|
+
|
|
38
|
+
Both providers expose `setTheme` and `toggleTheme` through `ThemeContext`, and the `ThemeToggle` component works under either.
|
|
39
|
+
|
|
40
|
+
A Next.js App Router setup looks like this:
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
// app/layout.tsx (server component)
|
|
44
|
+
import { cookies } from "next/headers";
|
|
45
|
+
import {
|
|
46
|
+
ClientThemeProvider,
|
|
47
|
+
StyledComponentsRegistry,
|
|
48
|
+
themeInitScript,
|
|
49
|
+
} from "cherry-styled-components";
|
|
50
|
+
import { theme, themeDark } from "./theme";
|
|
51
|
+
|
|
52
|
+
export default async function RootLayout({ children }) {
|
|
53
|
+
const cookieTheme = (await cookies()).get("theme")?.value;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<html lang="en">
|
|
57
|
+
<head>
|
|
58
|
+
{/* Seeds the theme cookie and prevents the dark-mode flash in
|
|
59
|
+
browsers without color-scheme client hints. */}
|
|
60
|
+
<script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
|
|
61
|
+
</head>
|
|
62
|
+
<body>
|
|
63
|
+
<StyledComponentsRegistry>
|
|
64
|
+
<ClientThemeProvider
|
|
65
|
+
theme={theme}
|
|
66
|
+
themeDark={themeDark}
|
|
67
|
+
$initial={cookieTheme === "dark" ? "dark" : "light"}
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
</ClientThemeProvider>
|
|
71
|
+
</StyledComponentsRegistry>
|
|
72
|
+
</body>
|
|
73
|
+
</html>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For the best first-visit experience in Chrome, also opt into color-scheme client hints in your middleware so the very first server render matches the OS preference:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// middleware.ts
|
|
82
|
+
res.headers.set("Accept-CH", "Sec-CH-Prefers-Color-Scheme");
|
|
83
|
+
res.headers.set("Vary", "Sec-CH-Prefers-Color-Scheme");
|
|
84
|
+
res.headers.set("Critical-CH", "Sec-CH-Prefers-Color-Scheme");
|
|
85
|
+
|
|
86
|
+
const hint = req.headers.get("Sec-CH-Prefers-Color-Scheme");
|
|
87
|
+
if (!req.cookies.get("theme")?.value && hint) {
|
|
88
|
+
res.cookies.set("theme", hint === "dark" ? "dark" : "light", {
|
|
89
|
+
path: "/",
|
|
90
|
+
maxAge: 60 * 60 * 24 * 365,
|
|
91
|
+
sameSite: "lax",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`resolveTheme(cookieValue, theme, themeDark)` is exported for other server code that needs the active theme object, e.g. `generateViewport` for the initial `theme-color`.
|
|
97
|
+
|
|
98
|
+
In a client-only app (like this repo's demo, see `src/main.tsx`), resolve the initial theme synchronously from the cookie, `localStorage`, or `matchMedia` before rendering and pass it as `$initial`.
|
|
99
|
+
|
|
100
|
+
## Component Previews
|
|
101
|
+
|
|
102
|
+
The dev server ships with a preview route that renders a single component in isolation, centered on the page. It is meant for visual inspection and for taking automated screenshots (e.g. with Playwright).
|
|
103
|
+
|
|
104
|
+
With `npm run dev` running:
|
|
105
|
+
|
|
106
|
+
- `/preview` shows an index page linking to every available preview.
|
|
107
|
+
- `/preview/<name>` renders one component centered inside a wrapper with the stable selector `#preview-box`.
|
|
108
|
+
|
|
109
|
+
The route is handled in `src/main.tsx` and the previews live in `src/preview.tsx`. Both are part of the demo app only and are not included in the library build.
|
|
110
|
+
|
|
111
|
+
### Available previews
|
|
112
|
+
|
|
113
|
+
| Name | Renders |
|
|
114
|
+
| ------------------ | ------------------------------------------------- |
|
|
115
|
+
| `accordion` | Card accordion, open by default |
|
|
116
|
+
| `accordion-inline` | Inline accordion variant, open by default |
|
|
117
|
+
| `avatar-dropzone` | Empty avatar dropzone, default size |
|
|
118
|
+
| `box` | Box surface with a placeholder block |
|
|
119
|
+
| `button` | Primary filled button |
|
|
120
|
+
| `button-secondary` | Secondary filled button |
|
|
121
|
+
| `button-tertiary` | Tertiary filled button |
|
|
122
|
+
| `button-outline` | Primary outline button |
|
|
123
|
+
| `checkbox` | Checked checkbox |
|
|
124
|
+
| `dropzone` | Block dropzone with prompt, browse, and hint text |
|
|
125
|
+
| `dropzone-inline` | Inline dropzone variant |
|
|
126
|
+
| `flex` | Flex row with three placeholder blocks |
|
|
127
|
+
| `grid` | Three-column grid with a full-width row |
|
|
128
|
+
| `icon` | Cherry icon at 48px |
|
|
129
|
+
| `icon-button` | Icon button with a settings icon |
|
|
130
|
+
| `input` | Text input with label and placeholder |
|
|
131
|
+
| `modal` | Modal, already open on a backdrop |
|
|
132
|
+
| `password` | Password field with label and placeholder |
|
|
133
|
+
| `radio` | Checked radio button |
|
|
134
|
+
| `range` | Range slider |
|
|
135
|
+
| `select` | Select with label |
|
|
136
|
+
| `textarea` | Textarea with label and value |
|
|
137
|
+
| `theme-toggle` | Pill-shaped sun/moon theme switch |
|
|
138
|
+
| `toast` | Success, error, and info toasts, fired on mount |
|
|
139
|
+
| `toggle` | Checked toggle |
|
|
140
|
+
|
|
141
|
+
Pure layout primitives with no visual output of their own (Container, Col, MaxWidth, Space) have no preview entry.
|
|
142
|
+
|
|
143
|
+
### Forcing a theme
|
|
144
|
+
|
|
145
|
+
Append `?theme=dark` or `?theme=light` to any preview URL to force a theme:
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
/preview/button?theme=dark
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The value is persisted to the `theme` cookie and `localStorage`, exactly like a `ThemeToggle` click, and the demo resolves it synchronously before the first render. It persists for subsequent navigations in the same browser context, so always pass the parameter explicitly when comparing themes to avoid leakage from a previous visit.
|
|
152
|
+
|
|
153
|
+
### Taking screenshots
|
|
154
|
+
|
|
155
|
+
`scripts/screenshot-previews.cjs` captures every preview in both themes and writes the framed images used by the documentation site. With the dev server running in another terminal:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
pnpm run dev
|
|
159
|
+
pnpm run screenshots
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
For each component it produces `<name>-light.png` and `<name>-dark.png` at 2x resolution (`deviceScaleFactor: 2`), then post-processes every image with sharp:
|
|
163
|
+
|
|
164
|
+
- Corners are rounded with the theme's `radius.lg` (12px, so 24px at 2x) and made transparent outside the radius.
|
|
165
|
+
- A 1px solid border is drawn in the theme's `grayLight` color: `#e5e7eb` for light, `#1a1a1a` for dark.
|
|
166
|
+
|
|
167
|
+
The border and radius are baked into the PNG rather than applied in CSS so that all images get the identical frame, including the two special captures:
|
|
168
|
+
|
|
169
|
+
- `modal` overlays the whole viewport, so it is captured as a full-page screenshot to include the backdrop. It is shot on a desktop-size viewport (1100px wide) so the modal's built-in `max-width: 500px` (active from the `lg` breakpoint) applies.
|
|
170
|
+
- `toast` renders in a corner outside `#preview-box`, so the toast list element is captured instead, with 40px of padding injected at capture time for breathing room.
|
|
171
|
+
|
|
172
|
+
Everything else is cropped to the `#preview-box` element. Animated previews (accordion, modal, toast) get a one-second settle delay before capture.
|
|
173
|
+
|
|
174
|
+
The script drives your installed Chrome through `playwright-core` (no browser download needed) and defaults can be overridden with env vars:
|
|
175
|
+
|
|
176
|
+
| Variable | Default |
|
|
177
|
+
| ------------- | -------------------------------------------------------------- |
|
|
178
|
+
| `PREVIEW_URL` | `http://localhost:5173/preview` |
|
|
179
|
+
| `OUT_DIR` | `../cherry-documentation/public/components` |
|
|
180
|
+
| `CHROME_PATH` | `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome` |
|
|
181
|
+
|
|
182
|
+
For one-off manual screenshots, target the `#preview-box` selector:
|
|
183
|
+
|
|
184
|
+
```js
|
|
185
|
+
await page.goto("http://localhost:5173/preview/button?theme=light");
|
|
186
|
+
await page.locator("#preview-box").screenshot({ path: "button-light.png" });
|
|
187
|
+
```
|
|
188
|
+
|
|
31
189
|
## Community
|
|
32
190
|
|
|
33
191
|
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
|
|
@@ -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-
|
|
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 };
|