@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.
@@ -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,4 @@
1
+ import type { MarketSelectorProps } from './market/market-selector-types.js';
2
+ declare const MarketSelector: import("svelte").Component<MarketSelectorProps, {}, "selectedMarket">;
3
+ type MarketSelector = ReturnType<typeof MarketSelector>;
4
+ export default MarketSelector;
@@ -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}
@@ -0,0 +1,4 @@
1
+ import type { SegmentedControlProps } from '../index.js';
2
+ declare const SegmentedControl: import("svelte").Component<SegmentedControlProps, {}, "value">;
3
+ type SegmentedControl = ReturnType<typeof SegmentedControl>;
4
+ export default SegmentedControl;
@@ -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>;