@bigtablet/design-system 0.1.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/dist/index.js ADDED
@@ -0,0 +1,487 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { X, ChevronDown, Check } from 'lucide-react';
3
+ import { ToastContainer, Slide, toast } from 'react-toastify';
4
+ import 'react-toastify/dist/ReactToastify.css';
5
+ import * as React5 from 'react';
6
+
7
+ // src/ui/display/card/index.tsx
8
+ var Card = ({ shadow = "sm", padding = "md", bordered, className, ...props }) => {
9
+ const cls = ["card", `card--shadow-${shadow}`, `card--p-${padding}`, bordered && "card--bordered", className].filter(Boolean).join(" ");
10
+ return /* @__PURE__ */ jsx("div", { className: cls, ...props });
11
+ };
12
+ var Alert = ({
13
+ variant = "info",
14
+ title,
15
+ icon,
16
+ closable,
17
+ onClose,
18
+ className,
19
+ children,
20
+ ...props
21
+ }) => {
22
+ return /* @__PURE__ */ jsxs(
23
+ "div",
24
+ {
25
+ className: ["alert", `alert--${variant}`, className].filter(Boolean).join(" "),
26
+ role: "alert",
27
+ "aria-live": "polite",
28
+ ...props,
29
+ children: [
30
+ icon && /* @__PURE__ */ jsx("span", { className: "alert__icon", "aria-hidden": "true", children: icon }),
31
+ /* @__PURE__ */ jsxs("div", { className: "alert__content", children: [
32
+ title && /* @__PURE__ */ jsx("div", { className: "alert__title", children: title }),
33
+ children && /* @__PURE__ */ jsx("div", { className: "alert__desc", children })
34
+ ] }),
35
+ closable && /* @__PURE__ */ jsx(
36
+ "button",
37
+ {
38
+ type: "button",
39
+ className: "alert__close",
40
+ "aria-label": "\uB2EB\uAE30",
41
+ onClick: onClose,
42
+ children: /* @__PURE__ */ jsx(X, { size: 16, "aria-hidden": "true", focusable: "false" })
43
+ }
44
+ )
45
+ ]
46
+ }
47
+ );
48
+ };
49
+ var Loading = ({ size = 24 }) => {
50
+ return /* @__PURE__ */ jsx("span", { className: "loading", style: { width: size, height: size }, "aria-label": "Loading" });
51
+ };
52
+ var ToastProvider = () => /* @__PURE__ */ jsx(
53
+ ToastContainer,
54
+ {
55
+ position: "bottom-right",
56
+ autoClose: 2e3,
57
+ hideProgressBar: false,
58
+ newestOnTop: false,
59
+ closeOnClick: true,
60
+ rtl: false,
61
+ pauseOnFocusLoss: true,
62
+ draggable: true,
63
+ pauseOnHover: true,
64
+ theme: "light",
65
+ transition: Slide
66
+ }
67
+ );
68
+ var useToast = () => {
69
+ return {
70
+ success: (msg) => toast.success(msg),
71
+ error: (msg) => toast.error(msg),
72
+ warning: (msg) => toast.warning(msg),
73
+ info: (msg) => toast.info(msg),
74
+ message: (msg) => toast(msg)
75
+ };
76
+ };
77
+ var Checkbox = ({ label, size = "md", indeterminate, className, ...props }) => {
78
+ const ref = React5.useRef(null);
79
+ React5.useEffect(() => {
80
+ if (ref.current) ref.current.indeterminate = !!indeterminate;
81
+ }, [indeterminate]);
82
+ return /* @__PURE__ */ jsxs("label", { className: ["checkbox", `checkbox--${size}`, className].filter(Boolean).join(" "), children: [
83
+ /* @__PURE__ */ jsx("input", { ref, type: "checkbox", className: "checkbox__input", ...props }),
84
+ /* @__PURE__ */ jsx("span", { className: "checkbox__box", "aria-hidden": true }),
85
+ label && /* @__PURE__ */ jsx("span", { className: "checkbox__label", children: label })
86
+ ] });
87
+ };
88
+ var FileInput = ({ label = "Choose file", onFiles, className, ...props }) => {
89
+ const id = React5.useId();
90
+ return /* @__PURE__ */ jsxs("div", { className: ["file", className].filter(Boolean).join(" "), children: [
91
+ /* @__PURE__ */ jsx("input", { id, type: "file", className: "file__input", onChange: (e) => onFiles?.(e.currentTarget.files), ...props }),
92
+ /* @__PURE__ */ jsx("label", { htmlFor: id, className: "file__label", children: label })
93
+ ] });
94
+ };
95
+ var Radio = ({ label, size = "md", className, ...props }) => {
96
+ return /* @__PURE__ */ jsxs("label", { className: ["radio", `radio--${size}`, className].filter(Boolean).join(" "), children: [
97
+ /* @__PURE__ */ jsx("input", { type: "radio", className: "radio__input", ...props }),
98
+ /* @__PURE__ */ jsx("span", { className: "radio__dot", "aria-hidden": true }),
99
+ label && /* @__PURE__ */ jsx("span", { className: "radio__label", children: label })
100
+ ] });
101
+ };
102
+ var Switch = ({ checked, defaultChecked, onChange, size = "md", disabled, className, ...props }) => {
103
+ const controlled = checked !== void 0;
104
+ const [inner, setInner] = React5.useState(!!defaultChecked);
105
+ const on = controlled ? !!checked : inner;
106
+ const toggle = () => {
107
+ if (disabled) return;
108
+ const next = !on;
109
+ if (!controlled) setInner(next);
110
+ onChange?.(next);
111
+ };
112
+ return /* @__PURE__ */ jsx(
113
+ "button",
114
+ {
115
+ type: "button",
116
+ role: "switch",
117
+ "aria-checked": on,
118
+ disabled,
119
+ onClick: toggle,
120
+ className: ["switch", `switch--${size}`, on && "is-on", disabled && "is-disabled", className].filter(Boolean).join(" "),
121
+ ...props,
122
+ children: /* @__PURE__ */ jsx("span", { className: "switch__thumb" })
123
+ }
124
+ );
125
+ };
126
+ var TextField = React5.forwardRef(
127
+ ({
128
+ id,
129
+ label,
130
+ helperText,
131
+ error,
132
+ success,
133
+ variant = "outline",
134
+ size = "md",
135
+ leftIcon,
136
+ rightIcon,
137
+ fullWidth,
138
+ className,
139
+ ...props
140
+ }, ref) => {
141
+ const inputId = id ?? React5.useId();
142
+ const helperId = helperText ? `${inputId}-help` : void 0;
143
+ const classNames = [
144
+ "tf__input",
145
+ `tf__input--${variant}`,
146
+ `tf__input--${size}`,
147
+ leftIcon && "tf__input--with-left",
148
+ rightIcon && "tf__input--with-right",
149
+ error && "tf__input--error",
150
+ success && "tf__input--success",
151
+ className
152
+ ].filter(Boolean).join(" ");
153
+ return /* @__PURE__ */ jsxs("div", { className: "tf", style: fullWidth ? { width: "100%" } : void 0, children: [
154
+ label && /* @__PURE__ */ jsx("label", { className: "tf__label", htmlFor: inputId, children: label }),
155
+ /* @__PURE__ */ jsxs("div", { className: "tf__wrapper", children: [
156
+ leftIcon && /* @__PURE__ */ jsx("span", { className: "tf__icon tf__icon--left", children: leftIcon }),
157
+ /* @__PURE__ */ jsx(
158
+ "input",
159
+ {
160
+ id: inputId,
161
+ ref,
162
+ className: classNames,
163
+ "aria-invalid": !!error,
164
+ "aria-describedby": helperId,
165
+ ...props
166
+ }
167
+ ),
168
+ rightIcon && /* @__PURE__ */ jsx("span", { className: "tf__icon tf__icon--right", children: rightIcon })
169
+ ] }),
170
+ helperText && /* @__PURE__ */ jsx(
171
+ "div",
172
+ {
173
+ id: helperId,
174
+ className: `tf__helper ${error ? "tf__helper--error" : ""}`,
175
+ children: helperText
176
+ }
177
+ )
178
+ ] });
179
+ }
180
+ );
181
+ TextField.displayName = "TextField";
182
+ var Button = ({
183
+ variant = "primary",
184
+ size = "md",
185
+ className,
186
+ ...props
187
+ }) => {
188
+ const classes = ["btn", `btn--${variant}`, `btn--${size}`, className].filter(Boolean).join(" ");
189
+ return /* @__PURE__ */ jsx("button", { className: classes, ...props });
190
+ };
191
+ function Select({
192
+ id,
193
+ label,
194
+ placeholder = "Select\u2026",
195
+ options,
196
+ value,
197
+ onChange,
198
+ defaultValue = null,
199
+ disabled,
200
+ size = "md",
201
+ variant = "outline",
202
+ fullWidth,
203
+ className
204
+ }) {
205
+ const internalId = React5.useId();
206
+ const selectId = id ?? internalId;
207
+ const isControlled = value !== void 0;
208
+ const [internal, setInternal] = React5.useState(defaultValue);
209
+ const currentValue = isControlled ? value ?? null : internal;
210
+ const [open, setOpen] = React5.useState(false);
211
+ const [activeIndex, setActiveIndex] = React5.useState(-1);
212
+ const wrapperRef = React5.useRef(null);
213
+ const listRef = React5.useRef(null);
214
+ const currentOption = React5.useMemo(
215
+ () => options.find((o) => o.value === currentValue) ?? null,
216
+ [options, currentValue]
217
+ );
218
+ const setValue = React5.useCallback(
219
+ (next) => {
220
+ const opt = options.find((o) => o.value === next) ?? null;
221
+ if (!isControlled) setInternal(next);
222
+ onChange?.(next, opt);
223
+ },
224
+ [isControlled, onChange, options]
225
+ );
226
+ React5.useEffect(() => {
227
+ const onDocClick = (e) => {
228
+ if (!wrapperRef.current) return;
229
+ if (!wrapperRef.current.contains(e.target)) setOpen(false);
230
+ };
231
+ document.addEventListener("mousedown", onDocClick);
232
+ return () => document.removeEventListener("mousedown", onDocClick);
233
+ }, []);
234
+ const moveActive = (dir) => {
235
+ if (!open) {
236
+ setOpen(true);
237
+ return;
238
+ }
239
+ let i = activeIndex;
240
+ const len = options.length;
241
+ for (let step = 0; step < len; step++) {
242
+ i = (i + dir + len) % len;
243
+ if (!options[i].disabled) {
244
+ setActiveIndex(i);
245
+ break;
246
+ }
247
+ }
248
+ };
249
+ const commitActive = () => {
250
+ if (activeIndex < 0 || activeIndex >= options.length) return;
251
+ const opt = options[activeIndex];
252
+ if (!opt.disabled) {
253
+ setValue(opt.value);
254
+ setOpen(false);
255
+ }
256
+ };
257
+ const onKeyDown = (e) => {
258
+ if (disabled) return;
259
+ switch (e.key) {
260
+ case " ":
261
+ case "Enter":
262
+ e.preventDefault();
263
+ if (!open) setOpen(true);
264
+ else commitActive();
265
+ break;
266
+ case "ArrowDown":
267
+ e.preventDefault();
268
+ moveActive(1);
269
+ break;
270
+ case "ArrowUp":
271
+ e.preventDefault();
272
+ moveActive(-1);
273
+ break;
274
+ case "Home":
275
+ e.preventDefault();
276
+ setOpen(true);
277
+ setActiveIndex(options.findIndex((o) => !o.disabled));
278
+ break;
279
+ case "End":
280
+ e.preventDefault();
281
+ setOpen(true);
282
+ for (let i = options.length - 1; i >= 0; i--) {
283
+ if (!options[i].disabled) {
284
+ setActiveIndex(i);
285
+ break;
286
+ }
287
+ }
288
+ break;
289
+ case "Escape":
290
+ e.preventDefault();
291
+ setOpen(false);
292
+ break;
293
+ }
294
+ };
295
+ React5.useEffect(() => {
296
+ if (open) {
297
+ const idx = Math.max(0, options.findIndex((o) => o.value === currentValue && !o.disabled));
298
+ setActiveIndex(idx === -1 ? 0 : idx);
299
+ }
300
+ }, [open, options, currentValue]);
301
+ return /* @__PURE__ */ jsxs(
302
+ "div",
303
+ {
304
+ ref: wrapperRef,
305
+ className: `select${className ? ` ${className}` : ""}`,
306
+ style: fullWidth ? { width: "100%" } : void 0,
307
+ children: [
308
+ label && /* @__PURE__ */ jsx("label", { htmlFor: selectId, className: "select__label", children: label }),
309
+ /* @__PURE__ */ jsxs(
310
+ "button",
311
+ {
312
+ id: selectId,
313
+ type: "button",
314
+ className: [
315
+ "select__control",
316
+ `select__control--${variant}`,
317
+ `select__control--${size}`,
318
+ open && "is-open",
319
+ disabled && "is-disabled"
320
+ ].filter(Boolean).join(" "),
321
+ "aria-haspopup": "listbox",
322
+ "aria-expanded": open,
323
+ "aria-controls": `${selectId}-listbox`,
324
+ onClick: () => !disabled && setOpen((o) => !o),
325
+ onKeyDown,
326
+ disabled,
327
+ children: [
328
+ /* @__PURE__ */ jsx("span", { className: currentOption ? "select__value" : "select__placeholder", children: currentOption ? currentOption.label : placeholder }),
329
+ /* @__PURE__ */ jsx("span", { className: "select__icon", "aria-hidden": true, children: /* @__PURE__ */ jsx(ChevronDown, { size: 16 }) })
330
+ ]
331
+ }
332
+ ),
333
+ open && /* @__PURE__ */ jsx(
334
+ "ul",
335
+ {
336
+ ref: listRef,
337
+ id: `${selectId}-listbox`,
338
+ role: "listbox",
339
+ className: "select__list",
340
+ children: options.map((opt, i) => {
341
+ const selected = currentValue === opt.value;
342
+ const active = i === activeIndex;
343
+ return /* @__PURE__ */ jsxs(
344
+ "li",
345
+ {
346
+ role: "option",
347
+ "aria-selected": selected,
348
+ className: [
349
+ "select__option",
350
+ selected && "is-selected",
351
+ active && "is-active",
352
+ opt.disabled && "is-disabled"
353
+ ].filter(Boolean).join(" "),
354
+ onMouseEnter: () => !opt.disabled && setActiveIndex(i),
355
+ onClick: () => {
356
+ if (opt.disabled) return;
357
+ setValue(opt.value);
358
+ setOpen(false);
359
+ },
360
+ children: [
361
+ /* @__PURE__ */ jsx("span", { children: opt.label }),
362
+ selected && /* @__PURE__ */ jsx(Check, { size: 16, "aria-hidden": true })
363
+ ]
364
+ },
365
+ opt.value
366
+ );
367
+ })
368
+ }
369
+ )
370
+ ]
371
+ }
372
+ );
373
+ }
374
+ var Pagination = ({ page, total, onChange, siblingCount = 1 }) => {
375
+ const clamp = (n) => Math.max(1, Math.min(total, n));
376
+ const range = (s, e) => Array.from({ length: e - s + 1 }, (_, i) => s + i);
377
+ const start = Math.max(2, page - siblingCount);
378
+ const end = Math.min(total - 1, page + siblingCount);
379
+ const mid = range(start, end);
380
+ const pages = [
381
+ 1,
382
+ ...start > 2 ? ["\u2026"] : [],
383
+ ...mid,
384
+ ...end < total - 1 ? ["\u2026"] : [],
385
+ total
386
+ ].filter((x, i, arr) => typeof x === "number" ? arr.indexOf(x) === i : true);
387
+ return /* @__PURE__ */ jsxs("nav", { className: "pagination", "aria-label": "Pagination", children: [
388
+ /* @__PURE__ */ jsx("button", { className: "pagination__item", onClick: () => onChange(clamp(page - 1)), disabled: page <= 1, children: "Prev" }),
389
+ pages.map(
390
+ (p, i) => typeof p === "number" ? /* @__PURE__ */ jsx(
391
+ "button",
392
+ {
393
+ className: ["pagination__item", p === page && "is-active"].filter(Boolean).join(" "),
394
+ onClick: () => onChange(p),
395
+ children: p
396
+ },
397
+ i
398
+ ) : /* @__PURE__ */ jsx("span", { className: "pagination__ellipsis", children: "\u2026" }, i)
399
+ ),
400
+ /* @__PURE__ */ jsx("button", { className: "pagination__item", onClick: () => onChange(clamp(page + 1)), disabled: page >= total, children: "Next" })
401
+ ] });
402
+ };
403
+ var Sidebar = ({
404
+ items = [],
405
+ activeKey,
406
+ onItemSelect,
407
+ width = 240,
408
+ collapsible = true,
409
+ defaultCollapsed,
410
+ className,
411
+ style
412
+ }) => {
413
+ const [collapsed, setCollapsed] = React5.useState(!!defaultCollapsed);
414
+ const list = Array.isArray(items) ? items : [];
415
+ return /* @__PURE__ */ jsxs(
416
+ "aside",
417
+ {
418
+ className: ["sidebar", collapsed && "is-collapsed", className].filter(Boolean).join(" "),
419
+ style: { width: collapsed ? 64 : width, ...style },
420
+ children: [
421
+ collapsible && /* @__PURE__ */ jsx(
422
+ "button",
423
+ {
424
+ type: "button",
425
+ className: "sidebar__toggle",
426
+ onClick: () => setCollapsed((c) => !c),
427
+ "aria-label": "Toggle sidebar"
428
+ }
429
+ ),
430
+ /* @__PURE__ */ jsx("nav", { className: "sidebar__nav", children: list.map((it) => /* @__PURE__ */ jsxs(
431
+ "button",
432
+ {
433
+ type: "button",
434
+ className: ["sidebar__item", activeKey === it.key && "is-active"].filter(Boolean).join(" "),
435
+ onClick: () => onItemSelect?.(it.key),
436
+ title: typeof it.label === "string" ? it.label : void 0,
437
+ children: [
438
+ it.icon && /* @__PURE__ */ jsx("span", { className: "sidebar__icon", children: React5.createElement(it.icon, { size: 16 }) }),
439
+ !collapsed && /* @__PURE__ */ jsx("span", { className: "sidebar__label", children: it.label })
440
+ ]
441
+ },
442
+ it.key
443
+ )) })
444
+ ]
445
+ }
446
+ );
447
+ };
448
+ var Modal = ({
449
+ open,
450
+ onClose,
451
+ closeOnOverlay = true,
452
+ width = 520,
453
+ title,
454
+ children,
455
+ ...props
456
+ }) => {
457
+ React5.useEffect(() => {
458
+ const onKey = (e) => e.key === "Escape" && onClose?.();
459
+ if (open) document.addEventListener("keydown", onKey);
460
+ return () => document.removeEventListener("keydown", onKey);
461
+ }, [open, onClose]);
462
+ if (!open) return null;
463
+ return /* @__PURE__ */ jsx(
464
+ "div",
465
+ {
466
+ className: "modal",
467
+ role: "dialog",
468
+ "aria-modal": "true",
469
+ onClick: () => closeOnOverlay && onClose?.(),
470
+ children: /* @__PURE__ */ jsxs(
471
+ "div",
472
+ {
473
+ className: "modal__panel",
474
+ style: { width },
475
+ onClick: (e) => e.stopPropagation(),
476
+ ...props,
477
+ children: [
478
+ title && /* @__PURE__ */ jsx("div", { className: "modal__header", children: title }),
479
+ /* @__PURE__ */ jsx("div", { className: "modal__body", children })
480
+ ]
481
+ }
482
+ )
483
+ }
484
+ );
485
+ };
486
+
487
+ export { Alert, Button, Card, Checkbox, FileInput, Loading, Modal, Pagination, Radio, Select, Sidebar, Switch, TextField, ToastProvider, useToast };
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@bigtablet/design-system",
3
+ "version": "0.1.0",
4
+ "description": "Bigtablet Design System UI Components",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "sideEffects": [
21
+ "**/*.scss",
22
+ "**/*.css"
23
+ ],
24
+ "keywords": [
25
+ "design-system",
26
+ "react",
27
+ "storybook",
28
+ "ui"
29
+ ],
30
+ "author": "sangmin",
31
+ "license": "MIT",
32
+ "peerDependencies": {
33
+ "lucide-react": ">=0.552.0",
34
+ "react": "^19",
35
+ "react-dom": "^19",
36
+ "react-toastify": ">=11.0.5"
37
+ },
38
+ "devDependencies": {
39
+ "@storybook/addon-essentials": "8.6.14",
40
+ "@storybook/react": "8.6.14",
41
+ "@storybook/react-vite": "8.6.14",
42
+ "@types/node": "^24",
43
+ "@types/react": "^19",
44
+ "@types/react-dom": "^19",
45
+ "esbuild-sass-plugin": "^3",
46
+ "lucide-react": "^0.552.0",
47
+ "react": "19.2.0",
48
+ "react-dom": "19.2.0",
49
+ "react-toastify": "^11.0.5",
50
+ "sass-embedded": "^1.93.3",
51
+ "storybook": "8.6.14",
52
+ "tsup": "^8.5.0",
53
+ "typescript": "^5",
54
+ "vite": "^5"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "scripts": {
60
+ "build": "tsup",
61
+ "dev": "tsup --watch",
62
+ "storybook": "storybook dev -p 6006",
63
+ "build:sb": "storybook build",
64
+ "test": "echo \"no tests yet\""
65
+ }
66
+ }