@usequeek/theme-kit 0.1.22 → 0.1.23
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/README.md +111 -5
- package/components/language-menu.css +349 -0
- package/components/language-menu.tsx +371 -0
- package/components/language-switcher.tsx +39 -8
- package/package.json +1 -1
- package/tests/fixtures/external-theme.tsx +7 -2
- package/tests/language-menu.test.tsx +598 -0
- package/tests/menu-placement.test.ts +63 -0
- package/utils/menu-placement.ts +91 -0
package/README.md
CHANGED
|
@@ -346,8 +346,10 @@ keeps the page, the query string and the hash.
|
|
|
346
346
|
```tsx
|
|
347
347
|
import { LanguageSwitcher } from '@usequeek/theme-kit/components/language-switcher';
|
|
348
348
|
|
|
349
|
-
<LanguageSwitcher />
|
|
350
|
-
<LanguageSwitcher variant="
|
|
349
|
+
<LanguageSwitcher variant="menu" /> // button + popover (sheet on phones) — recommended
|
|
350
|
+
<LanguageSwitcher variant="menu" side="top" /> // a footer: opens upward
|
|
351
|
+
<LanguageSwitcher variant="inline-list" /> // every language as a link (footer, up to four)
|
|
352
|
+
<LanguageSwitcher /> // native <select> — legacy
|
|
351
353
|
```
|
|
352
354
|
|
|
353
355
|
Place it unconditionally, in the header or the footer: it renders nothing on a
|
|
@@ -357,10 +359,114 @@ store's languages.
|
|
|
357
359
|
| Prop | Default | |
|
|
358
360
|
| --- | --- | --- |
|
|
359
361
|
| `className` | | Extra class on the root, to position or restyle it. |
|
|
360
|
-
| `variant` | `'select'` | `'
|
|
362
|
+
| `variant` | `'select'` | `'menu'`: a trigger button and a list of language links (below). `'inline-list'`: a link per language that wraps on narrow screens. `'select'`: one native dropdown, **legacy** — it works and is unchanged, but its open list is the operating system's menu and cannot be styled; prefer `menu`. |
|
|
363
|
+
| `side` | `'bottom'` | `menu` only. `'top'` opens above the trigger, for footers. Flips by itself when the preferred side does not fit. |
|
|
364
|
+
| `align` | `'end'` | `menu` only. Line the popover up with the trigger's inline `'end'` or `'start'` edge (mirrors in right-to-left). Flips and shifts to stay inside the viewport. |
|
|
361
365
|
| `showNativeName` | `true` | Label languages in their own language (`Français`) instead of English (`French`). |
|
|
362
|
-
| `showCode` | `false` |
|
|
363
|
-
| `children` | | Render-prop `(state) => ReactNode`: replaces the default control entirely (never called on a single-language store). `state` is `useLocales()` plus `label`, `labelFor(locale)
|
|
366
|
+
| `showCode` | `false` | Show the language code (`FR`) next to the name. |
|
|
367
|
+
| `children` | | Render-prop `(state) => ReactNode`: replaces the default control entirely (never called on a single-language store). `state` is `useLocales()` plus `label`, `labelFor(locale)`, `displayName(locale)`, `open` and `setOpen`. |
|
|
368
|
+
|
|
369
|
+
### The `menu` variant
|
|
370
|
+
|
|
371
|
+
A real `<button>` opens a list of language **links** (`<a href>` built from the
|
|
372
|
+
current page, so open-in-new-tab and copy-link work). On screens wider than
|
|
373
|
+
640px it is a popover under the trigger; below 640px it becomes a bottom sheet
|
|
374
|
+
with a scrim, safe-area padding, and page scroll locked while it is open. The
|
|
375
|
+
wide/narrow layout switch is CSS only (one media query), so the markup is the
|
|
376
|
+
same on both and server and client agree; JavaScript only consults the media
|
|
377
|
+
query while the menu opens, to lock scroll and to place the popover.
|
|
378
|
+
With more than four languages in a footer, use `menu` with `side="top"` rather
|
|
379
|
+
than `inline-list`. 2 to 20 languages are fine: the list scrolls past about
|
|
380
|
+
eight rows, and a long list can use two columns on wide screens (below).
|
|
381
|
+
|
|
382
|
+
It is the *disclosure* pattern, not a listbox, because choosing an item
|
|
383
|
+
navigates: the button has `aria-expanded` and `aria-controls` and the name
|
|
384
|
+
"Language: English"; the list is a `<ul>` labelled by its title; the current
|
|
385
|
+
language's link has `aria-current="true"`; there are no `option` roles.
|
|
386
|
+
|
|
387
|
+
| Key | |
|
|
388
|
+
| --- | --- |
|
|
389
|
+
| Enter, Space, ArrowDown on the button | Open, focus on the current language |
|
|
390
|
+
| ArrowUp on the button | Open, focus on the last language |
|
|
391
|
+
| ArrowDown / ArrowUp in the list | Next / previous link (wraps) |
|
|
392
|
+
| Home / End | First / last link |
|
|
393
|
+
| Typing letters | Jump to the language whose name or code starts with them (500 ms buffer) |
|
|
394
|
+
| Enter on a link | Follow it (native); Space does the same |
|
|
395
|
+
| Escape | Close and return focus to the button |
|
|
396
|
+
| Tab | Close the menu |
|
|
397
|
+
| Click outside, scrim tap | Close |
|
|
398
|
+
|
|
399
|
+
Only the focused link is in the tab order (roving tabindex), so Tab leaves the
|
|
400
|
+
list and closes it. Choosing a language closes the menu, returns focus to the
|
|
401
|
+
button and navigates (a modified click — new tab, copy link — is left to the
|
|
402
|
+
browser). Motion respects `prefers-reduced-motion` (an 80 ms fade only).
|
|
403
|
+
|
|
404
|
+
**Styling surface.** Every part has a class and a `data-part` attribute; the
|
|
405
|
+
root also carries state attributes:
|
|
406
|
+
|
|
407
|
+
| Part | Class | `data-part` |
|
|
408
|
+
| --- | --- | --- |
|
|
409
|
+
| Root | `qn-lang qn-lang--menu` | `data-qn-language-switcher`, `data-variant="menu"` |
|
|
410
|
+
| Trigger button | `qn-lang__trigger` | `trigger` |
|
|
411
|
+
| Trigger name / code / chevron | `qn-lang__trigger-label` / `__trigger-code` / `__chevron` | `trigger-label` / `trigger-code` / `chevron` |
|
|
412
|
+
| Scrim (sheet) | `qn-lang__scrim` | `scrim` |
|
|
413
|
+
| Popover / sheet | `qn-lang__popover` | `popover` |
|
|
414
|
+
| Sheet grabber | `qn-lang__handle` | `handle` |
|
|
415
|
+
| Group label | `qn-lang__title` | `title` |
|
|
416
|
+
| List / item | `qn-lang__options` / `__item` | `list` / `item` |
|
|
417
|
+
| Link (current: `aria-current="true"`, `data-current="true"`) | `qn-lang__option` | `option` |
|
|
418
|
+
| Name / code / current marker | `qn-lang__name` / `__code` / `__mark` | `name` / `code` / `mark` |
|
|
419
|
+
|
|
420
|
+
Root attributes: `data-state="open|closed"`, `data-side="bottom|top"`,
|
|
421
|
+
`data-align="start|end"`, `data-mode="popover|sheet"` (while open),
|
|
422
|
+
`data-count="<n>"`, `data-many="true"` (more than ten languages),
|
|
423
|
+
`data-show-code="true"`. The code spans are always rendered and hidden by
|
|
424
|
+
default, so a theme can show a code-only trigger with
|
|
425
|
+
`.my-header [data-part='trigger-label'] { display: none }` plus
|
|
426
|
+
`.my-header [data-part='trigger-code'] { display: inline }`.
|
|
427
|
+
|
|
428
|
+
| Variable | Default | Controls |
|
|
429
|
+
| --- | --- | --- |
|
|
430
|
+
| `--qn-lang-surface` | `--qn-surface` | popover / sheet background |
|
|
431
|
+
| `--qn-lang-ink` | `--qn-text` | text colour |
|
|
432
|
+
| `--qn-lang-muted` | `--qn-text-muted` | title and code colour |
|
|
433
|
+
| `--qn-lang-border` | `--qn-border` | hairlines |
|
|
434
|
+
| `--qn-lang-radius` | `--qn-radius-md` (8px) | trigger / popover corners |
|
|
435
|
+
| `--qn-lang-item-radius` | `--qn-radius-sm` (6px) | link corners |
|
|
436
|
+
| `--qn-lang-item-hover` / `--qn-lang-current-bg` | 6% ink / none | hover and current-link backgrounds |
|
|
437
|
+
| `--qn-lang-accent` | `--qn-accent` | current marker colour |
|
|
438
|
+
| `--qn-lang-focus` | `--qn-focus` | focus ring |
|
|
439
|
+
| `--qn-lang-shadow` | subtle | popover shadow |
|
|
440
|
+
| `--qn-lang-row-height` / `--qn-lang-sheet-row-height` | 44px / 52px | link height (popover / sheet) |
|
|
441
|
+
| `--qn-lang-gap` / `--qn-lang-pad` | 0 / 4px | space between links / inside the popover |
|
|
442
|
+
| `--qn-lang-offset` | 8px | gap between trigger and popover |
|
|
443
|
+
| `--qn-lang-popover-width` | 240px | popover width |
|
|
444
|
+
| `--qn-lang-many-width` / `--qn-lang-many-columns` | popover width / 1 | width and columns when `data-many="true"` (set `--qn-lang-many-columns: 2` and a wider width for two columns) |
|
|
445
|
+
| `--qn-lang-list-max` | 8.5 rows | height at which the list scrolls |
|
|
446
|
+
| `--qn-lang-sheet-radius` | 16px | sheet top corners |
|
|
447
|
+
| `--qn-lang-scrim` | 40% black | scrim colour |
|
|
448
|
+
| `--qn-lang-z` | 50 | z-index |
|
|
449
|
+
| `--qn-lang-duration` / `--qn-lang-ease` | 160ms / ease-out curve | open motion |
|
|
450
|
+
|
|
451
|
+
Each variable falls back to the matching `--qn-*` token, then a neutral
|
|
452
|
+
value, and is never set by the kit itself — set it on the root, on an
|
|
453
|
+
ancestor, or on `:root`. The sheet breakpoint (640px) is fixed.
|
|
454
|
+
|
|
455
|
+
Restyling recipe — no override of internals needed:
|
|
456
|
+
|
|
457
|
+
```css
|
|
458
|
+
.site-header [data-qn-language-switcher] {
|
|
459
|
+
--qn-lang-surface: #f3f0e9;
|
|
460
|
+
--qn-lang-ink: #1b2029;
|
|
461
|
+
--qn-lang-radius: 0;
|
|
462
|
+
--qn-lang-item-radius: 0;
|
|
463
|
+
--qn-lang-popover-width: 280px;
|
|
464
|
+
--qn-lang-sheet-radius: 0;
|
|
465
|
+
--qn-lang-duration: 120ms;
|
|
466
|
+
}
|
|
467
|
+
.site-header [data-part='trigger'] { border: 0; letter-spacing: 0.12em; text-transform: uppercase; }
|
|
468
|
+
.site-header [data-part='option'][aria-current='true'] { font-style: italic; }
|
|
469
|
+
```
|
|
364
470
|
|
|
365
471
|
**Tell the storefront the theme places it.** Set `languageSwitcher: 'theme'` in
|
|
366
472
|
the theme manifest. Without it the storefront keeps rendering its own floating
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
LanguageSwitcher variant="menu" — a trigger and a list of language links:
|
|
3
|
+
a popover on wide screens, a bottom sheet below 640px. Prefix: qn-lang.
|
|
4
|
+
|
|
5
|
+
STYLING SURFACE. Every part has a class AND a data-part attribute:
|
|
6
|
+
root .qn-lang--menu [data-qn-language-switcher][data-variant="menu"]
|
|
7
|
+
trigger .qn-lang__trigger [data-part="trigger"]
|
|
8
|
+
label/code .qn-lang__trigger-label [data-part="trigger-label"]
|
|
9
|
+
.qn-lang__trigger-code [data-part="trigger-code"]
|
|
10
|
+
chevron .qn-lang__chevron [data-part="chevron"]
|
|
11
|
+
scrim .qn-lang__scrim [data-part="scrim"]
|
|
12
|
+
popover/sheet .qn-lang__popover [data-part="popover"]
|
|
13
|
+
grabber .qn-lang__handle [data-part="handle"]
|
|
14
|
+
group label .qn-lang__title [data-part="title"]
|
|
15
|
+
list .qn-lang__options [data-part="list"]
|
|
16
|
+
item .qn-lang__item [data-part="item"]
|
|
17
|
+
link .qn-lang__option [data-part="option"] (aria-current / data-current on the current one)
|
|
18
|
+
name/code .qn-lang__name / __code [data-part="name"] / [data-part="code"]
|
|
19
|
+
marker .qn-lang__mark [data-part="mark"]
|
|
20
|
+
State on the root: data-state="open|closed", data-side="bottom|top",
|
|
21
|
+
data-align="start|end", data-mode="popover|sheet" (while open),
|
|
22
|
+
data-count="<n>", data-many="true" (more than 10), data-show-code="true".
|
|
23
|
+
|
|
24
|
+
VARIABLES (--qn-lang-*): each is read as var(--qn-lang-x, var(--qn-token, fallback)),
|
|
25
|
+
and none is declared on the root, so set them on the root, an ancestor or :root.
|
|
26
|
+
surface ink muted border radius item-radius item-hover current-bg accent
|
|
27
|
+
focus shadow row-height sheet-row-height gap pad offset popover-width
|
|
28
|
+
many-width many-columns list-max sheet-radius scrim z duration ease
|
|
29
|
+
Logical properties only: it mirrors itself in right-to-left storefronts.
|
|
30
|
+
============================================================ */
|
|
31
|
+
|
|
32
|
+
.qn-lang--menu {
|
|
33
|
+
position: relative;
|
|
34
|
+
display: inline-block;
|
|
35
|
+
max-inline-size: 100%;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* ── trigger ───────────────────────────────────────────── */
|
|
39
|
+
|
|
40
|
+
.qn-lang__trigger {
|
|
41
|
+
display: inline-flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 8px;
|
|
44
|
+
min-inline-size: 44px;
|
|
45
|
+
min-block-size: 44px;
|
|
46
|
+
padding-inline: 12px;
|
|
47
|
+
border: 1px solid var(--qn-lang-border, var(--qn-border, color-mix(in srgb, currentColor 22%, transparent)));
|
|
48
|
+
border-radius: var(--qn-lang-radius, var(--qn-radius-sm, 6px));
|
|
49
|
+
background: transparent;
|
|
50
|
+
color: var(--qn-lang-ink, var(--qn-text, inherit));
|
|
51
|
+
font: inherit;
|
|
52
|
+
line-height: 1.25;
|
|
53
|
+
white-space: nowrap;
|
|
54
|
+
cursor: pointer;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.qn-lang__trigger:hover {
|
|
58
|
+
border-color: var(--qn-lang-ink, var(--qn-text, currentColor));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.qn-lang__trigger:focus-visible,
|
|
62
|
+
.qn-lang__option:focus-visible {
|
|
63
|
+
outline: 2px solid var(--qn-lang-focus, var(--qn-focus, currentColor));
|
|
64
|
+
outline-offset: 2px;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.qn-lang__trigger-code,
|
|
68
|
+
.qn-lang__code {
|
|
69
|
+
display: none;
|
|
70
|
+
color: var(--qn-lang-muted, var(--qn-text-muted, color-mix(in srgb, currentColor 60%, transparent)));
|
|
71
|
+
font-size: 0.8em;
|
|
72
|
+
letter-spacing: 0.04em;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.qn-lang--menu[data-show-code='true'] .qn-lang__trigger-code,
|
|
76
|
+
.qn-lang--menu[data-show-code='true'] .qn-lang__code {
|
|
77
|
+
display: inline;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.qn-lang__trigger .qn-lang__chevron {
|
|
81
|
+
position: static;
|
|
82
|
+
inline-size: 10px;
|
|
83
|
+
block-size: 10px;
|
|
84
|
+
flex: none;
|
|
85
|
+
fill: none;
|
|
86
|
+
stroke: currentColor;
|
|
87
|
+
stroke-width: 1.8;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* ── popover ───────────────────────────────────────────── */
|
|
91
|
+
|
|
92
|
+
.qn-lang__popover {
|
|
93
|
+
position: absolute;
|
|
94
|
+
z-index: var(--qn-lang-z, 50);
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
box-sizing: border-box;
|
|
98
|
+
inline-size: var(--qn-lang-popover-width, 240px);
|
|
99
|
+
max-inline-size: calc(100vw - 32px);
|
|
100
|
+
max-block-size: var(--qn-lang-avail, none);
|
|
101
|
+
padding: var(--qn-lang-pad, 4px);
|
|
102
|
+
overflow: hidden;
|
|
103
|
+
border: 1px solid var(--qn-lang-border, var(--qn-border, color-mix(in srgb, currentColor 16%, transparent)));
|
|
104
|
+
border-radius: var(--qn-lang-radius, var(--qn-radius-md, 8px));
|
|
105
|
+
background: var(--qn-lang-surface, var(--qn-surface, Canvas));
|
|
106
|
+
color: var(--qn-lang-ink, var(--qn-text, CanvasText));
|
|
107
|
+
box-shadow: var(--qn-lang-shadow, 0 4px 16px rgb(0 0 0 / 0.08));
|
|
108
|
+
font: inherit;
|
|
109
|
+
text-align: start;
|
|
110
|
+
translate: var(--qn-lang-shift, 0px) 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.qn-lang__popover[hidden],
|
|
114
|
+
.qn-lang__scrim[hidden] {
|
|
115
|
+
display: none;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.qn-lang--menu[data-side='bottom'] > .qn-lang__popover {
|
|
119
|
+
inset-block-start: calc(100% + var(--qn-lang-offset, 8px));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.qn-lang--menu[data-side='top'] > .qn-lang__popover {
|
|
123
|
+
inset-block-end: calc(100% + var(--qn-lang-offset, 8px));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.qn-lang--menu[data-align='end'] > .qn-lang__popover {
|
|
127
|
+
inset-inline-end: 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.qn-lang--menu[data-align='start'] > .qn-lang__popover {
|
|
131
|
+
inset-inline-start: 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.qn-lang--menu[data-many='true'] > .qn-lang__popover {
|
|
135
|
+
inline-size: var(--qn-lang-many-width, var(--qn-lang-popover-width, 240px));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.qn-lang__handle,
|
|
139
|
+
.qn-lang__scrim {
|
|
140
|
+
display: none;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.qn-lang__title {
|
|
144
|
+
padding: 8px 12px 6px;
|
|
145
|
+
color: var(--qn-lang-muted, var(--qn-text-muted, color-mix(in srgb, currentColor 60%, transparent)));
|
|
146
|
+
font-size: 0.75em;
|
|
147
|
+
font-weight: 600;
|
|
148
|
+
letter-spacing: 0.06em;
|
|
149
|
+
text-transform: uppercase;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ── list ──────────────────────────────────────────────── */
|
|
153
|
+
|
|
154
|
+
.qn-lang__options {
|
|
155
|
+
display: grid;
|
|
156
|
+
grid-template-columns: repeat(var(--qn-lang-many-columns, 1), minmax(0, 1fr));
|
|
157
|
+
gap: var(--qn-lang-gap, 0);
|
|
158
|
+
flex: 1 1 auto;
|
|
159
|
+
min-block-size: 0;
|
|
160
|
+
max-block-size: var(--qn-lang-list-max, calc(var(--qn-lang-row-height, 44px) * 8.5));
|
|
161
|
+
margin: 0;
|
|
162
|
+
padding: 0;
|
|
163
|
+
overflow-y: auto;
|
|
164
|
+
overscroll-behavior: contain;
|
|
165
|
+
list-style: none;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* The extra columns only apply to long lists. */
|
|
169
|
+
.qn-lang--menu:not([data-many='true']) .qn-lang__options {
|
|
170
|
+
grid-template-columns: minmax(0, 1fr);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.qn-lang__item {
|
|
174
|
+
display: block;
|
|
175
|
+
margin: 0;
|
|
176
|
+
padding: 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.qn-lang__option {
|
|
180
|
+
display: flex;
|
|
181
|
+
align-items: center;
|
|
182
|
+
gap: 12px;
|
|
183
|
+
box-sizing: border-box;
|
|
184
|
+
min-block-size: var(--qn-lang-row-height, 44px);
|
|
185
|
+
padding-inline: 12px;
|
|
186
|
+
border-radius: var(--qn-lang-item-radius, var(--qn-radius-sm, 6px));
|
|
187
|
+
color: inherit;
|
|
188
|
+
font: inherit;
|
|
189
|
+
text-decoration: none;
|
|
190
|
+
cursor: pointer;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.qn-lang__option:hover {
|
|
194
|
+
background: var(--qn-lang-item-hover, color-mix(in srgb, currentColor 6%, transparent));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.qn-lang__option[aria-current='true'] {
|
|
198
|
+
background: var(--qn-lang-current-bg, transparent);
|
|
199
|
+
font-weight: 600;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.qn-lang__option:focus-visible {
|
|
203
|
+
outline-offset: -2px;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.qn-lang__name {
|
|
207
|
+
flex: 1 1 auto;
|
|
208
|
+
min-inline-size: 0;
|
|
209
|
+
overflow: hidden;
|
|
210
|
+
text-overflow: ellipsis;
|
|
211
|
+
white-space: nowrap;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.qn-lang__code {
|
|
215
|
+
flex: none;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.qn-lang__mark {
|
|
219
|
+
display: inline-flex;
|
|
220
|
+
inline-size: 16px;
|
|
221
|
+
block-size: 16px;
|
|
222
|
+
flex: none;
|
|
223
|
+
color: var(--qn-lang-accent, var(--qn-accent, currentColor));
|
|
224
|
+
visibility: hidden;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.qn-lang__option[aria-current='true'] .qn-lang__mark {
|
|
228
|
+
visibility: visible;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.qn-lang__mark svg {
|
|
232
|
+
inline-size: 100%;
|
|
233
|
+
block-size: 100%;
|
|
234
|
+
fill: none;
|
|
235
|
+
stroke: currentColor;
|
|
236
|
+
stroke-width: 1.8;
|
|
237
|
+
stroke-linecap: round;
|
|
238
|
+
stroke-linejoin: round;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* ── motion ────────────────────────────────────────────── */
|
|
242
|
+
|
|
243
|
+
@keyframes qn-lang-pop-down {
|
|
244
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
245
|
+
to { opacity: 1; transform: none; }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
@keyframes qn-lang-pop-up {
|
|
249
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
250
|
+
to { opacity: 1; transform: none; }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@keyframes qn-lang-sheet-in {
|
|
254
|
+
from { transform: translateY(100%); }
|
|
255
|
+
to { transform: none; }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
@keyframes qn-lang-fade-in {
|
|
259
|
+
from { opacity: 0; }
|
|
260
|
+
to { opacity: 1; }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.qn-lang--menu[data-side='bottom'] > .qn-lang__popover:not([hidden]) {
|
|
264
|
+
animation: qn-lang-pop-down var(--qn-lang-duration, 160ms) var(--qn-lang-ease, cubic-bezier(0.2, 0.8, 0.2, 1));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.qn-lang--menu[data-side='top'] > .qn-lang__popover:not([hidden]) {
|
|
268
|
+
animation: qn-lang-pop-up var(--qn-lang-duration, 160ms) var(--qn-lang-ease, cubic-bezier(0.2, 0.8, 0.2, 1));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
272
|
+
.qn-lang__chevron {
|
|
273
|
+
transition: rotate var(--qn-lang-duration, 160ms) var(--qn-lang-ease, ease);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.qn-lang--menu[data-state='open'] .qn-lang__trigger .qn-lang__chevron {
|
|
278
|
+
rotate: 180deg;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* ── bottom sheet (narrow screens) ─────────────────────── */
|
|
282
|
+
|
|
283
|
+
@media (max-width: 639.98px) {
|
|
284
|
+
.qn-lang__scrim:not([hidden]) {
|
|
285
|
+
position: fixed;
|
|
286
|
+
inset: 0;
|
|
287
|
+
z-index: calc(var(--qn-lang-z, 50) - 1);
|
|
288
|
+
display: block;
|
|
289
|
+
background: var(--qn-lang-scrim, rgb(0 0 0 / 0.4));
|
|
290
|
+
animation: qn-lang-fade-in var(--qn-lang-duration, 160ms) linear;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* [data-side][data-align] outranks the popover placement rules above. */
|
|
294
|
+
.qn-lang--menu[data-side][data-align] > .qn-lang__popover {
|
|
295
|
+
position: fixed;
|
|
296
|
+
inset-block: auto 0;
|
|
297
|
+
inset-inline: 0;
|
|
298
|
+
inline-size: auto;
|
|
299
|
+
max-inline-size: none;
|
|
300
|
+
max-block-size: 85vh;
|
|
301
|
+
max-block-size: 85dvh;
|
|
302
|
+
padding: var(--qn-lang-pad, 4px) var(--qn-lang-pad, 4px) calc(var(--qn-lang-pad, 4px) + env(safe-area-inset-bottom, 0px));
|
|
303
|
+
border-inline-width: 0;
|
|
304
|
+
border-block-end-width: 0;
|
|
305
|
+
border-start-start-radius: var(--qn-lang-sheet-radius, 16px);
|
|
306
|
+
border-start-end-radius: var(--qn-lang-sheet-radius, 16px);
|
|
307
|
+
border-end-start-radius: 0;
|
|
308
|
+
border-end-end-radius: 0;
|
|
309
|
+
translate: none;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
.qn-lang--menu[data-side][data-align] > .qn-lang__popover:not([hidden]) {
|
|
313
|
+
animation: qn-lang-sheet-in calc(var(--qn-lang-duration, 160ms) * 1.5) var(--qn-lang-ease, cubic-bezier(0.2, 0.8, 0.2, 1));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
.qn-lang__handle {
|
|
317
|
+
display: block;
|
|
318
|
+
inline-size: 36px;
|
|
319
|
+
block-size: 4px;
|
|
320
|
+
margin: 8px auto 4px;
|
|
321
|
+
border-radius: 2px;
|
|
322
|
+
background: color-mix(in srgb, currentColor 25%, transparent);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.qn-lang--menu .qn-lang__options,
|
|
326
|
+
.qn-lang--menu:not([data-many='true']) .qn-lang__options {
|
|
327
|
+
grid-template-columns: minmax(0, 1fr);
|
|
328
|
+
max-block-size: none;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.qn-lang__option {
|
|
332
|
+
min-block-size: var(--qn-lang-sheet-row-height, 52px);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/* Page scroll is locked behind an open sheet. */
|
|
337
|
+
body.qn-lang-lock {
|
|
338
|
+
overflow: hidden;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
@media (prefers-reduced-motion: reduce) {
|
|
342
|
+
.qn-lang--menu[data-side][data-align] > .qn-lang__popover:not([hidden]) {
|
|
343
|
+
animation: qn-lang-fade-in 80ms linear;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
.qn-lang__scrim:not([hidden]) {
|
|
347
|
+
animation-duration: 80ms;
|
|
348
|
+
}
|
|
349
|
+
}
|