@insymetri/styleguide 0.1.92 → 0.1.94
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/IIAccordion/IIAccordion.svelte +12 -2
- package/dist/IIAccordion/IIAccordion.svelte.d.ts +2 -0
- package/dist/IIFormField/IIFormField.svelte +14 -4
- package/dist/IIFormField/IIFormField.svelte.d.ts +6 -1
- package/dist/IISegmentedControl/IISegmentedControl.svelte +92 -5
- package/dist/IISegmentedControl/IISegmentedControlStories.svelte +27 -0
- package/dist/IITable/IITable.svelte +24 -6
- package/dist/IITable/IITable.svelte.d.ts +2 -0
- package/package.json +1 -1
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
trigger: Snippet<[{expanded: boolean}]>
|
|
10
10
|
children: Snippet
|
|
11
11
|
disabled?: boolean
|
|
12
|
+
/** `card` (default) frames each item; `plain` separates them with a hairline. */
|
|
13
|
+
variant?: 'card' | 'plain'
|
|
12
14
|
class?: string
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -18,6 +20,7 @@
|
|
|
18
20
|
trigger,
|
|
19
21
|
children,
|
|
20
22
|
disabled = false,
|
|
23
|
+
variant = 'card',
|
|
21
24
|
class: className,
|
|
22
25
|
}: Props = $props()
|
|
23
26
|
|
|
@@ -28,7 +31,14 @@
|
|
|
28
31
|
}
|
|
29
32
|
</script>
|
|
30
33
|
|
|
31
|
-
<div
|
|
34
|
+
<div
|
|
35
|
+
class={cn(
|
|
36
|
+
variant === 'plain'
|
|
37
|
+
? 'border-b border-subtle last:border-b-0'
|
|
38
|
+
: 'border border-primary rounded-10 overflow-hidden',
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
>
|
|
32
42
|
<button
|
|
33
43
|
type="button"
|
|
34
44
|
class="flex items-center gap-8 w-full bg-transparent border-none px-16 py-12 cursor-default text-left font-inherit hover:bg-background"
|
|
@@ -47,7 +57,7 @@
|
|
|
47
57
|
</div>
|
|
48
58
|
</button>
|
|
49
59
|
{#if expanded}
|
|
50
|
-
<div class=
|
|
60
|
+
<div class={variant === 'plain' ? '' : 'border-t border-primary'}>
|
|
51
61
|
{@render children()}
|
|
52
62
|
</div>
|
|
53
63
|
{/if}
|
|
@@ -7,6 +7,8 @@ type Props = {
|
|
|
7
7
|
}]>;
|
|
8
8
|
children: Snippet;
|
|
9
9
|
disabled?: boolean;
|
|
10
|
+
/** `card` (default) frames each item; `plain` separates them with a hairline. */
|
|
11
|
+
variant?: 'card' | 'plain';
|
|
10
12
|
class?: string;
|
|
11
13
|
};
|
|
12
14
|
declare const IIAccordion: import("svelte").Component<Props, {}, "expanded">;
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type {Snippet} from 'svelte'
|
|
3
|
+
import {cn} from '../utils/cn'
|
|
3
4
|
|
|
4
5
|
type Props = {
|
|
5
6
|
label?: string
|
|
6
7
|
id?: string
|
|
7
|
-
|
|
8
|
+
/** Control on the label's own line, right-aligned. Ignored without a `label`. */
|
|
9
|
+
labelAction?: Snippet
|
|
10
|
+
class?: string
|
|
8
11
|
}
|
|
9
12
|
|
|
10
|
-
let {label, id, children}: Props = $props()
|
|
13
|
+
let {label, id, labelAction, children, class: className}: Props & {children: Snippet} = $props()
|
|
11
14
|
</script>
|
|
12
15
|
|
|
13
|
-
<div class=
|
|
16
|
+
<div class={cn('mb-16 last:mb-0', className)}>
|
|
14
17
|
{#if label}
|
|
15
|
-
|
|
18
|
+
{#if labelAction}
|
|
19
|
+
<div class="mb-8 flex items-center justify-between gap-10">
|
|
20
|
+
<label class="block text-small-emphasis text-body" for={id}>{label}</label>
|
|
21
|
+
{@render labelAction()}
|
|
22
|
+
</div>
|
|
23
|
+
{:else}
|
|
24
|
+
<label class="block text-small-emphasis text-body mb-8" for={id}>{label}</label>
|
|
25
|
+
{/if}
|
|
16
26
|
{/if}
|
|
17
27
|
{@render children()}
|
|
18
28
|
</div>
|
|
@@ -2,8 +2,13 @@ import type { Snippet } from 'svelte';
|
|
|
2
2
|
type Props = {
|
|
3
3
|
label?: string;
|
|
4
4
|
id?: string;
|
|
5
|
+
/** Control on the label's own line, right-aligned. Ignored without a `label`. */
|
|
6
|
+
labelAction?: Snippet;
|
|
7
|
+
class?: string;
|
|
8
|
+
};
|
|
9
|
+
type $$ComponentProps = Props & {
|
|
5
10
|
children: Snippet;
|
|
6
11
|
};
|
|
7
|
-
declare const IIFormField: import("svelte").Component
|
|
12
|
+
declare const IIFormField: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
13
|
type IIFormField = ReturnType<typeof IIFormField>;
|
|
9
14
|
export default IIFormField;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import {untrack} from 'svelte'
|
|
2
3
|
import {cn} from '../utils/cn'
|
|
3
4
|
import {useDensity} from '../density'
|
|
4
5
|
|
|
@@ -52,6 +53,41 @@
|
|
|
52
53
|
|
|
53
54
|
const activeIndex = $derived(items.findIndex(item => item.value === value))
|
|
54
55
|
|
|
56
|
+
/* The thumb stays mounted even when nothing is selected so it can animate in and
|
|
57
|
+
out. `thumbIndex` remembers where it last was, so clearing the value fades it
|
|
58
|
+
out under the segment that was selected instead of yanking it back to the first. */
|
|
59
|
+
let thumbIndex = $state(untrack(() => Math.max(activeIndex, 0)))
|
|
60
|
+
let thumbVisible = $state(untrack(() => activeIndex >= 0))
|
|
61
|
+
let sliding = $state(false)
|
|
62
|
+
/* Seeded with the mount-time index on purpose: the effect below compares against
|
|
63
|
+
it and bails on its first run, so a control that renders already-selected shows
|
|
64
|
+
its thumb in place rather than animating it in on load. */
|
|
65
|
+
let prevIndex = untrack(() => activeIndex)
|
|
66
|
+
|
|
67
|
+
$effect(() => {
|
|
68
|
+
const next = activeIndex
|
|
69
|
+
if (next === prevIndex) return
|
|
70
|
+
const appearing = prevIndex < 0
|
|
71
|
+
prevIndex = next
|
|
72
|
+
|
|
73
|
+
if (next < 0) {
|
|
74
|
+
sliding = false
|
|
75
|
+
thumbVisible = false
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Appearing from nothing must not transition `left`: a thumb that slides in
|
|
80
|
+
from segment 0 reads as "it was always there, just hidden". Dropping the
|
|
81
|
+
slide lets it grow into place under the segment that was actually picked,
|
|
82
|
+
and the scale/opacity transition carries the motion instead. */
|
|
83
|
+
sliding = !appearing
|
|
84
|
+
thumbIndex = next
|
|
85
|
+
thumbVisible = true
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Guards against `items` shrinking underneath a remembered index.
|
|
89
|
+
const thumbPosition = $derived(Math.min(thumbIndex, Math.max(items.length - 1, 0)))
|
|
90
|
+
|
|
55
91
|
function select(v: string) {
|
|
56
92
|
value = v
|
|
57
93
|
onValueChange?.(v)
|
|
@@ -113,17 +149,19 @@
|
|
|
113
149
|
style="padding: {thumbInset[density.value]}px; grid-template-columns: repeat({items.length}, 1fr);"
|
|
114
150
|
>
|
|
115
151
|
<!-- Sliding thumb -->
|
|
116
|
-
{#if
|
|
152
|
+
{#if items.length > 0}
|
|
117
153
|
<div
|
|
118
|
-
class="absolute"
|
|
154
|
+
class="ii-seg-thumb absolute"
|
|
155
|
+
class:is-visible={thumbVisible}
|
|
156
|
+
class:is-sliding={sliding}
|
|
157
|
+
aria-hidden="true"
|
|
119
158
|
style:border-radius="calc(var(--ii-radius-control) - {thumbInset[density.value]}px)"
|
|
120
159
|
style="
|
|
121
160
|
top: {thumbInset[density.value]}px;
|
|
122
161
|
bottom: {thumbInset[density.value]}px;
|
|
123
|
-
left: calc({thumbInset[density.value]}px + {
|
|
162
|
+
left: calc({thumbInset[density.value]}px + {thumbPosition} * (100% - {thumbInset[density.value] * 2}px) / {items.length});
|
|
124
163
|
width: calc((100% - {thumbInset[density.value] * 2}px) / {items.length});
|
|
125
|
-
background: {getThumbColor(items[
|
|
126
|
-
transition: left 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
164
|
+
background: {getThumbColor(items[thumbPosition])};
|
|
127
165
|
"
|
|
128
166
|
></div>
|
|
129
167
|
{/if}
|
|
@@ -147,3 +185,52 @@
|
|
|
147
185
|
</button>
|
|
148
186
|
{/each}
|
|
149
187
|
</div>
|
|
188
|
+
|
|
189
|
+
<style>
|
|
190
|
+
/* Resting state is hidden-and-small, so the thumb has somewhere to grow from.
|
|
191
|
+
Also governs the exit: a transition uses the curve declared on the state it is
|
|
192
|
+
travelling *to*, so this rule is what plays when the selection is cleared —
|
|
193
|
+
short and ease-in, because leaving should not ask for attention. */
|
|
194
|
+
.ii-seg-thumb {
|
|
195
|
+
opacity: 0;
|
|
196
|
+
transform: scale(0.82);
|
|
197
|
+
pointer-events: none;
|
|
198
|
+
transition:
|
|
199
|
+
opacity 120ms ease-in,
|
|
200
|
+
transform 180ms ease-in;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* The entrance. The back-out curve grows the thumb past its resting size — about
|
|
204
|
+
1.02 at the peak — before settling, which is the gesture iOS and macOS
|
|
205
|
+
segmented controls use to say "this materialised here" rather than "this
|
|
206
|
+
blinked on". At these sizes the overshoot is barely a pixel; it reads as
|
|
207
|
+
liveliness in the deceleration, not as a bounce. Opacity finishes well before
|
|
208
|
+
the scale so the tail of the motion is pure settle, not fade. */
|
|
209
|
+
.ii-seg-thumb.is-visible {
|
|
210
|
+
opacity: 1;
|
|
211
|
+
transform: scale(1);
|
|
212
|
+
transition:
|
|
213
|
+
opacity 140ms ease-out,
|
|
214
|
+
transform 280ms cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* `left` only animates once the thumb is already on screen and moving between
|
|
218
|
+
segments — see the effect above. Must stay after `.is-visible`: equal
|
|
219
|
+
specificity, so source order decides. */
|
|
220
|
+
.ii-seg-thumb.is-sliding {
|
|
221
|
+
transition:
|
|
222
|
+
opacity 140ms ease-out,
|
|
223
|
+
transform 280ms cubic-bezier(0.34, 1.56, 0.64, 1),
|
|
224
|
+
background-color 200ms ease,
|
|
225
|
+
left 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@media (prefers-reduced-motion: reduce) {
|
|
229
|
+
.ii-seg-thumb,
|
|
230
|
+
.ii-seg-thumb.is-visible,
|
|
231
|
+
.ii-seg-thumb.is-sliding {
|
|
232
|
+
transform: none;
|
|
233
|
+
transition: opacity 100ms linear;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
</style>
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
let fullWidthMode = $state('allow')
|
|
12
12
|
let fullWidthThree = $state('monthly')
|
|
13
|
+
|
|
14
|
+
let emptyStart = $state('')
|
|
13
15
|
</script>
|
|
14
16
|
|
|
15
17
|
<div class="flex flex-col gap-32">
|
|
@@ -151,6 +153,31 @@
|
|
|
151
153
|
/>
|
|
152
154
|
</section>
|
|
153
155
|
|
|
156
|
+
<!-- Animating in from empty -->
|
|
157
|
+
<section>
|
|
158
|
+
<h2 class="text-default-emphasis text-primary mb-8">Animating in from empty</h2>
|
|
159
|
+
<p class="text-small text-secondary mb-12">
|
|
160
|
+
With no selection there is no thumb to slide, so picking a segment scales the thumb up
|
|
161
|
+
into place with a small overshoot instead of flashing it on. Clearing fades it back out
|
|
162
|
+
under the segment it was on. Moving between segments still slides.
|
|
163
|
+
</p>
|
|
164
|
+
<IISegmentedControl
|
|
165
|
+
items={[
|
|
166
|
+
{label: 'Monthly', value: 'monthly'},
|
|
167
|
+
{label: 'Quarterly', value: 'quarterly'},
|
|
168
|
+
{label: 'Annual', value: 'annual'},
|
|
169
|
+
]}
|
|
170
|
+
bind:value={emptyStart}
|
|
171
|
+
/>
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
class="mt-12 px-12 py-6 bg-button-primary text-button-primary-text rounded-control"
|
|
175
|
+
onclick={() => (emptyStart = '')}
|
|
176
|
+
>
|
|
177
|
+
Clear selection
|
|
178
|
+
</button>
|
|
179
|
+
</section>
|
|
180
|
+
|
|
154
181
|
<!-- Density-responsive -->
|
|
155
182
|
<section>
|
|
156
183
|
<h2 class="text-default-emphasis text-primary mb-8">Density Responsive</h2>
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
stickyHeader?: boolean
|
|
42
42
|
/** `sm` → text-small (default), `md` → text-default. */
|
|
43
43
|
size?: 'sm' | 'md'
|
|
44
|
+
/** `card` (default) frames the table; `plain` drops the frame and header fill for hairline rules. */
|
|
45
|
+
variant?: 'card' | 'plain'
|
|
44
46
|
class?: string
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -58,31 +60,45 @@
|
|
|
58
60
|
isExpanded,
|
|
59
61
|
stickyHeader = false,
|
|
60
62
|
size = 'sm',
|
|
63
|
+
variant = 'card',
|
|
61
64
|
class: className,
|
|
62
65
|
}: Props = $props()
|
|
63
66
|
|
|
64
67
|
const alignClass = {left: 'text-left', right: 'text-right', center: 'text-center'} as const
|
|
65
68
|
const cellText = $derived(size === 'sm' ? 'text-small' : 'text-default')
|
|
66
69
|
const interactive = $derived(!!onRowClick)
|
|
70
|
+
const plain = $derived(variant === 'plain')
|
|
71
|
+
|
|
72
|
+
const containerClass = $derived(
|
|
73
|
+
plain ? 'overflow-x-auto' : 'bg-surface-raised border border-subtle rounded-12 overflow-x-auto overflow-y-hidden'
|
|
74
|
+
)
|
|
75
|
+
const headClass = $derived(plain ? 'border-b border-subtle' : 'bg-background border-b border-primary')
|
|
76
|
+
const headCellClass = $derived(
|
|
77
|
+
plain
|
|
78
|
+
? 'px-12 pb-8 text-micro text-tertiary uppercase'
|
|
79
|
+
: 'px-14 py-9 text-tiny-emphasis text-tertiary uppercase tracking-wide'
|
|
80
|
+
)
|
|
81
|
+
const bodyCellClass = $derived(plain ? 'px-12 py-11' : 'px-14 py-10')
|
|
82
|
+
const rowRuleClass = $derived(plain ? 'border-subtle' : 'border-primary')
|
|
67
83
|
|
|
68
84
|
function cellClass(col: Column, row: T): string {
|
|
69
85
|
return typeof col.class === 'function' ? col.class(row) : (col.class ?? '')
|
|
70
86
|
}
|
|
71
87
|
</script>
|
|
72
88
|
|
|
73
|
-
<div class={cn(
|
|
89
|
+
<div class={cn(containerClass, className)}>
|
|
74
90
|
{#if children}
|
|
75
91
|
<table class="w-full border-collapse">
|
|
76
92
|
{@render children()}
|
|
77
93
|
</table>
|
|
78
94
|
{:else}
|
|
79
95
|
<table class="w-full border-collapse">
|
|
80
|
-
<thead class={cn(
|
|
96
|
+
<thead class={cn(headClass, stickyHeader && 'sticky top-0 z-1')}>
|
|
81
97
|
<tr>
|
|
82
98
|
{#each columns as col (col.key)}
|
|
83
99
|
<th
|
|
84
100
|
class={cn(
|
|
85
|
-
|
|
101
|
+
headCellClass,
|
|
86
102
|
alignClass[col.align ?? 'left'],
|
|
87
103
|
col.width,
|
|
88
104
|
col.headerClass
|
|
@@ -108,7 +124,8 @@
|
|
|
108
124
|
{#each rows as row (rowKey?.(row) ?? row)}
|
|
109
125
|
<tr
|
|
110
126
|
class={cn(
|
|
111
|
-
'border-b
|
|
127
|
+
'border-b last:border-b-0',
|
|
128
|
+
rowRuleClass,
|
|
112
129
|
interactive && 'cursor-pointer hover:bg-background',
|
|
113
130
|
rowClass?.(row)
|
|
114
131
|
)}
|
|
@@ -119,7 +136,8 @@
|
|
|
119
136
|
{#each columns as col (col.key)}
|
|
120
137
|
<td
|
|
121
138
|
class={cn(
|
|
122
|
-
|
|
139
|
+
bodyCellClass,
|
|
140
|
+
'text-body',
|
|
123
141
|
cellText,
|
|
124
142
|
alignClass[col.align ?? 'left'],
|
|
125
143
|
col.width,
|
|
@@ -131,7 +149,7 @@
|
|
|
131
149
|
{/each}
|
|
132
150
|
</tr>
|
|
133
151
|
{#if expandedRow && isExpanded?.(row)}
|
|
134
|
-
<tr class=
|
|
152
|
+
<tr class={cn('border-b last:border-b-0 bg-background', rowRuleClass)}>
|
|
135
153
|
<td colspan={columns.length} class="px-16 py-12">
|
|
136
154
|
{@render expandedRow(row)}
|
|
137
155
|
</td>
|
|
@@ -36,6 +36,8 @@ declare function $$render<T>(): {
|
|
|
36
36
|
stickyHeader?: boolean;
|
|
37
37
|
/** `sm` → text-small (default), `md` → text-default. */
|
|
38
38
|
size?: "sm" | "md";
|
|
39
|
+
/** `card` (default) frames the table; `plain` drops the frame and header fill for hairline rules. */
|
|
40
|
+
variant?: "card" | "plain";
|
|
39
41
|
class?: string;
|
|
40
42
|
};
|
|
41
43
|
exports: {};
|