@celestia-island/hikari 0.33.0 → 0.35.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.
- package/package.json +1 -1
- package/src/components/HkAffixPicker.scss +2 -11
- package/src/components/HkAffixPicker.test.tsx +97 -17
- package/src/components/HkAffixPicker.tsx +52 -32
- package/src/components/HkBoard.scss +83 -0
- package/src/components/HkBoard.tsx +396 -0
- package/src/components/HkLocalizedInput.test.tsx +105 -55
- package/src/components/HkMessageBox.scss +44 -0
- package/src/components/HkMessageBox.test.tsx +151 -0
- package/src/components/HkMessageBox.tsx +283 -0
- package/src/composables/useBoardCamera.ts +218 -0
- package/src/i18n/locales/en/components.json +9 -2
- package/src/i18n/locales/zh-Hans/components.json +9 -2
- package/src/i18n/locales/zh-Hant/components.json +9 -2
- package/src/index.ts +5 -0
- package/src/utils/boardCamera.test.ts +122 -0
- package/src/utils/boardCamera.ts +194 -0
- package/src/utils/boardEdges.test.ts +86 -0
- package/src/utils/boardEdges.ts +185 -0
package/package.json
CHANGED
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/* One selected entry: [flag] label (meta) •active-dot ×]
|
|
71
|
-
* The ×
|
|
72
|
-
*
|
|
71
|
+
* The × opens the shared confirm message box — the dialog's Confirm is
|
|
72
|
+
* what actually erases; the × alone never does. */
|
|
73
73
|
.hk-affix-tag {
|
|
74
74
|
display: flex;
|
|
75
75
|
align-items: center;
|
|
@@ -94,10 +94,6 @@
|
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
&[data-armed] {
|
|
98
|
-
border-color: rgb(var(--color-danger, 240 70 80) / 55%);
|
|
99
|
-
background: rgb(var(--color-danger, 240 70 80) / 12%);
|
|
100
|
-
}
|
|
101
97
|
}
|
|
102
98
|
|
|
103
99
|
.hk-affix-tag-body {
|
|
@@ -168,11 +164,6 @@
|
|
|
168
164
|
color: rgb(var(--color-danger, 240 70 80));
|
|
169
165
|
background: rgb(var(--color-danger, 240 70 80) / 12%);
|
|
170
166
|
}
|
|
171
|
-
|
|
172
|
-
[data-armed] & {
|
|
173
|
-
color: rgb(var(--color-danger, 240 70 80));
|
|
174
|
-
background: rgb(var(--color-danger, 240 70 80) / 18%);
|
|
175
|
-
}
|
|
176
167
|
}
|
|
177
168
|
|
|
178
169
|
/* ── option rows ────────────────────────────────────────────────────── */
|
|
@@ -18,6 +18,7 @@ interface MountOptions {
|
|
|
18
18
|
allowCustom?: boolean;
|
|
19
19
|
searchable?: boolean;
|
|
20
20
|
closeOnSelect?: boolean;
|
|
21
|
+
confirmRemove?: boolean;
|
|
21
22
|
disabled?: boolean;
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -40,6 +41,7 @@ function mountPicker(opts: MountOptions = {}) {
|
|
|
40
41
|
allowCustom: opts.allowCustom ?? false,
|
|
41
42
|
searchable: opts.searchable ?? true,
|
|
42
43
|
closeOnSelect: opts.closeOnSelect,
|
|
44
|
+
confirmRemove: opts.confirmRemove,
|
|
43
45
|
disabled: opts.disabled ?? false,
|
|
44
46
|
onSelect: (key: string) => events.select.push(key),
|
|
45
47
|
onRemove: (key: string) => events.remove.push(key),
|
|
@@ -100,7 +102,42 @@ async function untilSettled(check: () => number): Promise<void> {
|
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
|
|
105
|
+
/** Let the message box mount (own app) and its promise chain settle. */
|
|
106
|
+
async function flush() {
|
|
107
|
+
await nextTick();
|
|
108
|
+
await nextTick();
|
|
109
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function confirmButton(): HTMLButtonElement {
|
|
113
|
+
const btn = document.body.querySelector<HTMLButtonElement>(".hk-message-box-confirm");
|
|
114
|
+
expect(btn, "message box confirm button renders").toBeTruthy();
|
|
115
|
+
return btn!;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Dismiss every open message box and wait out its host's self-unmount
|
|
119
|
+
* (leave transition + timer) so no zombie app re-renders into the next
|
|
120
|
+
* test's DOM. No-op when a test never opened one. */
|
|
121
|
+
async function teardownMessageBoxes() {
|
|
122
|
+
if (!document.body.querySelector(".hk-message-box-confirm")) return;
|
|
123
|
+
for (const btn of [...document.body.querySelectorAll<HTMLButtonElement>(".hk-message-box-confirm")]) {
|
|
124
|
+
btn.click();
|
|
125
|
+
}
|
|
126
|
+
const deadline = Date.now() + 2500;
|
|
127
|
+
// The modal root (not just the confirm button) must be gone: the
|
|
128
|
+
// leave transition + cleanup timer fully unmount the host app, so no
|
|
129
|
+
// zombie registration lingers in the shared popup stack.
|
|
130
|
+
while (
|
|
131
|
+
(document.body.querySelector(".hk-message-box-confirm") ||
|
|
132
|
+
document.body.querySelector(".hk-modal-root")) &&
|
|
133
|
+
Date.now() < deadline
|
|
134
|
+
) {
|
|
135
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
afterEach(async () => {
|
|
140
|
+
await teardownMessageBoxes();
|
|
104
141
|
for (const { app, container } of mounts.splice(0)) {
|
|
105
142
|
app.unmount();
|
|
106
143
|
container.remove();
|
|
@@ -162,29 +199,72 @@ describe("HkAffixPicker", () => {
|
|
|
162
199
|
expect(rows().length).toBeGreaterThan(0);
|
|
163
200
|
});
|
|
164
201
|
|
|
165
|
-
it("
|
|
202
|
+
it("tag × opens a confirm dialog: Cancel keeps the tag, Confirm fires remove", async () => {
|
|
166
203
|
const { events, container } = mountPicker({ mode: "multi", selected: ["cn", "jp"] });
|
|
167
204
|
await openPopup(container);
|
|
168
|
-
|
|
169
|
-
x
|
|
170
|
-
|
|
171
|
-
|
|
205
|
+
// One tap never erases: the dialog names the entry instead.
|
|
206
|
+
tags()[0].querySelector<HTMLButtonElement>(".hk-affix-tag-x")!.click();
|
|
207
|
+
// The dialog mounts over several transition frames; wait for the
|
|
208
|
+
// settled state (box open, tag untouched) before asserting.
|
|
209
|
+
await untilBoxOpen('Remove "China"');
|
|
172
210
|
expect(events.remove).toHaveLength(0);
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
211
|
+
expect(tag("China")).toBeTruthy();
|
|
212
|
+
// Cancel keeps everything as it was.
|
|
213
|
+
[...document.body.querySelectorAll<HTMLButtonElement>(".hk-message-box-actions button")]
|
|
214
|
+
.find((b) => !b.classList.contains("hk-message-box-confirm"))!
|
|
215
|
+
.click();
|
|
216
|
+
await flush();
|
|
217
|
+
console.log("DBG-cancel openEvents", JSON.stringify(events.open), "tags", tags().length, "box", !!document.body.querySelector(".hk-message-box-text"));
|
|
218
|
+
expect(events.remove).toHaveLength(0);
|
|
219
|
+
expect(tag("China")).toBeTruthy();
|
|
220
|
+
// Second pass: the dialog's Confirm is what erases.
|
|
221
|
+
tags()[0].querySelector<HTMLButtonElement>(".hk-affix-tag-x")!.click();
|
|
222
|
+
await untilBoxOpen('Remove "China"');
|
|
223
|
+
confirmButton().click();
|
|
224
|
+
const deadline = Date.now() + 1500;
|
|
225
|
+
while (events.remove.length === 0 && Date.now() < deadline) {
|
|
226
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
227
|
+
await nextTick();
|
|
228
|
+
}
|
|
176
229
|
expect(events.remove).toEqual(["cn"]);
|
|
177
|
-
// Body click on an ARMED tag disarms it instead of picking.
|
|
178
|
-
const jpBody = tag("Japan")!.querySelector<HTMLButtonElement>(".hk-affix-tag-body")!;
|
|
179
|
-
tag("Japan")!.querySelector<HTMLButtonElement>(".hk-affix-tag-x")!.click();
|
|
180
|
-
await nextTick();
|
|
181
|
-
expect(tag("Japan")?.hasAttribute("data-armed")).toBe(true);
|
|
182
|
-
jpBody.click();
|
|
183
|
-
await nextTick();
|
|
184
|
-
expect(tag("Japan")?.hasAttribute("data-armed")).toBe(false);
|
|
185
230
|
expect(events.select).toHaveLength(0);
|
|
186
231
|
});
|
|
187
232
|
|
|
233
|
+
it("confirmRemove=false lets the × fire remove immediately, no dialog", async () => {
|
|
234
|
+
const { events, container } = mountPicker({
|
|
235
|
+
mode: "multi",
|
|
236
|
+
selected: ["cn"],
|
|
237
|
+
confirmRemove: false,
|
|
238
|
+
});
|
|
239
|
+
await openPopup(container);
|
|
240
|
+
tags()[0].querySelector<HTMLButtonElement>(".hk-affix-tag-x")!.click();
|
|
241
|
+
await flush();
|
|
242
|
+
expect(events.remove).toEqual(["cn"]);
|
|
243
|
+
expect(document.body.querySelector(".hk-message-box-text")).toBeNull();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
/** Poll until the confirm dialog is mounted AND shows the given body
|
|
247
|
+
* text — the box rides HkModal's multi-frame open transition. */
|
|
248
|
+
async function untilBoxOpen(fragment: string): Promise<void> {
|
|
249
|
+
const deadline = Date.now() + 1500;
|
|
250
|
+
while (Date.now() < deadline) {
|
|
251
|
+
const text = document.body.querySelector(".hk-message-box-text")?.textContent ?? "";
|
|
252
|
+
if (text.includes(fragment)) {
|
|
253
|
+
// One extra frame so sibling re-renders (scrollbar lock, panel
|
|
254
|
+
// reposition) triggered by the modal settle too.
|
|
255
|
+
await new Promise((resolve) => setTimeout(resolve, 30));
|
|
256
|
+
await nextTick();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
260
|
+
await nextTick();
|
|
261
|
+
}
|
|
262
|
+
expect(
|
|
263
|
+
document.body.querySelector(".hk-message-box-text")?.textContent ?? "",
|
|
264
|
+
`confirm dialog showing "${fragment}"`,
|
|
265
|
+
).toContain(fragment);
|
|
266
|
+
}
|
|
267
|
+
|
|
188
268
|
it("tag body click emits select; close-on-select default keeps multi open", async () => {
|
|
189
269
|
const { events, container } = mountPicker({ mode: "multi", selected: ["cn", "jp"] });
|
|
190
270
|
await openPopup(container);
|
|
@@ -7,6 +7,7 @@ import { useI18n } from "../i18n/context";
|
|
|
7
7
|
import HkInput from "./HkInput";
|
|
8
8
|
import HkListTransition from "./HkListTransition";
|
|
9
9
|
import HkMenu from "./HkMenu";
|
|
10
|
+
import HkMessageBox from "./HkMessageBox";
|
|
10
11
|
import "./HkAffixPicker.scss";
|
|
11
12
|
|
|
12
13
|
/** One pickable entry of the affix picker's searchable list. */
|
|
@@ -31,11 +32,12 @@ export interface HkAffixOption {
|
|
|
31
32
|
* opens one canonical popup:
|
|
32
33
|
*
|
|
33
34
|
* - a SEARCH FIELD on top (live filter over label / meta / keywords);
|
|
34
|
-
* - in `multi` mode, a TAG LIST of the currently selected keys
|
|
35
|
-
*
|
|
36
|
-
* (danger
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* - in `multi` mode, a TAG LIST of the currently selected keys.
|
|
36
|
+
* Deleting a tag is a TWO-STEP interaction: the × opens the shared
|
|
37
|
+
* HkMessageBox confirm dialog (danger tone, naming the entry), and
|
|
38
|
+
* only its Confirm fires `remove`. `confirmRemove={false}` opts
|
|
39
|
+
* out for hosts that confirm themselves. The popup stays open
|
|
40
|
+
* behind the dialog so several tags can be managed in one pass;
|
|
39
41
|
* - the option rows themselves: single mode shows every option and
|
|
40
42
|
* closes on pick; multi mode lists the NOT-yet-selected options and
|
|
41
43
|
* stays open for batch adds;
|
|
@@ -73,6 +75,9 @@ export const HkAffixPicker = defineComponent({
|
|
|
73
75
|
searchable: { type: Boolean, default: true },
|
|
74
76
|
/** Offer a "Use <query>" row for free-text entries. */
|
|
75
77
|
allowCustom: { type: Boolean, default: false },
|
|
78
|
+
/** Gate tag deletion behind a confirm message box (multi mode).
|
|
79
|
+
* Default true; pass false when the host runs its own guard. */
|
|
80
|
+
confirmRemove: { type: Boolean, default: true },
|
|
76
81
|
/** Override the default close-on-pick (single: true, multi: false).
|
|
77
82
|
* E.g. a multi picker that should close after each add passes
|
|
78
83
|
* true; a single picker that should stay open passes false. */
|
|
@@ -90,7 +95,8 @@ export const HkAffixPicker = defineComponent({
|
|
|
90
95
|
emits: {
|
|
91
96
|
/** A row or tag body was picked (multi add / single choose). */
|
|
92
97
|
select: (_key: string) => true,
|
|
93
|
-
/** A tag's
|
|
98
|
+
/** A tag's × (after the confirm dialog accepted) — the host drops
|
|
99
|
+
* that key. */
|
|
94
100
|
remove: (_key: string) => true,
|
|
95
101
|
/** The "Use <query>" row fired with the typed text. */
|
|
96
102
|
custom: (_query: string) => true,
|
|
@@ -106,14 +112,15 @@ export const HkAffixPicker = defineComponent({
|
|
|
106
112
|
const open = ref(false);
|
|
107
113
|
const chipRef = ref<HTMLElement | null>(null);
|
|
108
114
|
const query = ref("");
|
|
109
|
-
/**
|
|
110
|
-
|
|
115
|
+
/** True while our confirm dialog is up: clicks inside the dialog
|
|
116
|
+
* are outside THIS popup, and the panel's outside-close must not
|
|
117
|
+
* tear the tag list down mid-decision. */
|
|
118
|
+
const confirmHeld = ref(false);
|
|
111
119
|
|
|
112
|
-
// A fresh open starts calm: empty filter
|
|
120
|
+
// A fresh open starts calm: empty filter.
|
|
113
121
|
watch(open, (v) => {
|
|
114
122
|
if (!v) {
|
|
115
123
|
query.value = "";
|
|
116
|
-
armedKey.value = null;
|
|
117
124
|
}
|
|
118
125
|
emit("update:open", v);
|
|
119
126
|
});
|
|
@@ -179,13 +186,32 @@ export const HkAffixPicker = defineComponent({
|
|
|
179
186
|
if (resolvedCloseOnSelect.value) open.value = false;
|
|
180
187
|
}
|
|
181
188
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
+
/** Tag × pressed: unless the host opts out, the shared message box
|
|
190
|
+
* names the entry and asks for confirmation — the × alone never
|
|
191
|
+
* erases anything. Only an accepted dialog fires `remove`. The
|
|
192
|
+
* popup deliberately STAYS OPEN behind the dialog (both while it
|
|
193
|
+
* is up and after Confirm/Cancel) so several tags can be managed
|
|
194
|
+
* in one pass. */
|
|
195
|
+
async function requestRemove(tag: HkAffixOption) {
|
|
196
|
+
if (!props.confirmRemove) {
|
|
197
|
+
emit("remove", tag.key);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const removeLabel = t("hikari::affixPicker.remove", "Remove");
|
|
201
|
+
confirmHeld.value = true;
|
|
202
|
+
try {
|
|
203
|
+
const confirmed = await HkMessageBox.confirm({
|
|
204
|
+
title: t("hikari::affixPicker.removeConfirmTitle", "Remove entry"),
|
|
205
|
+
message: interpolate(t("hikari::affixPicker.removeConfirm", 'Remove "{label}"? This cannot be undone.'), {
|
|
206
|
+
label: tag.label,
|
|
207
|
+
}),
|
|
208
|
+
tone: "danger",
|
|
209
|
+
confirmText: removeLabel,
|
|
210
|
+
});
|
|
211
|
+
if (confirmed) emit("remove", tag.key);
|
|
212
|
+
} finally {
|
|
213
|
+
confirmHeld.value = false;
|
|
214
|
+
}
|
|
189
215
|
}
|
|
190
216
|
|
|
191
217
|
function useCustom() {
|
|
@@ -220,7 +246,6 @@ export const HkAffixPicker = defineComponent({
|
|
|
220
246
|
props.searchPlaceholder ?? t("hikari::affixPicker.search", "Search");
|
|
221
247
|
const emptyText = props.emptyText ?? t("hikari::affixPicker.empty", "No matches");
|
|
222
248
|
const removeLabel = t("hikari::affixPicker.remove", "Remove");
|
|
223
|
-
const confirmLabel = t("hikari::affixPicker.confirmDelete", "Confirm remove");
|
|
224
249
|
const placement = props.side === "suffix" ? "bottom-end" : "bottom-start";
|
|
225
250
|
return (
|
|
226
251
|
<>
|
|
@@ -263,6 +288,9 @@ export const HkAffixPicker = defineComponent({
|
|
|
263
288
|
items={[]}
|
|
264
289
|
open={open.value}
|
|
265
290
|
onUpdate:open={(v: boolean) => {
|
|
291
|
+
// Close requests that arrive while the confirm dialog is
|
|
292
|
+
// up are the dialog's own clicks — ignore them.
|
|
293
|
+
if (!v && confirmHeld.value) return;
|
|
266
294
|
open.value = v;
|
|
267
295
|
}}
|
|
268
296
|
anchorRef={chipRef.value}
|
|
@@ -287,13 +315,11 @@ export const HkAffixPicker = defineComponent({
|
|
|
287
315
|
class="hk-affix-tag-list"
|
|
288
316
|
>
|
|
289
317
|
{tags.map((tag) => {
|
|
290
|
-
const armed = armedKey.value === tag.key;
|
|
291
318
|
return (
|
|
292
319
|
<div
|
|
293
320
|
key={tag.key}
|
|
294
321
|
class="hk-affix-tag"
|
|
295
322
|
data-active={activeSet.value.includes(tag.key) || undefined}
|
|
296
|
-
data-armed={armed || undefined}
|
|
297
323
|
>
|
|
298
324
|
<button
|
|
299
325
|
type="button"
|
|
@@ -302,10 +328,6 @@ export const HkAffixPicker = defineComponent({
|
|
|
302
328
|
aria-label={`${t("hikari::affixPicker.switchTo", "Switch to")} ${tag.label}`}
|
|
303
329
|
onClick={(e: MouseEvent) => {
|
|
304
330
|
e.stopPropagation();
|
|
305
|
-
if (armed) {
|
|
306
|
-
armedKey.value = null;
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
331
|
pick(tag);
|
|
310
332
|
}}
|
|
311
333
|
>
|
|
@@ -325,16 +347,14 @@ export const HkAffixPicker = defineComponent({
|
|
|
325
347
|
<button
|
|
326
348
|
type="button"
|
|
327
349
|
class="hk-affix-tag-x"
|
|
328
|
-
aria-label={
|
|
329
|
-
title={
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
// intent, so the first click erases.
|
|
350
|
+
aria-label={`${removeLabel} — ${tag.label}`}
|
|
351
|
+
title={`${removeLabel} — ${tag.label}`}
|
|
352
|
+
// One tap opens the confirm dialog; the
|
|
353
|
+
// dialog's Confirm is what actually
|
|
354
|
+
// erases (fires `remove`).
|
|
334
355
|
onClick={(e: MouseEvent) => {
|
|
335
356
|
e.stopPropagation();
|
|
336
|
-
|
|
337
|
-
else toggleArm(tag.key);
|
|
357
|
+
void requestRemove(tag);
|
|
338
358
|
}}
|
|
339
359
|
onMousedown={(e: MouseEvent) => e.preventDefault()}
|
|
340
360
|
>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// HkBoard — the shared node-and-edge board surface.
|
|
2
|
+
// Geometry semantics live in utils/boardCamera.ts + utils/boardEdges.ts;
|
|
3
|
+
// this file only styles the layers (grid, world, edges, node shells,
|
|
4
|
+
// minimap dock).
|
|
5
|
+
|
|
6
|
+
.hk-board {
|
|
7
|
+
position: relative;
|
|
8
|
+
width: 100%;
|
|
9
|
+
height: 100%;
|
|
10
|
+
min-height: 0;
|
|
11
|
+
overflow: hidden;
|
|
12
|
+
background: rgb(var(--color-surface));
|
|
13
|
+
// gestures own the surface: no native scroll/selection interference
|
|
14
|
+
touch-action: none;
|
|
15
|
+
user-select: none;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.hk-board-grid {
|
|
19
|
+
position: absolute;
|
|
20
|
+
inset: 0;
|
|
21
|
+
pointer-events: none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.hk-board-world {
|
|
25
|
+
position: absolute;
|
|
26
|
+
top: 0;
|
|
27
|
+
left: 0;
|
|
28
|
+
transform-origin: 0 0;
|
|
29
|
+
will-change: transform;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.hk-board-edges {
|
|
33
|
+
position: absolute;
|
|
34
|
+
top: 0;
|
|
35
|
+
left: 0;
|
|
36
|
+
overflow: visible;
|
|
37
|
+
pointer-events: none;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.hk-board-edge {
|
|
41
|
+
fill: none;
|
|
42
|
+
stroke-linecap: round;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.hk-board-node {
|
|
46
|
+
position: absolute;
|
|
47
|
+
box-sizing: border-box;
|
|
48
|
+
|
|
49
|
+
&--draggable {
|
|
50
|
+
cursor: grab;
|
|
51
|
+
|
|
52
|
+
&:active {
|
|
53
|
+
cursor: grabbing;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.hk-board-node-shell {
|
|
59
|
+
width: 100%;
|
|
60
|
+
height: 100%;
|
|
61
|
+
display: flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
justify-content: center;
|
|
64
|
+
padding: 0 var(--space-6);
|
|
65
|
+
box-sizing: border-box;
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
text-overflow: ellipsis;
|
|
68
|
+
white-space: nowrap;
|
|
69
|
+
font-size: var(--text-2xs);
|
|
70
|
+
color: rgb(var(--color-text));
|
|
71
|
+
background: rgb(var(--color-surface));
|
|
72
|
+
border: 1px solid rgb(var(--color-primary) / 45%);
|
|
73
|
+
border-radius: var(--radius-sm);
|
|
74
|
+
box-shadow: var(--shadow-elevated);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.hk-board-minimap {
|
|
78
|
+
position: absolute;
|
|
79
|
+
right: var(--space-10);
|
|
80
|
+
bottom: var(--space-10);
|
|
81
|
+
z-index: 6;
|
|
82
|
+
pointer-events: auto;
|
|
83
|
+
}
|