@axis-ds/ui-react 1.5.2 → 1.6.1

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.
@@ -17,6 +17,16 @@ interface AvatarProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps
17
17
  /**
18
18
  * 사용자 프로필 이미지를 표시하는 아바타 컴포넌트
19
19
  * 이미지 로드 실패 시 이니셜 fallback을 표시한다.
20
+ *
21
+ * 실패 판정은 두 경로다(WI-0054).
22
+ * 1. `<img onError>`: 하이드레이션 뒤(또는 클라이언트 렌더)에 난 오류.
23
+ * 2. 마운트 시 검사: 서버 렌더된 이미지의 오류가 하이드레이션 전에 끝나면 onError 가 붙기 전이라
24
+ * 이벤트를 놓친다. 그래서 마운트(와 src 변경) 때 `complete && naturalWidth === 0` 이면 실패 후보로 보고,
25
+ * `img.decode()` 가 거부될 때만 실패로 확정한다. 고유 크기가 없는 SVG 처럼 정상 로드인데
26
+ * naturalWidth 가 0 일 수 있는 이미지는 decode 가 이행되므로 실패로 보지 않는다.
27
+ * decode 가 없는 환경에서는 판정하지 않는다(종전 동작).
28
+ *
29
+ * 실패 상태는 실패한 src 와 함께 기억한다. src 가 바뀌면 렌더 중에 초기화해 새 이미지를 다시 시도한다.
20
30
  */
21
31
  declare const Avatar: React.ForwardRefExoticComponent<AvatarProps & React.RefAttributes<HTMLDivElement>>;
22
32
 
@@ -17,6 +17,16 @@ interface AvatarProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps
17
17
  /**
18
18
  * 사용자 프로필 이미지를 표시하는 아바타 컴포넌트
19
19
  * 이미지 로드 실패 시 이니셜 fallback을 표시한다.
20
+ *
21
+ * 실패 판정은 두 경로다(WI-0054).
22
+ * 1. `<img onError>`: 하이드레이션 뒤(또는 클라이언트 렌더)에 난 오류.
23
+ * 2. 마운트 시 검사: 서버 렌더된 이미지의 오류가 하이드레이션 전에 끝나면 onError 가 붙기 전이라
24
+ * 이벤트를 놓친다. 그래서 마운트(와 src 변경) 때 `complete && naturalWidth === 0` 이면 실패 후보로 보고,
25
+ * `img.decode()` 가 거부될 때만 실패로 확정한다. 고유 크기가 없는 SVG 처럼 정상 로드인데
26
+ * naturalWidth 가 0 일 수 있는 이미지는 decode 가 이행되므로 실패로 보지 않는다.
27
+ * decode 가 없는 환경에서는 판정하지 않는다(종전 동작).
28
+ *
29
+ * 실패 상태는 실패한 src 와 함께 기억한다. src 가 바뀌면 렌더 중에 초기화해 새 이미지를 다시 시도한다.
20
30
  */
21
31
  declare const Avatar: React.ForwardRefExoticComponent<AvatarProps & React.RefAttributes<HTMLDivElement>>;
22
32
 
@@ -38,7 +38,28 @@ var import_utils = require("../utils.js");
38
38
  var import_variants = require("./variants.js");
39
39
  const Avatar = React.forwardRef(
40
40
  ({ className, size, src, alt, fallback, ...props }, ref) => {
41
- const [hasError, setHasError] = React.useState(false);
41
+ const [failedSrc, setFailedSrc] = React.useState(null);
42
+ if (failedSrc !== null && failedSrc !== src) {
43
+ setFailedSrc(null);
44
+ }
45
+ const hasError = failedSrc !== null && failedSrc === src;
46
+ const imgRef = React.useRef(null);
47
+ React.useEffect(() => {
48
+ const img = imgRef.current;
49
+ if (!img || !src) return;
50
+ if (!img.complete || img.naturalWidth !== 0) return;
51
+ if (typeof img.decode !== "function") return;
52
+ let cancelled = false;
53
+ img.decode().then(
54
+ () => void 0,
55
+ () => {
56
+ if (!cancelled) setFailedSrc(src);
57
+ }
58
+ );
59
+ return () => {
60
+ cancelled = true;
61
+ };
62
+ }, [src]);
42
63
  const initials = React.useMemo(() => {
43
64
  if (fallback) return fallback;
44
65
  if (!alt) return "?";
@@ -53,10 +74,11 @@ const Avatar = React.forwardRef(
53
74
  children: src && !hasError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
54
75
  "img",
55
76
  {
77
+ ref: imgRef,
56
78
  src,
57
79
  alt: alt || "Avatar",
58
80
  className: "aspect-square h-full w-full object-cover",
59
- onError: () => setHasError(true)
81
+ onError: () => setFailedSrc(src)
60
82
  }
61
83
  ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "flex h-full w-full items-center justify-center bg-[var(--axis-surface-tertiary)] text-[var(--axis-text-secondary)] font-medium", children: initials })
62
84
  }
@@ -5,7 +5,28 @@ import { cn } from "../utils.mjs";
5
5
  import { avatarVariants } from "./variants.mjs";
6
6
  const Avatar = React.forwardRef(
7
7
  ({ className, size, src, alt, fallback, ...props }, ref) => {
8
- const [hasError, setHasError] = React.useState(false);
8
+ const [failedSrc, setFailedSrc] = React.useState(null);
9
+ if (failedSrc !== null && failedSrc !== src) {
10
+ setFailedSrc(null);
11
+ }
12
+ const hasError = failedSrc !== null && failedSrc === src;
13
+ const imgRef = React.useRef(null);
14
+ React.useEffect(() => {
15
+ const img = imgRef.current;
16
+ if (!img || !src) return;
17
+ if (!img.complete || img.naturalWidth !== 0) return;
18
+ if (typeof img.decode !== "function") return;
19
+ let cancelled = false;
20
+ img.decode().then(
21
+ () => void 0,
22
+ () => {
23
+ if (!cancelled) setFailedSrc(src);
24
+ }
25
+ );
26
+ return () => {
27
+ cancelled = true;
28
+ };
29
+ }, [src]);
9
30
  const initials = React.useMemo(() => {
10
31
  if (fallback) return fallback;
11
32
  if (!alt) return "?";
@@ -20,10 +41,11 @@ const Avatar = React.forwardRef(
20
41
  children: src && !hasError ? /* @__PURE__ */ jsx(
21
42
  "img",
22
43
  {
44
+ ref: imgRef,
23
45
  src,
24
46
  alt: alt || "Avatar",
25
47
  className: "aspect-square h-full w-full object-cover",
26
- onError: () => setHasError(true)
48
+ onError: () => setFailedSrc(src)
27
49
  }
28
50
  ) : /* @__PURE__ */ jsx("span", { className: "flex h-full w-full items-center justify-center bg-[var(--axis-surface-tertiary)] text-[var(--axis-text-secondary)] font-medium", children: initials })
29
51
  }
@@ -10,7 +10,7 @@ declare const Command: React.ForwardRefExoticComponent<Omit<{
10
10
  ref?: React.Ref<HTMLDivElement>;
11
11
  } & {
12
12
  asChild?: boolean;
13
- }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
13
+ }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
14
14
  label?: string;
15
15
  shouldFilter?: boolean;
16
16
  filter?: (value: string, search: string, keywords?: string[]) => number;
@@ -26,7 +26,7 @@ declare const CommandInput: React.ForwardRefExoticComponent<Omit<Omit<Pick<Pick<
26
26
  ref?: React.Ref<HTMLInputElement>;
27
27
  } & {
28
28
  asChild?: boolean;
29
- }, "asChild" | "key" | keyof React.InputHTMLAttributes<HTMLInputElement>>, "type" | "value" | "onChange"> & {
29
+ }, "key" | "asChild" | keyof React.InputHTMLAttributes<HTMLInputElement>>, "onChange" | "value" | "type"> & {
30
30
  value?: string;
31
31
  onValueChange?: (search: string) => void;
32
32
  } & React.RefAttributes<HTMLInputElement>, "ref"> & React.RefAttributes<HTMLInputElement>>;
@@ -37,7 +37,7 @@ declare const CommandList: React.ForwardRefExoticComponent<Omit<{
37
37
  ref?: React.Ref<HTMLDivElement>;
38
38
  } & {
39
39
  asChild?: boolean;
40
- }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
40
+ }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
41
41
  label?: string;
42
42
  } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
43
43
  /** 검색 결과가 없을 때 표시되는 빈 상태 컴포넌트 */
@@ -47,7 +47,7 @@ declare const CommandEmpty: React.ForwardRefExoticComponent<Omit<{
47
47
  ref?: React.Ref<HTMLDivElement>;
48
48
  } & {
49
49
  asChild?: boolean;
50
- }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>> & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
50
+ }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
51
51
  /** 관련 명령어 항목들을 그룹화하는 컴포넌트 */
52
52
  declare const CommandGroup: React.ForwardRefExoticComponent<Omit<{
53
53
  children?: React.ReactNode;
@@ -55,7 +55,7 @@ declare const CommandGroup: React.ForwardRefExoticComponent<Omit<{
55
55
  ref?: React.Ref<HTMLDivElement>;
56
56
  } & {
57
57
  asChild?: boolean;
58
- }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>>, "value" | "heading"> & {
58
+ }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild">, "heading" | "value"> & {
59
59
  heading?: React.ReactNode;
60
60
  value?: string;
61
61
  forceMount?: boolean;
@@ -65,7 +65,7 @@ declare const CommandSeparator: React.ForwardRefExoticComponent<Omit<Pick<Pick<R
65
65
  ref?: React.Ref<HTMLDivElement>;
66
66
  } & {
67
67
  asChild?: boolean;
68
- }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
68
+ }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
69
69
  alwaysRender?: boolean;
70
70
  } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
71
71
  /** 명령어 팔레트의 개별 선택 가능한 항목 */
@@ -75,7 +75,7 @@ declare const CommandItem: React.ForwardRefExoticComponent<Omit<{
75
75
  ref?: React.Ref<HTMLDivElement>;
76
76
  } & {
77
77
  asChild?: boolean;
78
- }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>>, "disabled" | "value" | "onSelect"> & {
78
+ }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild">, "onSelect" | "disabled" | "value"> & {
79
79
  disabled?: boolean;
80
80
  onSelect?: (value: string) => void;
81
81
  value?: string;
@@ -10,7 +10,7 @@ declare const Command: React.ForwardRefExoticComponent<Omit<{
10
10
  ref?: React.Ref<HTMLDivElement>;
11
11
  } & {
12
12
  asChild?: boolean;
13
- }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
13
+ }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
14
14
  label?: string;
15
15
  shouldFilter?: boolean;
16
16
  filter?: (value: string, search: string, keywords?: string[]) => number;
@@ -26,7 +26,7 @@ declare const CommandInput: React.ForwardRefExoticComponent<Omit<Omit<Pick<Pick<
26
26
  ref?: React.Ref<HTMLInputElement>;
27
27
  } & {
28
28
  asChild?: boolean;
29
- }, "asChild" | "key" | keyof React.InputHTMLAttributes<HTMLInputElement>>, "type" | "value" | "onChange"> & {
29
+ }, "key" | "asChild" | keyof React.InputHTMLAttributes<HTMLInputElement>>, "onChange" | "value" | "type"> & {
30
30
  value?: string;
31
31
  onValueChange?: (search: string) => void;
32
32
  } & React.RefAttributes<HTMLInputElement>, "ref"> & React.RefAttributes<HTMLInputElement>>;
@@ -37,7 +37,7 @@ declare const CommandList: React.ForwardRefExoticComponent<Omit<{
37
37
  ref?: React.Ref<HTMLDivElement>;
38
38
  } & {
39
39
  asChild?: boolean;
40
- }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
40
+ }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
41
41
  label?: string;
42
42
  } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
43
43
  /** 검색 결과가 없을 때 표시되는 빈 상태 컴포넌트 */
@@ -47,7 +47,7 @@ declare const CommandEmpty: React.ForwardRefExoticComponent<Omit<{
47
47
  ref?: React.Ref<HTMLDivElement>;
48
48
  } & {
49
49
  asChild?: boolean;
50
- }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>> & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
50
+ }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
51
51
  /** 관련 명령어 항목들을 그룹화하는 컴포넌트 */
52
52
  declare const CommandGroup: React.ForwardRefExoticComponent<Omit<{
53
53
  children?: React.ReactNode;
@@ -55,7 +55,7 @@ declare const CommandGroup: React.ForwardRefExoticComponent<Omit<{
55
55
  ref?: React.Ref<HTMLDivElement>;
56
56
  } & {
57
57
  asChild?: boolean;
58
- }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>>, "value" | "heading"> & {
58
+ }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild">, "heading" | "value"> & {
59
59
  heading?: React.ReactNode;
60
60
  value?: string;
61
61
  forceMount?: boolean;
@@ -65,7 +65,7 @@ declare const CommandSeparator: React.ForwardRefExoticComponent<Omit<Pick<Pick<R
65
65
  ref?: React.Ref<HTMLDivElement>;
66
66
  } & {
67
67
  asChild?: boolean;
68
- }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
68
+ }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild"> & {
69
69
  alwaysRender?: boolean;
70
70
  } & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
71
71
  /** 명령어 팔레트의 개별 선택 가능한 항목 */
@@ -75,7 +75,7 @@ declare const CommandItem: React.ForwardRefExoticComponent<Omit<{
75
75
  ref?: React.Ref<HTMLDivElement>;
76
76
  } & {
77
77
  asChild?: boolean;
78
- }, "asChild" | "key" | keyof React.HTMLAttributes<HTMLDivElement>>, "disabled" | "value" | "onSelect"> & {
78
+ }, "key" | keyof React.HTMLAttributes<HTMLDivElement> | "asChild">, "onSelect" | "disabled" | "value"> & {
79
79
  disabled?: boolean;
80
80
  onSelect?: (value: string) => void;
81
81
  value?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axis-ds/ui-react",
3
- "version": "1.5.2",
3
+ "version": "1.6.1",
4
4
  "description": "AXIS Design System - Core React UI Components",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -292,7 +292,7 @@
292
292
  "clsx": "^2.1.0",
293
293
  "cmdk": "^1.1.1",
294
294
  "tailwind-merge": "^2.5.0",
295
- "@axis-ds/tokens": "1.5.1"
295
+ "@axis-ds/tokens": "1.6.0"
296
296
  },
297
297
  "devDependencies": {
298
298
  "@testing-library/jest-dom": "^6.9.1",