@spectral_labs/ui 1.0.2 → 1.1.1
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/app-bar/app-bar.svelte +19 -0
- package/dist/components/combobox/__tests__/ComboboxTest.svelte +8 -1
- package/dist/components/combobox/__tests__/combobox.test.svelte.js +59 -1
- package/dist/components/combobox/components/combobox-content.svelte +36 -5
- package/dist/components/combobox/components/combobox-content.svelte.d.ts.map +1 -1
- package/dist/components/combobox/index.d.ts +1 -1
- package/dist/components/combobox/index.d.ts.map +1 -1
- package/dist/components/combobox/types.d.ts +28 -0
- package/dist/components/combobox/types.d.ts.map +1 -1
- package/dist/components/language-select/__tests__/language-select.test.svelte.js +199 -0
- package/dist/components/language-select/__tests__/languages.test.js +87 -0
- package/dist/components/language-select/index.d.ts +3 -0
- package/dist/components/language-select/index.d.ts.map +1 -0
- package/dist/components/language-select/index.js +1 -0
- package/dist/components/language-select/language-select.svelte +200 -0
- package/dist/components/language-select/language-select.svelte.d.ts +5 -0
- package/dist/components/language-select/language-select.svelte.d.ts.map +1 -0
- package/dist/components/language-select/types.d.ts +42 -0
- package/dist/components/language-select/types.d.ts.map +1 -0
- package/dist/components/language-select/types.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/internal/i18n/index.d.ts +3 -0
- package/dist/internal/i18n/index.d.ts.map +1 -0
- package/dist/internal/i18n/index.js +1 -0
- package/dist/internal/i18n/languages.d.ts +33 -0
- package/dist/internal/i18n/languages.d.ts.map +1 -0
- package/dist/internal/i18n/languages.js +42 -0
- package/package.json +5 -1
|
@@ -83,6 +83,25 @@
|
|
|
83
83
|
flex-shrink: 0;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Push the trailing slot to the far right of `.sp-app-bar-row`. In small
|
|
88
|
+
* mode the row layout is `leading | title | children | trailing` ; only
|
|
89
|
+
* `.sp-app-bar-title` (when present) carries `flex: 1` to push trailing
|
|
90
|
+
* right. When the consumer renders the brand inside the `leading`
|
|
91
|
+
* snippet and omits `title` (e.g. to keep a clickable brand link), the
|
|
92
|
+
* trailing collapsed against the leading. `margin-left: auto` makes the
|
|
93
|
+
* trailing universally right-aligned without changing the title spacer
|
|
94
|
+
* logic — neutral when title is present (title already absorbs the row
|
|
95
|
+
* flex), corrective when title is absent.
|
|
96
|
+
*
|
|
97
|
+
* Medium/large modes use `.sp-app-bar-spacer` as the explicit flex
|
|
98
|
+
* spacer, so trailing is already right-aligned there. We scope this
|
|
99
|
+
* fix to `.small` only to avoid surprise.
|
|
100
|
+
*/
|
|
101
|
+
.sp-app-bar.small .sp-app-bar-trailing {
|
|
102
|
+
margin-left: auto;
|
|
103
|
+
}
|
|
104
|
+
|
|
86
105
|
.sp-app-bar-headline {
|
|
87
106
|
padding: 0 1rem 1.5rem;
|
|
88
107
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang='ts'>
|
|
2
|
+
import type { Align, Side } from '../../../internal/floating-layer/types.js'
|
|
2
3
|
import ComboboxContent from '../components/combobox-content.svelte'
|
|
3
4
|
import ComboboxInput from '../components/combobox-input.svelte'
|
|
4
5
|
import ComboboxItem from '../components/combobox-item.svelte'
|
|
@@ -11,6 +12,9 @@
|
|
|
11
12
|
portalTo,
|
|
12
13
|
disablePortal,
|
|
13
14
|
contentClass,
|
|
15
|
+
side,
|
|
16
|
+
sideOffset,
|
|
17
|
+
align,
|
|
14
18
|
onOpenChange,
|
|
15
19
|
}: {
|
|
16
20
|
value?: string
|
|
@@ -19,13 +23,16 @@
|
|
|
19
23
|
portalTo?: HTMLElement | string
|
|
20
24
|
disablePortal?: boolean
|
|
21
25
|
contentClass?: string
|
|
26
|
+
side?: Side
|
|
27
|
+
sideOffset?: number
|
|
28
|
+
align?: Align
|
|
22
29
|
onOpenChange?: (open: boolean) => void
|
|
23
30
|
} = $props()
|
|
24
31
|
</script>
|
|
25
32
|
|
|
26
33
|
<ComboboxRoot bind:value bind:open {name} {onOpenChange}>
|
|
27
34
|
<ComboboxInput placeholder='Search...' />
|
|
28
|
-
<ComboboxContent {portalTo} {disablePortal} class={contentClass}>
|
|
35
|
+
<ComboboxContent {portalTo} {disablePortal} class={contentClass} {side} {sideOffset} {align}>
|
|
29
36
|
<ComboboxItem value='apple'>Apple</ComboboxItem>
|
|
30
37
|
<ComboboxItem value='banana'>Banana</ComboboxItem>
|
|
31
38
|
<ComboboxItem value='cherry'>Cherry</ComboboxItem>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { flushSync, mount, unmount } from 'svelte';
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
3
|
import ComboboxRoot from '../components/combobox-root.svelte';
|
|
4
4
|
import ComboboxTest from './ComboboxTest.svelte';
|
|
5
5
|
describe('combobox', () => {
|
|
@@ -142,6 +142,64 @@ describe('combobox', () => {
|
|
|
142
142
|
// Should have opened because isComposing was false
|
|
143
143
|
expect(target.querySelector('.sp-combobox-root')?.getAttribute('data-state')).toBe('open');
|
|
144
144
|
});
|
|
145
|
+
describe('content positioning (FloatingLayerState)', () => {
|
|
146
|
+
// Floating UI's computePosition is async; give it a microtask to run.
|
|
147
|
+
async function openAndWaitForFloating(props = {}) {
|
|
148
|
+
setup(props);
|
|
149
|
+
const input = target.querySelector('[role="combobox"]');
|
|
150
|
+
input.dispatchEvent(new FocusEvent('focus', { bubbles: true }));
|
|
151
|
+
flushSync();
|
|
152
|
+
}
|
|
153
|
+
it('applies inline position:absolute on content after opening', async () => {
|
|
154
|
+
await openAndWaitForFloating({ disablePortal: true });
|
|
155
|
+
const content = target.querySelector('.sp-combobox-content');
|
|
156
|
+
await vi.waitFor(() => {
|
|
157
|
+
expect(content.style.position).toBe('absolute');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
it('applies inline top and left written by Floating UI (not CSS 100%/0)', async () => {
|
|
161
|
+
await openAndWaitForFloating({ disablePortal: true });
|
|
162
|
+
const content = target.querySelector('.sp-combobox-content');
|
|
163
|
+
await vi.waitFor(() => {
|
|
164
|
+
// Floating UI writes pixel values — in jsdom they resolve to '0px'
|
|
165
|
+
// since getBoundingClientRect returns zero, but the values must be
|
|
166
|
+
// *inline styles* (not CSS rules) so the '100%' top and '0' right
|
|
167
|
+
// that caused the bug are gone.
|
|
168
|
+
expect(content.style.top).toMatch(/px$/);
|
|
169
|
+
expect(content.style.left).toMatch(/px$/);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
it('accepts the side prop (default: bottom)', async () => {
|
|
173
|
+
await openAndWaitForFloating({ disablePortal: true, side: 'top' });
|
|
174
|
+
const content = target.querySelector('.sp-combobox-content');
|
|
175
|
+
expect(content).toBeTruthy();
|
|
176
|
+
});
|
|
177
|
+
it('accepts the sideOffset prop', async () => {
|
|
178
|
+
await openAndWaitForFloating({ disablePortal: true, sideOffset: 12 });
|
|
179
|
+
const content = target.querySelector('.sp-combobox-content');
|
|
180
|
+
expect(content).toBeTruthy();
|
|
181
|
+
});
|
|
182
|
+
it('accepts the align prop', async () => {
|
|
183
|
+
await openAndWaitForFloating({ disablePortal: true, align: 'center' });
|
|
184
|
+
const content = target.querySelector('.sp-combobox-content');
|
|
185
|
+
expect(content).toBeTruthy();
|
|
186
|
+
});
|
|
187
|
+
it('does not stretch to full body width anymore when portaled', async () => {
|
|
188
|
+
// Regression: the bug rendered the content with right:0 against body,
|
|
189
|
+
// producing a full-viewport-width menu. After the fix, the content is
|
|
190
|
+
// positioned (via FloatingLayerState) and does NOT have `right: 0`
|
|
191
|
+
// inherited from the stylesheet.
|
|
192
|
+
await openAndWaitForFloating({});
|
|
193
|
+
const content = document.body.querySelector('.sp-combobox-content');
|
|
194
|
+
await vi.waitFor(() => {
|
|
195
|
+
const cs = window.getComputedStyle(content);
|
|
196
|
+
// Floating UI writes `left` inline → `right` in CSS should not be 0
|
|
197
|
+
// (the old buggy rule `right: 0` is removed). A reasonable proxy:
|
|
198
|
+
// assert that `width` isn't pinned to viewport / body width.
|
|
199
|
+
expect(cs.right).not.toBe('0px');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
});
|
|
145
203
|
describe('portal opt-out (disablePortal)', () => {
|
|
146
204
|
it('portals to document.body by default', () => {
|
|
147
205
|
const localTarget = document.createElement('div');
|
|
@@ -2,12 +2,24 @@
|
|
|
2
2
|
import type { ComboboxContentProps } from '../types.js'
|
|
3
3
|
import { DismissibleLayerState } from '../../../internal/dismissible-layer/index.js'
|
|
4
4
|
import { EscapeLayerState } from '../../../internal/escape-layer/index.js'
|
|
5
|
+
import { FloatingLayerState } from '../../../internal/floating-layer/index.js'
|
|
5
6
|
import { isBrowser } from '../../../internal/helpers/index.js'
|
|
6
7
|
import { Portal } from '../../../internal/portal/index.js'
|
|
7
8
|
import { PresenceLayerState } from '../../../internal/presence-layer/index.js'
|
|
8
9
|
import { getComboboxContext } from '../combobox.svelte.js'
|
|
9
10
|
|
|
10
|
-
const {
|
|
11
|
+
const {
|
|
12
|
+
side = 'bottom',
|
|
13
|
+
sideOffset = 4,
|
|
14
|
+
align = 'start',
|
|
15
|
+
alignOffset = 0,
|
|
16
|
+
avoidCollisions = true,
|
|
17
|
+
portalTo,
|
|
18
|
+
disablePortal,
|
|
19
|
+
class: className,
|
|
20
|
+
children,
|
|
21
|
+
}: ComboboxContentProps = $props()
|
|
22
|
+
|
|
11
23
|
const ctx = getComboboxContext()
|
|
12
24
|
let contentEl: HTMLElement | undefined = $state()
|
|
13
25
|
|
|
@@ -32,16 +44,38 @@
|
|
|
32
44
|
ctx.close()
|
|
33
45
|
},
|
|
34
46
|
})
|
|
47
|
+
const floating = new FloatingLayerState({
|
|
48
|
+
reference: null,
|
|
49
|
+
floating: null,
|
|
50
|
+
get side() {
|
|
51
|
+
return side
|
|
52
|
+
},
|
|
53
|
+
get sideOffset() {
|
|
54
|
+
return sideOffset
|
|
55
|
+
},
|
|
56
|
+
get align() {
|
|
57
|
+
return align
|
|
58
|
+
},
|
|
59
|
+
get alignOffset() {
|
|
60
|
+
return alignOffset
|
|
61
|
+
},
|
|
62
|
+
get avoidCollisions() {
|
|
63
|
+
return avoidCollisions
|
|
64
|
+
},
|
|
65
|
+
})
|
|
35
66
|
|
|
36
67
|
$effect(() => {
|
|
37
68
|
if (ctx.open && isBrowser && contentEl) {
|
|
38
69
|
escapeLayer.mount()
|
|
39
70
|
dismissLayer.props.element = contentEl
|
|
40
71
|
dismissLayer.mount()
|
|
72
|
+
floating.setElements(ctx.inputEl, contentEl)
|
|
73
|
+
floating.startAutoUpdate()
|
|
41
74
|
}
|
|
42
75
|
return () => {
|
|
43
76
|
escapeLayer.unmount()
|
|
44
77
|
dismissLayer.unmount()
|
|
78
|
+
floating.stopAutoUpdate()
|
|
45
79
|
}
|
|
46
80
|
})
|
|
47
81
|
|
|
@@ -69,11 +103,8 @@
|
|
|
69
103
|
<style>
|
|
70
104
|
.sp-combobox-content {
|
|
71
105
|
position: absolute;
|
|
72
|
-
top: 100%;
|
|
73
|
-
left: 0;
|
|
74
|
-
right: 0;
|
|
75
106
|
z-index: 50;
|
|
76
|
-
|
|
107
|
+
min-width: var(--sp-floating-anchor-width, 12rem);
|
|
77
108
|
padding: 0.25rem;
|
|
78
109
|
background-color: var(--sp-sys-color-surface-container);
|
|
79
110
|
border-radius: var(--sp-sys-shape-corner-md, 12px);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"combobox-content.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/combobox/components/combobox-content.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"combobox-content.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/combobox/components/combobox-content.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AA4GvD,QAAA,MAAM,eAAe,0DAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
|
|
@@ -2,5 +2,5 @@ export { default as Content } from './components/combobox-content.svelte';
|
|
|
2
2
|
export { default as Input } from './components/combobox-input.svelte';
|
|
3
3
|
export { default as Item } from './components/combobox-item.svelte';
|
|
4
4
|
export { default as Root } from './components/combobox-root.svelte';
|
|
5
|
-
export type { ComboboxItemProps, ComboboxProps } from './types.js';
|
|
5
|
+
export type { ComboboxContentProps, ComboboxInputProps, ComboboxItemProps, ComboboxProps } from './types.js';
|
|
6
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/components/combobox/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,sCAAsC,CAAA;AACzE,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,oCAAoC,CAAA;AACrE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,mCAAmC,CAAA;AACnE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,mCAAmC,CAAA;AACnE,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/components/combobox/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,sCAAsC,CAAA;AACzE,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,oCAAoC,CAAA;AACrE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,mCAAmC,CAAA;AACnE,OAAO,EAAE,OAAO,IAAI,IAAI,EAAE,MAAM,mCAAmC,CAAA;AACnE,YAAY,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
+
import type { Align, Side } from '../../internal/floating-layer/types.js';
|
|
3
4
|
export interface ComboboxProps {
|
|
4
5
|
/** Selected value. */
|
|
5
6
|
value?: string;
|
|
@@ -24,6 +25,33 @@ export interface ComboboxInputProps extends Omit<HTMLInputAttributes, 'class' |
|
|
|
24
25
|
class?: string;
|
|
25
26
|
}
|
|
26
27
|
export interface ComboboxContentProps {
|
|
28
|
+
/**
|
|
29
|
+
* Preferred side of the input to render the listbox on. The listbox may
|
|
30
|
+
* flip to the opposite side if collision detection is enabled and there
|
|
31
|
+
* is not enough space.
|
|
32
|
+
* @default 'bottom'
|
|
33
|
+
*/
|
|
34
|
+
side?: Side;
|
|
35
|
+
/**
|
|
36
|
+
* Distance in pixels between the input and the listbox.
|
|
37
|
+
* @default 4
|
|
38
|
+
*/
|
|
39
|
+
sideOffset?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Alignment along the perpendicular axis of `side`.
|
|
42
|
+
* @default 'start'
|
|
43
|
+
*/
|
|
44
|
+
align?: Align;
|
|
45
|
+
/**
|
|
46
|
+
* Pixel offset along the alignment axis.
|
|
47
|
+
* @default 0
|
|
48
|
+
*/
|
|
49
|
+
alignOffset?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Whether the listbox should flip / shift to stay inside the viewport.
|
|
52
|
+
* @default true
|
|
53
|
+
*/
|
|
54
|
+
avoidCollisions?: boolean;
|
|
27
55
|
/**
|
|
28
56
|
* Target element or CSS selector to portal the content into.
|
|
29
57
|
* Defaults to `document.body`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/components/combobox/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/components/combobox/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AACrC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAC1D,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,wCAAwC,CAAA;AAEzE,MAAM,WAAW,aAAa;IAC5B,sBAAsB;IACtB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,4DAA4D;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,oCAAoC;IACpC,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,uFAAuF;IACvF,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACtC,sBAAsB;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,uBAAuB;IACvB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,4BAA4B;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAC9C,mBAAmB,EACjB,OAAO,GACP,UAAU,GACV,OAAO,GACP,UAAU,GACV,MAAM,GACN,MAAM,GACN,IAAI,GACJ,eAAe,GACf,eAAe,GACf,uBAAuB,GACvB,mBAAmB,GACnB,cAAc,GACd,SAAS,GACT,SAAS,GACT,WAAW,CACd;IACC,wBAAwB;IACxB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,IAAI,CAAC,EAAE,IAAI,CAAA;IACX;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,KAAK,CAAC,EAAE,KAAK,CAAA;IACb;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,EAAE,WAAW,GAAG,MAAM,CAAA;IAC/B;;;;;;;;;;;;OAYG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,4BAA4B;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { flushSync, mount, unmount } from 'svelte';
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { getLanguages } from '../../../internal/i18n/languages.js';
|
|
4
|
+
import LanguageSelect from '../language-select.svelte';
|
|
5
|
+
describe('language-select', () => {
|
|
6
|
+
let target;
|
|
7
|
+
let instance;
|
|
8
|
+
function setup(props = {}) {
|
|
9
|
+
target = document.createElement('div');
|
|
10
|
+
document.body.appendChild(target);
|
|
11
|
+
instance = mount(LanguageSelect, { target, props });
|
|
12
|
+
flushSync();
|
|
13
|
+
return target;
|
|
14
|
+
}
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (instance)
|
|
17
|
+
unmount(instance);
|
|
18
|
+
target?.remove();
|
|
19
|
+
});
|
|
20
|
+
// ── Shared behavior (variant-agnostic) ────────────────────────────────────
|
|
21
|
+
describe('shared behavior', () => {
|
|
22
|
+
it('renders the wrapper with the sp-language-select class (default variant)', () => {
|
|
23
|
+
setup();
|
|
24
|
+
expect(target.querySelector('.sp-language-select')).toBeTruthy();
|
|
25
|
+
});
|
|
26
|
+
it('renders the label when provided (default variant)', () => {
|
|
27
|
+
setup({ label: 'UI language' });
|
|
28
|
+
expect(target.querySelector('.sp-language-select-label')?.textContent).toBe('UI language');
|
|
29
|
+
});
|
|
30
|
+
it('applies the consumer-supplied class to the wrapper (default variant)', () => {
|
|
31
|
+
setup({ class: 'my-wrapper' });
|
|
32
|
+
expect(target.querySelector('.sp-language-select.my-wrapper')).toBeTruthy();
|
|
33
|
+
});
|
|
34
|
+
it('renders the wrapper correctly in combobox variant', () => {
|
|
35
|
+
setup({ variant: 'combobox' });
|
|
36
|
+
expect(target.querySelector('.sp-language-select')).toBeTruthy();
|
|
37
|
+
});
|
|
38
|
+
it('renders the label in combobox variant', () => {
|
|
39
|
+
setup({ variant: 'combobox', label: 'UI language' });
|
|
40
|
+
expect(target.querySelector('.sp-language-select-label')?.textContent).toBe('UI language');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
// ── Default variant: 'select' ─────────────────────────────────────────────
|
|
44
|
+
describe('variant="select" (default)', () => {
|
|
45
|
+
it('renders a Select trigger (button), not a Combobox input', () => {
|
|
46
|
+
setup();
|
|
47
|
+
// Both Select.Trigger and Combobox.Input use role="combobox" (WAI-ARIA
|
|
48
|
+
// 1.2 pattern). Distinguish by tag: the Select variant uses a <button>.
|
|
49
|
+
expect(target.querySelector('button.sp-select-trigger')).toBeTruthy();
|
|
50
|
+
expect(target.querySelector('input[role="combobox"]')).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
it('trigger has aria-haspopup="listbox"', () => {
|
|
53
|
+
setup();
|
|
54
|
+
const trigger = target.querySelector('.sp-select-trigger');
|
|
55
|
+
expect(trigger?.getAttribute('aria-haspopup')).toBe('listbox');
|
|
56
|
+
});
|
|
57
|
+
it('passes the label as aria-label on the trigger', () => {
|
|
58
|
+
setup({ label: 'UI language' });
|
|
59
|
+
const trigger = target.querySelector('.sp-select-trigger');
|
|
60
|
+
expect(trigger?.getAttribute('aria-label')).toBe('UI language');
|
|
61
|
+
});
|
|
62
|
+
it('shows the native name of the selected language in the trigger', () => {
|
|
63
|
+
setup({ value: 'fr' });
|
|
64
|
+
const trigger = target.querySelector('.sp-select-trigger');
|
|
65
|
+
expect(trigger?.textContent).toContain('Français');
|
|
66
|
+
});
|
|
67
|
+
it('shows the native name for a non-latin selection (ja)', () => {
|
|
68
|
+
setup({ value: 'ja' });
|
|
69
|
+
const trigger = target.querySelector('.sp-select-trigger');
|
|
70
|
+
expect(trigger?.textContent).toContain('日本語');
|
|
71
|
+
});
|
|
72
|
+
it('displays an RTL language (العربية) correctly', () => {
|
|
73
|
+
setup({ value: 'ar' });
|
|
74
|
+
const trigger = target.querySelector('.sp-select-trigger');
|
|
75
|
+
expect(trigger?.textContent).toContain('العربية');
|
|
76
|
+
});
|
|
77
|
+
it('renders the hidden form input via Select.Root when name and value are provided', () => {
|
|
78
|
+
setup({ name: 'locale', value: 'fr' });
|
|
79
|
+
const hidden = target.querySelector('input[type="hidden"][name="locale"]');
|
|
80
|
+
expect(hidden).toBeTruthy();
|
|
81
|
+
expect(hidden?.value).toBe('fr');
|
|
82
|
+
});
|
|
83
|
+
it('disables the trigger when disabled=true', () => {
|
|
84
|
+
setup({ disabled: true });
|
|
85
|
+
const trigger = target.querySelector('.sp-select-trigger');
|
|
86
|
+
expect(trigger?.disabled).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
it('accepts a custom items subset', () => {
|
|
89
|
+
const subset = getLanguages(['fr', 'en', 'de']);
|
|
90
|
+
setup({ items: subset, value: 'fr' });
|
|
91
|
+
const trigger = target.querySelector('.sp-select-trigger');
|
|
92
|
+
expect(trigger?.textContent).toContain('Français');
|
|
93
|
+
});
|
|
94
|
+
it('updates the trigger display and fires onValueChange when an item is clicked', () => {
|
|
95
|
+
const calls = [];
|
|
96
|
+
setup({ value: 'en', onValueChange: (code) => calls.push(code) });
|
|
97
|
+
// Open the Select by clicking the trigger.
|
|
98
|
+
const trigger = target.querySelector('button.sp-select-trigger');
|
|
99
|
+
trigger.click();
|
|
100
|
+
flushSync();
|
|
101
|
+
// Content is portaled to document.body by default.
|
|
102
|
+
const frItem = document.body.querySelector(`.sp-select-item[data-value="fr"]`);
|
|
103
|
+
expect(frItem).toBeTruthy();
|
|
104
|
+
frItem.click();
|
|
105
|
+
flushSync();
|
|
106
|
+
// Trigger display should now reflect the new selection.
|
|
107
|
+
expect(trigger.textContent).toContain('Français');
|
|
108
|
+
// onValueChange should have fired exactly once with the new code.
|
|
109
|
+
expect(calls).toEqual(['fr']);
|
|
110
|
+
// Cleanup: remove portaled content from document.body before next test.
|
|
111
|
+
document.body.querySelector('.sp-select-content')?.remove();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
// ── Opt-in variant: 'combobox' ────────────────────────────────────────────
|
|
115
|
+
describe('variant="combobox"', () => {
|
|
116
|
+
it('renders a Combobox root in closed state by default', () => {
|
|
117
|
+
setup({ variant: 'combobox' });
|
|
118
|
+
const root = target.querySelector('.sp-combobox-root');
|
|
119
|
+
expect(root).toBeTruthy();
|
|
120
|
+
expect(root?.getAttribute('data-state')).toBe('closed');
|
|
121
|
+
});
|
|
122
|
+
it('renders a Combobox input (not a Select trigger)', () => {
|
|
123
|
+
setup({ variant: 'combobox' });
|
|
124
|
+
// Distinguish by tag: the Combobox variant uses an <input>.
|
|
125
|
+
expect(target.querySelector('input[role="combobox"]')).toBeTruthy();
|
|
126
|
+
expect(target.querySelector('button.sp-select-trigger')).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
it('passes the label as aria-label on the input', () => {
|
|
129
|
+
setup({ variant: 'combobox', label: 'UI language' });
|
|
130
|
+
const input = target.querySelector('[role="combobox"]');
|
|
131
|
+
expect(input?.getAttribute('aria-label')).toBe('UI language');
|
|
132
|
+
});
|
|
133
|
+
it('uses the custom placeholder when no language is selected', () => {
|
|
134
|
+
setup({ variant: 'combobox', placeholder: 'Choose...' });
|
|
135
|
+
const input = target.querySelector('[role="combobox"]');
|
|
136
|
+
expect(input?.placeholder).toBe('Choose...');
|
|
137
|
+
});
|
|
138
|
+
it('falls back to a default placeholder when none is provided', () => {
|
|
139
|
+
setup({ variant: 'combobox' });
|
|
140
|
+
const input = target.querySelector('[role="combobox"]');
|
|
141
|
+
expect(input?.placeholder).toBe('Select a language');
|
|
142
|
+
});
|
|
143
|
+
it('shows the native name of the selected language in the input', () => {
|
|
144
|
+
setup({ variant: 'combobox', value: 'fr' });
|
|
145
|
+
const input = target.querySelector('[role="combobox"]');
|
|
146
|
+
expect(input?.value).toBe('Français');
|
|
147
|
+
});
|
|
148
|
+
it('shows the native name for a non-latin selection (ja)', () => {
|
|
149
|
+
setup({ variant: 'combobox', value: 'ja' });
|
|
150
|
+
const input = target.querySelector('[role="combobox"]');
|
|
151
|
+
expect(input?.value).toBe('日本語');
|
|
152
|
+
});
|
|
153
|
+
it('displays an RTL language (العربية) correctly', () => {
|
|
154
|
+
setup({ variant: 'combobox', value: 'ar' });
|
|
155
|
+
const input = target.querySelector('[role="combobox"]');
|
|
156
|
+
expect(input?.value).toBe('العربية');
|
|
157
|
+
});
|
|
158
|
+
it('renders a hidden form input with the BCP 47 code when name and value are provided', () => {
|
|
159
|
+
setup({ variant: 'combobox', name: 'locale', value: 'fr' });
|
|
160
|
+
const hidden = target.querySelector('input[type="hidden"][name="locale"]');
|
|
161
|
+
expect(hidden).toBeTruthy();
|
|
162
|
+
expect(hidden?.value).toBe('fr');
|
|
163
|
+
});
|
|
164
|
+
it('does not render a hidden input when no value is set', () => {
|
|
165
|
+
setup({ variant: 'combobox', name: 'locale' });
|
|
166
|
+
const hidden = target.querySelector('input[type="hidden"][name="locale"]');
|
|
167
|
+
expect(hidden).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
it('passes the disabled flag through to the combobox input', () => {
|
|
170
|
+
setup({ variant: 'combobox', disabled: true });
|
|
171
|
+
const input = target.querySelector('[role="combobox"]');
|
|
172
|
+
expect(input?.disabled).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
it('accepts a custom items subset and silently ignores values outside it', () => {
|
|
175
|
+
const subset = getLanguages(['fr', 'en', 'de']);
|
|
176
|
+
setup({ variant: 'combobox', items: subset, value: 'it' });
|
|
177
|
+
const input = target.querySelector('[role="combobox"]');
|
|
178
|
+
expect(input?.placeholder).toBe('Select a language');
|
|
179
|
+
});
|
|
180
|
+
it('updates the input display and fires onValueChange when an item is clicked', () => {
|
|
181
|
+
const calls = [];
|
|
182
|
+
setup({ variant: 'combobox', value: 'en', onValueChange: (code) => calls.push(code) });
|
|
183
|
+
// Open the listbox by focusing the input.
|
|
184
|
+
const input = target.querySelector('input[role="combobox"]');
|
|
185
|
+
input.dispatchEvent(new FocusEvent('focus', { bubbles: true }));
|
|
186
|
+
flushSync();
|
|
187
|
+
// Combobox.Content portals to document.body by default.
|
|
188
|
+
const frItem = document.body.querySelector(`.sp-combobox-item[data-value^="fr"]`);
|
|
189
|
+
expect(frItem).toBeTruthy();
|
|
190
|
+
frItem.click();
|
|
191
|
+
flushSync();
|
|
192
|
+
// After selection the input should reflect the new native name.
|
|
193
|
+
expect(input.value).toBe('Français');
|
|
194
|
+
// onValueChange should have fired exactly once with the new BCP 47 code.
|
|
195
|
+
expect(calls).toEqual(['fr']);
|
|
196
|
+
document.body.querySelector('.sp-combobox-content')?.remove();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { getLanguage, getLanguages, LANGUAGES, } from '../../../internal/i18n/languages.js';
|
|
3
|
+
describe('i18n/languages', () => {
|
|
4
|
+
describe('languages list', () => {
|
|
5
|
+
it('contains the 16 curated languages', () => {
|
|
6
|
+
expect(LANGUAGES).toHaveLength(16);
|
|
7
|
+
});
|
|
8
|
+
it('exposes valid BCP 47 codes', () => {
|
|
9
|
+
const codes = LANGUAGES.map(l => l.code);
|
|
10
|
+
expect(codes).toEqual([
|
|
11
|
+
'en',
|
|
12
|
+
'fr',
|
|
13
|
+
'de',
|
|
14
|
+
'it',
|
|
15
|
+
'id',
|
|
16
|
+
'ja',
|
|
17
|
+
'ko',
|
|
18
|
+
'pt',
|
|
19
|
+
'es',
|
|
20
|
+
'zh-Hans',
|
|
21
|
+
'hi',
|
|
22
|
+
'ar',
|
|
23
|
+
'bn',
|
|
24
|
+
'ru',
|
|
25
|
+
'ur',
|
|
26
|
+
'tr',
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
29
|
+
it('marks ar and ur as rtl and the rest as ltr', () => {
|
|
30
|
+
const rtl = LANGUAGES.filter(l => l.dir === 'rtl').map(l => l.code);
|
|
31
|
+
expect(rtl).toEqual(['ar', 'ur']);
|
|
32
|
+
const ltr = LANGUAGES.filter(l => l.dir === 'ltr').map(l => l.code);
|
|
33
|
+
expect(ltr).toHaveLength(14);
|
|
34
|
+
});
|
|
35
|
+
it('has a non-empty nativeName and englishName for each entry', () => {
|
|
36
|
+
for (const lang of LANGUAGES) {
|
|
37
|
+
expect(lang.nativeName.length).toBeGreaterThan(0);
|
|
38
|
+
expect(lang.englishName.length).toBeGreaterThan(0);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
it('is frozen (cannot be mutated)', () => {
|
|
42
|
+
expect(Object.isFrozen(LANGUAGES)).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
it('has no duplicate codes', () => {
|
|
45
|
+
const codes = LANGUAGES.map(l => l.code);
|
|
46
|
+
expect(new Set(codes).size).toBe(codes.length);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('getLanguages', () => {
|
|
50
|
+
it('returns the requested subset in the requested order', () => {
|
|
51
|
+
const result = getLanguages(['fr', 'en', 'de']);
|
|
52
|
+
expect(result.map(l => l.code)).toEqual(['fr', 'en', 'de']);
|
|
53
|
+
});
|
|
54
|
+
it('preserves user-specified order even when reversed vs LANGUAGES', () => {
|
|
55
|
+
const result = getLanguages(['de', 'en', 'fr']);
|
|
56
|
+
expect(result.map(l => l.code)).toEqual(['de', 'en', 'fr']);
|
|
57
|
+
});
|
|
58
|
+
it('silently drops unknown codes', () => {
|
|
59
|
+
const result = getLanguages(['fr', 'xx', 'en']);
|
|
60
|
+
expect(result.map(l => l.code)).toEqual(['fr', 'en']);
|
|
61
|
+
});
|
|
62
|
+
it('returns [] for an empty input', () => {
|
|
63
|
+
expect(getLanguages([])).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
it('returns full LanguageOption objects (not just codes)', () => {
|
|
66
|
+
const [fr] = getLanguages(['fr']);
|
|
67
|
+
expect(fr).toMatchObject({
|
|
68
|
+
code: 'fr',
|
|
69
|
+
nativeName: 'Français',
|
|
70
|
+
englishName: 'French',
|
|
71
|
+
dir: 'ltr',
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe('getLanguage', () => {
|
|
76
|
+
it('returns the language matching the given code', () => {
|
|
77
|
+
expect(getLanguage('ja')?.nativeName).toBe('日本語');
|
|
78
|
+
});
|
|
79
|
+
it('returns undefined for unknown codes', () => {
|
|
80
|
+
expect(getLanguage('xx')).toBeUndefined();
|
|
81
|
+
});
|
|
82
|
+
it('finds RTL languages correctly', () => {
|
|
83
|
+
expect(getLanguage('ar')?.dir).toBe('rtl');
|
|
84
|
+
expect(getLanguage('ur')?.dir).toBe('rtl');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/components/language-select/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,0BAA0B,CAAA;AACpE,YAAY,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as LanguageSelect } from './language-select.svelte';
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
<script lang='ts'>
|
|
2
|
+
import type { LanguageOption } from '../../internal/i18n/languages.js'
|
|
3
|
+
import type { LanguageSelectProps } from './types.js'
|
|
4
|
+
import { LANGUAGES } from '../../internal/i18n/languages.js'
|
|
5
|
+
import * as Combobox from '../combobox/index.js'
|
|
6
|
+
import * as Select from '../select/index.js'
|
|
7
|
+
|
|
8
|
+
let {
|
|
9
|
+
variant = 'select',
|
|
10
|
+
items = LANGUAGES,
|
|
11
|
+
value = $bindable<string | undefined>(undefined),
|
|
12
|
+
onValueChange,
|
|
13
|
+
label,
|
|
14
|
+
placeholder,
|
|
15
|
+
disabled = false,
|
|
16
|
+
name,
|
|
17
|
+
side,
|
|
18
|
+
sideOffset,
|
|
19
|
+
align,
|
|
20
|
+
class: className,
|
|
21
|
+
}: LanguageSelectProps = $props()
|
|
22
|
+
|
|
23
|
+
// ── Shared helpers ────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function nativeNameFor(code: string | undefined): string {
|
|
26
|
+
if (!code)
|
|
27
|
+
return ''
|
|
28
|
+
return items.find(l => l.code === code)?.nativeName ?? ''
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Combobox variant — composite value trick ─────────────────────────────
|
|
32
|
+
//
|
|
33
|
+
// `Combobox` built-in filter matches `item.value.includes(search)`, so we
|
|
34
|
+
// encode each option as `code␟native␟english` (U+241F is a reserved
|
|
35
|
+
// information separator never present in user-entered text) to let the
|
|
36
|
+
// filter match native name, English name, or BCP 47 code. The wrapper
|
|
37
|
+
// exposes only the BCP 47 code externally.
|
|
38
|
+
|
|
39
|
+
const SEP = '␟'
|
|
40
|
+
|
|
41
|
+
function toComposite(item: LanguageOption): string {
|
|
42
|
+
return `${item.code}${SEP}${item.nativeName.toLowerCase()}${SEP}${item.englishName.toLowerCase()}`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function codeOf(composite: string): string {
|
|
46
|
+
const [code = ''] = composite.split(SEP)
|
|
47
|
+
return code
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function compositeFor(code: string | undefined): string {
|
|
51
|
+
if (!code)
|
|
52
|
+
return ''
|
|
53
|
+
const item = items.find(l => l.code === code)
|
|
54
|
+
return item ? toComposite(item) : ''
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Sync plumbing ───────────────────────────────────────────────────────
|
|
58
|
+
//
|
|
59
|
+
// `lastExternalCode` tracks the most recent value that either flowed in
|
|
60
|
+
// from outside (consumer `bind:value` write) or out to outside (user
|
|
61
|
+
// interaction). It acts as a tiebreaker so the two sync effects don't
|
|
62
|
+
// overwrite each other: the "external → internal" effect only fires when
|
|
63
|
+
// `value` drifts from the last known external code, and the "internal →
|
|
64
|
+
// external" effect only fires when the internal state drifts from it.
|
|
65
|
+
|
|
66
|
+
let lastExternalCode: string | undefined = value
|
|
67
|
+
|
|
68
|
+
let selectValue = $state<string>(value ?? '')
|
|
69
|
+
let compositeValue = $state<string>(compositeFor(value))
|
|
70
|
+
let searchQuery = $state<string>(nativeNameFor(value))
|
|
71
|
+
|
|
72
|
+
// External `value` → internal state, only when value genuinely changed
|
|
73
|
+
// from outside (i.e. differs from the last code we synced).
|
|
74
|
+
$effect(() => {
|
|
75
|
+
if (value === lastExternalCode)
|
|
76
|
+
return
|
|
77
|
+
lastExternalCode = value
|
|
78
|
+
if (variant === 'select') {
|
|
79
|
+
const v = value ?? ''
|
|
80
|
+
if (selectValue !== v)
|
|
81
|
+
selectValue = v
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
const expected = compositeFor(value)
|
|
85
|
+
if (compositeValue !== expected)
|
|
86
|
+
compositeValue = expected
|
|
87
|
+
searchQuery = nativeNameFor(value)
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
// Internal state → external `value`, only when user interaction moved it.
|
|
92
|
+
$effect(() => {
|
|
93
|
+
if (variant !== 'select')
|
|
94
|
+
return
|
|
95
|
+
const code: string | undefined = selectValue || undefined
|
|
96
|
+
if (code !== lastExternalCode) {
|
|
97
|
+
lastExternalCode = code
|
|
98
|
+
value = code
|
|
99
|
+
if (code !== undefined)
|
|
100
|
+
onValueChange?.(code)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
$effect(() => {
|
|
105
|
+
if (variant !== 'combobox')
|
|
106
|
+
return
|
|
107
|
+
const code: string | undefined = codeOf(compositeValue) || undefined
|
|
108
|
+
if (code !== lastExternalCode) {
|
|
109
|
+
lastExternalCode = code
|
|
110
|
+
value = code
|
|
111
|
+
if (code !== undefined)
|
|
112
|
+
onValueChange?.(code)
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
function handleComboboxOpenChange(open: boolean): void {
|
|
117
|
+
// Combobox clears `search` on selection; restore native name on close so
|
|
118
|
+
// the input reflects the current selection even though search is empty.
|
|
119
|
+
if (open) {
|
|
120
|
+
searchQuery = ''
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const code = codeOf(compositeValue)
|
|
124
|
+
searchQuery = nativeNameFor(code)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
</script>
|
|
128
|
+
|
|
129
|
+
<div class="sp-language-select {className ?? ''}">
|
|
130
|
+
{#if label}
|
|
131
|
+
<span class='sp-language-select-label'>{label}</span>
|
|
132
|
+
{/if}
|
|
133
|
+
|
|
134
|
+
{#if variant === 'select'}
|
|
135
|
+
<Select.Root bind:value={selectValue} {disabled} {name}>
|
|
136
|
+
<Select.Trigger
|
|
137
|
+
placeholder={placeholder || 'Select a language'}
|
|
138
|
+
aria-label={label}
|
|
139
|
+
>
|
|
140
|
+
{#if value && nativeNameFor(value)}
|
|
141
|
+
<span class='sp-language-select-native sp-select-value' dir='auto'>{nativeNameFor(value)}</span>
|
|
142
|
+
{:else}
|
|
143
|
+
<span class='sp-select-value placeholder'>{placeholder || 'Select a language'}</span>
|
|
144
|
+
{/if}
|
|
145
|
+
</Select.Trigger>
|
|
146
|
+
<Select.Content {side} {sideOffset} {align}>
|
|
147
|
+
{#each items as item (item.code)}
|
|
148
|
+
<Select.Item value={item.code} label={item.nativeName}>
|
|
149
|
+
<span class='sp-language-select-native' dir='auto'>{item.nativeName}</span>
|
|
150
|
+
</Select.Item>
|
|
151
|
+
{/each}
|
|
152
|
+
</Select.Content>
|
|
153
|
+
</Select.Root>
|
|
154
|
+
{:else}
|
|
155
|
+
<Combobox.Root
|
|
156
|
+
bind:value={compositeValue}
|
|
157
|
+
bind:search={searchQuery}
|
|
158
|
+
{disabled}
|
|
159
|
+
onOpenChange={handleComboboxOpenChange}
|
|
160
|
+
>
|
|
161
|
+
<Combobox.Input
|
|
162
|
+
placeholder={placeholder || nativeNameFor(value) || 'Select a language'}
|
|
163
|
+
aria-label={label}
|
|
164
|
+
/>
|
|
165
|
+
<Combobox.Content {side} {sideOffset} {align}>
|
|
166
|
+
{#each items as item (item.code)}
|
|
167
|
+
<Combobox.Item value={toComposite(item)}>
|
|
168
|
+
<span class='sp-language-select-native' dir='auto'>{item.nativeName}</span>
|
|
169
|
+
</Combobox.Item>
|
|
170
|
+
{/each}
|
|
171
|
+
</Combobox.Content>
|
|
172
|
+
</Combobox.Root>
|
|
173
|
+
|
|
174
|
+
{#if name && value}
|
|
175
|
+
<input type='hidden' {name} {value} />
|
|
176
|
+
{/if}
|
|
177
|
+
{/if}
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<style>
|
|
181
|
+
.sp-language-select {
|
|
182
|
+
display: inline-flex;
|
|
183
|
+
flex-direction: column;
|
|
184
|
+
gap: var(--sp-sys-spacing-xs, 0.25rem);
|
|
185
|
+
min-width: 200px;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.sp-language-select-label {
|
|
189
|
+
font-size: var(--sp-sys-typescale-label-medium-size, 0.75rem);
|
|
190
|
+
line-height: 1.4;
|
|
191
|
+
color: var(--sp-sys-color-on-surface-variant, inherit);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* Bidirectional isolation for RTL native names displayed among LTR ones
|
|
195
|
+
(and vice versa). `dir="auto"` on the span triggers UA bidi resolution
|
|
196
|
+
based on the first strong character. */
|
|
197
|
+
.sp-language-select-native {
|
|
198
|
+
unicode-bidi: isolate;
|
|
199
|
+
}
|
|
200
|
+
</style>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LanguageSelectProps } from './types.js';
|
|
2
|
+
declare const LanguageSelect: import("svelte").Component<LanguageSelectProps, {}, "value">;
|
|
3
|
+
type LanguageSelect = ReturnType<typeof LanguageSelect>;
|
|
4
|
+
export default LanguageSelect;
|
|
5
|
+
//# sourceMappingURL=language-select.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"language-select.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/language-select/language-select.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAiLrD,QAAA,MAAM,cAAc,8DAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Align, Side } from '../../internal/floating-layer/types.js';
|
|
2
|
+
import type { LanguageOption } from '../../internal/i18n/languages.js';
|
|
3
|
+
export type LanguageSelectVariant = 'select' | 'combobox';
|
|
4
|
+
export interface LanguageSelectProps {
|
|
5
|
+
/**
|
|
6
|
+
* UI variant.
|
|
7
|
+
* - `'select'` (default) — classic dropdown: trigger button + listbox,
|
|
8
|
+
* keyboard typeahead on the native name's first letter. Best for short
|
|
9
|
+
* lists (2–6 languages) or when a compact control is preferred.
|
|
10
|
+
* - `'combobox'` — searchable input + filterable listbox. Search matches
|
|
11
|
+
* against the native name, English name, or BCP 47 code. Best for the
|
|
12
|
+
* full curated list or whenever keyword filtering is desirable.
|
|
13
|
+
* @default 'select'
|
|
14
|
+
*/
|
|
15
|
+
variant?: LanguageSelectVariant;
|
|
16
|
+
/**
|
|
17
|
+
* Subset of languages to show. Defaults to the full curated list.
|
|
18
|
+
* Use {@link getLanguages} to build a subset from BCP 47 codes.
|
|
19
|
+
*/
|
|
20
|
+
items?: readonly LanguageOption[];
|
|
21
|
+
/** Currently selected BCP 47 code. */
|
|
22
|
+
value?: string;
|
|
23
|
+
/** Called whenever the selected code changes. */
|
|
24
|
+
onValueChange?: (code: string) => void;
|
|
25
|
+
/** Optional visible label rendered above the control. */
|
|
26
|
+
label?: string;
|
|
27
|
+
/** Placeholder text when no language is selected. */
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
/** Disable the control. */
|
|
30
|
+
disabled?: boolean;
|
|
31
|
+
/** Name of the hidden form field (for native form submission). */
|
|
32
|
+
name?: string;
|
|
33
|
+
/** Preferred side of the trigger to render the listbox on. @default 'bottom' */
|
|
34
|
+
side?: Side;
|
|
35
|
+
/** Pixel distance between trigger and listbox. @default 4 */
|
|
36
|
+
sideOffset?: number;
|
|
37
|
+
/** Alignment along the perpendicular axis of `side`. @default 'start' */
|
|
38
|
+
align?: Align;
|
|
39
|
+
/** Extra CSS class for the wrapper. */
|
|
40
|
+
class?: string;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/components/language-select/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,wCAAwC,CAAA;AACzE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAA;AAEtE,MAAM,MAAM,qBAAqB,GAAG,QAAQ,GAAG,UAAU,CAAA;AAEzD,MAAM,WAAW,mBAAmB;IAClC;;;;;;;;;OASG;IACH,OAAO,CAAC,EAAE,qBAAqB,CAAA;IAC/B;;;OAGG;IACH,KAAK,CAAC,EAAE,SAAS,cAAc,EAAE,CAAA;IACjC,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iDAAiD;IACjD,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,2BAA2B;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,gFAAgF;IAChF,IAAI,CAAC,EAAE,IAAI,CAAA;IACX,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,yEAAyE;IACzE,KAAK,CAAC,EAAE,KAAK,CAAA;IACb,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAA;CACf"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export { Input } from './components/input/index.js';
|
|
|
44
44
|
export type { InputProps, InputSize } from './components/input/index.js';
|
|
45
45
|
export { Label } from './components/label/index.js';
|
|
46
46
|
export type { LabelProps } from './components/label/index.js';
|
|
47
|
+
export { LanguageSelect } from './components/language-select/index.js';
|
|
48
|
+
export type { LanguageSelectProps } from './components/language-select/index.js';
|
|
47
49
|
export * as LinkPreview from './components/link-preview/index.js';
|
|
48
50
|
export * as List from './components/list/index.js';
|
|
49
51
|
export * as Menubar from './components/menubar/index.js';
|
|
@@ -103,5 +105,7 @@ export * as Tree from './components/tree/index.js';
|
|
|
103
105
|
export * as Virtualizer from './components/virtualizer/index.js';
|
|
104
106
|
export type { DateRange, DateValue } from './internal/date-time/types.js';
|
|
105
107
|
export type { DropItem, DropOperation, DropPosition, DropTarget } from './internal/drag-drop/index.js';
|
|
108
|
+
export { getLanguage, getLanguages, LANGUAGES } from './internal/i18n/index.js';
|
|
109
|
+
export type { LanguageOption } from './internal/i18n/index.js';
|
|
106
110
|
export { CalendarDate, CalendarDateTime, getLocalTimeZone, today, ZonedDateTime } from '@internationalized/date';
|
|
107
111
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -35,6 +35,7 @@ export * as HoverCard from './components/hover-card/index.js';
|
|
|
35
35
|
export { Indicator } from './components/indicator/index.js';
|
|
36
36
|
export { Input } from './components/input/index.js';
|
|
37
37
|
export { Label } from './components/label/index.js';
|
|
38
|
+
export { LanguageSelect } from './components/language-select/index.js';
|
|
38
39
|
export * as LinkPreview from './components/link-preview/index.js';
|
|
39
40
|
export * as List from './components/list/index.js';
|
|
40
41
|
export * as Menubar from './components/menubar/index.js';
|
|
@@ -75,4 +76,6 @@ export { Toolbar, ToolbarGroup, ToolbarSeparator } from './components/toolbar/in
|
|
|
75
76
|
export * as Tooltip from './components/tooltip/index.js';
|
|
76
77
|
export * as Tree from './components/tree/index.js';
|
|
77
78
|
export * as Virtualizer from './components/virtualizer/index.js';
|
|
79
|
+
// i18n / locale utilities (re-exported for consumer convenience)
|
|
80
|
+
export { getLanguage, getLanguages, LANGUAGES } from './internal/i18n/index.js';
|
|
78
81
|
export { CalendarDate, CalendarDateTime, getLocalTimeZone, today, ZonedDateTime } from '@internationalized/date';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/internal/i18n/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AACrE,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getLanguage, getLanguages, LANGUAGES } from './languages.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated list of UI languages for Spectral UI locale pickers.
|
|
3
|
+
*
|
|
4
|
+
* Codes follow BCP 47. `nativeName` is the endonym (name in the language
|
|
5
|
+
* itself). `englishName` is used for keyword search and localization.
|
|
6
|
+
* `dir` is 'rtl' only for right-to-left scripts — consumers should apply
|
|
7
|
+
* bidi isolation (e.g. `dir="auto"`) when displaying these names.
|
|
8
|
+
*/
|
|
9
|
+
export interface LanguageOption {
|
|
10
|
+
/** BCP 47 language tag (e.g. `fr`, `zh-Hans`, `pt`). */
|
|
11
|
+
code: string;
|
|
12
|
+
/** Endonym — name of the language in the language itself. */
|
|
13
|
+
nativeName: string;
|
|
14
|
+
/** Name in English — used for keyword search. */
|
|
15
|
+
englishName: string;
|
|
16
|
+
/** Script direction. */
|
|
17
|
+
dir: 'ltr' | 'rtl';
|
|
18
|
+
}
|
|
19
|
+
export declare const LANGUAGES: readonly LanguageOption[];
|
|
20
|
+
/**
|
|
21
|
+
* Return a subset of {@link LANGUAGES} matching the given codes, preserving
|
|
22
|
+
* the order of the `codes` array. Unknown codes are silently dropped.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* getLanguages(['fr', 'en', 'de'])
|
|
26
|
+
* // → [{ code: 'fr', ... }, { code: 'en', ... }, { code: 'de', ... }]
|
|
27
|
+
*/
|
|
28
|
+
export declare function getLanguages(codes: readonly string[]): LanguageOption[];
|
|
29
|
+
/**
|
|
30
|
+
* Look up a single language option by BCP 47 code.
|
|
31
|
+
*/
|
|
32
|
+
export declare function getLanguage(code: string): LanguageOption | undefined;
|
|
33
|
+
//# sourceMappingURL=languages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"languages.d.ts","sourceRoot":"","sources":["../../../src/lib/internal/i18n/languages.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,cAAc;IAC7B,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAA;IACZ,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAA;IAClB,iDAAiD;IACjD,WAAW,EAAE,MAAM,CAAA;IACnB,wBAAwB;IACxB,GAAG,EAAE,KAAK,GAAG,KAAK,CAAA;CACnB;AAED,eAAO,MAAM,SAAS,EAAE,SAAS,cAAc,EAiB7C,CAAA;AAEF;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,cAAc,EAAE,CASvE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAEpE"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export const LANGUAGES = Object.freeze([
|
|
2
|
+
{ code: 'en', nativeName: 'English', englishName: 'English', dir: 'ltr' },
|
|
3
|
+
{ code: 'fr', nativeName: 'Français', englishName: 'French', dir: 'ltr' },
|
|
4
|
+
{ code: 'de', nativeName: 'Deutsch', englishName: 'German', dir: 'ltr' },
|
|
5
|
+
{ code: 'it', nativeName: 'Italiano', englishName: 'Italian', dir: 'ltr' },
|
|
6
|
+
{ code: 'id', nativeName: 'Indonesia', englishName: 'Indonesian', dir: 'ltr' },
|
|
7
|
+
{ code: 'ja', nativeName: '日本語', englishName: 'Japanese', dir: 'ltr' },
|
|
8
|
+
{ code: 'ko', nativeName: '한국어', englishName: 'Korean', dir: 'ltr' },
|
|
9
|
+
{ code: 'pt', nativeName: 'Português', englishName: 'Portuguese', dir: 'ltr' },
|
|
10
|
+
{ code: 'es', nativeName: 'Español', englishName: 'Spanish', dir: 'ltr' },
|
|
11
|
+
{ code: 'zh-Hans', nativeName: '简体中文', englishName: 'Chinese (Simplified)', dir: 'ltr' },
|
|
12
|
+
{ code: 'hi', nativeName: 'हिन्दी', englishName: 'Hindi', dir: 'ltr' },
|
|
13
|
+
{ code: 'ar', nativeName: 'العربية', englishName: 'Arabic', dir: 'rtl' },
|
|
14
|
+
{ code: 'bn', nativeName: 'বাংলা', englishName: 'Bengali', dir: 'ltr' },
|
|
15
|
+
{ code: 'ru', nativeName: 'Русский', englishName: 'Russian', dir: 'ltr' },
|
|
16
|
+
{ code: 'ur', nativeName: 'اردو', englishName: 'Urdu', dir: 'rtl' },
|
|
17
|
+
{ code: 'tr', nativeName: 'Türkçe', englishName: 'Turkish', dir: 'ltr' },
|
|
18
|
+
]);
|
|
19
|
+
/**
|
|
20
|
+
* Return a subset of {@link LANGUAGES} matching the given codes, preserving
|
|
21
|
+
* the order of the `codes` array. Unknown codes are silently dropped.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* getLanguages(['fr', 'en', 'de'])
|
|
25
|
+
* // → [{ code: 'fr', ... }, { code: 'en', ... }, { code: 'de', ... }]
|
|
26
|
+
*/
|
|
27
|
+
export function getLanguages(codes) {
|
|
28
|
+
const byCode = new Map(LANGUAGES.map(lang => [lang.code, lang]));
|
|
29
|
+
const result = [];
|
|
30
|
+
for (const code of codes) {
|
|
31
|
+
const lang = byCode.get(code);
|
|
32
|
+
if (lang)
|
|
33
|
+
result.push(lang);
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Look up a single language option by BCP 47 code.
|
|
39
|
+
*/
|
|
40
|
+
export function getLanguage(code) {
|
|
41
|
+
return LANGUAGES.find(lang => lang.code === code);
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spectral_labs/ui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.1.1",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"registry": "https://registry.npmjs.org",
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
5
9
|
"description": "Spectral UI — A styled and themeable design system for Svelte 5",
|
|
6
10
|
"license": "MIT",
|
|
7
11
|
"repository": {
|