@neoptocom/neopto-ui 0.9.1 → 0.9.3

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/dist/index.cjs CHANGED
@@ -151,23 +151,79 @@ function Card({
151
151
  children,
152
152
  className = "",
153
153
  style,
154
+ showDecorations = true,
154
155
  ...props
155
156
  }) {
156
157
  const mergedStyle = {
157
158
  borderRadius: "30px",
158
- background: "rgba(112, 133, 233, 0.05)",
159
+ backgroundColor: "color-mix(in srgb, var(--surface) 85%, transparent)",
159
160
  backdropFilter: "blur(75px)",
160
161
  WebkitBackdropFilter: "blur(75px)",
161
162
  // Safari support
163
+ color: "var(--fg)",
164
+ position: "relative",
165
+ overflow: "hidden",
166
+ transition: "background-color 0.3s ease, color 0.3s ease",
162
167
  ...style
163
168
  };
164
- return /* @__PURE__ */ jsxRuntime.jsx(
169
+ return /* @__PURE__ */ jsxRuntime.jsxs(
165
170
  "div",
166
171
  {
167
172
  className: `p-6 ${className}`,
168
173
  style: mergedStyle,
169
174
  ...props,
170
- children
175
+ children: [
176
+ showDecorations && /* @__PURE__ */ jsxRuntime.jsxs(
177
+ "svg",
178
+ {
179
+ style: {
180
+ position: "absolute",
181
+ top: 0,
182
+ left: 0,
183
+ width: "100%",
184
+ height: "100%",
185
+ pointerEvents: "none",
186
+ zIndex: 0
187
+ },
188
+ viewBox: "0 0 967 745",
189
+ fill: "none",
190
+ xmlns: "http://www.w3.org/2000/svg",
191
+ preserveAspectRatio: "none",
192
+ children: [
193
+ /* @__PURE__ */ jsxRuntime.jsxs("defs", { children: [
194
+ /* @__PURE__ */ jsxRuntime.jsxs("linearGradient", { id: "paint0_linear_card", x1: "109", y1: "744.5", x2: "855", y2: "744.5", gradientUnits: "userSpaceOnUse", children: [
195
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { stopColor: "#4BDD74", stopOpacity: "0" }),
196
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0.5", stopColor: "#4BDD74" }),
197
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "1", stopColor: "#4BDD74", stopOpacity: "0" })
198
+ ] }),
199
+ /* @__PURE__ */ jsxRuntime.jsxs("linearGradient", { id: "paint1_linear_card", x1: "967.5", y1: "10", x2: "967.5", y2: "652", gradientUnits: "userSpaceOnUse", children: [
200
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { stopColor: "#4BDD74", stopOpacity: "0" }),
201
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0.5", stopColor: "#4BDD74" }),
202
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "1", stopColor: "#4BDD74", stopOpacity: "0" })
203
+ ] }),
204
+ /* @__PURE__ */ jsxRuntime.jsxs("linearGradient", { id: "paint2_linear_card", x1: "877", y1: "0.5", x2: "90", y2: "0.5", gradientUnits: "userSpaceOnUse", children: [
205
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { stopColor: "#4BDD74", stopOpacity: "0" }),
206
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0.5", stopColor: "#4BDD74" }),
207
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "1", stopColor: "#4BDD74", stopOpacity: "0" })
208
+ ] }),
209
+ /* @__PURE__ */ jsxRuntime.jsxs("linearGradient", { id: "paint3_linear_card", x1: "0.5", y1: "34.5136", x2: "0.5", y2: "731.595", gradientUnits: "userSpaceOnUse", children: [
210
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { stopColor: "#4BDD74", stopOpacity: "0" }),
211
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0.5", stopColor: "#4BDD74" }),
212
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "1", stopColor: "#4BDD74", stopOpacity: "0" })
213
+ ] })
214
+ ] }),
215
+ /* @__PURE__ */ jsxRuntime.jsxs("g", { clipPath: "url(#clip0_card)", children: [
216
+ /* @__PURE__ */ jsxRuntime.jsx("line", { opacity: "0.8", x1: "855", y1: "744.5", x2: "109", y2: "744.5", stroke: "url(#paint0_linear_card)" }),
217
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "965.5", y1: "652", x2: "965.5", y2: "10", stroke: "url(#paint1_linear_card)" })
218
+ ] }),
219
+ /* @__PURE__ */ jsxRuntime.jsx("line", { opacity: "0.6", x1: "90", y1: "0.5", x2: "877", y2: "0.5", stroke: "url(#paint2_linear_card)" }),
220
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "0.5", y1: "731.595", x2: "0.500027", y2: "34.5136", stroke: "url(#paint3_linear_card)" }),
221
+ /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsx("clipPath", { id: "clip0_card", children: /* @__PURE__ */ jsxRuntime.jsx("rect", { width: "966", height: "744", rx: "10", transform: "matrix(-1 0 0 1 967 1)", fill: "white" }) }) })
222
+ ]
223
+ }
224
+ ),
225
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { position: "relative", zIndex: 1 }, children })
226
+ ]
171
227
  }
172
228
  );
173
229
  }
@@ -203,10 +259,28 @@ function Modal({
203
259
  open,
204
260
  onClose,
205
261
  title,
206
- closeOnOverlay = true,
207
- children
262
+ closeOnBackdrop = true,
263
+ children,
264
+ className = "",
265
+ zIndex = 50,
266
+ showDecorations = true
208
267
  }) {
209
268
  const [mounted, setMounted] = React2__namespace.useState(false);
269
+ const [isDark, setIsDark] = React2__namespace.useState(false);
270
+ React2__namespace.useEffect(() => {
271
+ const checkDarkMode = () => {
272
+ const hasDarkClass = document.documentElement.classList.contains("dark") || document.body.classList.contains("dark") || document.querySelector(".dark") !== null;
273
+ setIsDark(hasDarkClass);
274
+ };
275
+ checkDarkMode();
276
+ const observer = new MutationObserver(checkDarkMode);
277
+ observer.observe(document.documentElement, {
278
+ attributes: true,
279
+ attributeFilter: ["class"],
280
+ subtree: true
281
+ });
282
+ return () => observer.disconnect();
283
+ }, []);
210
284
  useIsomorphicLayoutEffect(() => {
211
285
  setMounted(true);
212
286
  if (!open) return;
@@ -225,40 +299,30 @@ function Modal({
225
299
  return () => window.removeEventListener("keydown", onKey);
226
300
  }, [open, onClose]);
227
301
  if (!mounted) return null;
228
- if (!open) return null;
229
- const overlay = /* @__PURE__ */ jsxRuntime.jsxs(
230
- "div",
302
+ const modal = /* @__PURE__ */ jsxRuntime.jsx("div", { className: isDark ? "dark" : "", children: /* @__PURE__ */ jsxRuntime.jsx(
303
+ BackgroundBlur,
231
304
  {
232
- className: "fixed inset-0 z-50 flex items-center justify-center",
233
- "aria-labelledby": "modal-title",
234
- role: "dialog",
235
- "aria-modal": "true",
236
- children: [
237
- /* @__PURE__ */ jsxRuntime.jsx(
238
- "div",
239
- {
240
- className: "absolute inset-0 bg-black/50",
241
- onClick: () => closeOnOverlay && onClose?.()
242
- }
243
- ),
244
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative z-10 w-full max-w-lg rounded-[var(--radius-lg)] bg-[var(--surface)] text-[var(--fg)] p-6 shadow-xl", children: [
245
- title ? /* @__PURE__ */ jsxRuntime.jsx("h2", { id: "modal-title", className: "mb-2 text-lg font-semibold", children: title }) : null,
246
- /* @__PURE__ */ jsxRuntime.jsx("div", { children }),
247
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4 flex justify-end gap-2", children: /* @__PURE__ */ jsxRuntime.jsx(
248
- "button",
249
- {
250
- onClick: onClose,
251
- className: "btn btn-outline",
252
- type: "button",
253
- children: "Close"
254
- }
255
- ) })
256
- ] })
257
- ]
305
+ open,
306
+ onClose: closeOnBackdrop ? onClose : void 0,
307
+ zIndex,
308
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
309
+ Card,
310
+ {
311
+ className: `w-full ${className || "max-w-lg"}`,
312
+ role: "dialog",
313
+ "aria-modal": "true",
314
+ "aria-labelledby": title ? "modal-title" : void 0,
315
+ showDecorations,
316
+ children: [
317
+ title && /* @__PURE__ */ jsxRuntime.jsx("h2", { id: "modal-title", className: "mb-4 text-xl font-semibold", children: title }),
318
+ /* @__PURE__ */ jsxRuntime.jsx("div", { children })
319
+ ]
320
+ }
321
+ )
258
322
  }
259
- );
323
+ ) });
260
324
  const container = document.body;
261
- return reactDom.createPortal(overlay, container);
325
+ return reactDom.createPortal(modal, container);
262
326
  }
263
327
  var typoStyles = {
264
328
  "display-lg": { fontSize: "57px", lineHeight: "64px", letterSpacing: "-0.25px" },
package/dist/index.d.cts CHANGED
@@ -45,21 +45,33 @@ type CardProps = React.HTMLAttributes<HTMLDivElement> & {
45
45
  children: React.ReactNode;
46
46
  /** Additional CSS classes */
47
47
  className?: string;
48
+ /** Show decorative elements (default: true) */
49
+ showDecorations?: boolean;
48
50
  };
49
- declare function Card({ children, className, style, ...props }: CardProps): react_jsx_runtime.JSX.Element;
51
+ declare function Card({ children, className, style, showDecorations, ...props }: CardProps): react_jsx_runtime.JSX.Element;
50
52
 
51
53
  type InputProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'>;
52
54
  declare const Input: React.ForwardRefExoticComponent<InputProps & React.RefAttributes<HTMLInputElement>>;
53
55
 
54
56
  type ModalProps = {
57
+ /** Whether the modal is open */
55
58
  open: boolean;
59
+ /** Callback when modal should close */
56
60
  onClose?: () => void;
61
+ /** Modal content */
57
62
  children?: React.ReactNode;
63
+ /** Optional title (rendered as heading) */
58
64
  title?: string;
59
- /** When true, closes when the overlay is clicked */
60
- closeOnOverlay?: boolean;
65
+ /** When true, closes when the backdrop is clicked (default: true) */
66
+ closeOnBackdrop?: boolean;
67
+ /** Custom className for the Card */
68
+ className?: string;
69
+ /** z-index for the modal (default: 50) */
70
+ zIndex?: number;
71
+ /** Show decorative elements on the Card (default: true) */
72
+ showDecorations?: boolean;
61
73
  };
62
- declare function Modal({ open, onClose, title, closeOnOverlay, children }: ModalProps): React.ReactPortal | null;
74
+ declare function Modal({ open, onClose, title, closeOnBackdrop, children, className, zIndex, showDecorations, }: ModalProps): React.ReactPortal | null;
63
75
 
64
76
  type TypoVariant = "display-lg" | "display-md" | "display-sm" | "headline-lg" | "headline-md" | "headline-sm" | "title-lg" | "title-md" | "title-sm" | "label-lg" | "label-md" | "label-sm" | "body-lg" | "body-md" | "body-sm" | "button";
65
77
  type TypoWeight = "normal" | "medium" | "semibold" | "bold";
package/dist/index.d.ts CHANGED
@@ -45,21 +45,33 @@ type CardProps = React.HTMLAttributes<HTMLDivElement> & {
45
45
  children: React.ReactNode;
46
46
  /** Additional CSS classes */
47
47
  className?: string;
48
+ /** Show decorative elements (default: true) */
49
+ showDecorations?: boolean;
48
50
  };
49
- declare function Card({ children, className, style, ...props }: CardProps): react_jsx_runtime.JSX.Element;
51
+ declare function Card({ children, className, style, showDecorations, ...props }: CardProps): react_jsx_runtime.JSX.Element;
50
52
 
51
53
  type InputProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'>;
52
54
  declare const Input: React.ForwardRefExoticComponent<InputProps & React.RefAttributes<HTMLInputElement>>;
53
55
 
54
56
  type ModalProps = {
57
+ /** Whether the modal is open */
55
58
  open: boolean;
59
+ /** Callback when modal should close */
56
60
  onClose?: () => void;
61
+ /** Modal content */
57
62
  children?: React.ReactNode;
63
+ /** Optional title (rendered as heading) */
58
64
  title?: string;
59
- /** When true, closes when the overlay is clicked */
60
- closeOnOverlay?: boolean;
65
+ /** When true, closes when the backdrop is clicked (default: true) */
66
+ closeOnBackdrop?: boolean;
67
+ /** Custom className for the Card */
68
+ className?: string;
69
+ /** z-index for the modal (default: 50) */
70
+ zIndex?: number;
71
+ /** Show decorative elements on the Card (default: true) */
72
+ showDecorations?: boolean;
61
73
  };
62
- declare function Modal({ open, onClose, title, closeOnOverlay, children }: ModalProps): React.ReactPortal | null;
74
+ declare function Modal({ open, onClose, title, closeOnBackdrop, children, className, zIndex, showDecorations, }: ModalProps): React.ReactPortal | null;
63
75
 
64
76
  type TypoVariant = "display-lg" | "display-md" | "display-sm" | "headline-lg" | "headline-md" | "headline-sm" | "title-lg" | "title-md" | "title-sm" | "label-lg" | "label-md" | "label-sm" | "body-lg" | "body-md" | "body-sm" | "button";
65
77
  type TypoWeight = "normal" | "medium" | "semibold" | "bold";
package/dist/index.js CHANGED
@@ -130,23 +130,79 @@ function Card({
130
130
  children,
131
131
  className = "",
132
132
  style,
133
+ showDecorations = true,
133
134
  ...props
134
135
  }) {
135
136
  const mergedStyle = {
136
137
  borderRadius: "30px",
137
- background: "rgba(112, 133, 233, 0.05)",
138
+ backgroundColor: "color-mix(in srgb, var(--surface) 85%, transparent)",
138
139
  backdropFilter: "blur(75px)",
139
140
  WebkitBackdropFilter: "blur(75px)",
140
141
  // Safari support
142
+ color: "var(--fg)",
143
+ position: "relative",
144
+ overflow: "hidden",
145
+ transition: "background-color 0.3s ease, color 0.3s ease",
141
146
  ...style
142
147
  };
143
- return /* @__PURE__ */ jsx(
148
+ return /* @__PURE__ */ jsxs(
144
149
  "div",
145
150
  {
146
151
  className: `p-6 ${className}`,
147
152
  style: mergedStyle,
148
153
  ...props,
149
- children
154
+ children: [
155
+ showDecorations && /* @__PURE__ */ jsxs(
156
+ "svg",
157
+ {
158
+ style: {
159
+ position: "absolute",
160
+ top: 0,
161
+ left: 0,
162
+ width: "100%",
163
+ height: "100%",
164
+ pointerEvents: "none",
165
+ zIndex: 0
166
+ },
167
+ viewBox: "0 0 967 745",
168
+ fill: "none",
169
+ xmlns: "http://www.w3.org/2000/svg",
170
+ preserveAspectRatio: "none",
171
+ children: [
172
+ /* @__PURE__ */ jsxs("defs", { children: [
173
+ /* @__PURE__ */ jsxs("linearGradient", { id: "paint0_linear_card", x1: "109", y1: "744.5", x2: "855", y2: "744.5", gradientUnits: "userSpaceOnUse", children: [
174
+ /* @__PURE__ */ jsx("stop", { stopColor: "#4BDD74", stopOpacity: "0" }),
175
+ /* @__PURE__ */ jsx("stop", { offset: "0.5", stopColor: "#4BDD74" }),
176
+ /* @__PURE__ */ jsx("stop", { offset: "1", stopColor: "#4BDD74", stopOpacity: "0" })
177
+ ] }),
178
+ /* @__PURE__ */ jsxs("linearGradient", { id: "paint1_linear_card", x1: "967.5", y1: "10", x2: "967.5", y2: "652", gradientUnits: "userSpaceOnUse", children: [
179
+ /* @__PURE__ */ jsx("stop", { stopColor: "#4BDD74", stopOpacity: "0" }),
180
+ /* @__PURE__ */ jsx("stop", { offset: "0.5", stopColor: "#4BDD74" }),
181
+ /* @__PURE__ */ jsx("stop", { offset: "1", stopColor: "#4BDD74", stopOpacity: "0" })
182
+ ] }),
183
+ /* @__PURE__ */ jsxs("linearGradient", { id: "paint2_linear_card", x1: "877", y1: "0.5", x2: "90", y2: "0.5", gradientUnits: "userSpaceOnUse", children: [
184
+ /* @__PURE__ */ jsx("stop", { stopColor: "#4BDD74", stopOpacity: "0" }),
185
+ /* @__PURE__ */ jsx("stop", { offset: "0.5", stopColor: "#4BDD74" }),
186
+ /* @__PURE__ */ jsx("stop", { offset: "1", stopColor: "#4BDD74", stopOpacity: "0" })
187
+ ] }),
188
+ /* @__PURE__ */ jsxs("linearGradient", { id: "paint3_linear_card", x1: "0.5", y1: "34.5136", x2: "0.5", y2: "731.595", gradientUnits: "userSpaceOnUse", children: [
189
+ /* @__PURE__ */ jsx("stop", { stopColor: "#4BDD74", stopOpacity: "0" }),
190
+ /* @__PURE__ */ jsx("stop", { offset: "0.5", stopColor: "#4BDD74" }),
191
+ /* @__PURE__ */ jsx("stop", { offset: "1", stopColor: "#4BDD74", stopOpacity: "0" })
192
+ ] })
193
+ ] }),
194
+ /* @__PURE__ */ jsxs("g", { clipPath: "url(#clip0_card)", children: [
195
+ /* @__PURE__ */ jsx("line", { opacity: "0.8", x1: "855", y1: "744.5", x2: "109", y2: "744.5", stroke: "url(#paint0_linear_card)" }),
196
+ /* @__PURE__ */ jsx("line", { x1: "965.5", y1: "652", x2: "965.5", y2: "10", stroke: "url(#paint1_linear_card)" })
197
+ ] }),
198
+ /* @__PURE__ */ jsx("line", { opacity: "0.6", x1: "90", y1: "0.5", x2: "877", y2: "0.5", stroke: "url(#paint2_linear_card)" }),
199
+ /* @__PURE__ */ jsx("line", { x1: "0.5", y1: "731.595", x2: "0.500027", y2: "34.5136", stroke: "url(#paint3_linear_card)" }),
200
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("clipPath", { id: "clip0_card", children: /* @__PURE__ */ jsx("rect", { width: "966", height: "744", rx: "10", transform: "matrix(-1 0 0 1 967 1)", fill: "white" }) }) })
201
+ ]
202
+ }
203
+ ),
204
+ /* @__PURE__ */ jsx("div", { style: { position: "relative", zIndex: 1 }, children })
205
+ ]
150
206
  }
151
207
  );
152
208
  }
@@ -182,10 +238,28 @@ function Modal({
182
238
  open,
183
239
  onClose,
184
240
  title,
185
- closeOnOverlay = true,
186
- children
241
+ closeOnBackdrop = true,
242
+ children,
243
+ className = "",
244
+ zIndex = 50,
245
+ showDecorations = true
187
246
  }) {
188
247
  const [mounted, setMounted] = React2.useState(false);
248
+ const [isDark, setIsDark] = React2.useState(false);
249
+ React2.useEffect(() => {
250
+ const checkDarkMode = () => {
251
+ const hasDarkClass = document.documentElement.classList.contains("dark") || document.body.classList.contains("dark") || document.querySelector(".dark") !== null;
252
+ setIsDark(hasDarkClass);
253
+ };
254
+ checkDarkMode();
255
+ const observer = new MutationObserver(checkDarkMode);
256
+ observer.observe(document.documentElement, {
257
+ attributes: true,
258
+ attributeFilter: ["class"],
259
+ subtree: true
260
+ });
261
+ return () => observer.disconnect();
262
+ }, []);
189
263
  useIsomorphicLayoutEffect(() => {
190
264
  setMounted(true);
191
265
  if (!open) return;
@@ -204,40 +278,30 @@ function Modal({
204
278
  return () => window.removeEventListener("keydown", onKey);
205
279
  }, [open, onClose]);
206
280
  if (!mounted) return null;
207
- if (!open) return null;
208
- const overlay = /* @__PURE__ */ jsxs(
209
- "div",
281
+ const modal = /* @__PURE__ */ jsx("div", { className: isDark ? "dark" : "", children: /* @__PURE__ */ jsx(
282
+ BackgroundBlur,
210
283
  {
211
- className: "fixed inset-0 z-50 flex items-center justify-center",
212
- "aria-labelledby": "modal-title",
213
- role: "dialog",
214
- "aria-modal": "true",
215
- children: [
216
- /* @__PURE__ */ jsx(
217
- "div",
218
- {
219
- className: "absolute inset-0 bg-black/50",
220
- onClick: () => closeOnOverlay && onClose?.()
221
- }
222
- ),
223
- /* @__PURE__ */ jsxs("div", { className: "relative z-10 w-full max-w-lg rounded-[var(--radius-lg)] bg-[var(--surface)] text-[var(--fg)] p-6 shadow-xl", children: [
224
- title ? /* @__PURE__ */ jsx("h2", { id: "modal-title", className: "mb-2 text-lg font-semibold", children: title }) : null,
225
- /* @__PURE__ */ jsx("div", { children }),
226
- /* @__PURE__ */ jsx("div", { className: "mt-4 flex justify-end gap-2", children: /* @__PURE__ */ jsx(
227
- "button",
228
- {
229
- onClick: onClose,
230
- className: "btn btn-outline",
231
- type: "button",
232
- children: "Close"
233
- }
234
- ) })
235
- ] })
236
- ]
284
+ open,
285
+ onClose: closeOnBackdrop ? onClose : void 0,
286
+ zIndex,
287
+ children: /* @__PURE__ */ jsxs(
288
+ Card,
289
+ {
290
+ className: `w-full ${className || "max-w-lg"}`,
291
+ role: "dialog",
292
+ "aria-modal": "true",
293
+ "aria-labelledby": title ? "modal-title" : void 0,
294
+ showDecorations,
295
+ children: [
296
+ title && /* @__PURE__ */ jsx("h2", { id: "modal-title", className: "mb-4 text-xl font-semibold", children: title }),
297
+ /* @__PURE__ */ jsx("div", { children })
298
+ ]
299
+ }
300
+ )
237
301
  }
238
- );
302
+ ) });
239
303
  const container = document.body;
240
- return createPortal(overlay, container);
304
+ return createPortal(modal, container);
241
305
  }
242
306
  var typoStyles = {
243
307
  "display-lg": { fontSize: "57px", lineHeight: "64px", letterSpacing: "-0.25px" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neoptocom/neopto-ui",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "private": false,
5
5
  "description": "A modern React component library built with Tailwind CSS v4 and TypeScript. Features dark mode, design tokens, and comprehensive Storybook documentation. Requires Tailwind v4+.",
6
6
  "keywords": [
@@ -5,20 +5,27 @@ export type CardProps = React.HTMLAttributes<HTMLDivElement> & {
5
5
  children: React.ReactNode;
6
6
  /** Additional CSS classes */
7
7
  className?: string;
8
+ /** Show decorative elements (default: true) */
9
+ showDecorations?: boolean;
8
10
  };
9
11
 
10
12
  export function Card({
11
13
  children,
12
14
  className = "",
13
15
  style,
16
+ showDecorations = true,
14
17
  ...props
15
18
  }: CardProps) {
16
19
  // Merge user styles with card styles
17
20
  const mergedStyle: React.CSSProperties = {
18
21
  borderRadius: "30px",
19
- background: "rgba(112, 133, 233, 0.05)",
22
+ backgroundColor: "color-mix(in srgb, var(--surface) 85%, transparent)",
20
23
  backdropFilter: "blur(75px)",
21
24
  WebkitBackdropFilter: "blur(75px)", // Safari support
25
+ color: "var(--fg)",
26
+ position: "relative",
27
+ overflow: "hidden",
28
+ transition: "background-color 0.3s ease, color 0.3s ease",
22
29
  ...style,
23
30
  };
24
31
 
@@ -28,8 +35,59 @@ export function Card({
28
35
  style={mergedStyle}
29
36
  {...props}
30
37
  >
31
- {children}
38
+ {showDecorations && (
39
+ <svg
40
+ style={{
41
+ position: "absolute",
42
+ top: 0,
43
+ left: 0,
44
+ width: "100%",
45
+ height: "100%",
46
+ pointerEvents: "none",
47
+ zIndex: 0,
48
+ }}
49
+ viewBox="0 0 967 745"
50
+ fill="none"
51
+ xmlns="http://www.w3.org/2000/svg"
52
+ preserveAspectRatio="none"
53
+ >
54
+ <defs>
55
+ <linearGradient id="paint0_linear_card" x1="109" y1="744.5" x2="855" y2="744.5" gradientUnits="userSpaceOnUse">
56
+ <stop stopColor="#4BDD74" stopOpacity="0"/>
57
+ <stop offset="0.5" stopColor="#4BDD74"/>
58
+ <stop offset="1" stopColor="#4BDD74" stopOpacity="0"/>
59
+ </linearGradient>
60
+ <linearGradient id="paint1_linear_card" x1="967.5" y1="10" x2="967.5" y2="652" gradientUnits="userSpaceOnUse">
61
+ <stop stopColor="#4BDD74" stopOpacity="0"/>
62
+ <stop offset="0.5" stopColor="#4BDD74"/>
63
+ <stop offset="1" stopColor="#4BDD74" stopOpacity="0"/>
64
+ </linearGradient>
65
+ <linearGradient id="paint2_linear_card" x1="877" y1="0.5" x2="90" y2="0.5" gradientUnits="userSpaceOnUse">
66
+ <stop stopColor="#4BDD74" stopOpacity="0"/>
67
+ <stop offset="0.5" stopColor="#4BDD74"/>
68
+ <stop offset="1" stopColor="#4BDD74" stopOpacity="0"/>
69
+ </linearGradient>
70
+ <linearGradient id="paint3_linear_card" x1="0.5" y1="34.5136" x2="0.5" y2="731.595" gradientUnits="userSpaceOnUse">
71
+ <stop stopColor="#4BDD74" stopOpacity="0"/>
72
+ <stop offset="0.5" stopColor="#4BDD74"/>
73
+ <stop offset="1" stopColor="#4BDD74" stopOpacity="0"/>
74
+ </linearGradient>
75
+ </defs>
76
+ <g clipPath="url(#clip0_card)"><line opacity="0.8" x1="855" y1="744.5" x2="109" y2="744.5" stroke="url(#paint0_linear_card)"/>
77
+ <line x1="965.5" y1="652" x2="965.5" y2="10" stroke="url(#paint1_linear_card)"/>
78
+ </g>
79
+ <line opacity="0.6" x1="90" y1="0.5" x2="877" y2="0.5" stroke="url(#paint2_linear_card)"/>
80
+ <line x1="0.5" y1="731.595" x2="0.500027" y2="34.5136" stroke="url(#paint3_linear_card)"/>
81
+ <defs>
82
+ <clipPath id="clip0_card">
83
+ <rect width="966" height="744" rx="10" transform="matrix(-1 0 0 1 967 1)" fill="white"/>
84
+ </clipPath>
85
+ </defs>
86
+ </svg>
87
+ )}
88
+ <div style={{ position: "relative", zIndex: 1 }}>
89
+ {children}
90
+ </div>
32
91
  </div>
33
92
  );
34
93
  }
35
-
@@ -1,13 +1,25 @@
1
1
  import * as React from "react";
2
2
  import { createPortal } from "react-dom";
3
+ import { BackgroundBlur } from "./BackgroundBlur";
4
+ import { Card } from "./Card";
3
5
 
4
6
  export type ModalProps = {
7
+ /** Whether the modal is open */
5
8
  open: boolean;
9
+ /** Callback when modal should close */
6
10
  onClose?: () => void;
11
+ /** Modal content */
7
12
  children?: React.ReactNode;
13
+ /** Optional title (rendered as heading) */
8
14
  title?: string;
9
- /** When true, closes when the overlay is clicked */
10
- closeOnOverlay?: boolean;
15
+ /** When true, closes when the backdrop is clicked (default: true) */
16
+ closeOnBackdrop?: boolean;
17
+ /** Custom className for the Card */
18
+ className?: string;
19
+ /** z-index for the modal (default: 50) */
20
+ zIndex?: number;
21
+ /** Show decorative elements on the Card (default: true) */
22
+ showDecorations?: boolean;
11
23
  };
12
24
 
13
25
  function useIsomorphicLayoutEffect(effect: React.EffectCallback, deps?: React.DependencyList) {
@@ -19,10 +31,36 @@ export function Modal({
19
31
  open,
20
32
  onClose,
21
33
  title,
22
- closeOnOverlay = true,
23
- children
34
+ closeOnBackdrop = true,
35
+ children,
36
+ className = "",
37
+ zIndex = 50,
38
+ showDecorations = true,
24
39
  }: ModalProps) {
25
40
  const [mounted, setMounted] = React.useState(false);
41
+ const [isDark, setIsDark] = React.useState(false);
42
+
43
+ // Detect dark mode
44
+ React.useEffect(() => {
45
+ const checkDarkMode = () => {
46
+ const hasDarkClass = document.documentElement.classList.contains("dark") ||
47
+ document.body.classList.contains("dark") ||
48
+ document.querySelector(".dark") !== null;
49
+ setIsDark(hasDarkClass);
50
+ };
51
+
52
+ checkDarkMode();
53
+
54
+ // Listen for changes to dark mode
55
+ const observer = new MutationObserver(checkDarkMode);
56
+ observer.observe(document.documentElement, {
57
+ attributes: true,
58
+ attributeFilter: ["class"],
59
+ subtree: true,
60
+ });
61
+
62
+ return () => observer.disconnect();
63
+ }, []);
26
64
 
27
65
  // Prevent body scroll when open
28
66
  useIsomorphicLayoutEffect(() => {
@@ -35,6 +73,7 @@ export function Modal({
35
73
  };
36
74
  }, [open]);
37
75
 
76
+ // ESC key to close
38
77
  React.useEffect(() => {
39
78
  if (!open) return;
40
79
  const onKey = (e: KeyboardEvent) => {
@@ -45,39 +84,32 @@ export function Modal({
45
84
  }, [open, onClose]);
46
85
 
47
86
  if (!mounted) return null;
48
- if (!open) return null;
49
87
 
50
- const overlay = (
51
- <div
52
- className="fixed inset-0 z-50 flex items-center justify-center"
53
- aria-labelledby="modal-title"
54
- role="dialog"
55
- aria-modal="true"
56
- >
57
- <div
58
- className="absolute inset-0 bg-black/50"
59
- onClick={() => closeOnOverlay && onClose?.()}
60
- />
61
- <div className="relative z-10 w-full max-w-lg rounded-[var(--radius-lg)] bg-[var(--surface)] text-[var(--fg)] p-6 shadow-xl">
62
- {title ? (
63
- <h2 id="modal-title" className="mb-2 text-lg font-semibold">
88
+ const modal = (
89
+ <div className={isDark ? "dark" : ""}>
90
+ <BackgroundBlur
91
+ open={open}
92
+ onClose={closeOnBackdrop ? onClose : undefined}
93
+ zIndex={zIndex}
94
+ >
95
+ <Card
96
+ className={`w-full ${className || "max-w-lg"}`}
97
+ role="dialog"
98
+ aria-modal="true"
99
+ aria-labelledby={title ? "modal-title" : undefined}
100
+ showDecorations={showDecorations}
101
+ >
102
+ {title && (
103
+ <h2 id="modal-title" className="mb-4 text-xl font-semibold">
64
104
  {title}
65
105
  </h2>
66
- ) : null}
106
+ )}
67
107
  <div>{children}</div>
68
- <div className="mt-4 flex justify-end gap-2">
69
- <button
70
- onClick={onClose}
71
- className="btn btn-outline"
72
- type="button"
73
- >
74
- Close
75
- </button>
76
- </div>
77
- </div>
108
+ </Card>
109
+ </BackgroundBlur>
78
110
  </div>
79
111
  );
80
112
 
81
113
  const container = document.body;
82
- return createPortal(overlay, container);
114
+ return createPortal(modal, container);
83
115
  }
@@ -0,0 +1,275 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { useState } from "react";
3
+ import { Modal } from "../components/Modal";
4
+ import { Button } from "../components/Button";
5
+ import Typo from "../components/Typo";
6
+
7
+ const meta = {
8
+ title: "Components/Modal",
9
+ component: Modal,
10
+ parameters: {
11
+ layout: "fullscreen",
12
+ },
13
+ tags: ["autodocs"],
14
+ } satisfies Meta<typeof Modal>;
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof meta>;
18
+
19
+ // Helper component to manage modal state
20
+ function ModalDemo(props: Partial<React.ComponentProps<typeof Modal>>) {
21
+ const [open, setOpen] = useState(false);
22
+
23
+ return (
24
+ <div className="p-8">
25
+ <Button onClick={() => setOpen(true)}>Open Modal</Button>
26
+ <Modal open={open} onClose={() => setOpen(false)} {...props}>
27
+ {props.children}
28
+ </Modal>
29
+ </div>
30
+ );
31
+ }
32
+
33
+ export const Default: Story = {
34
+ args: { open: false },
35
+ render: () => (
36
+ <ModalDemo>
37
+ <Typo variant="headline-md">Welcome!</Typo>
38
+ <Typo variant="body-md" className="mt-4">
39
+ This is a simple modal with custom content. Click outside or press ESC to close.
40
+ </Typo>
41
+ </ModalDemo>
42
+ ),
43
+ };
44
+
45
+ export const WithTitle: Story = {
46
+ args: { open: false },
47
+ render: () => (
48
+ <ModalDemo title="Modal Title">
49
+ <Typo variant="body-md">
50
+ This modal includes a title heading. You can still add any content you want below it.
51
+ </Typo>
52
+ <Typo variant="body-sm" className="mt-4 text-[var(--muted-fg)]">
53
+ Try clicking the backdrop to close, or press ESC.
54
+ </Typo>
55
+ </ModalDemo>
56
+ ),
57
+ };
58
+
59
+ export const NoBackdropClose: Story = {
60
+ args: { open: false },
61
+ render: () => (
62
+ <ModalDemo
63
+ title="Important Notice"
64
+ closeOnBackdrop={false}
65
+ >
66
+ <Typo variant="body-md">
67
+ This modal cannot be closed by clicking the backdrop.
68
+ </Typo>
69
+ <Typo variant="body-md" className="mt-4">
70
+ You must use the button below or press ESC to close.
71
+ </Typo>
72
+ <div className="mt-6">
73
+ <Button onClick={() => {}}>Acknowledge</Button>
74
+ </div>
75
+ </ModalDemo>
76
+ ),
77
+ };
78
+
79
+ export const CustomStyling: Story = {
80
+ args: { open: false },
81
+ render: () => (
82
+ <ModalDemo
83
+ title="Large Modal"
84
+ className="max-w-2xl p-12"
85
+ >
86
+ <Typo variant="body-md">
87
+ This modal has custom styling with a larger max-width and more padding.
88
+ </Typo>
89
+ <div className="mt-6 grid grid-cols-2 gap-4">
90
+ <div className="p-4 bg-[var(--muted)] rounded-2xl">
91
+ <Typo variant="label-lg" bold="semibold">Feature 1</Typo>
92
+ <Typo variant="body-sm" className="mt-2">Description here</Typo>
93
+ </div>
94
+ <div className="p-4 bg-[var(--muted)] rounded-2xl">
95
+ <Typo variant="label-lg" bold="semibold">Feature 2</Typo>
96
+ <Typo variant="body-sm" className="mt-2">Description here</Typo>
97
+ </div>
98
+ </div>
99
+ </ModalDemo>
100
+ ),
101
+ };
102
+
103
+ export const FormModal: Story = {
104
+ args: { open: false },
105
+ render: () => {
106
+ const [open, setOpen] = useState(false);
107
+ const [name, setName] = useState("");
108
+ const [email, setEmail] = useState("");
109
+
110
+ const handleSubmit = (e: React.FormEvent) => {
111
+ e.preventDefault();
112
+ alert(`Submitted: ${name}, ${email}`);
113
+ setOpen(false);
114
+ };
115
+
116
+ return (
117
+ <div className="p-8">
118
+ <Button onClick={() => setOpen(true)}>Open Form Modal</Button>
119
+ <Modal
120
+ open={open}
121
+ onClose={() => setOpen(false)}
122
+ title="Contact Form"
123
+ >
124
+ <form onSubmit={handleSubmit}>
125
+ <div className="space-y-4">
126
+ <div>
127
+ <label className="block mb-2 text-sm font-medium">Name</label>
128
+ <input
129
+ type="text"
130
+ value={name}
131
+ onChange={(e) => setName(e.target.value)}
132
+ className="w-full px-4 py-2 rounded-full border border-[var(--border)] bg-transparent"
133
+ placeholder="Enter your name"
134
+ required
135
+ />
136
+ </div>
137
+ <div>
138
+ <label className="block mb-2 text-sm font-medium">Email</label>
139
+ <input
140
+ type="email"
141
+ value={email}
142
+ onChange={(e) => setEmail(e.target.value)}
143
+ className="w-full px-4 py-2 rounded-full border border-[var(--border)] bg-transparent"
144
+ placeholder="Enter your email"
145
+ required
146
+ />
147
+ </div>
148
+ <div className="flex gap-3 justify-end pt-4">
149
+ <button
150
+ type="button"
151
+ onClick={() => setOpen(false)}
152
+ className="px-6 py-2 rounded-full border border-[var(--border)] hover:bg-[var(--muted)] transition-colors"
153
+ >
154
+ Cancel
155
+ </button>
156
+ <Button type="submit">Submit</Button>
157
+ </div>
158
+ </div>
159
+ </form>
160
+ </Modal>
161
+ </div>
162
+ );
163
+ },
164
+ };
165
+
166
+ export const ConfirmationDialog: Story = {
167
+ args: { open: false },
168
+ render: () => {
169
+ const [open, setOpen] = useState(false);
170
+
171
+ const handleConfirm = () => {
172
+ alert("Action confirmed!");
173
+ setOpen(false);
174
+ };
175
+
176
+ return (
177
+ <div className="p-8">
178
+ <Button variant="primary" onClick={() => setOpen(true)}>
179
+ Delete Item
180
+ </Button>
181
+ <Modal
182
+ open={open}
183
+ onClose={() => setOpen(false)}
184
+ title="Confirm Deletion"
185
+ closeOnBackdrop={false}
186
+ >
187
+ <Typo variant="body-md">
188
+ Are you sure you want to delete this item? This action cannot be undone.
189
+ </Typo>
190
+ <div className="flex gap-3 justify-end mt-6">
191
+ <button
192
+ onClick={() => setOpen(false)}
193
+ className="px-6 py-2 rounded-full border border-[var(--border)] hover:bg-[var(--muted)] transition-colors"
194
+ >
195
+ Cancel
196
+ </button>
197
+ <Button variant="primary" onClick={handleConfirm}>
198
+ Delete
199
+ </Button>
200
+ </div>
201
+ </Modal>
202
+ </div>
203
+ );
204
+ },
205
+ };
206
+
207
+ /**
208
+ * This story demonstrates the Card component's decorative elements.
209
+ * The Card inside the Modal includes subtle ellipse gradients and border accents
210
+ * that can be toggled on/off via the `showDecorations` prop.
211
+ */
212
+ export const WithCardDecorations: Story = {
213
+ args: { open: false },
214
+ render: () => {
215
+ const [open, setOpen] = useState(false);
216
+ const [showDecorations, setShowDecorations] = useState(true);
217
+
218
+ const ModalDemo = ({ children, ...props }: any) => (
219
+ <>
220
+ <button
221
+ onClick={() => setOpen(true)}
222
+ className="px-6 py-3 rounded-full bg-[var(--primary)] text-white hover:opacity-90 transition-opacity"
223
+ type="button"
224
+ >
225
+ Open Modal with Card Decorations
226
+ </button>
227
+ <label className="flex items-center gap-2 mt-4">
228
+ <input
229
+ type="checkbox"
230
+ checked={showDecorations}
231
+ onChange={(e) => setShowDecorations(e.target.checked)}
232
+ />
233
+ <span>Show Card Decorations</span>
234
+ </label>
235
+ <Modal
236
+ open={open}
237
+ onClose={() => setOpen(false)}
238
+ showDecorations={showDecorations}
239
+ {...props}
240
+ />
241
+ </>
242
+ );
243
+
244
+ return (
245
+ <ModalDemo title="Card with Decorative Elements">
246
+ <div className="space-y-4">
247
+ <Typo variant="body-md">
248
+ The Card component now includes decorative SVG elements from your Figma design:
249
+ </Typo>
250
+ <ul className="list-disc pl-6 space-y-2">
251
+ <li>
252
+ <Typo variant="body-sm">
253
+ <strong>Ellipse gradients</strong> - Subtle blue and white ellipses that add depth
254
+ </Typo>
255
+ </li>
256
+ <li>
257
+ <Typo variant="body-sm">
258
+ <strong>Gradient borders</strong> - Green gradient lines on all four sides
259
+ </Typo>
260
+ </li>
261
+ <li>
262
+ <Typo variant="body-sm">
263
+ <strong>Toggle option</strong> - Use the checkbox above to toggle decorations on/off
264
+ </Typo>
265
+ </li>
266
+ </ul>
267
+ <Typo variant="body-sm" className="text-gray-500">
268
+ Note: The current Modal is using showDecorations={showDecorations.toString()}
269
+ </Typo>
270
+ </div>
271
+ </ModalDemo>
272
+ );
273
+ },
274
+ };
275
+