@dorsk/tsumikit 0.80.1 → 0.80.3
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.
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
{href}
|
|
36
36
|
class="link tone-{tone} underline-{underline} align-{align} {klass}"
|
|
37
37
|
data-tsu="Link"
|
|
38
|
-
{...rest}>{@render children?.()}</a
|
|
38
|
+
{...rest}><span class="link-text"><span class="link-ink">{@render children?.()}</span></span></a
|
|
39
39
|
>
|
|
40
40
|
{:else}
|
|
41
41
|
<button
|
|
42
42
|
class="link tone-{tone} underline-{underline} align-{align} {klass}"
|
|
43
43
|
data-tsu="Link"
|
|
44
44
|
{...rest}
|
|
45
|
-
type="button">{@render children?.()}</button
|
|
45
|
+
type="button"><span class="link-text"><span class="link-ink">{@render children?.()}</span></span></button
|
|
46
46
|
>
|
|
47
47
|
{/if}
|
|
48
48
|
|
|
@@ -102,44 +102,52 @@
|
|
|
102
102
|
--link-hold: var(--dur-instant);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
/* Sweep line: the underline fills in from the left and leaves to the right.
|
|
105
|
+
/* Sweep line: the underline fills in from the left and leaves to the right. Both variants paint
|
|
106
|
+
on the inline .link-ink so they follow the text whatever box the app gives .link. */
|
|
106
107
|
.link:not(.underline-none):not(:global([data-fx-link]:not([data-fx-link='sweep'])) *),
|
|
107
108
|
:global([data-fx-link='sweep']) .link:not(.underline-none):not(:global([data-fx-link='sweep'] [data-fx-link]:not([data-fx-link='sweep'])) *) {
|
|
108
109
|
--link-base: 0%;
|
|
109
110
|
text-decoration: none;
|
|
110
|
-
background-image:
|
|
111
|
-
linear-gradient(currentColor, currentColor),
|
|
112
|
-
linear-gradient(color-mix(in srgb, currentColor 30%, transparent), color-mix(in srgb, currentColor 30%, transparent));
|
|
113
|
-
background-repeat: no-repeat;
|
|
114
|
-
background-position: var(--link-on, 100%) 100%, 0 100%;
|
|
115
|
-
background-size: var(--link-draw, 0%) 1.5px, var(--link-base) 1px;
|
|
116
|
-
transition:
|
|
117
|
-
color var(--dur-fast) ease,
|
|
118
|
-
background-size var(--dur-fast) var(--ease-out);
|
|
119
111
|
&.underline-always {
|
|
120
112
|
--link-base: 100%;
|
|
121
113
|
}
|
|
114
|
+
.link-ink {
|
|
115
|
+
background-image:
|
|
116
|
+
linear-gradient(currentColor, currentColor),
|
|
117
|
+
linear-gradient(color-mix(in srgb, currentColor 30%, transparent), color-mix(in srgb, currentColor 30%, transparent));
|
|
118
|
+
background-repeat: no-repeat;
|
|
119
|
+
background-position: var(--link-on, 100%) 100%, 0 100%;
|
|
120
|
+
background-size: var(--link-draw, 0%) 1.5px, var(--link-base) 1px;
|
|
121
|
+
transition: background-size var(--dur-fast) var(--ease-out);
|
|
122
|
+
}
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
/* Highlighter: a marker swipes in behind the words and fades on leave. */
|
|
125
126
|
:global([data-fx-link='highlight']) .link:not(.underline-none):not(:global([data-fx-link='highlight'] [data-fx-link]:not([data-fx-link='highlight'])) *) {
|
|
126
127
|
margin-inline: -0.18em;
|
|
127
128
|
padding-inline: 0.18em;
|
|
128
|
-
border-radius: 0.2em 0.35em 0.25em 0.4em;
|
|
129
|
-
background-image: linear-gradient(100deg, var(--link-mark), color-mix(in srgb, var(--link-mark) 70%, transparent));
|
|
130
|
-
background-repeat: no-repeat;
|
|
131
|
-
background-size: var(--link-draw, 0%) 100%;
|
|
132
129
|
text-decoration-color: var(--link-deco, color-mix(in srgb, currentColor 45%, transparent));
|
|
133
130
|
text-underline-offset: 0.2em;
|
|
134
131
|
transition:
|
|
135
132
|
color var(--dur-fast) ease,
|
|
136
133
|
text-decoration-color var(--dur-fast) ease,
|
|
137
|
-
--link-mark var(--link-in, var(--dur-exit)) ease
|
|
138
|
-
|
|
134
|
+
--link-mark var(--link-in, var(--dur-exit)) ease;
|
|
135
|
+
.link-text,
|
|
136
|
+
.link-ink {
|
|
137
|
+
margin-inline: -0.18em;
|
|
138
|
+
padding-inline: 0.18em;
|
|
139
|
+
}
|
|
140
|
+
.link-ink {
|
|
141
|
+
border-radius: 0.2em 0.35em 0.25em 0.4em;
|
|
142
|
+
background-image: linear-gradient(100deg, var(--link-mark), color-mix(in srgb, var(--link-mark) 70%, transparent));
|
|
143
|
+
background-repeat: no-repeat;
|
|
144
|
+
background-size: var(--link-draw, 0%) 100%;
|
|
145
|
+
transition: background-size var(--link-grow, var(--dur-instant)) var(--ease-out) var(--link-hold, var(--dur-exit));
|
|
146
|
+
}
|
|
139
147
|
}
|
|
140
148
|
@property --link-mark {
|
|
141
149
|
syntax: '<color>';
|
|
142
|
-
inherits:
|
|
150
|
+
inherits: true;
|
|
143
151
|
initial-value: transparent;
|
|
144
152
|
}
|
|
145
153
|
@media (pointer: coarse) {
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
placement = 'inline',
|
|
39
39
|
orientation = 'stacked',
|
|
40
40
|
variant = 'flush',
|
|
41
|
+
labels = 'fit',
|
|
41
42
|
maxWidth,
|
|
42
43
|
class: klass = '',
|
|
43
44
|
style: styleProp = '',
|
|
@@ -57,6 +58,13 @@
|
|
|
57
58
|
orientation?: 'stacked' | 'inline';
|
|
58
59
|
/** 'floating' = rounded elevated card inset from the edges. */
|
|
59
60
|
variant?: 'flush' | 'floating';
|
|
61
|
+
/**
|
|
62
|
+
* Stacked labels that run out of room. 'fit' shrinks the font down to
|
|
63
|
+
* `--navbar-label-min` (10px) so the longest word fits, then wraps to two
|
|
64
|
+
* lines, then ellipsis. 'truncate' = one line with ellipsis at full size.
|
|
65
|
+
* 'full' never clips: labels spill past their item.
|
|
66
|
+
*/
|
|
67
|
+
labels?: 'fit' | 'truncate' | 'full';
|
|
60
68
|
/** Caps and centres the row of items. */
|
|
61
69
|
maxWidth?: string;
|
|
62
70
|
class?: string;
|
|
@@ -64,6 +72,23 @@
|
|
|
64
72
|
[key: string]: unknown;
|
|
65
73
|
} = $props();
|
|
66
74
|
|
|
75
|
+
// Widest word across the labels in em, rounded up from typical UI sans metrics.
|
|
76
|
+
const charEm = (c: string) =>
|
|
77
|
+
/[ijlI.,:;!|']/.test(c) ? 0.28
|
|
78
|
+
: /[frtJ]/.test(c) ? 0.4
|
|
79
|
+
: /[mwMW]/.test(c) ? 0.94
|
|
80
|
+
: /[A-Z]/.test(c) ? 0.72
|
|
81
|
+
: /[0-9]/.test(c) ? 0.58
|
|
82
|
+
: /[a-z]/.test(c) ? 0.6
|
|
83
|
+
: c >= '\u2e80' ? 1
|
|
84
|
+
: 0.62;
|
|
85
|
+
const labelEm = $derived(
|
|
86
|
+
Math.max(
|
|
87
|
+
1,
|
|
88
|
+
...items.flatMap((item) => item.label.split(/\s+/).map((w) => [...w].reduce((sum, c) => sum + charEm(c), 0)))
|
|
89
|
+
).toFixed(2)
|
|
90
|
+
);
|
|
91
|
+
|
|
67
92
|
const hasBadge = (badge: NavBarItem['badge']) =>
|
|
68
93
|
badge !== undefined && badge !== null && badge !== '' && badge !== 0;
|
|
69
94
|
|
|
@@ -151,11 +176,11 @@
|
|
|
151
176
|
<nav
|
|
152
177
|
data-tsu="NavBar"
|
|
153
178
|
aria-label={label}
|
|
154
|
-
class="navbar {klass}"
|
|
179
|
+
class="navbar labels-{labels} {klass}"
|
|
155
180
|
class:bar={placement === 'bar'}
|
|
156
181
|
class:floating={variant === 'floating'}
|
|
157
182
|
class:stacked={orientation === 'stacked'}
|
|
158
|
-
style="{maxWidth ?
|
|
183
|
+
style="--navbar-label-em: {labelEm};{maxWidth ? ` --navbar-max: ${maxWidth};` : ''}{styleProp}"
|
|
159
184
|
{...rest}
|
|
160
185
|
>
|
|
161
186
|
<div class="navbar-row">
|
|
@@ -314,6 +339,28 @@
|
|
|
314
339
|
.stacked .navbar-label {
|
|
315
340
|
max-width: 100%;
|
|
316
341
|
}
|
|
342
|
+
.stacked.labels-fit :is(.navbar-inner, .navbar-side) {
|
|
343
|
+
gap: 0;
|
|
344
|
+
}
|
|
345
|
+
.stacked.labels-fit .navbar-link {
|
|
346
|
+
container-type: inline-size;
|
|
347
|
+
contain-intrinsic-inline-size: 4rem;
|
|
348
|
+
padding-inline: 0;
|
|
349
|
+
}
|
|
350
|
+
.stacked.labels-fit .navbar-label {
|
|
351
|
+
display: -webkit-box;
|
|
352
|
+
-webkit-box-orient: vertical;
|
|
353
|
+
-webkit-line-clamp: 2;
|
|
354
|
+
line-clamp: 2;
|
|
355
|
+
font-size: clamp(var(--navbar-label-min, calc(0.625rem * var(--fs-scale))), 100cqi / var(--navbar-label-em), 1em);
|
|
356
|
+
line-height: var(--lh-tight);
|
|
357
|
+
text-align: center;
|
|
358
|
+
white-space: normal;
|
|
359
|
+
}
|
|
360
|
+
.labels-full .navbar-label {
|
|
361
|
+
max-width: none;
|
|
362
|
+
overflow: visible;
|
|
363
|
+
}
|
|
317
364
|
.navbar-badge {
|
|
318
365
|
position: absolute;
|
|
319
366
|
top: -0.35rem;
|
|
@@ -33,6 +33,13 @@ type $$ComponentProps = {
|
|
|
33
33
|
orientation?: 'stacked' | 'inline';
|
|
34
34
|
/** 'floating' = rounded elevated card inset from the edges. */
|
|
35
35
|
variant?: 'flush' | 'floating';
|
|
36
|
+
/**
|
|
37
|
+
* Stacked labels that run out of room. 'fit' shrinks the font down to
|
|
38
|
+
* `--navbar-label-min` (10px) so the longest word fits, then wraps to two
|
|
39
|
+
* lines, then ellipsis. 'truncate' = one line with ellipsis at full size.
|
|
40
|
+
* 'full' never clips: labels spill past their item.
|
|
41
|
+
*/
|
|
42
|
+
labels?: 'fit' | 'truncate' | 'full';
|
|
36
43
|
/** Caps and centres the row of items. */
|
|
37
44
|
maxWidth?: string;
|
|
38
45
|
class?: string;
|
|
@@ -239,13 +239,18 @@
|
|
|
239
239
|
const pos = el.selectionStart ?? value.length;
|
|
240
240
|
const id = ++reqId;
|
|
241
241
|
const next = key ? await suggestSingle(schema, key, value) : await suggest(schema, value, pos);
|
|
242
|
-
if (id !== reqId
|
|
242
|
+
if (id !== reqId || document.activeElement !== el) return;
|
|
243
243
|
menu = next;
|
|
244
244
|
active = 0;
|
|
245
245
|
keyboardNavigation = false;
|
|
246
246
|
open = !!next && next.items.length > 0;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
function close() {
|
|
250
|
+
++reqId;
|
|
251
|
+
open = false;
|
|
252
|
+
}
|
|
253
|
+
|
|
249
254
|
function accept(i: number) {
|
|
250
255
|
if (!menu || !el) return;
|
|
251
256
|
const s = menu.items[i];
|
|
@@ -269,7 +274,7 @@
|
|
|
269
274
|
el.focus();
|
|
270
275
|
el.setSelectionRange(caret, caret);
|
|
271
276
|
if (s.advance) refresh();
|
|
272
|
-
else
|
|
277
|
+
else close();
|
|
273
278
|
});
|
|
274
279
|
}
|
|
275
280
|
|
|
@@ -282,7 +287,7 @@
|
|
|
282
287
|
const exit = closingQuoteExit(value, pos);
|
|
283
288
|
if (exit !== null) {
|
|
284
289
|
e.preventDefault();
|
|
285
|
-
|
|
290
|
+
close();
|
|
286
291
|
setCaret(exit);
|
|
287
292
|
return;
|
|
288
293
|
}
|
|
@@ -299,6 +304,7 @@
|
|
|
299
304
|
}
|
|
300
305
|
if (!open || !menu) {
|
|
301
306
|
if (e.key === 'Enter') submit();
|
|
307
|
+
else if (e.key === 'Escape') close();
|
|
302
308
|
return;
|
|
303
309
|
}
|
|
304
310
|
switch (e.key) {
|
|
@@ -319,13 +325,13 @@
|
|
|
319
325
|
break;
|
|
320
326
|
case 'Escape':
|
|
321
327
|
e.preventDefault();
|
|
322
|
-
|
|
328
|
+
close();
|
|
323
329
|
break;
|
|
324
330
|
}
|
|
325
331
|
}
|
|
326
332
|
|
|
327
333
|
function submit() {
|
|
328
|
-
|
|
334
|
+
close();
|
|
329
335
|
onsubmit?.(value);
|
|
330
336
|
}
|
|
331
337
|
|
|
@@ -399,7 +405,9 @@
|
|
|
399
405
|
}}
|
|
400
406
|
onblur={() => {
|
|
401
407
|
focused = false;
|
|
402
|
-
setTimeout(() =>
|
|
408
|
+
setTimeout(() => {
|
|
409
|
+
if (document.activeElement !== el) close();
|
|
410
|
+
}, 120);
|
|
403
411
|
}}
|
|
404
412
|
{onkeydown}
|
|
405
413
|
/>
|
package/package.json
CHANGED