@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.
- package/README.md +54 -0
- package/package.json +63 -0
- package/src/DemoApp.vue +400 -0
- package/src/components/HkActionBar.tsx +15 -0
- package/src/components/HkAlert.scss +121 -0
- package/src/components/HkAlert.tsx +85 -0
- package/src/components/HkAvatar.tsx +29 -0
- package/src/components/HkBadge.scss +117 -0
- package/src/components/HkBadge.tsx +50 -0
- package/src/components/HkBreadcrumb.tsx +39 -0
- package/src/components/HkButton.scss +128 -0
- package/src/components/HkButton.tsx +74 -0
- package/src/components/HkCard.scss +49 -0
- package/src/components/HkCard.tsx +38 -0
- package/src/components/HkCheckbox.scss +102 -0
- package/src/components/HkCheckbox.tsx +85 -0
- package/src/components/HkCollapse.tsx +48 -0
- package/src/components/HkColorPicker.scss +148 -0
- package/src/components/HkColorPicker.tsx +204 -0
- package/src/components/HkConfirmDialog.scss +12 -0
- package/src/components/HkConfirmDialog.tsx +75 -0
- package/src/components/HkDateTimePicker.scss +361 -0
- package/src/components/HkDateTimePicker.tsx +424 -0
- package/src/components/HkDivider.scss +85 -0
- package/src/components/HkDivider.tsx +51 -0
- package/src/components/HkDraggableGrid.scss +132 -0
- package/src/components/HkDraggableGrid.tsx +291 -0
- package/src/components/HkDraggableList.scss +110 -0
- package/src/components/HkDraggableList.tsx +238 -0
- package/src/components/HkDrawer.scss +175 -0
- package/src/components/HkDrawer.tsx +171 -0
- package/src/components/HkEmptyState.scss +42 -0
- package/src/components/HkEmptyState.tsx +41 -0
- package/src/components/HkErrorBoundary.scss +64 -0
- package/src/components/HkErrorBoundary.tsx +82 -0
- package/src/components/HkGaugeRing.scss +4 -0
- package/src/components/HkGaugeRing.tsx +130 -0
- package/src/components/HkHoverRevealAction.scss +66 -0
- package/src/components/HkHoverRevealAction.tsx +77 -0
- package/src/components/HkIcon.tsx +43 -0
- package/src/components/HkIconButton.tsx +42 -0
- package/src/components/HkImageViewer.scss +33 -0
- package/src/components/HkImageViewer.tsx +250 -0
- package/src/components/HkInput.scss +172 -0
- package/src/components/HkInput.tsx +175 -0
- package/src/components/HkKbd.tsx +26 -0
- package/src/components/HkKeywordSearchModal.scss +148 -0
- package/src/components/HkKeywordSearchModal.tsx +308 -0
- package/src/components/HkListTransition.scss +131 -0
- package/src/components/HkListTransition.tsx +29 -0
- package/src/components/HkLocalePickerPopup.tsx +42 -0
- package/src/components/HkLogo.scss +36 -0
- package/src/components/HkLogo.tsx +93 -0
- package/src/components/HkMarkdownRenderer.scss +141 -0
- package/src/components/HkMarkdownRenderer.tsx +123 -0
- package/src/components/HkMediaControlBar.scss +65 -0
- package/src/components/HkMediaControlBar.tsx +115 -0
- package/src/components/HkMediaPlayer.scss +29 -0
- package/src/components/HkMediaPlayer.tsx +210 -0
- package/src/components/HkMediaSlider.scss +54 -0
- package/src/components/HkMediaSlider.tsx +81 -0
- package/src/components/HkMediaVisualizer.scss +7 -0
- package/src/components/HkMediaVisualizer.tsx +119 -0
- package/src/components/HkMinimap.scss +76 -0
- package/src/components/HkMinimap.tsx +255 -0
- package/src/components/HkModal.scss +318 -0
- package/src/components/HkModal.tsx +481 -0
- package/src/components/HkModalBreadcrumb.scss +55 -0
- package/src/components/HkModalBreadcrumb.tsx +113 -0
- package/src/components/HkMorphingTabs.scss +141 -0
- package/src/components/HkMorphingTabs.tsx +113 -0
- package/src/components/HkNavItem.scss +81 -0
- package/src/components/HkNavItem.tsx +157 -0
- package/src/components/HkNumberInput.scss +154 -0
- package/src/components/HkNumberInput.tsx +161 -0
- package/src/components/HkPasswordInput.scss +289 -0
- package/src/components/HkPasswordInput.tsx +533 -0
- package/src/components/HkPhaseTransition.scss +25 -0
- package/src/components/HkPhaseTransition.tsx +40 -0
- package/src/components/HkPopover.scss +82 -0
- package/src/components/HkPopover.tsx +410 -0
- package/src/components/HkPopupSelect.scss +136 -0
- package/src/components/HkPopupSelect.tsx +199 -0
- package/src/components/HkProgressBar.scss +106 -0
- package/src/components/HkProgressBar.tsx +61 -0
- package/src/components/HkProgressDialog.tsx +67 -0
- package/src/components/HkProgressRing.scss +56 -0
- package/src/components/HkProgressRing.tsx +82 -0
- package/src/components/HkRadio.scss +119 -0
- package/src/components/HkRadio.tsx +75 -0
- package/src/components/HkRollingNumber.scss +37 -0
- package/src/components/HkRollingNumber.tsx +97 -0
- package/src/components/HkScrollContainer.scss +101 -0
- package/src/components/HkScrollContainer.tsx +362 -0
- package/src/components/HkSearchInput.scss +132 -0
- package/src/components/HkSearchInput.tsx +117 -0
- package/src/components/HkSelect.scss +143 -0
- package/src/components/HkSelect.tsx +230 -0
- package/src/components/HkSelectionGrid.scss +177 -0
- package/src/components/HkSelectionGrid.tsx +99 -0
- package/src/components/HkSelectionWaterfall.scss +93 -0
- package/src/components/HkSelectionWaterfall.tsx +90 -0
- package/src/components/HkSidebar.scss +81 -0
- package/src/components/HkSidebar.tsx +45 -0
- package/src/components/HkSkeleton.scss +44 -0
- package/src/components/HkSkeleton.tsx +30 -0
- package/src/components/HkSkeletonList.tsx +35 -0
- package/src/components/HkSpinner.scss +67 -0
- package/src/components/HkSpinner.tsx +41 -0
- package/src/components/HkSplash.scss +60 -0
- package/src/components/HkSplash.tsx +40 -0
- package/src/components/HkSwitch.scss +157 -0
- package/src/components/HkSwitch.tsx +72 -0
- package/src/components/HkTable.scss +198 -0
- package/src/components/HkTable.tsx +246 -0
- package/src/components/HkTabs.scss +109 -0
- package/src/components/HkTabs.tsx +111 -0
- package/src/components/HkTag.scss +88 -0
- package/src/components/HkTag.tsx +44 -0
- package/src/components/HkTextarea.scss +72 -0
- package/src/components/HkTextarea.tsx +104 -0
- package/src/components/HkTimeline.scss +139 -0
- package/src/components/HkTimeline.tsx +95 -0
- package/src/components/HkToast.scss +168 -0
- package/src/components/HkToast.tsx +175 -0
- package/src/components/HkTooltip.scss +47 -0
- package/src/components/HkTooltip.tsx +108 -0
- package/src/components/HkTree.scss +188 -0
- package/src/components/HkTree.tsx +217 -0
- package/src/components/HkTrendChart.tsx +180 -0
- package/src/components/HkWindowedItem.scss +17 -0
- package/src/components/HkWindowedItem.tsx +107 -0
- package/src/components/HkZoomToolbar.scss +35 -0
- package/src/components/HkZoomToolbar.tsx +69 -0
- package/src/composables/useBreakpoint.ts +41 -0
- package/src/composables/useClipboard.ts +37 -0
- package/src/composables/useConfirm.ts +35 -0
- package/src/composables/useProgressDialog.ts +52 -0
- package/src/composables/useReportedTransition.ts +69 -0
- package/src/composables/useScrollWindow.ts +21 -0
- package/src/composables/useToast.ts +2 -0
- package/src/demo.scss +24 -0
- package/src/demo.ts +22 -0
- package/src/env.d.ts +6 -0
- package/src/i18n/context.ts +89 -0
- package/src/i18n/locales/ar/components.json +43 -0
- package/src/i18n/locales/ar/components.toml +40 -0
- package/src/i18n/locales/de/components.json +43 -0
- package/src/i18n/locales/de/components.toml +40 -0
- package/src/i18n/locales/en/components.json +43 -0
- package/src/i18n/locales/en/components.toml +40 -0
- package/src/i18n/locales/es/components.json +43 -0
- package/src/i18n/locales/es/components.toml +40 -0
- package/src/i18n/locales/fr/components.json +43 -0
- package/src/i18n/locales/fr/components.toml +40 -0
- package/src/i18n/locales/ja/components.json +43 -0
- package/src/i18n/locales/ja/components.toml +40 -0
- package/src/i18n/locales/ko/components.json +43 -0
- package/src/i18n/locales/ko/components.toml +40 -0
- package/src/i18n/locales/pt/components.json +43 -0
- package/src/i18n/locales/pt/components.toml +40 -0
- package/src/i18n/locales/ru/components.json +43 -0
- package/src/i18n/locales/ru/components.toml +40 -0
- package/src/i18n/locales/zh-Hans/components.json +43 -0
- package/src/i18n/locales/zh-Hans/components.toml +40 -0
- package/src/i18n/locales/zh-Hant/components.json +43 -0
- package/src/i18n/locales/zh-Hant/components.toml +40 -0
- package/src/index.ts +137 -0
- package/src/render.ts +18 -0
- package/src/runtime/animationBus.ts +278 -0
- package/src/runtime/cronBus.ts +25 -0
- package/src/runtime/index.ts +12 -0
- package/src/runtime/useAsyncData.ts +30 -0
- package/src/runtime/useBreakpoint.ts +62 -0
- package/src/runtime/useClipboard.ts +37 -0
- package/src/runtime/useConfirm.ts +35 -0
- package/src/runtime/useOverlay.ts +82 -0
- package/src/runtime/usePopupManager.ts +89 -0
- package/src/runtime/useToast.ts +140 -0
- package/src/styles/index.scss +10 -0
- package/src/theme/index.ts +5 -0
- package/src/theme/presets.ts +307 -0
- package/src/theme/theme.scss +17 -0
- package/src/theme/useBackgroundLuminance.ts +145 -0
- package/src/theme/useSolarTime.ts +101 -0
- package/src/theme/useTheme.ts +141 -0
- 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
|
+
}
|
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
|
+
}
|