@iyulab/components 1.22.0 → 1.24.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +242 -0
  2. package/LICENSE +1 -1
  3. package/README.md +33 -1
  4. package/dist/components/UOverlayElement.d.ts +18 -0
  5. package/dist/components/UOverlayElement.js +24 -0
  6. package/dist/components/badge/UBadge.d.ts +5 -0
  7. package/dist/components/badge/UBadge.js +9 -0
  8. package/dist/components/badge/UBadge.styles.js +6 -0
  9. package/dist/components/dialog/UDialog.styles.js +1 -1
  10. package/dist/components/drawer/UDrawer.styles.js +1 -1
  11. package/dist/components/expander/UExpander.d.ts +38 -0
  12. package/dist/components/expander/UExpander.js +78 -0
  13. package/dist/components/expander/UExpander.styles.d.ts +1 -0
  14. package/dist/components/expander/UExpander.styles.js +95 -0
  15. package/dist/components/field/UField.styles.js +15 -0
  16. package/dist/components/icon/UIcon.d.ts +17 -4
  17. package/dist/components/icon/UIcon.js +9 -6
  18. package/dist/components/panel/UPanel.d.ts +0 -2
  19. package/dist/components/panel/UPanel.js +0 -5
  20. package/dist/components/skeleton/USkeleton.d.ts +7 -0
  21. package/dist/components/skeleton/USkeleton.js +7 -1
  22. package/dist/components/skeleton/USkeleton.styles.js +64 -0
  23. package/dist/components/tab-panel/UTabPanel.d.ts +4 -2
  24. package/dist/components/tab-panel/UTabPanel.js +0 -5
  25. package/dist/components/tag/UTag.d.ts +7 -0
  26. package/dist/components/tag/UTag.js +10 -1
  27. package/dist/components/tag/UTag.styles.js +4 -0
  28. package/dist/components/text/UText.d.ts +38 -0
  29. package/dist/components/text/UText.js +45 -0
  30. package/dist/components/text/UText.styles.d.ts +1 -0
  31. package/dist/components/text/UText.styles.js +91 -0
  32. package/dist/components/tree/UTree.d.ts +4 -2
  33. package/dist/components/tree/UTree.js +0 -5
  34. package/dist/components/tree-item/UTreeItem.d.ts +4 -1
  35. package/dist/components/tree-item/UTreeItem.js +0 -5
  36. package/dist/index.d.ts +2 -0
  37. package/dist/index.js +3 -1
  38. package/dist/react/UExpander.d.ts +16 -0
  39. package/dist/react/UExpander.js +13 -0
  40. package/dist/react/UText.d.ts +12 -0
  41. package/dist/react/UText.js +10 -0
  42. package/dist/react/index.d.ts +2 -0
  43. package/dist/react/index.js +2 -0
  44. package/dist/utilities/Dialog.js +1 -1
  45. package/dist/utilities/Locale.d.ts +34 -0
  46. package/dist/utilities/Locale.js +43 -6
  47. package/dist/utilities/Theme.d.ts +37 -0
  48. package/dist/utilities/Theme.js +65 -0
  49. package/dist/utilities/accent.d.ts +52 -0
  50. package/dist/utilities/accent.js +127 -0
  51. package/dist/utilities/icons.js +1 -1
  52. package/dist/utilities/statusIcon.d.ts +31 -0
  53. package/dist/utilities/statusIcon.js +34 -0
  54. package/package.json +5 -3
  55. package/skills/iyulab-components/SKILL.md +4 -1
  56. package/skills/iyulab-components/references/components/badge.md +2 -1
  57. package/skills/iyulab-components/references/components/button.md +3 -1
  58. package/skills/iyulab-components/references/components/copy-button.md +80 -0
  59. package/skills/iyulab-components/references/components/drawer.md +30 -0
  60. package/skills/iyulab-components/references/components/expander.md +70 -0
  61. package/skills/iyulab-components/references/components/icon.md +12 -0
  62. package/skills/iyulab-components/references/components/input.md +6 -0
  63. package/skills/iyulab-components/references/components/panel.md +6 -4
  64. package/skills/iyulab-components/references/components/skeleton.md +12 -0
  65. package/skills/iyulab-components/references/components/tab-panel.md +4 -5
  66. package/skills/iyulab-components/references/components/tag.md +18 -1
  67. package/skills/iyulab-components/references/components/text.md +79 -0
  68. package/skills/iyulab-components/references/components/textarea.md +5 -0
  69. package/skills/iyulab-components/references/components/tree.md +2 -4
  70. package/skills/iyulab-components/references/usage.md +73 -2
  71. package/skills/iyulab-components/references/utilities/theme.md +23 -4
@@ -22,7 +22,6 @@ var UTreeItem = class UTreeItem extends UElement {
22
22
  this.trigger = "item";
23
23
  this.value = "";
24
24
  this.draggable = false;
25
- this.droppable = false;
26
25
  this.leaf = true;
27
26
  this.depth = 0;
28
27
  this.selected = false;
@@ -185,10 +184,6 @@ __decorate([property({
185
184
  type: Boolean,
186
185
  reflect: true
187
186
  }), __decorateMetadata("design:type", Boolean)], UTreeItem.prototype, "draggable", void 0);
188
- __decorate([property({
189
- type: Boolean,
190
- reflect: true
191
- }), __decorateMetadata("design:type", Boolean)], UTreeItem.prototype, "droppable", void 0);
192
187
  __decorate([state(), __decorateMetadata("design:type", Boolean)], UTreeItem.prototype, "leaf", void 0);
193
188
  __decorate([state(), __decorateMetadata("design:type", Number)], UTreeItem.prototype, "depth", void 0);
194
189
  __decorate([state(), __decorateMetadata("design:type", Boolean)], UTreeItem.prototype, "selected", void 0);
package/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@ export * from './components/copy-button/UCopyButton.js';
17
17
  export * from './components/dialog/UDialog.js';
18
18
  export * from './components/divider/UDivider.js';
19
19
  export * from './components/drawer/UDrawer.js';
20
+ export * from './components/expander/UExpander.js';
20
21
  export * from './components/field/UField.js';
21
22
  export * from './components/form/UForm.js';
22
23
  export * from './components/icon/UIcon.js';
@@ -40,6 +41,7 @@ export * from './components/switch/USwitch.js';
40
41
  export * from './components/tab/UTab.js';
41
42
  export * from './components/tab-panel/UTabPanel.js';
42
43
  export * from './components/tag/UTag.js';
44
+ export * from './components/text/UText.js';
43
45
  export * from './components/textarea/UTextarea.js';
44
46
  export * from './components/tooltip/UTooltip.js';
45
47
  export * from './components/tree/UTree.js';
package/dist/index.js CHANGED
@@ -27,6 +27,7 @@ import { UCopyButton } from "./components/copy-button/UCopyButton.js";
27
27
  import { UDialog } from "./components/dialog/UDialog.js";
28
28
  import { UDivider } from "./components/divider/UDivider.js";
29
29
  import { UDrawer } from "./components/drawer/UDrawer.js";
30
+ import { UExpander } from "./components/expander/UExpander.js";
30
31
  import { UField } from "./components/field/UField.js";
31
32
  import { UForm } from "./components/form/UForm.js";
32
33
  import { UOption } from "./components/option/UOption.js";
@@ -46,6 +47,7 @@ import { USplitPanel } from "./components/split-panel/USplitPanel.js";
46
47
  import { USwitch } from "./components/switch/USwitch.js";
47
48
  import { UTab } from "./components/tab/UTab.js";
48
49
  import { UTabPanel } from "./components/tab-panel/UTabPanel.js";
50
+ import { UText } from "./components/text/UText.js";
49
51
  import { UTextarea } from "./components/textarea/UTextarea.js";
50
52
  import { UTreeItem } from "./components/tree-item/UTreeItem.js";
51
53
  import { UTree } from "./components/tree/UTree.js";
@@ -53,4 +55,4 @@ import { BrowserStorage } from "./utilities/BrowserStorage.js";
53
55
  import { Dialog } from "./utilities/Dialog.js";
54
56
  import { Theme } from "./utilities/Theme.js";
55
57
  import { Toast } from "./utilities/Toast.js";
56
- export { BrowserStorage, Dialog, IconCache, IconRegistry, Locale, OverlayManager, Theme, Toast, UAlert, UAvatar, UBadge, UBreadcrumb, UBreadcrumbItem, UButton, UButtonGroup, UCard, UCarousel, UCheckbox, UChip, UCopyButton, UDialog, UDivider, UDrawer, UElement, UField, UFloatingElement, UForm, UFormControlElement, UIcon, UIconButton, UInput, UMenu, UMenuItem, UOption, UOverlayElement, UPanel, UPopover, UProgressBar, UProgressRing, URadio, URating, USelect, USkeleton, USlider, USpinner, USplitPanel, USwitch, UTab, UTabPanel, UTag, UTextarea, UTooltip, UTree, UTreeItem, arrayAttrConverter, booleanAttrConverter, dateAttrConverter, getDefaultBaseUrl, getParentElement, jsonAttrConverter, querySelectorAllWithin, querySelectorWithin, setDefaultBaseUrl, urlAttrConverter };
58
+ export { BrowserStorage, Dialog, IconCache, IconRegistry, Locale, OverlayManager, Theme, Toast, UAlert, UAvatar, UBadge, UBreadcrumb, UBreadcrumbItem, UButton, UButtonGroup, UCard, UCarousel, UCheckbox, UChip, UCopyButton, UDialog, UDivider, UDrawer, UElement, UExpander, UField, UFloatingElement, UForm, UFormControlElement, UIcon, UIconButton, UInput, UMenu, UMenuItem, UOption, UOverlayElement, UPanel, UPopover, UProgressBar, UProgressRing, URadio, URating, USelect, USkeleton, USlider, USpinner, USplitPanel, USwitch, UTab, UTabPanel, UTag, UText, UTextarea, UTooltip, UTree, UTreeItem, arrayAttrConverter, booleanAttrConverter, dateAttrConverter, getDefaultBaseUrl, getParentElement, jsonAttrConverter, querySelectorAllWithin, querySelectorWithin, setDefaultBaseUrl, urlAttrConverter };
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { UExpander as UExpanderElement } from '../components/expander/UExpander';
3
+ import { type ExpandEventDetail } from '../events/ExpandEvent';
4
+ import { type CollapseEventDetail } from '../events/CollapseEvent';
5
+
6
+ export declare const UExpander: React.ForwardRefExoticComponent<
7
+ Omit<Partial<UExpanderElement>, keyof React.HTMLAttributes<UExpanderElement>>
8
+ & Omit<React.HTMLAttributes<UExpanderElement>, 'onExpand' | 'onCollapse'>
9
+ & React.RefAttributes<UExpanderElement>
10
+ & {
11
+ onExpand?: (event: CustomEvent<ExpandEventDetail>) => void;
12
+ onCollapse?: (event: CustomEvent<CollapseEventDetail>) => void;
13
+ }
14
+ >;
15
+
16
+ export type UExpanderProps = React.ComponentProps<typeof UExpander>;
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { createComponent } from '@lit/react';
3
+ import { UExpander as UExpanderElement } from '../components/expander/UExpander.js';
4
+
5
+ export const UExpander = createComponent({
6
+ react: React,
7
+ tagName: 'u-expander',
8
+ elementClass: UExpanderElement,
9
+ events: {
10
+ onExpand: 'expand',
11
+ onCollapse: 'collapse',
12
+ },
13
+ });
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+ import { UText as UTextElement } from '../components/text/UText';
3
+
4
+ export declare const UText: React.ForwardRefExoticComponent<
5
+ Omit<Partial<UTextElement>, keyof React.HTMLAttributes<UTextElement>>
6
+ & Omit<React.HTMLAttributes<UTextElement>, never>
7
+ & React.RefAttributes<UTextElement>
8
+ & {
9
+ }
10
+ >;
11
+
12
+ export type UTextProps = React.ComponentProps<typeof UText>;
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import { createComponent } from '@lit/react';
3
+ import { UText as UTextElement } from '../components/text/UText.js';
4
+
5
+ export const UText = createComponent({
6
+ react: React,
7
+ tagName: 'u-text',
8
+ elementClass: UTextElement,
9
+ events: {},
10
+ });
@@ -2,6 +2,7 @@ export { UTreeItem, UTreeItemProps } from './UTreeItem';
2
2
  export { UTree, UTreeProps } from './UTree';
3
3
  export { UTooltip, UTooltipProps } from './UTooltip';
4
4
  export { UTextarea, UTextareaProps } from './UTextarea';
5
+ export { UText, UTextProps } from './UText';
5
6
  export { UTag, UTagProps } from './UTag';
6
7
  export { UTabPanel, UTabPanelProps } from './UTabPanel';
7
8
  export { UTab, UTabProps } from './UTab';
@@ -25,6 +26,7 @@ export { UIconButton, UIconButtonProps } from './UIconButton';
25
26
  export { UIcon, UIconProps } from './UIcon';
26
27
  export { UForm, UFormProps } from './UForm';
27
28
  export { UField, UFieldProps } from './UField';
29
+ export { UExpander, UExpanderProps } from './UExpander';
28
30
  export { UDrawer, UDrawerProps } from './UDrawer';
29
31
  export { UDivider, UDividerProps } from './UDivider';
30
32
  export { UDialog, UDialogProps } from './UDialog';
@@ -2,6 +2,7 @@ export { UTreeItem } from './UTreeItem.js';
2
2
  export { UTree } from './UTree.js';
3
3
  export { UTooltip } from './UTooltip.js';
4
4
  export { UTextarea } from './UTextarea.js';
5
+ export { UText } from './UText.js';
5
6
  export { UTag } from './UTag.js';
6
7
  export { UTabPanel } from './UTabPanel.js';
7
8
  export { UTab } from './UTab.js';
@@ -25,6 +26,7 @@ export { UIconButton } from './UIconButton.js';
25
26
  export { UIcon } from './UIcon.js';
26
27
  export { UForm } from './UForm.js';
27
28
  export { UField } from './UField.js';
29
+ export { UExpander } from './UExpander.js';
28
30
  export { UDrawer } from './UDrawer.js';
29
31
  export { UDivider } from './UDivider.js';
30
32
  export { UDialog } from './UDialog.js';
@@ -115,7 +115,7 @@ var Dialog = class {
115
115
  try {
116
116
  await dialog.updateComplete;
117
117
  } catch (error) {
118
- console.error("[Dialog] 다이얼로그를 표시하지 못했습니다.", error);
118
+ console.error("[Dialog] Failed to show the dialog.", error);
119
119
  dialog.remove();
120
120
  return null;
121
121
  }
@@ -14,6 +14,31 @@ export type LocaleTag = SupportedLocale | (string & {});
14
14
  /** 라이브러리가 제공하는 chrome 문자열 키. 지금은 검증 메시지뿐이지만 이후 다른 UI 문구도 추가될 수 있다. */
15
15
  export type LocaleMessageKey = 'valueMissing' | 'badInput' | 'typeMismatch' | 'patternMismatch' | 'rangeUnderflow' | 'rangeOverflow' | 'stepMismatch' | 'tooShort' | 'tooLong';
16
16
  type LocaleTable = Record<LocaleMessageKey, string>;
17
+ /**
18
+ * 한 패키지(또는 앱 영역)의 문자열 묶음.
19
+ *
20
+ * 키 유니온을 **소비자가** 정하므로 라이브러리의 키셋은 커지지 않는다:
21
+ * ```ts
22
+ * const t = Locale.namespace<'empty' | 'loading'>('u-data-view');
23
+ * t.register('en', { empty: 'No data', loading: 'Loading…' });
24
+ * t.text('empty');
25
+ * ```
26
+ */
27
+ export interface LocaleNamespace<K extends string = string> {
28
+ /** 네임스페이스 이름. */
29
+ readonly name: string;
30
+ /**
31
+ * 로케일 하나의 테이블(전체 또는 일부)을 등록한다.
32
+ * 같은 로케일에 반복 호출하면 **병합**된다 — 일부 키만 넘겨도 나머지가 사라지지 않는다.
33
+ */
34
+ register(locale: LocaleTag, table: Partial<Record<K, string>>): void;
35
+ /**
36
+ * 활성 로케일 기준으로 문자열을 찾는다.
37
+ * 사슬(정확 일치 → base → en)에 없으면 **키 자체를 돌려준다** — 조용히 빈 문자열이
38
+ * 되는 것보다 화면에 드러나는 편이 낫다.
39
+ */
40
+ text(key: K, params?: Record<string, string | number>): string;
41
+ }
17
42
  /**
18
43
  * 검증 메시지 로케일을 관리하는 정적 유틸리티입니다.
19
44
  */
@@ -34,5 +59,14 @@ export declare class Locale {
34
59
  * `params`가 있으면 템플릿의 `{name}` 자리를 치환합니다.
35
60
  */
36
61
  static getValue(key: LocaleMessageKey, params?: Record<string, string | number>): string;
62
+ /**
63
+ * 네임스페이스 핸들을 얻습니다 — 같은 이름이면 **같은 저장소**를 가리킵니다.
64
+ *
65
+ * 상위 패키지가 자기 화면 문자열을 담는 자리입니다. 검증 메시지(`getValue`)의 키셋과
66
+ * 완전히 분리돼 있어, 이 API 를 써도 라이브러리의 `LocaleMessageKey` 는 커지지 않습니다.
67
+ *
68
+ * @param name 네임스페이스 이름. 패키지·컴포넌트 단위를 권장합니다(예: `'u-data-view'`).
69
+ */
70
+ static namespace<K extends string = string>(name: string): LocaleNamespace<K>;
37
71
  }
38
72
  export {};
@@ -39,21 +39,29 @@ function detectLocale() {
39
39
  return "en";
40
40
  }
41
41
  var active = detectLocale();
42
- /** 정확 일치 → base 언어(ko-KR → ko) → en 순으로 값을 찾는다. */
43
- function lookup(locale, key) {
42
+ /** 정확 일치 → base 언어(ko-KR → ko) → en 순의 조회 사슬. */
43
+ function chainOf(locale) {
44
44
  const norm = locale.toLowerCase();
45
45
  const base = norm.split("-")[0];
46
- const chain = base === norm ? [norm, "en"] : [
46
+ return base === norm ? [norm, "en"] : [
47
47
  norm,
48
48
  base,
49
49
  "en"
50
50
  ];
51
- for (const tag of chain) {
51
+ }
52
+ /** 템플릿의 `{name}` 자리를 치환한다. 값이 없는 자리는 그대로 남긴다(디버깅 단서). */
53
+ function interpolate(template, params) {
54
+ return params ? template.replace(/\{(\w+)\}/g, (_, name) => params[name] != null ? String(params[name]) : `{${name}}`) : template;
55
+ }
56
+ function lookup(locale, key) {
57
+ for (const tag of chainOf(locale)) {
52
58
  const value = overrides.get(tag)?.[key] ?? builtins.get(tag)?.[key];
53
59
  if (value) return value;
54
60
  }
55
61
  return builtins.get("en")[key];
56
62
  }
63
+ /** ns → 정규화 로케일 태그 → 테이블. */
64
+ var namespaces = /* @__PURE__ */ new Map();
57
65
  /**
58
66
  * 검증 메시지 로케일을 관리하는 정적 유틸리티입니다.
59
67
  */
@@ -85,8 +93,37 @@ var Locale = class {
85
93
  * `params`가 있으면 템플릿의 `{name}` 자리를 치환합니다.
86
94
  */
87
95
  static getValue(key, params) {
88
- const template = lookup(active, key);
89
- return params ? template.replace(/\{(\w+)\}/g, (_, name) => params[name] != null ? String(params[name]) : `{${name}}`) : template;
96
+ return interpolate(lookup(active, key), params);
97
+ }
98
+ /**
99
+ * 네임스페이스 핸들을 얻습니다 — 같은 이름이면 **같은 저장소**를 가리킵니다.
100
+ *
101
+ * 상위 패키지가 자기 화면 문자열을 담는 자리입니다. 검증 메시지(`getValue`)의 키셋과
102
+ * 완전히 분리돼 있어, 이 API 를 써도 라이브러리의 `LocaleMessageKey` 는 커지지 않습니다.
103
+ *
104
+ * @param name 네임스페이스 이름. 패키지·컴포넌트 단위를 권장합니다(예: `'u-data-view'`).
105
+ */
106
+ static namespace(name) {
107
+ return {
108
+ name,
109
+ register(locale, table) {
110
+ const byLocale = namespaces.get(name) ?? /* @__PURE__ */ new Map();
111
+ const norm = locale.toLowerCase();
112
+ byLocale.set(norm, {
113
+ ...byLocale.get(norm),
114
+ ...table
115
+ });
116
+ namespaces.set(name, byLocale);
117
+ },
118
+ text(key, params) {
119
+ const byLocale = namespaces.get(name);
120
+ for (const tag of chainOf(active)) {
121
+ const value = byLocale?.get(tag)?.[key];
122
+ if (value) return interpolate(value, params);
123
+ }
124
+ return key;
125
+ }
126
+ };
90
127
  }
91
128
  };
92
129
  //#endregion
@@ -65,6 +65,43 @@ export declare class Theme {
65
65
  static set(theme: ThemeType): void;
66
66
  /** 시스템 테마 변경을 처리하는 메서드 */
67
67
  private static handleSystemThemeChanged;
68
+ /** 마지막으로 지정된 브랜드 시드. 테마가 바뀌면 이 값으로 램프를 다시 계산한다. */
69
+ private static accentSeed;
70
+ /**
71
+ * 브랜드 색 하나로 `--u-primary-*` 램프를 만든다.
72
+ *
73
+ * ```ts
74
+ * Theme.accent('#6A1B9A'); // 램프 5단 + 면 위 글자색이 계산된다
75
+ * Theme.accent(null); // 해제 — 시트 기본값으로 되돌아간다
76
+ * ```
77
+ *
78
+ * 종전에는 소비자가 램프를 **손으로 열 줄 적었고**, 그 값이 대비 계약을 만족하는지는
79
+ * 아무것도 확인하지 않았다. 여기서는 «면 위 글자 4.5 · 바탕 위 글자 4.5 · 단 구분 1.20 ·
80
+ * 그래픽 3.0» 을 만족하는 값을 **계산해서** 넣는다(`utilities/accent.ts`).
81
+ *
82
+ * ⚠**바탕은 계산 시점의 `--u-bg-color` 를 읽는다** — 소비자가 바탕을 덮었으면 그 값을
83
+ * 기준으로 계산되고, 테마가 바뀌면 **다시 계산한다**(라이트/다크가 다른 램프를 갖는다).
84
+ * ⇒ `Theme.set()` · 시스템 테마 변경 뒤에 자동으로 재적용된다.
85
+ *
86
+ * ⚠**변수는 `documentElement` 의 인라인 스타일로 들어간다** — 시트보다 우선하므로
87
+ * 로드 순서에 기대지 않는다.
88
+ *
89
+ * 🔴**`--u-primary-bg-color` 는 세팅하지 않는다 — 이 자가 덮는 것은 7종 중 6종이다.**
90
+ * 그 토큰은 «글자를 얹는 옅은 면»(`u-tag` 의 `--tag-hue-surface`)이라 램프의 어느 단과도
91
+ * 성질이 다르고, 시트는 그 자리를 **5계열 × 2테마 = 10개 값으로 손수 짝지어** 두었다
92
+ * (라이트/다크가 같은 대비비를 쓰지 않는다 — 실측 1.14 대 1.03). 도출식으로 바꾸는 것은
93
+ * 팔레트 값 결정이라 사람 판단이 필요하다.
94
+ *
95
+ * ⇒ **시드만 넣으면 태그·선택된 행 같은 옅은 면이 파랑으로 남는다.** 함께 세팅할 것:
96
+ *
97
+ * ```ts
98
+ * Theme.accent('#7c3aed');
99
+ * document.documentElement.style.setProperty('--u-primary-bg-color', '#f3e8ff');
100
+ * ```
101
+ */
102
+ static accent(seed: string | null): void;
103
+ /** 현재 시드를 현재 테마의 바탕에 맞춰 다시 계산해 적용한다. */
104
+ private static applyAccent;
68
105
  /** 디버그 모드시 로그 출력 함수 (인스턴스 스코프) */
69
106
  private static log;
70
107
  }
@@ -1,7 +1,17 @@
1
1
  import { BrowserStorage } from "./BrowserStorage.js";
2
2
  import dark_css_default from "../_virtual/_glob-assets_raw/dark.css.e8ac080d.js";
3
3
  import light_css_default from "../_virtual/_glob-assets_raw/light.css.637cb2f8.js";
4
+ import { accentCustomProperties, deriveAccentRamp } from "./accent.js";
4
5
  //#region src/utilities/Theme.ts
6
+ /** 프로퍼티 «이름»만 필요할 때 쓰는 더미 — 값은 쓰지 않는다. */
7
+ var EMPTY_RAMP = {
8
+ weakest: "#000",
9
+ weaker: "#000",
10
+ weak: "#000",
11
+ color: "#000",
12
+ strong: "#000",
13
+ txt: "#000"
14
+ };
5
15
  /**
6
16
  * 스타일 시트 번들 로드, 내부 자산에서 CSS를 인라인으로 가져옵니다.
7
17
  * 빌드 시점에 정적 파일을 포함시키기 위해 vite의 `import.meta.glob`을 사용합니다.
@@ -139,6 +149,7 @@ var Theme = class {
139
149
  } catch (e) {
140
150
  this.log("Error applying theme:", e);
141
151
  }
152
+ this.applyAccent();
142
153
  if (this.storage !== null) {
143
154
  this.storage.set(this.STORAGE_THEME_KEY, theme);
144
155
  this.log("saved theme to storage", theme);
@@ -150,9 +161,63 @@ var Theme = class {
150
161
  const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
151
162
  document.documentElement.setAttribute("theme", isDark ? "dark" : "light");
152
163
  this.log("system theme changed, applied", isDark ? "dark" : "light");
164
+ this.applyAccent();
153
165
  }
154
166
  };
155
167
  }
168
+ static {
169
+ this.accentSeed = null;
170
+ }
171
+ /**
172
+ * 브랜드 색 하나로 `--u-primary-*` 램프를 만든다.
173
+ *
174
+ * ```ts
175
+ * Theme.accent('#6A1B9A'); // 램프 5단 + 면 위 글자색이 계산된다
176
+ * Theme.accent(null); // 해제 — 시트 기본값으로 되돌아간다
177
+ * ```
178
+ *
179
+ * 종전에는 소비자가 램프를 **손으로 열 줄 적었고**, 그 값이 대비 계약을 만족하는지는
180
+ * 아무것도 확인하지 않았다. 여기서는 «면 위 글자 4.5 · 바탕 위 글자 4.5 · 단 구분 1.20 ·
181
+ * 그래픽 3.0» 을 만족하는 값을 **계산해서** 넣는다(`utilities/accent.ts`).
182
+ *
183
+ * ⚠**바탕은 계산 시점의 `--u-bg-color` 를 읽는다** — 소비자가 바탕을 덮었으면 그 값을
184
+ * 기준으로 계산되고, 테마가 바뀌면 **다시 계산한다**(라이트/다크가 다른 램프를 갖는다).
185
+ * ⇒ `Theme.set()` · 시스템 테마 변경 뒤에 자동으로 재적용된다.
186
+ *
187
+ * ⚠**변수는 `documentElement` 의 인라인 스타일로 들어간다** — 시트보다 우선하므로
188
+ * 로드 순서에 기대지 않는다.
189
+ *
190
+ * 🔴**`--u-primary-bg-color` 는 세팅하지 않는다 — 이 자가 덮는 것은 7종 중 6종이다.**
191
+ * 그 토큰은 «글자를 얹는 옅은 면»(`u-tag` 의 `--tag-hue-surface`)이라 램프의 어느 단과도
192
+ * 성질이 다르고, 시트는 그 자리를 **5계열 × 2테마 = 10개 값으로 손수 짝지어** 두었다
193
+ * (라이트/다크가 같은 대비비를 쓰지 않는다 — 실측 1.14 대 1.03). 도출식으로 바꾸는 것은
194
+ * 팔레트 값 결정이라 사람 판단이 필요하다.
195
+ *
196
+ * ⇒ **시드만 넣으면 태그·선택된 행 같은 옅은 면이 파랑으로 남는다.** 함께 세팅할 것:
197
+ *
198
+ * ```ts
199
+ * Theme.accent('#7c3aed');
200
+ * document.documentElement.style.setProperty('--u-primary-bg-color', '#f3e8ff');
201
+ * ```
202
+ */
203
+ static accent(seed) {
204
+ this.accentSeed = seed;
205
+ this.applyAccent();
206
+ }
207
+ /** 현재 시드를 현재 테마의 바탕에 맞춰 다시 계산해 적용한다. */
208
+ static applyAccent() {
209
+ if (typeof document === "undefined") return;
210
+ const root = document.documentElement;
211
+ const names = Object.keys(accentCustomProperties(EMPTY_RAMP));
212
+ if (!this.accentSeed) {
213
+ for (const n of names) root.style.removeProperty(n);
214
+ return;
215
+ }
216
+ const bg = getComputedStyle(root).getPropertyValue("--u-bg-color").trim() || "#ffffff";
217
+ const ramp = deriveAccentRamp(this.accentSeed, bg);
218
+ for (const [name, value] of Object.entries(accentCustomProperties(ramp))) root.style.setProperty(name, value);
219
+ this.log("accent applied", this.accentSeed, "→", ramp);
220
+ }
156
221
  /** 디버그 모드시 로그 출력 함수 (인스턴스 스코프) */
157
222
  static log(...args) {
158
223
  if (this._isDebugMode) console.log("[theme]", ...args);
@@ -0,0 +1,52 @@
1
+ /**
2
+ * **브랜드 악센트 시드 → 역할 램프 파생.**
3
+ *
4
+ * 소비자가 시드 색 하나를 주면 `--u-primary-*` 5단과 그 면 위의 글자색을 계산한다.
5
+ * 종전에는 소비자가 램프를 **손으로 열 줄 적었고**, 그 값들이 대비 계약을 만족하는지는
6
+ * 아무것도 확인하지 않았다.
7
+ *
8
+ * ## 🔴 왜 CSS 만으로 하지 않는가 — 실측이 정한 것이다
9
+ *
10
+ * `color-mix()` 로 고정 비율만 섞으면 **밝은 시드에서 계약이 깨진다** — 노랑 시드의
11
+ * `-strong` 이 바탕 대비 **2.21**(기준 4.5)이었다. 목표 대비까지 섞으려면 **대비를 계산**
12
+ * 해야 하고 CSS 에는 그 수단이 없다. ⇒ 계산은 JS 가 하고, 결과만 커스텀 프로퍼티로 넣는다.
13
+ *
14
+ * ## ⚠ 두 목표를 «동시에» 건다
15
+ *
16
+ * 대비만 목표로 삼으면 시드가 이미 4.5 를 넘을 때 탐색이 **시드 자신에서 멈춰**
17
+ * `-strong` 과 `-color` 가 같은 값이 된다(실측: 5역할 중 4). 그러면 계약은 만족하는데
18
+ * **위계가 사라진다.** 그래서 «바탕과 4.5» 와 «`-color` 와 1.20» 을 함께 만족시킨다.
19
+ *
20
+ * ## 방향은 «바탕 기준»이다 — 라이트/다크가 같은 코드로 돈다
21
+ *
22
+ * `-strong` 은 바탕에서 **멀어지는** 쪽(라이트면 검정, 다크면 흰색), `-weak` 이하는 바탕에
23
+ * **가까워지는** 쪽이다. 다크 시트가 팔레트를 반전시켜 놓은 것과 같은 방향이다.
24
+ */
25
+ /** 역할 램프 한 벌. 시트의 `--u-{role}-color-*` 와 이름이 1:1 이다. */
26
+ export interface AccentRamp {
27
+ weakest: string;
28
+ weaker: string;
29
+ weak: string;
30
+ color: string;
31
+ strong: string;
32
+ /** 면(`color`) 위에 놓이는 글자색 — `--u-{role}-txt-color`. */
33
+ txt: string;
34
+ }
35
+ /** 대비 계약의 문턱값 — `tests/build/token-contrast.test.ts` 와 같은 값이다. */
36
+ export declare const AA_TEXT = 4.5;
37
+ export declare const AA_GRAPHIC = 3;
38
+ /** `-strong` 이 `-color` 와 갈려 보이기 위한 최소 대비(2026-08-04 채택). */
39
+ export declare const MIN_STEP_SEPARATION = 1.2;
40
+ /** `#rgb`·`#rrggbb`·`rgb(r g b)` 를 [r,g,b] 로. 해석할 수 없으면 `null`. */
41
+ export declare function parseColor(value: string): [number, number, number] | null;
42
+ export declare function toHex([r, g, b]: [number, number, number]): string;
43
+ export declare function luminance(color: string): number;
44
+ /** WCAG 2.1 명암비. */
45
+ export declare function contrast(a: string, b: string): number;
46
+ /** `color-mix(in srgb, a p%, b)` 와 같은 계산. */
47
+ export declare function mix(a: string, b: string, p: number): string;
48
+ /** 면 위의 글자를 고른다 — 검정/흰색 중 대비가 큰 쪽. */
49
+ export declare function pickOnColor(surface: string): string;
50
+ export declare function deriveAccentRamp(seed: string, bg: string): AccentRamp;
51
+ /** 램프를 커스텀 프로퍼티 이름 → 값 맵으로. */
52
+ export declare function accentCustomProperties(ramp: AccentRamp, role?: string): Record<string, string>;
@@ -0,0 +1,127 @@
1
+ //#region src/utilities/accent.ts
2
+ /** 대비 계약의 문턱값 — `tests/build/token-contrast.test.ts` 와 같은 값이다. */
3
+ var AA_TEXT = 4.5;
4
+ /** `-strong` 이 `-color` 와 갈려 보이기 위한 최소 대비(2026-08-04 채택). */
5
+ var MIN_STEP_SEPARATION = 1.2;
6
+ var clamp = (v) => Math.min(255, Math.max(0, v));
7
+ var channelLum = (c) => c <= .03928 ? c / 12.92 : Math.pow((c + .055) / 1.055, 2.4);
8
+ /** `#rgb`·`#rrggbb`·`rgb(r g b)` 를 [r,g,b] 로. 해석할 수 없으면 `null`. */
9
+ function parseColor(value) {
10
+ const v = value.trim();
11
+ const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(v);
12
+ if (hex) {
13
+ const h = hex[1].length === 3 ? hex[1].replace(/./g, (c) => c + c) : hex[1];
14
+ const n = parseInt(h, 16);
15
+ return [
16
+ n >> 16 & 255,
17
+ n >> 8 & 255,
18
+ n & 255
19
+ ];
20
+ }
21
+ const rgb = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)/i.exec(v);
22
+ if (rgb) return [
23
+ Number(rgb[1]),
24
+ Number(rgb[2]),
25
+ Number(rgb[3])
26
+ ];
27
+ return null;
28
+ }
29
+ function toHex([r, g, b]) {
30
+ return "#" + [
31
+ r,
32
+ g,
33
+ b
34
+ ].map((v) => Math.round(clamp(v)).toString(16).padStart(2, "0")).join("");
35
+ }
36
+ function luminance(color) {
37
+ const rgb = parseColor(color);
38
+ if (!rgb) return 0;
39
+ const [r, g, b] = rgb.map((v) => channelLum(v / 255));
40
+ return .2126 * r + .7152 * g + .0722 * b;
41
+ }
42
+ /** WCAG 2.1 명암비. */
43
+ function contrast(a, b) {
44
+ const [x, y] = [luminance(a), luminance(b)];
45
+ return (Math.max(x, y) + .05) / (Math.min(x, y) + .05);
46
+ }
47
+ /** `color-mix(in srgb, a p%, b)` 와 같은 계산. */
48
+ function mix(a, b, p) {
49
+ const [x, y] = [parseColor(a), parseColor(b)];
50
+ if (!x || !y) return a;
51
+ return toHex([
52
+ 0,
53
+ 1,
54
+ 2
55
+ ].map((i) => x[i] * p + y[i] * (1 - p)));
56
+ }
57
+ /** 면 위의 글자를 고른다 — 검정/흰색 중 대비가 큰 쪽. */
58
+ function pickOnColor(surface) {
59
+ return contrast(surface, "#000000") >= contrast(surface, "#ffffff") ? "#000000" : "#ffffff";
60
+ }
61
+ /**
62
+ * 시드에서 `--u-primary-*` 램프를 만든다.
63
+ *
64
+ * @param seed 브랜드 색
65
+ * @param bg 그 램프가 놓일 바탕(`--u-bg-color`) — 라이트/다크가 이 값 하나로 갈린다
66
+ */
67
+ var STEP = .02;
68
+ /**
69
+ * `from` 에서 `target` 쪽으로 조금씩 옮기며 **조건을 만족하는 첫 색**을 돌려준다.
70
+ * 못 찾으면 `null` — 호출부가 반대 방향을 시도한다.
71
+ */
72
+ function towards(from, target, ok) {
73
+ for (let p = 1; p >= 0; p -= STEP) {
74
+ const c = mix(from, target, p);
75
+ if (ok(c)) return c;
76
+ }
77
+ return null;
78
+ }
79
+ function deriveAccentRamp(seed, bg) {
80
+ const light = luminance(bg) > .5;
81
+ const away = light ? "#000000" : "#ffffff";
82
+ const near = light ? "#ffffff" : "#000000";
83
+ /**
84
+ * 🔴**방향은 항상 «바탕에서 멀어지는 쪽»이 아니다 — 실측이 이 폴백을 만들게 했다.**
85
+ *
86
+ * `#F5F5F5` 시드 + 다크 바탕 → `-strong` 을 흰색 쪽으로 밀어도 **구분 1.09** 가 한계다
87
+ * (시드가 이미 거의 흰색이다) ⇒ 반대로 «어둡게» 가야 갈린다
88
+ * `#6A1B9A` 시드 + 다크 바탕 → `-weak` 이 바탕 쪽으로는 그래픽 3.0 을 못 낸다
89
+ *
90
+ * ⇒ 한 방향을 시도하고 실패하면 **반대 방향**을 본다. 두 방향 다 실패하는 것만 극단값으로
91
+ * 떨어뜨린다.
92
+ */
93
+ const either = (from, ok, first, second) => towards(from, first, ok) ?? towards(from, second, ok);
94
+ const color = either(seed, (c) => contrast(c, pickOnColor(c)) >= 4.5, away, near) ?? seed;
95
+ const strongOk = (c) => contrast(c, bg) >= 4.5 && contrast(c, color) >= 1.2;
96
+ const strong = either(color, strongOk, away, near) ?? away;
97
+ let weak;
98
+ if (contrast(color, bg) >= 3) {
99
+ weak = color;
100
+ for (let p = 1; p >= 0; p -= STEP) {
101
+ const candidate = mix(color, bg, p);
102
+ if (contrast(candidate, bg) < 3) break;
103
+ weak = candidate;
104
+ }
105
+ } else weak = towards(color, away, (c) => contrast(c, bg) >= 3) ?? away;
106
+ return {
107
+ weakest: mix(weak, bg, .25),
108
+ weaker: mix(weak, bg, .5),
109
+ weak,
110
+ color,
111
+ strong,
112
+ txt: pickOnColor(color)
113
+ };
114
+ }
115
+ /** 램프를 커스텀 프로퍼티 이름 → 값 맵으로. */
116
+ function accentCustomProperties(ramp, role = "primary") {
117
+ return {
118
+ [`--u-${role}-color-weakest`]: ramp.weakest,
119
+ [`--u-${role}-color-weaker`]: ramp.weaker,
120
+ [`--u-${role}-color-weak`]: ramp.weak,
121
+ [`--u-${role}-color`]: ramp.color,
122
+ [`--u-${role}-color-strong`]: ramp.strong,
123
+ [`--u-${role}-txt-color`]: ramp.txt
124
+ };
125
+ }
126
+ //#endregion
127
+ export { AA_TEXT, MIN_STEP_SEPARATION, accentCustomProperties, contrast, deriveAccentRamp, luminance, mix, parseColor, pickOnColor, toHex };
@@ -187,7 +187,7 @@ var IconRegistry = class {
187
187
  IconCache.set(lib, name, svg);
188
188
  return svg;
189
189
  } catch (error) {
190
- console.error(`[IconRegistry] '${key}' 아이콘 리졸브 실패 (재시도 가능):`, error);
190
+ console.error(`[IconRegistry] Failed to resolve icon '${key}' (will retry):`, error);
191
191
  return;
192
192
  }
193
193
  })();
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 역할 축(의미)의 상태 → 내장 아이콘 이름.
3
+ *
4
+ * ## 왜 있나
5
+ *
6
+ * `u-tag`·`u-badge` 의 상태는 **색으로만** 전달돼 왔다. 색각 이상(남성 약 8%)이나 흑백
7
+ * 인쇄에서는 *"성공"* 과 *"실패"* 가 같은 회색 알약이 된다. 대비 계약(1.20.0)은
8
+ * *읽히는가* 를 지키지 *구별되는가* 를 지키지 않는다 — 역할 색 충돌 검사(ΔE)가
9
+ * **색 공간에서** 세운 그 구분을, 이 표는 **모양 공간에서** 세운다.
10
+ *
11
+ * ## 매핑하지 않는 것
12
+ *
13
+ * - **장식 축**(`blue`·`purple` …)은 *색 자체*를 뜻하지 의미를 뜻하지 않는다.
14
+ * - **`neutral`·`primary`** 는 상태가 아니다(`primary` 는 브랜드 강조이지 «좋음»이 아니다).
15
+ *
16
+ * ⇒ 그 자리에 `icon` 을 줘도 **아무것도 그리지 않는다.** 없는 의미를 아이콘으로 지어내면
17
+ * 그 아이콘이 잘못된 정보를 나른다.
18
+ *
19
+ * ⚠**`u-alert` 의 `status` 축(`error`·`notice` …)은 다른 어휘다** — 이름이 겹치지 않으므로
20
+ * 한 표로 합치지 않았다. 합치려면 두 축의 이름을 먼저 통일해야 하고, 그것은 파괴적 변경이다.
21
+ */
22
+ export declare const STATUS_ICON: {
23
+ readonly info: "info-circle-fill";
24
+ readonly success: "circle-check-fill";
25
+ readonly warning: "alert-triangle-fill";
26
+ readonly danger: "alert-circle-fill";
27
+ };
28
+ /** 의미를 갖는 역할 값. */
29
+ export type StatusRole = keyof typeof STATUS_ICON;
30
+ /** 역할 값이면 아이콘 이름을, 아니면 `undefined` 를 돌려준다. */
31
+ export declare function statusIcon(color?: string): string | undefined;