@bigtablet/design-system 3.10.1 → 3.11.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.css CHANGED
@@ -4908,7 +4908,8 @@ select:-webkit-autofill:disabled {
4908
4908
  width: 20px;
4909
4909
  height: 20px;
4910
4910
  }
4911
- .text_field_clear {
4911
+ .text_field_clear,
4912
+ .text_field_action > * {
4912
4913
  display: inline-flex;
4913
4914
  align-items: center;
4914
4915
  justify-content: center;
@@ -4923,11 +4924,13 @@ select:-webkit-autofill:disabled {
4923
4924
  transition: color 0.2s ease-in-out, background 0.2s ease-in-out;
4924
4925
  padding: 0;
4925
4926
  }
4926
- .text_field_clear:hover {
4927
+ .text_field_clear:hover,
4928
+ .text_field_action > *:hover {
4927
4929
  color: var(--bt-color-text-heading);
4928
4930
  background: var(--bt-color-state-hover-on-light);
4929
4931
  }
4930
- .text_field_clear:focus-visible {
4932
+ .text_field_clear:focus-visible,
4933
+ .text_field_action > *:focus-visible {
4931
4934
  outline: none;
4932
4935
  box-shadow: var(--bt-focus-ring);
4933
4936
  }
@@ -5001,6 +5004,7 @@ select:-webkit-autofill:disabled {
5001
5004
  .text_field_label,
5002
5005
  .text_field_container,
5003
5006
  .text_field_clear,
5007
+ .text_field_action > *,
5004
5008
  .text_field_helper {
5005
5009
  transition: none;
5006
5010
  }
package/dist/index.d.ts CHANGED
@@ -197,13 +197,17 @@ interface BadgeProps extends React$1.HTMLAttributes<HTMLSpanElement> {
197
197
  declare const Badge: ({ variant, shape, size, appearance, count, max, className, children, ...props }: BadgeProps) => React$1.JSX.Element;
198
198
 
199
199
  interface EmptyStateProps extends Omit<React$1.HTMLAttributes<HTMLDivElement>, "title"> {
200
- /** 일러스트 영역 (아이콘/이미지 등) */
200
+ /**
201
+ * 일러스트 영역 (아이콘/이미지 등). **장식 전용** - `aria-hidden="true"` 래퍼로 접근성 트리에서
202
+ * 제외된다. 포커스 가능한 요소(버튼/링크 등)를 넣으면 포커스는 가는데 보조기기엔 없는 요소가
203
+ * 되므로(WCAG 4.1.2) `action` 을 쓸 것.
204
+ */
201
205
  illustration?: React$1.ReactNode;
202
206
  /** 제목 */
203
207
  title?: React$1.ReactNode;
204
208
  /** 보조 설명 */
205
209
  description?: React$1.ReactNode;
206
- /** 액션 영역 (Button 등) */
210
+ /** 액션 영역 (Button 등). `aria-hidden` 을 붙이지 않아 보조기기에 그대로 노출된다. */
207
211
  action?: React$1.ReactNode;
208
212
  /** 크기 (기본 "md") */
209
213
  size?: "sm" | "md" | "lg";
@@ -229,9 +233,14 @@ interface ErrorStateProps extends Omit<React$1.HTMLAttributes<HTMLDivElement>, "
229
233
  title?: React$1.ReactNode;
230
234
  /** 보조 설명 */
231
235
  description?: React$1.ReactNode;
232
- /** 아이콘/일러스트 (미지정 시 기본 경고 아이콘. `null` 로 숨김) */
236
+ /**
237
+ * 아이콘/일러스트 (미지정 시 기본 경고 아이콘. `null` 로 숨김). **장식 전용** -
238
+ * `aria-hidden="true"` 래퍼로 접근성 트리에서 제외된다. 래퍼는 필수다: 기본 아이콘이
239
+ * 이름 없는 bare lucide svg 라서, 빼면 `role="alert"` 이 이름 없는 그래픽까지 읽는다.
240
+ * 포커스 가능한 요소는 `action` 을 쓸 것 (WCAG 4.1.2).
241
+ */
233
242
  icon?: React$1.ReactNode;
234
- /** 액션 영역 (재시도 버튼 등) */
243
+ /** 액션 영역 (재시도 버튼 등). `aria-hidden` 을 붙이지 않아 보조기기에 그대로 노출된다. */
235
244
  action?: React$1.ReactNode;
236
245
  /**
237
246
  * 레이아웃 모드 (기본 "page").
@@ -1994,10 +2003,36 @@ interface TextFieldProps extends Omit<React$1.InputHTMLAttributes<HTMLInputEleme
1994
2003
  error?: boolean;
1995
2004
  /** 성공(검증 통과) 상태 여부. `error` 가 true 면 무시된다. */
1996
2005
  success?: boolean;
1997
- /** 입력 필드 왼쪽에 표시할 아이콘 */
2006
+ /**
2007
+ * 입력 필드 왼쪽에 표시할 **장식** 아이콘. `aria-hidden` 으로 접근성 트리에서 제외된다.
2008
+ * 포커스 가능한 요소(버튼 등)를 넣어야 하면 `leadingAction` 을 쓸 것.
2009
+ */
1998
2010
  leadingIcon?: React$1.ReactNode;
1999
- /** 입력 필드 오른쪽에 표시할 아이콘 */
2011
+ /**
2012
+ * 입력 필드 오른쪽에 표시할 **장식** 아이콘. `aria-hidden` 으로 접근성 트리에서 제외된다.
2013
+ * 포커스 가능한 요소(버튼 등)를 넣어야 하면 `trailingAction` 을 쓸 것.
2014
+ */
2000
2015
  trailingIcon?: React$1.ReactNode;
2016
+ /**
2017
+ * 입력 필드 왼쪽 조작 요소(버튼 등). `aria-hidden` 을 붙이지 않아 보조기기에 그대로 노출된다.
2018
+ * 아이콘 칸의 위치·크기 CSS 를 재사용하고, 넘긴 요소가 40x40 히트 영역을 채운다(WCAG 2.5.8).
2019
+ */
2020
+ leadingAction?: React$1.ReactNode;
2021
+ /**
2022
+ * 입력 필드 오른쪽 조작 요소(버튼 등). `aria-hidden` 을 붙이지 않아 보조기기에 그대로 노출된다.
2023
+ * 아이콘 칸의 위치·크기 CSS 를 재사용하고, 넘긴 요소가 40x40 히트 영역을 채운다(WCAG 2.5.8).
2024
+ */
2025
+ trailingAction?: React$1.ReactNode;
2026
+ /**
2027
+ * 비밀번호 표시/숨기기 토글 버튼을 오른쪽에 내장한다.
2028
+ * 켜면 `type` 을 토글이 직접 관리한다 - 숨김 상태는 넘긴 `type`(보통 `"password"`), 표시 상태는 `"text"`.
2029
+ */
2030
+ showPasswordToggle?: boolean;
2031
+ /** 토글 버튼의 `aria-label`. i18n 은 앱이 주입한다 (기본값: 영문). */
2032
+ passwordToggleLabels?: {
2033
+ show: string;
2034
+ hide: string;
2035
+ };
2001
2036
  /** 값이 있을 때 오른쪽에 지우기(X) 버튼 표시 여부 */
2002
2037
  clearable?: boolean;
2003
2038
  /** 컨테이너 전체 너비 차지 여부 */
@@ -2028,7 +2063,7 @@ interface TextFieldProps extends Omit<React$1.InputHTMLAttributes<HTMLInputEleme
2028
2063
  * @returns 렌더링된 텍스트 필드 UI
2029
2064
  */
2030
2065
  declare const TextField: {
2031
- ({ id, label, showLabel, supportingText, error, success, leadingIcon, trailingIcon, clearable, fullWidth, size, variant, className, onValueChange, onChangeAction, imeStrategy, value, defaultValue, transformValue, ref, ...props }: TextFieldProps): React$1.JSX.Element;
2066
+ ({ id, label, showLabel, supportingText, error, success, leadingIcon, trailingIcon, leadingAction, trailingAction, showPasswordToggle, passwordToggleLabels, clearable, type, fullWidth, size, variant, className, onValueChange, onChangeAction, imeStrategy, value, defaultValue, transformValue, ref, ...props }: TextFieldProps): React$1.JSX.Element;
2032
2067
  displayName: string;
2033
2068
  };
2034
2069
 
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import './index.css';
3
3
  import * as React16 from 'react';
4
4
  import { createContext, useRef, useState, useId, useEffect, useContext, useCallback, useMemo, Fragment, useImperativeHandle, useLayoutEffect } from 'react';
5
5
  import { useSpring, animated } from '@react-spring/web';
6
- import { ChevronDown, ChevronRight, Globe, ChevronLeft, ArrowUp, ArrowDown, ArrowUpDown, XCircle, AlertTriangle, CheckCircle2, Info, Bell, Search, Check, Image, X, TriangleAlert } from 'lucide-react';
6
+ import { ChevronDown, ChevronRight, Globe, ChevronLeft, ArrowUp, ArrowDown, ArrowUpDown, XCircle, AlertTriangle, CheckCircle2, Info, Bell, Search, Check, Image, X, EyeOff, Eye, TriangleAlert } from 'lucide-react';
7
7
  import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
8
8
  import { createPortal } from 'react-dom';
9
9
 
@@ -4413,6 +4413,7 @@ var Textarea = ({
4413
4413
  };
4414
4414
  Textarea.displayName = "Textarea";
4415
4415
  var ClearIcon = () => /* @__PURE__ */ jsx(X, { size: iconSize.lg, "aria-hidden": "true" });
4416
+ var DEFAULT_PASSWORD_TOGGLE_LABELS = { show: "Show password", hide: "Hide password" };
4416
4417
  var TextField = ({
4417
4418
  id,
4418
4419
  label,
@@ -4422,7 +4423,12 @@ var TextField = ({
4422
4423
  success,
4423
4424
  leadingIcon,
4424
4425
  trailingIcon,
4426
+ leadingAction,
4427
+ trailingAction,
4428
+ showPasswordToggle,
4429
+ passwordToggleLabels,
4425
4430
  clearable,
4431
+ type,
4426
4432
  fullWidth,
4427
4433
  size = "md",
4428
4434
  variant = "outline",
@@ -4441,9 +4447,7 @@ var TextField = ({
4441
4447
  const helperId = supportingText ? `${inputId}-help` : void 0;
4442
4448
  const isControlled = value !== void 0;
4443
4449
  const applyTransform = (nextValue) => transformValue ? transformValue(nextValue) : nextValue;
4444
- const [innerValue, setInnerValue] = useState(
4445
- () => applyTransform(value ?? defaultValue ?? "")
4446
- );
4450
+ const [innerValue, setInnerValue] = useState(() => applyTransform(value ?? defaultValue ?? ""));
4447
4451
  const isComposingRef = useRef(false);
4448
4452
  const lastEmittedValueRef = useRef(innerValue);
4449
4453
  const [prevValue, setPrevValue] = useState(value);
@@ -4466,6 +4470,14 @@ var TextField = ({
4466
4470
  const handleClear = useCallback(() => {
4467
4471
  emit("");
4468
4472
  }, [emit]);
4473
+ const [passwordRevealed, setPasswordRevealed] = useState(false);
4474
+ const togglePassword = useCallback(() => {
4475
+ setPasswordRevealed((revealed) => !revealed);
4476
+ }, []);
4477
+ let resolvedType = type;
4478
+ if (showPasswordToggle) {
4479
+ resolvedType = passwordRevealed ? "text" : type ?? "password";
4480
+ }
4469
4481
  const isError = !!error;
4470
4482
  const isSuccess = !!success && !isError;
4471
4483
  const rootClassName = cn(
@@ -4479,11 +4491,32 @@ var TextField = ({
4479
4491
  props.disabled && "text_field_disabled",
4480
4492
  className
4481
4493
  );
4482
- const resolvedTrailing = clearable && innerValue ? /* @__PURE__ */ jsx("button", { type: "button", className: "text_field_clear", onClick: handleClear, "aria-label": "Clear", children: /* @__PURE__ */ jsx(ClearIcon, {}) }) : trailingIcon ? /* @__PURE__ */ jsx("span", { className: "text_field_icon", "aria-hidden": "true", children: trailingIcon }) : null;
4494
+ const passwordToggleLabel = passwordRevealed ? passwordToggleLabels?.hide ?? DEFAULT_PASSWORD_TOGGLE_LABELS.hide : passwordToggleLabels?.show ?? DEFAULT_PASSWORD_TOGGLE_LABELS.show;
4495
+ const resolvedTrailing = showPasswordToggle ? /* @__PURE__ */ jsx("span", { className: "text_field_icon text_field_action", children: /* @__PURE__ */ jsx(
4496
+ "button",
4497
+ {
4498
+ type: "button",
4499
+ onClick: togglePassword,
4500
+ "aria-label": passwordToggleLabel,
4501
+ disabled: props.disabled,
4502
+ children: passwordRevealed ? /* @__PURE__ */ jsx(EyeOff, { size: iconSize.lg, "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Eye, { size: iconSize.lg, "aria-hidden": "true" })
4503
+ }
4504
+ ) }) : clearable && innerValue ? /* @__PURE__ */ jsx(
4505
+ "button",
4506
+ {
4507
+ type: "button",
4508
+ className: "text_field_clear",
4509
+ onClick: handleClear,
4510
+ "aria-label": "Clear",
4511
+ disabled: props.disabled,
4512
+ children: /* @__PURE__ */ jsx(ClearIcon, {})
4513
+ }
4514
+ ) : trailingAction ? /* @__PURE__ */ jsx("span", { className: "text_field_icon text_field_action", children: trailingAction }) : trailingIcon ? /* @__PURE__ */ jsx("span", { className: "text_field_icon", "aria-hidden": "true", children: trailingIcon }) : null;
4515
+ const resolvedLeading = leadingAction ? /* @__PURE__ */ jsx("span", { className: "text_field_icon text_field_action", children: leadingAction }) : leadingIcon ? /* @__PURE__ */ jsx("span", { className: "text_field_icon", "aria-hidden": "true", children: leadingIcon }) : null;
4483
4516
  return /* @__PURE__ */ jsxs("div", { className: rootClassName, children: [
4484
4517
  label && showLabel && /* @__PURE__ */ jsx("label", { htmlFor: inputId, className: "text_field_label", children: label }),
4485
4518
  /* @__PURE__ */ jsx("div", { className: "text_field_container", children: /* @__PURE__ */ jsxs("div", { className: "text_field_inner", children: [
4486
- leadingIcon && /* @__PURE__ */ jsx("span", { className: "text_field_icon", "aria-hidden": "true", children: leadingIcon }),
4519
+ resolvedLeading,
4487
4520
  /* @__PURE__ */ jsx(
4488
4521
  "div",
4489
4522
  {
@@ -4501,6 +4534,7 @@ var TextField = ({
4501
4534
  "aria-describedby": helperId,
4502
4535
  "aria-label": !showLabel ? label : void 0,
4503
4536
  ...props,
4537
+ type: resolvedType,
4504
4538
  value: innerValue,
4505
4539
  onCompositionStart: () => {
4506
4540
  isComposingRef.current = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigtablet/design-system",
3
- "version": "3.10.1",
3
+ "version": "3.11.0",
4
4
  "description": "Bigtablet Design System UI Components",
5
5
  "type": "module",
6
6
  "types": "dist/index.d.ts",
@@ -106,9 +106,9 @@
106
106
  "storybook": "^10.4.6",
107
107
  "stylelint": "^17.14.0",
108
108
  "tsup": "^8.5.1",
109
- "tsx": "^4.22.4",
109
+ "tsx": "^4.23.12",
110
110
  "typescript": "^6.0.3",
111
- "vite": "^8.1.0",
111
+ "vite": "^8.2.1",
112
112
  "vitest": "^4.1.10"
113
113
  },
114
114
  "publishConfig": {