@streamscloud/kit 0.78.1 → 0.80.0
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/dist/ui/dialog/cmp.dialog.svelte +20 -17
- package/dist/ui/dialog/cmp.dialog.svelte.d.ts +8 -8
- package/dist/ui/dialog/layout/cmp.stepper-dialog.svelte +4 -4
- package/dist/ui/dialog/layout/cmp.stepper-dialog.svelte.d.ts +1 -0
- package/dist/ui/numeral-input/cmp.numeral-input.svelte +112 -97
- package/dist/ui/numeral-input/cmp.numeral-input.svelte.d.ts +22 -13
- package/dist/ui/tile-card/cmp.tile-card.svelte +99 -17
- package/dist/ui/tile-card/cmp.tile-card.svelte.d.ts +8 -11
- package/dist/ui/tile-card/index.d.ts +1 -1
- package/dist/ui/tile-card/types.d.ts +20 -0
- package/package.json +1 -1
|
@@ -51,15 +51,15 @@ A structured dialog panel with header (title + close button), scrollable body, a
|
|
|
51
51
|
| `--sc-kit--dialog--height` | Dialog height | `auto` |
|
|
52
52
|
| `--sc-kit--dialog--max-height` | Maximum height | `none` |
|
|
53
53
|
| `--sc-kit--dialog--min-height` | Minimum height | `auto` |
|
|
54
|
-
| `--sc-kit--dialog--padding-block` | Vertical padding for header/body/footer | `--sc-kit--space--5` |
|
|
55
|
-
| `--sc-kit--dialog--padding-inline` | Horizontal padding for header/body/footer | `--sc-kit--space--6` |
|
|
54
|
+
| `--sc-kit--dialog--padding-block` | Vertical padding for header/body/footer | `--sc-kit--space--5`; `14px` when the dialog is under 768px wide inside `DialogHost` |
|
|
55
|
+
| `--sc-kit--dialog--padding-inline` | Horizontal padding for header/body/footer | `--sc-kit--space--6`; `--sc-kit--space--4` when the dialog is under 768px wide inside `DialogHost` |
|
|
56
56
|
| `--sc-kit--dialog--body--overflow-y` | Body vertical overflow | `visible` |
|
|
57
|
-
| `--sc-kit--dialog--body--padding-block` | Body-specific vertical padding |
|
|
58
|
-
| `--sc-kit--dialog--body--padding-inline` | Body-specific horizontal padding |
|
|
59
|
-
| `--sc-kit--dialog--header--padding-block` | Header-specific vertical padding |
|
|
60
|
-
| `--sc-kit--dialog--header--padding-inline` | Header-specific horizontal padding |
|
|
61
|
-
| `--sc-kit--dialog--footer--padding-block` | Footer-specific vertical padding |
|
|
62
|
-
| `--sc-kit--dialog--footer--padding-inline` | Footer-specific horizontal padding |
|
|
57
|
+
| `--sc-kit--dialog--body--padding-block` | Body-specific vertical padding | `--sc-kit--dialog--padding-block` |
|
|
58
|
+
| `--sc-kit--dialog--body--padding-inline` | Body-specific horizontal padding | `--sc-kit--dialog--padding-inline` |
|
|
59
|
+
| `--sc-kit--dialog--header--padding-block` | Header-specific vertical padding | `--sc-kit--dialog--padding-block` |
|
|
60
|
+
| `--sc-kit--dialog--header--padding-inline` | Header-specific horizontal padding | `--sc-kit--dialog--padding-inline` |
|
|
61
|
+
| `--sc-kit--dialog--footer--padding-block` | Footer-specific vertical padding | `--sc-kit--dialog--padding-block` |
|
|
62
|
+
| `--sc-kit--dialog--footer--padding-inline` | Footer-specific horizontal padding | `--sc-kit--dialog--padding-inline` |
|
|
63
63
|
| `--sc-kit--dialog--title--font-size` | Title font size | `1.125rem` (18px) |
|
|
64
64
|
| `--sc-kit--dialog--title--line-height` | Title line height | `1.4` |
|
|
65
65
|
-->
|
|
@@ -73,13 +73,15 @@ A structured dialog panel with header (title + close button), scrollable body, a
|
|
|
73
73
|
--_dialog--height: var(--sc-kit--dialog--height, auto);
|
|
74
74
|
--_dialog--max-height: var(--sc-kit--dialog--max-height, none);
|
|
75
75
|
--_dialog--min-height: var(--sc-kit--dialog--min-height, auto);
|
|
76
|
-
--_dialog--padding-block: var(--sc-kit--
|
|
77
|
-
--_dialog--padding-inline: var(--sc-kit--
|
|
76
|
+
--_dialog--width-padding-block: var(--sc-kit--space--5);
|
|
77
|
+
--_dialog--width-padding-inline: var(--sc-kit--space--6);
|
|
78
|
+
--_dialog--padding-block: var(--sc-kit--dialog--padding-block, var(--_dialog--width-padding-block));
|
|
79
|
+
--_dialog--padding-inline: var(--sc-kit--dialog--padding-inline, var(--_dialog--width-padding-inline));
|
|
78
80
|
--_dialog--body--overflow-y: var(--sc-kit--dialog--body--overflow-y, visible);
|
|
79
|
-
--
|
|
80
|
-
--
|
|
81
|
-
--
|
|
82
|
-
--
|
|
81
|
+
--_--dialog--body--padding-block: var(--sc-kit--dialog--body--padding-block, var(--_dialog--padding-block));
|
|
82
|
+
--_--dialog--body--padding-inline: var(--sc-kit--dialog--body--padding-inline, var(--_dialog--padding-inline));
|
|
83
|
+
--_--dialog--header--padding-block: var(--sc-kit--dialog--header--padding-block, var(--_dialog--padding-block));
|
|
84
|
+
--_--dialog--header--padding-inline: var(--sc-kit--dialog--header--padding-inline, var(--_dialog--padding-inline));
|
|
83
85
|
--_dialog--footer--padding-block: var(--sc-kit--dialog--footer--padding-block, var(--_dialog--padding-block));
|
|
84
86
|
--_dialog--footer--padding-inline: var(--sc-kit--dialog--footer--padding-inline, var(--_dialog--padding-inline));
|
|
85
87
|
--_dialog--title-font-size: var(--sc-kit--dialog--title--font-size, 1.125rem);
|
|
@@ -98,7 +100,8 @@ A structured dialog panel with header (title + close button), scrollable body, a
|
|
|
98
100
|
}
|
|
99
101
|
@container (width < 768px) {
|
|
100
102
|
.dialog {
|
|
101
|
-
--_dialog--padding-
|
|
103
|
+
--_dialog--width-padding-block: 0.875rem;
|
|
104
|
+
--_dialog--width-padding-inline: var(--sc-kit--space--4);
|
|
102
105
|
}
|
|
103
106
|
}
|
|
104
107
|
.dialog__header {
|
|
@@ -107,7 +110,7 @@ A structured dialog panel with header (title + close button), scrollable body, a
|
|
|
107
110
|
align-items: center;
|
|
108
111
|
justify-content: space-between;
|
|
109
112
|
gap: var(--sc-kit--space--4);
|
|
110
|
-
padding: var(--
|
|
113
|
+
padding: var(--_--dialog--header--padding-block) var(--_--dialog--header--padding-inline);
|
|
111
114
|
border-bottom: 1px solid var(--_dialog--border-color);
|
|
112
115
|
-webkit-user-drag: none;
|
|
113
116
|
user-select: none;
|
|
@@ -130,7 +133,7 @@ A structured dialog panel with header (title + close button), scrollable body, a
|
|
|
130
133
|
flex: 1;
|
|
131
134
|
min-height: 0;
|
|
132
135
|
overflow-y: var(--_dialog--body--overflow-y);
|
|
133
|
-
padding: var(--
|
|
136
|
+
padding: var(--_--dialog--body--padding-block) var(--_--dialog--body--padding-inline);
|
|
134
137
|
--_cross-browser-scrollbar--thumb-color: var(--scrollbar--thumb-color, light-dark(#d1d5db, #4b5563));
|
|
135
138
|
--_cross-browser-scrollbar--track-color: var(--scrollbar--track-color, transparent);
|
|
136
139
|
}
|
|
@@ -52,15 +52,15 @@ interface $$IsomorphicComponent {
|
|
|
52
52
|
* | `--sc-kit--dialog--height` | Dialog height | `auto` |
|
|
53
53
|
* | `--sc-kit--dialog--max-height` | Maximum height | `none` |
|
|
54
54
|
* | `--sc-kit--dialog--min-height` | Minimum height | `auto` |
|
|
55
|
-
* | `--sc-kit--dialog--padding-block` | Vertical padding for header/body/footer | `--sc-kit--space--5` |
|
|
56
|
-
* | `--sc-kit--dialog--padding-inline` | Horizontal padding for header/body/footer | `--sc-kit--space--6` |
|
|
55
|
+
* | `--sc-kit--dialog--padding-block` | Vertical padding for header/body/footer | `--sc-kit--space--5`; `14px` when the dialog is under 768px wide inside `DialogHost` |
|
|
56
|
+
* | `--sc-kit--dialog--padding-inline` | Horizontal padding for header/body/footer | `--sc-kit--space--6`; `--sc-kit--space--4` when the dialog is under 768px wide inside `DialogHost` |
|
|
57
57
|
* | `--sc-kit--dialog--body--overflow-y` | Body vertical overflow | `visible` |
|
|
58
|
-
* | `--sc-kit--dialog--body--padding-block` | Body-specific vertical padding |
|
|
59
|
-
* | `--sc-kit--dialog--body--padding-inline` | Body-specific horizontal padding |
|
|
60
|
-
* | `--sc-kit--dialog--header--padding-block` | Header-specific vertical padding |
|
|
61
|
-
* | `--sc-kit--dialog--header--padding-inline` | Header-specific horizontal padding |
|
|
62
|
-
* | `--sc-kit--dialog--footer--padding-block` | Footer-specific vertical padding |
|
|
63
|
-
* | `--sc-kit--dialog--footer--padding-inline` | Footer-specific horizontal padding |
|
|
58
|
+
* | `--sc-kit--dialog--body--padding-block` | Body-specific vertical padding | `--sc-kit--dialog--padding-block` |
|
|
59
|
+
* | `--sc-kit--dialog--body--padding-inline` | Body-specific horizontal padding | `--sc-kit--dialog--padding-inline` |
|
|
60
|
+
* | `--sc-kit--dialog--header--padding-block` | Header-specific vertical padding | `--sc-kit--dialog--padding-block` |
|
|
61
|
+
* | `--sc-kit--dialog--header--padding-inline` | Header-specific horizontal padding | `--sc-kit--dialog--padding-inline` |
|
|
62
|
+
* | `--sc-kit--dialog--footer--padding-block` | Footer-specific vertical padding | `--sc-kit--dialog--padding-block` |
|
|
63
|
+
* | `--sc-kit--dialog--footer--padding-inline` | Footer-specific horizontal padding | `--sc-kit--dialog--padding-inline` |
|
|
64
64
|
* | `--sc-kit--dialog--title--font-size` | Title font size | `1.125rem` (18px) |
|
|
65
65
|
* | `--sc-kit--dialog--title--line-height` | Title line height | `1.4` |
|
|
66
66
|
*/
|
|
@@ -103,6 +103,7 @@ subtitle / close), vertical Stepper sidebar, scrollable body, and a Cancel / Bac
|
|
|
103
103
|
footer. Render it inside your own dialog view opened via `Dialogs.open()` — the layout owns step
|
|
104
104
|
navigation and the submitting state; your `on.finish` owns closing (`controller.ok(...)`).
|
|
105
105
|
Completed sidebar steps are clickable to go back; `canAdvance(step)` gates Next / Finish.
|
|
106
|
+
Header, body and sidebar block padding follow `Dialog`'s header / body padding vars.
|
|
106
107
|
|
|
107
108
|
### CSS Custom Properties
|
|
108
109
|
| Property | Description | Default |
|
|
@@ -116,14 +117,13 @@ Completed sidebar steps are clickable to go back; `canAdvance(step)` gates Next
|
|
|
116
117
|
--_sd--sidebar-width: var(--sc-kit--stepper-dialog--sidebar--width, 20rem);
|
|
117
118
|
--_sd--sidebar-background: var(--sc-kit--stepper-dialog--sidebar--background, var(--sc-kit--color--bg--field-alt));
|
|
118
119
|
--sc-kit--dialog--height: var(--sc-kit--stepper-dialog--height, auto);
|
|
119
|
-
--sc-kit--dialog--body--overflow-y: auto;
|
|
120
120
|
display: contents;
|
|
121
121
|
}
|
|
122
122
|
.stepper-dialog__header {
|
|
123
123
|
display: flex;
|
|
124
124
|
align-items: flex-start;
|
|
125
125
|
gap: var(--sc-kit--space--3);
|
|
126
|
-
padding: var(--
|
|
126
|
+
padding: var(--_--dialog--header--padding-block) var(--_--dialog--header--padding-inline);
|
|
127
127
|
border-block-end: 1px solid var(--sc-kit--color--border);
|
|
128
128
|
}
|
|
129
129
|
.stepper-dialog__icon {
|
|
@@ -169,7 +169,7 @@ Completed sidebar steps are clickable to go back; `canAdvance(step)` gates Next
|
|
|
169
169
|
}
|
|
170
170
|
.stepper-dialog__sidebar {
|
|
171
171
|
flex: 0 0 var(--_sd--sidebar-width);
|
|
172
|
-
padding: var(--
|
|
172
|
+
padding: var(--_--dialog--body--padding-block) var(--sc-kit--space--4);
|
|
173
173
|
background: var(--_sd--sidebar-background);
|
|
174
174
|
border-inline-end: 1px solid var(--sc-kit--color--border);
|
|
175
175
|
overflow-y: auto;
|
|
@@ -193,5 +193,5 @@ Completed sidebar steps are clickable to go back; `canAdvance(step)` gates Next
|
|
|
193
193
|
flex: 1;
|
|
194
194
|
min-block-size: 0;
|
|
195
195
|
overflow-y: auto;
|
|
196
|
-
padding: var(--
|
|
196
|
+
padding: var(--_--dialog--body--padding-block) var(--_--dialog--body--padding-inline);
|
|
197
197
|
}</style>
|
|
@@ -53,6 +53,7 @@ interface $$IsomorphicComponent {
|
|
|
53
53
|
* footer. Render it inside your own dialog view opened via `Dialogs.open()` — the layout owns step
|
|
54
54
|
* navigation and the submitting state; your `on.finish` owns closing (`controller.ok(...)`).
|
|
55
55
|
* Completed sidebar steps are clickable to go back; `canAdvance(step)` gates Next / Finish.
|
|
56
|
+
* Header, body and sidebar block padding follow `Dialog`'s header / body padding vars.
|
|
56
57
|
*
|
|
57
58
|
* ### CSS Custom Properties
|
|
58
59
|
* | Property | Description | Default |
|
|
@@ -1,120 +1,113 @@
|
|
|
1
1
|
<script lang="ts">import { InputClearButton } from '../_internal/input-clear-button';
|
|
2
|
-
import {
|
|
2
|
+
import { IconSlot } from '../icon';
|
|
3
3
|
import { formatNumeral, NumeralThousandGroupStyles, registerCursorTracker } from 'cleave-zen';
|
|
4
|
-
|
|
4
|
+
import { untrack } from 'svelte';
|
|
5
|
+
const { size = 'md', disabled = false, readonly = false, inert = false, error = false, borderless = false, placeholder = '', title = '', name = '', id = null, autofocus = false, min = null, max = null, decimals = 2, suffix, hideResolvedValue = false, 'aria-describedby': ariaDescribedby, 'aria-label': ariaLabel, 'aria-required': ariaRequired, icon, iconPosition = 'leading', secondaryIcon, ...mode } = $props();
|
|
5
6
|
let inputEl = $state.raw(undefined);
|
|
7
|
+
let focused = $state(false);
|
|
8
|
+
let typedValue = $state(undefined);
|
|
6
9
|
const formatOptions = $derived({
|
|
7
10
|
delimiter: ' ',
|
|
8
11
|
numeralDecimalScale: decimals,
|
|
9
12
|
numeralThousandsGroupStyle: NumeralThousandGroupStyles.THOUSAND,
|
|
10
13
|
numeralIntegerScale: 14
|
|
11
14
|
});
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
const toRoundedPlainString = (value) => Number(value.toFixed(decimals))
|
|
16
|
+
.toFixed(decimals)
|
|
17
|
+
.replace(/(\.\d*?)0+$/, '$1')
|
|
18
|
+
.replace(/\.$/, '');
|
|
19
|
+
const formatValue = (value) => value === null || value === undefined ? '' : formatNumeral(toRoundedPlainString(value), formatOptions);
|
|
20
|
+
const displayValue = $derived(formatValue(mode.value));
|
|
14
21
|
const clearButtonVisible = $derived(mode.clearable === true && !disabled && !readonly && !inert && mode.value !== null && mode.value !== undefined);
|
|
15
22
|
/** Imperative API exposed via `bind:this`. */
|
|
16
23
|
export const focus = () => inputEl?.focus();
|
|
17
24
|
export const select = () => inputEl?.select();
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
// Strip suffix + non-numeric chars except `.` `,` `-`, then normalize to dot-decimal.
|
|
24
|
-
let cleaned = suffix ? raw.replace(suffix, '') : raw;
|
|
25
|
-
cleaned = cleaned
|
|
26
|
-
.replace(/\s/g, '')
|
|
27
|
-
.replace(',', '.')
|
|
28
|
-
.replace(/[^0-9.-]/g, '');
|
|
29
|
-
const n = parseFloat(cleaned);
|
|
30
|
-
if (Number.isNaN(n)) {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
let clamped = n;
|
|
34
|
-
if (min !== null && clamped < min) {
|
|
35
|
-
clamped = min;
|
|
36
|
-
}
|
|
37
|
-
if (max !== null && clamped > max) {
|
|
38
|
-
clamped = max;
|
|
39
|
-
}
|
|
40
|
-
return Number(clamped.toFixed(decimals));
|
|
25
|
+
const parseValue = (formatted) => {
|
|
26
|
+
const n = parseFloat(formatted.replace(/\s/g, ''));
|
|
27
|
+
return Number.isNaN(n) ? null : n;
|
|
41
28
|
};
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
node.focus();
|
|
29
|
+
const resolveValue = (typed) => {
|
|
30
|
+
if (typed !== null) {
|
|
31
|
+
return Math.min(Math.max(typed, min ?? -Infinity), max ?? Infinity);
|
|
46
32
|
}
|
|
47
|
-
|
|
48
|
-
mode.on?.mounted?.({ input: node });
|
|
33
|
+
return mode.clearable ? null : (min ?? mode.value);
|
|
49
34
|
};
|
|
50
|
-
const
|
|
35
|
+
const resolvedTypedValue = $derived(typedValue === undefined ? undefined : resolveValue(typedValue));
|
|
36
|
+
const valueHintVisible = $derived(!hideResolvedValue && resolvedTypedValue !== typedValue);
|
|
37
|
+
const initialText = untrack(() => displayValue);
|
|
38
|
+
// Writing the prop into the field while focused would overwrite a typed "1" with min "8" before "12" can be typed.
|
|
39
|
+
$effect(() => {
|
|
40
|
+
if (inputEl && !focused) {
|
|
41
|
+
inputEl.value = displayValue;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
const emit = (kind, value) => {
|
|
51
45
|
if (mode.clearable) {
|
|
52
|
-
|
|
53
|
-
cb?.(parsed);
|
|
46
|
+
mode.on?.[kind]?.(value);
|
|
54
47
|
}
|
|
55
|
-
else {
|
|
56
|
-
|
|
57
|
-
if (parsed !== null) {
|
|
58
|
-
mode.on?.[kind]?.(parsed);
|
|
59
|
-
}
|
|
48
|
+
else if (value !== null) {
|
|
49
|
+
mode.on?.[kind]?.(value);
|
|
60
50
|
}
|
|
61
51
|
};
|
|
52
|
+
// Safari fires no native change once script has rewritten the value during input, so Enter and blur commit too.
|
|
53
|
+
const commit = () => {
|
|
54
|
+
if (!inputEl || typedValue === undefined) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const value = resolveValue(typedValue);
|
|
58
|
+
typedValue = undefined;
|
|
59
|
+
inputEl.value = formatValue(value);
|
|
60
|
+
emit('change', value);
|
|
61
|
+
};
|
|
62
62
|
const handleInput = () => {
|
|
63
63
|
if (!inputEl) {
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// get mangled, re-add nothing here (suffix re-appears on blur via displayValue resync).
|
|
69
|
-
const raw = suffix ? inputEl.value.replace(suffix, '') : inputEl.value;
|
|
70
|
-
let formatted = formatNumeral(raw, formatOptions);
|
|
71
|
-
// Clamp on the fly when typing pushes the value past min / max — re-format the clamped
|
|
72
|
-
// result so the visible input matches the emitted value.
|
|
73
|
-
const parsedRaw = parseFloat(formatted.replace(/\s/g, '').replace(',', '.'));
|
|
74
|
-
if (!Number.isNaN(parsedRaw)) {
|
|
75
|
-
let clamped = parsedRaw;
|
|
76
|
-
if (min !== null && clamped < min) {
|
|
77
|
-
clamped = min;
|
|
78
|
-
}
|
|
79
|
-
if (max !== null && clamped > max) {
|
|
80
|
-
clamped = max;
|
|
81
|
-
}
|
|
82
|
-
if (clamped !== parsedRaw) {
|
|
83
|
-
formatted = formatNumeral(String(Number(clamped.toFixed(decimals))), formatOptions);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
66
|
+
const raw = inputEl.value.includes('.') ? inputEl.value : inputEl.value.replace(',', '.');
|
|
67
|
+
const formatted = formatNumeral(raw, formatOptions);
|
|
86
68
|
if (inputEl.value !== formatted) {
|
|
87
69
|
inputEl.value = formatted;
|
|
88
70
|
}
|
|
89
|
-
|
|
90
|
-
emit(
|
|
71
|
+
typedValue = parseValue(formatted);
|
|
72
|
+
emit('input', resolveValue(typedValue));
|
|
91
73
|
};
|
|
92
|
-
const
|
|
93
|
-
if (!
|
|
94
|
-
|
|
74
|
+
const handleKeydown = (event) => {
|
|
75
|
+
if (event.key === 'Enter' && !event.isComposing) {
|
|
76
|
+
commit();
|
|
95
77
|
}
|
|
96
|
-
const parsed = parseValue(inputEl.value);
|
|
97
|
-
emit(parsed, 'change');
|
|
98
78
|
};
|
|
99
79
|
const handleClear = () => {
|
|
100
80
|
if (!inputEl) {
|
|
101
81
|
return;
|
|
102
82
|
}
|
|
103
83
|
inputEl.value = '';
|
|
104
|
-
|
|
105
|
-
emit(
|
|
84
|
+
typedValue = undefined;
|
|
85
|
+
emit('input', null);
|
|
86
|
+
emit('change', null);
|
|
106
87
|
inputEl.focus();
|
|
107
88
|
};
|
|
89
|
+
const handleFocus = () => {
|
|
90
|
+
focused = true;
|
|
91
|
+
mode.on?.focus?.();
|
|
92
|
+
};
|
|
108
93
|
const handleBlur = () => {
|
|
109
|
-
|
|
110
|
-
|
|
94
|
+
commit();
|
|
95
|
+
focused = false;
|
|
96
|
+
mode.on?.blur?.();
|
|
97
|
+
};
|
|
98
|
+
const initInput = (node) => {
|
|
99
|
+
inputEl = node;
|
|
100
|
+
// A field focused before hydration never gets a focus event, so the sync effect would overwrite what is typed next.
|
|
101
|
+
if (node.matches(':focus')) {
|
|
102
|
+
handleFocus();
|
|
103
|
+
}
|
|
104
|
+
if (autofocus) {
|
|
105
|
+
node.focus();
|
|
111
106
|
}
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
emit(parsed, 'blur');
|
|
107
|
+
const unregisterCursorTracker = registerCursorTracker({ input: node, delimiter: formatOptions.delimiter, delimiters: [] });
|
|
108
|
+
mode.on?.mounted?.({ input: node });
|
|
109
|
+
return { destroy: unregisterCursorTracker };
|
|
116
110
|
};
|
|
117
|
-
const focusInput = () => inputEl?.focus();
|
|
118
111
|
</script>
|
|
119
112
|
|
|
120
113
|
<div
|
|
@@ -124,18 +117,14 @@ const focusInput = () => inputEl?.focus();
|
|
|
124
117
|
class:numeral-input--inert={inert}
|
|
125
118
|
class:numeral-input--error={error}
|
|
126
119
|
class:numeral-input--borderless={borderless}
|
|
127
|
-
onclick={
|
|
120
|
+
onclick={focus}
|
|
128
121
|
onkeydown={() => undefined}
|
|
129
122
|
role="none">
|
|
130
123
|
{#if icon && iconPosition === 'leading'}
|
|
131
|
-
<span class="numeral-input__icon numeral-input__icon--leading" aria-hidden="true">
|
|
132
|
-
{#if typeof icon === 'string'}<Icon src={icon} />{:else}{@render icon()}{/if}
|
|
133
|
-
</span>
|
|
124
|
+
<span class="numeral-input__icon numeral-input__icon--leading" aria-hidden="true"><IconSlot icon={icon} /></span>
|
|
134
125
|
{/if}
|
|
135
126
|
{#if secondaryIcon && iconPosition === 'trailing'}
|
|
136
|
-
<span class="numeral-input__icon numeral-input__icon--leading" aria-hidden="true">
|
|
137
|
-
{#if typeof secondaryIcon === 'string'}<Icon src={secondaryIcon} />{:else}{@render secondaryIcon()}{/if}
|
|
138
|
-
</span>
|
|
127
|
+
<span class="numeral-input__icon numeral-input__icon--leading" aria-hidden="true"><IconSlot icon={secondaryIcon} /></span>
|
|
139
128
|
{/if}
|
|
140
129
|
|
|
141
130
|
<!-- svelte-ignore a11y_autofocus -->
|
|
@@ -152,38 +141,49 @@ const focusInput = () => inputEl?.focus();
|
|
|
152
141
|
inert={inert}
|
|
153
142
|
autofocus={autofocus}
|
|
154
143
|
title={title}
|
|
155
|
-
value={
|
|
144
|
+
value={initialText}
|
|
156
145
|
aria-invalid={error ? 'true' : undefined}
|
|
157
146
|
aria-describedby={ariaDescribedby}
|
|
158
147
|
aria-label={ariaLabel}
|
|
159
148
|
aria-required={ariaRequired ? 'true' : undefined}
|
|
160
149
|
oninput={handleInput}
|
|
161
|
-
onchange={
|
|
150
|
+
onchange={commit}
|
|
162
151
|
onblur={handleBlur}
|
|
163
|
-
onfocus={
|
|
152
|
+
onfocus={handleFocus}
|
|
153
|
+
onkeydown={handleKeydown} />
|
|
154
|
+
|
|
155
|
+
{#if valueHintVisible}
|
|
156
|
+
<span class="numeral-input__value-hint"><span aria-hidden="true">→</span> {formatValue(resolvedTypedValue)}</span>
|
|
157
|
+
{/if}
|
|
158
|
+
|
|
159
|
+
{#if suffix}
|
|
160
|
+
<span class="numeral-input__suffix">{suffix}</span>
|
|
161
|
+
{/if}
|
|
164
162
|
|
|
165
163
|
{#if clearButtonVisible}
|
|
166
164
|
<InputClearButton size={size} on={{ clear: handleClear }} />
|
|
167
165
|
{/if}
|
|
168
166
|
|
|
169
167
|
{#if icon && iconPosition === 'trailing'}
|
|
170
|
-
<span class="numeral-input__icon numeral-input__icon--trailing" aria-hidden="true">
|
|
171
|
-
{#if typeof icon === 'string'}<Icon src={icon} />{:else}{@render icon()}{/if}
|
|
172
|
-
</span>
|
|
168
|
+
<span class="numeral-input__icon numeral-input__icon--trailing" aria-hidden="true"><IconSlot icon={icon} /></span>
|
|
173
169
|
{/if}
|
|
174
170
|
{#if secondaryIcon && iconPosition === 'leading'}
|
|
175
|
-
<span class="numeral-input__icon numeral-input__icon--trailing" aria-hidden="true">
|
|
176
|
-
{#if typeof secondaryIcon === 'string'}<Icon src={secondaryIcon} />{:else}{@render secondaryIcon()}{/if}
|
|
177
|
-
</span>
|
|
171
|
+
<span class="numeral-input__icon numeral-input__icon--trailing" aria-hidden="true"><IconSlot icon={secondaryIcon} /></span>
|
|
178
172
|
{/if}
|
|
179
173
|
</div>
|
|
180
174
|
|
|
181
175
|
<!--
|
|
182
176
|
@component
|
|
183
|
-
Formatted number input with
|
|
184
|
-
|
|
185
|
-
Discriminated by `clearable`: `clearable: true` allows `null` (with × button);
|
|
186
|
-
requires a non-null value. CSS API mirrors `Input` for visual-language compatibility.
|
|
177
|
+
Formatted number input with space-grouped thousands, a dot or comma decimal mark, decimal scale, min/max clamping,
|
|
178
|
+
a muted trailing `suffix` unit, and smooth cursor tracking while typing
|
|
179
|
+
(via cleave-zen). Discriminated by `clearable`: `clearable: true` allows `null` (with × button);
|
|
180
|
+
`clearable: false` requires a non-null value. CSS API mirrors `Input` for visual-language compatibility.
|
|
181
|
+
|
|
182
|
+
While focused the text belongs to the user: a value change from outside lands on blur. Every
|
|
183
|
+
emitted value is clamped to `min` / `max`, but the typed text is left alone until commit (blur,
|
|
184
|
+
Enter, `change`) — so a prefix below `min` stays typeable. With `clearable: false` an empty
|
|
185
|
+
field emits `min`, or keeps the held value when there is no `min`. Whenever the typed text resolves
|
|
186
|
+
to a different value, that value shows next to the field as `→ 12` (off with `hideResolvedValue`).
|
|
187
187
|
|
|
188
188
|
### CSS Custom Properties
|
|
189
189
|
| Property | Description | Default |
|
|
@@ -204,6 +204,7 @@ requires a non-null value. CSS API mirrors `Input` for visual-language compatibi
|
|
|
204
204
|
| `--sc-kit--numeral-input--placeholder--color` | Placeholder text color | `var(--sc-kit--color--text--placeholder)` |
|
|
205
205
|
| `--sc-kit--numeral-input--icon--color` | Leading / trailing icon color (does NOT affect inner affordances) | `var(--sc-kit--color--text--muted)` |
|
|
206
206
|
| `--sc-kit--numeral-input--icon--size` | Leading / trailing icon size (does NOT affect inner affordances) | per size |
|
|
207
|
+
| `--sc-kit--numeral-input--suffix--color` | Suffix and value hint color | `var(--sc-kit--color--text--muted)` |
|
|
207
208
|
-->
|
|
208
209
|
|
|
209
210
|
<style>.numeral-input {
|
|
@@ -224,6 +225,7 @@ requires a non-null value. CSS API mirrors `Input` for visual-language compatibi
|
|
|
224
225
|
--_ni--placeholder-color: var(--sc-kit--numeral-input--placeholder--color, var(--sc-kit--color--text--placeholder));
|
|
225
226
|
--_ni--icon-color: var(--sc-kit--numeral-input--icon--color, var(--sc-kit--color--text--muted));
|
|
226
227
|
--_ni--icon-size: var(--sc-kit--numeral-input--icon--size, var(--_ni--size-icon-size, var(--sc-kit--field--icon--size--md)));
|
|
228
|
+
--_ni--suffix-color: var(--sc-kit--numeral-input--suffix--color, var(--sc-kit--color--text--muted));
|
|
227
229
|
--_--input-affordance--opacity: 0;
|
|
228
230
|
--_--input-affordance--scale: 0;
|
|
229
231
|
}
|
|
@@ -321,6 +323,19 @@ requires a non-null value. CSS API mirrors `Input` for visual-language compatibi
|
|
|
321
323
|
.numeral-input__el:disabled {
|
|
322
324
|
cursor: default;
|
|
323
325
|
}
|
|
326
|
+
.numeral-input__value-hint {
|
|
327
|
+
flex-shrink: 0;
|
|
328
|
+
margin-inline-start: var(--_ni--gap);
|
|
329
|
+
color: var(--_ni--suffix-color);
|
|
330
|
+
font-variant-numeric: tabular-nums;
|
|
331
|
+
white-space: nowrap;
|
|
332
|
+
}
|
|
333
|
+
.numeral-input__suffix {
|
|
334
|
+
flex-shrink: 0;
|
|
335
|
+
margin-inline-start: var(--_ni--gap);
|
|
336
|
+
color: var(--_ni--suffix-color);
|
|
337
|
+
white-space: nowrap;
|
|
338
|
+
}
|
|
324
339
|
.numeral-input__icon {
|
|
325
340
|
--sc-kit--icon--color: var(--_ni--icon-color);
|
|
326
341
|
--sc-kit--icon--size: var(--_ni--icon-size);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type IconProp } from '../icon';
|
|
2
2
|
type BaseProps = {
|
|
3
3
|
/** @default 'md' */
|
|
4
4
|
size?: 'sm' | 'md' | 'lg';
|
|
@@ -16,23 +16,25 @@ type BaseProps = {
|
|
|
16
16
|
name?: string;
|
|
17
17
|
id?: string | null;
|
|
18
18
|
autofocus?: boolean;
|
|
19
|
-
/** Minimum
|
|
19
|
+
/** Minimum value emitted to the consumer, also emitted for an empty `clearable: false` field; the typed text is left alone until commit. */
|
|
20
20
|
min?: number | null;
|
|
21
|
-
/** Maximum
|
|
21
|
+
/** Maximum value emitted to the consumer; the typed text is left alone until commit. */
|
|
22
22
|
max?: number | null;
|
|
23
23
|
/** Number of decimal places shown. @default 2 */
|
|
24
24
|
decimals?: number;
|
|
25
|
-
/**
|
|
25
|
+
/** Muted unit rendered after the value, outside the editable text (`px`, `%`, `°`). */
|
|
26
26
|
suffix?: string;
|
|
27
|
+
/** Hides the `→ 12` hint shown while the typed text resolves to a different value — for narrow fields. */
|
|
28
|
+
hideResolvedValue?: boolean;
|
|
27
29
|
'aria-describedby'?: string;
|
|
28
30
|
'aria-label'?: string;
|
|
29
31
|
'aria-required'?: boolean;
|
|
30
|
-
/** Primary icon — SVG source
|
|
31
|
-
icon?:
|
|
32
|
+
/** Primary icon — string SVG source, `{ src, color?, size? }` object, or custom snippet. */
|
|
33
|
+
icon?: IconProp;
|
|
32
34
|
/** Side of the input where the primary icon sits. @default 'leading' */
|
|
33
35
|
iconPosition?: 'leading' | 'trailing';
|
|
34
36
|
/** Optional secondary icon — always on the opposite side from `icon`. */
|
|
35
|
-
secondaryIcon?:
|
|
37
|
+
secondaryIcon?: IconProp;
|
|
36
38
|
};
|
|
37
39
|
type ClearableProps = BaseProps & {
|
|
38
40
|
/** Value can be cleared to `null`. Trigger shows an × button when set. */
|
|
@@ -44,7 +46,7 @@ type ClearableProps = BaseProps & {
|
|
|
44
46
|
mounted?: (data: {
|
|
45
47
|
input: HTMLInputElement;
|
|
46
48
|
}) => void;
|
|
47
|
-
blur?: (
|
|
49
|
+
blur?: () => void;
|
|
48
50
|
focus?: () => void;
|
|
49
51
|
};
|
|
50
52
|
};
|
|
@@ -58,16 +60,22 @@ type RequiredProps = BaseProps & {
|
|
|
58
60
|
mounted?: (data: {
|
|
59
61
|
input: HTMLInputElement;
|
|
60
62
|
}) => void;
|
|
61
|
-
blur?: (
|
|
63
|
+
blur?: () => void;
|
|
62
64
|
focus?: () => void;
|
|
63
65
|
};
|
|
64
66
|
};
|
|
65
67
|
type Props = ClearableProps | RequiredProps;
|
|
66
68
|
/**
|
|
67
|
-
* Formatted number input with
|
|
68
|
-
*
|
|
69
|
-
* Discriminated by `clearable`: `clearable: true` allows `null` (with × button);
|
|
70
|
-
* requires a non-null value. CSS API mirrors `Input` for visual-language compatibility.
|
|
69
|
+
* Formatted number input with space-grouped thousands, a dot or comma decimal mark, decimal scale, min/max clamping,
|
|
70
|
+
* a muted trailing `suffix` unit, and smooth cursor tracking while typing
|
|
71
|
+
* (via cleave-zen). Discriminated by `clearable`: `clearable: true` allows `null` (with × button);
|
|
72
|
+
* `clearable: false` requires a non-null value. CSS API mirrors `Input` for visual-language compatibility.
|
|
73
|
+
*
|
|
74
|
+
* While focused the text belongs to the user: a value change from outside lands on blur. Every
|
|
75
|
+
* emitted value is clamped to `min` / `max`, but the typed text is left alone until commit (blur,
|
|
76
|
+
* Enter, `change`) — so a prefix below `min` stays typeable. With `clearable: false` an empty
|
|
77
|
+
* field emits `min`, or keeps the held value when there is no `min`. Whenever the typed text resolves
|
|
78
|
+
* to a different value, that value shows next to the field as `→ 12` (off with `hideResolvedValue`).
|
|
71
79
|
*
|
|
72
80
|
* ### CSS Custom Properties
|
|
73
81
|
* | Property | Description | Default |
|
|
@@ -88,6 +96,7 @@ type Props = ClearableProps | RequiredProps;
|
|
|
88
96
|
* | `--sc-kit--numeral-input--placeholder--color` | Placeholder text color | `var(--sc-kit--color--text--placeholder)` |
|
|
89
97
|
* | `--sc-kit--numeral-input--icon--color` | Leading / trailing icon color (does NOT affect inner affordances) | `var(--sc-kit--color--text--muted)` |
|
|
90
98
|
* | `--sc-kit--numeral-input--icon--size` | Leading / trailing icon size (does NOT affect inner affordances) | per size |
|
|
99
|
+
* | `--sc-kit--numeral-input--suffix--color` | Suffix and value hint color | `var(--sc-kit--color--text--muted)` |
|
|
91
100
|
*/
|
|
92
101
|
declare const Cmp: import("svelte").Component<Props, {
|
|
93
102
|
/** Imperative API exposed via `bind:this`. */ focus: () => void;
|
|
@@ -1,16 +1,59 @@
|
|
|
1
1
|
<script lang="ts">import { Checkbox } from '../checkbox';
|
|
2
|
-
import {
|
|
2
|
+
import { Icon, IconSlot } from '../icon';
|
|
3
|
+
import { IconText } from '../icon-text';
|
|
3
4
|
import { PlayIndicator } from '../play-indicator';
|
|
4
|
-
import { Popover } from '../popover';
|
|
5
|
+
import { Popover, PopoverItem } from '../popover';
|
|
5
6
|
import { TileCardLocalization } from './tile-card-localization';
|
|
6
7
|
import IconMore from '@fluentui/svg-icons/icons/more_vertical_20_regular.svg?raw';
|
|
7
|
-
const { title, meta, preview,
|
|
8
|
+
const { title, meta, preview, actions, selected = false, checkboxVisibility = 'hover', on } = $props();
|
|
8
9
|
const localization = new TileCardLocalization();
|
|
10
|
+
const actionItems = $derived(actions?.items ?? []);
|
|
11
|
+
const actionsPosition = $derived(preview ? (actions?.position ?? 'footer') : 'footer');
|
|
12
|
+
let menuOpened = $state(false);
|
|
13
|
+
let keyboardFocused = $state(false);
|
|
9
14
|
// Safari paints no frame for preload="metadata" until a seek target is given.
|
|
10
15
|
const withFirstFrame = (src) => (src.includes('#') ? src : `${src}#t=0.001`);
|
|
11
16
|
</script>
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
{#snippet actionsBlock()}
|
|
19
|
+
{#if actionItems.length}
|
|
20
|
+
<div class="tile-card__actions tile-card__actions--{actionsPosition}">
|
|
21
|
+
{#if actionItems.length === 1 && actionsPosition === 'top' && actions?.singleAsButton}
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
class="tile-card__action"
|
|
25
|
+
class:tile-card__action--overlay={actionsPosition === 'top'}
|
|
26
|
+
aria-label={actionItems[0].label}
|
|
27
|
+
title={actionItems[0].label}
|
|
28
|
+
onclick={() => actionItems[0].onClick()}>
|
|
29
|
+
<IconSlot icon={actionItems[0].icon} fallbacks={{ size: 'control-md' }} />
|
|
30
|
+
</button>
|
|
31
|
+
{:else}
|
|
32
|
+
<Popover
|
|
33
|
+
position="bottom-end"
|
|
34
|
+
fixedPosition
|
|
35
|
+
aria-label={actions?.label ?? localization.actions}
|
|
36
|
+
on={{ opened: () => (menuOpened = true), closed: () => (menuOpened = false) }}>
|
|
37
|
+
{#snippet trigger()}
|
|
38
|
+
<span class="tile-card__action" class:tile-card__action--overlay={actionsPosition === 'top'}><Icon src={IconMore} size="control-md" /></span>
|
|
39
|
+
{/snippet}
|
|
40
|
+
{#each actionItems as action, index (index)}
|
|
41
|
+
<PopoverItem on={{ click: action.onClick }}><IconText icon={action.icon} size="sm">{action.label}</IconText></PopoverItem>
|
|
42
|
+
{/each}
|
|
43
|
+
</Popover>
|
|
44
|
+
{/if}
|
|
45
|
+
</div>
|
|
46
|
+
{/if}
|
|
47
|
+
{/snippet}
|
|
48
|
+
|
|
49
|
+
<div
|
|
50
|
+
class="tile-card"
|
|
51
|
+
class:tile-card--selected={selected}
|
|
52
|
+
class:tile-card--checkbox-always={checkboxVisibility === 'always'}
|
|
53
|
+
class:tile-card--menu-opened={menuOpened}
|
|
54
|
+
class:tile-card--keyboard-focused={keyboardFocused}
|
|
55
|
+
onfocusin={(e) => (keyboardFocused = (e.target as HTMLElement).matches(':focus-visible'))}
|
|
56
|
+
onfocusout={() => (keyboardFocused = false)}>
|
|
14
57
|
{#if on?.activate}
|
|
15
58
|
<button type="button" class="tile-card__hit" aria-label={title} title={title} onclick={() => on?.activate?.()}></button>
|
|
16
59
|
{/if}
|
|
@@ -33,17 +76,13 @@ const withFirstFrame = (src) => (src.includes('#') ? src : `${src}#t=0.001`);
|
|
|
33
76
|
<span class="tile-card__meta">{meta}</span>
|
|
34
77
|
{/if}
|
|
35
78
|
</div>
|
|
36
|
-
{#if
|
|
37
|
-
|
|
38
|
-
<Popover position="bottom-end" fixedPosition aria-label={menuLabel ?? localization.actions}>
|
|
39
|
-
{#snippet trigger()}
|
|
40
|
-
<ToolbarIconButton icon={IconMore} presentational />
|
|
41
|
-
{/snippet}
|
|
42
|
-
{@render menu()}
|
|
43
|
-
</Popover>
|
|
44
|
-
</div>
|
|
79
|
+
{#if actionsPosition === 'footer'}
|
|
80
|
+
{@render actionsBlock()}
|
|
45
81
|
{/if}
|
|
46
82
|
</div>
|
|
83
|
+
{#if actionsPosition === 'top'}
|
|
84
|
+
{@render actionsBlock()}
|
|
85
|
+
{/if}
|
|
47
86
|
{#if on?.select}
|
|
48
87
|
<div class="tile-card__checkbox">
|
|
49
88
|
<Checkbox checked={selected} variant="on-accent" title={localization.select} on={{ change: (value) => on?.select?.(value) }} />
|
|
@@ -53,8 +92,9 @@ const withFirstFrame = (src) => (src.includes('#') ? src : `${src}#t=0.001`);
|
|
|
53
92
|
|
|
54
93
|
<!--
|
|
55
94
|
@component
|
|
56
|
-
File-browser style tile: a full-bleed preview on top and a footer with title
|
|
57
|
-
checkbox via `on.select`, whole-card activation via
|
|
95
|
+
File-browser style tile: a full-bleed preview on top and a footer with title and optional meta line. `actions` sit at the end
|
|
96
|
+
of the footer or over the preview's top-right corner. Selection checkbox via `on.select`, whole-card activation via
|
|
97
|
+
`on.activate`. For a drop-target highlight wrap it in `DropZone`.
|
|
58
98
|
|
|
59
99
|
### CSS Custom Properties
|
|
60
100
|
| Property | Description | Default |
|
|
@@ -79,6 +119,7 @@ checkbox via `on.select`, whole-card activation via `on.activate`. For a drop-ta
|
|
|
79
119
|
--_tile-card--preview-aspect-ratio: var(--sc-kit--tile-card--preview--aspect-ratio, 7 / 5);
|
|
80
120
|
--_tile-card--footer-min-height: var(--sc-kit--tile-card--footer--min-height, 3.25rem);
|
|
81
121
|
--_tile-card--checkbox-opacity: 0;
|
|
122
|
+
--_tile-card--top-actions-opacity: 0;
|
|
82
123
|
position: relative;
|
|
83
124
|
display: flex;
|
|
84
125
|
flex-direction: column;
|
|
@@ -93,8 +134,12 @@ checkbox via `on.select`, whole-card activation via `on.activate`. For a drop-ta
|
|
|
93
134
|
.tile-card:hover {
|
|
94
135
|
--_tile-card--border-color: var(--_tile-card--border-color-hover);
|
|
95
136
|
}
|
|
96
|
-
.tile-card:hover, .tile-card
|
|
137
|
+
.tile-card:hover, .tile-card--keyboard-focused {
|
|
97
138
|
--_tile-card--checkbox-opacity: 1;
|
|
139
|
+
--_tile-card--top-actions-opacity: 1;
|
|
140
|
+
}
|
|
141
|
+
.tile-card--menu-opened {
|
|
142
|
+
--_tile-card--top-actions-opacity: 1;
|
|
98
143
|
}
|
|
99
144
|
.tile-card:has(> .tile-card__hit:focus-visible) {
|
|
100
145
|
outline: 2px solid var(--sc-kit--color--border--focus);
|
|
@@ -112,6 +157,7 @@ checkbox via `on.select`, whole-card activation via `on.activate`. For a drop-ta
|
|
|
112
157
|
@media (hover: none) {
|
|
113
158
|
.tile-card {
|
|
114
159
|
--_tile-card--checkbox-opacity: 1;
|
|
160
|
+
--_tile-card--top-actions-opacity: 1;
|
|
115
161
|
}
|
|
116
162
|
}
|
|
117
163
|
.tile-card__hit {
|
|
@@ -182,10 +228,46 @@ checkbox via `on.select`, whole-card activation via `on.activate`. For a drop-ta
|
|
|
182
228
|
white-space: nowrap;
|
|
183
229
|
overflow: hidden;
|
|
184
230
|
}
|
|
185
|
-
.tile-
|
|
231
|
+
.tile-card__actions {
|
|
186
232
|
position: relative;
|
|
187
233
|
display: flex;
|
|
188
234
|
}
|
|
235
|
+
.tile-card__actions--top {
|
|
236
|
+
position: absolute;
|
|
237
|
+
inset-block-start: var(--sc-kit--space--3);
|
|
238
|
+
inset-inline-end: var(--sc-kit--space--3);
|
|
239
|
+
opacity: var(--_tile-card--top-actions-opacity);
|
|
240
|
+
transition: opacity var(--sc-kit--duration--base) var(--sc-kit--ease--default);
|
|
241
|
+
}
|
|
242
|
+
.tile-card__action {
|
|
243
|
+
--sc-kit--icon--color: var(--sc-kit--color--text--muted);
|
|
244
|
+
display: inline-flex;
|
|
245
|
+
align-items: center;
|
|
246
|
+
justify-content: center;
|
|
247
|
+
inline-size: 1.75rem;
|
|
248
|
+
block-size: 1.75rem;
|
|
249
|
+
padding: 0;
|
|
250
|
+
background: transparent;
|
|
251
|
+
border: 0;
|
|
252
|
+
border-radius: var(--sc-kit--field--border-radius--sm);
|
|
253
|
+
cursor: pointer;
|
|
254
|
+
transition: background-color var(--sc-kit--duration--base) var(--sc-kit--ease--default);
|
|
255
|
+
}
|
|
256
|
+
.tile-card__action:hover {
|
|
257
|
+
background: var(--sc-kit--color--bg--hover);
|
|
258
|
+
}
|
|
259
|
+
.tile-card__action:focus-visible {
|
|
260
|
+
outline: 2px solid var(--sc-kit--color--border--focus);
|
|
261
|
+
outline-offset: 2px;
|
|
262
|
+
}
|
|
263
|
+
.tile-card__action--overlay {
|
|
264
|
+
--sc-kit--icon--color: var(--sc-kit--color--text--primary);
|
|
265
|
+
background: var(--sc-kit--color--bg--panel);
|
|
266
|
+
box-shadow: var(--sc-kit--shadow--md);
|
|
267
|
+
}
|
|
268
|
+
.tile-card__action--overlay:hover {
|
|
269
|
+
background: var(--sc-kit--color--bg--hover);
|
|
270
|
+
}
|
|
189
271
|
.tile-card__checkbox {
|
|
190
272
|
position: absolute;
|
|
191
273
|
inset-block-start: var(--sc-kit--space--3);
|
|
@@ -1,28 +1,25 @@
|
|
|
1
|
-
import type { TileCardCheckboxVisibility, TileCardPreview } from './types';
|
|
2
|
-
import type { Snippet } from 'svelte';
|
|
1
|
+
import type { TileCardActions, TileCardCheckboxVisibility, TileCardPreview } from './types';
|
|
3
2
|
type Props = {
|
|
4
3
|
title: string;
|
|
5
4
|
/** Secondary line under the title. */
|
|
6
5
|
meta?: string;
|
|
7
|
-
/** Top area: `image` / `video` render a drag-safe picture or frame (contain, never upscaled; a video gets a play glyph); a snippet renders custom content. The area is `pointer-events: none` — interactive content belongs in `
|
|
6
|
+
/** Top area: `image` / `video` render a drag-safe picture or frame (contain, never upscaled; a video gets a play glyph); a snippet renders custom content. The area is `pointer-events: none` — interactive content belongs in `actions` or `on.activate`. */
|
|
8
7
|
preview?: TileCardPreview;
|
|
9
|
-
|
|
10
|
-
menu?: Snippet;
|
|
11
|
-
/** Accessible name of the kebab menu. @default localized "Actions" */
|
|
12
|
-
menuLabel?: string;
|
|
8
|
+
actions?: TileCardActions;
|
|
13
9
|
selected?: boolean;
|
|
14
|
-
/** When the checkbox shows: `'hover'` — on hover, focus
|
|
10
|
+
/** When the checkbox shows: `'hover'` — on hover, keyboard focus, when selected and on touch screens; `'always'` — permanently. @default 'hover' */
|
|
15
11
|
checkboxVisibility?: TileCardCheckboxVisibility;
|
|
16
12
|
on?: {
|
|
17
13
|
/** Renders the checkbox at the top-left of the preview. */
|
|
18
14
|
select?: (value: boolean) => void;
|
|
19
|
-
/** Fires on a click on the card surface or Enter / Space; the checkbox and the
|
|
15
|
+
/** Fires on a click on the card surface or Enter / Space; the checkbox and the actions don't trigger it. */
|
|
20
16
|
activate?: () => void;
|
|
21
17
|
};
|
|
22
18
|
};
|
|
23
19
|
/**
|
|
24
|
-
* File-browser style tile: a full-bleed preview on top and a footer with title
|
|
25
|
-
* checkbox via `on.select`, whole-card activation via
|
|
20
|
+
* File-browser style tile: a full-bleed preview on top and a footer with title and optional meta line. `actions` sit at the end
|
|
21
|
+
* of the footer or over the preview's top-right corner. Selection checkbox via `on.select`, whole-card activation via
|
|
22
|
+
* `on.activate`. For a drop-target highlight wrap it in `DropZone`.
|
|
26
23
|
*
|
|
27
24
|
* ### CSS Custom Properties
|
|
28
25
|
* | Property | Description | Default |
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type { TileCardCheckboxVisibility, TileCardPreview } from './types';
|
|
1
|
+
export type { TileCardAction, TileCardActions, TileCardCheckboxVisibility, TileCardPreview } from './types';
|
|
2
2
|
export { default as TileCard } from './cmp.tile-card.svelte';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IconProp } from '../icon';
|
|
1
2
|
import type { Snippet } from 'svelte';
|
|
2
3
|
export type TileCardPreview = {
|
|
3
4
|
image: string;
|
|
@@ -6,3 +7,22 @@ export type TileCardPreview = {
|
|
|
6
7
|
poster?: string;
|
|
7
8
|
} | Snippet;
|
|
8
9
|
export type TileCardCheckboxVisibility = 'hover' | 'always';
|
|
10
|
+
/** One card action. `label` is the menu item text, or the accessible name and tooltip when it renders as a lone icon button. */
|
|
11
|
+
export type TileCardAction = {
|
|
12
|
+
label: string;
|
|
13
|
+
icon: IconProp;
|
|
14
|
+
onClick: () => void;
|
|
15
|
+
};
|
|
16
|
+
export type TileCardActions = {
|
|
17
|
+
items: TileCardAction[];
|
|
18
|
+
/**
|
|
19
|
+
* `'footer'` — at the end of the footer; `'top'` — over the top-right corner of the preview, revealed on hover, keyboard focus, while its menu is
|
|
20
|
+
* open and on touch screens. A card without a `preview` always uses `'footer'`.
|
|
21
|
+
* @default 'footer'
|
|
22
|
+
*/
|
|
23
|
+
position?: 'footer' | 'top';
|
|
24
|
+
/** With `position: 'top'`, a single item renders as its own icon button instead of a one-item kebab menu. @default false */
|
|
25
|
+
singleAsButton?: boolean;
|
|
26
|
+
/** Accessible name of the kebab menu. @default localized "Actions" */
|
|
27
|
+
label?: string;
|
|
28
|
+
};
|