@celestia-island/hikari 0.55.293 → 0.55.295

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celestia-island/hikari",
3
- "version": "0.55.293",
3
+ "version": "0.55.295",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Hikari UI library, Vue 3 runtime — one design system for Vue 3 and Rust/WASM applications",
@@ -62,6 +62,7 @@ describe("registerAnimations", () => {
62
62
  expect(byName.get("hk-pwd-breathe")?.infinite).toBe(true);
63
63
  expect(byName.get("s-nav-item-badge-pulse")?.infinite).toBe(true);
64
64
  expect(byName.get("s-status-bar-reconnecting-breathe")?.infinite).toBe(true);
65
+ expect(byName.get("hk-range-ribbon-pulse")?.infinite).toBe(true);
65
66
  expect(byName.get("s-auth-card-in")?.infinite).toBe(false);
66
67
  expect(byName.get("hk-rolling-number-up")?.infinite).toBe(false);
67
68
  expect(byName.get("hk-modal-breadcrumb-in")?.infinite).toBe(false);
@@ -138,3 +138,8 @@ registerCssAnimation("hk-persistent-toast-card-in");
138
138
  // (opacity + 6px rise, plays once; the card unmounts at dwell end, so
139
139
  // no leave keyframe).
140
140
  registerCssAnimation("hk-screen-identify-in");
141
+ // components/HkRangeRibbon.scss — pending-segment breathe (loops forever):
142
+ // a queued / in-flight row keeps pulsing while it has not settled, so a
143
+ // brushed window still reads as live. Reduced motion keeps the dashed
144
+ // pending look and drops the loop (house pattern, see HkSkeleton).
145
+ registerCssAnimation("hk-range-ribbon-pulse", { infinite: true });
@@ -546,3 +546,118 @@ describe("HkModal chrome / content split", () => {
546
546
  });
547
547
  });
548
548
  });
549
+
550
+ describe("HkModal Escape precedence", () => {
551
+ it("lets a nested control own Escape while a bare Escape still closes", async () => {
552
+ const container = document.createElement("div");
553
+ document.body.appendChild(container);
554
+ containers.push(container);
555
+
556
+ const open = ref(true);
557
+ const emitted: boolean[] = [];
558
+ const Wrapper = defineComponent({
559
+ setup() {
560
+ return () =>
561
+ h(HkModal, {
562
+ modelValue: open.value,
563
+ closable: true,
564
+ title: "Escape precedence",
565
+ "onUpdate:modelValue": (v: boolean) => { emitted.push(v); open.value = v; },
566
+ }, {
567
+ default: () => h("div", {
568
+ class: "escape-owner",
569
+ onKeydown: (e: KeyboardEvent) => {
570
+ if (e.key === "Escape") e.preventDefault();
571
+ },
572
+ }, "nested control"),
573
+ });
574
+ },
575
+ });
576
+ const app = createApp(Wrapper);
577
+ mounts.push(app);
578
+ app.mount(container);
579
+
580
+ await vi.waitFor(() => {
581
+ expect(document.body.querySelector(".escape-owner")).not.toBeNull();
582
+ });
583
+ const owner = document.body.querySelector<HTMLElement>(".escape-owner")!;
584
+
585
+ // The nested control consumes Escape: the window must stay open.
586
+ owner.dispatchEvent(
587
+ new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }),
588
+ );
589
+ await nextTick();
590
+ expect(emitted, "a consumed Escape must not close the window").toEqual([]);
591
+ expect(open.value).toBe(true);
592
+
593
+ // A bare Escape (nothing below consumed it) still closes.
594
+ const content = document.body.querySelector<HTMLElement>(".hk-modal-content")!;
595
+ content.dispatchEvent(
596
+ new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }),
597
+ );
598
+ await nextTick();
599
+ expect(emitted).toEqual([false]);
600
+ });
601
+
602
+ it("keeps a consumed Escape from reaching page-level Escape listeners", async () => {
603
+ // One press must mean one action: before the precedence rule a closable
604
+ // window swallowed every Escape at its root, so a handled press travelling
605
+ // on would hand the same key to a page-level listener (a revealed strip, a
606
+ // pointer-reorder cancel) while the nested control also acted on it.
607
+ const container = document.createElement("div");
608
+ document.body.appendChild(container);
609
+ containers.push(container);
610
+ const open = ref(true);
611
+ const emitted: boolean[] = [];
612
+ const Wrapper = defineComponent({
613
+ setup() {
614
+ return () =>
615
+ h(HkModal, {
616
+ modelValue: open.value,
617
+ closable: true,
618
+ title: "Escape propagation",
619
+ "onUpdate:modelValue": (v: boolean) => { emitted.push(v); open.value = v; },
620
+ }, {
621
+ default: () => h("div", {
622
+ class: "escape-owner",
623
+ onKeydown: (e: KeyboardEvent) => {
624
+ if (e.key === "Escape") e.preventDefault();
625
+ },
626
+ }, "nested control"),
627
+ });
628
+ },
629
+ });
630
+ const app = createApp(Wrapper);
631
+ mounts.push(app);
632
+ app.mount(container);
633
+ await vi.waitFor(() => {
634
+ expect(document.body.querySelector(".escape-owner")).not.toBeNull();
635
+ });
636
+
637
+ const pageHits: string[] = [];
638
+ const pageListener = (e: KeyboardEvent) => {
639
+ if (e.key === "Escape") pageHits.push("escape");
640
+ };
641
+ document.addEventListener("keydown", pageListener);
642
+ try {
643
+ const owner = document.body.querySelector<HTMLElement>(".escape-owner")!;
644
+ owner.dispatchEvent(
645
+ new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }),
646
+ );
647
+ await nextTick();
648
+ expect(emitted, "a consumed Escape must not close the window").toEqual([]);
649
+ expect(pageHits, "a consumed Escape must not reach page-level listeners").toEqual([]);
650
+
651
+ // A bare Escape closes AND is still swallowed (the pre-existing rule).
652
+ const content = document.body.querySelector<HTMLElement>(".hk-modal-content")!;
653
+ content.dispatchEvent(
654
+ new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true }),
655
+ );
656
+ await nextTick();
657
+ expect(emitted).toEqual([false]);
658
+ expect(pageHits, "a closing Escape is swallowed by the window").toEqual([]);
659
+ } finally {
660
+ document.removeEventListener("keydown", pageListener);
661
+ }
662
+ });
663
+ });
@@ -489,6 +489,23 @@ export default defineComponent({
489
489
 
490
490
  function onKeydown(e: KeyboardEvent) {
491
491
  if (e.key === "Escape") {
492
+ // Cross-component precedence: a nested control that already consumed
493
+ // Escape (the range ribbon's "Escape resets the range" is the live
494
+ // case) owns the key, so the window must not close underneath it.
495
+ // `defaultPrevented` is the DOM-canonical "I handled this" signal for
496
+ // inline, preventDefault-only consumers — the package's pickers and
497
+ // panels instead stopPropagation (or teleport out of this subtree)
498
+ // and therefore never reach this handler either way.
499
+ //
500
+ // Keep swallowing the press here (`stopPropagation`) like a bare
501
+ // Escape does: before this precedence rule, a closable window consumed
502
+ // every Escape at this root, so letting a handled one travel on would
503
+ // hand the same press to page-level Escape listeners (a revealed strip,
504
+ // a pointer-reorder cancel) and produce one-press-two-actions.
505
+ if (e.defaultPrevented) {
506
+ if (props.closable) e.stopPropagation();
507
+ return;
508
+ }
492
509
  if (props.closable) {
493
510
  e.preventDefault();
494
511
  e.stopPropagation();
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ HkRangeRibbon,
5
+ type RibbonLane,
6
+ type RibbonSegment,
7
+ type RibbonStatus,
8
+ type RibbonTip,
9
+ type RibbonTone,
10
+ } from "../index";
11
+
12
+ /**
13
+ * Public barrel guard for HkRangeRibbon (R3 review: deleting the component's
14
+ * export block from src/index.ts kept the entire 4696-test suite green —
15
+ * every other test reaches the module through the direct "./HkRangeRibbon"
16
+ * path, so only an external consumer would have noticed). Same shape as
17
+ * HkCalendarHeatmap.barrel.test.ts.
18
+ */
19
+ describe("the public barrel", () => {
20
+ it("re-exports the ribbon component under its public name", () => {
21
+ expect(HkRangeRibbon, "HkRangeRibbon must be re-exported").toBeTruthy();
22
+ expect(HkRangeRibbon.name).toBe("HkRangeRibbon");
23
+ });
24
+
25
+ it("re-exports the ribbon's public types", () => {
26
+ // Type-only exports vanish at runtime, so each one is CONSUMED here: a
27
+ // dropped `type Ribbon*` line fails vue-tsc on this file instead of
28
+ // leaving the runtime suite green.
29
+ const lane: RibbonLane = { key: "cpu", label: "CPU" };
30
+ const tone: RibbonTone = "success";
31
+ const status: RibbonStatus = "pending";
32
+ const tip: RibbonTip = { title: "Boot", lines: ["12ms"] };
33
+ const segment: RibbonSegment = {
34
+ id: "boot",
35
+ lane: lane.key,
36
+ start: 0,
37
+ end: 0.2,
38
+ label: "boot",
39
+ tone,
40
+ status,
41
+ tip,
42
+ };
43
+ expect(segment).toMatchObject({ id: "boot", lane: "cpu", start: 0, end: 0.2 });
44
+ expect(segment.tip?.lines).toEqual(["12ms"]);
45
+ });
46
+ });
@@ -0,0 +1,401 @@
1
+ @use "../styles/polyfill" as *;
2
+
3
+ /* HkRangeRibbon — a multi-lane sequence ribbon with a brushed window.
4
+ Anatomy: a thin OVERVIEW strip (the full domain, always), the window
5
+ highlight + outside scrims + two edge handles riding over it, then the
6
+ lane list whose segment bars show either the full domain or the window.
7
+
8
+ Geometry is part of the contract and pinned by HkRangeRibbon.test.tsx:
9
+ the strip is 32px tall, a handle is an 8px half-overlapping grip
10
+ (translateX(-50%) so `left: 20%` centers it on the window edge), a
11
+ clipped segment keeps a 2px minimum so edge slivers stay visible, and
12
+ the reset chip clears the edge handle (right: 4px = the handle's
13
+ half-width, so a handle at 0% / 100% is never covered).
14
+
15
+ Every colour/size comes from hikari theme tokens; the only motion is the
16
+ pending-segment breathe, registered in animation/registerAnimations.ts. */
17
+
18
+ .hk-range-ribbon {
19
+ // ONE label column for both surfaces: the overview paints through an
20
+ // empty gutter of this width, so an x% bar sits at the same inline
21
+ // position in the strip and in its lane (the strip locates segments).
22
+ --hk-range-ribbon-label-col: minmax(48px, 84px);
23
+
24
+ display: flex;
25
+ flex-direction: column;
26
+ gap: var(--space-6);
27
+ width: 100%;
28
+ min-width: 0;
29
+ }
30
+
31
+ // ── Overview strip (the full-domain minimap + brush surface) ─────────────
32
+ .hk-range-ribbon-overview {
33
+ position: relative;
34
+ display: grid;
35
+ grid-template-columns: var(--hk-range-ribbon-label-col) minmax(0, 1fr);
36
+ align-items: center;
37
+ gap: var(--space-4);
38
+ min-width: 0;
39
+ }
40
+
41
+ // The empty first cell that lines the strip up with the lane tracks.
42
+ .hk-range-ribbon-overview-gutter {
43
+ display: block;
44
+ min-width: 0;
45
+ }
46
+
47
+ // The strip's own coordinate space: handles/reset are positioned against
48
+ // this box, which is exactly the strip's width (never the gutter too).
49
+ .hk-range-ribbon-overview-field {
50
+ position: relative;
51
+ min-width: 0;
52
+ }
53
+
54
+ .hk-range-ribbon-strip {
55
+ position: relative;
56
+ display: block;
57
+ height: 32px;
58
+ margin: 0;
59
+ border-radius: var(--radius-sm);
60
+ background: rgb(var(--color-surface) / 55%);
61
+ box-shadow: inset 0 0 0 1px rgb(var(--color-border) / 60%);
62
+ cursor: crosshair;
63
+ overflow: hidden;
64
+ touch-action: none;
65
+ @include user-select(none);
66
+ }
67
+
68
+ .hk-range-ribbon[data-window-enabled="false"] .hk-range-ribbon-strip {
69
+ cursor: default;
70
+ }
71
+
72
+ .hk-range-ribbon-strip:focus-visible {
73
+ outline: 2px solid rgb(var(--color-primary));
74
+ outline-offset: 2px;
75
+ }
76
+
77
+ .hk-range-ribbon-overview-lanes {
78
+ position: absolute;
79
+ inset: 4px 0;
80
+ display: flex;
81
+ flex-direction: column;
82
+ gap: 1px;
83
+ pointer-events: none;
84
+ }
85
+
86
+ .hk-range-ribbon-overview-row {
87
+ position: relative;
88
+ flex: 1 1 0;
89
+ min-height: 1px;
90
+ }
91
+
92
+ .hk-range-ribbon-overview-segment {
93
+ position: absolute;
94
+ top: 0;
95
+ bottom: 0;
96
+ min-width: 1px;
97
+ border-radius: 1px;
98
+ background: rgb(var(--color-muted) / 45%);
99
+ }
100
+
101
+ .hk-range-ribbon-overview-segment[data-tone="success"] {
102
+ background: rgb(var(--color-success) / 60%);
103
+ }
104
+
105
+ .hk-range-ribbon-overview-segment[data-tone="secondary"] {
106
+ background: rgb(var(--color-secondary) / 55%);
107
+ }
108
+
109
+ .hk-range-ribbon-overview-segment[data-tone="warning"] {
110
+ background: rgb(var(--color-warning) / 70%);
111
+ }
112
+
113
+ .hk-range-ribbon-overview-segment[data-tone="error"] {
114
+ background: rgb(var(--color-error) / 65%);
115
+ }
116
+
117
+ .hk-range-ribbon-overview-segment[data-tone="muted"] {
118
+ background: rgb(var(--color-muted) / 25%);
119
+ }
120
+
121
+ .hk-range-ribbon-overview-segment[data-status="error"] {
122
+ background: rgb(var(--color-error) / 85%);
123
+ }
124
+
125
+ .hk-range-ribbon-overview-segment[data-status="pending"] {
126
+ opacity: 0.5;
127
+ }
128
+
129
+ // Outside-the-window dim: two scrims rather than one masked sheet, so the
130
+ // highlighted window reads as a hole in the overview (and stays testable).
131
+ .hk-range-ribbon-scrim {
132
+ position: absolute;
133
+ top: 0;
134
+ bottom: 0;
135
+ background: rgb(var(--color-background) / 62%);
136
+ pointer-events: none;
137
+ }
138
+
139
+ .hk-range-ribbon-window {
140
+ position: absolute;
141
+ top: 0;
142
+ bottom: 0;
143
+ border: 1px solid rgb(var(--color-primary));
144
+ border-radius: 2px;
145
+ background: rgb(var(--color-primary) / 10%);
146
+ pointer-events: none;
147
+ }
148
+
149
+ .hk-range-ribbon-handle {
150
+ position: absolute;
151
+ top: 3px;
152
+ bottom: 3px;
153
+ z-index: 2;
154
+ width: 8px;
155
+ transform: translateX(-50%);
156
+ border: 1px solid rgb(var(--color-surface));
157
+ border-radius: var(--radius-full);
158
+ background: rgb(var(--color-primary));
159
+ cursor: ew-resize;
160
+ touch-action: none;
161
+ // Dragging the grip must not start a native text selection (house
162
+ // pattern: HkMediaSlider's track).
163
+ @include user-select(none);
164
+ }
165
+
166
+ .hk-range-ribbon-handle:hover {
167
+ box-shadow: 0 0 0 2px rgb(var(--color-primary) / 25%);
168
+ }
169
+
170
+ .hk-range-ribbon-handle:focus-visible {
171
+ outline: 2px solid rgb(var(--color-primary));
172
+ outline-offset: 2px;
173
+ }
174
+
175
+ .hk-range-ribbon-reset {
176
+ position: absolute;
177
+ top: 4px;
178
+ right: 4px;
179
+ z-index: 3;
180
+ display: inline-flex;
181
+ align-items: center;
182
+ justify-content: center;
183
+ width: 18px;
184
+ height: 18px;
185
+ padding: 0;
186
+ border: 1px solid rgb(var(--color-border));
187
+ border-radius: var(--radius-sm);
188
+ background: rgb(var(--color-surface) / 85%);
189
+ color: rgb(var(--color-text));
190
+ cursor: pointer;
191
+ transition: background-color var(--duration-fast, 0.15s) ease;
192
+ }
193
+
194
+ .hk-range-ribbon-reset:hover {
195
+ background: rgb(var(--color-surface));
196
+ }
197
+
198
+ .hk-range-ribbon-reset:focus-visible {
199
+ outline: 2px solid rgb(var(--color-primary));
200
+ outline-offset: 1px;
201
+ }
202
+
203
+ // Visually hidden, still read by AT: labels the keyboard contract the
204
+ // strip's slider role cannot express by itself.
205
+ .hk-range-ribbon-hint {
206
+ position: absolute;
207
+ width: 1px;
208
+ height: 1px;
209
+ margin: -1px;
210
+ padding: 0;
211
+ border: 0;
212
+ overflow: hidden;
213
+ clip: rect(0 0 0 0);
214
+ white-space: nowrap;
215
+ }
216
+
217
+ // ── Lane list ───────────────────────────────────────────────────────────
218
+ .hk-range-ribbon-lanes {
219
+ display: flex;
220
+ flex-direction: column;
221
+ gap: var(--space-2);
222
+ min-width: 0;
223
+ }
224
+
225
+ .hk-range-ribbon-lane {
226
+ display: grid;
227
+ grid-template-columns: var(--hk-range-ribbon-label-col) minmax(0, 1fr);
228
+ align-items: center;
229
+ gap: var(--space-4);
230
+ min-width: 0;
231
+ }
232
+
233
+ .hk-range-ribbon-lane-label {
234
+ overflow: hidden;
235
+ color: rgb(var(--color-muted));
236
+ font-size: var(--text-2xs);
237
+ // 1.3 floor: an overflow clip box shorter than the font ink box cuts the
238
+ // descenders of g/j/p/q/y (textClip.contract.test.ts).
239
+ line-height: 1.3;
240
+ text-overflow: ellipsis;
241
+ white-space: nowrap;
242
+ }
243
+
244
+ .hk-range-ribbon-lane-track {
245
+ position: relative;
246
+ height: 20px;
247
+ border-radius: var(--radius-sm);
248
+ background: rgb(var(--color-muted) / 8%);
249
+ }
250
+
251
+ // ── Segment bars ────────────────────────────────────────────────────────
252
+ // No overflow:hidden here: the tip card is a child and must float above
253
+ // the neighbouring lanes. The label clips itself instead.
254
+ .hk-range-ribbon-segment {
255
+ position: absolute;
256
+ top: 3px;
257
+ bottom: 3px;
258
+ display: flex;
259
+ align-items: center;
260
+ box-sizing: border-box;
261
+ min-width: 2px;
262
+ padding: 0 var(--space-2);
263
+ border: 1px solid rgb(var(--color-border) / 45%);
264
+ border-radius: var(--radius-sm);
265
+ background: rgb(var(--color-muted) / 30%);
266
+ color: rgb(var(--color-text));
267
+ font-size: var(--text-2xs);
268
+ line-height: 1.3;
269
+ cursor: pointer;
270
+ }
271
+
272
+ .hk-range-ribbon-segment[data-tone="success"] {
273
+ border-color: rgb(var(--color-success) / 55%);
274
+ background: rgb(var(--color-success) / 22%);
275
+ }
276
+
277
+ .hk-range-ribbon-segment[data-tone="secondary"] {
278
+ border-color: rgb(var(--color-secondary) / 55%);
279
+ background: rgb(var(--color-secondary) / 20%);
280
+ }
281
+
282
+ .hk-range-ribbon-segment[data-tone="warning"] {
283
+ border-color: rgb(var(--color-warning) / 65%);
284
+ background: rgb(var(--color-warning) / 24%);
285
+ }
286
+
287
+ .hk-range-ribbon-segment[data-tone="error"] {
288
+ border-color: rgb(var(--color-error) / 60%);
289
+ background: rgb(var(--color-error) / 22%);
290
+ }
291
+
292
+ .hk-range-ribbon-segment[data-tone="muted"] {
293
+ border-color: rgb(var(--color-muted) / 35%);
294
+ background: rgb(var(--color-muted) / 16%);
295
+ color: rgb(var(--color-muted));
296
+ }
297
+
298
+ // Status channels win over the tone channel: an errored row is an error
299
+ // regardless of what tone it also carries.
300
+ .hk-range-ribbon-segment[data-status="error"] {
301
+ border-color: rgb(var(--color-error));
302
+ background: rgb(var(--color-error) / 32%);
303
+ }
304
+
305
+ // Queued / in-flight rows still breathe — the one motion in the ribbon.
306
+ .hk-range-ribbon-segment[data-status="pending"] {
307
+ border-style: dashed;
308
+ animation: hk-range-ribbon-pulse 1.6s ease-in-out infinite;
309
+ }
310
+
311
+ @keyframes hk-range-ribbon-pulse {
312
+ 0%,
313
+ 100% {
314
+ opacity: 1;
315
+ }
316
+
317
+ 50% {
318
+ opacity: 0.5;
319
+ }
320
+ }
321
+
322
+ // House pattern (HkSkeleton / HkStatusPill): keep the dashed pending look,
323
+ // drop the loop.
324
+ @media (prefers-reduced-motion: reduce) {
325
+ .hk-range-ribbon-segment[data-status="pending"] {
326
+ animation: none;
327
+ }
328
+ }
329
+
330
+ .hk-range-ribbon-segment[data-selected="true"] {
331
+ border-color: rgb(var(--color-primary));
332
+ box-shadow: 0 0 0 1px rgb(var(--color-primary));
333
+ z-index: 1;
334
+ }
335
+
336
+ .hk-range-ribbon-segment:hover {
337
+ border-color: rgb(var(--color-primary) / 70%);
338
+ }
339
+
340
+ .hk-range-ribbon-segment:focus-visible {
341
+ z-index: 2;
342
+ border-color: rgb(var(--color-primary));
343
+ outline: 2px solid rgb(var(--color-primary));
344
+ outline-offset: 1px;
345
+ }
346
+
347
+ .hk-range-ribbon-segment-label {
348
+ display: block;
349
+ width: 100%;
350
+ overflow: hidden;
351
+ // Same descender floor as the lane label: this is the clip box.
352
+ line-height: 1.3;
353
+ text-overflow: ellipsis;
354
+ white-space: nowrap;
355
+ }
356
+
357
+ // ── Tip card ────────────────────────────────────────────────────────────
358
+ .hk-range-ribbon-tip {
359
+ position: absolute;
360
+ bottom: calc(100% + var(--space-6));
361
+ left: 50%;
362
+ z-index: 4;
363
+ display: flex;
364
+ flex-direction: column;
365
+ gap: 1px;
366
+ min-width: 108px;
367
+ max-width: 240px;
368
+ padding: var(--space-4) var(--space-6);
369
+ transform: translateX(-50%);
370
+ border: 1px solid rgb(var(--color-border));
371
+ border-radius: var(--radius-md);
372
+ background: rgb(var(--color-surface));
373
+ box-shadow: 0 6px 18px rgb(var(--color-text) / 16%);
374
+ color: rgb(var(--color-text));
375
+ font-size: var(--text-2xs);
376
+ line-height: 1.35;
377
+ text-align: start;
378
+ white-space: normal;
379
+ pointer-events: none;
380
+ }
381
+
382
+ // A tip on a segment near either end of the track grows INWARD from that
383
+ // segment's own edge instead of centring (and overflowing) the ribbon.
384
+ .hk-range-ribbon-tip[data-anchor="start"] {
385
+ left: 0;
386
+ transform: none;
387
+ }
388
+
389
+ .hk-range-ribbon-tip[data-anchor="end"] {
390
+ left: auto;
391
+ right: 0;
392
+ transform: none;
393
+ }
394
+
395
+ .hk-range-ribbon-tip-title {
396
+ font-weight: 600;
397
+ }
398
+
399
+ .hk-range-ribbon-tip-line {
400
+ color: rgb(var(--color-muted));
401
+ }