@celestia-island/hikari 0.4.4

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 (187) hide show
  1. package/README.md +54 -0
  2. package/package.json +63 -0
  3. package/src/DemoApp.vue +400 -0
  4. package/src/components/HkActionBar.tsx +15 -0
  5. package/src/components/HkAlert.scss +121 -0
  6. package/src/components/HkAlert.tsx +85 -0
  7. package/src/components/HkAvatar.tsx +29 -0
  8. package/src/components/HkBadge.scss +117 -0
  9. package/src/components/HkBadge.tsx +50 -0
  10. package/src/components/HkBreadcrumb.tsx +39 -0
  11. package/src/components/HkButton.scss +128 -0
  12. package/src/components/HkButton.tsx +74 -0
  13. package/src/components/HkCard.scss +49 -0
  14. package/src/components/HkCard.tsx +38 -0
  15. package/src/components/HkCheckbox.scss +102 -0
  16. package/src/components/HkCheckbox.tsx +85 -0
  17. package/src/components/HkCollapse.tsx +48 -0
  18. package/src/components/HkColorPicker.scss +148 -0
  19. package/src/components/HkColorPicker.tsx +204 -0
  20. package/src/components/HkConfirmDialog.scss +12 -0
  21. package/src/components/HkConfirmDialog.tsx +75 -0
  22. package/src/components/HkDateTimePicker.scss +361 -0
  23. package/src/components/HkDateTimePicker.tsx +424 -0
  24. package/src/components/HkDivider.scss +85 -0
  25. package/src/components/HkDivider.tsx +51 -0
  26. package/src/components/HkDraggableGrid.scss +132 -0
  27. package/src/components/HkDraggableGrid.tsx +291 -0
  28. package/src/components/HkDraggableList.scss +110 -0
  29. package/src/components/HkDraggableList.tsx +238 -0
  30. package/src/components/HkDrawer.scss +175 -0
  31. package/src/components/HkDrawer.tsx +171 -0
  32. package/src/components/HkEmptyState.scss +42 -0
  33. package/src/components/HkEmptyState.tsx +41 -0
  34. package/src/components/HkErrorBoundary.scss +64 -0
  35. package/src/components/HkErrorBoundary.tsx +82 -0
  36. package/src/components/HkGaugeRing.scss +4 -0
  37. package/src/components/HkGaugeRing.tsx +130 -0
  38. package/src/components/HkHoverRevealAction.scss +66 -0
  39. package/src/components/HkHoverRevealAction.tsx +77 -0
  40. package/src/components/HkIcon.tsx +43 -0
  41. package/src/components/HkIconButton.tsx +42 -0
  42. package/src/components/HkImageViewer.scss +33 -0
  43. package/src/components/HkImageViewer.tsx +250 -0
  44. package/src/components/HkInput.scss +172 -0
  45. package/src/components/HkInput.tsx +175 -0
  46. package/src/components/HkKbd.tsx +26 -0
  47. package/src/components/HkKeywordSearchModal.scss +148 -0
  48. package/src/components/HkKeywordSearchModal.tsx +308 -0
  49. package/src/components/HkListTransition.scss +131 -0
  50. package/src/components/HkListTransition.tsx +29 -0
  51. package/src/components/HkLocalePickerPopup.tsx +42 -0
  52. package/src/components/HkLogo.scss +36 -0
  53. package/src/components/HkLogo.tsx +93 -0
  54. package/src/components/HkMarkdownRenderer.scss +141 -0
  55. package/src/components/HkMarkdownRenderer.tsx +123 -0
  56. package/src/components/HkMediaControlBar.scss +65 -0
  57. package/src/components/HkMediaControlBar.tsx +115 -0
  58. package/src/components/HkMediaPlayer.scss +29 -0
  59. package/src/components/HkMediaPlayer.tsx +210 -0
  60. package/src/components/HkMediaSlider.scss +54 -0
  61. package/src/components/HkMediaSlider.tsx +81 -0
  62. package/src/components/HkMediaVisualizer.scss +7 -0
  63. package/src/components/HkMediaVisualizer.tsx +119 -0
  64. package/src/components/HkMinimap.scss +76 -0
  65. package/src/components/HkMinimap.tsx +255 -0
  66. package/src/components/HkModal.scss +318 -0
  67. package/src/components/HkModal.tsx +481 -0
  68. package/src/components/HkModalBreadcrumb.scss +55 -0
  69. package/src/components/HkModalBreadcrumb.tsx +113 -0
  70. package/src/components/HkMorphingTabs.scss +141 -0
  71. package/src/components/HkMorphingTabs.tsx +113 -0
  72. package/src/components/HkNavItem.scss +81 -0
  73. package/src/components/HkNavItem.tsx +157 -0
  74. package/src/components/HkNumberInput.scss +154 -0
  75. package/src/components/HkNumberInput.tsx +161 -0
  76. package/src/components/HkPasswordInput.scss +289 -0
  77. package/src/components/HkPasswordInput.tsx +533 -0
  78. package/src/components/HkPhaseTransition.scss +25 -0
  79. package/src/components/HkPhaseTransition.tsx +40 -0
  80. package/src/components/HkPopover.scss +82 -0
  81. package/src/components/HkPopover.tsx +410 -0
  82. package/src/components/HkPopupSelect.scss +136 -0
  83. package/src/components/HkPopupSelect.tsx +199 -0
  84. package/src/components/HkProgressBar.scss +106 -0
  85. package/src/components/HkProgressBar.tsx +61 -0
  86. package/src/components/HkProgressDialog.tsx +67 -0
  87. package/src/components/HkProgressRing.scss +56 -0
  88. package/src/components/HkProgressRing.tsx +82 -0
  89. package/src/components/HkRadio.scss +119 -0
  90. package/src/components/HkRadio.tsx +75 -0
  91. package/src/components/HkRollingNumber.scss +37 -0
  92. package/src/components/HkRollingNumber.tsx +97 -0
  93. package/src/components/HkScrollContainer.scss +101 -0
  94. package/src/components/HkScrollContainer.tsx +362 -0
  95. package/src/components/HkSearchInput.scss +132 -0
  96. package/src/components/HkSearchInput.tsx +117 -0
  97. package/src/components/HkSelect.scss +143 -0
  98. package/src/components/HkSelect.tsx +230 -0
  99. package/src/components/HkSelectionGrid.scss +177 -0
  100. package/src/components/HkSelectionGrid.tsx +99 -0
  101. package/src/components/HkSelectionWaterfall.scss +93 -0
  102. package/src/components/HkSelectionWaterfall.tsx +90 -0
  103. package/src/components/HkSidebar.scss +81 -0
  104. package/src/components/HkSidebar.tsx +45 -0
  105. package/src/components/HkSkeleton.scss +44 -0
  106. package/src/components/HkSkeleton.tsx +30 -0
  107. package/src/components/HkSkeletonList.tsx +35 -0
  108. package/src/components/HkSpinner.scss +67 -0
  109. package/src/components/HkSpinner.tsx +41 -0
  110. package/src/components/HkSplash.scss +60 -0
  111. package/src/components/HkSplash.tsx +40 -0
  112. package/src/components/HkSwitch.scss +157 -0
  113. package/src/components/HkSwitch.tsx +72 -0
  114. package/src/components/HkTable.scss +198 -0
  115. package/src/components/HkTable.tsx +246 -0
  116. package/src/components/HkTabs.scss +109 -0
  117. package/src/components/HkTabs.tsx +111 -0
  118. package/src/components/HkTag.scss +88 -0
  119. package/src/components/HkTag.tsx +44 -0
  120. package/src/components/HkTextarea.scss +72 -0
  121. package/src/components/HkTextarea.tsx +104 -0
  122. package/src/components/HkTimeline.scss +139 -0
  123. package/src/components/HkTimeline.tsx +95 -0
  124. package/src/components/HkToast.scss +168 -0
  125. package/src/components/HkToast.tsx +175 -0
  126. package/src/components/HkTooltip.scss +47 -0
  127. package/src/components/HkTooltip.tsx +108 -0
  128. package/src/components/HkTree.scss +188 -0
  129. package/src/components/HkTree.tsx +217 -0
  130. package/src/components/HkTrendChart.tsx +180 -0
  131. package/src/components/HkWindowedItem.scss +17 -0
  132. package/src/components/HkWindowedItem.tsx +107 -0
  133. package/src/components/HkZoomToolbar.scss +35 -0
  134. package/src/components/HkZoomToolbar.tsx +69 -0
  135. package/src/composables/useBreakpoint.ts +41 -0
  136. package/src/composables/useClipboard.ts +37 -0
  137. package/src/composables/useConfirm.ts +35 -0
  138. package/src/composables/useProgressDialog.ts +52 -0
  139. package/src/composables/useReportedTransition.ts +69 -0
  140. package/src/composables/useScrollWindow.ts +21 -0
  141. package/src/composables/useToast.ts +2 -0
  142. package/src/demo.scss +24 -0
  143. package/src/demo.ts +22 -0
  144. package/src/env.d.ts +6 -0
  145. package/src/i18n/context.ts +89 -0
  146. package/src/i18n/locales/ar/components.json +43 -0
  147. package/src/i18n/locales/ar/components.toml +40 -0
  148. package/src/i18n/locales/de/components.json +43 -0
  149. package/src/i18n/locales/de/components.toml +40 -0
  150. package/src/i18n/locales/en/components.json +43 -0
  151. package/src/i18n/locales/en/components.toml +40 -0
  152. package/src/i18n/locales/es/components.json +43 -0
  153. package/src/i18n/locales/es/components.toml +40 -0
  154. package/src/i18n/locales/fr/components.json +43 -0
  155. package/src/i18n/locales/fr/components.toml +40 -0
  156. package/src/i18n/locales/ja/components.json +43 -0
  157. package/src/i18n/locales/ja/components.toml +40 -0
  158. package/src/i18n/locales/ko/components.json +43 -0
  159. package/src/i18n/locales/ko/components.toml +40 -0
  160. package/src/i18n/locales/pt/components.json +43 -0
  161. package/src/i18n/locales/pt/components.toml +40 -0
  162. package/src/i18n/locales/ru/components.json +43 -0
  163. package/src/i18n/locales/ru/components.toml +40 -0
  164. package/src/i18n/locales/zh-Hans/components.json +43 -0
  165. package/src/i18n/locales/zh-Hans/components.toml +40 -0
  166. package/src/i18n/locales/zh-Hant/components.json +43 -0
  167. package/src/i18n/locales/zh-Hant/components.toml +40 -0
  168. package/src/index.ts +137 -0
  169. package/src/render.ts +18 -0
  170. package/src/runtime/animationBus.ts +278 -0
  171. package/src/runtime/cronBus.ts +25 -0
  172. package/src/runtime/index.ts +12 -0
  173. package/src/runtime/useAsyncData.ts +30 -0
  174. package/src/runtime/useBreakpoint.ts +62 -0
  175. package/src/runtime/useClipboard.ts +37 -0
  176. package/src/runtime/useConfirm.ts +35 -0
  177. package/src/runtime/useOverlay.ts +82 -0
  178. package/src/runtime/usePopupManager.ts +89 -0
  179. package/src/runtime/useToast.ts +140 -0
  180. package/src/styles/index.scss +10 -0
  181. package/src/theme/index.ts +5 -0
  182. package/src/theme/presets.ts +307 -0
  183. package/src/theme/theme.scss +17 -0
  184. package/src/theme/useBackgroundLuminance.ts +145 -0
  185. package/src/theme/useSolarTime.ts +101 -0
  186. package/src/theme/useTheme.ts +141 -0
  187. package/src/tokens.scss +26 -0
@@ -0,0 +1,104 @@
1
+ import {
2
+ computed,
3
+ defineComponent,
4
+ nextTick,
5
+ onBeforeUnmount,
6
+ onMounted,
7
+ ref,
8
+ watch,
9
+ } from "vue";
10
+ import "./HkTextarea.scss";
11
+
12
+ export default defineComponent({
13
+ name: "HkTextarea",
14
+ props: {
15
+ modelValue: { type: String, default: "" },
16
+ placeholder: { type: String, default: undefined },
17
+ disabled: { type: Boolean, default: false },
18
+ rows: { type: Number, default: 4 },
19
+ autoGrow: { type: Boolean, default: false },
20
+ maxLength: { type: Number, default: undefined },
21
+ showCount: { type: Boolean, default: false },
22
+ },
23
+ emits: {
24
+ "update:modelValue": (_value: string) => true,
25
+ },
26
+ setup(props, { emit }) {
27
+ const textareaRef = ref<HTMLTextAreaElement | null>(null);
28
+ let ro: ResizeObserver | null = null;
29
+ let lastWidth = -1;
30
+
31
+ function autoResize() {
32
+ const ta = textareaRef.value;
33
+ if (!ta || !props.autoGrow) return;
34
+ ta.style.height = "auto";
35
+ ta.style.height = `${ta.scrollHeight}px`;
36
+ }
37
+
38
+ watch(
39
+ () => props.modelValue,
40
+ () => {
41
+ if (props.autoGrow) nextTick(autoResize);
42
+ },
43
+ );
44
+
45
+ onMounted(() => {
46
+ if (!props.autoGrow) return;
47
+ const ta = textareaRef.value;
48
+ if (!ta) return;
49
+ ro = new ResizeObserver(() => {
50
+ const el = textareaRef.value;
51
+ if (!el) return;
52
+ const w = el.clientWidth;
53
+ if (w === lastWidth) return;
54
+ lastWidth = w;
55
+ autoResize();
56
+ });
57
+ ro.observe(ta);
58
+ nextTick(autoResize);
59
+ });
60
+
61
+ onBeforeUnmount(() => ro?.disconnect());
62
+
63
+ function onInput(e: Event) {
64
+ emit("update:modelValue", (e.target as HTMLTextAreaElement).value);
65
+ }
66
+
67
+ const charCount = computed(() => props.modelValue.length);
68
+ const isOverLimit = computed(() =>
69
+ props.maxLength != null && charCount.value > props.maxLength
70
+ );
71
+
72
+ const wrapperClass = computed(() => [
73
+ "hk-textarea",
74
+ props.disabled ? "hk-textarea-disabled" : "",
75
+ isOverLimit.value ? "hk-textarea-overlimit" : "",
76
+ ]);
77
+
78
+ return () => (
79
+ <div class={wrapperClass.value}>
80
+ <textarea
81
+ ref={textareaRef}
82
+ class={[
83
+ "hk-textarea-field",
84
+ props.autoGrow ? "hk-textarea-field-autogrow" : "",
85
+ ]}
86
+ value={props.modelValue}
87
+ placeholder={props.placeholder}
88
+ disabled={props.disabled}
89
+ rows={props.rows}
90
+ maxlength={props.maxLength}
91
+ onInput={onInput}
92
+ />
93
+ {props.showCount && props.maxLength != null ? (
94
+ <div class="hk-textarea-count">
95
+ <span class={isOverLimit.value ? "hk-textarea-count-over" : ""}>
96
+ {charCount.value}
97
+ </span>
98
+ <span> / {props.maxLength}</span>
99
+ </div>
100
+ ) : null}
101
+ </div>
102
+ );
103
+ },
104
+ });
@@ -0,0 +1,139 @@
1
+ .hk-timeline {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: 0;
5
+ width: 100%;
6
+ }
7
+
8
+ .hk-timeline[data-orientation="vertical"] {
9
+ flex-direction: column;
10
+ align-items: flex-start;
11
+ }
12
+
13
+ .hk-timeline-step {
14
+ display: flex;
15
+ align-items: center;
16
+ gap: var(--hi-space-8, 8px);
17
+ position: relative;
18
+ flex: 1 1 auto;
19
+ min-width: 0;
20
+
21
+ [data-el="indicator"] {
22
+ width: 24px;
23
+ height: 24px;
24
+ min-width: 24px;
25
+ min-height: 24px;
26
+ max-width: 24px;
27
+ max-height: 24px;
28
+ border-radius: 50%;
29
+ box-sizing: border-box;
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: center;
33
+ flex-shrink: 0;
34
+ flex-grow: 0;
35
+ visibility: visible;
36
+ overflow: visible;
37
+ }
38
+
39
+ [data-el="num"] {
40
+ font-size: var(--hi-text-xs, 0.75rem);
41
+ font-weight: 600;
42
+ line-height: 1;
43
+ display: block;
44
+ }
45
+
46
+ [data-el="check"] {
47
+ display: block;
48
+ }
49
+
50
+ [data-el="label"] {
51
+ font-size: var(--hi-text-xs, 0.75rem);
52
+ white-space: nowrap;
53
+ flex-shrink: 0;
54
+ }
55
+
56
+ [data-el="connector"] {
57
+ height: 2px;
58
+ flex: 1 1 0%;
59
+ min-width: 0.5rem;
60
+ margin: 0 var(--hi-space-4, 4px);
61
+ }
62
+ }
63
+
64
+ .hk-timeline-step[data-last] {
65
+ flex: 0 0 auto;
66
+ }
67
+
68
+ .hk-timeline[data-orientation="vertical"] .hk-timeline-step {
69
+ flex-direction: row;
70
+ flex: 0 0 auto;
71
+ padding-bottom: var(--hi-space-24, 24px);
72
+
73
+ [data-el="connector"] {
74
+ width: 2px;
75
+ height: 1rem;
76
+ flex: 0 0 auto;
77
+ position: absolute;
78
+ left: 0.6875rem;
79
+ top: 1.5rem;
80
+ margin: 0;
81
+ }
82
+ }
83
+
84
+ .hk-timeline-step[data-status="completed"] {
85
+ [data-el="indicator"] {
86
+ background-color: var(--hi-color-primary);
87
+ color: #fff;
88
+ }
89
+
90
+ [data-el="label"] {
91
+ color: var(--hi-color-text-primary);
92
+ }
93
+
94
+ [data-el="connector"] {
95
+ background: var(--hi-color-primary);
96
+ }
97
+ }
98
+
99
+ .hk-timeline-step[data-status="active"] {
100
+ [data-el="indicator"] {
101
+ background-color: color-mix(in srgb, var(--hi-color-primary) 15%, transparent);
102
+ color: var(--hi-color-primary);
103
+ border: 2px solid var(--hi-color-primary);
104
+ }
105
+
106
+ [data-el="label"] {
107
+ color: var(--hi-color-text-primary);
108
+ }
109
+
110
+ [data-el="connector"] {
111
+ background: color-mix(in srgb, var(--hi-color-border) 30%, transparent);
112
+ }
113
+ }
114
+
115
+ .hk-timeline-step[data-status="pending"] {
116
+ [data-el="indicator"] {
117
+ background-color: var(--hi-color-surface);
118
+ color: var(--hi-color-muted);
119
+ border: 2px solid color-mix(in srgb, var(--hi-color-border) 30%, transparent);
120
+ }
121
+
122
+ [data-el="label"] {
123
+ color: var(--hi-color-muted);
124
+ }
125
+
126
+ [data-el="connector"] {
127
+ background: color-mix(in srgb, var(--hi-color-border) 30%, transparent);
128
+ }
129
+ }
130
+
131
+ .hk-timeline-step[data-clickable] {
132
+ cursor: pointer;
133
+
134
+ &:hover [data-el="indicator"] {
135
+ transform: scale(1.12);
136
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--hi-color-primary) 15%, transparent);
137
+ transition: transform var(--hi-duration-normal, 0.3s) ease, box-shadow var(--hi-duration-normal, 0.3s) ease;
138
+ }
139
+ }
@@ -0,0 +1,95 @@
1
+ import { defineComponent, type PropType } from "vue";
2
+ import { Check } from "lucide-vue-next";
3
+
4
+ import "./HkTimeline.scss";
5
+
6
+ export type TimelineStepStatus = "completed" | "active" | "pending";
7
+
8
+ export interface TimelineStep {
9
+ key: string;
10
+ label: string;
11
+ icon?: string;
12
+ }
13
+
14
+ export default defineComponent({
15
+ name: "HkTimeline",
16
+ props: {
17
+ steps: { type: Array as PropType<TimelineStep[]>, required: true },
18
+ currentKey: { type: String, required: true },
19
+ orientation: {
20
+ type: String as PropType<"horizontal" | "vertical">,
21
+ default: "horizontal",
22
+ },
23
+ clickable: { type: Boolean, default: false },
24
+ },
25
+ emits: {
26
+ select: (_key: string) => true,
27
+ },
28
+ setup(props, { emit }) {
29
+ return () => {
30
+ const currentIndex = props.steps.findIndex(
31
+ (s) => s.key === props.currentKey,
32
+ );
33
+
34
+ return (
35
+ <div
36
+ class="hk-timeline"
37
+ data-orientation={props.orientation}
38
+ >
39
+ {props.steps.map((step, idx) => {
40
+ const status: TimelineStepStatus =
41
+ idx < currentIndex
42
+ ? "completed"
43
+ : idx === currentIndex
44
+ ? "active"
45
+ : "pending";
46
+
47
+ const isLast = idx === props.steps.length - 1;
48
+
49
+ return (
50
+ <div
51
+ key={step.key}
52
+ class="hk-timeline-step"
53
+ data-status={status}
54
+ data-last={isLast || undefined}
55
+ data-clickable={(props.clickable && status === "completed") || undefined}
56
+ role={props.clickable ? "button" : undefined}
57
+ tabindex={props.clickable && status === "completed" ? 0 : undefined}
58
+ onClick={() => {
59
+ if (props.clickable && status === "completed")
60
+ emit("select", step.key);
61
+ }}
62
+ onKeydown={(e: KeyboardEvent) => {
63
+ if (
64
+ props.clickable &&
65
+ status === "completed" &&
66
+ (e.key === "Enter" || e.key === " ")
67
+ ) {
68
+ e.preventDefault();
69
+ emit("select", step.key);
70
+ }
71
+ }}
72
+ >
73
+ <span
74
+ data-el="indicator"
75
+ aria-hidden="true"
76
+ >
77
+ {status === "completed" && (
78
+ <Check size={12} data-el="check" strokeWidth={3} />
79
+ )}
80
+ {status !== "completed" && (
81
+ <span data-el="num">{idx + 1}</span>
82
+ )}
83
+ </span>
84
+ <span data-el="label">{step.label}</span>
85
+ {!isLast && (
86
+ <div data-el="connector" />
87
+ )}
88
+ </div>
89
+ );
90
+ })}
91
+ </div>
92
+ );
93
+ };
94
+ },
95
+ });
@@ -0,0 +1,168 @@
1
+ .hk-toast-container {
2
+ position: fixed;
3
+ top: 4rem;
4
+ right: 1rem;
5
+ z-index: var(--hk-z-toast, 9999);
6
+ max-width: 24rem;
7
+ pointer-events: none;
8
+ }
9
+
10
+ .hk-toast-container > div {
11
+ position: relative;
12
+ display: flex;
13
+ flex-direction: column;
14
+ gap: var(--hi-space-12, 12px);
15
+ }
16
+
17
+ .hk-toast-item {
18
+ display: flex;
19
+ align-items: flex-start;
20
+ gap: var(--hi-space-8, 8px);
21
+ padding: var(--hi-space-12, 12px) var(--hi-space-16, 16px);
22
+ border-radius: var(--hi-radius-sm, 4px);
23
+ font-size: var(--hi-text-base, 0.875rem);
24
+ line-height: 1.4;
25
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
26
+ pointer-events: auto;
27
+ backdrop-filter: blur(12px);
28
+ user-select: none;
29
+ }
30
+
31
+ .hk-toast-body {
32
+ flex: 1;
33
+ min-width: 0;
34
+ display: flex;
35
+ flex-direction: column;
36
+ gap: var(--hi-space-4, 4px);
37
+ }
38
+
39
+ .hk-toast-icon {
40
+ display: flex;
41
+ align-items: center;
42
+ width: 1.5rem;
43
+ height: 1.5rem;
44
+ flex-shrink: 0;
45
+ }
46
+
47
+ .hk-toast-message {
48
+ flex: 1;
49
+ min-width: 0;
50
+ display: -webkit-box;
51
+ -webkit-line-clamp: 3;
52
+ -webkit-box-orient: vertical;
53
+ overflow: hidden;
54
+ white-space: normal;
55
+ }
56
+
57
+ .hk-toast-count {
58
+ align-self: flex-start;
59
+ font-size: 0.78em;
60
+ font-variant-numeric: tabular-nums;
61
+ letter-spacing: 0.02em;
62
+ opacity: 0.75;
63
+ padding: 2px 8px;
64
+ border-radius: 999px;
65
+ background: rgb(0 0 0 / 20%);
66
+ white-space: nowrap;
67
+ }
68
+
69
+ .hk-toast-error {
70
+ background: var(--hi-color-danger, #f85149);
71
+ color: #fff;
72
+ }
73
+
74
+ .hk-toast-success {
75
+ background: var(--hi-color-success, #3fb950);
76
+ color: #fff;
77
+ }
78
+
79
+ .hk-toast-warning {
80
+ background: var(--hi-color-warning, #d29922);
81
+ color: #fff;
82
+ }
83
+
84
+ .hk-toast-info {
85
+ background: var(--hi-color-info, #58a6ff);
86
+ color: #fff;
87
+ }
88
+
89
+ .hk-toast-loading {
90
+ background: var(--hi-color-info, #58a6ff);
91
+ color: #fff;
92
+ }
93
+
94
+ .hk-toast-loading-spinner {
95
+ border: 2px solid rgba(255, 255, 255, 0.3);
96
+ border-top-color: #fff;
97
+ border-radius: 50%;
98
+ animation: hk-toast-spin 0.7s linear infinite;
99
+ box-sizing: border-box;
100
+ }
101
+
102
+ @keyframes hk-toast-spin {
103
+ to { transform: rotate(360deg); }
104
+ }
105
+
106
+ .hk-toast-copy-btn,
107
+ .hk-toast-close {
108
+ display: flex;
109
+ align-items: center;
110
+ justify-content: center;
111
+ width: 1.5rem;
112
+ height: 1.5rem;
113
+ flex-shrink: 0;
114
+ border: none;
115
+ background: transparent;
116
+ color: inherit;
117
+ cursor: pointer;
118
+ border-radius: var(--hi-radius-sm, 4px);
119
+ opacity: 0.7;
120
+ transition: opacity var(--hi-duration-fast, 0.15s) ease;
121
+ padding: 0;
122
+ }
123
+
124
+ .hk-toast-copy-btn:hover,
125
+ .hk-toast-close:hover {
126
+ opacity: 1;
127
+ }
128
+
129
+ .hk-toast-enter-active {
130
+ transition-property: background-color, border-color, color, box-shadow, opacity, transform;
131
+ transition-duration: var(--hi-duration-normal, 0.3s);
132
+ transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
133
+ }
134
+
135
+ .hk-toast-leave-active {
136
+ transition-property: background-color, border-color, color, box-shadow, opacity, transform;
137
+ transition-duration: var(--hi-duration-normal, 0.3s);
138
+ transition-timing-function: cubic-bezier(0.36, 0, 0.66, 1);
139
+ }
140
+
141
+ .hk-toast-enter-from {
142
+ opacity: 0;
143
+ transform: translateX(1rem);
144
+ }
145
+
146
+ .hk-toast-leave-to {
147
+ opacity: 0;
148
+ transform: translateX(1rem);
149
+ }
150
+
151
+ .hk-toast-leave-active {
152
+ position: absolute;
153
+ width: 100%;
154
+ }
155
+
156
+ .hk-toast-move {
157
+ transition: transform var(--hi-duration-normal, 0.3s) ease;
158
+ }
159
+
160
+ .hk-toast-msg-enter-active,
161
+ .hk-toast-msg-leave-active {
162
+ transition: opacity var(--hi-duration-fast, 0.15s) ease;
163
+ }
164
+
165
+ .hk-toast-msg-enter-from,
166
+ .hk-toast-msg-leave-to {
167
+ opacity: 0;
168
+ }
@@ -0,0 +1,175 @@
1
+ import { computed, defineComponent, ref, Teleport, Transition, TransitionGroup, watch } from "vue";
2
+ import { AlertTriangle, CheckCircle, Copy, Info, X, XCircle } from "lucide-vue-next";
3
+ import { useToast, type ToastItem, type ToastMessage, type ToastType } from "../runtime/useToast";
4
+ import { useClipboard } from "../runtime/useClipboard";
5
+ import { useI18n } from "../i18n/context";
6
+ import "./HkToast.scss";
7
+
8
+ const LONG_THRESHOLD = 50;
9
+
10
+ function isLong(msg: string) {
11
+ return msg.length > LONG_THRESHOLD;
12
+ }
13
+
14
+ function LoadingSpinner({ size }: { size: number }) {
15
+ return (
16
+ <div
17
+ class="hk-toast-loading-spinner"
18
+ style={{ width: `${size}px`, height: `${size}px` }}
19
+ />
20
+ );
21
+ }
22
+
23
+ function renderIcon(type: ToastType, size = 18) {
24
+ if (type === "error") return <XCircle size={size} />;
25
+ if (type === "success") return <CheckCircle size={size} />;
26
+ if (type === "warning") return <AlertTriangle size={size} />;
27
+ if (type === "loading") return <LoadingSpinner size={size} />;
28
+ return <Info size={size} />;
29
+ }
30
+
31
+ function renderCountLabel(count: number) {
32
+ return `${count} msgs`;
33
+ }
34
+
35
+ const ACTION_ICON_SIZE = 18;
36
+
37
+ const HkToastItem = defineComponent({
38
+ name: "HkToastItem",
39
+ props: {
40
+ toast: { type: Object as () => ToastItem, required: true },
41
+ },
42
+ emits: {
43
+ remove: (_id: number) => true,
44
+ },
45
+ setup(props, { emit }) {
46
+ const { t } = useI18n();
47
+ const clipboard = useClipboard();
48
+ const displayedMsgId = ref<number | null>(null);
49
+ const animating = ref(false);
50
+
51
+ watch(
52
+ () => props.toast.id,
53
+ () => {
54
+ const msgs = props.toast.messages;
55
+ displayedMsgId.value = msgs.length > 0 ? msgs[msgs.length - 1].id : null;
56
+ animating.value = false;
57
+ },
58
+ { immediate: true },
59
+ );
60
+
61
+ function latestMsgId(): number | null {
62
+ const msgs = props.toast.messages;
63
+ return msgs.length > 0 ? msgs[msgs.length - 1].id : null;
64
+ }
65
+
66
+ function tryAdvance() {
67
+ if (animating.value) return;
68
+ const latest = latestMsgId();
69
+ if (latest === null || latest === displayedMsgId.value) return;
70
+ animating.value = true;
71
+ displayedMsgId.value = latest;
72
+ }
73
+
74
+ watch(() => props.toast.messages.length, () => tryAdvance());
75
+
76
+ function onMsgAfterEnter() {
77
+ animating.value = false;
78
+ tryAdvance();
79
+ }
80
+
81
+ const displayedMessage = computed<ToastMessage | null>(() => {
82
+ const id = displayedMsgId.value;
83
+ if (id === null) return null;
84
+ return props.toast.messages.find((m) => m.id === id) ?? null;
85
+ });
86
+
87
+ function handleCopy() {
88
+ const text = props.toast.messages.map((m) => m.text).join("\n\n");
89
+ clipboard.copy(text);
90
+ }
91
+
92
+ function handleClose() {
93
+ emit("remove", props.toast.id);
94
+ }
95
+
96
+ return () => {
97
+ const msg = displayedMessage.value;
98
+ const text = msg?.text ?? "";
99
+ const long = isLong(text);
100
+ const count = props.toast.messages.length;
101
+
102
+ return (
103
+ <div class={["hk-toast-item", `hk-toast-${props.toast.type}`]}>
104
+ <span class="hk-toast-icon">{renderIcon(props.toast.type)}</span>
105
+ <div class="hk-toast-body">
106
+ <Transition
107
+ name="hk-toast-msg"
108
+ mode="out-in"
109
+ onAfterEnter={onMsgAfterEnter}
110
+ >
111
+ <span
112
+ key={msg?.id ?? 0}
113
+ class="hk-toast-message"
114
+ title={text}
115
+ >
116
+ {text}
117
+ </span>
118
+ </Transition>
119
+ {count > 1 && (
120
+ <span class="hk-toast-count">{renderCountLabel(count)}</span>
121
+ )}
122
+ </div>
123
+ {props.toast.copyable && (
124
+ <button
125
+ class="hk-toast-copy-btn"
126
+ title={t("hk.toast.copy", "Copy")}
127
+ onClick={(e) => {
128
+ e.stopPropagation();
129
+ handleCopy();
130
+ }}
131
+ >
132
+ <Copy size={ACTION_ICON_SIZE} />
133
+ </button>
134
+ )}
135
+ <button
136
+ class="hk-toast-close"
137
+ aria-label={t("hk.toast.close", "Close")}
138
+ onClick={(e) => {
139
+ e.stopPropagation();
140
+ handleClose();
141
+ }}
142
+ >
143
+ <X size={ACTION_ICON_SIZE} />
144
+ </button>
145
+ </div>
146
+ );
147
+ };
148
+ },
149
+ });
150
+
151
+ export default defineComponent({
152
+ name: "HkToast",
153
+ setup() {
154
+ const { toasts, remove } = useToast();
155
+
156
+ return () => (
157
+ <Teleport to="body">
158
+ <div class="hk-toast-container">
159
+ <TransitionGroup
160
+ tag="div"
161
+ name="hk-toast"
162
+ >
163
+ {toasts.map((item) => (
164
+ <HkToastItem
165
+ key={item.id}
166
+ toast={item}
167
+ onRemove={(id: number) => remove(id)}
168
+ />
169
+ ))}
170
+ </TransitionGroup>
171
+ </div>
172
+ </Teleport>
173
+ );
174
+ },
175
+ });