@celestia-island/hikari 0.55.292 → 0.55.294

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.292",
3
+ "version": "0.55.294",
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 });
@@ -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
+ }