@magicvr/schema-ui-ui 0.1.1 → 0.1.3
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/components/data-table.js +156 -0
- package/components/ui/async-state.js +29 -0
- package/components/ui/badge.js +21 -0
- package/components/ui/breadcrumbs.js +126 -0
- package/components/ui/button.js +30 -0
- package/components/ui/card.js +16 -0
- package/components/ui/index.d.ts +10 -10
- package/components/ui/index.js +13 -0
- package/components/ui/input.js +8 -0
- package/components/ui/label.js +6 -0
- package/components/ui/skeleton.js +6 -0
- package/components/ui/textarea.js +8 -0
- package/i18n/catalog.d.ts +1 -1
- package/i18n/catalog.js +96 -0
- package/i18n/format.d.ts +1 -1
- package/i18n/format.js +35 -0
- package/i18n/locale.js +79 -0
- package/i18n/messages/en-US.json +954 -0
- package/i18n/messages/zh-CN.json +954 -0
- package/i18n/runtime.d.ts +3 -3
- package/i18n/runtime.js +184 -0
- package/i18n/timezone.js +117 -0
- package/lib/datetime.js +36 -0
- package/lib/utils.js +5 -0
- package/package.json +5 -5
- package/index.js +0 -6223
package/i18n/runtime.d.ts
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* - Exposes `t` / `formatDate` / `formatNumber` to components.
|
|
10
10
|
*/
|
|
11
11
|
import { type ReactNode } from "react";
|
|
12
|
-
import { type MessageParams } from "./catalog";
|
|
13
|
-
import { type Locale, type LocalePreference } from "./locale";
|
|
14
|
-
import { type TimezonePreference } from "./timezone";
|
|
12
|
+
import { type MessageParams } from "./catalog.js";
|
|
13
|
+
import { type Locale, type LocalePreference } from "./locale.js";
|
|
14
|
+
import { type TimezonePreference } from "./timezone.js";
|
|
15
15
|
export declare const LOCALE_STORAGE_KEY = "schema-ui:locale";
|
|
16
16
|
export declare function readStoredLocale(): string | null;
|
|
17
17
|
export declare function writeStoredLocale(preference: LocalePreference): void;
|
package/i18n/runtime.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* I18n React runtime (S1 · C4/C5).
|
|
4
|
+
*
|
|
5
|
+
* - Resolves the effective locale via `resolveLocale` (frozen priority).
|
|
6
|
+
* - Persists the user's explicit choice in localStorage["schema-ui:locale"]
|
|
7
|
+
* (single channel, same pattern as the theme mechanism; login/logout never
|
|
8
|
+
* clears it — D-002 §I-L10N-002).
|
|
9
|
+
* - Applies `document.documentElement.lang` on locale change.
|
|
10
|
+
* - Exposes `t` / `formatDate` / `formatNumber` to components.
|
|
11
|
+
*/
|
|
12
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
|
|
13
|
+
import { createTranslator } from "./catalog.js";
|
|
14
|
+
import { formatDate as formatDateImpl, formatNumber as formatNumberImpl } from "./format.js";
|
|
15
|
+
import { defaultBrowserLanguages, DEFAULT_LOCALE, normalizePreference, resolveLocale, } from "./locale.js";
|
|
16
|
+
import { AUTO_TIMEZONE, detectBrowserTimezone, normalizeTimezonePreference, readStoredTimezone, resolveEffectiveTimezone, writeStoredTimezone, } from "./timezone.js";
|
|
17
|
+
export const LOCALE_STORAGE_KEY = "schema-ui:locale";
|
|
18
|
+
export function readStoredLocale() {
|
|
19
|
+
try {
|
|
20
|
+
if (typeof localStorage === "undefined") {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return localStorage.getItem(LOCALE_STORAGE_KEY);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Privacy mode / disabled storage throws SecurityError — same posture as
|
|
27
|
+
// tokens.ts / theme.ts. Never let locale boot white-screen the tree.
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function writeStoredLocale(preference) {
|
|
32
|
+
try {
|
|
33
|
+
if (typeof localStorage === "undefined") {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (preference === "auto") {
|
|
37
|
+
localStorage.removeItem(LOCALE_STORAGE_KEY);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
localStorage.setItem(LOCALE_STORAGE_KEY, preference);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Best-effort persist; the in-memory preference still applies this page.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Applies the effective locale to <html lang>. No-op outside a browser. */
|
|
48
|
+
export function applyLocaleToDocument(locale) {
|
|
49
|
+
if (typeof document !== "undefined") {
|
|
50
|
+
document.documentElement.lang = locale;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// ── active-locale registry ────────────────────────────────────────────────────
|
|
54
|
+
// Module-level current locale for non-React consumers (API fetchers attach it
|
|
55
|
+
// as Accept-Language so the server negotiates the same language — VP-007 S4).
|
|
56
|
+
let activeLocale = "en-US";
|
|
57
|
+
/** Returns the provider's currently effective locale (defaults en-US). */
|
|
58
|
+
export function getActiveLocale() {
|
|
59
|
+
return activeLocale;
|
|
60
|
+
}
|
|
61
|
+
/** Internal: keeps the registry in sync with the provider's effective locale. */
|
|
62
|
+
export function setActiveLocale(locale) {
|
|
63
|
+
activeLocale = locale;
|
|
64
|
+
}
|
|
65
|
+
const I18nContext = createContext(null);
|
|
66
|
+
export function I18nProvider({ children, systemDefault = null, stored, browserLanguages, siteTimezone = null, storedTimezone, detectTimezone, siteDefaultCurrency = null, systemDefaultUrl, }) {
|
|
67
|
+
const [preference, setPreferenceState] = useState(() => {
|
|
68
|
+
const raw = stored !== undefined ? stored : readStoredLocale();
|
|
69
|
+
return normalizePreference(raw);
|
|
70
|
+
});
|
|
71
|
+
const [fetchedSystemDefault, setFetchedSystemDefault] = useState(null);
|
|
72
|
+
const [fetchedSiteTimezone, setFetchedSiteTimezone] = useState(null);
|
|
73
|
+
const [fetchedSiteDefaultCurrency, setFetchedSiteDefaultCurrency] = useState(null);
|
|
74
|
+
const [timezonePreference, setTimezonePreferenceState] = useState(() => {
|
|
75
|
+
const raw = storedTimezone !== undefined ? storedTimezone : readStoredTimezone();
|
|
76
|
+
return normalizeTimezonePreference(raw);
|
|
77
|
+
});
|
|
78
|
+
const browserList = browserLanguages !== undefined ? browserLanguages : defaultBrowserLanguages();
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
if (systemDefaultUrl === undefined) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
let cancelled = false;
|
|
84
|
+
fetch(systemDefaultUrl)
|
|
85
|
+
.then((response) => (response.ok ? response.json() : null))
|
|
86
|
+
.then((body) => {
|
|
87
|
+
if (cancelled) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const record = body;
|
|
91
|
+
const locale = typeof record?.defaultLocale === "string" ? record.defaultLocale : null;
|
|
92
|
+
setFetchedSystemDefault(locale === "auto" ? null : locale);
|
|
93
|
+
const zone = typeof record?.siteTimezone === "string" ? record.siteTimezone : null;
|
|
94
|
+
setFetchedSiteTimezone(zone === "auto" ? null : zone);
|
|
95
|
+
const currency = typeof record?.defaultCurrency === "string" ? record.defaultCurrency.trim().toUpperCase() : null;
|
|
96
|
+
setFetchedSiteDefaultCurrency(currency === "" || currency === null ? null : currency);
|
|
97
|
+
})
|
|
98
|
+
.catch(() => {
|
|
99
|
+
if (!cancelled) {
|
|
100
|
+
setFetchedSystemDefault(null);
|
|
101
|
+
setFetchedSiteTimezone(null);
|
|
102
|
+
setFetchedSiteDefaultCurrency(null);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
return () => {
|
|
106
|
+
cancelled = true;
|
|
107
|
+
};
|
|
108
|
+
}, [systemDefaultUrl]);
|
|
109
|
+
const effectiveSystemDefault = systemDefault ?? fetchedSystemDefault;
|
|
110
|
+
const effectiveSiteTimezone = siteTimezone ?? fetchedSiteTimezone;
|
|
111
|
+
const effectiveSiteDefaultCurrency = siteDefaultCurrency ?? fetchedSiteDefaultCurrency;
|
|
112
|
+
const locale = useMemo(() => resolveLocale({
|
|
113
|
+
stored: preference === "auto" ? null : preference,
|
|
114
|
+
systemDefault: effectiveSystemDefault,
|
|
115
|
+
browserLanguages: browserList,
|
|
116
|
+
}), [preference, effectiveSystemDefault, browserList]);
|
|
117
|
+
const timezone = useMemo(() => resolveEffectiveTimezone({
|
|
118
|
+
stored: timezonePreference === AUTO_TIMEZONE ? null : timezonePreference,
|
|
119
|
+
siteDefault: effectiveSiteTimezone,
|
|
120
|
+
detect: detectTimezone ?? detectBrowserTimezone,
|
|
121
|
+
}), [timezonePreference, effectiveSiteTimezone, detectTimezone]);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
applyLocaleToDocument(locale);
|
|
124
|
+
setActiveLocale(locale);
|
|
125
|
+
}, [locale]);
|
|
126
|
+
const setPreference = useCallback((next) => {
|
|
127
|
+
writeStoredLocale(next);
|
|
128
|
+
setPreferenceState(next);
|
|
129
|
+
}, []);
|
|
130
|
+
const setTimezonePreference = useCallback((next) => {
|
|
131
|
+
writeStoredTimezone(next);
|
|
132
|
+
setTimezonePreferenceState(next);
|
|
133
|
+
}, []);
|
|
134
|
+
const t = useMemo(() => createTranslator(locale), [locale]);
|
|
135
|
+
const formatDate = useCallback((value, options = {}) => {
|
|
136
|
+
const requested = options.timeZone !== undefined && options.timeZone !== "" ? options.timeZone : null;
|
|
137
|
+
const zone = requested ?? (timezone === AUTO_TIMEZONE ? null : timezone);
|
|
138
|
+
return formatDateImpl(value, locale, zone === null ? {} : { timeZone: zone });
|
|
139
|
+
}, [locale, timezone]);
|
|
140
|
+
const formatNumber = useCallback((value, options) => formatNumberImpl(value, locale, options), [locale]);
|
|
141
|
+
const value = useMemo(() => ({
|
|
142
|
+
locale,
|
|
143
|
+
preference,
|
|
144
|
+
setPreference,
|
|
145
|
+
timezone,
|
|
146
|
+
timezonePreference,
|
|
147
|
+
setTimezonePreference,
|
|
148
|
+
defaultCurrency: effectiveSiteDefaultCurrency ?? "",
|
|
149
|
+
t,
|
|
150
|
+
formatDate,
|
|
151
|
+
formatNumber,
|
|
152
|
+
}), [
|
|
153
|
+
locale,
|
|
154
|
+
preference,
|
|
155
|
+
setPreference,
|
|
156
|
+
timezone,
|
|
157
|
+
timezonePreference,
|
|
158
|
+
setTimezonePreference,
|
|
159
|
+
effectiveSiteDefaultCurrency,
|
|
160
|
+
t,
|
|
161
|
+
formatDate,
|
|
162
|
+
formatNumber,
|
|
163
|
+
]);
|
|
164
|
+
return _jsx(I18nContext.Provider, { value: value, children: children });
|
|
165
|
+
}
|
|
166
|
+
export function useI18n() {
|
|
167
|
+
const value = useContext(I18nContext);
|
|
168
|
+
if (value === null) {
|
|
169
|
+
throw new Error("useI18n must be used within an I18nProvider");
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Tolerant translator hook for deep renderer internals.
|
|
175
|
+
*
|
|
176
|
+
* Returns the provider's translator, or a safe default (en-US resolution +
|
|
177
|
+
* missing-key observable fallback) when no provider is mounted. Production
|
|
178
|
+
* always mounts I18nProvider; bare component tests and pre-provider surfaces
|
|
179
|
+
* degrade to the documented safe fallback instead of throwing.
|
|
180
|
+
*/
|
|
181
|
+
export function useTranslate() {
|
|
182
|
+
const value = useContext(I18nContext);
|
|
183
|
+
return useMemo(() => value?.t ?? createTranslator(DEFAULT_LOCALE), [value]);
|
|
184
|
+
}
|
package/i18n/timezone.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timezone pure-logic unit (workspace-020 · R2 · C1).
|
|
3
|
+
*
|
|
4
|
+
* `resolveEffectiveTimezone` computes the effective timezone from the user's
|
|
5
|
+
* explicit override, the session probe, and the site default, per contract
|
|
6
|
+
* GOAL-002 D-001 §2 (user-confirmed I-001, 2026-08-26):
|
|
7
|
+
*
|
|
8
|
+
* L1 user override (localStorage "schema-ui:timezone") → L2 session probe
|
|
9
|
+
* (Intl) → L3 site default (siteTimezone) → L4 "auto" fallback
|
|
10
|
+
*
|
|
11
|
+
* Keeping the decision logic in plain functions lets vitest exercise every
|
|
12
|
+
* branch; the probe is injectable so tests do not depend on the host zone.
|
|
13
|
+
*/
|
|
14
|
+
export const TIMEZONE_STORAGE_KEY = "schema-ui:timezone";
|
|
15
|
+
export const AUTO_TIMEZONE = "auto";
|
|
16
|
+
/**
|
|
17
|
+
* Validates an IANA timezone name via Intl (RangeError on invalid names).
|
|
18
|
+
* Side-effect-free apart from the noexcept Intl probe; returns false for
|
|
19
|
+
* empty / non-string / unknown zones instead of throwing.
|
|
20
|
+
*/
|
|
21
|
+
export function isValidIanaTimeZone(raw) {
|
|
22
|
+
if (typeof raw !== "string") {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const trimmed = raw.trim();
|
|
26
|
+
if (trimmed === "") {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
// The format call itself never renders; constructing with the option is
|
|
31
|
+
// the validation probe (RangeError for unknown "Foo/Bar" style names).
|
|
32
|
+
new Intl.DateTimeFormat("en-US", { timeZone: trimmed });
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Normalizes a raw stored/site value into a TimezonePreference.
|
|
41
|
+
* Any value that is not a valid IANA name resolves to "auto".
|
|
42
|
+
*/
|
|
43
|
+
export function normalizeTimezonePreference(raw) {
|
|
44
|
+
if (typeof raw !== "string") {
|
|
45
|
+
return AUTO_TIMEZONE;
|
|
46
|
+
}
|
|
47
|
+
const trimmed = raw.trim();
|
|
48
|
+
if (trimmed === "" || trimmed.toLowerCase() === AUTO_TIMEZONE) {
|
|
49
|
+
return AUTO_TIMEZONE;
|
|
50
|
+
}
|
|
51
|
+
return isValidIanaTimeZone(trimmed) ? trimmed : AUTO_TIMEZONE;
|
|
52
|
+
}
|
|
53
|
+
/** Reads the stored user override; best-effort (privacy mode → null). */
|
|
54
|
+
export function readStoredTimezone() {
|
|
55
|
+
try {
|
|
56
|
+
if (typeof localStorage === "undefined") {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return localStorage.getItem(TIMEZONE_STORAGE_KEY);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Persists the user override; "auto" removes the key (single channel). */
|
|
66
|
+
export function writeStoredTimezone(preference) {
|
|
67
|
+
try {
|
|
68
|
+
if (typeof localStorage === "undefined") {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (preference === AUTO_TIMEZONE) {
|
|
72
|
+
localStorage.removeItem(TIMEZONE_STORAGE_KEY);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
localStorage.setItem(TIMEZONE_STORAGE_KEY, preference);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Best-effort persist; the in-memory preference still applies this page.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Real session probe: the host zone from Intl.resolvedOptions(). */
|
|
83
|
+
export function detectBrowserTimezone() {
|
|
84
|
+
try {
|
|
85
|
+
const zone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
86
|
+
return typeof zone === "string" ? zone : "";
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return "";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolves the effective timezone per contract §2 (L1 → L2 → L3 → L4).
|
|
94
|
+
* Returns an IANA name, or "auto" when nothing is configured/detectable —
|
|
95
|
+
* consumers then fall back to the locale's default zone.
|
|
96
|
+
*/
|
|
97
|
+
export function resolveEffectiveTimezone(input) {
|
|
98
|
+
const stored = normalizeTimezonePreference(input.stored);
|
|
99
|
+
if (stored !== AUTO_TIMEZONE) {
|
|
100
|
+
return stored; // L1 · user override wins
|
|
101
|
+
}
|
|
102
|
+
let detected = "";
|
|
103
|
+
try {
|
|
104
|
+
detected = String(input.detect() ?? "");
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
detected = "";
|
|
108
|
+
}
|
|
109
|
+
if (isValidIanaTimeZone(detected)) {
|
|
110
|
+
return detected; // L2 · session probe
|
|
111
|
+
}
|
|
112
|
+
const site = normalizeTimezonePreference(input.siteDefault);
|
|
113
|
+
if (site !== AUTO_TIMEZONE) {
|
|
114
|
+
return site; // L3 · site default
|
|
115
|
+
}
|
|
116
|
+
return AUTO_TIMEZONE; // L4 · embedded default
|
|
117
|
+
}
|
package/lib/datetime.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display-time formatting (GOAL-011 table style): ISO-8601 timestamps are
|
|
3
|
+
* rendered in a human-readable local-time form ("2026-08-01 18:02") instead
|
|
4
|
+
* of the raw wire value ("2026-08-01T18:02:44.000Z").
|
|
5
|
+
*
|
|
6
|
+
* Returns null when the value is not a renderable timestamp so callers fall
|
|
7
|
+
* back to the raw text (numbers, booleans, plain strings, arbitrary dates
|
|
8
|
+
* with other shapes stay untouched).
|
|
9
|
+
*/
|
|
10
|
+
/** Matches the ISO-8601 timestamps shipped by the API (UTC Z or ±hh:mm). */
|
|
11
|
+
const ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/;
|
|
12
|
+
function pad2(value) {
|
|
13
|
+
return String(value).padStart(2, "0");
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Formats an ISO-8601 timestamp string to local "YYYY-MM-DD HH:mm". Returns
|
|
17
|
+
* null for non-timestamp values, malformed dates, or non-string input.
|
|
18
|
+
*/
|
|
19
|
+
export function formatDisplayTime(value) {
|
|
20
|
+
if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN.test(value)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const date = new Date(value);
|
|
24
|
+
if (Number.isNaN(date.getTime())) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return (date.getFullYear() +
|
|
28
|
+
"-" +
|
|
29
|
+
pad2(date.getMonth() + 1) +
|
|
30
|
+
"-" +
|
|
31
|
+
pad2(date.getDate()) +
|
|
32
|
+
" " +
|
|
33
|
+
pad2(date.getHours()) +
|
|
34
|
+
":" +
|
|
35
|
+
pad2(date.getMinutes()));
|
|
36
|
+
}
|
package/lib/utils.js
ADDED
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magicvr/schema-ui-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "schema-ui-core 包面(
|
|
6
|
-
"main": "index.js",
|
|
7
|
-
"types": "
|
|
5
|
+
"description": "schema-ui-core 包面(ui)",
|
|
6
|
+
"main": "components/ui/index.js",
|
|
7
|
+
"types": "components/ui/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"types": "
|
|
10
|
+
"types": "components/ui/index.d.ts",
|
|
11
11
|
"import": "./index.js"
|
|
12
12
|
},
|
|
13
13
|
"./*": "./*"
|