@bigtablet/design-system 1.16.1 → 1.17.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 CHANGED
@@ -20,6 +20,7 @@ Bigtablet의 공식 디자인 시스템으로, Foundation(디자인 토큰)과 C
20
20
  - [주요 특징](#주요-특징)
21
21
  - [설치](#설치)
22
22
  - [빠른 시작](#빠른-시작)
23
+ - [Vanilla JS (HTML/CSS/JS)](#vanilla-js-htmlcssjs)
23
24
  - [컴포넌트](#컴포넌트)
24
25
  - [Foundation (디자인 토큰)](#foundation-디자인-토큰)
25
26
  - [개발 가이드](#개발-가이드)
@@ -32,6 +33,7 @@ Bigtablet의 공식 디자인 시스템으로, Foundation(디자인 토큰)과 C
32
33
  - **React 19 지원** - 최신 React 버전 완벽 지원
33
34
  - **TypeScript** - 완전한 타입 안정성
34
35
  - **Pure React / Next.js** - 프레임워크별 최적화된 번들 제공
36
+ - **Vanilla JS** - Thymeleaf, JSP 등 서버 템플릿 지원
35
37
  - **디자인 토큰** - 일관된 색상, 타이포그래피, 간격 시스템
36
38
  - **접근성(a11y)** - 키보드 네비게이션, 스크린 리더 호환
37
39
  - **Storybook** - 인터랙티브 문서화
@@ -100,6 +102,33 @@ export default function Layout({ children }) {
100
102
 
101
103
  ---
102
104
 
105
+ ## Vanilla JS (HTML/CSS/JS)
106
+
107
+ React 없이 **Thymeleaf, JSP, PHP** 등 서버 템플릿 환경에서 사용할 수 있습니다.
108
+
109
+ ```html
110
+ <!-- CSS -->
111
+ <link rel="stylesheet" href="https://unpkg.com/@bigtablet/design-system/dist/vanilla/bigtablet.min.css">
112
+
113
+ <!-- JS (선택) -->
114
+ <script src="https://unpkg.com/@bigtablet/design-system/dist/vanilla/bigtablet.min.js"></script>
115
+ ```
116
+
117
+ ```html
118
+ <button class="bt-button bt-button--md bt-button--primary">버튼</button>
119
+
120
+ <div class="bt-text-field">
121
+ <label class="bt-text-field__label">이메일</label>
122
+ <div class="bt-text-field__wrap">
123
+ <input type="text" class="bt-text-field__input bt-text-field__input--outline bt-text-field__input--md">
124
+ </div>
125
+ </div>
126
+ ```
127
+
128
+ 👉 **[Vanilla JS 전체 문서 보기](./docs/VANILLA.md)**
129
+
130
+ ---
131
+
103
132
  ## 컴포넌트
104
133
 
105
134
  ### General
package/dist/index.css CHANGED
@@ -689,11 +689,6 @@
689
689
  margin-top: 0;
690
690
  margin-bottom: 0.25rem;
691
691
  }
692
- .select_list_portal {
693
- position: fixed;
694
- margin-top: 0;
695
- margin-bottom: 0;
696
- }
697
692
  .select_option {
698
693
  width: 100%;
699
694
  box-sizing: border-box;
package/dist/index.js CHANGED
@@ -316,10 +316,8 @@ var Select = ({
316
316
  const [isOpen, setIsOpen] = React3.useState(false);
317
317
  const [activeIndex, setActiveIndex] = React3.useState(-1);
318
318
  const [dropUp, setDropUp] = React3.useState(false);
319
- const [listPosition, setListPosition] = React3.useState({ top: 0, left: 0, width: 0 });
320
319
  const wrapperRef = React3.useRef(null);
321
320
  const controlRef = React3.useRef(null);
322
- const listRef = React3.useRef(null);
323
321
  const currentOption = React3.useMemo(
324
322
  () => options.find((o) => o.value === currentValue) ?? null,
325
323
  [options, currentValue]
@@ -334,10 +332,10 @@ var Select = ({
334
332
  );
335
333
  React3.useEffect(() => {
336
334
  const onDocClick = (e) => {
337
- const target = e.target;
338
- if (wrapperRef.current?.contains(target)) return;
339
- if (listRef.current?.contains(target)) return;
340
- setIsOpen(false);
335
+ if (!wrapperRef.current) return;
336
+ if (!wrapperRef.current.contains(e.target)) {
337
+ setIsOpen(false);
338
+ }
341
339
  };
342
340
  document.addEventListener("mousedown", onDocClick);
343
341
  return () => document.removeEventListener("mousedown", onDocClick);
@@ -408,33 +406,14 @@ var Select = ({
408
406
  const idx = options.findIndex((o) => o.value === currentValue && !o.disabled);
409
407
  setActiveIndex(idx >= 0 ? idx : Math.max(0, options.findIndex((o) => !o.disabled)));
410
408
  }, [isOpen, options, currentValue]);
411
- const updatePosition = React3.useCallback(() => {
412
- if (!controlRef.current) return;
409
+ React3.useLayoutEffect(() => {
410
+ if (!isOpen || !controlRef.current) return;
413
411
  const rect = controlRef.current.getBoundingClientRect();
414
412
  const listHeight = Math.min(options.length * 40, 288);
415
413
  const spaceBelow = window.innerHeight - rect.bottom;
416
414
  const spaceAbove = rect.top;
417
- const shouldDropUp = spaceBelow < listHeight && spaceAbove > spaceBelow;
418
- setDropUp(shouldDropUp);
419
- setListPosition({
420
- top: shouldDropUp ? rect.top - listHeight - 4 : rect.bottom + 4,
421
- left: rect.left,
422
- width: rect.width
423
- });
424
- }, [options.length]);
425
- React3.useLayoutEffect(() => {
426
- if (!isOpen) return;
427
- updatePosition();
428
- }, [isOpen, updatePosition]);
429
- React3.useEffect(() => {
430
- if (!isOpen) return;
431
- window.addEventListener("scroll", updatePosition, true);
432
- window.addEventListener("resize", updatePosition);
433
- return () => {
434
- window.removeEventListener("scroll", updatePosition, true);
435
- window.removeEventListener("resize", updatePosition);
436
- };
437
- }, [isOpen, updatePosition]);
415
+ setDropUp(spaceBelow < listHeight && spaceAbove > spaceBelow);
416
+ }, [isOpen, options.length]);
438
417
  const rootClassName = ["select", className ?? ""].filter(Boolean).join(" ");
439
418
  const controlClassName = [
440
419
  "select_control",
@@ -443,50 +422,10 @@ var Select = ({
443
422
  isOpen && "is_open",
444
423
  disabled && "is_disabled"
445
424
  ].filter(Boolean).join(" ");
446
- const renderList = () => /* @__PURE__ */ jsx(
447
- "ul",
448
- {
449
- ref: listRef,
450
- id: `${selectId}_listbox`,
451
- role: "listbox",
452
- className: `select_list select_list_portal${dropUp ? " select_list_up" : ""}`,
453
- style: {
454
- position: "fixed",
455
- top: listPosition.top,
456
- left: listPosition.left,
457
- width: listPosition.width
458
- },
459
- children: options.map((opt, i) => {
460
- const selected = currentValue === opt.value;
461
- const active = i === activeIndex;
462
- const optionClassName = [
463
- "select_option",
464
- selected && "is_selected",
465
- active && "is_active",
466
- opt.disabled && "is_disabled"
467
- ].filter(Boolean).join(" ");
468
- return /* @__PURE__ */ jsxs(
469
- "li",
470
- {
471
- role: "option",
472
- "aria-selected": selected,
473
- className: optionClassName,
474
- onMouseEnter: () => !opt.disabled && setActiveIndex(i),
475
- onClick: () => {
476
- if (opt.disabled) return;
477
- setValue(opt.value);
478
- setIsOpen(false);
479
- },
480
- children: [
481
- /* @__PURE__ */ jsx("span", { children: opt.label }),
482
- selected && /* @__PURE__ */ jsx(Check, { size: 16, "aria-hidden": "true" })
483
- ]
484
- },
485
- opt.value
486
- );
487
- })
488
- }
489
- );
425
+ const listClassName = [
426
+ "select_list",
427
+ dropUp && "select_list_up"
428
+ ].filter(Boolean).join(" ");
490
429
  return /* @__PURE__ */ jsxs("div", { ref: wrapperRef, className: rootClassName, style: fullWidth ? { width: "100%" } : void 0, children: [
491
430
  label && /* @__PURE__ */ jsx("label", { htmlFor: selectId, className: "select_label", children: label }),
492
431
  /* @__PURE__ */ jsxs(
@@ -515,7 +454,43 @@ var Select = ({
515
454
  ]
516
455
  }
517
456
  ),
518
- isOpen && typeof document !== "undefined" && createPortal(renderList(), document.body)
457
+ isOpen && /* @__PURE__ */ jsx(
458
+ "ul",
459
+ {
460
+ id: `${selectId}_listbox`,
461
+ role: "listbox",
462
+ className: listClassName,
463
+ children: options.map((opt, i) => {
464
+ const selected = currentValue === opt.value;
465
+ const active = i === activeIndex;
466
+ const optionClassName = [
467
+ "select_option",
468
+ selected && "is_selected",
469
+ active && "is_active",
470
+ opt.disabled && "is_disabled"
471
+ ].filter(Boolean).join(" ");
472
+ return /* @__PURE__ */ jsxs(
473
+ "li",
474
+ {
475
+ role: "option",
476
+ "aria-selected": selected,
477
+ className: optionClassName,
478
+ onMouseEnter: () => !opt.disabled && setActiveIndex(i),
479
+ onClick: () => {
480
+ if (opt.disabled) return;
481
+ setValue(opt.value);
482
+ setIsOpen(false);
483
+ },
484
+ children: [
485
+ /* @__PURE__ */ jsx("span", { children: opt.label }),
486
+ selected && /* @__PURE__ */ jsx(Check, { size: 16, "aria-hidden": "true" })
487
+ ]
488
+ },
489
+ opt.value
490
+ );
491
+ })
492
+ }
493
+ )
519
494
  ] });
520
495
  };
521
496
  var Switch = ({