@uniai-fe/uds-primitives 0.6.12 → 0.6.14

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
@@ -27,10 +27,11 @@ const nextConfig = {
27
27
  export default nextConfig;
28
28
  ```
29
29
 
30
- 앱 루트에서 foundation CSS를 1회 import 하면 모든 primitives 토큰을 공유합니다.
30
+ CSS-only 프로젝트는 앱 루트에서 foundation CSS를 먼저 로드한 primitives CSS를 로드합니다. primitives CSS는 foundation token이 이미 로드되어 있다고 가정합니다.
31
31
 
32
32
  ```ts
33
33
  import "@uniai-fe/uds-foundation/css";
34
+ import "@uniai-fe/uds-primitives/css";
34
35
  ```
35
36
 
36
37
  ## 사용 예시
@@ -47,8 +48,23 @@ export default function Page() {
47
48
  }
48
49
  ```
49
50
 
51
+ ## Public import surface
52
+
53
+ `@uniai-fe/uds-primitives`의 공식 안정 public surface는 root namespace import다.
54
+
55
+ ```tsx
56
+ import { Button, Input, Select } from "@uniai-fe/uds-primitives";
57
+ ```
58
+
59
+ - component/type/hook 소비는 root namespace import를 기준으로 안내한다.
60
+ - `@uniai-fe/uds-primitives/styles`, `@uniai-fe/uds-primitives/css`, `@uniai-fe/uds-primitives/init/dayjs`, `@uniai-fe/uds-primitives/init/mantine`, `@uniai-fe/uds-primitives/mantine-style`는 package export map에 있는 style/init entry다.
61
+ - `@uniai-fe/uds-primitives/button`, `@uniai-fe/uds-primitives/input` 같은 short category subpath는 현재 안정 public surface로 안내하지 않는다.
62
+ - category subpath 공개 여부는 package contract gate 후보로 남긴다.
63
+
50
64
  ## 제공 도구 목록
51
65
 
66
+ 아래 목록은 root namespace에서 확인하는 API inventory다. 카테고리 이름은 도구 탐색용이며, category subpath import 가능 여부의 SOT가 아니다.
67
+
52
68
  - `Alternate.EmptyData`
53
69
  - `Alternate.LoadingDefault`
54
70
  - `Alternate.LoadingIcon`
@@ -354,36 +370,40 @@ function Templates() {
354
370
 
355
371
  ## 스타일 내보내기
356
372
 
357
- foundation CSS는 앱 루트에서 한 번만 직접 import해야 한다. primitives `styles/css` 엔트리는 **컴포넌트 SCSS만** 포함하며 foundation 토큰을 다시 로드하지 않는다.
373
+ primitives `styles/css` 엔트리는 primitives component styles만 제공하며 foundation 토큰을 다시 로드하지 않는다. Foundation style은 service app root 또는 global stylesheet에서 먼저 로드해야 한다.
358
374
 
359
- ```scss
360
- @use "@uniai-fe/uds-foundation/css";
361
- @use "@uniai-fe/uds-primitives/styles";
362
- ```
375
+ ### CSS-only consumer setup
363
376
 
364
- Next.js/webpack 환경에서 Sass를 사용하지 않는 경우에는 아래처럼 CSS를 순서대로 import한다.
377
+ Sass를 사용하지 않는 Next.js/webpack 소비자는 CSS entry를 순서대로 import한다.
365
378
 
366
379
  ```ts
367
380
  import "@uniai-fe/uds-foundation/css";
368
381
  import "@uniai-fe/uds-primitives/css";
369
382
  ```
370
383
 
371
- 컴포넌트 단위로 필요한 스타일만 선택해 불러오려면 각 경로(`components/{name}/index.scss`)를 직접 import하면 된다.
384
+ ### Sass consumer setup
385
+
386
+ Sass 소비자는 foundation Sass public entry를 먼저 로드하고 primitives aggregate entry를 이어서 로드한다.
372
387
 
373
388
  ```scss
374
- @use "@uniai-fe/uds-primitives/button/index.scss";
375
- @use "@uniai-fe/uds-primitives/navigation/index.scss";
389
+ @use "@uniai-fe/uds-foundation/scss";
390
+ @use "@uniai-fe/uds-primitives/styles";
376
391
  ```
377
392
 
393
+ ### Storybook/local render setup
394
+
395
+ modules repo 내부 Storybook은 source style 변경을 빠르게 확인하기 위해 Preview에서 `@uniai-fe/uds-foundation/css` 이후 `@uniai-fe/uds-primitives/styles`를 로드한다. 이 설정은 Storybook local render setup이며 외부 consumer setup의 SOT가 아니다.
396
+
378
397
  ThemeProvider는 CSS를 import하지 않으므로 foundation/primitives styles를 앱 루트에서 1회만 로드하면 중복 없이 토큰 매핑이 적용된다. Provider 자체는 foundation 패키지(`@uniai-fe/uds-foundation/provider`)에서만 export된다(one-source 규칙).
379
398
 
380
- > modules 레포 내부(Storybook 등)에서는 개발 편의를 위해 `@uniai-fe/uds-primitives/styles` 엔트리를 import하지만, 외부 서비스/패키지는 `@uniai-fe/uds-primitives/css` 엔트리만 사용한다.
399
+ Mantine CSS 포함 책임은 이번 문서 보정에서 확정하지 않는다. `@uniai-fe/uds-primitives/css`, `@uniai-fe/uds-primitives/styles`, root init, `mantine-style` 어느 entry가 공식 책임을 갖는지는 별도 package/style contract gate에서 정리한다.
381
400
 
382
401
  ### 토큰 스코프 & ThemeProvider
383
402
 
384
- - primitives 소스 SCSS모든 디자인 토큰을 `:root`에 선언하고, 빌드 시 `scripts/merge-theme-root.mjs`가 토큰 블록을 하나의 `:root { ... }`로 합쳐 중복 선언을 제거한다.
385
- - ThemeProvider 루트 DOM에 `.uds-theme-root` 클래스를 주입하므로, 서비스 앱은 ThemeProvider를 layout 최상단에 배치한 `@uniai-fe/uds-foundation/css` `@uniai-fe/uds-primitives/css` 순으로 import하면 된다.
386
- - modules 레포(Storybook 등)는 SCSS 원본을 직접 import하지만 외부 소비자는 CSS 엔트리만 사용한다는 정책을 README/CODEX-RULES에 명시해둔다.
403
+ - primitives stylesfoundation token이미 로드되어 있다고 가정한다.
404
+ - ThemeProvider 루트 DOM에 `.uds-theme-root` 클래스를 주입하지만 CSS를 import하지 않는다. 서비스 앱은 ThemeProvider를 layout 최상단에 배치하더라도 style entry를 별도로 로드해야 한다.
405
+ - CSS-only 소비자는 `@uniai-fe/uds-foundation/css` -> `@uniai-fe/uds-primitives/css` 순서를 사용한다.
406
+ - Sass 소비자는 `@uniai-fe/uds-foundation/scss` -> `@uniai-fe/uds-primitives/styles` 순서를 사용한다.
387
407
 
388
408
  ## Next.js 통합 예시
389
409
 
@@ -402,17 +422,23 @@ const nextConfig = {
402
422
  export default nextConfig;
403
423
  ```
404
424
 
425
+ 아래 두 style load 방식 중 하나만 선택한다.
426
+
427
+ Sass 방식:
428
+
405
429
  ```scss
406
430
  /* app/globals.scss */
407
- @use "@uniai-fe/uds-foundation/css";
431
+ @use "@uniai-fe/uds-foundation/scss";
408
432
  @use "@uniai-fe/uds-primitives/styles";
409
433
  ```
410
434
 
435
+ CSS-only 방식:
436
+
411
437
  ```tsx
412
438
  // app/layout.tsx
413
439
  import type { ReactNode } from "react";
414
440
  import "@uniai-fe/uds-foundation/css";
415
- import "@uniai-fe/uds-primitives/styles";
441
+ import "@uniai-fe/uds-primitives/css";
416
442
  import { ThemeProvider } from "@uniai-fe/uds-foundation/provider";
417
443
 
418
444
  export default function RootLayout({ children }: { children: ReactNode }) {
@@ -426,7 +452,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
426
452
  }
427
453
  ```
428
454
 
429
- 예시는 ThemeProvider가 foundation 패키지에서만 export되고 CSS를 재import하지 않는 현재 구조를 기준으로 하므로, `globals.scss` 또는 루트에서 foundation/primitives styles를 반드시 각각 한 번 로드해야 한다. Sass 기반 프로젝트는 `@use "@uniai-fe/uds-foundation/css"; @use "@uniai-fe/uds-primitives/styles";`, CSS-only 프로젝트는 `import "@uniai-fe/uds-foundation/css"; import "@uniai-fe/uds-primitives/css";`를 사용한다.
455
+ 방식 모두 ThemeProvider가 foundation 패키지에서만 export되고 CSS를 재import하지 않는 현재 구조를 기준으로 한다. `globals.scss` 또는 루트에서 foundation/primitives styles를 반드시 각각 한 번 로드해야 한다. Sass 기반 프로젝트는 `@use "@uniai-fe/uds-foundation/scss"; @use "@uniai-fe/uds-primitives/styles";`, CSS-only 프로젝트는 `import "@uniai-fe/uds-foundation/css"; import "@uniai-fe/uds-primitives/css";`를 사용한다.
430
456
 
431
457
  모든 컴포넌트는 `.component` 클래스 + CSS 변수 기반으로 override가 가능하며, 버튼처럼 Slot(left/right/icon 등)을 제공하는 항목은 `CONTEXT-*.md` 문서에 상세 API를 기록했습니다. 불필요한 `data-*` attribute는 제거했고, 상태 표시는 `:disabled`, `[aria-busy="true"]` 같은 표준 attribute만 사용합니다.
432
458
 
package/dist/styles.css CHANGED
@@ -998,6 +998,8 @@
998
998
  --badge-line-height: var(--theme-badge-line-height-xsmall);
999
999
  --badge-letter-spacing: var(--theme-badge-letter-spacing-xsmall);
1000
1000
  --badge-dot-size: var(--theme-badge-dot-size);
1001
+ --badge-border-width: 0px;
1002
+ --badge-border-color: transparent;
1001
1003
  display: flex;
1002
1004
  align-items: center;
1003
1005
  justify-content: center;
@@ -1007,7 +1009,9 @@
1007
1009
  margin: 0;
1008
1010
  padding-inline: var(--badge-padding-inline);
1009
1011
  padding-block: 0;
1010
- border: 0;
1012
+ border-width: var(--badge-border-width);
1013
+ border-style: solid;
1014
+ border-color: var(--badge-border-color);
1011
1015
  box-sizing: border-box;
1012
1016
  border-radius: var(--badge-radius);
1013
1017
  white-space: nowrap;
@@ -1042,21 +1046,20 @@
1042
1046
  .badge:where([data-style=fill]) {
1043
1047
  background-color: var(--badge-background-color, var(--theme-badge-fill-background-default));
1044
1048
  color: var(--badge-text-color, var(--theme-badge-fill-text-default));
1045
- border-color: transparent;
1049
+ --badge-border-color: transparent;
1046
1050
  }
1047
1051
 
1048
1052
  .badge:where([data-style=outlined]) {
1049
1053
  background-color: var(--badge-background-color, var(--theme-badge-outlined-background-default));
1050
1054
  color: var(--badge-text-color, var(--theme-badge-outlined-text-default));
1051
- border-width: 1px;
1052
- border-style: solid;
1053
- border-color: var(--badge-border-color, var(--theme-badge-outlined-border-default));
1055
+ --badge-border-width: 1px;
1056
+ --badge-border-color: var(--theme-badge-outlined-border-default);
1054
1057
  }
1055
1058
 
1056
1059
  .badge:where([data-style=count]) {
1057
1060
  background-color: var(--badge-background-color, var(--theme-badge-count-background-default));
1058
1061
  color: var(--badge-text-color, var(--theme-badge-count-text-default));
1059
- border-color: transparent;
1062
+ --badge-border-color: transparent;
1060
1063
  border-radius: calc(var(--badge-height) / 2);
1061
1064
  }
1062
1065
 
@@ -3569,14 +3572,21 @@ figure.chip {
3569
3572
  }
3570
3573
 
3571
3574
  .input-date-trigger-icon {
3572
- margin: 0;
3573
- display: inline-flex;
3575
+ display: flex;
3574
3576
  align-items: center;
3575
3577
  justify-content: center;
3578
+ flex: 0 0 28px;
3576
3579
  width: 28px;
3577
3580
  height: 28px;
3581
+ margin: 0;
3582
+ padding: 0;
3583
+ border: 0;
3584
+ background-color: transparent;
3578
3585
  color: var(--color-label-alternative);
3579
- pointer-events: none;
3586
+ cursor: pointer;
3587
+ }
3588
+ .input-date-trigger-icon:where([aria-disabled=true]) {
3589
+ cursor: default;
3580
3590
  }
3581
3591
 
3582
3592
  .input-date-trigger-input.input-priority-table .input-field {
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-primitives",
3
- "version": "0.6.12",
3
+ "version": "0.6.14",
4
4
  "description": "UNIAI Design System; Primitives Components Package",
5
5
  "type": "module",
6
6
  "private": false,
7
7
  "sideEffects": [
8
8
  "./src/**/*.scss",
9
9
  "./styles.scss",
10
+ "./dist/styles.css",
10
11
  "./src/**/*.css",
11
12
  "./src/init/*.ts"
12
13
  ],
@@ -94,7 +95,7 @@
94
95
  "@mantine/hooks": "^8.3.18",
95
96
  "@svgr/webpack": "^8.1.0",
96
97
  "@types/node": "^24.12.3",
97
- "@types/react": "^19.2.14",
98
+ "@types/react": "^19.2.15",
98
99
  "@types/react-dom": "^19.2.3",
99
100
  "@uniai-fe/eslint-config": "workspace:*",
100
101
  "@uniai-fe/next-devkit": "workspace:*",
@@ -103,7 +104,7 @@
103
104
  "@uniai-fe/util-functions": "workspace:*",
104
105
  "eslint": "^9.39.2",
105
106
  "prettier": "^3.8.3",
106
- "react-hook-form": "^7.75.0",
107
+ "react-hook-form": "^7.76.0",
107
108
  "sass": "^1.99.0",
108
109
  "typescript": "5.9.3"
109
110
  }
@@ -45,7 +45,12 @@ const Badge = forwardRef<HTMLElementTagNameMap["figure"], BadgeProps>(
45
45
  ...(backgroundColor
46
46
  ? { "--badge-background-color": backgroundColor }
47
47
  : null),
48
- ...(borderColor ? { "--badge-border-color": borderColor } : null),
48
+ ...(borderColor
49
+ ? {
50
+ "--badge-border-color": borderColor,
51
+ "--badge-border-width": "1px",
52
+ }
53
+ : null),
49
54
  } as CSSProperties;
50
55
  // dot 스타일은 실제 원형 포인트 요소를 별도로 렌더링한다.
51
56
  const renderDot =
@@ -7,6 +7,8 @@
7
7
  --badge-line-height: var(--theme-badge-line-height-xsmall);
8
8
  --badge-letter-spacing: var(--theme-badge-letter-spacing-xsmall);
9
9
  --badge-dot-size: var(--theme-badge-dot-size);
10
+ --badge-border-width: 0px;
11
+ --badge-border-color: transparent;
10
12
  display: flex;
11
13
  align-items: center;
12
14
  justify-content: center;
@@ -16,7 +18,9 @@
16
18
  margin: 0;
17
19
  padding-inline: var(--badge-padding-inline);
18
20
  padding-block: 0;
19
- border: 0;
21
+ border-width: var(--badge-border-width);
22
+ border-style: solid;
23
+ border-color: var(--badge-border-color);
20
24
  box-sizing: border-box;
21
25
  border-radius: var(--badge-radius);
22
26
  white-space: nowrap;
@@ -54,7 +58,7 @@
54
58
  var(--theme-badge-fill-background-default)
55
59
  );
56
60
  color: var(--badge-text-color, var(--theme-badge-fill-text-default));
57
- border-color: transparent;
61
+ --badge-border-color: transparent;
58
62
  }
59
63
 
60
64
  .badge:where([data-style="outlined"]) {
@@ -63,12 +67,8 @@
63
67
  var(--theme-badge-outlined-background-default)
64
68
  );
65
69
  color: var(--badge-text-color, var(--theme-badge-outlined-text-default));
66
- border-width: 1px;
67
- border-style: solid;
68
- border-color: var(
69
- --badge-border-color,
70
- var(--theme-badge-outlined-border-default)
71
- );
70
+ --badge-border-width: 1px;
71
+ --badge-border-color: var(--theme-badge-outlined-border-default);
72
72
  }
73
73
 
74
74
  .badge:where([data-style="count"]) {
@@ -77,7 +77,7 @@
77
77
  var(--theme-badge-count-background-default)
78
78
  );
79
79
  color: var(--badge-text-color, var(--theme-badge-count-text-default));
80
- border-color: transparent;
80
+ --badge-border-color: transparent;
81
81
  border-radius: calc(var(--badge-height) / 2);
82
82
  }
83
83
 
@@ -8,7 +8,7 @@ import type {
8
8
  KeyboardEvent,
9
9
  MouseEvent,
10
10
  } from "react";
11
- import { forwardRef } from "react";
11
+ import { forwardRef, useCallback, useRef } from "react";
12
12
  import { Calendar } from "../../../calendar";
13
13
  import { InputFoundation } from "../foundation";
14
14
  import type { InputCalendarTriggerViewProps } from "../../types";
@@ -52,6 +52,22 @@ const InputDateTrigger = forwardRef<
52
52
  },
53
53
  ref,
54
54
  ) => {
55
+ const triggerInputRef = useRef<HTMLInputElement | null>(null);
56
+
57
+ const setTriggerInputRef = useCallback(
58
+ (node: HTMLInputElement | null) => {
59
+ triggerInputRef.current = node;
60
+ if (typeof ref === "function") {
61
+ ref(node);
62
+ return;
63
+ }
64
+ if (ref) {
65
+ ref.current = node;
66
+ }
67
+ },
68
+ [ref],
69
+ );
70
+
55
71
  /**
56
72
  * Radix `asChild`가 주입한 onClick을 보존하기 위해 restProps.onClick을 병합한다.
57
73
  * (Input Date 자체 onClick 계약도 함께 실행)
@@ -63,6 +79,17 @@ const InputDateTrigger = forwardRef<
63
79
  }
64
80
  onClick?.(event as never);
65
81
  };
82
+
83
+ const handleIconClick = (event: MouseEvent<HTMLButtonElement>) => {
84
+ event.preventDefault();
85
+ if (disabled || readOnly) {
86
+ return;
87
+ }
88
+
89
+ triggerInputRef.current?.focus();
90
+ triggerInputRef.current?.click();
91
+ };
92
+
66
93
  // 변경: Date trigger는 기본적으로 직접 타이핑을 막고, 달력 선택을 단일 입력 경로로 유지한다.
67
94
  const shouldBlockTyping = !disabled && !readOnly;
68
95
 
@@ -113,10 +140,23 @@ const InputDateTrigger = forwardRef<
113
140
  }
114
141
  };
115
142
 
143
+ const calendarIcon = (
144
+ <button
145
+ type="button"
146
+ className="input-date-trigger-icon"
147
+ tabIndex={-1}
148
+ aria-label="달력 열기"
149
+ aria-disabled={disabled || readOnly ? true : undefined}
150
+ onClick={handleIconClick}
151
+ >
152
+ <Calendar.Icon.Calendar />
153
+ </button>
154
+ );
155
+
116
156
  return (
117
157
  <InputFoundation.Base
118
158
  {...restProps}
119
- ref={ref}
159
+ ref={setTriggerInputRef}
120
160
  value={displayValue}
121
161
  // PopOver.Trigger(asChild)가 주입한 `type="button"`으로 placeholder가 사라지는 문제를 막기 위해
122
162
  // Date trigger는 항상 text type을 유지한다.
@@ -137,21 +177,9 @@ const InputDateTrigger = forwardRef<
137
177
  onPaste={handlePaste}
138
178
  onDrop={handleDrop}
139
179
  // table priority는 icon을 왼쪽 슬롯에 배치한다.
140
- left={
141
- priority === "table" ? (
142
- <figure className="input-date-trigger-icon" aria-hidden="true">
143
- <Calendar.Icon.Calendar />
144
- </figure>
145
- ) : undefined
146
- }
180
+ left={priority === "table" ? calendarIcon : undefined}
147
181
  // 기본(priority != table)은 기존처럼 icon을 오른쪽 슬롯에 유지한다.
148
- right={
149
- priority === "table" ? undefined : (
150
- <figure className="input-date-trigger-icon" aria-hidden="true">
151
- <Calendar.Icon.Calendar />
152
- </figure>
153
- )
154
- }
182
+ right={priority === "table" ? undefined : calendarIcon}
155
183
  />
156
184
  );
157
185
  },
@@ -10,15 +10,22 @@
10
10
  }
11
11
 
12
12
  .input-date-trigger-icon {
13
- margin: 0;
14
- display: inline-flex;
13
+ display: flex;
15
14
  align-items: center;
16
15
  justify-content: center;
16
+ flex: 0 0 28px;
17
17
  width: 28px;
18
18
  height: 28px;
19
+ margin: 0;
20
+ padding: 0;
21
+ border: 0;
22
+ background-color: transparent;
19
23
  color: var(--color-label-alternative);
20
- // 아이콘 영역 클릭은 input으로 위임한다.
21
- pointer-events: none;
24
+ cursor: pointer;
25
+
26
+ &:where([aria-disabled="true"]) {
27
+ cursor: default;
28
+ }
22
29
  }
23
30
 
24
31
  .input-date-trigger-input.input-priority-table {