@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,180 @@
1
+ import { defineComponent, onMounted, onUnmounted, ref, watch, type PropType } from "vue";
2
+
3
+ import { LineChart } from "echarts/charts";
4
+ import { DataZoomComponent, GridComponent, LegendComponent, MarkLineComponent, TooltipComponent } from "echarts/components";
5
+ import * as echarts from "echarts/core";
6
+ import { CanvasRenderer } from "echarts/renderers";
7
+
8
+ echarts.use([
9
+ LineChart,
10
+ GridComponent,
11
+ TooltipComponent,
12
+ LegendComponent,
13
+ DataZoomComponent,
14
+ MarkLineComponent,
15
+ CanvasRenderer,
16
+ ]);
17
+
18
+ export interface TrendPoint {
19
+ time: number;
20
+ value: number;
21
+ }
22
+
23
+ export interface AlarmThresholds {
24
+ hh?: number;
25
+ h?: number;
26
+ l?: number;
27
+ ll?: number;
28
+ }
29
+
30
+ export interface TrendPen {
31
+ label: string;
32
+ data: TrendPoint[];
33
+ thresholds?: AlarmThresholds;
34
+ }
35
+
36
+ /**
37
+ * Streaming line-chart wrapper around echarts. Takes any number of
38
+ * "pens" (labelled time-series) and renders them against a rolling
39
+ * time window, with optional alarm threshold lines (hh/h/l/ll) per pen.
40
+ * Pure echarts plumbing — no domain logic, so any consumer can feed it
41
+ * metrics, telemetry or analytics series.
42
+ */
43
+ export default defineComponent({
44
+ name: "HkTrendChart",
45
+ props: {
46
+ pens: { type: Array as PropType<TrendPen[]>, default: () => [] },
47
+ height: { type: String, default: "240px" },
48
+ showThresholds: { type: Boolean, default: true },
49
+ timeWindowMs: { type: Number, default: 300_000 }, // 5 minutes
50
+ },
51
+ setup(props) {
52
+ const chartEl = ref<HTMLElement>();
53
+ let chart: echarts.ECharts | null = null;
54
+ let resizeCleanup: (() => void) | undefined;
55
+
56
+ function renderChart() {
57
+ if (!chart || props.pens.length === 0) return;
58
+
59
+ const now = Date.now();
60
+ const minTime = now - props.timeWindowMs;
61
+
62
+ const series = props.pens.map((pen) => ({
63
+ name: pen.label,
64
+ type: "line" as const,
65
+ data: pen.data
66
+ .filter((p) => p.time >= minTime)
67
+ .map((p) => [p.time, p.value]),
68
+ smooth: true,
69
+ symbol: "none",
70
+ lineStyle: { width: 2 },
71
+ }));
72
+
73
+ const markLines: Record<string, unknown>[] = [];
74
+ if (props.showThresholds) {
75
+ for (const pen of props.pens) {
76
+ if (pen.thresholds) {
77
+ const t = pen.thresholds;
78
+ if (t.hh != null)
79
+ markLines.push({
80
+ yAxis: t.hh,
81
+ lineStyle: { color: "#FF1744", type: "dashed", width: 1 },
82
+ label: { show: false },
83
+ });
84
+ if (t.h != null)
85
+ markLines.push({
86
+ yAxis: t.h,
87
+ lineStyle: { color: "#FF6D00", type: "dotted", width: 1 },
88
+ label: { show: false },
89
+ });
90
+ if (t.l != null)
91
+ markLines.push({
92
+ yAxis: t.l,
93
+ lineStyle: { color: "#FFD600", type: "dotted", width: 1 },
94
+ label: { show: false },
95
+ });
96
+ if (t.ll != null)
97
+ markLines.push({
98
+ yAxis: t.ll,
99
+ lineStyle: { color: "#FF1744", type: "dashed", width: 1 },
100
+ label: { show: false },
101
+ });
102
+ }
103
+ }
104
+ }
105
+
106
+ chart.setOption(
107
+ {
108
+ tooltip: {
109
+ trigger: "axis",
110
+ axisPointer: { type: "cross" },
111
+ },
112
+ legend: {
113
+ top: 0,
114
+ textStyle: { fontSize: 11 },
115
+ },
116
+ grid: { left: 50, right: 20, top: 30, bottom: 40 },
117
+ xAxis: {
118
+ type: "time",
119
+ min: minTime,
120
+ max: now,
121
+ },
122
+ yAxis: { type: "value", scale: true },
123
+ dataZoom: [{ type: "inside" }],
124
+ series:
125
+ markLines.length > 0
126
+ ? series.map((s) => ({
127
+ ...s,
128
+ markLine: { silent: true, data: markLines.map((m) => [{ ...m }, { ...m }]) },
129
+ }))
130
+ : series,
131
+ },
132
+ // Merge (not notMerge): this fires on every streaming tick (deep watch
133
+ // on pens). notMerge:true replaced the whole option each tick, which
134
+ // reset the user's dataZoom pan/zoom on every update and rebuilt every
135
+ // series. Merge updates data incrementally and — because we never
136
+ // specify dataZoom start/end here — preserves the user's current zoom.
137
+ );
138
+ }
139
+
140
+ function onResize() {
141
+ chart?.resize();
142
+ }
143
+
144
+ onMounted(() => {
145
+ if (chartEl.value) {
146
+ chart = echarts.init(chartEl.value);
147
+ renderChart();
148
+ // Keep the canvas in sync with its container. echarts doesn't
149
+ // observe its own element, so without this the chart keeps its
150
+ // initial pixel size when the column narrows (mobile rotation,
151
+ // sidebar collapse, container resize) and renders off-canvas or
152
+ // leaves dead space. ResizeObserver covers container-driven
153
+ // changes; the window listener catches the cases where the
154
+ // observer isn't supported / doesn't fire (older WebViews).
155
+ const el = chartEl.value;
156
+ window.addEventListener("resize", onResize);
157
+ let ro: ResizeObserver | undefined;
158
+ if (typeof ResizeObserver !== "undefined") {
159
+ ro = new ResizeObserver(onResize);
160
+ ro.observe(el);
161
+ }
162
+ resizeCleanup = () => {
163
+ window.removeEventListener("resize", onResize);
164
+ ro?.disconnect();
165
+ };
166
+ }
167
+ });
168
+
169
+ onUnmounted(() => {
170
+ resizeCleanup?.();
171
+ resizeCleanup = undefined;
172
+ chart?.dispose();
173
+ chart = null;
174
+ });
175
+
176
+ watch(() => props.pens, renderChart, { deep: true });
177
+
178
+ return () => <div ref={chartEl} style={{ width: "100%", height: props.height }} />;
179
+ },
180
+ });
@@ -0,0 +1,17 @@
1
+ .hk-windowed-item {
2
+ display: block;
3
+ }
4
+
5
+ .hk-windowed-item[data-state="placeholder"] {
6
+ break-inside: avoid;
7
+ page-break-inside: avoid;
8
+ -webkit-column-break-inside: avoid;
9
+ pointer-events: none;
10
+ background: transparent;
11
+ }
12
+
13
+ .hk-windowed-item[data-state="real"] {
14
+ break-inside: avoid;
15
+ page-break-inside: avoid;
16
+ -webkit-column-break-inside: avoid;
17
+ }
@@ -0,0 +1,107 @@
1
+ import { defineComponent, onBeforeUnmount, onMounted, ref, shallowRef, watch, type PropType } from "vue";
2
+
3
+ import "./HkWindowedItem.scss";
4
+ import { useScrollWindow } from "../composables/useScrollWindow";
5
+ import { onceFrame } from "../runtime/animationBus";
6
+
7
+ export default defineComponent({
8
+ name: "HkWindowedItem",
9
+ props: {
10
+ itemKey: { type: String, default: undefined },
11
+ estimatedHeight: { type: Number, default: 96 },
12
+ overscanScreens: { type: Number as PropType<number | null>, default: null },
13
+ as: { type: String, default: "div" },
14
+ },
15
+ setup(props, { slots }) {
16
+ const ctx = useScrollWindow();
17
+ const root = shallowRef<HTMLElement | null>(null);
18
+ const inWindow = ref(false);
19
+ const measuredHeight = ref<number | null>(null);
20
+ let io: IntersectionObserver | null = null;
21
+
22
+ function placeholderHeight(): number {
23
+ return measuredHeight.value ?? props.estimatedHeight;
24
+ }
25
+
26
+ function withinBuffer(el: HTMLElement, scrollEl: HTMLElement, overscan: number): boolean {
27
+ const elRect = el.getBoundingClientRect();
28
+ const rootRect = scrollEl.getBoundingClientRect();
29
+ const buf = scrollEl.clientHeight * overscan;
30
+ return elRect.bottom >= rootRect.top - buf && elRect.top <= rootRect.bottom + buf;
31
+ }
32
+
33
+ function buildObserver(target: HTMLElement, scrollEl: HTMLElement | null) {
34
+ io?.disconnect();
35
+ const overscan = props.overscanScreens ?? ctx?.overscanScreens ?? 1;
36
+ const margin = `${Math.round(overscan * 100)}% 0px ${Math.round(overscan * 100)}% 0px`;
37
+ io = new IntersectionObserver(
38
+ (entries) => {
39
+ for (const e of entries) {
40
+ inWindow.value = e.isIntersecting;
41
+ }
42
+ },
43
+ {
44
+ root: scrollEl,
45
+ rootMargin: margin,
46
+ threshold: 0,
47
+ },
48
+ );
49
+ io.observe(target);
50
+ }
51
+
52
+ function measure() {
53
+ const el = root.value;
54
+ if (!el) return;
55
+ const h = el.offsetHeight;
56
+ if (h > 0) measuredHeight.value = h;
57
+ }
58
+
59
+ onMounted(() => {
60
+ const scrollEl = ctx?.scrollRoot.value ?? null;
61
+ const overscan = props.overscanScreens ?? ctx?.overscanScreens ?? 1;
62
+ if (scrollEl && root.value) {
63
+ inWindow.value = withinBuffer(root.value, scrollEl, overscan);
64
+ } else {
65
+ inWindow.value = true;
66
+ }
67
+ if (root.value) buildObserver(root.value, scrollEl);
68
+ onceFrame(measure);
69
+ });
70
+
71
+ watch(
72
+ () => ctx?.scrollRoot.value ?? null,
73
+ (scrollEl) => {
74
+ if (root.value) buildObserver(root.value, scrollEl);
75
+ },
76
+ );
77
+
78
+ watch(inWindow, (v) => {
79
+ if (v) {
80
+ onceFrame(measure);
81
+ }
82
+ });
83
+
84
+ onBeforeUnmount(() => {
85
+ io?.disconnect();
86
+ io = null;
87
+ });
88
+
89
+ return () => {
90
+ const Tag = props.as as "div";
91
+ const showReal = inWindow.value || !ctx;
92
+ const placeholderStyle = showReal ? undefined : { height: `${placeholderHeight()}px` };
93
+ return (
94
+ <Tag
95
+ ref={root}
96
+ class="hk-windowed-item"
97
+ data-state={showReal ? "real" : "placeholder"}
98
+ data-windowed-key={props.itemKey}
99
+ style={placeholderStyle}
100
+ aria-hidden={showReal ? undefined : "true"}
101
+ >
102
+ {showReal ? slots.default?.() : null}
103
+ </Tag>
104
+ );
105
+ };
106
+ },
107
+ });
@@ -0,0 +1,35 @@
1
+ .hk-zoom-toolbar {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: 2px;
5
+ position: absolute;
6
+ top: 8px;
7
+ right: 8px;
8
+ z-index: 10;
9
+ border-radius: var(--radius-md);
10
+ background: rgb(var(--color-surface) / 85%);
11
+ border: 1px solid rgb(var(--color-border) / 8%);
12
+ padding: 2px 4px;
13
+ backdrop-filter: blur(var(--blur-sm));
14
+ }
15
+
16
+ .hk-zoom-btn.hk-btn {
17
+ width: 24px;
18
+ height: 24px;
19
+ padding: 0;
20
+ }
21
+
22
+ .hk-zoom-label {
23
+ font-size: var(--text-2xs);
24
+ font-family: var(--font-mono, monospace);
25
+ color: rgb(var(--color-muted));
26
+ min-width: 32px;
27
+ text-align: center;
28
+ user-select: none;
29
+ }
30
+
31
+ .hk-zoom-reset-btn {
32
+ margin-left: 2px;
33
+ border-left: 1px solid rgb(var(--color-border) / 8%);
34
+ padding-left: 4px;
35
+ }
@@ -0,0 +1,69 @@
1
+ import { defineComponent } from "vue";
2
+ import { Maximize2, ZoomIn, ZoomOut } from "lucide-vue-next";
3
+
4
+ import HButton from "./HkButton";
5
+ import { useI18n } from "../i18n/context";
6
+ import "./HkZoomToolbar.scss";
7
+
8
+ /**
9
+ * Floating zoom control cluster for image/pan viewers. Reflects viewer state
10
+ * via props and forwards intents via events; renders nothing when the view
11
+ * is fully zoomed out (nothing to undo).
12
+ */
13
+ export default defineComponent({
14
+ name: "HkZoomToolbar",
15
+ props: {
16
+ zoom: { type: Number, required: true },
17
+ canZoomIn: { type: Boolean, required: true },
18
+ canZoomOut: { type: Boolean, required: true },
19
+ isZoomed: { type: Boolean, required: true },
20
+ },
21
+ emits: {
22
+ zoomIn: () => true,
23
+ zoomOut: () => true,
24
+ reset: () => true,
25
+ },
26
+ setup(props, { emit }) {
27
+ const { t } = useI18n();
28
+ return () => {
29
+ if (!props.isZoomed && !props.canZoomOut) return null;
30
+
31
+ return (
32
+ <div class="hk-zoom-toolbar">
33
+ <HButton
34
+ variant="ghost"
35
+ size="sm"
36
+ class="hk-zoom-btn"
37
+ disabled={!props.canZoomOut}
38
+ ariaLabel={t("hk.zoomToolbar.zoomOut", "Zoom out")}
39
+ onClick={() => emit("zoomOut")}
40
+ >
41
+ <ZoomOut size={14} />
42
+ </HButton>
43
+ <span class="hk-zoom-label">{Math.round(props.zoom * 100)}%</span>
44
+ <HButton
45
+ variant="ghost"
46
+ size="sm"
47
+ class="hk-zoom-btn"
48
+ disabled={!props.canZoomIn}
49
+ ariaLabel={t("hk.zoomToolbar.zoomIn", "Zoom in")}
50
+ onClick={() => emit("zoomIn")}
51
+ >
52
+ <ZoomIn size={14} />
53
+ </HButton>
54
+ {props.isZoomed && (
55
+ <HButton
56
+ variant="ghost"
57
+ size="sm"
58
+ class={["hk-zoom-btn", "hk-zoom-reset-btn"]}
59
+ ariaLabel={t("hk.zoomToolbar.reset", "Reset zoom")}
60
+ onClick={() => emit("reset")}
61
+ >
62
+ <Maximize2 size={14} />
63
+ </HButton>
64
+ )}
65
+ </div>
66
+ );
67
+ };
68
+ },
69
+ });
@@ -0,0 +1,41 @@
1
+ import { ref, onMounted, onUnmounted } from "vue";
2
+
3
+ type Breakpoint = "sm" | "md" | "lg" | "xl";
4
+
5
+ const breakpoints: Record<Breakpoint, number> = {
6
+ sm: 640,
7
+ md: 768,
8
+ lg: 1024,
9
+ xl: 1280,
10
+ };
11
+
12
+ export function useBreakpoint() {
13
+ const current = ref<Breakpoint>("md");
14
+
15
+ function update() {
16
+ const width = window.innerWidth;
17
+ if (width >= breakpoints.xl) current.value = "xl";
18
+ else if (width >= breakpoints.lg) current.value = "lg";
19
+ else if (width >= breakpoints.md) current.value = "md";
20
+ else current.value = "sm";
21
+ }
22
+
23
+ function isUp(bp: Breakpoint) {
24
+ return window.innerWidth >= breakpoints[bp];
25
+ }
26
+
27
+ function isDown(bp: Breakpoint) {
28
+ return window.innerWidth < breakpoints[bp];
29
+ }
30
+
31
+ onMounted(() => {
32
+ update();
33
+ window.addEventListener("resize", update);
34
+ });
35
+
36
+ onUnmounted(() => {
37
+ window.removeEventListener("resize", update);
38
+ });
39
+
40
+ return { current, isUp, isDown };
41
+ }
@@ -0,0 +1,37 @@
1
+ import { ref } from "vue";
2
+
3
+ export function useClipboard() {
4
+ const copied = ref(false);
5
+
6
+ async function copy(text: string): Promise<boolean> {
7
+ try {
8
+ await navigator.clipboard.writeText(text);
9
+ copied.value = true;
10
+ setTimeout(() => {
11
+ copied.value = false;
12
+ }, 2000);
13
+ return true;
14
+ } catch {
15
+ const textarea = document.createElement("textarea");
16
+ textarea.value = text;
17
+ textarea.style.position = "fixed";
18
+ textarea.style.opacity = "0";
19
+ document.body.appendChild(textarea);
20
+ textarea.select();
21
+ try {
22
+ document.execCommand("copy");
23
+ copied.value = true;
24
+ setTimeout(() => {
25
+ copied.value = false;
26
+ }, 2000);
27
+ return true;
28
+ } catch {
29
+ return false;
30
+ } finally {
31
+ document.body.removeChild(textarea);
32
+ }
33
+ }
34
+ }
35
+
36
+ return { copied, copy };
37
+ }
@@ -0,0 +1,35 @@
1
+ import { ref } from "vue";
2
+
3
+ export function useConfirm() {
4
+ const resolveRef = ref<((value: boolean) => void) | null>(null);
5
+ const open = ref(false);
6
+ const message = ref("");
7
+ const title = ref("Confirm");
8
+ const confirmText = ref("Confirm");
9
+ const cancelText = ref("Cancel");
10
+
11
+ function confirm(text: string, opts?: { title?: string; confirmText?: string; cancelText?: string }): Promise<boolean> {
12
+ return new Promise<boolean>((resolve) => {
13
+ resolveRef.value = resolve;
14
+ message.value = text;
15
+ title.value = opts?.title ?? "Confirm";
16
+ confirmText.value = opts?.confirmText ?? "Confirm";
17
+ cancelText.value = opts?.cancelText ?? "Cancel";
18
+ open.value = true;
19
+ });
20
+ }
21
+
22
+ function onConfirm() {
23
+ open.value = false;
24
+ resolveRef.value?.(true);
25
+ resolveRef.value = null;
26
+ }
27
+
28
+ function onCancel() {
29
+ open.value = false;
30
+ resolveRef.value?.(false);
31
+ resolveRef.value = null;
32
+ }
33
+
34
+ return { open, title, message, confirmText, cancelText, confirm, onConfirm, onCancel };
35
+ }
@@ -0,0 +1,52 @@
1
+ import { reactive } from "vue";
2
+
3
+ export interface ProgressDialogState {
4
+ open: boolean;
5
+ title: string;
6
+ logs: string[];
7
+ value: number | null;
8
+ max: number;
9
+ }
10
+
11
+ export interface ProgressDialogHandle {
12
+ log(line: string): void;
13
+ setProgress(done: number, total?: number): void;
14
+ close(): void;
15
+ }
16
+
17
+ const MAX_LOG_LINES = 200;
18
+
19
+ const state = reactive<ProgressDialogState>({
20
+ open: false,
21
+ title: "",
22
+ logs: [],
23
+ value: null,
24
+ max: 100,
25
+ });
26
+
27
+ export function showProgressDialog(opts: { title: string }): ProgressDialogHandle {
28
+ state.title = opts.title;
29
+ state.logs = [];
30
+ state.value = null;
31
+ state.max = 100;
32
+ state.open = true;
33
+ return {
34
+ log(line: string) {
35
+ state.logs.push(line);
36
+ if (state.logs.length > MAX_LOG_LINES) {
37
+ state.logs.splice(0, state.logs.length - MAX_LOG_LINES);
38
+ }
39
+ },
40
+ setProgress(done: number, total?: number) {
41
+ if (typeof total === "number") state.max = total;
42
+ state.value = done;
43
+ },
44
+ close() {
45
+ state.open = false;
46
+ },
47
+ };
48
+ }
49
+
50
+ export function useProgressDialog(): ProgressDialogState {
51
+ return state;
52
+ }
@@ -0,0 +1,69 @@
1
+ import { onUnmounted } from "vue";
2
+ import { scheduleCronAfter, type CronHandle } from "../runtime/cronBus";
3
+ import { reportTransition, type AnimationHandle } from "../runtime/animationBus";
4
+
5
+ export interface ReportedTransitionTrack {
6
+ run(): void;
7
+ cancel(): void;
8
+ }
9
+
10
+ export interface ReportedTransition extends ReportedTransitionTrack {
11
+ track(key: string): ReportedTransitionTrack;
12
+ }
13
+
14
+ export function useReportedTransition(durationMs: number): ReportedTransition {
15
+ interface TrackState {
16
+ handle: AnimationHandle | null;
17
+ timer: CronHandle | null;
18
+ }
19
+ const tracks = new Map<string, TrackState>();
20
+ const MAIN_TRACK_KEY = "";
21
+
22
+ function getTrack(key: string): TrackState {
23
+ let t = tracks.get(key);
24
+ if (!t) {
25
+ t = { handle: null, timer: null };
26
+ tracks.set(key, t);
27
+ }
28
+ return t;
29
+ }
30
+
31
+ function cancelTrack(key: string) {
32
+ const t = tracks.get(key);
33
+ if (!t) return;
34
+ t.timer?.disconnect();
35
+ t.timer = null;
36
+ t.handle?.disconnect();
37
+ t.handle = null;
38
+ }
39
+
40
+ function runTrack(key: string) {
41
+ const t = getTrack(key);
42
+ t.timer?.disconnect();
43
+ t.timer = null;
44
+ t.handle?.disconnect();
45
+ t.handle = reportTransition(durationMs);
46
+ t.timer = scheduleCronAfter(() => {
47
+ t.timer = null;
48
+ t.handle?.disconnect();
49
+ t.handle = null;
50
+ }, durationMs);
51
+ }
52
+
53
+ function cancelAll() {
54
+ for (const key of tracks.keys()) cancelTrack(key);
55
+ }
56
+
57
+ onUnmounted(cancelAll);
58
+
59
+ return {
60
+ run: () => runTrack(MAIN_TRACK_KEY),
61
+ cancel: () => cancelTrack(MAIN_TRACK_KEY),
62
+ track(key: string): ReportedTransitionTrack {
63
+ return {
64
+ run: () => runTrack(key),
65
+ cancel: () => cancelTrack(key),
66
+ };
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,21 @@
1
+ import { inject, provide, type InjectionKey, type Ref } from "vue";
2
+
3
+ export interface ScrollWindowContext {
4
+ scrollRoot: Ref<HTMLElement | null>;
5
+ overscanScreens: number;
6
+ }
7
+
8
+ const SCROLL_WINDOW_KEY: InjectionKey<ScrollWindowContext> = Symbol("scroll-window");
9
+
10
+ export function provideScrollWindow(
11
+ scrollRoot: Ref<HTMLElement | null>,
12
+ overscanScreens = 1,
13
+ ): ScrollWindowContext {
14
+ const ctx: ScrollWindowContext = { scrollRoot, overscanScreens };
15
+ provide(SCROLL_WINDOW_KEY, ctx);
16
+ return ctx;
17
+ }
18
+
19
+ export function useScrollWindow(): ScrollWindowContext | null {
20
+ return inject(SCROLL_WINDOW_KEY, null);
21
+ }
@@ -0,0 +1,2 @@
1
+ export { useToast } from "../runtime/useToast";
2
+ export type { ToastItem, ToastMessage, ToastType } from "../runtime/useToast";
package/src/demo.scss ADDED
@@ -0,0 +1,24 @@
1
+ body {
2
+ margin: 0; padding: 2rem;
3
+ font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
4
+ background: rgb(var(--color-background, 14 15 24));
5
+ color: rgb(var(--color-text, 192 202 245));
6
+ }
7
+ .demo {
8
+ max-width: 800px; margin: 0 auto;
9
+ }
10
+ h1 { font-size: 2rem; margin-bottom: 2rem; text-align: center; }
11
+ h2 { font-size: 1.25rem; margin: 2rem 0 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border-faint, rgba(255,255,255,0.1)); }
12
+ section { margin-bottom: 2rem; }
13
+ .row {
14
+ display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
15
+ margin-bottom: 12px;
16
+ }
17
+ .icons { gap: 16px; }
18
+ .icon-grid {
19
+ display: inline-flex; flex-direction: column; align-items: center; gap: 4px;
20
+ }
21
+ .icon-grid small { font-size: 0.625rem; opacity: 0.5; }
22
+ .input-result, .checked-feedback, .switch-state {
23
+ margin-top: 8px; font-size: 0.75rem; opacity: 0.7;
24
+ }