@iyulab/chat-components 0.3.0 → 0.4.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 (108) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/assets/action-prompt.md.js +3 -0
  3. package/dist/assets/widget-prompt.md.js +3 -0
  4. package/dist/components/actions/UQuestionAction.component.d.ts +18 -0
  5. package/dist/components/actions/UQuestionAction.component.js +58 -0
  6. package/dist/components/actions/UQuestionAction.d.ts +7 -0
  7. package/dist/components/actions/UQuestionAction.js +5 -0
  8. package/dist/components/actions/UQuestionAction.styles.js +45 -0
  9. package/dist/components/blocks/UCodeBlock.component.d.ts +5 -3
  10. package/dist/components/blocks/UCodeBlock.component.js +14 -2
  11. package/dist/components/blocks/UCodeBlock.styles.js +6 -0
  12. package/dist/components/blocks/UFilesBlock.component.d.ts +22 -0
  13. package/dist/components/blocks/UFilesBlock.component.js +192 -0
  14. package/dist/components/blocks/UFilesBlock.d.ts +7 -0
  15. package/dist/components/blocks/UFilesBlock.js +5 -0
  16. package/dist/components/blocks/UFilesBlock.styles.d.ts +1 -0
  17. package/dist/components/blocks/UFilesBlock.styles.js +206 -0
  18. package/dist/components/blocks/UJsonBlock.component.d.ts +3 -3
  19. package/dist/components/blocks/UJsonBlock.component.js +2 -2
  20. package/dist/components/blocks/UMarkedBlock.component.d.ts +17 -22
  21. package/dist/components/blocks/UMarkedBlock.component.js +92 -62
  22. package/dist/components/blocks/URefBlock.component.d.ts +3 -3
  23. package/dist/components/blocks/URefBlock.component.js +6 -7
  24. package/dist/components/blocks/UTableBlock.component.d.ts +49 -0
  25. package/dist/components/blocks/UTableBlock.component.js +228 -0
  26. package/dist/components/blocks/UTableBlock.d.ts +7 -0
  27. package/dist/components/blocks/UTableBlock.js +5 -0
  28. package/dist/components/blocks/UTableBlock.styles.d.ts +1 -0
  29. package/dist/components/blocks/UTableBlock.styles.js +134 -0
  30. package/dist/components/blocks/UTextBlock.component.d.ts +3 -3
  31. package/dist/components/blocks/UTextBlock.component.js +2 -2
  32. package/dist/components/blocks/UThinkBlock.component.d.ts +3 -3
  33. package/dist/components/blocks/UThinkBlock.component.js +2 -2
  34. package/dist/components/blocks/UToolBlock.component.d.ts +3 -3
  35. package/dist/components/blocks/UToolBlock.component.js +2 -2
  36. package/dist/components/buttons/UAttachButton.component.d.ts +3 -3
  37. package/dist/components/buttons/UAttachButton.component.js +2 -2
  38. package/dist/components/buttons/UCopyButton.component.d.ts +3 -3
  39. package/dist/components/buttons/UCopyButton.component.js +2 -2
  40. package/dist/components/buttons/UReportButton.component.d.ts +3 -3
  41. package/dist/components/buttons/UReportButton.component.js +2 -2
  42. package/dist/components/buttons/URetryButton.component.d.ts +3 -3
  43. package/dist/components/buttons/URetryButton.component.js +2 -2
  44. package/dist/components/buttons/UShareButton.component.d.ts +3 -3
  45. package/dist/components/buttons/UShareButton.component.js +2 -2
  46. package/dist/components/buttons/UVoteButton.component.d.ts +3 -3
  47. package/dist/components/buttons/UVoteButton.component.js +7 -4
  48. package/dist/components/message/UMessage.component.d.ts +7 -7
  49. package/dist/components/message/UMessage.component.js +16 -50
  50. package/dist/components/message/UMessage.styles.js +38 -11
  51. package/dist/components/prompt/UPrompt.component.d.ts +3 -3
  52. package/dist/components/prompt/UPrompt.component.js +2 -2
  53. package/dist/components/prompt/UPrompt.styles.js +28 -0
  54. package/dist/components/references/URefCard.component.d.ts +9 -6
  55. package/dist/components/references/URefCard.component.js +14 -10
  56. package/dist/components/references/URefCardGroup.component.d.ts +4 -3
  57. package/dist/components/references/URefCardGroup.component.js +3 -3
  58. package/dist/components/references/URefTag.component.d.ts +3 -3
  59. package/dist/components/references/URefTag.component.js +2 -2
  60. package/dist/components/widgets/UChartWidget.component.d.ts +36 -0
  61. package/dist/components/widgets/UChartWidget.component.js +180 -0
  62. package/dist/components/widgets/UChartWidget.d.ts +7 -0
  63. package/dist/components/widgets/UChartWidget.js +5 -0
  64. package/dist/components/widgets/UChartWidget.styles.d.ts +1 -0
  65. package/dist/components/widgets/UChartWidget.styles.js +86 -0
  66. package/dist/components/widgets/UImagesWidget.component.d.ts +30 -0
  67. package/dist/components/widgets/UImagesWidget.component.js +164 -0
  68. package/dist/components/widgets/UImagesWidget.d.ts +7 -0
  69. package/dist/components/widgets/UImagesWidget.js +5 -0
  70. package/dist/components/widgets/UImagesWidget.styles.d.ts +1 -0
  71. package/dist/components/widgets/UImagesWidget.styles.js +218 -0
  72. package/dist/components/widgets/UMapWidget.component.d.ts +20 -0
  73. package/dist/components/widgets/UMapWidget.component.js +65 -0
  74. package/dist/components/widgets/UMapWidget.d.ts +7 -0
  75. package/dist/components/widgets/UMapWidget.js +5 -0
  76. package/dist/components/widgets/UMapWidget.styles.d.ts +1 -0
  77. package/dist/components/widgets/UMapWidget.styles.js +47 -0
  78. package/dist/components/widgets/UVideoWidget.component.d.ts +21 -0
  79. package/dist/components/widgets/UVideoWidget.component.js +106 -0
  80. package/dist/components/widgets/UVideoWidget.d.ts +7 -0
  81. package/dist/components/widgets/UVideoWidget.js +5 -0
  82. package/dist/components/widgets/UVideoWidget.styles.d.ts +1 -0
  83. package/dist/components/widgets/UVideoWidget.styles.js +36 -0
  84. package/dist/components/widgets/UWidget.component.d.ts +43 -0
  85. package/dist/components/widgets/UWidget.component.js +140 -0
  86. package/dist/components/widgets/UWidget.d.ts +7 -0
  87. package/dist/components/widgets/UWidget.js +5 -0
  88. package/dist/components/widgets/UWidget.styles.d.ts +1 -0
  89. package/dist/components/widgets/UWidget.styles.js +33 -0
  90. package/dist/index.d.ts +13 -1
  91. package/dist/index.js +20 -2
  92. package/dist/types/Actions.d.ts +24 -0
  93. package/dist/types/Actions.js +34 -0
  94. package/dist/types/BlockItem.d.ts +32 -1
  95. package/dist/types/JsonSchema.d.ts +59 -0
  96. package/dist/types/Widgets.d.ts +34 -0
  97. package/dist/types/Widgets.js +115 -0
  98. package/dist/utilities/ActionPromptBuilder.d.ts +40 -0
  99. package/dist/utilities/ActionPromptBuilder.js +93 -0
  100. package/dist/utilities/WidgetPromptBuilder.d.ts +28 -0
  101. package/dist/utilities/WidgetPromptBuilder.js +87 -0
  102. package/package.json +12 -12
  103. package/dist/components/loaders/UDotLoader.component.d.ts +0 -9
  104. package/dist/components/loaders/UDotLoader.component.js +0 -23
  105. package/dist/components/loaders/UDotLoader.d.ts +0 -7
  106. package/dist/components/loaders/UDotLoader.js +0 -5
  107. package/dist/components/loaders/UDotLoader.styles.js +0 -50
  108. /package/dist/components/{loaders/UDotLoader.styles.d.ts → actions/UQuestionAction.styles.d.ts} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 (2026-03-03)
4
+
5
+ ### Breaking Changes
6
+ - `UMessage`의 `items` prop이 제거됨 — slot 방식으로 블록 컴포넌트를 직접 삽입해야 함
7
+ - `UMessage`의 `BaseElement` 의존성이 `UElement`로 변경됨
8
+
9
+ ### Added
10
+
11
+ #### Widget System
12
+ - `UWidget` - 위젯 컨테이너 컴포넌트 (widget-json 블록 기반 동적 렌더링)
13
+ - `UImagesWidget` - 이미지 갤러리 위젯 (스크롤 가능한 이미지 목록 표시)
14
+ - `UVideoWidget` - 비디오 임베드 위젯 (YouTube, Vimeo, 직접 URL 지원)
15
+ - `UMapWidget` - OpenStreetMap 기반 지도 위젯 (위경도 좌표 및 마커 표시)
16
+ - `UChartWidget` - Chart.js 기반 차트 위젯 (bar, line, pie, doughnut 등 8가지 타입, PNG/JSON 다운로드, 전체화면, 다크모드 테마 자동 감지)
17
+
18
+ #### Action System
19
+ - `UQuestionAction` - 클릭 가능한 선택지를 제공하는 질문 액션 컴포넌트 (action-json 블록 기반)
20
+
21
+ #### New Block Types
22
+ - `UFilesBlock` - 파일 첨부 목록 표시 블록 (파일명, 크기, MIME 타입, 다운로드 URL 지원, 삭제 버튼 옵션)
23
+ - `UTableBlock` - 테이블 표시 블록
24
+
25
+ #### New Types
26
+ - `JsonSchema` - LLM용 JSON Schema 타입 (`BooleanJsonSchema`, `StringJsonSchema`, `NumberJsonSchema`, `ArrayJsonSchema`, `ObjectJsonSchema`)
27
+ - `ActionDefinition`, `ActionSchema`, `PresetAction` - 액션 정의 및 프리셋 타입
28
+ - `WidgetDefinition`, `PresetWidget` - 위젯 정의 및 프리셋 타입 (`Images`, `Video`, `Map`, `Chart`)
29
+ - `FilesBlockItem`, `FileItem` - 파일 블록 타입
30
+ - `MessageFit` - 메시지 너비 설정 타입 (`full` | `auto`)
31
+
32
+ #### New Utilities
33
+ - `ActionPromptBuilder` - LLM 액션 프롬프트 자동 생성 유틸리티
34
+ - `WidgetPromptBuilder` - LLM 위젯 프롬프트 자동 생성 유틸리티
35
+
36
+ ### Changed
37
+ - `UMessage`: `BaseElement` → `UElement` 로 변경, `items` prop 제거 및 slot 기반 렌더링으로 전환, `fit` 속성(`MessageFit`) 추가
38
+ - `UMarkedBlock`: 마크다운 렌더링 로직 전면 개선
39
+ - `UPrompt`: 스타일 개선
40
+
41
+ ### Removed
42
+ - `UDotLoader` 컴포넌트 제거
43
+
44
+ ---
45
+
3
46
  ## 0.3.0 (2026-01-22)
4
47
 
5
48
  - Renamed `TextBlockReference` to `ReferenceCitation` and field `name` to `label`
@@ -0,0 +1,3 @@
1
+ const template = "## Interactive Actions\n\nYou can guide the user with interactive actions by appending `action-json` fenced code blocks at the end of your response.\nThe user will see these as interactive UI elements and can click them to continue the conversation.\n\n**Output format (the fence language must be `action-json`):**\n```action-json\n{\n \"type\": \"action-type-here\",\n \"properties\" : {\n \"key\": \"value\"\n }\n}\n```\n\n**Rules — follow these STRICTLY. Violation means the feature will not work:**\n1. ALWAYS wrap the block in triple backticks with the language identifier `action-json` exactly — never use `json`, plain text, or any other identifier.\n2. `type` must be one of the exact strings listed below. Do not invent new types.\n3. Place ALL action blocks at the very end of your response, after all prose content.\n4. Output valid JSON only — no comments, no trailing commas, no markdown inside the block.\n5. Only include actions when they genuinely help the user continue the conversation.\n6. Do NOT describe or explain the action block in surrounding text (e.g. do not write \"Here are some suggested questions:\"). Just output the block.\n\n**Available actions:**\n{{ACTION_LIST}}\n\n---\n\n{{ACTION_DOCS}}\n";
2
+
3
+ export { template as default };
@@ -0,0 +1,3 @@
1
+ const template = "## Renderable Widgets\n\nYou can render interactive visual widgets inside your response.\nWhen a widget would make your answer clearer or more useful, output a `widget-json` fenced code block.\n\n**Output format (the fence language must be `widget-json`):**\n```widget-json\n{\n \"tag\": \"exact-widget-tag-here\",\n \"properties\": {\n \"key\": \"value\"\n }\n}\n```\n\n**Rules — follow these strictly:**\n1. The fenced block language identifier must be `widget-json`, not `json` or anything else.\n2. `tag` must be one of the exact strings listed below. Never invent a tag.\n3. Include every property listed under `required`. Omit optional properties only if not needed.\n4. For schema-less `object` fields (e.g., Chart.js `data` / `options`), output a complete, realistic configuration using your knowledge.\n5. Output valid JSON — no comments, no trailing commas.\n\n**Available widgets:**\n{{WIDGET_LIST}}\n\n---\n\n{{WIDGET_DOCS}}\n";
2
+
3
+ export { template as default };
@@ -0,0 +1,18 @@
1
+ import { nothing } from 'lit';
2
+ import { UElement } from '@iyulab/components/dist/components/UElement.js';
3
+ /**
4
+ * 질문/선택지 제안 블록 컴포넌트
5
+ *
6
+ * LLM이 질문 텍스트(`question`)와 클릭 선택지(`choices`)를 제시하고,
7
+ * `input: true`일 때는 사용자가 직접 텍스트를 입력할 수도 있습니다.
8
+ */
9
+ export declare class UQuestionAction extends UElement {
10
+ static styles: import('lit').CSSResultGroup[];
11
+ static dependencies: Record<string, typeof UElement>;
12
+ /** LLM이 제시하는 질문 텍스트 */
13
+ question?: string;
14
+ /** 클릭 가능한 선택지 목록 */
15
+ choices: string[];
16
+ render(): import('lit-html').TemplateResult<1> | typeof nothing;
17
+ private handleChoiceClick;
18
+ }
@@ -0,0 +1,58 @@
1
+ import { nothing, html } from 'lit';
2
+ import { property } from 'lit/decorators.js';
3
+ import { repeat } from 'lit/directives/repeat.js';
4
+ import { UElement } from '@iyulab/components/dist/components/UElement.js';
5
+ import { UButton } from '@iyulab/components/dist/components/button/UButton.component.js';
6
+ import { styles } from './UQuestionAction.styles.js';
7
+
8
+ var __defProp = Object.defineProperty;
9
+ var __decorateClass = (decorators, target, key, kind) => {
10
+ var result = void 0 ;
11
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
12
+ if (decorator = decorators[i])
13
+ result = (decorator(target, key, result) ) || result;
14
+ if (result) __defProp(target, key, result);
15
+ return result;
16
+ };
17
+ class UQuestionAction extends UElement {
18
+ constructor() {
19
+ super(...arguments);
20
+ this.choices = [];
21
+ }
22
+ static {
23
+ this.styles = [super.styles, styles];
24
+ }
25
+ static {
26
+ this.dependencies = {
27
+ "u-button": UButton
28
+ };
29
+ }
30
+ render() {
31
+ if (!this.choices || this.choices.length === 0) {
32
+ return nothing;
33
+ }
34
+ return html`
35
+ <p class="question" ?hidden=${!this.question}>${this.question}</p>
36
+ <div class="choices">
37
+ ${repeat(this.choices, (value, index) => html`
38
+ <u-button @click=${() => this.handleChoiceClick(index)}>
39
+ ${value}
40
+ </u-button>
41
+ `)}
42
+ </div>
43
+ `;
44
+ }
45
+ handleChoiceClick(index) {
46
+ const value = this.choices[index];
47
+ if (!value) return;
48
+ this.emit("u-submit", { value });
49
+ }
50
+ }
51
+ __decorateClass([
52
+ property({ type: String })
53
+ ], UQuestionAction.prototype, "question");
54
+ __decorateClass([
55
+ property({ type: Array })
56
+ ], UQuestionAction.prototype, "choices");
57
+
58
+ export { UQuestionAction };
@@ -0,0 +1,7 @@
1
+ import { UQuestionAction } from './UQuestionAction.component.js';
2
+ declare global {
3
+ interface HTMLElementTagNameMap {
4
+ "u-question-action": UQuestionAction;
5
+ }
6
+ }
7
+ export { UQuestionAction };
@@ -0,0 +1,5 @@
1
+ import { UQuestionAction } from './UQuestionAction.component.js';
2
+
3
+ UQuestionAction.define("u-question-action");
4
+
5
+ export { UQuestionAction };
@@ -0,0 +1,45 @@
1
+ import { css } from 'lit';
2
+
3
+ const styles = css`
4
+ :host {
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: 0.5em;
8
+ margin: 0.75em 0;
9
+ }
10
+
11
+ .question {
12
+ margin: 0 0 0.25em;
13
+ font-size: 0.9em;
14
+ color: var(--u-txt-color-weak);
15
+ line-height: 1.5;
16
+ }
17
+
18
+ .choices {
19
+ display: flex;
20
+ flex-direction: column;
21
+ gap: 0.4em;
22
+ }
23
+
24
+ u-button {
25
+ display: block;
26
+ width: fit-content;
27
+ padding: 0.5em 0.75em;
28
+ font-size: 0.875em;
29
+ justify-content: flex-start;
30
+ line-height: 1.4;
31
+ border-radius: 10px;
32
+ box-shadow: 0 1px 2px var(--u-shadow-color-weaker);
33
+ transition: all 0.15s ease;
34
+ }
35
+ u-button:hover {
36
+ background-color: var(--u-blue-0);
37
+ border-color: var(--u-blue-400);
38
+ transform: translateY(-1px);
39
+ }
40
+ u-button:active {
41
+ transform: translateY(0);
42
+ }
43
+ `;
44
+
45
+ export { styles };
@@ -1,13 +1,15 @@
1
- import { BaseElement } from '@iyulab/components/dist/components/BaseElement.js';
1
+ import { UElement } from '@iyulab/components/dist/components/UElement.js';
2
2
  /**
3
3
  * 코드 블록을 렌더링하는 컴포넌트입니다.
4
4
  * 언어와 코드를 받아 syntax highlighting을 적용합니다.
5
5
  */
6
- export declare class UCodeBlock extends BaseElement {
6
+ export declare class UCodeBlock extends UElement {
7
7
  static styles: import('lit').CSSResultGroup[];
8
- static dependencies: Record<string, typeof BaseElement>;
8
+ static dependencies: Record<string, typeof UElement>;
9
9
  /** 클립보드 복사 상태를 나타내는 플래그입니다. */
10
10
  isCopied: boolean;
11
+ /** 코드 블록이 로딩 중인지 여부를 나타냅니다. */
12
+ loading: boolean;
11
13
  /** 코드 블록의 헤더를 숨길지 여부를 지정합니다. */
12
14
  headless: boolean;
13
15
  /** 코드 언어를 지정합니다. */
@@ -2,7 +2,9 @@ import { html } from 'lit';
2
2
  import { state, property } from 'lit/decorators.js';
3
3
  import { unsafeHTML } from 'lit/directives/unsafe-html.js';
4
4
  import hljs from 'highlight.js';
5
- import { BaseElement } from '@iyulab/components/dist/components/BaseElement.js';
5
+ import { UElement } from '@iyulab/components/dist/components/UElement.js';
6
+ import { UIcon } from '@iyulab/components/dist/components/icon/UIcon.component.js';
7
+ import { USpinner } from '@iyulab/components/dist/components/spinner/USpinner.component.js';
6
8
  import { UCopyButton } from '../buttons/UCopyButton.component.js';
7
9
  import { styles } from './UCodeBlock.styles.js';
8
10
 
@@ -15,10 +17,11 @@ var __decorateClass = (decorators, target, key, kind) => {
15
17
  if (result) __defProp(target, key, result);
16
18
  return result;
17
19
  };
18
- class UCodeBlock extends BaseElement {
20
+ class UCodeBlock extends UElement {
19
21
  constructor() {
20
22
  super(...arguments);
21
23
  this.isCopied = false;
24
+ this.loading = false;
22
25
  this.headless = false;
23
26
  this.lang = "plaintext";
24
27
  /**
@@ -36,6 +39,8 @@ class UCodeBlock extends BaseElement {
36
39
  }
37
40
  static {
38
41
  this.dependencies = {
42
+ "u-icon": UIcon,
43
+ "u-spinner": USpinner,
39
44
  "u-copy-button": UCopyButton
40
45
  };
41
46
  }
@@ -44,9 +49,13 @@ class UCodeBlock extends BaseElement {
44
49
  const value = this.value || "";
45
50
  return html`
46
51
  <div class="header" ?hidden=${this.headless}>
52
+ <span class="status">
53
+ ${this.loading ? html`<u-spinner></u-spinner>` : html`<u-icon lib="internal" name="code-slash"></u-icon>`}
54
+ </span>
47
55
  <span class="lang">
48
56
  ${lang}
49
57
  </span>
58
+ <div style="flex: 1"></div>
50
59
  <u-copy-button
51
60
  .value=${value}
52
61
  ></u-copy-button>
@@ -63,6 +72,9 @@ class UCodeBlock extends BaseElement {
63
72
  __decorateClass([
64
73
  state()
65
74
  ], UCodeBlock.prototype, "isCopied");
75
+ __decorateClass([
76
+ property({ type: Boolean, reflect: true })
77
+ ], UCodeBlock.prototype, "loading");
66
78
  __decorateClass([
67
79
  property({ type: Boolean, reflect: true })
68
80
  ], UCodeBlock.prototype, "headless");
@@ -51,9 +51,15 @@ const styles = css`
51
51
  flex-direction: row;
52
52
  align-items: center;
53
53
  justify-content: space-between;
54
+ gap: 12px;
54
55
  margin-bottom: 12px;
55
56
  user-select: none;
56
57
  }
58
+ .header .status {
59
+ display: inline-flex;
60
+ font-size: 12px;
61
+ color: var(--u-txt-color-strong);
62
+ }
57
63
  .header .lang {
58
64
  font-family: Arial, Helvetica, sans-serif;
59
65
  font-size: 12px;
@@ -0,0 +1,22 @@
1
+ import { UElement } from '@iyulab/components/dist/components/UElement.js';
2
+ import { FileItem } from '../../types/BlockItem.js';
3
+ /**
4
+ * 여러 파일을 그리드 형태로 표시하는 블록 컴포넌트입니다.
5
+ */
6
+ export declare class UFilesBlock extends UElement {
7
+ static styles: import('lit').CSSResultGroup[];
8
+ static dependencies: Record<string, typeof UElement>;
9
+ /** 파일 목록 */
10
+ files: FileItem[];
11
+ /** 삭제 버튼 표시 여부 */
12
+ removable: boolean;
13
+ render(): unknown;
14
+ private handleDownloadClick;
15
+ private handleRemoveClick;
16
+ /** MIME 타입 → Bootstrap Icon 이름 */
17
+ private resolveIcon;
18
+ /** 파일명 또는 MIME 타입에서 확장자를 추출합니다. */
19
+ private resolveExt;
20
+ /** bytes → 읽기 좋은 크기 문자열 */
21
+ private formatSize;
22
+ }
@@ -0,0 +1,192 @@
1
+ import { nothing, html } from 'lit';
2
+ import { property } from 'lit/decorators.js';
3
+ import { repeat } from 'lit/directives/repeat.js';
4
+ import { UElement } from '@iyulab/components/dist/components/UElement.js';
5
+ import { UIcon } from '@iyulab/components/dist/components/icon/UIcon.component.js';
6
+ import { UButton } from '@iyulab/components/dist/components/button/UButton.component.js';
7
+ import { UProgressBar } from '@iyulab/components/dist/components/progress-bar/UProgressBar.component.js';
8
+ import { styles } from './UFilesBlock.styles.js';
9
+
10
+ var __defProp = Object.defineProperty;
11
+ var __decorateClass = (decorators, target, key, kind) => {
12
+ var result = void 0 ;
13
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
14
+ if (decorator = decorators[i])
15
+ result = (decorator(target, key, result) ) || result;
16
+ if (result) __defProp(target, key, result);
17
+ return result;
18
+ };
19
+ class UFilesBlock extends UElement {
20
+ constructor() {
21
+ super(...arguments);
22
+ this.files = [];
23
+ this.removable = false;
24
+ this.handleDownloadClick = (e) => {
25
+ e.stopPropagation();
26
+ const index = e.currentTarget.dataset.index;
27
+ if (index == null) return;
28
+ const file = this.files.at(Number(index));
29
+ if (!file?.downloadUrl) return;
30
+ const a = document.createElement("a");
31
+ a.href = file.downloadUrl;
32
+ a.download = file.name;
33
+ a.target = "_blank";
34
+ a.rel = "noopener noreferrer";
35
+ document.body.appendChild(a);
36
+ a.click();
37
+ a.remove();
38
+ };
39
+ this.handleRemoveClick = (e) => {
40
+ e.stopPropagation();
41
+ const index = e.currentTarget.dataset.index;
42
+ if (index == null) return;
43
+ const file = this.files.at(Number(index));
44
+ if (!file) return;
45
+ this.emit("u-remove", { index, file });
46
+ };
47
+ }
48
+ static {
49
+ this.styles = [super.styles, styles];
50
+ }
51
+ static {
52
+ this.dependencies = {
53
+ "u-icon": UIcon,
54
+ "u-button": UButton,
55
+ "u-progress-bar": UProgressBar
56
+ };
57
+ }
58
+ render() {
59
+ if (!this.files?.length) return nothing;
60
+ return repeat(this.files, (_, i) => i, (f, i) => {
61
+ const phase = f.upload?.phase ?? "done";
62
+ const isUploading = phase === "uploading";
63
+ const isError = phase === "error";
64
+ return html`
65
+ <div class="item" phase=${phase}>
66
+ <div class="thumbnail">
67
+ <u-icon lib="bootstrap" name=${this.resolveIcon(f.type)}></u-icon>
68
+ <div class="thumbnail-overlay" ?hidden=${!isUploading}>
69
+ <u-icon lib="bootstrap" name="cloud-arrow-up"></u-icon>
70
+ </div>
71
+ <div class="thumbnail-overlay" ?hidden=${!isError}>
72
+ <u-icon lib="bootstrap" name="exclamation-circle-fill"></u-icon>
73
+ </div>
74
+ <u-button class="download-btn"
75
+ ?hidden=${!f.downloadUrl || isUploading}
76
+ data-index=${i}
77
+ title="Download"
78
+ variant="borderless"
79
+ @click=${this.handleDownloadClick}>
80
+ <u-icon lib="bootstrap" name="download"></u-icon>
81
+ </u-button>
82
+ </div>
83
+ <div class="info">
84
+ <div class="name" title=${f.name}>${f.name}</div>
85
+ <div class="meta">
86
+ <span class="type" ?hidden=${!f.type || isError}>
87
+ ${this.resolveExt(f.name, f.type)}
88
+ </span>
89
+ <span class="size" ?hidden=${f.size == null || isError}>
90
+ ${this.formatSize(f.size || 0)}
91
+ </span>
92
+ <span class="error-msg" ?hidden=${!isError}>
93
+ <u-icon lib="bootstrap" name="exclamation-triangle"></u-icon>
94
+ ${f.upload?.message ?? "File upload failed"}
95
+ </span>
96
+ </div>
97
+ </div>
98
+ <u-progress-bar class="upload-progress"
99
+ ?hidden=${!isUploading}
100
+ ?indeterminate=${f.upload?.progress == null}
101
+ value=${f.upload?.progress ?? 0}
102
+ ></u-progress-bar>
103
+ <u-button class="remove-btn"
104
+ ?hidden=${!this.removable}
105
+ data-index=${i}
106
+ title="Remove"
107
+ variant="borderless"
108
+ @click=${this.handleRemoveClick}>
109
+ <u-icon lib="bootstrap" name="x-lg"></u-icon>
110
+ </u-button>
111
+ </div>
112
+ `;
113
+ });
114
+ }
115
+ /** MIME 타입 → Bootstrap Icon 이름 */
116
+ resolveIcon(mimeType) {
117
+ if (!mimeType) return "file-earmark";
118
+ if (mimeType.startsWith("image/")) return "file-earmark-image";
119
+ if (mimeType.startsWith("video/")) return "file-earmark-play";
120
+ if (mimeType.startsWith("audio/")) return "file-earmark-music";
121
+ if (mimeType === "application/pdf") return "file-earmark-pdf";
122
+ const codeTypes = [
123
+ "application/json",
124
+ "application/javascript",
125
+ "application/typescript",
126
+ "application/xml",
127
+ "text/html",
128
+ "text/css",
129
+ "text/javascript",
130
+ "text/x-python",
131
+ "text/x-java-source"
132
+ ];
133
+ if (codeTypes.includes(mimeType) || mimeType.startsWith("text/x-"))
134
+ return "file-earmark-code";
135
+ if ([
136
+ "application/vnd.ms-excel",
137
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
138
+ "text/csv"
139
+ ].includes(mimeType))
140
+ return "file-earmark-spreadsheet";
141
+ if ([
142
+ "application/zip",
143
+ "application/x-zip-compressed",
144
+ "application/x-tar",
145
+ "application/x-rar-compressed",
146
+ "application/gzip",
147
+ "application/x-7z-compressed"
148
+ ].includes(mimeType))
149
+ return "file-earmark-zip";
150
+ if (mimeType.startsWith("text/")) return "file-earmark-text";
151
+ return "file-earmark";
152
+ }
153
+ /** 파일명 또는 MIME 타입에서 확장자를 추출합니다. */
154
+ resolveExt(name, mimeType) {
155
+ if (name) {
156
+ const dot = name.lastIndexOf(".");
157
+ if (dot !== -1 && dot < name.length - 1)
158
+ return name.slice(dot + 1).toLowerCase();
159
+ }
160
+ const map = {
161
+ "image/jpeg": "jpg",
162
+ "image/png": "png",
163
+ "image/gif": "gif",
164
+ "image/webp": "webp",
165
+ "image/svg+xml": "svg",
166
+ "application/pdf": "pdf",
167
+ "text/plain": "txt",
168
+ "text/csv": "csv",
169
+ "application/json": "json",
170
+ "application/zip": "zip"
171
+ };
172
+ return mimeType ? map[mimeType] ?? mimeType.split("/").pop() ?? "" : "";
173
+ }
174
+ /** bytes → 읽기 좋은 크기 문자열 */
175
+ formatSize(bytes) {
176
+ if (bytes < 1024)
177
+ return `${bytes} B`;
178
+ if (bytes < 1024 ** 2)
179
+ return `${(bytes / 1024).toFixed(1)} KB`;
180
+ if (bytes < 1024 ** 3)
181
+ return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
182
+ return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
183
+ }
184
+ }
185
+ __decorateClass([
186
+ property({ type: Array })
187
+ ], UFilesBlock.prototype, "files");
188
+ __decorateClass([
189
+ property({ type: Boolean, reflect: true })
190
+ ], UFilesBlock.prototype, "removable");
191
+
192
+ export { UFilesBlock };
@@ -0,0 +1,7 @@
1
+ import { UFilesBlock } from './UFilesBlock.component.js';
2
+ declare global {
3
+ interface HTMLElementTagNameMap {
4
+ "u-files-block": UFilesBlock;
5
+ }
6
+ }
7
+ export { UFilesBlock };
@@ -0,0 +1,5 @@
1
+ import { UFilesBlock } from './UFilesBlock.component.js';
2
+
3
+ UFilesBlock.define("u-files-block");
4
+
5
+ export { UFilesBlock };
@@ -0,0 +1 @@
1
+ export declare const styles: import('lit').CSSResult;