@celestia-island/hikari 0.55.273 → 0.55.275
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/HkBarChart.scss +28 -2
- package/src/components/HkBarChart.test.tsx +126 -0
- package/src/components/HkBarChart.tsx +119 -20
- package/src/components/HkPersistentToast.scss +4 -0
- package/src/components/HkPersistentToast.test.tsx +9 -0
- package/src/components/HkPersistentToast.tsx +4 -2
- package/src/components/HkTrendChart.test.tsx +307 -1
- package/src/components/HkTrendChart.tsx +58 -6
- package/src/components/barChart.ts +12 -3
- package/src/components/trendGeometry.ts +330 -12
package/package.json
CHANGED
|
@@ -40,15 +40,41 @@
|
|
|
40
40
|
font-variant-numeric: tabular-nums;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// Above-bar value label (showValues) — the same 10-unit voice as the
|
|
44
|
+
// axis, a step stronger so the count reads first.
|
|
45
|
+
&__value {
|
|
46
|
+
fill: rgb(var(--color-text) / 0.75);
|
|
47
|
+
font-size: 10px;
|
|
48
|
+
text-anchor: middle;
|
|
49
|
+
font-variant-numeric: tabular-nums;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Slot mark (a bar's icon — the supership Star): a nested <svg> whose
|
|
53
|
+
// ink follows currentColor, so lucide's stroke picks up the axis voice
|
|
54
|
+
// without per-icon wiring.
|
|
55
|
+
&__xmark {
|
|
56
|
+
color: rgb(var(--color-text) / 0.55);
|
|
57
|
+
}
|
|
58
|
+
|
|
43
59
|
&__bar {
|
|
44
60
|
fill: rgb(var(--color-primary));
|
|
45
61
|
outline: none;
|
|
46
62
|
cursor: pointer;
|
|
47
|
-
transition: filter 0.16s ease;
|
|
63
|
+
transition: filter 0.16s ease, opacity 0.16s ease;
|
|
48
64
|
|
|
49
65
|
&:hover,
|
|
50
|
-
&:focus-visible
|
|
66
|
+
&:focus-visible,
|
|
67
|
+
// Dim-pair head (activeIndex): keeps the hover lift while the
|
|
68
|
+
// pointer may sit anywhere — the host pairs bars by wiring
|
|
69
|
+
// activeIndex from the hover events.
|
|
70
|
+
&[data-hot] {
|
|
51
71
|
filter: brightness(1.08) saturate(1.15);
|
|
52
72
|
}
|
|
73
|
+
|
|
74
|
+
// Dim-pair follower: every other bar recedes, same ease family as
|
|
75
|
+
// the lift so the pair breathes together.
|
|
76
|
+
&[data-dim] {
|
|
77
|
+
opacity: 0.45;
|
|
78
|
+
}
|
|
53
79
|
}
|
|
54
80
|
}
|
|
@@ -27,6 +27,10 @@ const BARS = [
|
|
|
27
27
|
{ label: "Wed", value: 0 },
|
|
28
28
|
];
|
|
29
29
|
|
|
30
|
+
/** A tiny stand-in for a lucide glyph: any icon FUNCTION draws the
|
|
31
|
+
* 24-unit box the slot-mark wrapper maps onto its 12-unit mark. */
|
|
32
|
+
const StarGlyph = () => <rect x="0" y="0" width="24" height="24" />;
|
|
33
|
+
|
|
30
34
|
describe("HkBarChart", () => {
|
|
31
35
|
it("renders one labeled bar per bucket and skips zero buckets' paths", () => {
|
|
32
36
|
const el = mount(<HkBarChart bars={BARS} />);
|
|
@@ -59,4 +63,126 @@ describe("HkBarChart", () => {
|
|
|
59
63
|
"weekly playtime",
|
|
60
64
|
);
|
|
61
65
|
});
|
|
66
|
+
|
|
67
|
+
// ── showValues ───────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
it("showValues renders formatValue above each bar and skips zero buckets", () => {
|
|
70
|
+
const el = mount(
|
|
71
|
+
<HkBarChart bars={BARS} showValues formatValue={(v) => `${Math.round(v / 3600)}h`} />,
|
|
72
|
+
);
|
|
73
|
+
const values = [...el.querySelectorAll(".hk-bar-chart__value")];
|
|
74
|
+
// The zero bucket (Wed) carries no label — the wowsp tier histogram's
|
|
75
|
+
// old labelY-null behavior: an empty slot never shows a stray "0".
|
|
76
|
+
expect(values.map((t) => t.textContent)).toEqual(["1h", "2h"]);
|
|
77
|
+
// Each label's baseline sits exactly 5 units above its bar's top —
|
|
78
|
+
// the path's y coordinates sit at odd indices ("M x0 y L x0 y… Q x y
|
|
79
|
+
// x y …"), and their minimum IS the top.
|
|
80
|
+
const topOf = (d: string) => {
|
|
81
|
+
const nums = d.split(/[\sMLQAZ]+/).filter(Boolean).map(Number);
|
|
82
|
+
return Math.min(...nums.filter((_, i) => i % 2 === 1));
|
|
83
|
+
};
|
|
84
|
+
const tops = [...el.querySelectorAll(".hk-bar-chart__bar")].map((p) =>
|
|
85
|
+
topOf(p.getAttribute("d") ?? ""),
|
|
86
|
+
);
|
|
87
|
+
expect(Number(values[0].getAttribute("y"))).toBeCloseTo(tops[0] - 5, 6);
|
|
88
|
+
expect(Number(values[1].getAttribute("y"))).toBeCloseTo(tops[1] - 5, 6);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("showValues off by default — old usage renders no value labels", () => {
|
|
92
|
+
const el = mount(<HkBarChart bars={BARS} />);
|
|
93
|
+
expect(el.querySelectorAll(".hk-bar-chart__value")).toHaveLength(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ── hover event + activeIndex dim-pair ───────────────────────────
|
|
97
|
+
|
|
98
|
+
it("emits hover on bar mouseenter / mouseleave with focus / blur parity", () => {
|
|
99
|
+
const hovers: (number | null)[] = [];
|
|
100
|
+
const el = mount(<HkBarChart bars={BARS} onHover={(i) => hovers.push(i)} />);
|
|
101
|
+
const bars = [...el.querySelectorAll(".hk-bar-chart__bar")];
|
|
102
|
+
bars[0].dispatchEvent(new Event("mouseenter"));
|
|
103
|
+
bars[1].dispatchEvent(new Event("focus"));
|
|
104
|
+
bars[1].dispatchEvent(new Event("blur"));
|
|
105
|
+
bars[0].dispatchEvent(new Event("mouseleave"));
|
|
106
|
+
expect(hovers).toEqual([0, 1, null, null]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("activeIndex carries data-hot on the paired bar and data-dim on the others", () => {
|
|
110
|
+
const el = mount(<HkBarChart bars={BARS} activeIndex={0} />);
|
|
111
|
+
const bars = [...el.querySelectorAll(".hk-bar-chart__bar")];
|
|
112
|
+
expect(bars[0].getAttribute("data-hot")).toBe("true");
|
|
113
|
+
expect(bars[0].hasAttribute("data-dim")).toBe(false);
|
|
114
|
+
expect(bars[1].getAttribute("data-dim")).toBe("true");
|
|
115
|
+
expect(bars[1].hasAttribute("data-hot")).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("defaults to no pair — no data-hot / data-dim anywhere", () => {
|
|
119
|
+
const el = mount(<HkBarChart bars={BARS} />);
|
|
120
|
+
expect(el.querySelector("[data-hot], [data-dim]")).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ── icon slot mark (the supership Star) ──────────────────────────
|
|
124
|
+
|
|
125
|
+
it("renders a resolvable icon in place of the x-label text, sized and centered", () => {
|
|
126
|
+
const el = mount(
|
|
127
|
+
<HkBarChart
|
|
128
|
+
bars={[
|
|
129
|
+
{ label: "10", value: 5, icon: StarGlyph },
|
|
130
|
+
{ label: "11", value: 7 },
|
|
131
|
+
]}
|
|
132
|
+
/>,
|
|
133
|
+
);
|
|
134
|
+
const marks = [...el.querySelectorAll(".hk-bar-chart__xmark")];
|
|
135
|
+
expect(marks).toHaveLength(1);
|
|
136
|
+
// The nested-svg wrapper maps the 24-unit lucide box onto a 12-unit
|
|
137
|
+
// mark centered on slot 0 (centerX 182) at the x-label's optical
|
|
138
|
+
// center (baselineY 196 + 4).
|
|
139
|
+
expect(marks[0].getAttribute("viewBox")).toBe("0 0 24 24");
|
|
140
|
+
expect(marks[0].getAttribute("width")).toBe("12");
|
|
141
|
+
expect(marks[0].getAttribute("height")).toBe("12");
|
|
142
|
+
expect(marks[0].getAttribute("x")).toBe("176");
|
|
143
|
+
expect(marks[0].getAttribute("y")).toBe("200");
|
|
144
|
+
// The icon swapped out that slot's text; the other slot keeps its text.
|
|
145
|
+
expect([...el.querySelectorAll(".hk-bar-chart__x")].map((t) => t.textContent)).toEqual([
|
|
146
|
+
"11",
|
|
147
|
+
]);
|
|
148
|
+
// The bar's aria string keeps the TEXT label, icon or not.
|
|
149
|
+
expect(el.querySelector(".hk-bar-chart__bar")?.getAttribute("aria-label")).toContain("10");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("normalizes icons like HkPillToggleGroup — vnode passes through, junk falls back to text", () => {
|
|
153
|
+
const el = mount(
|
|
154
|
+
<HkBarChart
|
|
155
|
+
bars={[
|
|
156
|
+
{ label: "a", value: 3, icon: <rect x="4" y="4" width="16" height="16" /> },
|
|
157
|
+
{ label: "b", value: 4, icon: "not-an-icon" },
|
|
158
|
+
]}
|
|
159
|
+
/>,
|
|
160
|
+
);
|
|
161
|
+
// The prebuilt vnode rides the mark wrapper; the junk string does not
|
|
162
|
+
// blank the slot — it falls back to the text label.
|
|
163
|
+
expect(el.querySelectorAll(".hk-bar-chart__xmark")).toHaveLength(1);
|
|
164
|
+
expect(el.querySelectorAll(".hk-bar-chart__xmark rect")).toHaveLength(1);
|
|
165
|
+
expect([...el.querySelectorAll(".hk-bar-chart__x")].map((t) => t.textContent)).toEqual(["b"]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ── back-compat: the wowsp PlaytimeView usage surface ────────────
|
|
169
|
+
|
|
170
|
+
it("old-props usage renders exactly the old surface", () => {
|
|
171
|
+
const el = mount(
|
|
172
|
+
<HkBarChart
|
|
173
|
+
bars={BARS.map((b) => ({ label: b.label, value: b.value, hint: b.hint }))}
|
|
174
|
+
width={480}
|
|
175
|
+
formatValue={(v) => `${Math.round(v / 3600)}h`}
|
|
176
|
+
/>,
|
|
177
|
+
);
|
|
178
|
+
expect(el.querySelectorAll(".hk-bar-chart__bar")).toHaveLength(2);
|
|
179
|
+
expect(el.querySelectorAll(".hk-bar-chart__value")).toHaveLength(0);
|
|
180
|
+
expect(el.querySelectorAll(".hk-bar-chart__xmark")).toHaveLength(0);
|
|
181
|
+
expect(el.querySelector("[data-hot], [data-dim]")).toBeNull();
|
|
182
|
+
expect([...el.querySelectorAll(".hk-bar-chart__x")].map((t) => t.textContent)).toEqual([
|
|
183
|
+
"Mon",
|
|
184
|
+
"Tue",
|
|
185
|
+
"Wed",
|
|
186
|
+
]);
|
|
187
|
+
});
|
|
62
188
|
});
|
|
@@ -1,8 +1,43 @@
|
|
|
1
|
-
import { computed, defineComponent, type PropType } from "vue";
|
|
1
|
+
import { computed, defineComponent, h, type PropType, type VNode } from "vue";
|
|
2
2
|
|
|
3
3
|
import { barLayout, type BarSlot } from "./barChart";
|
|
4
4
|
import "./HkBarChart.scss";
|
|
5
5
|
|
|
6
|
+
/** One bar entry. */
|
|
7
|
+
export interface HkBarChartBar {
|
|
8
|
+
/** Text label under the slot — also the bar's aria string, even when
|
|
9
|
+
* an `icon` replaces the visible text. */
|
|
10
|
+
label: string;
|
|
11
|
+
value: number;
|
|
12
|
+
/** Hover/focus hint; defaults to `label: formatValue(value)`. */
|
|
13
|
+
hint?: string;
|
|
14
|
+
/** Optional slot mark rendered IN PLACE of the x-label text (the tier
|
|
15
|
+
* histogram's supership Star). Accepts EITHER a component (a lucide
|
|
16
|
+
* icon function, a defineComponent options object) or a prebuilt
|
|
17
|
+
* vnode — normalized exactly like HkPillToggleGroup's icon handling;
|
|
18
|
+
* anything else falls back to the text label (never a blank slot). */
|
|
19
|
+
icon?: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Normalize a bar icon to a renderable vnode — the HkPillToggleGroup
|
|
23
|
+
* convention verbatim: a component (function or options object) is
|
|
24
|
+
* instantiated with `h`; a prebuilt vnode passes through; anything
|
|
25
|
+
* else (string, number, null) renders nothing, and the caller falls
|
|
26
|
+
* back to the text label. */
|
|
27
|
+
function resolveIcon(icon: unknown): VNode | null {
|
|
28
|
+
if (icon == null) return null;
|
|
29
|
+
if (typeof icon === "object") return icon as VNode;
|
|
30
|
+
if (typeof icon === "function") return h(icon as Parameters<typeof h>[0]);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The slot-mark box (wowsp's tier-star convention): lucide glyphs draw
|
|
35
|
+
* in a 24×24 box, so the wrapper nested <svg> maps that box onto a
|
|
36
|
+
* 12-unit mark centered on the x-label's optical center — the 10-unit
|
|
37
|
+
* text's baseline is baselineY + 14 and its glyph box spans
|
|
38
|
+
* ≈ baselineY + 6 … + 14, so a mark from +4 to +16 shares its center. */
|
|
39
|
+
const BAR_MARK = { glyphBox: 24, size: 12, top: 4 } as const;
|
|
40
|
+
|
|
6
41
|
/**
|
|
7
42
|
* HkBarChart — one bar per bucket over a hand-drawn declarative SVG (no
|
|
8
43
|
* chart library; the HkTreemap/HkCalendarHeatmap dependency-free line).
|
|
@@ -16,6 +51,15 @@ import "./HkBarChart.scss";
|
|
|
16
51
|
* wowsp delegated-tooltip hook reads — hikari itself renders no native
|
|
17
52
|
* title bubble).
|
|
18
53
|
*
|
|
54
|
+
* The tier histogram's needs ride along as OPTIONAL props (wowsp
|
|
55
|
+
* ShipDistCharts): `showValues` paints formatValue(value) above each
|
|
56
|
+
* bar (empty slots carry no label), `hover` + `activeIndex` implement a
|
|
57
|
+
* dim-pair — the HOST wires activeIndex from the hover events, the bar
|
|
58
|
+
* at activeIndex carries `data-hot` and every other bar `data-dim` —
|
|
59
|
+
* and a bar's `icon` replaces its x-label text (the supership slot's
|
|
60
|
+
* nested-svg Star, positioned/sized here from the 24-unit lucide box).
|
|
61
|
+
* Defaults reproduce the old output exactly.
|
|
62
|
+
*
|
|
19
63
|
* Dumb by design: the caller owns aggregation and passes the settled
|
|
20
64
|
* buckets in. All geometry lives in ./barChart (pure, pinned by its own
|
|
21
65
|
* tests).
|
|
@@ -27,7 +71,7 @@ export default defineComponent({
|
|
|
27
71
|
props: {
|
|
28
72
|
/** The bars, in axis order (oldest → newest for trend charts). */
|
|
29
73
|
bars: {
|
|
30
|
-
type: Array as PropType<
|
|
74
|
+
type: Array as PropType<HkBarChartBar[]>,
|
|
31
75
|
required: true,
|
|
32
76
|
},
|
|
33
77
|
/** Frame size (the SVG viewBox; CSS scales it). */
|
|
@@ -42,10 +86,25 @@ export default defineComponent({
|
|
|
42
86
|
ariaLabel: { type: String, default: null },
|
|
43
87
|
/** Rendered when every bucket is zero / the array is empty. */
|
|
44
88
|
emptyText: { type: String, default: "No data" },
|
|
89
|
+
/** Paint formatValue(value) as a small label ABOVE each bar. Skip
|
|
90
|
+
* rule: bars with value <= 0 carry NO label (their slot stays an
|
|
91
|
+
* empty slot) — the wowsp tier histogram's old labelY-null behavior,
|
|
92
|
+
* so an unplayed bucket never shows a stray "0". */
|
|
93
|
+
showValues: { type: Boolean, default: false },
|
|
94
|
+
/** The dim-pair head: when set, the bar at this index carries
|
|
95
|
+
* `data-hot` (kept hover lift) and every other bar carries
|
|
96
|
+
* `data-dim` (≈0.45 opacity). The HOST implements the pair by
|
|
97
|
+
* wiring this from the `hover` events; null (the default) draws no
|
|
98
|
+
* pair at all. */
|
|
99
|
+
activeIndex: { type: Number as PropType<number | null>, default: null },
|
|
45
100
|
},
|
|
46
101
|
emits: {
|
|
47
102
|
/** A bar was clicked (its bucket index). */
|
|
48
103
|
select: (index: number) => typeof index === "number",
|
|
104
|
+
/** The pointer entered a bar (its bucket index) or left it / the
|
|
105
|
+
* keyboard focus moved onto or off one (focus/blur parity — null
|
|
106
|
+
* when the hover ended). */
|
|
107
|
+
hover: (index: number | null) => typeof index === "number" || index === null,
|
|
49
108
|
},
|
|
50
109
|
setup(props, { emit }) {
|
|
51
110
|
const layout = computed(() =>
|
|
@@ -60,7 +119,12 @@ export default defineComponent({
|
|
|
60
119
|
props.ariaLabel ??
|
|
61
120
|
props.bars.map((b) => `${b.label} ${props.formatValue(b.value)}`).join(", "),
|
|
62
121
|
);
|
|
63
|
-
|
|
122
|
+
/** The dim-pair head, normalized: anything but a number (undefined,
|
|
123
|
+
* null, garbage) means "no pair". */
|
|
124
|
+
const active = computed(() =>
|
|
125
|
+
typeof props.activeIndex === "number" ? props.activeIndex : null,
|
|
126
|
+
);
|
|
127
|
+
const hintOf = (b: HkBarChartBar, i: number): string =>
|
|
64
128
|
b.hint ?? `${b.label}: ${props.formatValue(b.value)}`;
|
|
65
129
|
|
|
66
130
|
return () => {
|
|
@@ -90,23 +154,58 @@ export default defineComponent({
|
|
|
90
154
|
</text>
|
|
91
155
|
</g>
|
|
92
156
|
))}
|
|
93
|
-
{l.bars.map((s: BarSlot, i: number) =>
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
157
|
+
{l.bars.map((s: BarSlot, i: number) => {
|
|
158
|
+
const bar = props.bars[i];
|
|
159
|
+
const mark = resolveIcon(bar.icon);
|
|
160
|
+
return (
|
|
161
|
+
<g key={i}>
|
|
162
|
+
{s.path ? (
|
|
163
|
+
<path
|
|
164
|
+
class="hk-bar-chart__bar"
|
|
165
|
+
d={s.path}
|
|
166
|
+
data-hint={hintOf(bar, i)}
|
|
167
|
+
aria-label={hintOf(bar, i)}
|
|
168
|
+
tabindex={0}
|
|
169
|
+
data-hot={active.value === i || undefined}
|
|
170
|
+
data-dim={(active.value !== null && active.value !== i) || undefined}
|
|
171
|
+
onClick={() => emit("select", i)}
|
|
172
|
+
onMouseenter={() => emit("hover", i)}
|
|
173
|
+
onMouseleave={() => emit("hover", null)}
|
|
174
|
+
onFocus={() => emit("hover", i)}
|
|
175
|
+
onBlur={() => emit("hover", null)}
|
|
176
|
+
/>
|
|
177
|
+
) : null}
|
|
178
|
+
{props.showValues && s.labelY != null ? (
|
|
179
|
+
<text class="hk-bar-chart__value" x={s.centerX} y={s.labelY}>
|
|
180
|
+
{props.formatValue(bar.value)}
|
|
181
|
+
</text>
|
|
182
|
+
) : null}
|
|
183
|
+
{mark != null ? (
|
|
184
|
+
// The slot mark (the supership's Star): a nested <svg>
|
|
185
|
+
// wrapper maps the icon's 24-unit glyph box onto a
|
|
186
|
+
// 12-unit mark centered on the x-label's optical
|
|
187
|
+
// center — positioned HERE so the consumer passes a
|
|
188
|
+
// bare lucide component and knows no geometry. Ink
|
|
189
|
+
// (currentColor) lives in SCSS.
|
|
190
|
+
<svg
|
|
191
|
+
class="hk-bar-chart__xmark"
|
|
192
|
+
x={s.centerX - BAR_MARK.size / 2}
|
|
193
|
+
y={l.baselineY + BAR_MARK.top}
|
|
194
|
+
width={BAR_MARK.size}
|
|
195
|
+
height={BAR_MARK.size}
|
|
196
|
+
viewBox={`0 0 ${BAR_MARK.glyphBox} ${BAR_MARK.glyphBox}`}
|
|
197
|
+
aria-hidden="true"
|
|
198
|
+
>
|
|
199
|
+
{mark}
|
|
200
|
+
</svg>
|
|
201
|
+
) : (
|
|
202
|
+
<text class="hk-bar-chart__x" x={s.centerX} y={l.baselineY + 14}>
|
|
203
|
+
{bar.label}
|
|
204
|
+
</text>
|
|
205
|
+
)}
|
|
206
|
+
</g>
|
|
207
|
+
);
|
|
208
|
+
})}
|
|
110
209
|
</svg>
|
|
111
210
|
);
|
|
112
211
|
};
|
|
@@ -65,6 +65,10 @@ button.hk-persistent-toast {
|
|
|
65
65
|
color: rgb(var(--color-muted));
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
.hk-persistent-toast--success .hk-persistent-toast__icon {
|
|
69
|
+
color: rgb(var(--color-success));
|
|
70
|
+
}
|
|
71
|
+
|
|
68
72
|
.hk-persistent-toast__label {
|
|
69
73
|
overflow: hidden;
|
|
70
74
|
text-overflow: ellipsis;
|
|
@@ -111,6 +111,15 @@ describe("HkPersistentToast inert default", () => {
|
|
|
111
111
|
expect(container.querySelector(".hk-persistent-toast__spinner")).not.toBeNull();
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
it("renders a static icon for the success tone", () => {
|
|
115
|
+
const { container } = mount({ props: { tone: "success", label: "Saved" } });
|
|
116
|
+
const c = chip(container);
|
|
117
|
+
expect(c.classList.contains("hk-persistent-toast--success")).toBe(true);
|
|
118
|
+
// Success renders a static lucide icon like the other non-loading tones.
|
|
119
|
+
expect(container.querySelector(".hk-persistent-toast__spinner")).toBeNull();
|
|
120
|
+
expect(c.querySelector("svg")).not.toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
114
123
|
it("renders the ✕ sibling button only when dismissible", () => {
|
|
115
124
|
const onDismiss = vi.fn();
|
|
116
125
|
const { container } = mount({ props: { label: "x", dismissible: true }, onDismiss });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AlertTriangle, Info, X } from "lucide-vue-next";
|
|
1
|
+
import { AlertTriangle, Check, Info, X } from "lucide-vue-next";
|
|
2
2
|
import {
|
|
3
3
|
computed,
|
|
4
4
|
defineComponent,
|
|
@@ -15,7 +15,7 @@ import { useI18n } from "../i18n/context";
|
|
|
15
15
|
import "./HkPersistentToast.scss";
|
|
16
16
|
import { ensureOverlayRoot } from "../overlay/overlayRoot";
|
|
17
17
|
|
|
18
|
-
export type PersistentToastTone = "loading" | "info" | "warning" | "error";
|
|
18
|
+
export type PersistentToastTone = "loading" | "info" | "warning" | "error" | "success";
|
|
19
19
|
|
|
20
20
|
/** Viewport gutter the detail card clamps into (mirror of
|
|
21
21
|
* --viewport-gutter's desktop value; the card is chrome-adjacent and
|
|
@@ -251,6 +251,8 @@ export default defineComponent({
|
|
|
251
251
|
<span class="hk-persistent-toast__spinner" aria-hidden="true" />
|
|
252
252
|
) : props.tone === "error" || props.tone === "warning" ? (
|
|
253
253
|
<AlertTriangle size={12} class="hk-persistent-toast__icon" aria-hidden="true" />
|
|
254
|
+
) : props.tone === "success" ? (
|
|
255
|
+
<Check size={12} class="hk-persistent-toast__icon" aria-hidden="true" />
|
|
254
256
|
) : (
|
|
255
257
|
<Info size={12} class="hk-persistent-toast__icon" aria-hidden="true" />
|
|
256
258
|
)}
|