@celestia-island/hikari 0.55.303 → 0.55.304
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/HkAffixPicker.scss +16 -3
- package/src/components/HkDatePicker.scss +7 -2
- package/src/components/HkFileField.scss +3 -21
- package/src/components/HkFileField.tsx +9 -16
- package/src/components/HkFilterPanel.scss +22 -44
- package/src/components/HkFilterPanel.tsx +14 -10
- package/src/components/HkKeywordSearchModal.scss +3 -16
- package/src/components/HkKeywordSearchModal.tsx +20 -15
- package/src/components/HkModal.open-close-scale.test.ts +28 -18
- package/src/components/HkModal.scss +19 -19
- package/src/components/HkPasswordSurface.scss +9 -2
- package/src/components/HkSearchInput.scss +11 -4
- package/src/components/HkSelectionGrid.scss +3 -29
- package/src/components/HkSelectionGrid.tsx +7 -4
- package/src/components/HkTag.scss +4 -24
- package/src/components/HkTag.tsx +7 -4
- package/src/components/HkThemePanelContent.tsx +9 -5
- package/src/components/HkThemeToggle.scss +13 -23
- package/src/components/HkTokenInput.scss +19 -14
- package/src/components/HkTokenInput.tsx +7 -4
- package/src/components/HkUserPicker.scss +4 -17
- package/src/components/HkUserPicker.tsx +9 -4
- package/src/components/closeFamily.contract.test.tsx +44 -8
package/package.json
CHANGED
|
@@ -181,6 +181,13 @@
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
.hk-affix-tag-x {
|
|
184
|
+
/* Wave B2 hybrid decision: the × keeps its own onmousedown
|
|
185
|
+
prevent-default (tapping must not steal focus or start a selection) and
|
|
186
|
+
its 1.5rem field density, so it deliberately stays a BARE button — the
|
|
187
|
+
contract is pinned Rust-side
|
|
188
|
+
(`affix_tag_close_stays_a_bare_button_by_contract`). What standardizes
|
|
189
|
+
is the INK: it consumes the icon-button channel vars (current values as
|
|
190
|
+
first fallbacks = zero visual change). */
|
|
184
191
|
flex: none;
|
|
185
192
|
display: inline-flex;
|
|
186
193
|
align-items: center;
|
|
@@ -191,15 +198,21 @@
|
|
|
191
198
|
border: 0;
|
|
192
199
|
border-radius: var(--radius-full, 999px);
|
|
193
200
|
background: transparent;
|
|
194
|
-
color: rgb(var(--color-muted));
|
|
201
|
+
color: var(--hi-icon-button-icon-color, rgb(var(--color-muted)));
|
|
195
202
|
cursor: pointer;
|
|
196
203
|
appearance: none;
|
|
197
204
|
transition: color 0.12s ease, background 0.12s ease;
|
|
198
205
|
|
|
199
206
|
&:hover,
|
|
200
207
|
&:focus-visible {
|
|
201
|
-
color:
|
|
202
|
-
|
|
208
|
+
color: var(
|
|
209
|
+
--hi-icon-button-icon-color-active,
|
|
210
|
+
rgb(var(--color-error, 247 118 142))
|
|
211
|
+
);
|
|
212
|
+
background: var(
|
|
213
|
+
--hi-icon-button-bg-hover,
|
|
214
|
+
rgb(var(--color-error, 247 118 142) / 12%)
|
|
215
|
+
);
|
|
203
216
|
}
|
|
204
217
|
}
|
|
205
218
|
|
|
@@ -95,6 +95,11 @@
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
.hk-dp-clear {
|
|
98
|
+
/* Wave B1 hybrid decision (see HkSearchInput's twin): tabindex=-1 + own
|
|
99
|
+
keydown stop-propagation = stays a BARE button, pinned by
|
|
100
|
+
`date_clear_stays_a_bare_button_by_contract`. The reveal-on-hover
|
|
101
|
+
mechanism is the field's own contract and stays. What standardizes is
|
|
102
|
+
the INK via the icon-button channel vars (zero visual change). */
|
|
98
103
|
appearance: none;
|
|
99
104
|
display: inline-flex;
|
|
100
105
|
align-items: center;
|
|
@@ -104,7 +109,7 @@
|
|
|
104
109
|
border: 0;
|
|
105
110
|
background: transparent;
|
|
106
111
|
border-radius: var(--radius-sm, 4px);
|
|
107
|
-
color: var(--hi-color-muted);
|
|
112
|
+
color: var(--hi-icon-button-icon-color, var(--hi-color-muted));
|
|
108
113
|
cursor: pointer;
|
|
109
114
|
opacity: 0;
|
|
110
115
|
transition: opacity var(--duration-fast, 0.15s) ease, color var(--duration-fast, 0.15s) ease;
|
|
@@ -116,7 +121,7 @@
|
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
&:hover {
|
|
119
|
-
color: var(--hi-color-text-primary);
|
|
124
|
+
color: var(--hi-icon-button-icon-color-active, var(--hi-color-text-primary));
|
|
120
125
|
}
|
|
121
126
|
}
|
|
122
127
|
|
|
@@ -98,28 +98,10 @@
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
.hk-file-clear {
|
|
101
|
+
/* The clear affordance is the STANDARD icon button (wave B1): S24 exactly
|
|
102
|
+
replaces the old 1.5rem box; box, ink, hover and focus come from the
|
|
103
|
+
component. The seat keeps only its flex slot in the trigger row. */
|
|
101
104
|
flex-shrink: 0;
|
|
102
|
-
display: inline-flex;
|
|
103
|
-
align-items: center;
|
|
104
|
-
justify-content: center;
|
|
105
|
-
width: 1.5rem;
|
|
106
|
-
height: 1.5rem;
|
|
107
|
-
padding: 0;
|
|
108
|
-
background: transparent;
|
|
109
|
-
border: 0;
|
|
110
|
-
border-radius: var(--radius-xs, 2px);
|
|
111
|
-
color: rgb(var(--color-text) / 55%);
|
|
112
|
-
cursor: pointer;
|
|
113
|
-
|
|
114
|
-
&:hover {
|
|
115
|
-
background: rgb(var(--color-text) / 8%);
|
|
116
|
-
color: rgb(var(--color-text));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
&:focus-visible {
|
|
120
|
-
outline: 2px solid var(--hi-color-primary, #7aa2f7);
|
|
121
|
-
outline-offset: 1px;
|
|
122
|
-
}
|
|
123
105
|
}
|
|
124
106
|
|
|
125
107
|
/* The native input stays in the DOM for the trigger to click, but is
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { X } from "lucide-vue-next";
|
|
2
|
+
|
|
3
|
+
import HkIconButton from "./HkIconButton";
|
|
4
|
+
|
|
1
5
|
import { computed, defineComponent, ref, useId, watch } from "vue";
|
|
2
6
|
|
|
3
7
|
import { useI18n } from "../i18n/context";
|
|
@@ -170,26 +174,15 @@ export default defineComponent({
|
|
|
170
174
|
</span>
|
|
171
175
|
</button>
|
|
172
176
|
{files.length > 0 && !props.disabled && (
|
|
173
|
-
<
|
|
174
|
-
type="button"
|
|
177
|
+
<HkIconButton
|
|
175
178
|
class="hk-file-clear"
|
|
179
|
+
size={24}
|
|
180
|
+
variant="ghost"
|
|
176
181
|
aria-label={t("hikari::filePicker.fieldClear", "Clear selection")}
|
|
177
182
|
onClick={clear}
|
|
178
183
|
>
|
|
179
|
-
<
|
|
180
|
-
|
|
181
|
-
fill="none"
|
|
182
|
-
stroke="currentColor"
|
|
183
|
-
stroke-width="2"
|
|
184
|
-
stroke-linecap="round"
|
|
185
|
-
width="14"
|
|
186
|
-
height="14"
|
|
187
|
-
aria-hidden="true"
|
|
188
|
-
>
|
|
189
|
-
<line x1="18" y1="6" x2="6" y2="18" />
|
|
190
|
-
<line x1="6" y1="6" x2="18" y2="18" />
|
|
191
|
-
</svg>
|
|
192
|
-
</button>
|
|
184
|
+
{{ icon: () => <X aria-hidden="true" /> }}
|
|
185
|
+
</HkIconButton>
|
|
193
186
|
)}
|
|
194
187
|
{props.mode === "local" && (
|
|
195
188
|
<input
|
|
@@ -83,33 +83,23 @@
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
.hk-filter-panel-op {
|
|
86
|
+
/* Wave B2: the operator toggles are STANDARD icon buttons at the S28 rung
|
|
87
|
+
(the old 1.75rem box). The pressed operator's persistent primary tint
|
|
88
|
+
rides the seat's channel overrides, scoped to [aria-pressed="true"];
|
|
89
|
+
font-weight stays a seat concern (the component carries no text). */
|
|
86
90
|
flex: 0 0 auto;
|
|
87
91
|
scroll-snap-align: center;
|
|
88
|
-
min-width: 1.75rem;
|
|
89
|
-
height: 1.75rem;
|
|
90
|
-
padding: 0 var(--space-6, 0.375rem);
|
|
91
|
-
display: inline-flex;
|
|
92
|
-
align-items: center;
|
|
93
|
-
justify-content: center;
|
|
94
|
-
border: none;
|
|
95
|
-
border-radius: var(--radius-sm, 6px);
|
|
96
|
-
background: transparent;
|
|
97
|
-
color: rgb(var(--color-muted, var(--color-text-secondary)));
|
|
98
|
-
font-size: var(--text-sm, 0.875rem);
|
|
99
|
-
line-height: 1;
|
|
100
|
-
cursor: pointer;
|
|
101
|
-
transition: background 0.15s ease, color 0.15s ease;
|
|
102
|
-
}
|
|
103
92
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
93
|
+
&[aria-pressed="true"] {
|
|
94
|
+
--hi-icon-button-bg: rgb(var(--color-primary) / 18%);
|
|
95
|
+
--hi-icon-button-icon-color: rgb(var(--color-primary));
|
|
96
|
+
--hi-icon-button-icon-color-active: rgb(var(--color-primary));
|
|
97
|
+
/* Keep the primary tint THROUGH hover (the old file-order cascade
|
|
98
|
+
did): without this the pressed op falls back to the ghost gray-20
|
|
99
|
+
hover wash and reads un-pressed under the pointer. */
|
|
100
|
+
--hi-icon-button-bg-hover: rgb(var(--color-primary) / 18%);
|
|
101
|
+
font-weight: 700;
|
|
102
|
+
}
|
|
113
103
|
}
|
|
114
104
|
|
|
115
105
|
.hk-filter-panel-op:disabled {
|
|
@@ -133,28 +123,16 @@
|
|
|
133
123
|
}
|
|
134
124
|
|
|
135
125
|
.hk-filter-panel-remove {
|
|
126
|
+
/* Wave B2: the condition remove is the STANDARD icon button (S24 = the
|
|
127
|
+
old 1.5rem box). The error-tinted hover rides the channel vars — ink
|
|
128
|
+
hover-scoped (the token-input lesson: a rest-scoped ink var reads at
|
|
129
|
+
rest); the standard layer owns disabled presentation. */
|
|
136
130
|
flex: 0 0 auto;
|
|
137
|
-
|
|
138
|
-
height: 1.5rem;
|
|
139
|
-
display: inline-flex;
|
|
140
|
-
align-items: center;
|
|
141
|
-
justify-content: center;
|
|
142
|
-
border: none;
|
|
143
|
-
border-radius: var(--radius-sm, 6px);
|
|
144
|
-
background: transparent;
|
|
145
|
-
color: rgb(var(--color-muted, var(--color-text-secondary)));
|
|
146
|
-
cursor: pointer;
|
|
147
|
-
transition: background 0.15s ease, color 0.15s ease;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
.hk-filter-panel-remove:hover {
|
|
151
|
-
background: rgb(var(--color-error) / 12%);
|
|
152
|
-
color: rgb(var(--color-error));
|
|
153
|
-
}
|
|
131
|
+
--hi-icon-button-bg-hover: rgb(var(--color-error) / 12%);
|
|
154
132
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
133
|
+
&:hover {
|
|
134
|
+
--hi-icon-button-icon-color: rgb(var(--color-error));
|
|
135
|
+
}
|
|
158
136
|
}
|
|
159
137
|
|
|
160
138
|
/* The growth verb: parked in the OPERATOR COLUMN (grid-column 2), so it
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import HkIconButton from "./HkIconButton";
|
|
2
|
+
|
|
1
3
|
import { computed, defineComponent, nextTick, ref, watch, type PropType } from "vue";
|
|
2
4
|
import { Plus, X } from "lucide-vue-next";
|
|
3
5
|
|
|
@@ -325,18 +327,19 @@ export default defineComponent({
|
|
|
325
327
|
{ops.map((op) => {
|
|
326
328
|
const glyph = glyphOpFor(kind, op);
|
|
327
329
|
return (
|
|
328
|
-
<
|
|
330
|
+
<HkIconButton
|
|
329
331
|
key={op}
|
|
330
|
-
type="button"
|
|
331
332
|
class="hk-filter-panel-op"
|
|
333
|
+
size={28}
|
|
334
|
+
variant="ghost"
|
|
332
335
|
aria-pressed={cond.op === op}
|
|
333
336
|
aria-label={t(OPERATOR_LABEL[glyph].key, OPERATOR_LABEL[glyph].fallback)}
|
|
334
|
-
title
|
|
337
|
+
{...{ title: t(OPERATOR_LABEL[glyph].key, OPERATOR_LABEL[glyph].fallback) }}
|
|
335
338
|
disabled={props.disabled || !field}
|
|
336
339
|
onClick={() => emitCondition(index, { op })}
|
|
337
340
|
>
|
|
338
|
-
<HkFilterOperatorIcon op={glyph} />
|
|
339
|
-
</
|
|
341
|
+
{{ icon: () => <HkFilterOperatorIcon op={glyph} /> }}
|
|
342
|
+
</HkIconButton>
|
|
340
343
|
);
|
|
341
344
|
})}
|
|
342
345
|
</div>
|
|
@@ -394,19 +397,20 @@ export default defineComponent({
|
|
|
394
397
|
* page refetches without it). Sibling conditions on
|
|
395
398
|
* the same field survive — index-targeted removal,
|
|
396
399
|
* never key-targeted. */}
|
|
397
|
-
<
|
|
398
|
-
type="button"
|
|
400
|
+
<HkIconButton
|
|
399
401
|
class="hk-filter-panel-remove"
|
|
402
|
+
size={24}
|
|
403
|
+
variant="ghost"
|
|
400
404
|
aria-label={t("hikari::filterPanel.removeCondition", "Remove condition")}
|
|
401
|
-
title
|
|
405
|
+
{...{ title: t("hikari::filterPanel.removeCondition", "Remove condition") }}
|
|
402
406
|
disabled={props.disabled}
|
|
403
407
|
onClick={() => {
|
|
404
408
|
const next = props.modelValue.filter((_, i) => i !== index);
|
|
405
409
|
emit("update:modelValue", next);
|
|
406
410
|
}}
|
|
407
411
|
>
|
|
408
|
-
<X
|
|
409
|
-
</
|
|
412
|
+
{{ icon: () => <X aria-hidden="true" /> }}
|
|
413
|
+
</HkIconButton>
|
|
410
414
|
</div>
|
|
411
415
|
</div>
|
|
412
416
|
);
|
|
@@ -41,23 +41,10 @@
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
.hk-kw-search-clear {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
color: var(--hi-color-text-secondary, #666666);
|
|
48
|
-
cursor: pointer;
|
|
49
|
-
display: inline-flex;
|
|
44
|
+
/* Wave B2/B3: the clear affordance is the STANDARD icon button — box,
|
|
45
|
+
ink, hover and focus come from the component; the seat keeps only the
|
|
46
|
+
field's inner padding slot. */
|
|
50
47
|
padding: var(--space-2, 0.125rem);
|
|
51
|
-
border-radius: var(--radius-sm, 0.25rem);
|
|
52
|
-
|
|
53
|
-
&:hover {
|
|
54
|
-
color: var(--hi-color-text-primary, #1e1e1e);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
&:focus-visible {
|
|
58
|
-
outline: 2px solid color-mix(in srgb, var(--hi-color-primary, #7aa2f7) 55%, transparent);
|
|
59
|
-
outline-offset: 2px;
|
|
60
|
-
}
|
|
61
48
|
}
|
|
62
49
|
|
|
63
50
|
/* ONE SCROLLBAR PER WINDOW (2026-09-08 audit): the results list is NOT a
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import HkIconButton from "./HkIconButton";
|
|
2
|
+
|
|
1
3
|
import { computed, defineComponent, onBeforeUnmount, ref, watch, type PropType } from "vue";
|
|
2
4
|
|
|
3
5
|
import { useI18n } from "../i18n/context";
|
|
@@ -241,9 +243,10 @@ export default defineComponent({
|
|
|
241
243
|
autofocus
|
|
242
244
|
/>
|
|
243
245
|
{query.value && (
|
|
244
|
-
<
|
|
246
|
+
<HkIconButton
|
|
245
247
|
class="hk-kw-search-clear"
|
|
246
|
-
|
|
248
|
+
size={24}
|
|
249
|
+
variant="ghost"
|
|
247
250
|
aria-label={t("hikari::keywordSearch.clear", "Clear")}
|
|
248
251
|
onClick={() => {
|
|
249
252
|
query.value = "";
|
|
@@ -251,19 +254,21 @@ export default defineComponent({
|
|
|
251
254
|
semanticResults.value = [];
|
|
252
255
|
}}
|
|
253
256
|
>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
257
|
+
{{ icon: () => (
|
|
258
|
+
<svg
|
|
259
|
+
width="14"
|
|
260
|
+
height="14"
|
|
261
|
+
viewBox="0 0 24 24"
|
|
262
|
+
fill="none"
|
|
263
|
+
stroke="currentColor"
|
|
264
|
+
stroke-width="2"
|
|
265
|
+
stroke-linecap="round"
|
|
266
|
+
>
|
|
267
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
268
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
269
|
+
</svg>
|
|
270
|
+
) }}
|
|
271
|
+
</HkIconButton>
|
|
267
272
|
)}
|
|
268
273
|
</div>
|
|
269
274
|
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
* region above all) stays horizontally AND vertically centered at every t;
|
|
10
10
|
* the translate prefix is byte-identical in every state so the ride can
|
|
11
11
|
* never re-seat the frame. `.hk-modal-body` keeps a ride of its own:
|
|
12
|
-
* `scale(
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* vertical factor stays 1 because the body's Y already
|
|
16
|
-
* scaleY (own 0.9 would compound to 0.81). The retired
|
|
17
|
-
* 2026-11-04 whole-frame `scale(1, 0.9)
|
|
18
|
-
* uniform `scale(0.9)
|
|
12
|
+
* `scale(0.95, 1) → scale(1, 1)` — the horizontal factor starts slightly
|
|
13
|
+
* NARROW and spreads out to fill the window (user direction 2026-11-06,
|
|
14
|
+
* revising the first cut: a 1.1 overshoot read as uglier than a slight
|
|
15
|
+
* pinch), while its vertical factor stays 1 because the body's Y already
|
|
16
|
+
* rides the frame's scaleY (own 0.9 would compound to 0.81). The retired
|
|
17
|
+
* grammars: the 2026-11-04 whole-frame `scale(1, 0.9)`, the 2026-11-05
|
|
18
|
+
* body-only uniform `scale(0.9)`, and the 2026-11-06 `scale(1.1, 1)`
|
|
19
|
+
* overshoot.
|
|
19
20
|
*
|
|
20
21
|
* Why compile: a source-text scanner can be dodged by a later duplicate
|
|
21
22
|
* rule, a block comment, SCSS interpolation, `@extend` or an `@use`d
|
|
@@ -347,7 +348,7 @@ const BODY_ENTER_TRANSITION =
|
|
|
347
348
|
const BODY_LEAVE_TRANSITION =
|
|
348
349
|
"transform var(--hk-modal-duration, 0.32s) var(--hk-modal-ease-in, cubic-bezier(0.45, 0, 0.55, 1))";
|
|
349
350
|
|
|
350
|
-
describe("HkModal desktop open/close contract (compiled cascade, user direction 2026-11-
|
|
351
|
+
describe("HkModal desktop open/close contract (compiled cascade, user direction 2026-11-06)", () => {
|
|
351
352
|
it("compiles into a stylesheet with the ride rules present (known-positive)", () => {
|
|
352
353
|
// Guard against a vacuous suite: the compiled sheet must really contain
|
|
353
354
|
// the frame rule, the body rule and the reduced-motion flatten.
|
|
@@ -436,11 +437,11 @@ describe("HkModal desktop open/close contract (compiled cascade, user direction
|
|
|
436
437
|
expect(leaveWill).toBe("opacity, transform");
|
|
437
438
|
});
|
|
438
439
|
|
|
439
|
-
it("rides the content region X
|
|
440
|
-
// 2026-11-06: the
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
//
|
|
440
|
+
it("rides the content region X 0.95→1 with Y pinned at 1, about its center", () => {
|
|
441
|
+
// 2026-11-06 (revised): the content starts slightly NARROW (95% of the
|
|
442
|
+
// window width) and spreads to 100%; the vertical factor stays 1 — the
|
|
443
|
+
// body's Y already rides the frame's scaleY, so its own 1 keeps the
|
|
444
|
+
// compound at exactly 0.9→1.
|
|
444
445
|
const expect_ = (state: string, value: string) => {
|
|
445
446
|
const frame = frameWith(...RIDE.find((r) => r.name === state)!.classes);
|
|
446
447
|
const body = child(frame, "hk-modal-body");
|
|
@@ -453,8 +454,8 @@ describe("HkModal desktop open/close contract (compiled cascade, user direction
|
|
|
453
454
|
const [x = "50%", y = "50%"] = origin;
|
|
454
455
|
expect([x, y], state).toEqual(["50%", "50%"]);
|
|
455
456
|
};
|
|
456
|
-
expect_("enter-from", "scale(
|
|
457
|
-
expect_("leave-to", "scale(
|
|
457
|
+
expect_("enter-from", "scale(0.95, 1)");
|
|
458
|
+
expect_("leave-to", "scale(0.95, 1)");
|
|
458
459
|
expect_("enter-to", "scale(1, 1)");
|
|
459
460
|
expect_("leave-from", "scale(1, 1)");
|
|
460
461
|
// …and the same with the opt-in draggable flag on, where an
|
|
@@ -463,7 +464,7 @@ describe("HkModal desktop open/close contract (compiled cascade, user direction
|
|
|
463
464
|
const frame = draggableFrameWith(...state.classes);
|
|
464
465
|
const body = child(frame, "hk-modal-body");
|
|
465
466
|
const expected =
|
|
466
|
-
state.name === "enter-from" || state.name === "leave-to" ? "scale(
|
|
467
|
+
state.name === "enter-from" || state.name === "leave-to" ? "scale(0.95, 1)" : "scale(1, 1)";
|
|
467
468
|
expect(winner(body, "transform", DESKTOP), `draggable ${state.name}`).toBe(expected);
|
|
468
469
|
}
|
|
469
470
|
// At rest the body carries no transform at all.
|
|
@@ -546,10 +547,19 @@ describe("HkModal desktop open/close contract (compiled cascade, user direction
|
|
|
546
547
|
expect(winner(frameWith("hk-modal-content-leave-from", "hk-modal-content-leave-active"), "transform", MOBILE)).toBe("none");
|
|
547
548
|
const resting = frameWith();
|
|
548
549
|
expect(winner(resting, "transform", MOBILE)).toBe("none");
|
|
549
|
-
// No content-region scale in the sheet layout
|
|
550
|
+
// No content-region scale in the sheet layout — at EITHER end of the
|
|
551
|
+
// ride. The settled states used to go unpinned (a max-width block
|
|
552
|
+
// could scale the body there and no assertion noticed; R3 mutation
|
|
553
|
+
// 2026-11-06), so all four states are spelled out.
|
|
550
554
|
const body = child(frame, "hk-modal-body");
|
|
551
555
|
expect(winner(body, "transform", MOBILE)).toBeUndefined();
|
|
552
556
|
expect(winner(child(leaveEnd, "hk-modal-body"), "transform", MOBILE)).toBeUndefined();
|
|
557
|
+
expect(
|
|
558
|
+
winner(child(frameWith("hk-modal-content-enter-to", "hk-modal-content-enter-active"), "hk-modal-body"), "transform", MOBILE),
|
|
559
|
+
).toBeUndefined();
|
|
560
|
+
expect(
|
|
561
|
+
winner(child(frameWith("hk-modal-content-leave-from", "hk-modal-content-leave-active"), "hk-modal-body"), "transform", MOBILE),
|
|
562
|
+
).toBeUndefined();
|
|
553
563
|
});
|
|
554
564
|
|
|
555
565
|
it("never lets ANY rule put a transform on a chrome surface", () => {
|
|
@@ -590,7 +600,7 @@ describe("HkModal desktop open/close contract (compiled cascade, user direction
|
|
|
590
600
|
// three translate+scaleY states (reduced-motion flatten included);
|
|
591
601
|
// body: the two two-arg scales.
|
|
592
602
|
const LEGAL_FRAME = new Set([CENTERING, FRAME_FROM, FRAME_TO]);
|
|
593
|
-
const LEGAL_BODY = new Set(["scale(
|
|
603
|
+
const LEGAL_BODY = new Set(["scale(0.95, 1)", "scale(1, 1)"]);
|
|
594
604
|
const offenders: string[] = [];
|
|
595
605
|
// Media-independent on purpose: a ride selector declared at top level
|
|
596
606
|
// (no media query) applies at every width, so it must obey the same
|
|
@@ -114,14 +114,14 @@
|
|
|
114
114
|
// IS the window-height motion the user asked for.
|
|
115
115
|
//
|
|
116
116
|
// The content region keeps a ride of its own: `.hk-modal-body` settles
|
|
117
|
-
// `scale(
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
117
|
+
// `scale(0.95, 1) → scale(1, 1)` about its center. The horizontal factor
|
|
118
|
+
// starts slightly NARROW (user direction 2026-11-06, after reviewing the
|
|
119
|
+
// first cut: a 1.1 overshoot read as uglier than a slight pinch, so the
|
|
120
|
+
// content starts at 95% width and spreads out to fill the window). The
|
|
121
|
+
// vertical factor stays exactly 1: the body's Y dimension already rides
|
|
122
|
+
// the frame's `scaleY`, so its own 1 makes the compound 0.9 → 1 — the
|
|
123
|
+
// same vertical ride as before, never the doubled 0.81 a second Y factor
|
|
124
|
+
// would produce.
|
|
125
125
|
//
|
|
126
126
|
// Hosts retune the ride through the `--hk-modal-*` tokens (duration /
|
|
127
127
|
// easing) or the body padding vars — NOT by setting `transform` on
|
|
@@ -131,13 +131,13 @@
|
|
|
131
131
|
// single-class transform would be overridden mid-ride and again under
|
|
132
132
|
// reduced motion.
|
|
133
133
|
//
|
|
134
|
-
// No clip-path and no Y offset ride the open/close:
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
134
|
+
// No clip-path and no Y offset ride the open/close: the visible box IS the
|
|
135
|
+
// frame at every t, so nothing is ever sliced off (the 2026-09-14 clip
|
|
136
|
+
// unveil — and the #508/#512/#524 lineage before it — stays retired). All
|
|
137
|
+
// compositor-level: zero layout per frame. The centering translate lives
|
|
138
|
+
// on the base .hk-modal-content rule, so the frame never re-seats
|
|
139
|
+
// mid-flight. Scoped to ≥768px; ≤767px keeps the sheet's translateY
|
|
140
|
+
// choreography below.
|
|
141
141
|
// ------
|
|
142
142
|
|
|
143
143
|
@media (min-width: 768px) {
|
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// Close: the reveal's TRUE reverse — same chrome fade, while the window
|
|
156
|
-
// settles back toward 90% height and the content
|
|
156
|
+
// settles back toward 90% height and the content pinches back in.
|
|
157
157
|
.hk-modal-content-leave-active {
|
|
158
158
|
transition:
|
|
159
159
|
opacity var(--hk-modal-duration, 0.32s)
|
|
@@ -205,11 +205,11 @@
|
|
|
205
205
|
will-change: transform;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
// Both ends two-arg `scale`: X
|
|
209
|
-
// vertical ride comes from the frame's scaleY, not
|
|
208
|
+
// Both ends two-arg `scale`: X starts at 95% and spreads to 1, Y stays 1
|
|
209
|
+
// (the body's vertical ride comes from the frame's scaleY, not itself).
|
|
210
210
|
.hk-modal-content-enter-from .hk-modal-body,
|
|
211
211
|
.hk-modal-content-leave-to .hk-modal-body {
|
|
212
|
-
transform: scale(
|
|
212
|
+
transform: scale(0.95, 1);
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
.hk-modal-content-enter-to .hk-modal-body,
|
|
@@ -275,7 +275,11 @@
|
|
|
275
275
|
padding: 0.25rem;
|
|
276
276
|
border: none;
|
|
277
277
|
background: transparent;
|
|
278
|
-
|
|
278
|
+
/* Wave B2/B3 hybrid decision: the eye is a PRESS-AND-HOLD control
|
|
279
|
+
(mousedown/up + keydown/up drive revealing — pinned Rust-side
|
|
280
|
+
`pwd_eye_stays_a_bare_button_by_contract`), so it stays a BARE button.
|
|
281
|
+
The ink consumes the icon-button channel vars (zero visual change). */
|
|
282
|
+
color: var(--hi-icon-button-icon-color, var(--hi-color-text-secondary, #666666));
|
|
279
283
|
cursor: pointer;
|
|
280
284
|
touch-action: none;
|
|
281
285
|
/* iOS long-press must not summon the system callout / magnifier over
|
|
@@ -284,7 +288,10 @@
|
|
|
284
288
|
-webkit-touch-callout: none;
|
|
285
289
|
|
|
286
290
|
&:hover:not([data-revealing]) {
|
|
287
|
-
color: var(
|
|
291
|
+
color: var(
|
|
292
|
+
--hi-icon-button-icon-color-active,
|
|
293
|
+
var(--hi-color-text-primary, #1e1e1e)
|
|
294
|
+
);
|
|
288
295
|
}
|
|
289
296
|
|
|
290
297
|
/* While held, the affordance reads as active: full primary tint like
|
|
@@ -64,6 +64,13 @@
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
.hk-search-input-clear {
|
|
67
|
+
/* Wave B1 hybrid decision: this clear is tabindex=-1 (Escape and the
|
|
68
|
+
field's own keyboard path own dismissal) and keeps the field's hit-band
|
|
69
|
+
density, so it deliberately stays a BARE button — pinned by
|
|
70
|
+
`search_clear_stays_a_bare_button_by_contract`. What standardizes is the
|
|
71
|
+
INK: it consumes the same icon-button channel vars the standard
|
|
72
|
+
component reads, so theming moves together (current values kept as the
|
|
73
|
+
first fallback = zero visual change). */
|
|
67
74
|
display: flex;
|
|
68
75
|
align-items: center;
|
|
69
76
|
justify-content: center;
|
|
@@ -73,7 +80,7 @@
|
|
|
73
80
|
padding: 0;
|
|
74
81
|
background: transparent;
|
|
75
82
|
border: none;
|
|
76
|
-
color: var(--hi-color-muted);
|
|
83
|
+
color: var(--hi-icon-button-icon-color, var(--hi-color-muted));
|
|
77
84
|
cursor: pointer;
|
|
78
85
|
transition:
|
|
79
86
|
background-color var(--duration-fast, 0.15s) ease,
|
|
@@ -85,12 +92,12 @@
|
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
&:hover {
|
|
88
|
-
background: color-mix(in srgb, var(--hi-color-surface) 60%, transparent);
|
|
89
|
-
color: var(--hi-color-text-primary);
|
|
95
|
+
background: var(--hi-icon-button-bg-hover, color-mix(in srgb, var(--hi-color-surface) 60%, transparent));
|
|
96
|
+
color: var(--hi-icon-button-icon-color-active, var(--hi-color-text-primary));
|
|
90
97
|
}
|
|
91
98
|
|
|
92
99
|
&:active {
|
|
93
|
-
background: color-mix(in srgb, var(--hi-color-surface) 80%, transparent);
|
|
100
|
+
background: var(--hi-icon-button-bg-active, color-mix(in srgb, var(--hi-color-surface) 80%, transparent));
|
|
94
101
|
}
|
|
95
102
|
}
|
|
96
103
|
|
|
@@ -264,36 +264,10 @@
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
.hk-selection-grid-action {
|
|
267
|
+
/* Wave B2: per-item actions are STANDARD icon buttons (S24/ghost) — box,
|
|
268
|
+
ink, hover and focus come from the component; the seat keeps the grid's
|
|
269
|
+
flex slot. */
|
|
267
270
|
flex: none;
|
|
268
|
-
display: inline-flex;
|
|
269
|
-
align-items: center;
|
|
270
|
-
justify-content: center;
|
|
271
|
-
width: 1.5rem;
|
|
272
|
-
height: 1.5rem;
|
|
273
|
-
padding: 0;
|
|
274
|
-
border: 0;
|
|
275
|
-
border-radius: var(--radius-sm, 4px);
|
|
276
|
-
background: transparent;
|
|
277
|
-
color: var(--hi-color-text-tertiary, #8b949e);
|
|
278
|
-
cursor: pointer;
|
|
279
|
-
transition:
|
|
280
|
-
color 200ms ease,
|
|
281
|
-
background 200ms ease;
|
|
282
|
-
|
|
283
|
-
&:hover:not(:disabled) {
|
|
284
|
-
color: rgb(var(--color-text));
|
|
285
|
-
background: rgb(var(--color-text) / 8%);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
&:focus-visible {
|
|
289
|
-
outline: 2px solid rgb(var(--color-primary));
|
|
290
|
-
outline-offset: -2px;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
&:disabled {
|
|
294
|
-
opacity: 0.45;
|
|
295
|
-
cursor: not-allowed;
|
|
296
|
-
}
|
|
297
271
|
}
|
|
298
272
|
|
|
299
273
|
/* ── Optional selected edge ──────────────────────────────────────────────
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import HkIconButton from "./HkIconButton";
|
|
2
|
+
|
|
1
3
|
import { Check } from "lucide-vue-next";
|
|
2
4
|
import { defineComponent, type PropType } from "vue";
|
|
3
5
|
|
|
@@ -156,11 +158,12 @@ export default defineComponent({
|
|
|
156
158
|
{item.actions.map((action) => {
|
|
157
159
|
const ActionIcon = action.icon;
|
|
158
160
|
return (
|
|
159
|
-
<
|
|
161
|
+
<HkIconButton
|
|
160
162
|
key={action.id}
|
|
161
|
-
type="button"
|
|
162
163
|
class="hk-selection-grid-action"
|
|
163
|
-
|
|
164
|
+
size={24}
|
|
165
|
+
variant="ghost"
|
|
166
|
+
{...{ title: action.label }}
|
|
164
167
|
aria-label={action.label}
|
|
165
168
|
disabled={action.disabled || undefined}
|
|
166
169
|
onClick={(e: MouseEvent) => {
|
|
@@ -171,7 +174,7 @@ export default defineComponent({
|
|
|
171
174
|
}}
|
|
172
175
|
>
|
|
173
176
|
{ActionIcon && <ActionIcon size={14} />}
|
|
174
|
-
</
|
|
177
|
+
</HkIconButton>
|
|
175
178
|
);
|
|
176
179
|
})}
|
|
177
180
|
</div>
|
|
@@ -31,32 +31,12 @@
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
.hk-tag-close {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
/* The chip × is the STANDARD icon button at the S16 rung (wave B1): the
|
|
35
|
+
14px bespoke box, the opacity dance and the focus outline are retired —
|
|
36
|
+
box/ink/hover/focus come from the component. The seat keeps only the
|
|
37
|
+
chip's trailing-edge spacing. 14px → 16px is the declared growth. */
|
|
37
38
|
flex-shrink: 0;
|
|
38
|
-
width: 14px;
|
|
39
|
-
height: 14px;
|
|
40
|
-
border: none;
|
|
41
|
-
background: transparent;
|
|
42
|
-
color: inherit;
|
|
43
|
-
cursor: pointer;
|
|
44
|
-
opacity: 0.5;
|
|
45
|
-
padding: 0;
|
|
46
|
-
/* Trailing edge of the chip in both directions. */
|
|
47
39
|
margin-inline-start: var(--space-2, 2px);
|
|
48
|
-
border-radius: var(--radius-sm, 4px);
|
|
49
|
-
transition: opacity var(--duration-fast, 0.15s) ease;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
.hk-tag-close:hover {
|
|
53
|
-
opacity: 1;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/* Keyboard focus indicator (house focus-ring pattern). */
|
|
57
|
-
.hk-tag-close:focus-visible {
|
|
58
|
-
outline: 2px solid rgb(var(--color-primary));
|
|
59
|
-
outline-offset: 2px;
|
|
60
40
|
}
|
|
61
41
|
|
|
62
42
|
.hk-tag-default {
|
package/src/components/HkTag.tsx
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { X } from "lucide-vue-next";
|
|
2
|
+
|
|
3
|
+
import HkIconButton from "./HkIconButton";
|
|
2
4
|
import { computed, defineComponent, type PropType } from "vue";
|
|
3
5
|
|
|
4
6
|
import "./HkTag.scss";
|
|
@@ -38,14 +40,15 @@ export default defineComponent({
|
|
|
38
40
|
)}
|
|
39
41
|
{slots.default?.()}
|
|
40
42
|
{props.closable && (
|
|
41
|
-
<
|
|
42
|
-
type="button"
|
|
43
|
+
<HkIconButton
|
|
43
44
|
class="hk-tag-close"
|
|
45
|
+
size={16}
|
|
46
|
+
variant="ghost"
|
|
44
47
|
aria-label={props.closeLabel}
|
|
45
48
|
onClick={() => emit("close")}
|
|
46
49
|
>
|
|
47
|
-
<X
|
|
48
|
-
</
|
|
50
|
+
{{ icon: () => <X aria-hidden="true" /> }}
|
|
51
|
+
</HkIconButton>
|
|
49
52
|
)}
|
|
50
53
|
</span>
|
|
51
54
|
);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import HkIconButton from "./HkIconButton";
|
|
2
|
+
|
|
1
3
|
import { computed, defineComponent, onUnmounted, ref, watch, type PropType } from "vue";
|
|
2
4
|
import { Monitor, MoonStar, Palette, Sun, Trash as Trash2 } from "lucide-vue-next";
|
|
3
5
|
import {
|
|
@@ -292,14 +294,16 @@ export const HkThemePanelContent = defineComponent({
|
|
|
292
294
|
{trailingSlot ? (
|
|
293
295
|
<span class="s-theme-item-trailing">{trailingSlot(scope)}</span>
|
|
294
296
|
) : th.isCustom ? (
|
|
295
|
-
<
|
|
296
|
-
type="button"
|
|
297
|
+
<HkIconButton
|
|
297
298
|
class="s-theme-item-delete"
|
|
298
|
-
|
|
299
|
+
size={24}
|
|
300
|
+
variant="ghost"
|
|
301
|
+
aria-label={t("hikari::theme.deleteTheme")}
|
|
302
|
+
{...{ title: t("hikari::theme.deleteTheme") }}
|
|
299
303
|
onClick={() => removeCustomTheme(th.id)}
|
|
300
304
|
>
|
|
301
|
-
<Trash2
|
|
302
|
-
</
|
|
305
|
+
{{ icon: () => <Trash2 aria-hidden="true" /> }}
|
|
306
|
+
</HkIconButton>
|
|
303
307
|
) : null}
|
|
304
308
|
</div>
|
|
305
309
|
);
|
|
@@ -345,32 +345,20 @@
|
|
|
345
345
|
* pill's trailing edge (the row is relative), not a reserved column:
|
|
346
346
|
* built-in rows show nothing there and the pill runs to the row edge. */
|
|
347
347
|
.s-theme-item-delete {
|
|
348
|
+
/* Wave B2/B3: the custom-theme delete is the STANDARD icon button at S24
|
|
349
|
+
(= the old 1.5rem desktop box). The seat keeps the row's parked-edge
|
|
350
|
+
positioning (absolute, trailing, vertically centred) — real geometry.
|
|
351
|
+
The error-tinted hover rides the channel vars, ink hover-scoped (token
|
|
352
|
+
lesson); the inset error focus ring yields to the standard ring. */
|
|
348
353
|
position: absolute;
|
|
349
354
|
inset-inline-end: var(--space-8, 0.5rem);
|
|
350
355
|
top: 50%;
|
|
351
356
|
transform: translateY(-50%);
|
|
352
|
-
|
|
353
|
-
align-items: center;
|
|
354
|
-
justify-content: center;
|
|
355
|
-
width: 1.5rem;
|
|
356
|
-
height: 1.5rem;
|
|
357
|
-
border: none;
|
|
358
|
-
border-radius: var(--radius-sm, 6px);
|
|
359
|
-
background: transparent;
|
|
360
|
-
color: rgb(var(--color-muted));
|
|
361
|
-
cursor: pointer;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
.s-theme-item-delete:hover {
|
|
365
|
-
background: rgb(var(--color-error) / 15%);
|
|
366
|
-
color: rgb(var(--color-error));
|
|
367
|
-
}
|
|
357
|
+
--hi-icon-button-bg-hover: rgb(var(--color-error) / 15%);
|
|
368
358
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
outline: 2px solid rgb(var(--color-error));
|
|
373
|
-
outline-offset: -2px;
|
|
359
|
+
&:hover {
|
|
360
|
+
--hi-icon-button-icon-color: rgb(var(--color-error));
|
|
361
|
+
}
|
|
374
362
|
}
|
|
375
363
|
|
|
376
364
|
/* Touch-sized delete target on phone widths, scaled for the 44px sheet
|
|
@@ -379,8 +367,10 @@
|
|
|
379
367
|
* label never collide on long custom scheme names. */
|
|
380
368
|
@media (max-width: 767px) {
|
|
381
369
|
.s-theme-item-delete {
|
|
382
|
-
width
|
|
383
|
-
|
|
370
|
+
/* Set the COMPONENT's size var (not width/height — the S24 rung's
|
|
371
|
+
min/max clamps would defeat them): the phone delete target keeps its
|
|
372
|
+
36px sheet-row contract. */
|
|
373
|
+
--hi-icon-button-size: 2.25rem;
|
|
384
374
|
}
|
|
385
375
|
|
|
386
376
|
.s-theme-item-row[data-custom] .s-theme-item-name {
|
|
@@ -68,21 +68,26 @@
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
.hk-token-input-clear {
|
|
71
|
+
/* The editor's clear affordance is the STANDARD icon button (wave B1):
|
|
72
|
+
S24 replaces the old 26px box. The OLD contract is preserved exactly:
|
|
73
|
+
muted ink at rest, error ink + wash on hover (an R3 review round caught
|
|
74
|
+
a first draft that set the ink var at rest — red-at-rest was never the
|
|
75
|
+
design). The hover-scoped override keeps ghost's rest ink while the
|
|
76
|
+
error channels apply on hover/press only. */
|
|
71
77
|
flex: 0 0 auto;
|
|
72
|
-
display: inline-flex;
|
|
73
|
-
align-items: center;
|
|
74
|
-
justify-content: center;
|
|
75
78
|
margin-top: var(--space-4, 4px);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
cursor: pointer;
|
|
83
|
-
}
|
|
79
|
+
--hi-icon-button-bg-hover: rgb(var(--color-error) / 10%);
|
|
80
|
+
--hi-icon-button-bg-active: rgb(var(--color-error) / 16%);
|
|
81
|
+
|
|
82
|
+
&:hover {
|
|
83
|
+
--hi-icon-button-icon-color: rgb(var(--color-error));
|
|
84
|
+
}
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
&:active {
|
|
87
|
+
/* Press ink reads the -active channel (HkIconButton.scss
|
|
88
|
+
`&:active .hk-icon-button-icon`), so the base var alone is a no-op
|
|
89
|
+
here — set both. */
|
|
90
|
+
--hi-icon-button-icon-color: rgb(var(--color-error));
|
|
91
|
+
--hi-icon-button-icon-color-active: rgb(var(--color-error));
|
|
92
|
+
}
|
|
88
93
|
}
|
|
@@ -5,6 +5,8 @@ import HkInput from "./HkInput";
|
|
|
5
5
|
import HkSelect from "./HkSelect";
|
|
6
6
|
import { X } from "lucide-vue-next";
|
|
7
7
|
|
|
8
|
+
import HkIconButton from "./HkIconButton";
|
|
9
|
+
|
|
8
10
|
import { useI18n } from "../i18n/context";
|
|
9
11
|
import "./HkTokenInput.scss";
|
|
10
12
|
|
|
@@ -159,14 +161,15 @@ export default defineComponent({
|
|
|
159
161
|
onUpdate:modelValue={(v: number | null) => emit("update:modelValue", v)}
|
|
160
162
|
onUpdate:unit={(u: TokenUnit) => (editorUnit.value = u)}
|
|
161
163
|
/>
|
|
162
|
-
<
|
|
163
|
-
type="button"
|
|
164
|
+
<HkIconButton
|
|
164
165
|
class="hk-token-input-clear"
|
|
166
|
+
size={24}
|
|
167
|
+
variant="ghost"
|
|
165
168
|
aria-label={clearLabel.value}
|
|
166
169
|
onClick={clearValue}
|
|
167
170
|
>
|
|
168
|
-
<X
|
|
169
|
-
</
|
|
171
|
+
{{ icon: () => <X aria-hidden="true" /> }}
|
|
172
|
+
</HkIconButton>
|
|
170
173
|
</div>
|
|
171
174
|
),
|
|
172
175
|
}}
|
|
@@ -75,24 +75,11 @@
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
.hk-user-picker-tag-x {
|
|
78
|
+
/* Wave B2: the tag close is the STANDARD icon button at S16 — the literal
|
|
79
|
+
multiplication-sign text button (audited as the family's worst
|
|
80
|
+
offender) is retired along with its bespoke box; the seat keeps only
|
|
81
|
+
the chip's flex slot. */
|
|
78
82
|
flex: none;
|
|
79
|
-
display: inline-flex;
|
|
80
|
-
align-items: center;
|
|
81
|
-
justify-content: center;
|
|
82
|
-
width: 1rem;
|
|
83
|
-
height: 1rem;
|
|
84
|
-
border: none;
|
|
85
|
-
border-radius: 999px;
|
|
86
|
-
background: transparent;
|
|
87
|
-
color: rgb(var(--color-text) / 60%);
|
|
88
|
-
font-size: 0.875rem;
|
|
89
|
-
line-height: 1;
|
|
90
|
-
cursor: pointer;
|
|
91
|
-
|
|
92
|
-
&:hover {
|
|
93
|
-
background: rgb(var(--color-text) / 10%);
|
|
94
|
-
color: rgb(var(--color-text));
|
|
95
|
-
}
|
|
96
83
|
}
|
|
97
84
|
|
|
98
85
|
.hk-user-picker-add {
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
// Semantics (the HkAffixPicker vocabulary): "single" closes on pick and
|
|
14
14
|
// replaces the selection; "multi" toggles rows in a working copy and
|
|
15
15
|
// commits on Confirm. The field's tag × removes directly — no dialog.
|
|
16
|
+
import { X } from "lucide-vue-next";
|
|
17
|
+
|
|
18
|
+
import HkIconButton from "./HkIconButton";
|
|
19
|
+
|
|
16
20
|
import { computed, defineComponent, ref, watch, type PropType } from "vue";
|
|
17
21
|
|
|
18
22
|
import { useI18n } from "../i18n/context";
|
|
@@ -186,17 +190,18 @@ export default defineComponent({
|
|
|
186
190
|
{user.email && <span class="hk-user-picker-tag-email">{user.email}</span>}
|
|
187
191
|
</span>
|
|
188
192
|
{!props.disabled && (
|
|
189
|
-
<
|
|
190
|
-
type="button"
|
|
193
|
+
<HkIconButton
|
|
191
194
|
class="hk-user-picker-tag-x"
|
|
195
|
+
size={16}
|
|
196
|
+
variant="ghost"
|
|
192
197
|
aria-label={`${t("hikari::userPicker.remove", "Remove")} ${tagTitle(user)}`}
|
|
193
198
|
onClick={(e: MouseEvent) => {
|
|
194
199
|
e.stopPropagation();
|
|
195
200
|
removeTag(user.uid);
|
|
196
201
|
}}
|
|
197
202
|
>
|
|
198
|
-
|
|
199
|
-
</
|
|
203
|
+
{{ icon: () => <X aria-hidden="true" /> }}
|
|
204
|
+
</HkIconButton>
|
|
200
205
|
)}
|
|
201
206
|
</span>
|
|
202
207
|
))}
|
|
@@ -2,6 +2,8 @@ import { afterEach, describe, expect, it } from "vitest";
|
|
|
2
2
|
import { createApp, h, nextTick } from "vue";
|
|
3
3
|
|
|
4
4
|
import HkAlert from "./HkAlert";
|
|
5
|
+
import HkFileField from "./HkFileField";
|
|
6
|
+
import HkTag from "./HkTag";
|
|
5
7
|
import HkPersistentToast from "./HkPersistentToast";
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -26,6 +28,24 @@ const seats = [
|
|
|
26
28
|
render: () => h(HkAlert, { message: "m", closable: true }),
|
|
27
29
|
seat: "hk-alert-close",
|
|
28
30
|
},
|
|
31
|
+
{
|
|
32
|
+
// Wave B1: the file field's clear rides the standard button at S24.
|
|
33
|
+
name: "HkFileField clear",
|
|
34
|
+
render: () =>
|
|
35
|
+
h(HkFileField, {
|
|
36
|
+
modelValue: [{ name: "a.txt" }],
|
|
37
|
+
}),
|
|
38
|
+
seat: "hk-file-clear",
|
|
39
|
+
rung: 24,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
// Wave B1: the chip close rides S16 (the 14px bespoke box is retired;
|
|
43
|
+
// 14 -> 16 is the declared density growth).
|
|
44
|
+
name: "HkTag chip close",
|
|
45
|
+
render: () => h(HkTag, { closable: true, closeLabel: "Remove" }),
|
|
46
|
+
seat: "hk-tag-close",
|
|
47
|
+
rung: 16,
|
|
48
|
+
},
|
|
29
49
|
{
|
|
30
50
|
name: "HkPersistentToast dismiss",
|
|
31
51
|
render: () => h(HkPersistentToast, { label: "l", dismissible: true }),
|
|
@@ -48,7 +68,8 @@ afterEach(() => {
|
|
|
48
68
|
});
|
|
49
69
|
|
|
50
70
|
describe("close-button family rides the standard icon button", () => {
|
|
51
|
-
for (const
|
|
71
|
+
for (const entry of seats) {
|
|
72
|
+
const { name, render, seat } = entry;
|
|
52
73
|
it(`${name} renders the standard class list plus its seat`, async () => {
|
|
53
74
|
const host = mountSeat(render);
|
|
54
75
|
await nextTick();
|
|
@@ -59,20 +80,22 @@ describe("close-button family rides the standard icon button", () => {
|
|
|
59
80
|
// wrapper would leave the sheet styling the wrong box.
|
|
60
81
|
expect(control!.tagName, `${name} must be a button`).toBe("BUTTON");
|
|
61
82
|
|
|
83
|
+
const rung = (entry as typeof entry & { rung?: number }).rung ?? 24;
|
|
62
84
|
const tokens = [...control!.classList];
|
|
63
85
|
expect(tokens, `${name} carries the base layer`).toContain("hk-icon-button");
|
|
64
|
-
expect(tokens, `${name} carries the
|
|
86
|
+
expect(tokens, `${name} carries the ${rung}px rung`).toContain(
|
|
87
|
+
`hk-icon-button-${rung}`,
|
|
88
|
+
);
|
|
65
89
|
expect(tokens, `${name} carries the ghost variant`).toContain(
|
|
66
90
|
"hk-icon-button-ghost",
|
|
67
91
|
);
|
|
68
92
|
expect(tokens, `${name} carries its seat`).toContain(seat);
|
|
69
93
|
// Documented order: base, size, variant, seat.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
);
|
|
73
|
-
expect(
|
|
74
|
-
|
|
75
|
-
);
|
|
94
|
+
const base = tokens.indexOf("hk-icon-button");
|
|
95
|
+
const rungAt = tokens.indexOf(`hk-icon-button-${rung}`);
|
|
96
|
+
const ghost = tokens.indexOf("hk-icon-button-ghost");
|
|
97
|
+
expect(base).toBeLessThan(rungAt);
|
|
98
|
+
expect(rungAt).toBeLessThan(ghost);
|
|
76
99
|
expect(tokens.indexOf("hk-icon-button-ghost")).toBeLessThan(
|
|
77
100
|
tokens.indexOf(seat),
|
|
78
101
|
);
|
|
@@ -91,6 +114,19 @@ describe("close-button family rides the standard icon button", () => {
|
|
|
91
114
|
});
|
|
92
115
|
}
|
|
93
116
|
|
|
117
|
+
it("sources the wave-B1 field clears from the standard component too", () => {
|
|
118
|
+
// The token editor's clear lives inside an HkAdaptiveDialog slot (no
|
|
119
|
+
// bare mount path), so it is pinned at SOURCE level like the toast.
|
|
120
|
+
const token = readSource("HkTokenInput.tsx");
|
|
121
|
+
expect(token, "token clear rides HkIconButton").toMatch(
|
|
122
|
+
new RegExp('<HkIconButton[\\s\\S]{0,200}?class="hk-token-input-clear"'),
|
|
123
|
+
);
|
|
124
|
+
expect(token, "token clear is not a raw button").not.toMatch(
|
|
125
|
+
new RegExp('<button[^>]*class="hk-token-input-clear"'),
|
|
126
|
+
);
|
|
127
|
+
expect(token, "token clear rides the 24px rung").toContain("size={24}");
|
|
128
|
+
});
|
|
129
|
+
|
|
94
130
|
it("sources the toast controls from the standard component too", () => {
|
|
95
131
|
// HkToast is driven by useToast rather than props, so it cannot be
|
|
96
132
|
// mounted bare here; pin the source contract instead — the two controls
|