@viax.io/uxm 4.36.0 → 4.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +74 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js.map +1 -1
- package/dist/previews/index.cjs +3 -0
- package/dist/previews/index.cjs.map +1 -1
- package/dist/previews/index.d.ts +3 -2
- package/dist/previews/index.d.ts.map +1 -1
- package/dist/previews/index.js +2 -0
- package/dist/previews/index.js.map +1 -1
- package/dist/studio/index.cjs.map +1 -1
- package/dist/studio/index.d.ts +2 -2
- package/dist/studio/index.js.map +1 -1
- package/dist/studio/lib/registry/app.cjs +52 -0
- package/dist/studio/lib/registry/app.cjs.map +1 -1
- package/dist/studio/lib/registry/app.d.ts.map +1 -1
- package/dist/studio/lib/registry/app.js +52 -0
- package/dist/studio/lib/registry/app.js.map +1 -1
- package/dist/studio/lib/registry/buttons.cjs +2 -2
- package/dist/studio/lib/registry/buttons.cjs.map +1 -1
- package/dist/studio/lib/registry/buttons.js +2 -2
- package/dist/studio/lib/registry/buttons.js.map +1 -1
- package/dist/studio/lib/registry/composite.cjs +1 -1
- package/dist/studio/lib/registry/composite.cjs.map +1 -1
- package/dist/studio/lib/registry/composite.js +1 -1
- package/dist/studio/lib/registry/composite.js.map +1 -1
- package/dist/studio/lib/registry/diagram.cjs +1 -1
- package/dist/studio/lib/registry/diagram.cjs.map +1 -1
- package/dist/studio/lib/registry/diagram.js +1 -1
- package/dist/studio/lib/registry/diagram.js.map +1 -1
- package/dist/studio/lib/registry/display.cjs +2 -2
- package/dist/studio/lib/registry/display.cjs.map +1 -1
- package/dist/studio/lib/registry/display.js +2 -2
- package/dist/studio/lib/registry/display.js.map +1 -1
- package/dist/studio/lib/registry/feedback.cjs +3 -3
- package/dist/studio/lib/registry/feedback.cjs.map +1 -1
- package/dist/studio/lib/registry/feedback.js +3 -3
- package/dist/studio/lib/registry/feedback.js.map +1 -1
- package/dist/studio/lib/registry/inputs.cjs +12 -12
- package/dist/studio/lib/registry/inputs.cjs.map +1 -1
- package/dist/studio/lib/registry/inputs.js +12 -12
- package/dist/studio/lib/registry/inputs.js.map +1 -1
- package/dist/studio/lib/types.cjs.map +1 -1
- package/dist/studio/persistence/generate-css.cjs +15 -0
- package/dist/studio/persistence/generate-css.cjs.map +1 -1
- package/dist/studio/persistence/generate-css.d.ts.map +1 -1
- package/dist/studio/persistence/generate-css.js +15 -0
- package/dist/studio/persistence/generate-css.js.map +1 -1
- package/dist/studio/shell/canvas.cjs +1 -0
- package/dist/studio/shell/canvas.cjs.map +1 -1
- package/dist/studio/shell/canvas.d.ts.map +1 -1
- package/dist/studio/shell/canvas.js +2 -0
- package/dist/studio/shell/canvas.js.map +1 -1
- package/dist/ui/calendar/calendar.cjs +3 -1
- package/dist/ui/calendar/calendar.cjs.map +1 -1
- package/dist/ui/calendar/calendar.d.ts +6 -2
- package/dist/ui/calendar/calendar.d.ts.map +1 -1
- package/dist/ui/calendar/calendar.js +3 -1
- package/dist/ui/calendar/calendar.js.map +1 -1
- package/dist/ui/currency-input/currency-input.cjs +3 -1
- package/dist/ui/currency-input/currency-input.cjs.map +1 -1
- package/dist/ui/currency-input/currency-input.d.ts +4 -2
- package/dist/ui/currency-input/currency-input.d.ts.map +1 -1
- package/dist/ui/currency-input/currency-input.js +3 -1
- package/dist/ui/currency-input/currency-input.js.map +1 -1
- package/dist/ui/date-input/date-input.cjs +2 -1
- package/dist/ui/date-input/date-input.cjs.map +1 -1
- package/dist/ui/date-input/date-input.d.ts +11 -1
- package/dist/ui/date-input/date-input.d.ts.map +1 -1
- package/dist/ui/date-input/date-input.js +2 -1
- package/dist/ui/date-input/date-input.js.map +1 -1
- package/dist/ui/editable-cell/editable-cell.cjs +20 -6
- package/dist/ui/editable-cell/editable-cell.cjs.map +1 -1
- package/dist/ui/editable-cell/editable-cell.d.ts +24 -1
- package/dist/ui/editable-cell/editable-cell.d.ts.map +1 -1
- package/dist/ui/editable-cell/editable-cell.js +20 -6
- package/dist/ui/editable-cell/editable-cell.js.map +1 -1
- package/dist/ui/file-upload/file-upload.cjs +36 -9
- package/dist/ui/file-upload/file-upload.cjs.map +1 -1
- package/dist/ui/file-upload/file-upload.d.ts +46 -4
- package/dist/ui/file-upload/file-upload.d.ts.map +1 -1
- package/dist/ui/file-upload/file-upload.js +36 -9
- package/dist/ui/file-upload/file-upload.js.map +1 -1
- package/dist/ui/index.cjs +12 -2
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.ts +5 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +7 -1
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/language-switcher/index.cjs +29 -0
- package/dist/ui/language-switcher/index.cjs.map +1 -0
- package/dist/ui/language-switcher/index.d.ts +3 -0
- package/dist/ui/language-switcher/index.d.ts.map +1 -0
- package/dist/ui/language-switcher/index.js +5 -0
- package/dist/ui/language-switcher/index.js.map +1 -0
- package/dist/ui/language-switcher/language-switcher-preview.cjs +137 -0
- package/dist/ui/language-switcher/language-switcher-preview.cjs.map +1 -0
- package/dist/ui/language-switcher/language-switcher-preview.d.ts +3 -0
- package/dist/ui/language-switcher/language-switcher-preview.d.ts.map +1 -0
- package/dist/ui/language-switcher/language-switcher-preview.js +113 -0
- package/dist/ui/language-switcher/language-switcher-preview.js.map +1 -0
- package/dist/ui/language-switcher/language-switcher.cjs +127 -0
- package/dist/ui/language-switcher/language-switcher.cjs.map +1 -0
- package/dist/ui/language-switcher/language-switcher.css +95 -0
- package/dist/ui/language-switcher/language-switcher.d.ts +73 -0
- package/dist/ui/language-switcher/language-switcher.d.ts.map +1 -0
- package/dist/ui/language-switcher/language-switcher.js +103 -0
- package/dist/ui/language-switcher/language-switcher.js.map +1 -0
- package/dist/ui/locale/index.cjs +33 -0
- package/dist/ui/locale/index.cjs.map +1 -0
- package/dist/ui/locale/index.d.ts +3 -0
- package/dist/ui/locale/index.d.ts.map +1 -0
- package/dist/ui/locale/index.js +7 -0
- package/dist/ui/locale/index.js.map +1 -0
- package/dist/ui/locale/locale.cjs +43 -0
- package/dist/ui/locale/locale.cjs.map +1 -0
- package/dist/ui/locale/locale.d.ts +49 -0
- package/dist/ui/locale/locale.d.ts.map +1 -0
- package/dist/ui/locale/locale.js +17 -0
- package/dist/ui/locale/locale.js.map +1 -0
- package/dist/ui/page-shell/page-shell.cjs.map +1 -1
- package/dist/ui/page-shell/page-shell.d.ts +1 -1
- package/dist/ui/page-shell/page-shell.d.ts.map +1 -1
- package/dist/ui/page-shell/page-shell.js.map +1 -1
- package/dist/ui/range-slider/range-slider.cjs +7 -3
- package/dist/ui/range-slider/range-slider.cjs.map +1 -1
- package/dist/ui/range-slider/range-slider.d.ts +17 -1
- package/dist/ui/range-slider/range-slider.d.ts.map +1 -1
- package/dist/ui/range-slider/range-slider.js +7 -3
- package/dist/ui/range-slider/range-slider.js.map +1 -1
- package/dist/ui/styles.css +2 -1
- package/package.json +4 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/ui/language-switcher/language-switcher-preview.tsx"],"sourcesContent":["import { useState, type CSSProperties } from 'react';\n\nimport { cn } from '@/helpers';\nimport type { PreviewProps } from '@/previews/types';\nimport { Icon, LanguageSwitcher } from '@/ui';\n\ntype Styles = PreviewProps['styles'];\n\n/** A spread of scripts — Latin, Cyrillic, CJK — so the endonym rendering and\n * the trigger's width behaviour are both visible at a glance. */\nconst DEMO_LOCALES = ['en-US', 'de-DE', 'uk-UA', 'fr-FR', 'ja-JP', 'pl-PL'];\n\nfunction buildVars(styles: Styles): CSSProperties {\n return {\n '--uxm-language-switcher-bg': styles.bg as string,\n '--uxm-language-switcher-border-color': styles.borderColor as string,\n '--uxm-language-switcher-radius': `${styles.radius}px`,\n '--uxm-language-switcher-color': styles.color as string,\n '--uxm-language-switcher-font-size': `${styles.fontSize}px`,\n '--uxm-language-switcher-gap': `${styles.gap}px`,\n '--uxm-language-switcher-padding-x': `${styles.paddingX}px`,\n '--uxm-language-switcher-padding-y': `${styles.paddingY}px`,\n '--uxm-language-switcher-hover-bg': styles.hoverBg as string,\n '--uxm-language-switcher-hover-border-color': styles.hoverBorderColor as string,\n '--uxm-language-switcher-open-bg': styles.openBg as string,\n '--uxm-language-switcher-open-border-color': styles.openBorderColor as string,\n '--uxm-language-switcher-focus-ring': styles.focusRing as string,\n '--uxm-language-switcher-caret-color': styles.caretColor as string,\n '--uxm-language-switcher-disabled-opacity':\n styles.disabledOpacity != null ? String(styles.disabledOpacity) : undefined,\n } as CSSProperties;\n}\n\n/**\n * Static showcase — hand-rendered trigger markup, not the real atom.\n *\n * The atom routes `className` to the Listbox root and exposes no hook onto the\n * trigger, so the `--state-*` forced modifier cannot be projected through it\n * (same reason ButtonGroup's preview hand-renders). Inert: `pointer-events:\n * none` stops the production `:hover` firing on mouse-over and `tabIndex={-1}`\n * keeps it out of the focus order, so a real Tab press cannot override the\n * forced state being demonstrated.\n */\nfunction StaticShowcase({ state, variant }: { state: string; variant: 'full' | 'compact' }) {\n return (\n <div className=\"uxm-language-switcher\" style={{ pointerEvents: 'none' }}>\n <button\n type=\"button\"\n tabIndex={-1}\n aria-hidden=\"true\"\n className={cn(\n 'uxm-language-switcher__trigger',\n `uxm-language-switcher__trigger--${variant}`,\n state !== 'default' && `uxm-language-switcher__trigger--state-${state}`,\n )}\n >\n <Icon glyph=\"globe\" size={16} />\n {variant === 'full' && <span className=\"uxm-language-switcher__current\">Deutsch</span>}\n <span className=\"uxm-language-switcher__caret\">\n <Icon\n glyph=\"chevron-down\"\n size={12}\n strokeWidth={2.2}\n style={{ transform: state === 'open' ? 'rotate(180deg)' : 'none' }}\n />\n </span>\n </button>\n </div>\n );\n}\n\nexport function LanguageSwitcherPreview({ styles, variants }: PreviewProps) {\n const variant = ((variants.variant as string) ?? 'full') as 'full' | 'compact';\n const state = (variants.state as string) ?? 'default';\n const [locale, setLocale] = useState('en-US');\n const cssVars = buildVars(styles);\n\n const sectionLabel = {\n fontSize: 11,\n color: 'var(--color-text-muted)',\n marginBottom: 12,\n letterSpacing: 0.5,\n textTransform: 'uppercase' as const,\n };\n\n return (\n <div style={{ display: 'flex', flexDirection: 'column', gap: 32, ...cssVars } as CSSProperties}>\n <div>\n <div style={sectionLabel}>\n {variant} · {state} state\n </div>\n <StaticShowcase state={state} variant={variant} />\n </div>\n\n {/* Live instance — the real atom, exercising the production :hover /\n :focus-visible / open rules through the same projected vars. */}\n <div>\n <div style={sectionLabel}>interactive</div>\n <LanguageSwitcher\n locales={DEMO_LOCALES}\n value={locale}\n onChange={setLocale}\n variant={variant}\n disabled={state === 'disabled'}\n />\n </div>\n\n {/* The zero-cost-absence rule, shown rather than described: the same atom\n with a single configured locale emits nothing. A realm with one\n language depends on this, and it is easy to regress into a disabled\n control, so it earns a slot. */}\n <div>\n <div style={sectionLabel}>one locale configured → renders nothing</div>\n <div\n style={{\n border: '1px dashed var(--color-border)',\n borderRadius: 8,\n padding: 12,\n fontSize: 12,\n color: 'var(--color-text-muted)',\n }}\n >\n <LanguageSwitcher locales={['en-US']} value=\"en-US\" onChange={() => {}} />\n (empty — no wrapper, no disabled control)\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var language_switcher_exports = {};
|
|
20
|
+
__export(language_switcher_exports, {
|
|
21
|
+
LanguageSwitcher: () => LanguageSwitcher
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(language_switcher_exports);
|
|
24
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
25
|
+
var import_react = require("react");
|
|
26
|
+
var import_helpers = require("../../helpers/index.cjs");
|
|
27
|
+
var import_icon = require("../icon/index.cjs");
|
|
28
|
+
var import_listbox = require("../listbox/index.cjs");
|
|
29
|
+
function endonym(locale, withRegion) {
|
|
30
|
+
const base = locale.split("-")[0];
|
|
31
|
+
try {
|
|
32
|
+
const display = new Intl.DisplayNames([locale], { type: "language" });
|
|
33
|
+
const name = display.of(withRegion ? locale : base);
|
|
34
|
+
if (!name || name === locale || name === base) return locale;
|
|
35
|
+
return name.charAt(0).toLocaleUpperCase(locale) + name.slice(1);
|
|
36
|
+
} catch {
|
|
37
|
+
return locale;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function buildLabels(locales) {
|
|
41
|
+
const perBase = /* @__PURE__ */ new Map();
|
|
42
|
+
for (const locale of locales) {
|
|
43
|
+
const base = locale.split("-")[0];
|
|
44
|
+
perBase.set(base, (perBase.get(base) ?? 0) + 1);
|
|
45
|
+
}
|
|
46
|
+
return new Map(
|
|
47
|
+
locales.map((locale) => [
|
|
48
|
+
locale,
|
|
49
|
+
endonym(locale, (perBase.get(locale.split("-")[0]) ?? 0) > 1)
|
|
50
|
+
])
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
function LanguageSwitcher({
|
|
54
|
+
locales,
|
|
55
|
+
value,
|
|
56
|
+
onChange,
|
|
57
|
+
getLabel,
|
|
58
|
+
variant = "full",
|
|
59
|
+
label = "Language",
|
|
60
|
+
searchLabel = "Search languages",
|
|
61
|
+
searchable = "auto",
|
|
62
|
+
disabled = false,
|
|
63
|
+
className,
|
|
64
|
+
style
|
|
65
|
+
}) {
|
|
66
|
+
const items = (0, import_react.useMemo)(() => {
|
|
67
|
+
const labels = getLabel ? null : buildLabels(locales);
|
|
68
|
+
return locales.map((locale) => ({
|
|
69
|
+
locale,
|
|
70
|
+
label: getLabel ? getLabel(locale) : labels.get(locale) ?? locale
|
|
71
|
+
}));
|
|
72
|
+
}, [locales, getLabel]);
|
|
73
|
+
if (locales.length <= 1) return null;
|
|
74
|
+
const selected = items.find((item) => item.locale === value) ?? null;
|
|
75
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
76
|
+
import_listbox.Listbox,
|
|
77
|
+
{
|
|
78
|
+
items,
|
|
79
|
+
getKey: (item) => item.locale,
|
|
80
|
+
getLabel: (item) => item.label,
|
|
81
|
+
value: selected,
|
|
82
|
+
onChange: (next) => next && onChange(next.locale),
|
|
83
|
+
disabled,
|
|
84
|
+
searchable,
|
|
85
|
+
searchPlaceholder: searchLabel,
|
|
86
|
+
searchAriaLabel: searchLabel,
|
|
87
|
+
matchAnchorWidth: false,
|
|
88
|
+
minPanelWidth: 180,
|
|
89
|
+
placement: "bottom-end",
|
|
90
|
+
className: (0, import_helpers.cn)("uxm-language-switcher", className),
|
|
91
|
+
"aria-label": label,
|
|
92
|
+
renderTrigger: ({ open, triggerProps }) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
93
|
+
"button",
|
|
94
|
+
{
|
|
95
|
+
...triggerProps,
|
|
96
|
+
type: "button",
|
|
97
|
+
className: (0, import_helpers.cn)(
|
|
98
|
+
"uxm-language-switcher__trigger",
|
|
99
|
+
`uxm-language-switcher__trigger--${variant}`,
|
|
100
|
+
open && "uxm-language-switcher__trigger--open"
|
|
101
|
+
),
|
|
102
|
+
style,
|
|
103
|
+
"aria-label": label,
|
|
104
|
+
children: [
|
|
105
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icon.Icon, { glyph: "globe", size: 16, "aria-hidden": "true" }),
|
|
106
|
+
variant === "full" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "uxm-language-switcher__current", children: selected?.label ?? value }),
|
|
107
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "uxm-language-switcher__caret", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
108
|
+
import_icon.Icon,
|
|
109
|
+
{
|
|
110
|
+
glyph: "chevron-down",
|
|
111
|
+
size: 12,
|
|
112
|
+
strokeWidth: 2.2,
|
|
113
|
+
style: { transform: open ? "rotate(180deg)" : "none" }
|
|
114
|
+
}
|
|
115
|
+
) })
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
),
|
|
119
|
+
renderItem: (item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "uxm-language-switcher__row", lang: item.locale, children: item.label })
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
124
|
+
0 && (module.exports = {
|
|
125
|
+
LanguageSwitcher
|
|
126
|
+
});
|
|
127
|
+
//# sourceMappingURL=language-switcher.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/ui/language-switcher/language-switcher.tsx"],"sourcesContent":["import { useMemo } from 'react';\n\nimport { cn } from '@/helpers';\n\nimport { Icon } from '../icon';\nimport { Listbox } from '../listbox';\n\nimport type { CSSProperties } from 'react';\n\nexport type LanguageSwitcherVariant = 'full' | 'compact';\n\nexport interface LanguageSwitcherProps {\n /**\n * BCP-47 tags the app offers, e.g. `[\"en-US\", \"de-DE\"]`.\n *\n * **The consumer supplies this — the atom never fetches it.** In a viax\n * portal it comes from the realm's `getSupportedLocales`; the library has no\n * data layer and must not grow one.\n *\n * When this holds **one entry or none, the component renders `null`** — not a\n * disabled control, not a hidden one. Nothing is emitted, so a single-locale\n * app pays zero layout cost.\n */\n locales: string[];\n /** The currently selected tag. Must be one of `locales`. Controlled. */\n value: string;\n /** Fires with the picked BCP-47 tag. */\n onChange: (locale: string) => void;\n /**\n * Override the label shown for a tag. The default renders the language's\n * **endonym** — its name in its own language (`Deutsch`, not `German`) — via\n * `Intl.DisplayNames`, which is why no hand-written name map ships here: a\n * static map goes stale and cannot cover a realm's arbitrary locale list.\n *\n * Pass this only for a genuinely different rendering (adding a region, a\n * flag, a marketing name). It is not the translation hook — endonyms are the\n * same in every UI language, so this does NOT change with the app's locale.\n */\n getLabel?: (locale: string) => string;\n /** `full` (default) shows the current endonym beside the globe; `compact` is icon-only, for tight top bars. */\n variant?: LanguageSwitcherVariant;\n /**\n * Accessible name for the trigger. Default `\"Language\"`. The trigger's\n * visible text is a language name, which tells a screen-reader user *what*\n * is selected but not *what the control does* — so this is the only thing\n * naming the control, and it must be translated by the consumer.\n */\n label?: string;\n /** Accessible name for the panel's search box. Default `\"Search languages\"`. */\n searchLabel?: string;\n /** Reveal the panel's search box. Defaults to `'auto'` — shown past the shared Listbox threshold (6). */\n searchable?: boolean | 'auto';\n disabled?: boolean;\n className?: string;\n style?: CSSProperties;\n}\n\n/**\n * The endonym for a tag: the language's name written in that language.\n *\n * `Intl.DisplayNames` is asked for the name *in the target locale itself*\n * (`new Intl.DisplayNames([tag], …)`), which is what turns `de-DE` into\n * `Deutsch` rather than `German`. That is the accessibility requirement — a\n * reader who only speaks German has to recognise their own language in the list.\n *\n * Resolved on the **base language subtag**, not the full tag: `.of(\"de-DE\")`\n * yields `\"Deutsch (Deutschland)\"` and `.of(\"en-US\")` yields\n * `\"American English\"`, and a switcher listing countries when it is choosing\n * languages reads as noise. The region is added back by {@link buildLabels}\n * only where it actually disambiguates.\n *\n * Falls back to the bare tag on any runtime that rejects the locale, so an\n * unusual tag degrades to something selectable instead of throwing.\n */\nfunction endonym(locale: string, withRegion: boolean): string {\n const base = locale.split('-')[0];\n try {\n const display = new Intl.DisplayNames([locale], { type: 'language' });\n const name = display.of(withRegion ? locale : base);\n if (!name || name === locale || name === base) return locale;\n // CLDR yields lowercase endonyms for several languages (`français`,\n // `español`). A standalone item in a picker reads as a proper noun in the\n // UI, so title-case the first character only — never the whole string,\n // which would corrupt scripts that have no case at all.\n return name.charAt(0).toLocaleUpperCase(locale) + name.slice(1);\n } catch {\n return locale;\n }\n}\n\n/**\n * Label every tag in the list, adding the region **only where two tags share a\n * base language**.\n *\n * A realm offering `[\"en-US\", \"de-DE\"]` gets `English` / `Deutsch`. One\n * offering `[\"en-US\", \"en-GB\"]` cannot: two rows both reading `English` are\n * unusable, so those become `American English` / `British English`. The\n * decision is per base language, so a list mixing both cases keeps the short\n * form where it is unambiguous.\n */\nfunction buildLabels(locales: string[]): Map<string, string> {\n const perBase = new Map<string, number>();\n for (const locale of locales) {\n const base = locale.split('-')[0];\n perBase.set(base, (perBase.get(base) ?? 0) + 1);\n }\n return new Map(\n locales.map((locale) => [\n locale,\n endonym(locale, (perBase.get(locale.split('-')[0]) ?? 0) > 1),\n ]),\n );\n}\n\n/**\n * LanguageSwitcher — the single, library-owned control for changing the app's\n * language.\n *\n * Deliberately **presentational and data-free**: it takes the locale list, the\n * current value, and a change handler, and it renders. It issues no query,\n * reads no context, and persists nothing. Where the list comes from (a realm\n * config, a static array), how the choice is stored (localStorage, a user\n * preference mutation), and what re-renders afterwards are all the consumer's\n * concerns — the same separation `UxmLocaleProvider` keeps, and the reason one\n * switcher can serve every viax surface without any of them forking it.\n *\n * Pair it with `UxmLocaleProvider`: this control picks the tag, that provider\n * pushes it into every atom's `Intl` formatting.\n *\n * ```tsx\n * <LanguageSwitcher\n * locales={supportedLocales}\n * value={locale}\n * onChange={setLocale}\n * label={t('common.language')}\n * />\n * ```\n */\nexport function LanguageSwitcher({\n locales,\n value,\n onChange,\n getLabel,\n variant = 'full',\n label = 'Language',\n searchLabel = 'Search languages',\n searchable = 'auto',\n disabled = false,\n className,\n style,\n}: LanguageSwitcherProps) {\n // Endonyms never change with the app's UI language, so this memo is keyed on\n // the list alone — switching locale does not invalidate it.\n const items = useMemo(() => {\n const labels = getLabel ? null : buildLabels(locales);\n return locales.map((locale) => ({\n locale,\n label: getLabel ? getLabel(locale) : labels!.get(locale) ?? locale,\n }));\n }, [locales, getLabel]);\n\n // Zero-cost absence, not a disabled control: a realm with one configured\n // locale has no choice to offer, and an inert affordance would imply it does.\n // Guarded AFTER the hooks above so hook order stays stable when a locale list\n // arrives asynchronously and grows from one entry to several.\n if (locales.length <= 1) return null;\n\n const selected = items.find((item) => item.locale === value) ?? null;\n\n return (\n <Listbox<{ locale: string; label: string }>\n items={items}\n getKey={(item) => item.locale}\n getLabel={(item) => item.label}\n value={selected}\n onChange={(next) => next && onChange(next.locale)}\n disabled={disabled}\n searchable={searchable}\n searchPlaceholder={searchLabel}\n searchAriaLabel={searchLabel}\n matchAnchorWidth={false}\n minPanelWidth={180}\n placement=\"bottom-end\"\n className={cn('uxm-language-switcher', className)}\n aria-label={label}\n renderTrigger={({ open, triggerProps }) => (\n <button\n {...triggerProps}\n type=\"button\"\n className={cn(\n 'uxm-language-switcher__trigger',\n `uxm-language-switcher__trigger--${variant}`,\n open && 'uxm-language-switcher__trigger--open',\n )}\n style={style}\n aria-label={label}\n >\n <Icon glyph=\"globe\" size={16} aria-hidden=\"true\" />\n {variant === 'full' && (\n <span className=\"uxm-language-switcher__current\">\n {selected?.label ?? value}\n </span>\n )}\n <span className=\"uxm-language-switcher__caret\" aria-hidden=\"true\">\n <Icon\n glyph=\"chevron-down\"\n size={12}\n strokeWidth={2.2}\n style={{ transform: open ? 'rotate(180deg)' : 'none' }}\n />\n </span>\n </button>\n )}\n renderItem={(item) => (\n <span className=\"uxm-language-switcher__row\" lang={item.locale}>\n {item.label}\n </span>\n )}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
@charset "UTF-8";
|
|
2
|
+
/* ── language-switcher ── */
|
|
3
|
+
/* LanguageSwitcher — globe + current endonym + caret, opening the shared
|
|
4
|
+
Listbox panel. The panel itself is NOT styled here: it inherits the
|
|
5
|
+
library-wide `--uxm-listbox-*` surface so every picker in the app agrees,
|
|
6
|
+
and a studio save on Listbox themes this control's dropdown too. Only the
|
|
7
|
+
trigger is this atom's own surface. */
|
|
8
|
+
/* The atom passes its class into `Listbox`, which merges it onto its OWN root:
|
|
9
|
+
`cn('uxm-listbox', className)`. That root is `width: 100%` — correct for the
|
|
10
|
+
field-embedded pickers (CurrencyInput, PhoneInput) but wrong for a standalone
|
|
11
|
+
trigger, which must size to its content.
|
|
12
|
+
|
|
13
|
+
Both selectors are one class, so specificity ties and cascade order decides —
|
|
14
|
+
and `listbox.css` is imported AFTER `language-switcher.css`, so `width: 100%`
|
|
15
|
+
would win and stretch the switcher across its flex parent, pushing whatever
|
|
16
|
+
follows it out of the row. Qualifying with `.uxm-listbox` raises specificity
|
|
17
|
+
to (0,2,0) so this wins regardless of import order. Do NOT "fix" this by
|
|
18
|
+
reordering the @imports in styles.css — that ordering is alphabetical and
|
|
19
|
+
load-bearing elsewhere. */
|
|
20
|
+
.uxm-listbox.uxm-language-switcher {
|
|
21
|
+
display: inline-flex;
|
|
22
|
+
flex-shrink: 0;
|
|
23
|
+
width: auto;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.uxm-language-switcher {
|
|
27
|
+
display: inline-flex;
|
|
28
|
+
}
|
|
29
|
+
.uxm-language-switcher__trigger {
|
|
30
|
+
align-items: center;
|
|
31
|
+
background-color: var(--uxm-language-switcher-bg, transparent);
|
|
32
|
+
border: 1px solid var(--uxm-language-switcher-border-color, var(--color-border));
|
|
33
|
+
border-radius: var(--uxm-language-switcher-radius, 8px);
|
|
34
|
+
color: var(--uxm-language-switcher-color, var(--color-text));
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
display: inline-flex;
|
|
37
|
+
font-size: calc(var(--uxm-language-switcher-font-size, 13px) * var(--type-scale, 1));
|
|
38
|
+
font-weight: 500;
|
|
39
|
+
gap: var(--uxm-language-switcher-gap, 6px);
|
|
40
|
+
line-height: 1.2;
|
|
41
|
+
padding: var(--uxm-language-switcher-padding-y, 7px) var(--uxm-language-switcher-padding-x, 10px);
|
|
42
|
+
transition: background-color 0.15s, border-color 0.15s, color 0.15s;
|
|
43
|
+
/* `--state-*` forced modifiers sit beside the real pseudo-classes (the
|
|
44
|
+
input family's pattern) so the studio can paint a state on an inert
|
|
45
|
+
preview without synthesising pointer or focus events. */
|
|
46
|
+
}
|
|
47
|
+
.uxm-language-switcher__trigger--state-hover, .uxm-language-switcher__trigger:hover:not(:disabled) {
|
|
48
|
+
background-color: var(--uxm-language-switcher-hover-bg, var(--color-surface));
|
|
49
|
+
border-color: var(--uxm-language-switcher-hover-border-color, var(--color-border-strong, var(--color-border)));
|
|
50
|
+
}
|
|
51
|
+
.uxm-language-switcher__trigger {
|
|
52
|
+
/* AA focus indicator: a 2px ring at >=3:1 against both the trigger fill and
|
|
53
|
+
the surface behind it. `outline` (not box-shadow) so Windows High
|
|
54
|
+
Contrast keeps it. */
|
|
55
|
+
}
|
|
56
|
+
.uxm-language-switcher__trigger--state-focus, .uxm-language-switcher__trigger:focus-visible {
|
|
57
|
+
outline: 2px solid var(--uxm-language-switcher-focus-ring, var(--color-accent));
|
|
58
|
+
outline-offset: 2px;
|
|
59
|
+
}
|
|
60
|
+
.uxm-language-switcher__trigger--state-open, .uxm-language-switcher__trigger--open {
|
|
61
|
+
background-color: var(--uxm-language-switcher-open-bg, var(--color-surface));
|
|
62
|
+
border-color: var(--uxm-language-switcher-open-border-color, var(--color-accent));
|
|
63
|
+
}
|
|
64
|
+
.uxm-language-switcher__trigger {
|
|
65
|
+
/* Icon-only: drop the label but keep the tap target square and >=32px so it
|
|
66
|
+
still clears the pointer-target guidance in a dense top bar. */
|
|
67
|
+
}
|
|
68
|
+
.uxm-language-switcher__trigger--compact {
|
|
69
|
+
justify-content: center;
|
|
70
|
+
min-width: var(--uxm-language-switcher-compact-size, 34px);
|
|
71
|
+
padding-inline: var(--uxm-language-switcher-compact-padding-x, 8px);
|
|
72
|
+
}
|
|
73
|
+
.uxm-language-switcher__trigger--state-disabled, .uxm-language-switcher__trigger:disabled {
|
|
74
|
+
cursor: not-allowed;
|
|
75
|
+
opacity: var(--uxm-language-switcher-disabled-opacity, 0.5);
|
|
76
|
+
}
|
|
77
|
+
.uxm-language-switcher__current {
|
|
78
|
+
white-space: nowrap;
|
|
79
|
+
}
|
|
80
|
+
.uxm-language-switcher__caret {
|
|
81
|
+
align-items: center;
|
|
82
|
+
color: var(--uxm-language-switcher-caret-color, var(--color-text-muted));
|
|
83
|
+
display: inline-flex;
|
|
84
|
+
}
|
|
85
|
+
.uxm-language-switcher__caret svg {
|
|
86
|
+
transition: transform 0.15s;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Row inside the shared Listbox panel. `lang` is set per row by the component
|
|
90
|
+
so a screen reader switches voice to the right language when reading an
|
|
91
|
+
endonym — without it, "Deutsch" is announced with English phonetics. */
|
|
92
|
+
.uxm-language-switcher__row {
|
|
93
|
+
display: block;
|
|
94
|
+
white-space: nowrap;
|
|
95
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
export type LanguageSwitcherVariant = 'full' | 'compact';
|
|
3
|
+
export interface LanguageSwitcherProps {
|
|
4
|
+
/**
|
|
5
|
+
* BCP-47 tags the app offers, e.g. `["en-US", "de-DE"]`.
|
|
6
|
+
*
|
|
7
|
+
* **The consumer supplies this — the atom never fetches it.** In a viax
|
|
8
|
+
* portal it comes from the realm's `getSupportedLocales`; the library has no
|
|
9
|
+
* data layer and must not grow one.
|
|
10
|
+
*
|
|
11
|
+
* When this holds **one entry or none, the component renders `null`** — not a
|
|
12
|
+
* disabled control, not a hidden one. Nothing is emitted, so a single-locale
|
|
13
|
+
* app pays zero layout cost.
|
|
14
|
+
*/
|
|
15
|
+
locales: string[];
|
|
16
|
+
/** The currently selected tag. Must be one of `locales`. Controlled. */
|
|
17
|
+
value: string;
|
|
18
|
+
/** Fires with the picked BCP-47 tag. */
|
|
19
|
+
onChange: (locale: string) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Override the label shown for a tag. The default renders the language's
|
|
22
|
+
* **endonym** — its name in its own language (`Deutsch`, not `German`) — via
|
|
23
|
+
* `Intl.DisplayNames`, which is why no hand-written name map ships here: a
|
|
24
|
+
* static map goes stale and cannot cover a realm's arbitrary locale list.
|
|
25
|
+
*
|
|
26
|
+
* Pass this only for a genuinely different rendering (adding a region, a
|
|
27
|
+
* flag, a marketing name). It is not the translation hook — endonyms are the
|
|
28
|
+
* same in every UI language, so this does NOT change with the app's locale.
|
|
29
|
+
*/
|
|
30
|
+
getLabel?: (locale: string) => string;
|
|
31
|
+
/** `full` (default) shows the current endonym beside the globe; `compact` is icon-only, for tight top bars. */
|
|
32
|
+
variant?: LanguageSwitcherVariant;
|
|
33
|
+
/**
|
|
34
|
+
* Accessible name for the trigger. Default `"Language"`. The trigger's
|
|
35
|
+
* visible text is a language name, which tells a screen-reader user *what*
|
|
36
|
+
* is selected but not *what the control does* — so this is the only thing
|
|
37
|
+
* naming the control, and it must be translated by the consumer.
|
|
38
|
+
*/
|
|
39
|
+
label?: string;
|
|
40
|
+
/** Accessible name for the panel's search box. Default `"Search languages"`. */
|
|
41
|
+
searchLabel?: string;
|
|
42
|
+
/** Reveal the panel's search box. Defaults to `'auto'` — shown past the shared Listbox threshold (6). */
|
|
43
|
+
searchable?: boolean | 'auto';
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
className?: string;
|
|
46
|
+
style?: CSSProperties;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* LanguageSwitcher — the single, library-owned control for changing the app's
|
|
50
|
+
* language.
|
|
51
|
+
*
|
|
52
|
+
* Deliberately **presentational and data-free**: it takes the locale list, the
|
|
53
|
+
* current value, and a change handler, and it renders. It issues no query,
|
|
54
|
+
* reads no context, and persists nothing. Where the list comes from (a realm
|
|
55
|
+
* config, a static array), how the choice is stored (localStorage, a user
|
|
56
|
+
* preference mutation), and what re-renders afterwards are all the consumer's
|
|
57
|
+
* concerns — the same separation `UxmLocaleProvider` keeps, and the reason one
|
|
58
|
+
* switcher can serve every viax surface without any of them forking it.
|
|
59
|
+
*
|
|
60
|
+
* Pair it with `UxmLocaleProvider`: this control picks the tag, that provider
|
|
61
|
+
* pushes it into every atom's `Intl` formatting.
|
|
62
|
+
*
|
|
63
|
+
* ```tsx
|
|
64
|
+
* <LanguageSwitcher
|
|
65
|
+
* locales={supportedLocales}
|
|
66
|
+
* value={locale}
|
|
67
|
+
* onChange={setLocale}
|
|
68
|
+
* label={t('common.language')}
|
|
69
|
+
* />
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export declare function LanguageSwitcher({ locales, value, onChange, getLabel, variant, label, searchLabel, searchable, disabled, className, style, }: LanguageSwitcherProps): import("react/jsx-runtime").JSX.Element | null;
|
|
73
|
+
//# sourceMappingURL=language-switcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"language-switcher.d.ts","sourceRoot":"","sources":["../../../src/ui/language-switcher/language-switcher.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3C,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,SAAS,CAAC;AAEzD,MAAM,WAAW,qBAAqB;IACpC;;;;;;;;;;OAUG;IACH,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IACtC,+GAA+G;IAC/G,OAAO,CAAC,EAAE,uBAAuB,CAAC;IAClC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yGAAyG;IACzG,UAAU,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC9B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AA2DD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,OAAgB,EAChB,KAAkB,EAClB,WAAgC,EAChC,UAAmB,EACnB,QAAgB,EAChB,SAAS,EACT,KAAK,GACN,EAAE,qBAAqB,kDAsEvB"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { cn } from "../../helpers/index.js";
|
|
4
|
+
import { Icon } from "../icon/index.js";
|
|
5
|
+
import { Listbox } from "../listbox/index.js";
|
|
6
|
+
function endonym(locale, withRegion) {
|
|
7
|
+
const base = locale.split("-")[0];
|
|
8
|
+
try {
|
|
9
|
+
const display = new Intl.DisplayNames([locale], { type: "language" });
|
|
10
|
+
const name = display.of(withRegion ? locale : base);
|
|
11
|
+
if (!name || name === locale || name === base) return locale;
|
|
12
|
+
return name.charAt(0).toLocaleUpperCase(locale) + name.slice(1);
|
|
13
|
+
} catch {
|
|
14
|
+
return locale;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function buildLabels(locales) {
|
|
18
|
+
const perBase = /* @__PURE__ */ new Map();
|
|
19
|
+
for (const locale of locales) {
|
|
20
|
+
const base = locale.split("-")[0];
|
|
21
|
+
perBase.set(base, (perBase.get(base) ?? 0) + 1);
|
|
22
|
+
}
|
|
23
|
+
return new Map(
|
|
24
|
+
locales.map((locale) => [
|
|
25
|
+
locale,
|
|
26
|
+
endonym(locale, (perBase.get(locale.split("-")[0]) ?? 0) > 1)
|
|
27
|
+
])
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
function LanguageSwitcher({
|
|
31
|
+
locales,
|
|
32
|
+
value,
|
|
33
|
+
onChange,
|
|
34
|
+
getLabel,
|
|
35
|
+
variant = "full",
|
|
36
|
+
label = "Language",
|
|
37
|
+
searchLabel = "Search languages",
|
|
38
|
+
searchable = "auto",
|
|
39
|
+
disabled = false,
|
|
40
|
+
className,
|
|
41
|
+
style
|
|
42
|
+
}) {
|
|
43
|
+
const items = useMemo(() => {
|
|
44
|
+
const labels = getLabel ? null : buildLabels(locales);
|
|
45
|
+
return locales.map((locale) => ({
|
|
46
|
+
locale,
|
|
47
|
+
label: getLabel ? getLabel(locale) : labels.get(locale) ?? locale
|
|
48
|
+
}));
|
|
49
|
+
}, [locales, getLabel]);
|
|
50
|
+
if (locales.length <= 1) return null;
|
|
51
|
+
const selected = items.find((item) => item.locale === value) ?? null;
|
|
52
|
+
return /* @__PURE__ */ jsx(
|
|
53
|
+
Listbox,
|
|
54
|
+
{
|
|
55
|
+
items,
|
|
56
|
+
getKey: (item) => item.locale,
|
|
57
|
+
getLabel: (item) => item.label,
|
|
58
|
+
value: selected,
|
|
59
|
+
onChange: (next) => next && onChange(next.locale),
|
|
60
|
+
disabled,
|
|
61
|
+
searchable,
|
|
62
|
+
searchPlaceholder: searchLabel,
|
|
63
|
+
searchAriaLabel: searchLabel,
|
|
64
|
+
matchAnchorWidth: false,
|
|
65
|
+
minPanelWidth: 180,
|
|
66
|
+
placement: "bottom-end",
|
|
67
|
+
className: cn("uxm-language-switcher", className),
|
|
68
|
+
"aria-label": label,
|
|
69
|
+
renderTrigger: ({ open, triggerProps }) => /* @__PURE__ */ jsxs(
|
|
70
|
+
"button",
|
|
71
|
+
{
|
|
72
|
+
...triggerProps,
|
|
73
|
+
type: "button",
|
|
74
|
+
className: cn(
|
|
75
|
+
"uxm-language-switcher__trigger",
|
|
76
|
+
`uxm-language-switcher__trigger--${variant}`,
|
|
77
|
+
open && "uxm-language-switcher__trigger--open"
|
|
78
|
+
),
|
|
79
|
+
style,
|
|
80
|
+
"aria-label": label,
|
|
81
|
+
children: [
|
|
82
|
+
/* @__PURE__ */ jsx(Icon, { glyph: "globe", size: 16, "aria-hidden": "true" }),
|
|
83
|
+
variant === "full" && /* @__PURE__ */ jsx("span", { className: "uxm-language-switcher__current", children: selected?.label ?? value }),
|
|
84
|
+
/* @__PURE__ */ jsx("span", { className: "uxm-language-switcher__caret", "aria-hidden": "true", children: /* @__PURE__ */ jsx(
|
|
85
|
+
Icon,
|
|
86
|
+
{
|
|
87
|
+
glyph: "chevron-down",
|
|
88
|
+
size: 12,
|
|
89
|
+
strokeWidth: 2.2,
|
|
90
|
+
style: { transform: open ? "rotate(180deg)" : "none" }
|
|
91
|
+
}
|
|
92
|
+
) })
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
),
|
|
96
|
+
renderItem: (item) => /* @__PURE__ */ jsx("span", { className: "uxm-language-switcher__row", lang: item.locale, children: item.label })
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
export {
|
|
101
|
+
LanguageSwitcher
|
|
102
|
+
};
|
|
103
|
+
//# sourceMappingURL=language-switcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/ui/language-switcher/language-switcher.tsx"],"sourcesContent":["import { useMemo } from 'react';\n\nimport { cn } from '@/helpers';\n\nimport { Icon } from '../icon';\nimport { Listbox } from '../listbox';\n\nimport type { CSSProperties } from 'react';\n\nexport type LanguageSwitcherVariant = 'full' | 'compact';\n\nexport interface LanguageSwitcherProps {\n /**\n * BCP-47 tags the app offers, e.g. `[\"en-US\", \"de-DE\"]`.\n *\n * **The consumer supplies this — the atom never fetches it.** In a viax\n * portal it comes from the realm's `getSupportedLocales`; the library has no\n * data layer and must not grow one.\n *\n * When this holds **one entry or none, the component renders `null`** — not a\n * disabled control, not a hidden one. Nothing is emitted, so a single-locale\n * app pays zero layout cost.\n */\n locales: string[];\n /** The currently selected tag. Must be one of `locales`. Controlled. */\n value: string;\n /** Fires with the picked BCP-47 tag. */\n onChange: (locale: string) => void;\n /**\n * Override the label shown for a tag. The default renders the language's\n * **endonym** — its name in its own language (`Deutsch`, not `German`) — via\n * `Intl.DisplayNames`, which is why no hand-written name map ships here: a\n * static map goes stale and cannot cover a realm's arbitrary locale list.\n *\n * Pass this only for a genuinely different rendering (adding a region, a\n * flag, a marketing name). It is not the translation hook — endonyms are the\n * same in every UI language, so this does NOT change with the app's locale.\n */\n getLabel?: (locale: string) => string;\n /** `full` (default) shows the current endonym beside the globe; `compact` is icon-only, for tight top bars. */\n variant?: LanguageSwitcherVariant;\n /**\n * Accessible name for the trigger. Default `\"Language\"`. The trigger's\n * visible text is a language name, which tells a screen-reader user *what*\n * is selected but not *what the control does* — so this is the only thing\n * naming the control, and it must be translated by the consumer.\n */\n label?: string;\n /** Accessible name for the panel's search box. Default `\"Search languages\"`. */\n searchLabel?: string;\n /** Reveal the panel's search box. Defaults to `'auto'` — shown past the shared Listbox threshold (6). */\n searchable?: boolean | 'auto';\n disabled?: boolean;\n className?: string;\n style?: CSSProperties;\n}\n\n/**\n * The endonym for a tag: the language's name written in that language.\n *\n * `Intl.DisplayNames` is asked for the name *in the target locale itself*\n * (`new Intl.DisplayNames([tag], …)`), which is what turns `de-DE` into\n * `Deutsch` rather than `German`. That is the accessibility requirement — a\n * reader who only speaks German has to recognise their own language in the list.\n *\n * Resolved on the **base language subtag**, not the full tag: `.of(\"de-DE\")`\n * yields `\"Deutsch (Deutschland)\"` and `.of(\"en-US\")` yields\n * `\"American English\"`, and a switcher listing countries when it is choosing\n * languages reads as noise. The region is added back by {@link buildLabels}\n * only where it actually disambiguates.\n *\n * Falls back to the bare tag on any runtime that rejects the locale, so an\n * unusual tag degrades to something selectable instead of throwing.\n */\nfunction endonym(locale: string, withRegion: boolean): string {\n const base = locale.split('-')[0];\n try {\n const display = new Intl.DisplayNames([locale], { type: 'language' });\n const name = display.of(withRegion ? locale : base);\n if (!name || name === locale || name === base) return locale;\n // CLDR yields lowercase endonyms for several languages (`français`,\n // `español`). A standalone item in a picker reads as a proper noun in the\n // UI, so title-case the first character only — never the whole string,\n // which would corrupt scripts that have no case at all.\n return name.charAt(0).toLocaleUpperCase(locale) + name.slice(1);\n } catch {\n return locale;\n }\n}\n\n/**\n * Label every tag in the list, adding the region **only where two tags share a\n * base language**.\n *\n * A realm offering `[\"en-US\", \"de-DE\"]` gets `English` / `Deutsch`. One\n * offering `[\"en-US\", \"en-GB\"]` cannot: two rows both reading `English` are\n * unusable, so those become `American English` / `British English`. The\n * decision is per base language, so a list mixing both cases keeps the short\n * form where it is unambiguous.\n */\nfunction buildLabels(locales: string[]): Map<string, string> {\n const perBase = new Map<string, number>();\n for (const locale of locales) {\n const base = locale.split('-')[0];\n perBase.set(base, (perBase.get(base) ?? 0) + 1);\n }\n return new Map(\n locales.map((locale) => [\n locale,\n endonym(locale, (perBase.get(locale.split('-')[0]) ?? 0) > 1),\n ]),\n );\n}\n\n/**\n * LanguageSwitcher — the single, library-owned control for changing the app's\n * language.\n *\n * Deliberately **presentational and data-free**: it takes the locale list, the\n * current value, and a change handler, and it renders. It issues no query,\n * reads no context, and persists nothing. Where the list comes from (a realm\n * config, a static array), how the choice is stored (localStorage, a user\n * preference mutation), and what re-renders afterwards are all the consumer's\n * concerns — the same separation `UxmLocaleProvider` keeps, and the reason one\n * switcher can serve every viax surface without any of them forking it.\n *\n * Pair it with `UxmLocaleProvider`: this control picks the tag, that provider\n * pushes it into every atom's `Intl` formatting.\n *\n * ```tsx\n * <LanguageSwitcher\n * locales={supportedLocales}\n * value={locale}\n * onChange={setLocale}\n * label={t('common.language')}\n * />\n * ```\n */\nexport function LanguageSwitcher({\n locales,\n value,\n onChange,\n getLabel,\n variant = 'full',\n label = 'Language',\n searchLabel = 'Search languages',\n searchable = 'auto',\n disabled = false,\n className,\n style,\n}: LanguageSwitcherProps) {\n // Endonyms never change with the app's UI language, so this memo is keyed on\n // the list alone — switching locale does not invalidate it.\n const items = useMemo(() => {\n const labels = getLabel ? null : buildLabels(locales);\n return locales.map((locale) => ({\n locale,\n label: getLabel ? getLabel(locale) : labels!.get(locale) ?? locale,\n }));\n }, [locales, getLabel]);\n\n // Zero-cost absence, not a disabled control: a realm with one configured\n // locale has no choice to offer, and an inert affordance would imply it does.\n // Guarded AFTER the hooks above so hook order stays stable when a locale list\n // arrives asynchronously and grows from one entry to several.\n if (locales.length <= 1) return null;\n\n const selected = items.find((item) => item.locale === value) ?? null;\n\n return (\n <Listbox<{ locale: string; label: string }>\n items={items}\n getKey={(item) => item.locale}\n getLabel={(item) => item.label}\n value={selected}\n onChange={(next) => next && onChange(next.locale)}\n disabled={disabled}\n searchable={searchable}\n searchPlaceholder={searchLabel}\n searchAriaLabel={searchLabel}\n matchAnchorWidth={false}\n minPanelWidth={180}\n placement=\"bottom-end\"\n className={cn('uxm-language-switcher', className)}\n aria-label={label}\n renderTrigger={({ open, triggerProps }) => (\n <button\n {...triggerProps}\n type=\"button\"\n className={cn(\n 'uxm-language-switcher__trigger',\n `uxm-language-switcher__trigger--${variant}`,\n open && 'uxm-language-switcher__trigger--open',\n )}\n style={style}\n aria-label={label}\n >\n <Icon glyph=\"globe\" size={16} aria-hidden=\"true\" />\n {variant === 'full' && (\n <span className=\"uxm-language-switcher__current\">\n {selected?.label ?? value}\n </span>\n )}\n <span className=\"uxm-language-switcher__caret\" aria-hidden=\"true\">\n <Icon\n glyph=\"chevron-down\"\n size={12}\n strokeWidth={2.2}\n style={{ transform: open ? 'rotate(180deg)' : 'none' }}\n />\n </span>\n </button>\n )}\n renderItem={(item) => (\n <span className=\"uxm-language-switcher__row\" lang={item.locale}>\n {item.label}\n </span>\n )}\n />\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var locale_exports = {};
|
|
20
|
+
__export(locale_exports, {
|
|
21
|
+
DEFAULT_UXM_LOCALE: () => import_locale.DEFAULT_UXM_LOCALE,
|
|
22
|
+
UxmLocaleProvider: () => import_locale.UxmLocaleProvider,
|
|
23
|
+
useUxmLocale: () => import_locale.useUxmLocale
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(locale_exports);
|
|
26
|
+
var import_locale = require("./locale.cjs");
|
|
27
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
28
|
+
0 && (module.exports = {
|
|
29
|
+
DEFAULT_UXM_LOCALE,
|
|
30
|
+
UxmLocaleProvider,
|
|
31
|
+
useUxmLocale
|
|
32
|
+
});
|
|
33
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/ui/locale/index.ts"],"sourcesContent":["export { DEFAULT_UXM_LOCALE, UxmLocaleProvider, useUxmLocale } from './locale';\nexport type { UxmLocaleProviderProps } from './locale';\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAoE;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/locale/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC/E,YAAY,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/ui/locale/index.ts"],"sourcesContent":["export { DEFAULT_UXM_LOCALE, UxmLocaleProvider, useUxmLocale } from './locale';\nexport type { UxmLocaleProviderProps } from './locale';\n"],"mappings":"AAAA,SAAS,oBAAoB,mBAAmB,oBAAoB;","names":[]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var locale_exports = {};
|
|
20
|
+
__export(locale_exports, {
|
|
21
|
+
DEFAULT_UXM_LOCALE: () => DEFAULT_UXM_LOCALE,
|
|
22
|
+
UxmLocaleProvider: () => UxmLocaleProvider,
|
|
23
|
+
useUxmLocale: () => useUxmLocale
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(locale_exports);
|
|
26
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
27
|
+
var import_react = require("react");
|
|
28
|
+
const DEFAULT_UXM_LOCALE = "en-US";
|
|
29
|
+
const UxmLocaleContext = (0, import_react.createContext)(void 0);
|
|
30
|
+
function UxmLocaleProvider({ locale, children }) {
|
|
31
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(UxmLocaleContext.Provider, { value: locale, children });
|
|
32
|
+
}
|
|
33
|
+
function useUxmLocale(explicit) {
|
|
34
|
+
const fromContext = (0, import_react.useContext)(UxmLocaleContext);
|
|
35
|
+
return explicit ?? fromContext ?? DEFAULT_UXM_LOCALE;
|
|
36
|
+
}
|
|
37
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
38
|
+
0 && (module.exports = {
|
|
39
|
+
DEFAULT_UXM_LOCALE,
|
|
40
|
+
UxmLocaleProvider,
|
|
41
|
+
useUxmLocale
|
|
42
|
+
});
|
|
43
|
+
//# sourceMappingURL=locale.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/ui/locale/locale.tsx"],"sourcesContent":["import { createContext, useContext } from 'react';\n\nimport type { ReactNode } from 'react';\n\n/**\n * Locale used by every UXM atom that formats dates, numbers, or units when\n * neither a `locale` prop nor a surrounding {@link UxmLocaleProvider} says\n * otherwise. Matches the English defaults baked into the atoms' label props,\n * so an app that localises nothing keeps its current rendering.\n */\nexport const DEFAULT_UXM_LOCALE = 'en-US';\n\nconst UxmLocaleContext = createContext<string | undefined>(undefined);\n\nexport interface UxmLocaleProviderProps {\n /** BCP-47 tag — e.g. `\"uk-UA\"`, `\"de-DE\"`. Passed straight to `Intl`. */\n locale: string;\n children?: ReactNode;\n}\n\n/**\n * Broadcasts one BCP-47 locale to every UXM atom below it.\n *\n * This is deliberately **not** an i18n engine — UXM ships no message\n * catalogue and no translation runtime. Translated *copy* stays the\n * consumer's job and arrives through each atom's label props (`clearLabel`,\n * `labels`, …). What this provider carries is the one thing those props\n * cannot express: which locale `Intl` should format with, so month names,\n * decimal separators, and byte units follow the app instead of defaulting to\n * US English.\n *\n * Mount it once near the app root; the atoms that need it read it themselves:\n *\n * ```tsx\n * <UxmLocaleProvider locale=\"uk-UA\">\n * <App />\n * </UxmLocaleProvider>\n * ```\n *\n * A `locale` prop on an individual atom still wins over the provider, so a\n * single always-USD amount or an always-ISO date can opt out locally.\n */\nexport function UxmLocaleProvider({ locale, children }: UxmLocaleProviderProps) {\n return <UxmLocaleContext.Provider value={locale}>{children}</UxmLocaleContext.Provider>;\n}\n\n/**\n * Resolve the locale an atom should format with, in precedence order:\n * explicit prop → nearest {@link UxmLocaleProvider} → {@link DEFAULT_UXM_LOCALE}.\n *\n * Atoms take `locale?: string` and pass it straight in, which keeps the\n * per-instance override working exactly as it did before the provider existed:\n *\n * ```tsx\n * const locale = useUxmLocale(localeProp);\n * ```\n */\nexport function useUxmLocale(explicit?: string): string {\n const fromContext = useContext(UxmLocaleContext);\n return explicit ?? fromContext ?? DEFAULT_UXM_LOCALE;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2CS;AA3CT,mBAA0C;AAUnC,MAAM,qBAAqB;AAElC,MAAM,uBAAmB,4BAAkC,MAAS;AA8B7D,SAAS,kBAAkB,EAAE,QAAQ,SAAS,GAA2B;AAC9E,SAAO,4CAAC,iBAAiB,UAAjB,EAA0B,OAAO,QAAS,UAAS;AAC7D;AAaO,SAAS,aAAa,UAA2B;AACtD,QAAM,kBAAc,yBAAW,gBAAgB;AAC/C,SAAO,YAAY,eAAe;AACpC;","names":[]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Locale used by every UXM atom that formats dates, numbers, or units when
|
|
4
|
+
* neither a `locale` prop nor a surrounding {@link UxmLocaleProvider} says
|
|
5
|
+
* otherwise. Matches the English defaults baked into the atoms' label props,
|
|
6
|
+
* so an app that localises nothing keeps its current rendering.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DEFAULT_UXM_LOCALE = "en-US";
|
|
9
|
+
export interface UxmLocaleProviderProps {
|
|
10
|
+
/** BCP-47 tag — e.g. `"uk-UA"`, `"de-DE"`. Passed straight to `Intl`. */
|
|
11
|
+
locale: string;
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Broadcasts one BCP-47 locale to every UXM atom below it.
|
|
16
|
+
*
|
|
17
|
+
* This is deliberately **not** an i18n engine — UXM ships no message
|
|
18
|
+
* catalogue and no translation runtime. Translated *copy* stays the
|
|
19
|
+
* consumer's job and arrives through each atom's label props (`clearLabel`,
|
|
20
|
+
* `labels`, …). What this provider carries is the one thing those props
|
|
21
|
+
* cannot express: which locale `Intl` should format with, so month names,
|
|
22
|
+
* decimal separators, and byte units follow the app instead of defaulting to
|
|
23
|
+
* US English.
|
|
24
|
+
*
|
|
25
|
+
* Mount it once near the app root; the atoms that need it read it themselves:
|
|
26
|
+
*
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <UxmLocaleProvider locale="uk-UA">
|
|
29
|
+
* <App />
|
|
30
|
+
* </UxmLocaleProvider>
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* A `locale` prop on an individual atom still wins over the provider, so a
|
|
34
|
+
* single always-USD amount or an always-ISO date can opt out locally.
|
|
35
|
+
*/
|
|
36
|
+
export declare function UxmLocaleProvider({ locale, children }: UxmLocaleProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the locale an atom should format with, in precedence order:
|
|
39
|
+
* explicit prop → nearest {@link UxmLocaleProvider} → {@link DEFAULT_UXM_LOCALE}.
|
|
40
|
+
*
|
|
41
|
+
* Atoms take `locale?: string` and pass it straight in, which keeps the
|
|
42
|
+
* per-instance override working exactly as it did before the provider existed:
|
|
43
|
+
*
|
|
44
|
+
* ```tsx
|
|
45
|
+
* const locale = useUxmLocale(localeProp);
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function useUxmLocale(explicit?: string): string;
|
|
49
|
+
//# sourceMappingURL=locale.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locale.d.ts","sourceRoot":"","sources":["../../../src/ui/locale/locale.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,UAAU,CAAC;AAI1C,MAAM,WAAW,sBAAsB;IACrC,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,sBAAsB,2CAE7E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAGtD"}
|