@makolabs/ripple 1.9.1 → 1.10.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/forms/MarketSelector.svelte +91 -0
- package/dist/forms/MarketSelector.svelte.d.ts +4 -0
- package/dist/forms/SegmentedControl.svelte +181 -0
- package/dist/forms/SegmentedControl.svelte.d.ts +4 -0
- package/dist/forms/form-types.d.ts +33 -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 +42 -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 +72 -0
- package/dist/forms/segmented-control.js +107 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
variant = undefined,
|
|
18
|
+
collapsed = false,
|
|
19
|
+
appearance = 'surface',
|
|
20
|
+
orientation = 'horizontal',
|
|
21
|
+
labelLayout = undefined,
|
|
22
|
+
labelClass = undefined,
|
|
23
|
+
color = Color.PRIMARY,
|
|
24
|
+
size = Size.BASE,
|
|
25
|
+
compact = false,
|
|
26
|
+
disabled = false,
|
|
27
|
+
class: className = '',
|
|
28
|
+
onchange = undefined,
|
|
29
|
+
testId
|
|
30
|
+
}: MarketSelectorProps = $props();
|
|
31
|
+
|
|
32
|
+
const resolvedAppearance = $derived.by(() => {
|
|
33
|
+
if (variant === 'default') return 'clarkDefault';
|
|
34
|
+
if (variant === 'sidebar') return 'clarkSidebar';
|
|
35
|
+
return appearance;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const resolvedLabelLayout = $derived(labelLayout ?? (variant !== undefined ? 'inline' : 'above'));
|
|
39
|
+
|
|
40
|
+
const resolvedOrientation = $derived(
|
|
41
|
+
variant === 'sidebar' && collapsed ? 'vertical' : orientation
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const resolvedCompact = $derived(variant === 'sidebar' && collapsed ? true : compact);
|
|
45
|
+
|
|
46
|
+
const resolvedLabelClass = $derived(
|
|
47
|
+
labelClass ?? (variant === 'sidebar' && collapsed && label ? 'sr-only' : undefined)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const options = $derived.by((): SegmentedOption[] => {
|
|
51
|
+
return markets.map((code) => ({
|
|
52
|
+
value: code,
|
|
53
|
+
label: code,
|
|
54
|
+
title: COUNTRY_NAMES[code],
|
|
55
|
+
prefix: showFlags ? countryCodeToFlagEmoji(code) : undefined
|
|
56
|
+
}));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
$effect.pre(() => {
|
|
60
|
+
const m = markets;
|
|
61
|
+
if (m.length === 0) return;
|
|
62
|
+
const cur = selectedMarket;
|
|
63
|
+
if (cur !== undefined && m.includes(cur as CountryCode)) return;
|
|
64
|
+
selectedMarket =
|
|
65
|
+
defaultMarket !== undefined && m.includes(defaultMarket) ? defaultMarket : m[0];
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function handleChange(v: string) {
|
|
69
|
+
onchange?.(v as CountryCode);
|
|
70
|
+
}
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
{#if markets.length > 0}
|
|
74
|
+
<SegmentedControl
|
|
75
|
+
class={className}
|
|
76
|
+
bind:value={selectedMarket}
|
|
77
|
+
{options}
|
|
78
|
+
{name}
|
|
79
|
+
{label}
|
|
80
|
+
appearance={resolvedAppearance}
|
|
81
|
+
orientation={resolvedOrientation}
|
|
82
|
+
labelLayout={resolvedLabelLayout}
|
|
83
|
+
labelClass={resolvedLabelClass}
|
|
84
|
+
{color}
|
|
85
|
+
{size}
|
|
86
|
+
compact={resolvedCompact}
|
|
87
|
+
{disabled}
|
|
88
|
+
{testId}
|
|
89
|
+
onchange={handleChange}
|
|
90
|
+
/>
|
|
91
|
+
{/if}
|
|
@@ -0,0 +1,181 @@
|
|
|
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 { segmentedLabelClasses, 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
|
+
labelLayout = 'above',
|
|
23
|
+
labelClass = '',
|
|
24
|
+
disabled = false,
|
|
25
|
+
errors = [],
|
|
26
|
+
onchange = undefined,
|
|
27
|
+
testId
|
|
28
|
+
}: SegmentedControlProps = $props();
|
|
29
|
+
|
|
30
|
+
const hasError = $derived(errors && errors.length > 0);
|
|
31
|
+
|
|
32
|
+
const clarkCollapsed = $derived(
|
|
33
|
+
compact && appearance === 'clarkSidebar' && orientation === 'vertical'
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const trackClass = $derived(
|
|
37
|
+
cn(
|
|
38
|
+
segmentedTrack({
|
|
39
|
+
appearance,
|
|
40
|
+
orientation,
|
|
41
|
+
clarkCollapsed
|
|
42
|
+
}),
|
|
43
|
+
orientation === 'vertical' && compact && appearance !== 'clarkSidebar' && 'items-stretch'
|
|
44
|
+
)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const rootClass = $derived(
|
|
48
|
+
cn(
|
|
49
|
+
labelLayout === 'inline' ? 'flex flex-row items-center gap-2' : 'flex flex-col gap-2',
|
|
50
|
+
className
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const labelTextClass = $derived(cn(segmentedLabelClasses(appearance, !!hasError), labelClass));
|
|
55
|
+
|
|
56
|
+
async function handleSelect(optionValue: string, optionDisabled: boolean | undefined) {
|
|
57
|
+
if (disabled || optionDisabled) return;
|
|
58
|
+
if (optionValue === value) return;
|
|
59
|
+
value = optionValue;
|
|
60
|
+
onchange?.(optionValue);
|
|
61
|
+
await tick();
|
|
62
|
+
const idx = options.findIndex((o) => o.value === optionValue);
|
|
63
|
+
if (idx >= 0 && typeof document !== 'undefined') {
|
|
64
|
+
document.getElementById(`${groupId}-seg-${idx}`)?.focus();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolvedTitle(option: (typeof options)[number]): string {
|
|
69
|
+
return option.title ?? option.label;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isOptionDisabled(option: (typeof options)[number]): boolean {
|
|
73
|
+
return disabled || !!option.disabled;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function moveSelection(fromIndex: number, delta: 1 | -1) {
|
|
77
|
+
const len = options.length;
|
|
78
|
+
if (len === 0) return;
|
|
79
|
+
let i = fromIndex;
|
|
80
|
+
for (let step = 0; step < len; step++) {
|
|
81
|
+
i = (i + delta + len) % len;
|
|
82
|
+
const opt = options[i];
|
|
83
|
+
if (!isOptionDisabled(opt)) {
|
|
84
|
+
void handleSelect(opt.value, opt.disabled);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function handleSegmentKeydown(e: KeyboardEvent, index: number) {
|
|
91
|
+
const forward = orientation === 'horizontal' ? e.key === 'ArrowRight' : e.key === 'ArrowDown';
|
|
92
|
+
const backward = orientation === 'horizontal' ? e.key === 'ArrowLeft' : e.key === 'ArrowUp';
|
|
93
|
+
if (forward) {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
moveSelection(index, 1);
|
|
96
|
+
} else if (backward) {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
moveSelection(index, -1);
|
|
99
|
+
} else if (e.key === 'Home') {
|
|
100
|
+
e.preventDefault();
|
|
101
|
+
const first = options.findIndex((o) => !isOptionDisabled(o));
|
|
102
|
+
if (first >= 0) void handleSelect(options[first].value, options[first].disabled);
|
|
103
|
+
} else if (e.key === 'End') {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
for (let i = options.length - 1; i >= 0; i--) {
|
|
106
|
+
if (!isOptionDisabled(options[i])) {
|
|
107
|
+
void handleSelect(options[i].value, options[i].disabled);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const isClark = $derived(appearance === 'clarkDefault' || appearance === 'clarkSidebar');
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<div class={rootClass}>
|
|
118
|
+
{#if label}
|
|
119
|
+
<span id="{groupId}-label" class={labelTextClass}>
|
|
120
|
+
{label}
|
|
121
|
+
</span>
|
|
122
|
+
{/if}
|
|
123
|
+
{#if hasError}
|
|
124
|
+
<div class="text-danger-500 text-sm">{errors[0]}</div>
|
|
125
|
+
{/if}
|
|
126
|
+
|
|
127
|
+
<div
|
|
128
|
+
class={trackClass}
|
|
129
|
+
role="radiogroup"
|
|
130
|
+
aria-labelledby={label ? `${groupId}-label` : undefined}
|
|
131
|
+
aria-orientation={orientation === 'vertical' ? 'vertical' : 'horizontal'}
|
|
132
|
+
data-testid={buildTestId('segmented', undefined, testId)}
|
|
133
|
+
>
|
|
134
|
+
{#each options as option, index (option.value)}
|
|
135
|
+
{@const isSelected = value === option.value}
|
|
136
|
+
{@const isFirst = index === 0}
|
|
137
|
+
{@const isLast = index === options.length - 1}
|
|
138
|
+
{@const segDisabled = disabled || option.disabled}
|
|
139
|
+
<button
|
|
140
|
+
id="{groupId}-seg-{index}"
|
|
141
|
+
type="button"
|
|
142
|
+
role="radio"
|
|
143
|
+
aria-checked={isSelected}
|
|
144
|
+
aria-disabled={segDisabled ? 'true' : undefined}
|
|
145
|
+
tabindex={segDisabled ? -1 : isSelected ? 0 : -1}
|
|
146
|
+
title={resolvedTitle(option)}
|
|
147
|
+
class={segmentClasses({
|
|
148
|
+
selected: isSelected,
|
|
149
|
+
disabled: !!segDisabled,
|
|
150
|
+
appearance,
|
|
151
|
+
color: color as VariantColors,
|
|
152
|
+
size: size as VariantSizes,
|
|
153
|
+
isFirst,
|
|
154
|
+
isLast,
|
|
155
|
+
orientation,
|
|
156
|
+
clarkCollapsed
|
|
157
|
+
})}
|
|
158
|
+
data-testid={buildTestId('segmented', 'item', testId, index)}
|
|
159
|
+
onclick={() => handleSelect(option.value, option.disabled)}
|
|
160
|
+
onkeydown={(e) => handleSegmentKeydown(e, index)}
|
|
161
|
+
>
|
|
162
|
+
{#if option.prefix}
|
|
163
|
+
<span class="text-base leading-none" aria-hidden="true">{option.prefix}</span>
|
|
164
|
+
{/if}
|
|
165
|
+
<span
|
|
166
|
+
class={cn(
|
|
167
|
+
!compact && (size === Size.XS || size === Size.SM) && !isClark && 'text-sm',
|
|
168
|
+
isClark && !compact && 'text-xs font-medium',
|
|
169
|
+
compact && 'sr-only'
|
|
170
|
+
)}
|
|
171
|
+
>
|
|
172
|
+
{option.label}
|
|
173
|
+
</span>
|
|
174
|
+
</button>
|
|
175
|
+
{/each}
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{#if name}
|
|
180
|
+
<input type="hidden" {name} {value} />
|
|
181
|
+
{/if}
|
|
@@ -166,3 +166,36 @@ 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
|
+
/**
|
|
184
|
+
* - `surface` / `inverted`: generic Ripple styling (`color` applies).
|
|
185
|
+
* - `clarkDefault` / `clarkSidebar`: match Clark AR market bar (fixed blue selection, inline-friendly).
|
|
186
|
+
*/
|
|
187
|
+
appearance?: 'surface' | 'inverted' | 'clarkDefault' | 'clarkSidebar';
|
|
188
|
+
orientation?: 'horizontal' | 'vertical';
|
|
189
|
+
color?: VariantColors;
|
|
190
|
+
size?: VariantSizes;
|
|
191
|
+
/** Hide segment label text; prefix only (`sr-only`); with Clark sidebar + vertical = collapsed column */
|
|
192
|
+
compact?: boolean;
|
|
193
|
+
/** Label above the track vs beside the track (Clark uses `inline`) */
|
|
194
|
+
labelLayout?: 'above' | 'inline';
|
|
195
|
+
/** Optional class on the visible label (e.g. `sr-only` when collapsed sidebar) */
|
|
196
|
+
labelClass?: ClassValue;
|
|
197
|
+
disabled?: boolean;
|
|
198
|
+
errors?: string[];
|
|
199
|
+
onchange?: (value: string) => void;
|
|
200
|
+
testId?: string;
|
|
201
|
+
};
|
|
@@ -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>;
|