@celestia-island/hikari 0.55.332 → 0.55.335
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/HkDockBar.tsx +26 -5
- package/src/components/HkKanban.scss +23 -0
- package/src/components/HkKanban.tsx +125 -78
- package/src/components/HkProgressBar.scss +52 -0
- package/src/components/HkProgressBar.styles.test.ts +79 -0
- package/src/components/HkProgressBar.test.tsx +202 -1
- package/src/components/HkProgressBar.tsx +146 -21
- package/src/components/HkScrollContainer.dockRide.test.tsx +762 -0
- package/src/components/HkScrollContainer.fade.contract.test.ts +14 -4
- package/src/components/HkScrollContainer.scss +127 -0
- package/src/components/HkScrollContainer.tsx +248 -27
- package/src/components/HkTooltip.scss +29 -0
- package/src/components/HkTooltip.styles.test.ts +53 -0
- package/src/components/HkTooltip.test.tsx +248 -1
- package/src/components/HkTooltip.tsx +185 -16
- package/src/components/HkTreemap.scss +6 -1
- package/src/components/HkTreemap.test.tsx +100 -6
- package/src/components/HkTreemap.tsx +79 -11
- package/src/components/HkTreemapStage.scss +5 -1
- package/src/components/HkTreemapStage.test.tsx +52 -1
- package/src/components/HkTreemapStage.tsx +18 -3
- package/src/index.ts +5 -1
package/package.json
CHANGED
|
@@ -42,11 +42,32 @@ export type HkDockBarSurface = "glass" | "solid";
|
|
|
42
42
|
*
|
|
43
43
|
* Relationship to HkScrollContainer: that component's dockTop /
|
|
44
44
|
* dockBottom named slots are the contract for docks that live INSIDE a
|
|
45
|
-
* scrolling region
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
45
|
+
* scrolling region. Those slots place a dock two ways (2026-11-10
|
|
46
|
+
* addition — `dockMode`):
|
|
47
|
+
* - `ride` (the pinned-to-the-visible-bottom-edge form): the dock
|
|
48
|
+
* mounts inside the scroll viewport after the content, on a spacer
|
|
49
|
+
* exactly one measured dock height tall, `position: sticky; bottom:
|
|
50
|
+
* 0`. The air between the content end and the dock is therefore
|
|
51
|
+
* exactly one dock height; short content lets the dock follow the
|
|
52
|
+
* content end (the leftover window height falls below it), long
|
|
53
|
+
* content pins it to the visible bottom edge. Use it for a dock that
|
|
54
|
+
* belongs to the CONTENT (a cruise canvas's bottom bar under its
|
|
55
|
+
* last row). The container's ride wrapper is `pointer-events: none`
|
|
56
|
+
* (it covers content while pinned), so this carrier's own box
|
|
57
|
+
* contract is what keeps the dock interactive: the anchor box catches
|
|
58
|
+
* nothing, the surface catches everything.
|
|
59
|
+
* - `pin` (the default, older form): the dock is a non-scrolling flex
|
|
60
|
+
* SIBLING of the viewport, so it sits on the container's edge and the
|
|
61
|
+
* viewport shrinks by its height — correct for chrome that owns the
|
|
62
|
+
* region's frame rather than trailing its content.
|
|
63
|
+
* HkDockBar is the carrier for docks that FLOAT over a canvas or page
|
|
64
|
+
* instead. Pick by host: scrolling region → HkScrollContainer dock slots
|
|
65
|
+
* (`ride` to follow the content, `pin` to own the edge); canvas/page
|
|
66
|
+
* overlay → HkDockBar. Do not nest an HkDockBar inside a scroll viewport
|
|
67
|
+
* expecting the container to place it — the container's slots do the
|
|
68
|
+
* placing, and a dock it mounted inside the viewport (ride) makes
|
|
69
|
+
* `closest(".hk-scroll-dock")` true, which is how a carrier knows to stop
|
|
70
|
+
* publishing shell-level clearance for it.
|
|
50
71
|
*
|
|
51
72
|
* Surfaces:
|
|
52
73
|
* - `glass` (default): translucent surface + backdrop blur.
|
|
@@ -17,9 +17,32 @@
|
|
|
17
17
|
display: flex;
|
|
18
18
|
align-items: stretch;
|
|
19
19
|
gap: var(--space-12);
|
|
20
|
+
/* A short board fills the strip: the lanes box is at least the viewport
|
|
21
|
+
height, so lane chrome reaches the strip's floor. */
|
|
20
22
|
min-height: 100%;
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
/* ── Ride boards: the lanes box must follow its CONTENT ────────────────
|
|
26
|
+
2026-11-10 review S0, measured on a real board (chest cruise canvas):
|
|
27
|
+
in a column-flex strip viewport `min-height: 100%` makes the lanes BOX
|
|
28
|
+
exactly as tall as the viewport, so the ride spacer + sticky dock —
|
|
29
|
+
which are IN FLOW after that box — always sit below the fold. The dock
|
|
30
|
+
can then only hug the visible bottom edge, the spacer clears nothing,
|
|
31
|
+
and the air goes back to being the window's leftover (measured 275.8px
|
|
32
|
+
static / 563.8px at the scroll end against a 139px dock, i.e. no
|
|
33
|
+
improvement over the pin board and worse when scrolled). Releasing the
|
|
34
|
+
min-height lets the box end at the real content end, which is what the
|
|
35
|
+
ride placement needs: the spacer becomes the visible air and the dock
|
|
36
|
+
trails the content.
|
|
37
|
+
|
|
38
|
+
Ride only, by the root hook HkKanban emits for a ride host: a pin board
|
|
39
|
+
must keep the full-height lanes box it always had, so this rule never
|
|
40
|
+
reaches an existing board. Specificity (0,3,0) beats the base rule
|
|
41
|
+
above; a host that wants its own rule can override it the same way. */
|
|
42
|
+
.hk-kanban[data-dock-mode="ride"] .hk-kanban-lanes {
|
|
43
|
+
min-height: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
23
46
|
.hk-kanban[data-axis="vertical"] .hk-kanban-lanes {
|
|
24
47
|
flex-direction: column;
|
|
25
48
|
}
|
|
@@ -105,6 +105,26 @@ export default defineComponent({
|
|
|
105
105
|
* contract generates no spacer and the shrink pin is inert in the
|
|
106
106
|
* block viewport — a horizontal board gets a no-op by design. */
|
|
107
107
|
dockTailClearance: { type: Boolean, default: false },
|
|
108
|
+
/** Where a `dockBottom` dock lives, forwarded to the strip's
|
|
109
|
+
* HkScrollContainer (see its `dockMode`): "pin" (default) keeps the
|
|
110
|
+
* dock on the strip's bottom edge, "ride" moves it into the strip's
|
|
111
|
+
* content flow so the air above it is the measured dock occupancy and
|
|
112
|
+
* the dock follows the content. Only effective on axis="vertical"/
|
|
113
|
+
* "both" boards — a horizontal strip carries its tail on the cross
|
|
114
|
+
* axis, where the container's ride placement is gated off by design.
|
|
115
|
+
*
|
|
116
|
+
* A ride board ALSO carries `data-dock-mode="ride"` on its root — only
|
|
117
|
+
* while a `dockBottom` slot is actually mounted, since that hook
|
|
118
|
+
* releases this component's full-height lanes box
|
|
119
|
+
* (see HkKanban.scss): `.hk-kanban-lanes { min-height: 100% }` keeps
|
|
120
|
+
* the lanes box as tall as the viewport, which would push the in-flow
|
|
121
|
+
* spacer + dock below the fold and turn the air back into the
|
|
122
|
+
* window's leftover (measured on a real board: 275.8px static vs the
|
|
123
|
+
* 139px dock, i.e. zero improvement over the pin board). The
|
|
124
|
+
* attribute is emitted only while a ride dock is genuinely mounted (not
|
|
125
|
+
* for pin, not for a horizontal board, not for an empty slot), so every
|
|
126
|
+
* existing board is untouched. */
|
|
127
|
+
dockMode: { type: String as PropType<"pin" | "ride">, default: "pin" },
|
|
108
128
|
},
|
|
109
129
|
emits: ["move"],
|
|
110
130
|
setup(props, { slots, emit, expose }) {
|
|
@@ -185,90 +205,117 @@ export default defineComponent({
|
|
|
185
205
|
|
|
186
206
|
expose({ scrollHost, dragging });
|
|
187
207
|
|
|
188
|
-
return () =>
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
return () => {
|
|
209
|
+
// Docks attach to the strip's VERTICAL tail: `axis="horizontal"`
|
|
210
|
+
// boards keep the pre-2026-11-10 shape (no forwarded dock slot), the
|
|
211
|
+
// same gate the container itself applies to the ride placement —
|
|
212
|
+
// there the tail is the cross axis, where an in-flow block child
|
|
213
|
+
// leaks onto the hidden vertical axis.
|
|
214
|
+
const docksOnTail = stripAxis.value !== "horizontal";
|
|
215
|
+
// Styling hook AND behaviour switch for the CONTENT HEIGHT of the
|
|
216
|
+
// board (see the dockMode prop doc): a ride dock only follows the
|
|
217
|
+
// content when the lanes box is allowed to end above the viewport
|
|
218
|
+
// bottom. Ride only — a pin board keeps the full-height lanes box it
|
|
219
|
+
// always had, so no existing board changes. Gated on the dock actually
|
|
220
|
+
// being mounted too (S2-2): a requested-but-empty slot is legal, and
|
|
221
|
+
// releasing the lanes box for it would only shrink lane chrome on a
|
|
222
|
+
// short board while no dock is there to justify it.
|
|
223
|
+
const rideBoard = docksOnTail && props.dockMode === "ride" && !!slots.dockBottom;
|
|
224
|
+
return (
|
|
225
|
+
<div
|
|
226
|
+
class="hk-kanban"
|
|
227
|
+
data-axis={props.axis}
|
|
228
|
+
data-dock-mode={rideBoard ? "ride" : undefined}
|
|
229
|
+
data-dragging={dragging.value ? "" : undefined}
|
|
230
|
+
aria-label={props.ariaLabel}
|
|
231
|
+
>
|
|
232
|
+
{props.lanes.length === 0
|
|
233
|
+
? (slots.empty?.() ?? null)
|
|
234
|
+
: (
|
|
235
|
+
<HScrollContainer
|
|
236
|
+
ref={scrollHost}
|
|
237
|
+
class="hk-kanban-strip"
|
|
238
|
+
axis={stripAxis.value}
|
|
239
|
+
overscanScreens={1}
|
|
240
|
+
dockTailClearance={props.dockTailClearance}
|
|
241
|
+
dockMode={props.dockMode}
|
|
212
242
|
>
|
|
213
|
-
{
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
onDragover={(event: DragEvent) => onDragOver(laneKey, event)}
|
|
223
|
-
onDragleave={(event: DragEvent) => onDragLeave(laneKey, event)}
|
|
224
|
-
onDrop={(event: DragEvent) => onDrop(laneKey, cards.length, event)}
|
|
243
|
+
{{
|
|
244
|
+
default: () => (
|
|
245
|
+
<div
|
|
246
|
+
class="hk-kanban-lanes"
|
|
247
|
+
// One variable; the stylesheet decides whether it is the
|
|
248
|
+
// lane's width (horizontal boards) or its height (vertical).
|
|
249
|
+
style={
|
|
250
|
+
props.laneSize ? { "--hk-kanban-lane-size": props.laneSize } : undefined
|
|
251
|
+
}
|
|
225
252
|
>
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
<
|
|
240
|
-
|
|
241
|
-
class="hk-kanban-card"
|
|
242
|
-
data-card={cardKey}
|
|
243
|
-
data-card-index={cardIndex}
|
|
244
|
-
draggable={props.draggable || undefined}
|
|
245
|
-
onDragstart={(event: DragEvent) =>
|
|
246
|
-
onDragStart(card, laneKey, cardIndex, event)
|
|
247
|
-
}
|
|
248
|
-
onDragend={onDragEnd}
|
|
249
|
-
>
|
|
250
|
-
{slots.card?.({
|
|
251
|
-
card,
|
|
252
|
-
cardIndex,
|
|
253
|
+
{props.lanes.map((lane, laneIndex) => {
|
|
254
|
+
const laneKey = props.laneKeyOf(lane, laneIndex);
|
|
255
|
+
const cards = cardsFor(lane, laneIndex);
|
|
256
|
+
return (
|
|
257
|
+
<section
|
|
258
|
+
key={laneKey}
|
|
259
|
+
class="hk-kanban-lane"
|
|
260
|
+
data-lane={laneKey}
|
|
261
|
+
data-drop-target={dropTarget.value === laneKey ? "" : undefined}
|
|
262
|
+
onDragover={(event: DragEvent) => onDragOver(laneKey, event)}
|
|
263
|
+
onDragleave={(event: DragEvent) => onDragLeave(laneKey, event)}
|
|
264
|
+
onDrop={(event: DragEvent) => onDrop(laneKey, cards.length, event)}
|
|
265
|
+
>
|
|
266
|
+
<header class="hk-kanban-lane-header">
|
|
267
|
+
{slots.laneHeader?.({
|
|
253
268
|
lane,
|
|
254
269
|
laneIndex,
|
|
255
270
|
laneKey,
|
|
271
|
+
cards,
|
|
256
272
|
})}
|
|
273
|
+
</header>
|
|
274
|
+
<div class="hk-kanban-cards">
|
|
275
|
+
{cards.map((card, cardIndex) => {
|
|
276
|
+
const cardKey =
|
|
277
|
+
props.cardKeyOf?.(card, cardIndex) ?? `${laneKey}#${cardIndex}`;
|
|
278
|
+
return (
|
|
279
|
+
<div
|
|
280
|
+
key={cardKey}
|
|
281
|
+
class="hk-kanban-card"
|
|
282
|
+
data-card={cardKey}
|
|
283
|
+
data-card-index={cardIndex}
|
|
284
|
+
draggable={props.draggable || undefined}
|
|
285
|
+
onDragstart={(event: DragEvent) =>
|
|
286
|
+
onDragStart(card, laneKey, cardIndex, event)
|
|
287
|
+
}
|
|
288
|
+
onDragend={onDragEnd}
|
|
289
|
+
>
|
|
290
|
+
{slots.card?.({
|
|
291
|
+
card,
|
|
292
|
+
cardIndex,
|
|
293
|
+
lane,
|
|
294
|
+
laneIndex,
|
|
295
|
+
laneKey,
|
|
296
|
+
})}
|
|
297
|
+
</div>
|
|
298
|
+
);
|
|
299
|
+
})}
|
|
300
|
+
{/* The tail keeps the lane's empty space hittable
|
|
301
|
+
(the lane itself owns the drop). */}
|
|
302
|
+
<div class="hk-kanban-lane-tail" />
|
|
257
303
|
</div>
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
304
|
+
{slots.laneFooter?.({ lane, laneIndex, laneKey, cards })}
|
|
305
|
+
</section>
|
|
306
|
+
);
|
|
307
|
+
})}
|
|
308
|
+
</div>
|
|
309
|
+
),
|
|
310
|
+
// Forwarded so a board's own chrome (a cruise canvas's dock
|
|
311
|
+
// under the lanes) keeps the container's dock contract —
|
|
312
|
+
// including the --hk-scroll-dock-* heights it publishes.
|
|
313
|
+
dockBottom: docksOnTail ? slots.dockBottom : undefined,
|
|
314
|
+
}}
|
|
315
|
+
</HScrollContainer>
|
|
316
|
+
)}
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
};
|
|
273
320
|
},
|
|
274
321
|
});
|
|
@@ -111,6 +111,57 @@
|
|
|
111
111
|
background: rgb(var(--color-primary));
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// ── overspend (max + overValue) ─────────────────────────────────────
|
|
115
|
+
// The track grows to `max + overValue`: the main fill keeps the quota's
|
|
116
|
+
// share, the overspend stretch takes the rest to the inline end, and the
|
|
117
|
+
// 1px divider is CARVED out of the stretch's inline-start edge — the track
|
|
118
|
+
// background shows through the seam. A painted line would need a colour of
|
|
119
|
+
// its own (and could not sit correctly over the translucent track); the
|
|
120
|
+
// carved gap is one geometry that also mirrors correctly in RTL.
|
|
121
|
+
.hk-progress-bar-overspend {
|
|
122
|
+
position: absolute;
|
|
123
|
+
top: 0;
|
|
124
|
+
height: 100%;
|
|
125
|
+
// Square on the seam side, rounded at the track's end: the 1px gap then
|
|
126
|
+
// reads as a divider over the whole track height instead of a lens
|
|
127
|
+
// between two round caps.
|
|
128
|
+
border-start-start-radius: 0;
|
|
129
|
+
border-end-start-radius: 0;
|
|
130
|
+
border-start-end-radius: var(--radius-full);
|
|
131
|
+
border-end-end-radius: var(--radius-full);
|
|
132
|
+
transition:
|
|
133
|
+
width var(--duration-normal) var(--ease-standard),
|
|
134
|
+
inset-inline-start var(--duration-normal) var(--ease-standard);
|
|
135
|
+
|
|
136
|
+
&-muted {
|
|
137
|
+
background: rgb(var(--color-muted));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
&-danger {
|
|
141
|
+
background: rgb(var(--color-error));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// While an overspend stretch follows it, the main fill squares its
|
|
146
|
+
// inline-end corners so the seam divider stays exactly 1px wide.
|
|
147
|
+
.hk-progress-bar-track--overspend .hk-progress-bar-fill {
|
|
148
|
+
border-start-end-radius: 0;
|
|
149
|
+
border-end-end-radius: 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Overspend caption ("超支 0.8 度"), riding the label row after the value.
|
|
153
|
+
.hk-progress-bar-overspend-label {
|
|
154
|
+
margin-inline-start: var(--space-6);
|
|
155
|
+
|
|
156
|
+
&-muted {
|
|
157
|
+
color: rgb(var(--color-muted));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
&-danger {
|
|
161
|
+
color: rgb(var(--color-error));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
114
165
|
// Keyframes animate the logical inset so the sweep mirrors in RTL together
|
|
115
166
|
// with the fill/secondary anchoring (LTR-identical).
|
|
116
167
|
@keyframes hk-progress-indeterminate {
|
|
@@ -138,6 +189,7 @@
|
|
|
138
189
|
// (house guard: the family bar-fill pattern).
|
|
139
190
|
.hk-progress-bar-fill,
|
|
140
191
|
.hk-progress-bar-secondary,
|
|
192
|
+
.hk-progress-bar-overspend,
|
|
141
193
|
.hk-progress-bar-seg {
|
|
142
194
|
transition: none;
|
|
143
195
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Source contract for the overspend stretch's look. vitest cannot see
|
|
9
|
+
* stylesheets (happy-dom loads no SCSS), so a deleted rule would leave the
|
|
10
|
+
* DOM geometry guards green while the stretch painted as another primary
|
|
11
|
+
* fill — the whole point of the second segment is that it reads as PAST
|
|
12
|
+
* quota.
|
|
13
|
+
*/
|
|
14
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const scss = readFileSync(join(here, "HkProgressBar.scss"), "utf8");
|
|
16
|
+
|
|
17
|
+
/** The DECLARATIONS of a rule, comments stripped (a comment that names a
|
|
18
|
+
* property must not satisfy a guard about it). */
|
|
19
|
+
function ruleBlock(selector: string): string {
|
|
20
|
+
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
21
|
+
const head = new RegExp(`${escaped}\\s*\\{`).exec(scss);
|
|
22
|
+
if (!head) throw new Error(`no rule for ${selector}`);
|
|
23
|
+
// Nested SCSS blocks: walk braces to the matching close.
|
|
24
|
+
const open = head.index + head[0].length - 1;
|
|
25
|
+
let depth = 0;
|
|
26
|
+
let end = open;
|
|
27
|
+
for (; end < scss.length; end++) {
|
|
28
|
+
if (scss[end] === "{") depth += 1;
|
|
29
|
+
else if (scss[end] === "}") {
|
|
30
|
+
depth -= 1;
|
|
31
|
+
if (depth === 0) break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return scss.slice(open + 1, end)
|
|
35
|
+
.split("\n")
|
|
36
|
+
.filter((line) => {
|
|
37
|
+
const t = line.trim();
|
|
38
|
+
return !t.startsWith("*") && !t.startsWith("/*") && !t.startsWith("//");
|
|
39
|
+
})
|
|
40
|
+
.join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("HkProgressBar overspend sheet contract", () => {
|
|
44
|
+
it("positions the stretch as an inline-end block on the stretched track", () => {
|
|
45
|
+
const rule = ruleBlock(".hk-progress-bar-overspend");
|
|
46
|
+
expect(rule).toMatch(/position:\s*absolute/);
|
|
47
|
+
// Logical anchoring — the stretch sits at the inline end in RTL too.
|
|
48
|
+
expect(scss).not.toMatch(/\.hk-progress-bar-overspend[^{]*\{[^}]*\b(?:left|right):/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("tones the stretch muted by default and error-toned for danger", () => {
|
|
52
|
+
const block = ruleBlock(".hk-progress-bar-overspend");
|
|
53
|
+
expect(block).toMatch(/&-muted\s*\{[^}]*background:\s*rgb\(var\(--color-muted\)\)/);
|
|
54
|
+
expect(block).toMatch(/&-danger\s*\{[^}]*background:\s*rgb\(var\(--color-error\)\)/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("squares both sides of the 1px seam so it reads as a divider", () => {
|
|
58
|
+
const block = ruleBlock(".hk-progress-bar-overspend");
|
|
59
|
+
expect(block).toMatch(/border-start-start-radius:\s*0/);
|
|
60
|
+
expect(block).toMatch(/border-end-start-radius:\s*0/);
|
|
61
|
+
const fill = ruleBlock(".hk-progress-bar-track--overspend .hk-progress-bar-fill");
|
|
62
|
+
expect(fill).toMatch(/border-start-end-radius:\s*0/);
|
|
63
|
+
expect(fill).toMatch(/border-end-end-radius:\s*0/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("moves with the family cadence and stops under reduced motion", () => {
|
|
67
|
+
const block = ruleBlock(".hk-progress-bar-overspend");
|
|
68
|
+
expect(block).toMatch(/transition:[\s\S]*width var\(--duration-normal\)/);
|
|
69
|
+
const media = /@media \(prefers-reduced-motion: reduce\) \{([\s\S]*?)\n\}/.exec(scss);
|
|
70
|
+
expect(media, "reduced-motion guard").not.toBeNull();
|
|
71
|
+
expect(media![1]!).toMatch(/\.hk-progress-bar-overspend[\s\S]*transition:\s*none/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("co-locates the caption with the value label row", () => {
|
|
75
|
+
const rule = ruleBlock(".hk-progress-bar-overspend-label");
|
|
76
|
+
expect(rule).toMatch(/margin-inline-start:\s*var\(--space-6\)/);
|
|
77
|
+
expect(rule).toMatch(/&-danger\s*\{[^}]*color:\s*rgb\(var\(--color-error\)\)/);
|
|
78
|
+
});
|
|
79
|
+
});
|