@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.
Files changed (30) hide show
  1. package/dist/components/app-bar/app-bar.svelte +19 -0
  2. package/dist/components/combobox/__tests__/ComboboxTest.svelte +8 -1
  3. package/dist/components/combobox/__tests__/combobox.test.svelte.js +59 -1
  4. package/dist/components/combobox/components/combobox-content.svelte +36 -5
  5. package/dist/components/combobox/components/combobox-content.svelte.d.ts.map +1 -1
  6. package/dist/components/combobox/index.d.ts +1 -1
  7. package/dist/components/combobox/index.d.ts.map +1 -1
  8. package/dist/components/combobox/types.d.ts +28 -0
  9. package/dist/components/combobox/types.d.ts.map +1 -1
  10. package/dist/components/language-select/__tests__/language-select.test.svelte.js +199 -0
  11. package/dist/components/language-select/__tests__/languages.test.js +87 -0
  12. package/dist/components/language-select/index.d.ts +3 -0
  13. package/dist/components/language-select/index.d.ts.map +1 -0
  14. package/dist/components/language-select/index.js +1 -0
  15. package/dist/components/language-select/language-select.svelte +200 -0
  16. package/dist/components/language-select/language-select.svelte.d.ts +5 -0
  17. package/dist/components/language-select/language-select.svelte.d.ts.map +1 -0
  18. package/dist/components/language-select/types.d.ts +42 -0
  19. package/dist/components/language-select/types.d.ts.map +1 -0
  20. package/dist/components/language-select/types.js +1 -0
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3 -0
  24. package/dist/internal/i18n/index.d.ts +3 -0
  25. package/dist/internal/i18n/index.d.ts.map +1 -0
  26. package/dist/internal/i18n/index.js +1 -0
  27. package/dist/internal/i18n/languages.d.ts +33 -0
  28. package/dist/internal/i18n/languages.d.ts.map +1 -0
  29. package/dist/internal/i18n/languages.js +42 -0
  30. 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 { portalTo, disablePortal, class: className, children }: ComboboxContentProps = $props()
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
- margin-top: 4px;
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;AAyEvD,QAAA,MAAM,eAAe,0DAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
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;AAE1D,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;;;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"}
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,3 @@
1
+ export { default as LanguageSelect } from './language-select.svelte';
2
+ export type { LanguageSelectProps } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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
@@ -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,3 @@
1
+ export { getLanguage, getLanguages, LANGUAGES } from './languages.js';
2
+ export type { LanguageOption } from './languages.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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.0.2",
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": {