@celestia-island/hikari 0.55.163 → 0.55.165
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/HkCornerBadge.scss +132 -0
- package/src/components/HkCornerBadge.styles.test.ts +61 -0
- package/src/components/HkCornerBadge.test.tsx +86 -0
- package/src/components/HkCornerBadge.tsx +91 -0
- package/src/components/HkFilterOperatorIcon.tsx +69 -0
- package/src/components/HkFilterPanel.scss +8 -0
- package/src/components/HkFilterPanel.styles.test.ts +6 -0
- package/src/components/HkFilterPanel.test.tsx +228 -161
- package/src/components/HkFilterPanel.tsx +206 -114
- package/src/components/HkFilterTrigger.mobile.test.tsx +1 -1
- package/src/components/HkFilterTrigger.scss +5 -18
- package/src/components/HkFilterTrigger.test.tsx +28 -27
- package/src/components/HkFilterTrigger.tsx +25 -22
- package/src/components/HkSearchTrigger.scss +6 -17
- package/src/components/HkSearchTrigger.test.tsx +2 -2
- package/src/components/HkSearchTrigger.tsx +6 -1
- package/src/i18n/locales/ar/components.json +1 -0
- package/src/i18n/locales/de/components.json +1 -0
- package/src/i18n/locales/en/components.json +1 -0
- package/src/i18n/locales/es/components.json +1 -0
- package/src/i18n/locales/fr/components.json +1 -0
- package/src/i18n/locales/ja/components.json +1 -0
- package/src/i18n/locales/ko/components.json +1 -0
- package/src/i18n/locales/pt/components.json +1 -0
- package/src/i18n/locales/ru/components.json +1 -0
- package/src/i18n/locales/zh-Hans/components.json +1 -0
- package/src/i18n/locales/zh-Hant/components.json +1 -0
- package/src/index.ts +4 -1
package/package.json
CHANGED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HkCornerBadge — the generic corner mark (dot / count / label) pinned
|
|
3
|
+
* to one corner of the default-slot anchor (see HkCornerBadge.tsx for
|
|
4
|
+
* the contract). Geometry follows the house pattern the trigger dots
|
|
5
|
+
* and the Rust family's .hk-badge-wrapper established: the anchor is a
|
|
6
|
+
* relative inline-flex wrapper (the badge escapes nothing — hosts wrap
|
|
7
|
+
* ANY child), the mark centers ON the corner via translate(50%,-50%)
|
|
8
|
+
* with an optional --hk-corner-badge-offset clearance.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
.hk-corner-badge-anchor {
|
|
12
|
+
position: relative;
|
|
13
|
+
display: inline-flex;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.hk-corner-badge {
|
|
17
|
+
position: absolute;
|
|
18
|
+
/* Centered on the corner; the corner data-attribute flips the axes
|
|
19
|
+
* and the offset var adds clearance without re-deriving transforms. */
|
|
20
|
+
transform: translate(var(--hk-corner-badge-tx, 50%), var(--hk-corner-badge-ty, -50%));
|
|
21
|
+
pointer-events: none;
|
|
22
|
+
white-space: nowrap;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.hk-corner-badge[data-corner="top-right"] {
|
|
26
|
+
top: var(--hk-corner-badge-offset, 0px);
|
|
27
|
+
right: var(--hk-corner-badge-offset, 0px);
|
|
28
|
+
--hk-corner-badge-tx: 50%;
|
|
29
|
+
--hk-corner-badge-ty: -50%;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.hk-corner-badge[data-corner="top-left"] {
|
|
33
|
+
top: var(--hk-corner-badge-offset, 0px);
|
|
34
|
+
left: var(--hk-corner-badge-offset, 0px);
|
|
35
|
+
--hk-corner-badge-tx: -50%;
|
|
36
|
+
--hk-corner-badge-ty: -50%;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.hk-corner-badge[data-corner="bottom-right"] {
|
|
40
|
+
bottom: var(--hk-corner-badge-offset, 0px);
|
|
41
|
+
right: var(--hk-corner-badge-offset, 0px);
|
|
42
|
+
--hk-corner-badge-tx: 50%;
|
|
43
|
+
--hk-corner-badge-ty: 50%;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.hk-corner-badge[data-corner="bottom-left"] {
|
|
47
|
+
bottom: var(--hk-corner-badge-offset, 0px);
|
|
48
|
+
left: var(--hk-corner-badge-offset, 0px);
|
|
49
|
+
--hk-corner-badge-tx: -50%;
|
|
50
|
+
--hk-corner-badge-ty: 50%;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* Dot form: a filled circle in the variant's tone (same token trio
|
|
54
|
+
* HkBadge uses — variant classes below set all three channels). */
|
|
55
|
+
.hk-corner-badge--dot {
|
|
56
|
+
width: 8px;
|
|
57
|
+
height: 8px;
|
|
58
|
+
border-radius: 50%;
|
|
59
|
+
background: var(--hk-badge-bg);
|
|
60
|
+
box-shadow: 0 0 0 1.5px rgb(var(--color-surface) / 85%);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Pill form: the compact count/label chip (18px min, text-4xs mono
|
|
64
|
+
* digits — notification-badge geometry across the family). */
|
|
65
|
+
.hk-corner-badge--pill {
|
|
66
|
+
min-width: 18px;
|
|
67
|
+
height: 18px;
|
|
68
|
+
padding: 0 5px;
|
|
69
|
+
display: inline-flex;
|
|
70
|
+
align-items: center;
|
|
71
|
+
justify-content: center;
|
|
72
|
+
border-radius: var(--radius-full, 999px);
|
|
73
|
+
background: var(--hk-badge-bg);
|
|
74
|
+
color: var(--hk-badge-text);
|
|
75
|
+
border: 1px solid var(--hk-badge-border);
|
|
76
|
+
font-size: var(--text-4xs, 0.625rem);
|
|
77
|
+
font-weight: 700;
|
|
78
|
+
line-height: 1;
|
|
79
|
+
font-variant-numeric: tabular-nums;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Variant tones — the same channel triplets HkBadge.scss ships; kept
|
|
83
|
+
* literal here so the corner badge owns its own (tiny) sheet. */
|
|
84
|
+
.hk-corner-badge-default {
|
|
85
|
+
--hk-badge-bg: rgb(var(--color-primary) / 12%);
|
|
86
|
+
--hk-badge-text: rgb(var(--color-text));
|
|
87
|
+
--hk-badge-border: rgb(var(--color-primary) / 35%);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.hk-corner-badge-primary {
|
|
91
|
+
--hk-badge-bg: rgb(var(--color-primary));
|
|
92
|
+
--hk-badge-text: rgb(var(--color-on-primary, 255 255 255));
|
|
93
|
+
--hk-badge-border: transparent;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.hk-corner-badge-success {
|
|
97
|
+
--hk-badge-bg: rgb(var(--color-success));
|
|
98
|
+
--hk-badge-text: rgb(var(--color-on-primary, 255 255 255));
|
|
99
|
+
--hk-corner-badge-ring: rgb(var(--color-surface) / 85%);
|
|
100
|
+
--hk-badge-border: transparent;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.hk-corner-badge-error {
|
|
104
|
+
--hk-badge-bg: rgb(var(--color-error));
|
|
105
|
+
--hk-badge-text: rgb(var(--color-on-primary, 255 255 255));
|
|
106
|
+
--hk-badge-border: transparent;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.hk-corner-badge-warning {
|
|
110
|
+
--hk-badge-bg: rgb(var(--color-warning));
|
|
111
|
+
--hk-badge-text: rgb(var(--color-text));
|
|
112
|
+
--hk-badge-border: transparent;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.hk-corner-badge-info {
|
|
116
|
+
--hk-badge-bg: rgb(var(--color-info, var(--color-primary)));
|
|
117
|
+
--hk-badge-text: rgb(var(--color-on-primary, 255 255 255));
|
|
118
|
+
--hk-badge-border: transparent;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.hk-corner-badge-muted {
|
|
122
|
+
--hk-badge-bg: rgb(var(--color-text) / 55%);
|
|
123
|
+
--hk-badge-text: rgb(var(--color-surface));
|
|
124
|
+
--hk-badge-border: transparent;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* The success dot's ring token feeds the dot form's surface cutout so
|
|
128
|
+
* the mark reads ON the anchor, not under it (the icon-group marker's
|
|
129
|
+
* trick, generalized). */
|
|
130
|
+
.hk-corner-badge-success.hk-corner-badge--dot {
|
|
131
|
+
box-shadow: 0 0 0 1.5px var(--hk-corner-badge-ring, rgb(var(--color-surface) / 85%));
|
|
132
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { compile } from "sass";
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Compiled-stylesheet guard for HkCornerBadge — the anchoring contract
|
|
10
|
+
* (2026-10-15): the anchor wrapper is a relative inline-flex host (any
|
|
11
|
+
* child can carry the mark), the mark centers ON its corner via
|
|
12
|
+
* translate(±50%,±50%), and each data-corner flips the right axes.
|
|
13
|
+
* Vitest stubs CSS, so the runtime suite can never catch a lost rule.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const componentDir = resolve(dirname(fileURLToPath(import.meta.url)));
|
|
17
|
+
|
|
18
|
+
const css = compile(resolve(componentDir, "HkCornerBadge.scss"), {
|
|
19
|
+
style: "expanded",
|
|
20
|
+
}).css;
|
|
21
|
+
|
|
22
|
+
function ruleFor(selector: string): string {
|
|
23
|
+
const start = css.indexOf(selector);
|
|
24
|
+
expect(start, `${selector} present in the compiled sheet`).toBeGreaterThanOrEqual(0);
|
|
25
|
+
const open = css.indexOf("{", start);
|
|
26
|
+
const close = css.indexOf("}", open);
|
|
27
|
+
return css.slice(start, close + 1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("HkCornerBadge stylesheet contract", () => {
|
|
31
|
+
it("compiles", () => {
|
|
32
|
+
expect(css).toContain(".hk-corner-badge");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("the anchor is a relative inline-flex wrapper (any child carries the mark)", () => {
|
|
36
|
+
const rule = ruleFor(".hk-corner-badge-anchor {");
|
|
37
|
+
expect(rule).toContain("position: relative");
|
|
38
|
+
expect(rule).toContain("display: inline-flex");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("the mark never takes pointer events and never wraps", () => {
|
|
42
|
+
const rule = ruleFor(".hk-corner-badge {");
|
|
43
|
+
expect(rule).toContain("position: absolute");
|
|
44
|
+
expect(rule).toContain("pointer-events: none");
|
|
45
|
+
expect(rule).toContain("white-space: nowrap");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("each corner flips the translate axes", () => {
|
|
49
|
+
expect(ruleFor('.hk-corner-badge[data-corner=top-right] {')).toContain("--hk-corner-badge-tx: 50%");
|
|
50
|
+
expect(ruleFor('.hk-corner-badge[data-corner=top-left] {')).toContain("--hk-corner-badge-tx: -50%");
|
|
51
|
+
expect(ruleFor('.hk-corner-badge[data-corner=bottom-right] {')).toContain("--hk-corner-badge-ty: 50%");
|
|
52
|
+
expect(ruleFor('.hk-corner-badge[data-corner=bottom-left] {')).toContain("--hk-corner-badge-tx: -50%");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("the dot is round and the pill is a tabular-numbers chip", () => {
|
|
56
|
+
const dot = ruleFor(".hk-corner-badge--dot {");
|
|
57
|
+
expect(dot).toContain("border-radius: 50%");
|
|
58
|
+
const pill = ruleFor(".hk-corner-badge--pill {");
|
|
59
|
+
expect(pill).toContain("font-variant-numeric: tabular-nums");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { createApp, h } from "vue";
|
|
3
|
+
|
|
4
|
+
import HkCornerBadge from "./HkCornerBadge";
|
|
5
|
+
|
|
6
|
+
const mounts: Array<{ app: ReturnType<typeof createApp>; container: HTMLElement }> = [];
|
|
7
|
+
|
|
8
|
+
function mount(node: ReturnType<typeof h>) {
|
|
9
|
+
const container = document.createElement("div");
|
|
10
|
+
document.body.appendChild(container);
|
|
11
|
+
const app = createApp({ render: () => node });
|
|
12
|
+
app.mount(container);
|
|
13
|
+
mounts.push({ app, container });
|
|
14
|
+
return container;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
for (const { app, container } of mounts.splice(0)) {
|
|
19
|
+
app.unmount();
|
|
20
|
+
container.remove();
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("HkCornerBadge — the generic corner mark (2026-10-15)", () => {
|
|
25
|
+
it("anchors the default slot in a relative wrapper with no mark by default", () => {
|
|
26
|
+
const c = mount(h(HkCornerBadge, null, () => h("button", { class: "probe" }, "x")));
|
|
27
|
+
const anchor = c.querySelector(".hk-corner-badge-anchor")!;
|
|
28
|
+
expect(anchor, "anchor wrapper present").toBeTruthy();
|
|
29
|
+
// (The relative/inline-flex anchoring is pinned by the compiled-css
|
|
30
|
+
// contract test — vitest stubs styles, so no computed assertions.)
|
|
31
|
+
expect(c.querySelector(".probe"), "slot content rendered").toBeTruthy();
|
|
32
|
+
expect(c.querySelector(".hk-corner-badge")).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("dot form: a variant-colored dot at the requested corner, decorative by default", () => {
|
|
36
|
+
const c = mount(
|
|
37
|
+
h(HkCornerBadge, { dot: true, variant: "success", corner: "bottom-right" }, () => h("i")),
|
|
38
|
+
);
|
|
39
|
+
const badge = c.querySelector(".hk-corner-badge")!;
|
|
40
|
+
expect(badge).toBeTruthy();
|
|
41
|
+
expect(badge.className).toContain("hk-corner-badge--dot");
|
|
42
|
+
expect(badge.className).toContain("hk-corner-badge-success");
|
|
43
|
+
expect(badge.getAttribute("data-corner")).toBe("bottom-right");
|
|
44
|
+
expect(badge.getAttribute("aria-hidden")).toBe("true");
|
|
45
|
+
expect(badge.getAttribute("role")).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("count form: renders the number, caps past max, hides zero unless showZero", () => {
|
|
49
|
+
const c = mount(h(HkCornerBadge, { count: 7 }, () => h("i")));
|
|
50
|
+
expect(c.querySelector(".hk-corner-badge")!.textContent).toBe("7");
|
|
51
|
+
const capped = mount(h(HkCornerBadge, { count: 130, max: 99 }, () => h("i")));
|
|
52
|
+
expect(capped.querySelector(".hk-corner-badge")!.textContent).toBe("99+");
|
|
53
|
+
const zero = mount(h(HkCornerBadge, { count: 0 }, () => h("i")));
|
|
54
|
+
expect(zero.querySelector(".hk-corner-badge")).toBeNull();
|
|
55
|
+
const zeroShown = mount(h(HkCornerBadge, { count: 0, showZero: true }, () => h("i")));
|
|
56
|
+
expect(zeroShown.querySelector(".hk-corner-badge")!.textContent).toBe("0");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("custom label form: a text pill from the default slot", () => {
|
|
60
|
+
const c = mount(h(HkCornerBadge, { variant: "primary", text: "5h" }, () => h("i")));
|
|
61
|
+
const badge = c.querySelector(".hk-corner-badge")!;
|
|
62
|
+
expect(badge.textContent).toBe("5h");
|
|
63
|
+
expect(badge.className).toContain("hk-corner-badge--pill");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("accessible mode: role=img + aria-label instead of aria-hidden", () => {
|
|
67
|
+
const c = mount(h(HkCornerBadge, { dot: true, label: "3 active filters" }, () => h("i")));
|
|
68
|
+
const badge = c.querySelector(".hk-corner-badge")!;
|
|
69
|
+
expect(badge.getAttribute("role")).toBe("img");
|
|
70
|
+
expect(badge.getAttribute("aria-label")).toBe("3 active filters");
|
|
71
|
+
expect(badge.getAttribute("aria-hidden")).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("dot wins when both dot and count/text are set", () => {
|
|
75
|
+
const c = mount(h(HkCornerBadge, { dot: true, count: 9 }, () => h("i")));
|
|
76
|
+
const badge = c.querySelector(".hk-corner-badge")!;
|
|
77
|
+
expect(badge.className).toContain("hk-corner-badge--dot");
|
|
78
|
+
expect(badge.textContent).toBe("");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("offset writes the clearance custom property", () => {
|
|
82
|
+
const c = mount(h(HkCornerBadge, { dot: true, offset: 2 }, () => h("i")));
|
|
83
|
+
const badge = c.querySelector(".hk-corner-badge") as HTMLElement;
|
|
84
|
+
expect(badge.style.getPropertyValue("--hk-corner-badge-offset")).toBe("2px");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { computed, defineComponent, type PropType } from "vue";
|
|
2
|
+
|
|
3
|
+
import "./HkCornerBadge.scss";
|
|
4
|
+
|
|
5
|
+
/** The color channels HkBadge already standardizes (same variant names,
|
|
6
|
+
* same override escape hatch — kept in lockstep with HkBadge.tsx). */
|
|
7
|
+
export type CornerBadgeVariant =
|
|
8
|
+
| "default"
|
|
9
|
+
| "primary"
|
|
10
|
+
| "success"
|
|
11
|
+
| "error"
|
|
12
|
+
| "warning"
|
|
13
|
+
| "info"
|
|
14
|
+
| "muted";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* HkCornerBadge — the generic corner mark for ANY anchor (user
|
|
18
|
+
* direction 2026-10-15): a green/red/traffic dot, a notification count
|
|
19
|
+
* ("9+"), or a custom text label pinned to one corner of whatever sits
|
|
20
|
+
* in the default slot. The Vue package had every consumer hand-rolling
|
|
21
|
+
* its own absolute span (the filter/search trigger dots, the icon-group
|
|
22
|
+
* marker, chest's dock time-range pill); this is the one component they
|
|
23
|
+
* converge on — API parity with the Rust family's badge.rs (dot / count
|
|
24
|
+
* / max "N+" / showZero), plus the corner + custom-label the web needs.
|
|
25
|
+
*
|
|
26
|
+
* Decorative by default (aria-hidden): the mark VISUALIZES state the
|
|
27
|
+
* anchor already exposes (a filtered trigger, a selected range); pass
|
|
28
|
+
* `label` when it is the only place a count is announced.
|
|
29
|
+
*/
|
|
30
|
+
export default defineComponent({
|
|
31
|
+
name: "HkCornerBadge",
|
|
32
|
+
props: {
|
|
33
|
+
variant: { type: String as PropType<CornerBadgeVariant>, default: "primary" },
|
|
34
|
+
/** Bare dot form (the green/red/traffic light). */
|
|
35
|
+
dot: { type: Boolean, default: false },
|
|
36
|
+
/** Count form: renders the number, or "max+" past `max`. Hidden at
|
|
37
|
+
* 0 unless showZero (notification semantics). */
|
|
38
|
+
count: { type: Number, default: undefined },
|
|
39
|
+
max: { type: Number, default: undefined },
|
|
40
|
+
showZero: { type: Boolean, default: false },
|
|
41
|
+
/** Custom label text (the pill form). The default slot is the
|
|
42
|
+
* ANCHOR, never the label — overloading it made every anchored
|
|
43
|
+
* child's own text leak into the badge. */
|
|
44
|
+
text: { type: String, default: undefined },
|
|
45
|
+
/** Which corner of the anchor. */
|
|
46
|
+
corner: {
|
|
47
|
+
type: String as PropType<"top-right" | "top-left" | "bottom-right" | "bottom-left">,
|
|
48
|
+
default: "top-right",
|
|
49
|
+
},
|
|
50
|
+
/** Extra clearance from the corner in px (0 = sit ON the corner,
|
|
51
|
+
* the translate(50%,-50%) centering). */
|
|
52
|
+
offset: { type: Number, default: 0 },
|
|
53
|
+
/** Accessible name for the mark (without it the badge is
|
|
54
|
+
* aria-hidden decoration). */
|
|
55
|
+
label: { type: String, default: undefined },
|
|
56
|
+
},
|
|
57
|
+
setup(props, { slots }) {
|
|
58
|
+
const text = computed<string | null>(() => {
|
|
59
|
+
if (props.count !== undefined) {
|
|
60
|
+
if (props.count === 0 && !props.showZero) return null;
|
|
61
|
+
if (props.max !== undefined && props.count > props.max) return `${props.max}+`;
|
|
62
|
+
return String(props.count);
|
|
63
|
+
}
|
|
64
|
+
return props.text && props.text.length > 0 ? props.text : null;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const show = computed(() => props.dot || text.value !== null);
|
|
68
|
+
|
|
69
|
+
return () => (
|
|
70
|
+
<span class="hk-corner-badge-anchor">
|
|
71
|
+
{slots.default?.()}
|
|
72
|
+
{show.value && (
|
|
73
|
+
<span
|
|
74
|
+
class={[
|
|
75
|
+
"hk-corner-badge",
|
|
76
|
+
`hk-corner-badge-${props.variant}`,
|
|
77
|
+
props.dot ? "hk-corner-badge--dot" : "hk-corner-badge--pill",
|
|
78
|
+
]}
|
|
79
|
+
data-corner={props.corner}
|
|
80
|
+
style={props.offset ? { "--hk-corner-badge-offset": `${props.offset}px` } : undefined}
|
|
81
|
+
aria-hidden={props.label ? undefined : "true"}
|
|
82
|
+
role={props.label ? "img" : undefined}
|
|
83
|
+
aria-label={props.label}
|
|
84
|
+
>
|
|
85
|
+
{props.dot ? null : text.value}
|
|
86
|
+
</span>
|
|
87
|
+
)}
|
|
88
|
+
</span>
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { defineComponent } from "vue";
|
|
2
|
+
import { Equal, EqualApproximately, EqualNot } from "lucide-vue-next";
|
|
3
|
+
|
|
4
|
+
import type { HkFilterOperator } from "./HkFilterPanel";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The operator glyph as an ICON, never a text character (user direction
|
|
8
|
+
* 2026-10-15: font stacks render U+2249 ≉ unevenly — the operator
|
|
9
|
+
* buttons are UI chrome, so they draw like every other icon in the
|
|
10
|
+
* family). Lucide ships =, ≠ and ≈; the ordered four and ≉ are
|
|
11
|
+
* hikari-authored in lucide's exact stroke grammar (24×24, stroke 2,
|
|
12
|
+
* currentColor) — ≉ is lucide's own ≈ path pair with EqualNot's slash
|
|
13
|
+
* composed in, so the set reads as one family.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Lucide's EqualApproximately path pair (ISC, verbatim) + its EqualNot
|
|
17
|
+
// slash — composed rather than reinvented.
|
|
18
|
+
const NAPPROX_PATHS = [
|
|
19
|
+
"M5 15a6.5 6.5 0 0 1 7 0 6.5 6.5 0 0 0 7 0",
|
|
20
|
+
"M5 9a6.5 6.5 0 0 1 7 0 6.5 6.5 0 0 0 7 0",
|
|
21
|
+
"m5 19 14-14",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const CUSTOM: Partial<Record<HkFilterOperator, string[]>> = {
|
|
25
|
+
napprox: NAPPROX_PATHS,
|
|
26
|
+
lt: ["m19 6-11 6 11 6"],
|
|
27
|
+
lte: ["m19 5-11 6 11 6", "M8 20h11"],
|
|
28
|
+
gt: ["m5 6 11 6-11 6"],
|
|
29
|
+
gte: ["m5 5 11 6-11 6", "M5 20h11"],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const LUCIDE: Partial<Record<HkFilterOperator, typeof Equal>> = {
|
|
33
|
+
eq: Equal,
|
|
34
|
+
ne: EqualNot,
|
|
35
|
+
approx: EqualApproximately,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default defineComponent({
|
|
39
|
+
name: "HkFilterOperatorIcon",
|
|
40
|
+
props: {
|
|
41
|
+
op: { type: String as () => HkFilterOperator, required: true },
|
|
42
|
+
size: { type: Number, default: 14 },
|
|
43
|
+
},
|
|
44
|
+
setup(props) {
|
|
45
|
+
return () => {
|
|
46
|
+
const Lucide = LUCIDE[props.op];
|
|
47
|
+
if (Lucide) return <Lucide size={props.size} aria-hidden="true" />;
|
|
48
|
+
const paths = CUSTOM[props.op]!;
|
|
49
|
+
return (
|
|
50
|
+
<svg
|
|
51
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
52
|
+
width={props.size}
|
|
53
|
+
height={props.size}
|
|
54
|
+
viewBox="0 0 24 24"
|
|
55
|
+
fill="none"
|
|
56
|
+
stroke="currentColor"
|
|
57
|
+
stroke-width="2"
|
|
58
|
+
stroke-linecap="round"
|
|
59
|
+
stroke-linejoin="round"
|
|
60
|
+
aria-hidden="true"
|
|
61
|
+
>
|
|
62
|
+
{paths.map((d) => (
|
|
63
|
+
<path key={d} d={d} />
|
|
64
|
+
))}
|
|
65
|
+
</svg>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
});
|
|
@@ -42,6 +42,14 @@
|
|
|
42
42
|
/* Minimum outer margins for every column (user direction
|
|
43
43
|
* 2026-10-12): the rows never touch the panel edge. */
|
|
44
44
|
padding: var(--space-8, 0.5rem) var(--space-12, 0.75rem);
|
|
45
|
+
/* Empty panel collapses to the add verb's own width (user direction
|
|
46
|
+
* 2026-10-15): no rows means nothing to align columns against, so the
|
|
47
|
+
* popover hugs the 添加条件 button instead of holding the full-row
|
|
48
|
+
* width. Desktop only — the phone branch below re-widens to 100%. */
|
|
49
|
+
&[data-empty] {
|
|
50
|
+
width: max-content;
|
|
51
|
+
min-width: 0;
|
|
52
|
+
}
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
/* A row is not a box — it donates its three cells to the panel grid so
|
|
@@ -73,6 +73,12 @@ describe("HkFilterPanel stylesheet contract", () => {
|
|
|
73
73
|
expect(rule).toContain("justify-self: start");
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
+
it("the empty panel collapses to the add verb's own width", () => {
|
|
77
|
+
// 2026-10-15: no rows = nothing to align columns against — the
|
|
78
|
+
// desktop popover hugs the 添加条件 button (data-empty marker).
|
|
79
|
+
expect(css).toMatch(/\.hk-filter-panel\[data-empty\]\s*\{[^}]*width:\s*max-content/);
|
|
80
|
+
});
|
|
81
|
+
|
|
76
82
|
it("the phone operator column keeps its widened cap (≥ the old 4.25rem)", () => {
|
|
77
83
|
expect(css).toContain("@media (max-width: 767px)");
|
|
78
84
|
const media = css.slice(css.indexOf("@media (max-width: 767px)"));
|