@makolabs/ripple 1.9.0 → 1.9.2
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/forms/MarketSelector.svelte +67 -0
- package/dist/forms/MarketSelector.svelte.d.ts +4 -0
- package/dist/forms/SegmentedControl.svelte +156 -0
- package/dist/forms/SegmentedControl.svelte.d.ts +4 -0
- package/dist/forms/form-types.d.ts +26 -0
- package/dist/forms/market/country-data.d.ts +5 -0
- package/dist/forms/market/country-data.js +504 -0
- package/dist/forms/market/flag-emoji.d.ts +5 -0
- package/dist/forms/market/flag-emoji.js +11 -0
- package/dist/forms/market/market-selector-types.d.ts +27 -0
- package/dist/forms/market/market-selector-types.js +1 -0
- package/dist/forms/market/market.d.ts +255 -0
- package/dist/forms/market/market.js +252 -0
- package/dist/forms/segmented-control.d.ts +39 -0
- package/dist/forms/segmented-control.js +54 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +6 -0
- package/dist/layout/sidebar/Sidebar.svelte +10 -1
- package/dist/layout/sidebar/sidebar-types.d.ts +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Color, Size } from '../variants.js';
|
|
3
|
+
import SegmentedControl from './SegmentedControl.svelte';
|
|
4
|
+
import { COUNTRY_NAMES } from './market/country-data.js';
|
|
5
|
+
import { countryCodeToFlagEmoji } from './market/flag-emoji.js';
|
|
6
|
+
import type { CountryCode } from './market/country-data.js';
|
|
7
|
+
import type { MarketSelectorProps } from './market/market-selector-types.js';
|
|
8
|
+
import type { SegmentedOption } from '../index.js';
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
markets,
|
|
12
|
+
selectedMarket = $bindable(),
|
|
13
|
+
defaultMarket,
|
|
14
|
+
name = '',
|
|
15
|
+
showFlags = true,
|
|
16
|
+
label = '',
|
|
17
|
+
appearance = 'surface',
|
|
18
|
+
orientation = 'horizontal',
|
|
19
|
+
color = Color.PRIMARY,
|
|
20
|
+
size = Size.BASE,
|
|
21
|
+
compact = false,
|
|
22
|
+
disabled = false,
|
|
23
|
+
class: className = '',
|
|
24
|
+
onchange = undefined,
|
|
25
|
+
testId
|
|
26
|
+
}: MarketSelectorProps = $props();
|
|
27
|
+
|
|
28
|
+
const options = $derived.by((): SegmentedOption[] => {
|
|
29
|
+
return markets.map((code) => ({
|
|
30
|
+
value: code,
|
|
31
|
+
label: code,
|
|
32
|
+
title: COUNTRY_NAMES[code],
|
|
33
|
+
prefix: showFlags ? countryCodeToFlagEmoji(code) : undefined
|
|
34
|
+
}));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
$effect.pre(() => {
|
|
38
|
+
const m = markets;
|
|
39
|
+
if (m.length === 0) return;
|
|
40
|
+
const cur = selectedMarket;
|
|
41
|
+
if (cur !== undefined && m.includes(cur as CountryCode)) return;
|
|
42
|
+
selectedMarket =
|
|
43
|
+
defaultMarket !== undefined && m.includes(defaultMarket) ? defaultMarket : m[0];
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function handleChange(v: string) {
|
|
47
|
+
onchange?.(v as CountryCode);
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
{#if markets.length > 0}
|
|
52
|
+
<SegmentedControl
|
|
53
|
+
class={className}
|
|
54
|
+
bind:value={selectedMarket}
|
|
55
|
+
{options}
|
|
56
|
+
{name}
|
|
57
|
+
{label}
|
|
58
|
+
{appearance}
|
|
59
|
+
{orientation}
|
|
60
|
+
{color}
|
|
61
|
+
{size}
|
|
62
|
+
{compact}
|
|
63
|
+
{disabled}
|
|
64
|
+
{testId}
|
|
65
|
+
onchange={handleChange}
|
|
66
|
+
/>
|
|
67
|
+
{/if}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import { cn } from '../helper/cls.js';
|
|
4
|
+
import { buildTestId } from '../helper/testid.js';
|
|
5
|
+
import { Color, Size } from '../variants.js';
|
|
6
|
+
import { segmentedTrack, segmentClasses } from './segmented-control.js';
|
|
7
|
+
import type { SegmentedControlProps, VariantColors, VariantSizes } from '../index.js';
|
|
8
|
+
|
|
9
|
+
const groupId = `seg-${Math.random().toString(36).slice(2, 11)}`;
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
options = [],
|
|
13
|
+
value = $bindable(options.length > 0 ? options[0].value : ''),
|
|
14
|
+
name = '',
|
|
15
|
+
label = '',
|
|
16
|
+
class: className = '',
|
|
17
|
+
appearance = 'surface',
|
|
18
|
+
orientation = 'horizontal',
|
|
19
|
+
color = Color.PRIMARY,
|
|
20
|
+
size = Size.BASE,
|
|
21
|
+
compact = false,
|
|
22
|
+
disabled = false,
|
|
23
|
+
errors = [],
|
|
24
|
+
onchange = undefined,
|
|
25
|
+
testId
|
|
26
|
+
}: SegmentedControlProps = $props();
|
|
27
|
+
|
|
28
|
+
const hasError = $derived(errors && errors.length > 0);
|
|
29
|
+
|
|
30
|
+
const trackClass = $derived(
|
|
31
|
+
cn(
|
|
32
|
+
segmentedTrack({ appearance, orientation }),
|
|
33
|
+
orientation === 'vertical' && compact && 'items-stretch'
|
|
34
|
+
)
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
async function handleSelect(optionValue: string, optionDisabled: boolean | undefined) {
|
|
38
|
+
if (disabled || optionDisabled) return;
|
|
39
|
+
if (optionValue === value) return;
|
|
40
|
+
value = optionValue;
|
|
41
|
+
onchange?.(optionValue);
|
|
42
|
+
await tick();
|
|
43
|
+
const idx = options.findIndex((o) => o.value === optionValue);
|
|
44
|
+
if (idx >= 0 && typeof document !== 'undefined') {
|
|
45
|
+
document.getElementById(`${groupId}-seg-${idx}`)?.focus();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolvedTitle(option: (typeof options)[number]): string {
|
|
50
|
+
return option.title ?? option.label;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isOptionDisabled(option: (typeof options)[number]): boolean {
|
|
54
|
+
return disabled || !!option.disabled;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function moveSelection(fromIndex: number, delta: 1 | -1) {
|
|
58
|
+
const len = options.length;
|
|
59
|
+
if (len === 0) return;
|
|
60
|
+
let i = fromIndex;
|
|
61
|
+
for (let step = 0; step < len; step++) {
|
|
62
|
+
i = (i + delta + len) % len;
|
|
63
|
+
const opt = options[i];
|
|
64
|
+
if (!isOptionDisabled(opt)) {
|
|
65
|
+
void handleSelect(opt.value, opt.disabled);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function handleSegmentKeydown(e: KeyboardEvent, index: number) {
|
|
72
|
+
const forward = orientation === 'horizontal' ? e.key === 'ArrowRight' : e.key === 'ArrowDown';
|
|
73
|
+
const backward = orientation === 'horizontal' ? e.key === 'ArrowLeft' : e.key === 'ArrowUp';
|
|
74
|
+
if (forward) {
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
moveSelection(index, 1);
|
|
77
|
+
} else if (backward) {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
moveSelection(index, -1);
|
|
80
|
+
} else if (e.key === 'Home') {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
const first = options.findIndex((o) => !isOptionDisabled(o));
|
|
83
|
+
if (first >= 0) void handleSelect(options[first].value, options[first].disabled);
|
|
84
|
+
} else if (e.key === 'End') {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
for (let i = options.length - 1; i >= 0; i--) {
|
|
87
|
+
if (!isOptionDisabled(options[i])) {
|
|
88
|
+
void handleSelect(options[i].value, options[i].disabled);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<div class={cn('flex flex-col gap-2', className)}>
|
|
97
|
+
{#if hasError}
|
|
98
|
+
<div class="text-danger-500 text-sm">{errors[0]}</div>
|
|
99
|
+
{/if}
|
|
100
|
+
{#if label}
|
|
101
|
+
<span
|
|
102
|
+
id="{groupId}-label"
|
|
103
|
+
class={cn('text-sm font-medium', hasError ? 'text-danger-500' : 'text-default-700')}
|
|
104
|
+
>
|
|
105
|
+
{label}
|
|
106
|
+
</span>
|
|
107
|
+
{/if}
|
|
108
|
+
|
|
109
|
+
<div
|
|
110
|
+
class={trackClass}
|
|
111
|
+
role="radiogroup"
|
|
112
|
+
aria-labelledby={label ? `${groupId}-label` : undefined}
|
|
113
|
+
aria-orientation={orientation === 'vertical' ? 'vertical' : 'horizontal'}
|
|
114
|
+
data-testid={buildTestId('segmented', undefined, testId)}
|
|
115
|
+
>
|
|
116
|
+
{#each options as option, index (option.value)}
|
|
117
|
+
{@const isSelected = value === option.value}
|
|
118
|
+
{@const isFirst = index === 0}
|
|
119
|
+
{@const isLast = index === options.length - 1}
|
|
120
|
+
{@const segDisabled = disabled || option.disabled}
|
|
121
|
+
<button
|
|
122
|
+
id="{groupId}-seg-{index}"
|
|
123
|
+
type="button"
|
|
124
|
+
role="radio"
|
|
125
|
+
aria-checked={isSelected}
|
|
126
|
+
aria-disabled={segDisabled ? 'true' : undefined}
|
|
127
|
+
tabindex={segDisabled ? -1 : isSelected ? 0 : -1}
|
|
128
|
+
title={resolvedTitle(option)}
|
|
129
|
+
class={segmentClasses({
|
|
130
|
+
selected: isSelected,
|
|
131
|
+
disabled: !!segDisabled,
|
|
132
|
+
appearance,
|
|
133
|
+
color: color as VariantColors,
|
|
134
|
+
size: size as VariantSizes,
|
|
135
|
+
isFirst,
|
|
136
|
+
isLast,
|
|
137
|
+
orientation
|
|
138
|
+
})}
|
|
139
|
+
data-testid={buildTestId('segmented', 'item', testId, index)}
|
|
140
|
+
onclick={() => handleSelect(option.value, option.disabled)}
|
|
141
|
+
onkeydown={(e) => handleSegmentKeydown(e, index)}
|
|
142
|
+
>
|
|
143
|
+
{#if option.prefix}
|
|
144
|
+
<span class="text-base leading-none" aria-hidden="true">{option.prefix}</span>
|
|
145
|
+
{/if}
|
|
146
|
+
<span class:text-sm={size === Size.XS || size === Size.SM} class:sr-only={compact}>
|
|
147
|
+
{option.label}
|
|
148
|
+
</span>
|
|
149
|
+
</button>
|
|
150
|
+
{/each}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{#if name}
|
|
155
|
+
<input type="hidden" {name} {value} />
|
|
156
|
+
{/if}
|
|
@@ -166,3 +166,29 @@ export type RadioPillProps = {
|
|
|
166
166
|
onchange?: (value: string) => void;
|
|
167
167
|
testId?: string;
|
|
168
168
|
};
|
|
169
|
+
/** Single segment in a fused SegmentedControl bar (not TabGroup — no panels). */
|
|
170
|
+
export type SegmentedOption = RadioOption & {
|
|
171
|
+
/** Shown before label, e.g. flag emoji */
|
|
172
|
+
prefix?: string;
|
|
173
|
+
/** `title` on the segment button */
|
|
174
|
+
title?: string;
|
|
175
|
+
disabled?: boolean;
|
|
176
|
+
};
|
|
177
|
+
export type SegmentedControlProps = {
|
|
178
|
+
options: SegmentedOption[];
|
|
179
|
+
value?: string;
|
|
180
|
+
name?: string;
|
|
181
|
+
label?: string;
|
|
182
|
+
class?: ClassValue;
|
|
183
|
+
/** Light track (e.g. page toolbar) vs dark track (e.g. sidebar) */
|
|
184
|
+
appearance?: 'surface' | 'inverted';
|
|
185
|
+
orientation?: 'horizontal' | 'vertical';
|
|
186
|
+
color?: VariantColors;
|
|
187
|
+
size?: VariantSizes;
|
|
188
|
+
/** Hide label text; keep prefix only (label is `sr-only` for screen readers) */
|
|
189
|
+
compact?: boolean;
|
|
190
|
+
disabled?: boolean;
|
|
191
|
+
errors?: string[];
|
|
192
|
+
onchange?: (value: string) => void;
|
|
193
|
+
testId?: string;
|
|
194
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** ISO 3166-1 alpha-2 codes (generated from ISO dataset). */
|
|
2
|
+
export declare const ALL_COUNTRY_CODES: readonly ["AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW"];
|
|
3
|
+
export type CountryCode = (typeof ALL_COUNTRY_CODES)[number];
|
|
4
|
+
/** English short names (ISO list). */
|
|
5
|
+
export declare const COUNTRY_NAMES: Record<CountryCode, string>;
|