milk-lib 0.0.22 → 0.0.24
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/components/SegmentedControl/SegmentedControl.svelte +200 -0
- package/dist/components/SegmentedControl/SegmentedControl.svelte.d.ts +4 -0
- package/dist/components/SegmentedControl/SegmentedControl.types.d.ts +55 -0
- package/dist/components/SegmentedControl/SegmentedControl.types.js +2 -0
- package/dist/components/SegmentedControl/SegmentedControlItem.svelte +111 -0
- package/dist/components/SegmentedControl/SegmentedControlItem.svelte.d.ts +4 -0
- package/dist/components/SegmentedControl/index.d.ts +8 -0
- package/dist/components/SegmentedControl/index.js +8 -0
- package/dist/components/Select/Select.svelte +4 -2
- package/dist/components/Select/Select.types.d.ts +1 -0
- package/dist/components/TextInput/TextInput.svelte +1 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { setContext } from 'svelte';
|
|
3
|
+
import { writable } from 'svelte/store';
|
|
4
|
+
import {
|
|
5
|
+
SEGMENTED_CONTROL_CONTEXT,
|
|
6
|
+
type ISegmentedControlContext,
|
|
7
|
+
type ISegmentedControlItemEntry,
|
|
8
|
+
type ISegmentedControlRootProps
|
|
9
|
+
} from './SegmentedControl.types';
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
value = $bindable(),
|
|
13
|
+
defaultValue,
|
|
14
|
+
orientation = 'horizontal',
|
|
15
|
+
gap = '2px',
|
|
16
|
+
disabled = false,
|
|
17
|
+
classNames = '',
|
|
18
|
+
style,
|
|
19
|
+
dataVariant,
|
|
20
|
+
onValueChange,
|
|
21
|
+
children
|
|
22
|
+
}: ISegmentedControlRootProps = $props();
|
|
23
|
+
|
|
24
|
+
if (value === undefined && defaultValue !== undefined) {
|
|
25
|
+
value = defaultValue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const selectedValue = writable<string | null>(value ?? defaultValue ?? null);
|
|
29
|
+
const disabledState = writable<boolean>(disabled);
|
|
30
|
+
let items: ISegmentedControlItemEntry[] = [];
|
|
31
|
+
|
|
32
|
+
function registerItem(entry: ISegmentedControlItemEntry) {
|
|
33
|
+
const index = items.findIndex(item => item.id === entry.id);
|
|
34
|
+
if (index === -1) {
|
|
35
|
+
items = [...items, entry];
|
|
36
|
+
} else {
|
|
37
|
+
items = [
|
|
38
|
+
...items.slice(0, index),
|
|
39
|
+
entry,
|
|
40
|
+
...items.slice(index + 1)
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function unregisterItem(id: symbol) {
|
|
46
|
+
items = items.filter(item => item.id !== id);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function selectValue(newValue: string) {
|
|
50
|
+
if (disabled) return;
|
|
51
|
+
if (value === newValue) return;
|
|
52
|
+
const target = items.find(item => item.value === newValue);
|
|
53
|
+
if (target?.disabled) return;
|
|
54
|
+
value = newValue;
|
|
55
|
+
selectedValue.set(newValue);
|
|
56
|
+
onValueChange?.(newValue);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const context: ISegmentedControlContext = {
|
|
60
|
+
orientation,
|
|
61
|
+
disabled: disabledState,
|
|
62
|
+
selectedValue,
|
|
63
|
+
selectValue,
|
|
64
|
+
registerItem,
|
|
65
|
+
unregisterItem
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
setContext(SEGMENTED_CONTROL_CONTEXT, context);
|
|
69
|
+
|
|
70
|
+
$effect(() => {
|
|
71
|
+
context.orientation = orientation;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
$effect(() => {
|
|
75
|
+
disabledState.set(disabled);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
$effect(() => {
|
|
79
|
+
selectedValue.set(value ?? defaultValue ?? null);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
let rootStyle = $state('');
|
|
83
|
+
$effect(() => {
|
|
84
|
+
const normalized = style ? style.trim() : '';
|
|
85
|
+
const normalizedStyle = normalized
|
|
86
|
+
? normalized.endsWith(';')
|
|
87
|
+
? normalized
|
|
88
|
+
: `${normalized};`
|
|
89
|
+
: '';
|
|
90
|
+
rootStyle = `${normalizedStyle} --segmented-control-gap: ${gap};`;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
function moveFocus(step: number) {
|
|
94
|
+
if (!items.length) return;
|
|
95
|
+
if (!items.some(entry => !entry.disabled)) return;
|
|
96
|
+
|
|
97
|
+
let nextIndex = items.findIndex(entry => entry.value === value);
|
|
98
|
+
if (nextIndex === -1) {
|
|
99
|
+
nextIndex = step > 0 ? -1 : 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
103
|
+
nextIndex = (nextIndex + step + items.length) % items.length;
|
|
104
|
+
const entry = items[nextIndex];
|
|
105
|
+
if (!entry?.disabled) {
|
|
106
|
+
selectValue(entry.value);
|
|
107
|
+
entry.el?.focus();
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function focusEdge(isFirst: boolean) {
|
|
114
|
+
if (!items.length) return;
|
|
115
|
+
const ordered = isFirst ? items : [...items].reverse();
|
|
116
|
+
for (const entry of ordered) {
|
|
117
|
+
if (!entry.disabled) {
|
|
118
|
+
selectValue(entry.value);
|
|
119
|
+
entry.el?.focus();
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
126
|
+
if (disabled) return;
|
|
127
|
+
const { key } = event;
|
|
128
|
+
if (orientation === 'horizontal') {
|
|
129
|
+
if (key === 'ArrowRight') {
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
moveFocus(1);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (key === 'ArrowLeft') {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
moveFocus(-1);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (orientation === 'vertical') {
|
|
142
|
+
if (key === 'ArrowDown') {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
moveFocus(1);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (key === 'ArrowUp') {
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
moveFocus(-1);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (key === 'Home') {
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
focusEdge(true);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (key === 'End') {
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
focusEdge(false);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
</script>
|
|
165
|
+
|
|
166
|
+
<div
|
|
167
|
+
class={`SegmentedControlRoot ${classNames}`}
|
|
168
|
+
role="group"
|
|
169
|
+
aria-orientation={orientation}
|
|
170
|
+
aria-disabled={disabled}
|
|
171
|
+
data-orientation={orientation}
|
|
172
|
+
style={rootStyle}
|
|
173
|
+
onkeydown={handleKeydown}
|
|
174
|
+
{...(dataVariant ? { 'data-variant': dataVariant } : {})}
|
|
175
|
+
>
|
|
176
|
+
{@render children?.()}
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<style>.SegmentedControlRoot {
|
|
180
|
+
--segmented-control-padding: 2px;
|
|
181
|
+
--segmented-control-radius: var(--border-radius-button, 10px);
|
|
182
|
+
--segmented-control-border: 1px solid var(--line-base);
|
|
183
|
+
display: inline-flex;
|
|
184
|
+
gap: var(--segmented-control-gap, 2px);
|
|
185
|
+
padding: var(--segmented-control-padding);
|
|
186
|
+
border: var(--segmented-control-border);
|
|
187
|
+
border-radius: var(--segmented-control-radius);
|
|
188
|
+
background-color: var(--bg-base);
|
|
189
|
+
align-items: stretch;
|
|
190
|
+
justify-content: flex-start;
|
|
191
|
+
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
|
192
|
+
}
|
|
193
|
+
.SegmentedControlRoot[data-orientation=vertical] {
|
|
194
|
+
flex-direction: column;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.SegmentedControlRoot:focus-within {
|
|
198
|
+
border-color: var(--line-base);
|
|
199
|
+
box-shadow: 0 0 0 1px var(--line-base);
|
|
200
|
+
}</style>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ISegmentedControlRootProps } from './SegmentedControl.types';
|
|
2
|
+
declare const SegmentedControl: import("svelte").Component<ISegmentedControlRootProps, {}, "value">;
|
|
3
|
+
type SegmentedControl = ReturnType<typeof SegmentedControl>;
|
|
4
|
+
export default SegmentedControl;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { Writable } from 'svelte/store';
|
|
3
|
+
/** Shared context key so items can find the current root instance. */
|
|
4
|
+
export declare const SEGMENTED_CONTROL_CONTEXT: unique symbol;
|
|
5
|
+
export type SegmentedControlOrientation = 'horizontal' | 'vertical';
|
|
6
|
+
export interface ISegmentedControlItemEntry {
|
|
7
|
+
id: symbol;
|
|
8
|
+
value: string;
|
|
9
|
+
el: HTMLButtonElement | null;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface ISegmentedControlContext {
|
|
13
|
+
orientation: SegmentedControlOrientation;
|
|
14
|
+
disabled: Writable<boolean>;
|
|
15
|
+
selectedValue: Writable<string | null>;
|
|
16
|
+
selectValue: (value: string) => void;
|
|
17
|
+
registerItem: (entry: ISegmentedControlItemEntry) => void;
|
|
18
|
+
unregisterItem: (id: symbol) => void;
|
|
19
|
+
}
|
|
20
|
+
export interface ISegmentedControlRootProps {
|
|
21
|
+
/** Controlled value. */
|
|
22
|
+
value?: string | null;
|
|
23
|
+
/** Fallback selection when `value` is not supplied. */
|
|
24
|
+
defaultValue?: string;
|
|
25
|
+
/** Exposed gap between items. */
|
|
26
|
+
gap?: string;
|
|
27
|
+
/** Direction of the control. */
|
|
28
|
+
orientation?: SegmentedControlOrientation;
|
|
29
|
+
/** Disable the whole control. */
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
/** Custom CSS class names for the root wrapper. */
|
|
32
|
+
classNames?: string;
|
|
33
|
+
/** Inline style helpers for the root wrapper. */
|
|
34
|
+
style?: string;
|
|
35
|
+
/** Arbitrary variant attribute. */
|
|
36
|
+
dataVariant?: string;
|
|
37
|
+
/** Callback that fires when the active value changes. */
|
|
38
|
+
onValueChange?: (value: string) => void;
|
|
39
|
+
children: Snippet;
|
|
40
|
+
}
|
|
41
|
+
export interface ISegmentedControlItemProps {
|
|
42
|
+
/** Unique identifier for the item (must be provided). */
|
|
43
|
+
value: string;
|
|
44
|
+
/** Disable just this item. */
|
|
45
|
+
disabled?: boolean;
|
|
46
|
+
/** Custom CSS class names applied to the item button. */
|
|
47
|
+
classNames?: string;
|
|
48
|
+
/** Inline styles for the item button. */
|
|
49
|
+
style?: string;
|
|
50
|
+
/** Arbitrary variant attribute. */
|
|
51
|
+
dataVariant?: string;
|
|
52
|
+
/** Click handler passed from the consumer. */
|
|
53
|
+
onClick?: (event: MouseEvent) => void;
|
|
54
|
+
children: Snippet;
|
|
55
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getContext, onDestroy } from 'svelte';
|
|
3
|
+
import { writable } from 'svelte/store';
|
|
4
|
+
import {
|
|
5
|
+
SEGMENTED_CONTROL_CONTEXT,
|
|
6
|
+
type ISegmentedControlContext,
|
|
7
|
+
type ISegmentedControlItemProps
|
|
8
|
+
} from './SegmentedControl.types';
|
|
9
|
+
|
|
10
|
+
const fallbackSelectedValue = writable<string | null>(null);
|
|
11
|
+
const fallbackDisabled = writable(false);
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
value,
|
|
15
|
+
disabled = false,
|
|
16
|
+
classNames = '',
|
|
17
|
+
style,
|
|
18
|
+
dataVariant,
|
|
19
|
+
onClick,
|
|
20
|
+
children
|
|
21
|
+
}: ISegmentedControlItemProps = $props();
|
|
22
|
+
|
|
23
|
+
const context = getContext<ISegmentedControlContext>(SEGMENTED_CONTROL_CONTEXT);
|
|
24
|
+
const selectedValueStore = context?.selectedValue ?? fallbackSelectedValue;
|
|
25
|
+
const disabledStore = context?.disabled ?? fallbackDisabled;
|
|
26
|
+
const itemId = Symbol(value);
|
|
27
|
+
let buttonEl: HTMLButtonElement | null = null;
|
|
28
|
+
let isDisabled = $derived(disabled || $disabledStore);
|
|
29
|
+
let isSelected = $derived($selectedValueStore === value);
|
|
30
|
+
let tabIndex = $derived(($selectedValueStore !== null) ? (isSelected ? 0 : -1) : 0);
|
|
31
|
+
|
|
32
|
+
$effect(() => {
|
|
33
|
+
if (context) {
|
|
34
|
+
context.registerItem({
|
|
35
|
+
id: itemId,
|
|
36
|
+
value,
|
|
37
|
+
el: buttonEl,
|
|
38
|
+
disabled: isDisabled
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
let buttonClass = $state('');
|
|
44
|
+
$effect(() => {
|
|
45
|
+
const base = ['SegmentedControlItem'];
|
|
46
|
+
if (isSelected) base.push('SegmentedControlItem--selected');
|
|
47
|
+
if (isDisabled) base.push('SegmentedControlItem--disabled');
|
|
48
|
+
if (classNames) base.push(classNames);
|
|
49
|
+
buttonClass = base.join(' ');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
onDestroy(() => {
|
|
53
|
+
context?.unregisterItem(itemId);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function handleClick(event: MouseEvent) {
|
|
57
|
+
if (isDisabled) return;
|
|
58
|
+
context?.selectValue(value);
|
|
59
|
+
onClick?.(event);
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
class={buttonClass}
|
|
66
|
+
role="radio"
|
|
67
|
+
aria-checked={isSelected}
|
|
68
|
+
aria-disabled={isDisabled}
|
|
69
|
+
tabindex={tabIndex}
|
|
70
|
+
data-state={isSelected ? 'on' : 'off'}
|
|
71
|
+
{style}
|
|
72
|
+
onclick={handleClick}
|
|
73
|
+
bind:this={buttonEl}
|
|
74
|
+
disabled={isDisabled}
|
|
75
|
+
{...(dataVariant ? { 'data-variant': dataVariant } : {})}
|
|
76
|
+
>
|
|
77
|
+
{@render children?.()}
|
|
78
|
+
</button>
|
|
79
|
+
|
|
80
|
+
<style>.SegmentedControlItem {
|
|
81
|
+
flex: 1;
|
|
82
|
+
min-width: 0;
|
|
83
|
+
border: 0;
|
|
84
|
+
border-radius: 6px;
|
|
85
|
+
background: transparent;
|
|
86
|
+
color: var(--text-base-muted);
|
|
87
|
+
font-size: 0.875rem;
|
|
88
|
+
line-height: 1.3;
|
|
89
|
+
padding: 0.35rem 0.9rem;
|
|
90
|
+
cursor: pointer;
|
|
91
|
+
text-align: center;
|
|
92
|
+
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
|
93
|
+
align-items: center;
|
|
94
|
+
display: inline-flex;
|
|
95
|
+
justify-content: center;
|
|
96
|
+
gap: 0.35rem;
|
|
97
|
+
outline: none;
|
|
98
|
+
}
|
|
99
|
+
.SegmentedControlItem:not(:disabled):hover:not([data-state=on]) {
|
|
100
|
+
background: var(--bg-base-200);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.SegmentedControlItem[data-state=on] {
|
|
104
|
+
background: var(--bg-base-emp-100);
|
|
105
|
+
color: var(--text-base-inv);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.SegmentedControlItem--disabled {
|
|
109
|
+
opacity: 0.5;
|
|
110
|
+
cursor: not-allowed;
|
|
111
|
+
}</style>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ISegmentedControlItemProps } from './SegmentedControl.types';
|
|
2
|
+
declare const SegmentedControlItem: import("svelte").Component<ISegmentedControlItemProps, {}, "">;
|
|
3
|
+
type SegmentedControlItem = ReturnType<typeof SegmentedControlItem>;
|
|
4
|
+
export default SegmentedControlItem;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import SegmentedControlRoot from './SegmentedControl.svelte';
|
|
2
|
+
import SegmentedControlItem from './SegmentedControlItem.svelte';
|
|
3
|
+
declare const SegmentedControl: import("svelte").Component<import("./SegmentedControl.types").ISegmentedControlRootProps, {}, "value"> & {
|
|
4
|
+
Root: import("svelte").Component<import("./SegmentedControl.types").ISegmentedControlRootProps, {}, "value">;
|
|
5
|
+
Item: import("svelte").Component<import("./SegmentedControl.types").ISegmentedControlItemProps, {}, "">;
|
|
6
|
+
};
|
|
7
|
+
export { SegmentedControl, SegmentedControlRoot, SegmentedControlItem };
|
|
8
|
+
export * from './SegmentedControl.types';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import SegmentedControlRoot from './SegmentedControl.svelte';
|
|
2
|
+
import SegmentedControlItem from './SegmentedControlItem.svelte';
|
|
3
|
+
const SegmentedControl = Object.assign(SegmentedControlRoot, {
|
|
4
|
+
Root: SegmentedControlRoot,
|
|
5
|
+
Item: SegmentedControlItem
|
|
6
|
+
});
|
|
7
|
+
export { SegmentedControl, SegmentedControlRoot, SegmentedControlItem };
|
|
8
|
+
export * from './SegmentedControl.types';
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
minWidthMenu,
|
|
29
29
|
menuGap,
|
|
30
30
|
menuMaxHeight,
|
|
31
|
-
searchable=true
|
|
31
|
+
searchable=true,
|
|
32
|
+
onClear
|
|
32
33
|
}: ISelectProps = $props();
|
|
33
34
|
|
|
34
35
|
const menuId = `select-menu-${crypto.randomUUID()}`;
|
|
@@ -155,6 +156,7 @@
|
|
|
155
156
|
const handleClear = () => {
|
|
156
157
|
hideMenu();
|
|
157
158
|
value = null;
|
|
159
|
+
onClear?.();
|
|
158
160
|
}
|
|
159
161
|
|
|
160
162
|
</script>
|
|
@@ -168,7 +170,7 @@
|
|
|
168
170
|
readonly
|
|
169
171
|
onFocus={focusHandler}
|
|
170
172
|
onKeyDown={handleControlKeyDown}
|
|
171
|
-
onClear={handleClear}
|
|
173
|
+
onClear={onClear ? handleClear : undefined}
|
|
172
174
|
pseudoFocus={isMenuVisible}
|
|
173
175
|
onClick={handleControlClick}
|
|
174
176
|
variant="contained"
|
package/dist/components/index.js
CHANGED