@groveback/ui 0.4.0 → 0.7.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/dist/SignIn.d.ts +15 -0
- package/dist/components.d.ts +19 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +178 -69
- package/dist/locale.d.ts +32 -0
- package/dist/messages.d.ts +2 -0
- package/dist/runtime.d.ts +32 -0
- package/package.json +1 -1
package/dist/SignIn.d.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* through the same endpoints, and the same rate limits, as a hand-written form would.
|
|
11
11
|
*/
|
|
12
12
|
import type { ReactNode } from 'react';
|
|
13
|
+
import type { UiMessages } from './messages';
|
|
13
14
|
import type { Doc } from './types';
|
|
14
15
|
/**
|
|
15
16
|
* The slice of the SDK's `client.auth` the gate uses. `login` may resolve with
|
|
@@ -32,6 +33,20 @@ export interface SignInProps {
|
|
|
32
33
|
allowRegister?: boolean | undefined;
|
|
33
34
|
/** Called with the user after a successful sign-in or registration. */
|
|
34
35
|
onSignedIn?: ((user: Doc) => void) | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Language for the form's own words. Optional, and normally unnecessary: under
|
|
38
|
+
* `<GroveRoutes>` the gate reads the shell's locale like every other primitive.
|
|
39
|
+
*
|
|
40
|
+
* It exists for the mount that wraps the WHOLE router — `<SignIn><GroveRoutes /></SignIn>`,
|
|
41
|
+
* the way an app puts every page behind a session. That places the form above the provider
|
|
42
|
+
* carrying the locale, so a Spanish app came up with an English sign-in screen in front of
|
|
43
|
+
* translated pages. Absent, the gate falls back to the provider when there is one and then
|
|
44
|
+
* to the viewer's own languages, so that mount is now translated with no prop at all; pass
|
|
45
|
+
* one to drive it from the app's own switcher.
|
|
46
|
+
*/
|
|
47
|
+
locale?: string | undefined;
|
|
48
|
+
/** Overrides for the form's words, merged over the shipped catalogue (see `messages.ts`). */
|
|
49
|
+
messages?: Partial<UiMessages> | undefined;
|
|
35
50
|
/** What a signed-in viewer sees — normally the app's routes. */
|
|
36
51
|
children: ReactNode;
|
|
37
52
|
}
|
package/dist/components.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export declare function Stack(props: {
|
|
|
37
37
|
}): import("react").JSX.Element;
|
|
38
38
|
export declare function Grid(props: {
|
|
39
39
|
columns: number;
|
|
40
|
+
/** Columns below the breakpoint; defaults to one. */
|
|
41
|
+
columnsMobile?: number;
|
|
40
42
|
gap?: number;
|
|
41
43
|
width?: keyof typeof WIDTH;
|
|
42
44
|
children: ReactNode;
|
|
@@ -58,6 +60,23 @@ export declare function Text({ muted, children }: {
|
|
|
58
60
|
* `href` is still real, which is what keeps middle-click, "open in new tab" and crawlers
|
|
59
61
|
* working — the click handler only takes over the plain-left-click case.
|
|
60
62
|
*/
|
|
63
|
+
/**
|
|
64
|
+
* The control a viewer switches language with.
|
|
65
|
+
*
|
|
66
|
+
* Renders nothing unless the app actually offers a choice: fewer than two document locales, or
|
|
67
|
+
* no `setLocale` on the context (an app driving `locale` as a fixed prop owns it — see
|
|
68
|
+
* `GroveRoutes`). So a monolingual app looks exactly as it did before this existed.
|
|
69
|
+
*
|
|
70
|
+
* It offers EVERY locale the document declares, not the ones this package ships chrome for.
|
|
71
|
+
* The screens' own words are the substance of the page and are translated into all of them;
|
|
72
|
+
* hiding a language because the package has no "Save" in it would hide a page that is fully
|
|
73
|
+
* translated apart from one button. Chrome the catalogue lacks degrades to English, which is
|
|
74
|
+
* a word rather than a blank — and the `messages` override is the documented way out (D86).
|
|
75
|
+
*
|
|
76
|
+
* A native `<select>`: keyboard and screen-reader behaviour for free, and a phone renders its
|
|
77
|
+
* own picker instead of a menu that has to be re-implemented for touch.
|
|
78
|
+
*/
|
|
79
|
+
export declare function LocaleSwitcher(): import("react").JSX.Element | null;
|
|
61
80
|
export declare function Header(props: {
|
|
62
81
|
brand?: ReactNode;
|
|
63
82
|
brandHref?: string | undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -10,14 +10,15 @@
|
|
|
10
10
|
* convention — this package is browser React bundled by `bun build`, not backend ESM, so the
|
|
11
11
|
* convention buys nothing and extensionless is what every bundler agrees on.
|
|
12
12
|
*/
|
|
13
|
-
import { Button, DataTable, Detail, Divider, Form, Grid, Header, Heading, Stack, Text } from './components';
|
|
13
|
+
import { Button, DataTable, Detail, Divider, Form, Grid, Header, Heading, LocaleSwitcher, Stack, Text } from './components';
|
|
14
|
+
import { localeLabel } from './locale';
|
|
14
15
|
import { MarkdownField, renderMarkdown } from './MarkdownField';
|
|
15
16
|
import { RelationSelect } from './RelationSelect';
|
|
16
17
|
import { GroveRoutes, RouteIndex, matchRoute, resolveRoute } from './routes';
|
|
17
18
|
import { SignIn } from './SignIn';
|
|
18
19
|
import { uiMessagesFor } from './messages';
|
|
19
|
-
import { GroveUiProvider, detectLocale, formatValue, resolveHref, useAction, useGroveUi, useMessages, usePageTitle, useUiMessages, useUiText } from './runtime';
|
|
20
|
-
export { Button, DataTable, Detail, Divider, Form, Grid, GroveRoutes, GroveUiProvider, Header, Heading, MarkdownField, renderMarkdown, RelationSelect, RouteIndex, SignIn, Stack, Text, detectLocale, formatValue, matchRoute, resolveHref, resolveRoute, useAction, useGroveUi, useMessages, usePageTitle, uiMessagesFor, useUiMessages, useUiText, };
|
|
20
|
+
import { GroveUiProvider, detectLocale, formatValue, resolveHref, useAction, useGroveUi, useMessages, usePageTitle, useStandaloneUiText, useUiMessages, useUiText } from './runtime';
|
|
21
|
+
export { Button, DataTable, Detail, Divider, Form, Grid, GroveRoutes, GroveUiProvider, Header, Heading, LocaleSwitcher, MarkdownField, renderMarkdown, RelationSelect, RouteIndex, SignIn, Stack, Text, detectLocale, formatValue, localeLabel, matchRoute, resolveHref, resolveRoute, useAction, useGroveUi, useMessages, usePageTitle, useStandaloneUiText, uiMessagesFor, useUiMessages, useUiText, };
|
|
21
22
|
export type { RelationSelectProps } from './RelationSelect';
|
|
22
23
|
export type { GroveUiContext } from './runtime';
|
|
23
24
|
export type { UiMessageKey, UiMessages } from './messages';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
// src/components.tsx
|
|
2
2
|
import { useCallback as useCallback2, useEffect as useEffect3, useRef as useRef2, useState as useState3 } from "react";
|
|
3
3
|
|
|
4
|
+
// src/locale.ts
|
|
5
|
+
var LOCALE_STORAGE_KEY = "grove.locale";
|
|
6
|
+
function readStoredLocale(available) {
|
|
7
|
+
if (typeof window === "undefined")
|
|
8
|
+
return;
|
|
9
|
+
let stored = null;
|
|
10
|
+
try {
|
|
11
|
+
stored = window.localStorage.getItem(LOCALE_STORAGE_KEY);
|
|
12
|
+
} catch {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (stored === null)
|
|
16
|
+
return;
|
|
17
|
+
return available.includes(stored) ? stored : undefined;
|
|
18
|
+
}
|
|
19
|
+
function storeLocale(locale) {
|
|
20
|
+
if (typeof window === "undefined")
|
|
21
|
+
return;
|
|
22
|
+
try {
|
|
23
|
+
window.localStorage.setItem(LOCALE_STORAGE_KEY, locale);
|
|
24
|
+
} catch {}
|
|
25
|
+
}
|
|
26
|
+
function localeLabel(locale) {
|
|
27
|
+
try {
|
|
28
|
+
const names = new Intl.DisplayNames([locale], { type: "language" });
|
|
29
|
+
const label = names.of(locale);
|
|
30
|
+
if (label && label.toLowerCase() !== locale.toLowerCase()) {
|
|
31
|
+
return label.charAt(0).toUpperCase() + label.slice(1);
|
|
32
|
+
}
|
|
33
|
+
} catch {}
|
|
34
|
+
return locale;
|
|
35
|
+
}
|
|
36
|
+
|
|
4
37
|
// src/MarkdownField.tsx
|
|
5
38
|
import { useState } from "react";
|
|
6
39
|
|
|
@@ -17,6 +50,7 @@ var EN = {
|
|
|
17
50
|
"common.no": "No",
|
|
18
51
|
"header.openMenu": "Open menu",
|
|
19
52
|
"header.closeMenu": "Close menu",
|
|
53
|
+
"locale.label": "Language",
|
|
20
54
|
"table.empty": "Nothing here yet.",
|
|
21
55
|
"form.save": "Save",
|
|
22
56
|
"form.saving": "Saving…",
|
|
@@ -62,6 +96,7 @@ var ES = {
|
|
|
62
96
|
"common.no": "No",
|
|
63
97
|
"header.openMenu": "Abrir menú",
|
|
64
98
|
"header.closeMenu": "Cerrar menú",
|
|
99
|
+
"locale.label": "Idioma",
|
|
65
100
|
"table.empty": "Todavía no hay nada.",
|
|
66
101
|
"form.save": "Guardar",
|
|
67
102
|
"form.saving": "Guardando…",
|
|
@@ -138,6 +173,12 @@ function useUiMessages() {
|
|
|
138
173
|
const ctx = useGroveUi();
|
|
139
174
|
return useMemo(() => ({ ...uiMessagesFor(ctx.locale), ...ctx.messages }), [ctx]);
|
|
140
175
|
}
|
|
176
|
+
function useStandaloneUiText(locale, overrides) {
|
|
177
|
+
const ctx = useGroveUi();
|
|
178
|
+
const active = locale ?? ctx.locale ?? detectLocale(Object.keys(UI_LOCALES));
|
|
179
|
+
const messages = { ...ctx.messages, ...overrides };
|
|
180
|
+
return useCallback((key) => messages[key] ?? uiMessagesFor(active)[key], [active, ctx.messages, overrides]);
|
|
181
|
+
}
|
|
141
182
|
function resolveHref(href, doc) {
|
|
142
183
|
if (!doc)
|
|
143
184
|
return href;
|
|
@@ -538,6 +579,7 @@ import { jsx as jsx4, jsxs as jsxs3, Fragment } from "react/jsx-runtime";
|
|
|
538
579
|
var WIDTH_CLASS = { narrow: "w-[12%]", normal: "", wide: "w-[45%]" };
|
|
539
580
|
var GAP = ["gap-0", "gap-1", "gap-2", "gap-3", "gap-4", "gap-5", "gap-6", "gap-7", "gap-8", "gap-9", "gap-10", "gap-11", "gap-12"];
|
|
540
581
|
var COLUMNS = ["", "md:grid-cols-1", "md:grid-cols-2", "md:grid-cols-3", "md:grid-cols-4", "md:grid-cols-5", "md:grid-cols-6"];
|
|
582
|
+
var BASE_COLUMNS = ["", "grid-cols-1", "grid-cols-2", "grid-cols-3", "grid-cols-4", "grid-cols-5", "grid-cols-6"];
|
|
541
583
|
var WIDTH = { full: "w-full", container: "w-full max-w-5xl mx-auto", narrow: "w-full max-w-xl mx-auto" };
|
|
542
584
|
var ALIGN = { start: "items-start", center: "items-center", end: "items-end", stretch: "items-stretch" };
|
|
543
585
|
var JUSTIFY = { start: "justify-start", center: "justify-center", end: "justify-end", between: "justify-between" };
|
|
@@ -549,9 +591,13 @@ function Stack(props) {
|
|
|
549
591
|
});
|
|
550
592
|
}
|
|
551
593
|
function Grid(props) {
|
|
552
|
-
const { columns, gap = 4, width, children } = props;
|
|
594
|
+
const { columns, columnsMobile, gap = 4, width, children } = props;
|
|
595
|
+
const mobile = Math.min(Math.max(columnsMobile ?? 1, 1), 6);
|
|
596
|
+
const desktop = Math.min(Math.max(columns, 1), 6);
|
|
553
597
|
return /* @__PURE__ */ jsx4("div", {
|
|
554
|
-
|
|
598
|
+
"data-grove-cols": desktop,
|
|
599
|
+
"data-grove-cols-mobile": mobile,
|
|
600
|
+
className: cx("grid", BASE_COLUMNS[mobile], COLUMNS[desktop], GAP[Math.min(Math.max(gap, 0), 12)], width && WIDTH[width]),
|
|
555
601
|
children
|
|
556
602
|
});
|
|
557
603
|
}
|
|
@@ -574,11 +620,30 @@ function Text({ muted, children }) {
|
|
|
574
620
|
children
|
|
575
621
|
});
|
|
576
622
|
}
|
|
623
|
+
function LocaleSwitcher() {
|
|
624
|
+
const { locale, locales, setLocale } = useGroveUi();
|
|
625
|
+
const t = useUiText();
|
|
626
|
+
if (!setLocale || !locales || locales.length < 2)
|
|
627
|
+
return null;
|
|
628
|
+
return /* @__PURE__ */ jsx4("select", {
|
|
629
|
+
"data-grove-locale": true,
|
|
630
|
+
"aria-label": t("locale.label"),
|
|
631
|
+
value: locale ?? locales[0],
|
|
632
|
+
onChange: (e) => setLocale(e.target.value),
|
|
633
|
+
className: cx(INPUT_CLASS, "py-1 pr-7 text-xs"),
|
|
634
|
+
children: locales.map((tag) => /* @__PURE__ */ jsx4("option", {
|
|
635
|
+
value: tag,
|
|
636
|
+
children: localeLabel(tag)
|
|
637
|
+
}, tag))
|
|
638
|
+
});
|
|
639
|
+
}
|
|
577
640
|
function Header(props) {
|
|
578
641
|
const { brand, brandHref = "/", links, actions, sticky } = props;
|
|
579
642
|
const dispatch = useAction();
|
|
580
643
|
const t = useUiText();
|
|
644
|
+
const { onHeaderMounted } = useGroveUi();
|
|
581
645
|
const [open, setOpen] = useState3(false);
|
|
646
|
+
useEffect3(() => onHeaderMounted?.(), [onHeaderMounted]);
|
|
582
647
|
const go = useCallback2((href) => (e) => {
|
|
583
648
|
if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0)
|
|
584
649
|
return;
|
|
@@ -609,70 +674,76 @@ function Header(props) {
|
|
|
609
674
|
children: link.label
|
|
610
675
|
}, i))
|
|
611
676
|
}) : null,
|
|
612
|
-
|
|
613
|
-
"
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
677
|
+
/* @__PURE__ */ jsxs3("div", {
|
|
678
|
+
className: "ml-auto flex items-center gap-2",
|
|
679
|
+
children: [
|
|
680
|
+
actions && actions.length > 0 ? /* @__PURE__ */ jsx4("div", {
|
|
681
|
+
"data-grove-nav-actions": true,
|
|
682
|
+
className: "hidden items-center gap-2 md:flex",
|
|
683
|
+
children: actions.map((a, i) => /* @__PURE__ */ jsx4(Button, {
|
|
684
|
+
action: a.action,
|
|
685
|
+
variant: a.variant,
|
|
686
|
+
children: a.label
|
|
687
|
+
}, i))
|
|
688
|
+
}) : null,
|
|
689
|
+
/* @__PURE__ */ jsx4(LocaleSwitcher, {}),
|
|
690
|
+
hasNav ? /* @__PURE__ */ jsx4("button", {
|
|
691
|
+
type: "button",
|
|
692
|
+
"data-grove-nav-toggle": true,
|
|
693
|
+
"aria-label": t(open ? "header.closeMenu" : "header.openMenu"),
|
|
694
|
+
"aria-expanded": open,
|
|
695
|
+
onClick: () => setOpen((v) => !v),
|
|
696
|
+
className: "inline-flex items-center justify-center rounded-md p-2 text-neutral-600 hover:bg-neutral-100 hover:text-neutral-900 md:hidden dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-100",
|
|
697
|
+
children: /* @__PURE__ */ jsx4("svg", {
|
|
698
|
+
width: "20",
|
|
699
|
+
height: "20",
|
|
700
|
+
viewBox: "0 0 24 24",
|
|
701
|
+
fill: "none",
|
|
702
|
+
stroke: "currentColor",
|
|
703
|
+
strokeWidth: "2",
|
|
704
|
+
strokeLinecap: "round",
|
|
705
|
+
"aria-hidden": "true",
|
|
706
|
+
children: open ? /* @__PURE__ */ jsxs3(Fragment, {
|
|
707
|
+
children: [
|
|
708
|
+
/* @__PURE__ */ jsx4("line", {
|
|
709
|
+
x1: "18",
|
|
710
|
+
y1: "6",
|
|
711
|
+
x2: "6",
|
|
712
|
+
y2: "18"
|
|
713
|
+
}),
|
|
714
|
+
/* @__PURE__ */ jsx4("line", {
|
|
715
|
+
x1: "6",
|
|
716
|
+
y1: "6",
|
|
717
|
+
x2: "18",
|
|
718
|
+
y2: "18"
|
|
719
|
+
})
|
|
720
|
+
]
|
|
721
|
+
}) : /* @__PURE__ */ jsxs3(Fragment, {
|
|
722
|
+
children: [
|
|
723
|
+
/* @__PURE__ */ jsx4("line", {
|
|
724
|
+
x1: "3",
|
|
725
|
+
y1: "6",
|
|
726
|
+
x2: "21",
|
|
727
|
+
y2: "6"
|
|
728
|
+
}),
|
|
729
|
+
/* @__PURE__ */ jsx4("line", {
|
|
730
|
+
x1: "3",
|
|
731
|
+
y1: "12",
|
|
732
|
+
x2: "21",
|
|
733
|
+
y2: "12"
|
|
734
|
+
}),
|
|
735
|
+
/* @__PURE__ */ jsx4("line", {
|
|
736
|
+
x1: "3",
|
|
737
|
+
y1: "18",
|
|
738
|
+
x2: "21",
|
|
739
|
+
y2: "18"
|
|
740
|
+
})
|
|
741
|
+
]
|
|
671
742
|
})
|
|
672
|
-
|
|
673
|
-
})
|
|
674
|
-
|
|
675
|
-
})
|
|
743
|
+
})
|
|
744
|
+
}) : null
|
|
745
|
+
]
|
|
746
|
+
})
|
|
676
747
|
]
|
|
677
748
|
}),
|
|
678
749
|
hasNav && open ? /* @__PURE__ */ jsxs3("div", {
|
|
@@ -1105,6 +1176,13 @@ function resolveRoute(routes, path) {
|
|
|
1105
1176
|
function GroveRoutes(props) {
|
|
1106
1177
|
const { routes, context, fallback, children, gate } = props;
|
|
1107
1178
|
const [path, setPath] = useState4(() => window.location.pathname);
|
|
1179
|
+
const offered = context?.locales;
|
|
1180
|
+
const pinned = context?.locale;
|
|
1181
|
+
const [chosen, setChosen] = useState4(() => pinned !== undefined || !offered || offered.length === 0 ? undefined : readStoredLocale(offered) ?? detectLocale(offered));
|
|
1182
|
+
const setLocale = useCallback3((next) => {
|
|
1183
|
+
setChosen(next);
|
|
1184
|
+
storeLocale(next);
|
|
1185
|
+
}, []);
|
|
1108
1186
|
useEffect4(() => {
|
|
1109
1187
|
const onPop = () => setPath(window.location.pathname);
|
|
1110
1188
|
window.addEventListener("popstate", onPop);
|
|
@@ -1114,7 +1192,23 @@ function GroveRoutes(props) {
|
|
|
1114
1192
|
window.history.pushState({}, "", href);
|
|
1115
1193
|
setPath(href.split(/[?#]/)[0] ?? href);
|
|
1116
1194
|
}, []);
|
|
1117
|
-
const
|
|
1195
|
+
const active = chosen ?? pinned;
|
|
1196
|
+
useEffect4(() => {
|
|
1197
|
+
if (active !== undefined)
|
|
1198
|
+
document.documentElement.lang = active;
|
|
1199
|
+
}, [active]);
|
|
1200
|
+
const [headers, setHeaders] = useState4(0);
|
|
1201
|
+
const onHeaderMounted = useCallback3(() => {
|
|
1202
|
+
setHeaders((n) => n + 1);
|
|
1203
|
+
return () => setHeaders((n) => n - 1);
|
|
1204
|
+
}, []);
|
|
1205
|
+
const value = useMemo3(() => ({
|
|
1206
|
+
...context,
|
|
1207
|
+
navigate,
|
|
1208
|
+
onHeaderMounted,
|
|
1209
|
+
...chosen !== undefined ? { locale: chosen } : {},
|
|
1210
|
+
...pinned === undefined && offered && offered.length > 1 ? { setLocale } : {}
|
|
1211
|
+
}), [context, navigate, chosen, pinned, offered, setLocale, onHeaderMounted]);
|
|
1118
1212
|
const hit = resolveRoute(routes, path);
|
|
1119
1213
|
const isRoot = (path.replace(/\/+$/, "") || "/") === "/";
|
|
1120
1214
|
const matched = hit ? hit.route.render(hit.params) : fallback ?? (isRoot ? /* @__PURE__ */ jsx5(RouteIndex, {
|
|
@@ -1125,7 +1219,11 @@ function GroveRoutes(props) {
|
|
|
1125
1219
|
value,
|
|
1126
1220
|
children: [
|
|
1127
1221
|
children,
|
|
1128
|
-
screen
|
|
1222
|
+
screen,
|
|
1223
|
+
headers === 0 ? /* @__PURE__ */ jsx5("div", {
|
|
1224
|
+
className: "fixed right-3 bottom-3 z-50",
|
|
1225
|
+
children: /* @__PURE__ */ jsx5(LocaleSwitcher, {})
|
|
1226
|
+
}) : null
|
|
1129
1227
|
]
|
|
1130
1228
|
});
|
|
1131
1229
|
}
|
|
@@ -1191,8 +1289,16 @@ function RouteIndex({ routes }) {
|
|
|
1191
1289
|
import { useCallback as useCallback4, useRef as useRef3, useState as useState5 } from "react";
|
|
1192
1290
|
import { jsx as jsx6, jsxs as jsxs5, Fragment as Fragment2 } from "react/jsx-runtime";
|
|
1193
1291
|
function SignIn(props) {
|
|
1194
|
-
const {
|
|
1195
|
-
|
|
1292
|
+
const {
|
|
1293
|
+
auth,
|
|
1294
|
+
title,
|
|
1295
|
+
allowRegister = typeof auth.register === "function",
|
|
1296
|
+
onSignedIn,
|
|
1297
|
+
locale,
|
|
1298
|
+
messages,
|
|
1299
|
+
children
|
|
1300
|
+
} = props;
|
|
1301
|
+
const t = useStandaloneUiText(locale, messages);
|
|
1196
1302
|
const [signedIn, setSignedIn] = useState5(() => auth.getTokens() !== null);
|
|
1197
1303
|
const [mode, setMode] = useState5("login");
|
|
1198
1304
|
const [email, setEmail] = useState5("");
|
|
@@ -1365,6 +1471,7 @@ export {
|
|
|
1365
1471
|
GroveUiProvider,
|
|
1366
1472
|
Header,
|
|
1367
1473
|
Heading,
|
|
1474
|
+
LocaleSwitcher,
|
|
1368
1475
|
MarkdownField,
|
|
1369
1476
|
RelationSelect,
|
|
1370
1477
|
RouteIndex,
|
|
@@ -1373,6 +1480,7 @@ export {
|
|
|
1373
1480
|
Text,
|
|
1374
1481
|
detectLocale,
|
|
1375
1482
|
formatValue,
|
|
1483
|
+
localeLabel,
|
|
1376
1484
|
matchRoute,
|
|
1377
1485
|
renderMarkdown,
|
|
1378
1486
|
resolveHref,
|
|
@@ -1382,6 +1490,7 @@ export {
|
|
|
1382
1490
|
useGroveUi,
|
|
1383
1491
|
useMessages,
|
|
1384
1492
|
usePageTitle,
|
|
1493
|
+
useStandaloneUiText,
|
|
1385
1494
|
useUiMessages,
|
|
1386
1495
|
useUiText
|
|
1387
1496
|
};
|
package/dist/locale.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remembering the viewer's chosen language.
|
|
3
|
+
*
|
|
4
|
+
* Pure and storage-aware but React-free, so the rules below are testable without a renderer —
|
|
5
|
+
* the same split the dashboard uses between its `i18n.ts` and `i18n-context.tsx`.
|
|
6
|
+
*
|
|
7
|
+
* Two rules matter more than the storage itself:
|
|
8
|
+
* - A stored value is never trusted. It is user-editable, and it outlives the document that
|
|
9
|
+
* produced it: an app that dropped `de` must not keep serving German because a browser
|
|
10
|
+
* still remembers it. Anything not on offer falls back rather than sticking.
|
|
11
|
+
* - Storage is allowed to fail. It throws in a private window and when site data is blocked,
|
|
12
|
+
* and a language preference is never worth taking the page down for.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Where the choice lives. Namespaced under `grove.` like the generated app's own
|
|
16
|
+
* `grove.session`, and deliberately NOT the dashboard's `groveback.ui.locale`: this ships
|
|
17
|
+
* INTO a user's app, which may be served from the same origin as nothing in particular.
|
|
18
|
+
*/
|
|
19
|
+
export declare const LOCALE_STORAGE_KEY = "grove.locale";
|
|
20
|
+
/** The stored choice, if it is still one of `available`. */
|
|
21
|
+
export declare function readStoredLocale(available: readonly string[]): string | undefined;
|
|
22
|
+
/** Remember a choice. A failed write costs the memory, never the switch itself. */
|
|
23
|
+
export declare function storeLocale(locale: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* How a language names ITSELF — "Español", not "Spanish". The person hunting for the switcher
|
|
26
|
+
* is the one who cannot read the language currently on screen, so every option is rendered in
|
|
27
|
+
* its own tongue.
|
|
28
|
+
*
|
|
29
|
+
* Document locales are arbitrary validated tags, so no shipped label table could cover them;
|
|
30
|
+
* `Intl.DisplayNames` knows them, and the raw tag is the honest fallback when it does not.
|
|
31
|
+
*/
|
|
32
|
+
export declare function localeLabel(locale: string): string;
|
package/dist/messages.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface UiMessages {
|
|
|
26
26
|
'common.no': string;
|
|
27
27
|
'header.openMenu': string;
|
|
28
28
|
'header.closeMenu': string;
|
|
29
|
+
/** Accessible name of the language switcher — the options label themselves. */
|
|
30
|
+
'locale.label': string;
|
|
29
31
|
'table.empty': string;
|
|
30
32
|
'form.save': string;
|
|
31
33
|
'form.saving': string;
|
package/dist/runtime.d.ts
CHANGED
|
@@ -21,6 +21,24 @@ export interface GroveUiContext {
|
|
|
21
21
|
/** Confirmation gate for destructive actions; defaults to window.confirm. */
|
|
22
22
|
confirm?: (message: string) => boolean | Promise<boolean>;
|
|
23
23
|
locale?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Every locale the document offers, default first — what a switcher puts on the menu.
|
|
26
|
+
* Absent, or shorter than two entries, means a monolingual app: no switcher renders.
|
|
27
|
+
*/
|
|
28
|
+
locales?: readonly string[] | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Switches the app's language. Supplied by `GroveRoutes`, which holds the locale in state
|
|
31
|
+
* and persists the choice; absent when an app drives `locale` itself as a fixed prop, and
|
|
32
|
+
* `LocaleSwitcher` then renders nothing rather than offering a control that does nothing.
|
|
33
|
+
*/
|
|
34
|
+
setLocale?: ((locale: string) => void) | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Called by `Header` while it is mounted — it returns its own cleanup, so the call site is
|
|
37
|
+
* a one-liner `useEffect`. The shell counts headers to decide whether to float a language
|
|
38
|
+
* switcher of its own: a document with no header, or a screen that opted out, renders none,
|
|
39
|
+
* and without this a multilingual app would ship a switcher nothing puts on the page.
|
|
40
|
+
*/
|
|
41
|
+
onHeaderMounted?: (() => () => void) | undefined;
|
|
24
42
|
currency?: string;
|
|
25
43
|
/**
|
|
26
44
|
* Overrides for the primitives' OWN strings (`messages.ts`) — the chrome an app never
|
|
@@ -51,6 +69,20 @@ export declare function uiText(ctx: GroveUiContext, key: UiMessageKey): string;
|
|
|
51
69
|
export declare function useUiText(): (key: UiMessageKey) => string;
|
|
52
70
|
/** The chrome catalogue in force — for the few places that need several keys at once. */
|
|
53
71
|
export declare function useUiMessages(): UiMessages;
|
|
72
|
+
/**
|
|
73
|
+
* The chrome translator for a component that may render OUTSIDE the provider.
|
|
74
|
+
*
|
|
75
|
+
* `SignIn` is the case this exists for: the generated app can mount it around the whole
|
|
76
|
+
* router (`<SignIn><GroveRoutes /></SignIn>`), which puts the form ABOVE the provider that
|
|
77
|
+
* carries the locale — so the sign-in screen of a Spanish app came up in English while every
|
|
78
|
+
* page behind it was translated. Rather than make the app thread a locale through by hand,
|
|
79
|
+
* such a component resolves one itself: an explicit prop, else the provider when there is
|
|
80
|
+
* one, else the viewer's own languages.
|
|
81
|
+
*
|
|
82
|
+
* `available` is the set to match the viewer against — the locales the catalogue ships, since
|
|
83
|
+
* a component outside the provider has no document to ask.
|
|
84
|
+
*/
|
|
85
|
+
export declare function useStandaloneUiText(locale: string | undefined, overrides: Partial<UiMessages> | undefined): (key: UiMessageKey) => string;
|
|
54
86
|
/** Substitute `:param` segments in an href with a document's values. */
|
|
55
87
|
export declare function resolveHref(href: string, doc?: Doc): string;
|
|
56
88
|
export interface DispatchOptions {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groveback/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "React primitives for Groveback Studio screens — tables, forms, detail views and relation pickers that read through the Groveback SDK, so every query passes the policy engine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|