@undefineds.co/xpod 0.4.19 → 0.4.20
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/api/container/common.js +2 -1
- package/dist/api/handlers/AiConfigHandler.js +10 -2
- package/dist/runtime/host/node/NodeRuntimeHost.js +7 -3
- package/dist/runtime/port-finder.d.ts +0 -5
- package/dist/runtime/port-finder.js +24 -12
- package/node_modules/@undefineds.co/extension-sdk/dist/react/solid-auth-boundary.d.ts +20 -2
- package/node_modules/@undefineds.co/extension-sdk/dist/react/solid-auth-boundary.js +142 -125
- package/node_modules/@undefineds.co/shared-ui/dist/focus.d.ts +1 -1
- package/node_modules/@undefineds.co/shared-ui/dist/focus.js +1 -1
- package/node_modules/@undefineds.co/shared-ui/dist/index.d.ts +1 -0
- package/node_modules/@undefineds.co/shared-ui/dist/index.js +1 -0
- package/node_modules/@undefineds.co/shared-ui/dist/login/LoginModal.d.ts +2 -0
- package/node_modules/@undefineds.co/shared-ui/dist/login/LoginModal.js +2 -0
- package/node_modules/@undefineds.co/shared-ui/dist/login/LoginView.d.ts +3 -0
- package/node_modules/@undefineds.co/shared-ui/dist/login/LoginView.js +3 -0
- package/node_modules/@undefineds.co/shared-ui/dist/login.d.ts +15 -0
- package/node_modules/@undefineds.co/shared-ui/dist/login.js +15 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/AccountSections.d.ts +141 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/AccountSections.js +67 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/ConsentView.d.ts +53 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/ConsentView.js +27 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/DeviceDialogs.d.ts +40 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/DeviceDialogs.js +33 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.d.ts +15 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpChrome.js +8 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpViews.d.ts +93 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/IdpViews.js +88 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/NetworkPanel.d.ts +46 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/NetworkPanel.js +42 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignIn.d.ts +63 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignIn.js +65 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.d.ts +31 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/PodSignInFrame.js +111 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/StorageBadge.d.ts +8 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/StorageBadge.js +7 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/XpodMark.d.ts +8 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/XpodMark.js +8 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/account-parts.d.ts +37 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/account-parts.js +33 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/copy.d.ts +156 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/copy.js +304 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/index.d.ts +20 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/index.js +18 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.d.ts +69 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/parts.js +65 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/types.d.ts +14 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/types.js +1 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/webid-name.d.ts +5 -0
- package/node_modules/@undefineds.co/shared-ui/dist/pod-sign-in/webid-name.js +14 -0
- package/node_modules/@undefineds.co/shared-ui/dist/storage-bootstrap.d.ts +3 -0
- package/node_modules/@undefineds.co/shared-ui/dist/storage-bootstrap.js +3 -0
- package/node_modules/@undefineds.co/shared-ui/dist/theme.css +168 -83
- package/node_modules/@undefineds.co/shared-ui/dist/webid-auth.d.ts +5 -0
- package/node_modules/@undefineds.co/shared-ui/dist/webid-auth.js +5 -0
- package/package.json +2 -2
- package/static/app/assets/main.css +1 -1
- package/static/app/assets/main.js +55 -15
- package/static/dashboard/assets/{AiConfigContext-B5QP7ow2.js → AiConfigContext-CZEW16so.js} +1 -1
- package/static/dashboard/assets/{AiConfigPage-C_TdlUjq.js → AiConfigPage-Cfp0fRX1.js} +1 -1
- package/static/dashboard/assets/{DocumentProcessingPanel-CormNOdI.js → DocumentProcessingPanel-DaaRCLp6.js} +1 -1
- package/static/dashboard/assets/{IndexLifecyclePanel-S9wNNyHO.js → IndexLifecyclePanel-BCz3jNM8.js} +1 -1
- package/static/dashboard/assets/{IndexSubjectPanel-CofZT1oE.js → IndexSubjectPanel-DVCVIt7V.js} +1 -1
- package/static/dashboard/assets/ModelAssignmentsPanel-DxauxtEY.js +1 -0
- package/static/dashboard/assets/{ModelsPage-DZ46VnUh.js → ModelsPage-B8F3AtZL.js} +3 -3
- package/static/dashboard/assets/{NetworkPage-TwbIWASf.js → NetworkPage-Cu7ty71x.js} +2 -2
- package/static/dashboard/assets/{PaneListHeader-BtVnyutC.js → PaneListHeader-BEAXSlD9.js} +1 -1
- package/static/dashboard/assets/{SearchIndexingPanel-BCq_YMOP.js → SearchIndexingPanel-DEr1EDDC.js} +1 -1
- package/static/dashboard/assets/{StatusSubjectPanel-DsUKTysU.js → StatusSubjectPanel-Cg92MGhO.js} +1 -1
- package/static/dashboard/assets/{StatusWorkspace-CaVJBRqI.js → StatusWorkspace-BmJSOF3z.js} +1 -1
- package/static/dashboard/assets/{SystemSettingsPage-DRWfPjtu.js → SystemSettingsPage-rziu4Q3U.js} +1 -1
- package/static/dashboard/assets/SystemSettingsSubjectPanel-D16DGDNW.js +33 -0
- package/static/dashboard/assets/{XpodAiConnectionsPodStore-C9tHqutw.js → XpodAiConnectionsPodStore-FlZ0LEn0.js} +1 -1
- package/static/dashboard/assets/{activity-tgjewO88.js → activity-D55X-5L5.js} +1 -1
- package/static/dashboard/assets/{badge-Czun47II.js → badge-CRf1ISRJ.js} +1 -1
- package/static/dashboard/assets/{collection-runtime-DnYlU23-.js → collection-runtime-CfXMuwNc.js} +1 -1
- package/static/dashboard/assets/dashboard-B3kGI946.css +1 -0
- package/static/dashboard/assets/{dashboard-CF8SbVSR.js → dashboard-D4DWPaAy.js} +122 -122
- package/static/dashboard/assets/{dialog-D-Yc_lg-.js → dialog-EVyIyybL.js} +1 -1
- package/static/dashboard/assets/{download-vFuOkjCi.js → download-Et4aRDT2.js} +1 -1
- package/static/dashboard/assets/{external-link-Cg_VIecD.js → external-link-DT746o7t.js} +1 -1
- package/static/dashboard/assets/{index-IXCNeEwi.js → index-D2agcVRx.js} +3 -3
- package/static/dashboard/assets/{index-0au0WwkE.js → index-_wC13YGI.js} +1 -1
- package/static/dashboard/assets/{index-YDcVky3o.js → index-z6TnJzW6.js} +1 -1
- package/static/dashboard/assets/{key-round-9n1xgLbG.js → key-round-CHnogQ1D.js} +1 -1
- package/static/dashboard/assets/{rotate-ccw-DJ6s86DW.js → rotate-ccw-BNVDC4Zr.js} +1 -1
- package/static/dashboard/assets/{skeleton-Dg9HePtI.js → skeleton-BzsZekVf.js} +1 -1
- package/static/dashboard/assets/{waypoints-uxc_qOzk.js → waypoints-CLZA22UN.js} +1 -1
- package/static/dashboard/assets/{workspace-layout-BNtAXhPW.js → workspace-layout-urYcwAhS.js} +1 -1
- package/static/dashboard/dashboard.html +2 -2
- package/static/settings/assets/{AiConfigContext-y6ha7pGs.js → AiConfigContext-B2cVJFNu.js} +1 -1
- package/static/settings/assets/AiConfigPage-D_5jSPPF.js +1 -0
- package/static/settings/assets/{DocumentProcessingPanel-Oa_cxMcn.js → DocumentProcessingPanel-DodGlYIk.js} +1 -1
- package/static/settings/assets/{IndexLifecyclePanel-DlrnI0Kh.js → IndexLifecyclePanel-BtH-0GKv.js} +1 -1
- package/static/settings/assets/{IndexSubjectPanel-Dhsl0W32.js → IndexSubjectPanel-CqKhUcw2.js} +1 -1
- package/static/settings/assets/ModelAssignmentsPanel-Cam9V9BN.js +1 -0
- package/static/settings/assets/{ModelsPage-CuNW1hGX.js → ModelsPage-DWFic09l.js} +3 -3
- package/static/settings/assets/{NetworkPage-BBD2Lc_P.js → NetworkPage-CSwHoAWm.js} +2 -2
- package/static/settings/assets/{PaneListHeader-C81adSBX.js → PaneListHeader-270Z179Z.js} +1 -1
- package/static/settings/assets/{SearchIndexingPanel-Dc4QKWd0.js → SearchIndexingPanel-ATHKMDBQ.js} +1 -1
- package/static/settings/assets/{StatusSubjectPanel-DuxOvqzS.js → StatusSubjectPanel-DDWo3v3d.js} +1 -1
- package/static/settings/assets/StatusWorkspace-BYgqeCBM.js +1 -0
- package/static/settings/assets/SystemSettingsPage-BRaQAFWC.js +1 -0
- package/static/settings/assets/SystemSettingsSubjectPanel-Dc0roh9d.js +33 -0
- package/static/settings/assets/{XpodAiConnectionsPodStore-G0ClZksP.js → XpodAiConnectionsPodStore-OndzbigX.js} +2 -2
- package/static/settings/assets/{activity-DT8pzOea.js → activity-BAZEToJ3.js} +1 -1
- package/static/settings/assets/auth-callback-Bd3tvCOa.js +1 -0
- package/static/settings/assets/{badge-CvKRjSY2.js → badge-Cpol0LWF.js} +1 -1
- package/static/settings/assets/{collection-runtime-C6ZAp2cM.js → collection-runtime-B5vGx3gk.js} +1 -1
- package/static/settings/assets/{dialog-LFluK7Nx.js → dialog-_7AVvv5q.js} +1 -1
- package/static/settings/assets/{download-CZJeqUm1.js → download-DRAi-dnn.js} +1 -1
- package/static/settings/assets/{external-link-BsDft3Oe.js → external-link--Yq8inDb.js} +1 -1
- package/static/settings/assets/global-B3kGI946.css +1 -0
- package/static/settings/assets/{global-cmKEraT4.js → global-DMtP8ZF4.js} +111 -111
- package/static/settings/assets/{index-BiqSFOS1.js → index-C36H49PZ.js} +1 -1
- package/static/settings/assets/{index-XRgmPBKw.js → index-CQR3iePj.js} +1 -1
- package/static/settings/assets/{index-BMPlaRWG.js → index-DURnCfoQ.js} +1 -1
- package/static/settings/assets/{index-browser-B0slhJKc.js → index-browser-kAl2JmYB.js} +1 -1
- package/static/settings/assets/{key-round-D1XsWbN_.js → key-round-DzpWBVN9.js} +1 -1
- package/static/settings/assets/{rotate-ccw-CEe0P0fY.js → rotate-ccw-CtVsCcAx.js} +1 -1
- package/static/settings/assets/settings-DQtt6lql.js +1 -0
- package/static/settings/assets/{skeleton-DA404w6m.js → skeleton-R_ACks7d.js} +1 -1
- package/static/settings/assets/{waypoints-DJDWc4S9.js → waypoints-D-Qv_sME.js} +1 -1
- package/static/settings/assets/{workspace-layout-K38jT4OV.js → workspace-layout-BSMCQq35.js} +1 -1
- package/static/settings/auth-callback.html +3 -3
- package/static/settings/settings.html +3 -3
- package/static/dashboard/assets/ModelAssignmentsPanel-9tgOjj-y.js +0 -1
- package/static/dashboard/assets/SystemSettingsSubjectPanel-CgDP7Ii2.js +0 -33
- package/static/dashboard/assets/dashboard-DvuM3VTP.css +0 -1
- package/static/settings/assets/AiConfigPage-CIcIWdXA.js +0 -1
- package/static/settings/assets/ModelAssignmentsPanel-CHoiRZeL.js +0 -1
- package/static/settings/assets/StatusWorkspace-DeD3zi3G.js +0 -1
- package/static/settings/assets/SystemSettingsPage-8sHOaPb8.js +0 -1
- package/static/settings/assets/SystemSettingsSubjectPanel-B1JXut7S.js +0 -33
- package/static/settings/assets/auth-callback-1BlsOtMG.js +0 -1
- package/static/settings/assets/global-DvuM3VTP.css +0 -1
- package/static/settings/assets/settings-DfjkqEJ8.js +0 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { formatCopy, resolvePodSignInCopy } from './copy.js';
|
|
4
|
+
import { IdpChrome } from './IdpChrome.js';
|
|
5
|
+
import { ActionButton, CheckboxRow, ScreenLayout, Disclosure, Field, fieldClass, outlineButtonClass, primaryButtonClass, textButtonClass, } from './parts.js';
|
|
6
|
+
import { Input } from '../input.js';
|
|
7
|
+
import { cn } from '../utils.js';
|
|
8
|
+
function resolveIdpCopy({ locale, copy }) {
|
|
9
|
+
return resolvePodSignInCopy(locale, copy);
|
|
10
|
+
}
|
|
11
|
+
export function IdpFrameHeader({ serviceName = 'Xpod', serviceHost, serviceIcon, locale, copy }) {
|
|
12
|
+
return (_jsx(IdpChrome, { serviceName: serviceName, serviceHost: serviceHost, icon: serviceIcon, locale: locale, serviceLabel: copy?.serviceLabel }));
|
|
13
|
+
}
|
|
14
|
+
const inputClass = fieldClass;
|
|
15
|
+
/** A 36px text action: a real link when the host navigates to a page, otherwise a button. */
|
|
16
|
+
function TextAction({ href, onClick, disabled, className, children }) {
|
|
17
|
+
const classes = cn(textButtonClass, 'inline-flex items-center text-primary hover:underline', className);
|
|
18
|
+
return href && !disabled
|
|
19
|
+
? _jsx("a", { href: href, className: classes, children: children })
|
|
20
|
+
: _jsx("button", { type: "button", className: classes, disabled: disabled, onClick: onClick, children: children });
|
|
21
|
+
}
|
|
22
|
+
/** B1: sign in to the sign-in service. Email and password only ever appear here. */
|
|
23
|
+
export function IdpSignInView(props) {
|
|
24
|
+
const { serviceName, returnToAppName, error, fieldErrors, pending = false, remember, defaultEmail = '', defaultPassword = '', onFieldChange, onRememberChange, onSubmit, onForgot, forgotHref, onRegister, registerHref, onUseOtherSolid, } = props;
|
|
25
|
+
const copy = resolveIdpCopy(props);
|
|
26
|
+
const [email, setEmail] = useState(defaultEmail);
|
|
27
|
+
const [password, setPassword] = useState(defaultPassword);
|
|
28
|
+
const submit = (event) => {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
if (!pending)
|
|
31
|
+
onSubmit({ email: email.trim(), password });
|
|
32
|
+
};
|
|
33
|
+
return (_jsxs(ScreenLayout, { "data-pod-sign-in-state": "idp-sign-in", chrome: _jsx(IdpFrameHeader, { ...props }), onSubmit: submit, actions: (_jsxs(_Fragment, { children: [error ? _jsx("p", { role: "alert", className: "text-[13px] text-destructive", children: error }) : null, _jsx(ActionButton, { type: "submit", className: primaryButtonClass, busy: pending, children: copy.signIn }), _jsxs("div", { className: "flex items-center justify-between gap-2", children: [onRegister || registerHref ? (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx("span", { className: "text-xs text-muted-foreground", children: copy.noAccount }), _jsx(TextAction, { href: registerHref, disabled: pending, onClick: onRegister, children: copy.registerLink })] })) : _jsx("span", {}), onUseOtherSolid ? (_jsx("button", { type: "button", className: cn(textButtonClass, 'text-primary hover:underline'), disabled: pending, onClick: onUseOtherSolid, children: copy.useOtherSolid })) : null] })] })), children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("h1", { className: "text-[17px] font-semibold text-foreground", children: formatCopy(copy.signInTitle, { service: serviceName }) }), returnToAppName ? (_jsx("p", { className: "text-sm text-muted-foreground", children: formatCopy(copy.returnToApp, { app: returnToAppName }) })) : null] }), _jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(Field, { label: copy.email, error: fieldErrors?.email, children: (fieldProps) => (_jsx(Input, { ...fieldProps, name: "email", type: "email", autoComplete: "username", autoCapitalize: "none", spellCheck: false, className: inputClass, value: email, disabled: pending, onChange: (event) => {
|
|
34
|
+
setEmail(event.target.value);
|
|
35
|
+
onFieldChange?.('email', event.target.value);
|
|
36
|
+
} })) }), _jsx(Field, { label: copy.password, error: fieldErrors?.password, labelAside: onForgot || forgotHref ? (_jsx(TextAction, { className: "h-7 text-[13px]", href: forgotHref, onClick: onForgot, children: copy.forgotPassword })) : undefined, children: (fieldProps) => (_jsx(Input, { ...fieldProps, name: "password", type: "password", autoComplete: "current-password", className: inputClass, value: password, disabled: pending, onChange: (event) => {
|
|
37
|
+
setPassword(event.target.value);
|
|
38
|
+
onFieldChange?.('password', event.target.value);
|
|
39
|
+
} })) }), onRememberChange ? (_jsx(CheckboxRow, { checked: remember, onChange: onRememberChange, label: copy.rememberDevice, disabled: pending })) : null] })] }));
|
|
40
|
+
}
|
|
41
|
+
/** B2: register. The explicit form is what makes creating a WebID and Pod not implicit. */
|
|
42
|
+
export function IdpRegisterView(props) {
|
|
43
|
+
const { serviceName, returnToAppName, usernamePreview, requireUsername, fieldErrors, error, pending = false, defaultEmail = '', defaultPassword = '', defaultUsername = '', onFieldChange, onSubmit, onSignIn, } = props;
|
|
44
|
+
const copy = resolveIdpCopy(props);
|
|
45
|
+
const [username, setUsername] = useState(defaultUsername);
|
|
46
|
+
const [email, setEmail] = useState(defaultEmail);
|
|
47
|
+
const [password, setPassword] = useState(defaultPassword);
|
|
48
|
+
const submit = (event) => {
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
if (pending)
|
|
51
|
+
return;
|
|
52
|
+
onSubmit({
|
|
53
|
+
...(requireUsername ? { username: username.trim() } : {}),
|
|
54
|
+
email: email.trim(),
|
|
55
|
+
password,
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
return (_jsxs(ScreenLayout, { "data-pod-sign-in-state": "idp-register", chrome: _jsx(IdpFrameHeader, { ...props }), onSubmit: submit, actions: (_jsxs(_Fragment, { children: [error ? _jsx("p", { role: "alert", className: "text-[13px] text-destructive", children: error }) : null, _jsx(ActionButton, { type: "submit", className: primaryButtonClass, busy: pending, children: copy.registerSubmit }), onSignIn ? (_jsx("button", { type: "button", className: cn(textButtonClass, 'self-center text-primary hover:underline'), disabled: pending, onClick: onSignIn, children: copy.haveAccount })) : null] })), children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("h1", { className: "text-[17px] font-semibold text-foreground", children: formatCopy(copy.registerTitle, { service: serviceName }) }), returnToAppName ? (_jsx("p", { className: "text-sm text-muted-foreground", children: formatCopy(copy.returnToApp, { app: returnToAppName }) })) : null] }), _jsxs("div", { className: "flex flex-col gap-4", children: [requireUsername ? (_jsx(Field, { label: copy.username, error: fieldErrors?.username, hint: usernamePreview ? _jsx("span", { role: "status", className: "font-mono text-xs", children: usernamePreview }) : undefined, children: (fieldProps) => (_jsx(Input, { ...fieldProps, name: "username", type: "text", autoComplete: "username", autoCapitalize: "none", spellCheck: false, className: inputClass, value: username, disabled: pending, onChange: (event) => {
|
|
59
|
+
setUsername(event.target.value);
|
|
60
|
+
onFieldChange?.('username', event.target.value);
|
|
61
|
+
} })) })) : null, _jsx(Field, { label: copy.email, error: fieldErrors?.email, children: (fieldProps) => (_jsx(Input, { ...fieldProps, name: "email", type: "email", autoComplete: "email", autoCapitalize: "none", spellCheck: false, className: inputClass, value: email, disabled: pending, onChange: (event) => {
|
|
62
|
+
setEmail(event.target.value);
|
|
63
|
+
onFieldChange?.('email', event.target.value);
|
|
64
|
+
} })) }), _jsx(Field, { label: copy.password, error: fieldErrors?.password, children: (fieldProps) => (_jsx(Input, { ...fieldProps, name: "password", type: "password", autoComplete: "new-password", className: inputClass, value: password, disabled: pending, onChange: (event) => {
|
|
65
|
+
setPassword(event.target.value);
|
|
66
|
+
onFieldChange?.('password', event.target.value);
|
|
67
|
+
} })) }), _jsx(Disclosure, { summary: copy.podElsewhereSummary, children: copy.podElsewhereBody })] })] }));
|
|
68
|
+
}
|
|
69
|
+
/** B3: the account has no WebID. Create one here, or leave for the account page to store it elsewhere. */
|
|
70
|
+
export function IdpNoWebIdView(props) {
|
|
71
|
+
const { appName, defaultName, nameHint, error, pending = false, onNameChange, onCreate, onChooseOtherLocation } = props;
|
|
72
|
+
const copy = resolveIdpCopy(props);
|
|
73
|
+
const [name, setName] = useState(defaultName ?? '');
|
|
74
|
+
const hasNameField = defaultName !== undefined;
|
|
75
|
+
const trimmed = name.trim();
|
|
76
|
+
const canSubmit = !hasNameField || (Boolean(trimmed) && nameHint?.tone !== 'error');
|
|
77
|
+
const submit = (event) => {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
if (!pending && canSubmit)
|
|
80
|
+
onCreate(trimmed);
|
|
81
|
+
};
|
|
82
|
+
return (_jsxs(ScreenLayout, { "data-pod-sign-in-state": "idp-no-webid", chrome: _jsx(IdpFrameHeader, { ...props }), onSubmit: submit, actions: (_jsxs(_Fragment, { children: [error ? _jsx("p", { role: "alert", className: "text-[13px] text-destructive", children: error }) : null, _jsx(ActionButton, { type: "submit", className: primaryButtonClass, busy: pending, disabled: !canSubmit, children: copy.createAndContinue }), onChooseOtherLocation ? (_jsx(ActionButton, { type: "button", variant: "outline", className: outlineButtonClass, disabled: pending, onClick: onChooseOtherLocation, children: copy.chooseOtherLocation })) : null] })), children: [_jsxs("div", { className: "flex flex-col gap-1", children: [_jsx("h1", { className: "text-[17px] font-semibold text-foreground", children: copy.noWebIdTitle }), _jsx("p", { className: "text-sm leading-[22px] text-muted-foreground", children: formatCopy(copy.noWebIdLead, { app: appName }) })] }), _jsx("div", { className: "flex flex-col gap-4", children: hasNameField ? (_jsx(Field, { label: copy.webIdName, error: nameHint?.tone === 'error' ? nameHint.text : undefined, hint: nameHint && nameHint.tone !== 'error'
|
|
83
|
+
? _jsx("span", { role: "status", children: nameHint.text })
|
|
84
|
+
: _jsx("span", { children: copy.noWebIdLocation }), children: (fieldProps) => (_jsx(Input, { ...fieldProps, name: "webIdName", type: "text", autoComplete: "off", autoCapitalize: "none", spellCheck: false, className: inputClass, value: name, disabled: pending, onChange: (event) => {
|
|
85
|
+
setName(event.target.value);
|
|
86
|
+
onNameChange?.(event.target.value);
|
|
87
|
+
} })) })) : null })] }));
|
|
88
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type PodSignInCopy, type PodSignInLocale } from './copy';
|
|
2
|
+
export type ProbeState = 'idle' | 'checking' | 'ok' | 'failed';
|
|
3
|
+
/** This computer, the local network, and other networks (the public entry). */
|
|
4
|
+
export interface NetworkChecks {
|
|
5
|
+
local: ProbeState;
|
|
6
|
+
lan: ProbeState;
|
|
7
|
+
wan: ProbeState;
|
|
8
|
+
}
|
|
9
|
+
export interface TunnelFieldDescriptor {
|
|
10
|
+
key: string;
|
|
11
|
+
label: string;
|
|
12
|
+
/** Rendered as a password input. */
|
|
13
|
+
secret?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* One tunnel option, generated from the provider catalog (`TunnelProviderCatalog`:
|
|
17
|
+
* `id`, `label`, `parameterFields`, plus the credential field). The panel renders
|
|
18
|
+
* fields from this description and never branches on the provider.
|
|
19
|
+
*/
|
|
20
|
+
export interface TunnelOption {
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
parameterFields: readonly TunnelFieldDescriptor[];
|
|
24
|
+
credentialField?: TunnelFieldDescriptor;
|
|
25
|
+
}
|
|
26
|
+
export interface NetworkPanelProps {
|
|
27
|
+
checks: NetworkChecks;
|
|
28
|
+
tunnels: readonly TunnelOption[];
|
|
29
|
+
selectedTunnelId?: string;
|
|
30
|
+
/** Field values keyed by `TunnelFieldDescriptor.key`. */
|
|
31
|
+
fieldValues: Record<string, string>;
|
|
32
|
+
/** Label of the tunnel that makes the public entry reachable, once it does. */
|
|
33
|
+
activeTunnelLabel?: string;
|
|
34
|
+
busy?: boolean;
|
|
35
|
+
onSelectTunnel(id: string): void;
|
|
36
|
+
onFieldChange(key: string, value: string): void;
|
|
37
|
+
/** "Enable and recheck". */
|
|
38
|
+
onEnable(): void;
|
|
39
|
+
onRecheck(): void;
|
|
40
|
+
/** When given (inside "add device"), the actions become "Skip, set up later" / "Enable and check". */
|
|
41
|
+
onSkip?(): void;
|
|
42
|
+
locale?: PodSignInLocale;
|
|
43
|
+
copy?: Partial<PodSignInCopy>;
|
|
44
|
+
}
|
|
45
|
+
/** Network access of one device: three checks, and four tunnels when the public entry fails. */
|
|
46
|
+
export declare function NetworkPanel({ checks, tunnels, selectedTunnelId, fieldValues, activeTunnelLabel, busy, onSelectTunnel, onFieldChange, onEnable, onRecheck, onSkip, locale, copy: overrides, }: NetworkPanelProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Check, Minus, X } from 'lucide-react';
|
|
3
|
+
import { useId } from 'react';
|
|
4
|
+
import { cn } from '../utils.js';
|
|
5
|
+
import { formatCopy, resolvePodSignInCopy } from './copy.js';
|
|
6
|
+
import { ActionButton, Field, Spinner, fieldClass, outlineButtonClass, primaryButtonClass } from './parts.js';
|
|
7
|
+
function probeLabel(state, copy) {
|
|
8
|
+
switch (state) {
|
|
9
|
+
case 'idle': return copy.probeIdle;
|
|
10
|
+
case 'checking': return copy.probeChecking;
|
|
11
|
+
case 'ok': return copy.probeOk;
|
|
12
|
+
case 'failed': return copy.probeFailed;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function ProbeIcon({ state }) {
|
|
16
|
+
if (state === 'checking')
|
|
17
|
+
return _jsx(Spinner, {});
|
|
18
|
+
if (state === 'ok')
|
|
19
|
+
return _jsx(Check, { "aria-hidden": "true", className: "h-4 w-4 text-success" });
|
|
20
|
+
if (state === 'failed')
|
|
21
|
+
return _jsx(X, { "aria-hidden": "true", className: "h-4 w-4 text-warning" });
|
|
22
|
+
return _jsx(Minus, { "aria-hidden": "true", className: "h-4 w-4 text-muted-foreground" });
|
|
23
|
+
}
|
|
24
|
+
/** Network access of one device: three checks, and four tunnels when the public entry fails. */
|
|
25
|
+
export function NetworkPanel({ checks, tunnels, selectedTunnelId, fieldValues, activeTunnelLabel, busy = false, onSelectTunnel, onFieldChange, onEnable, onRecheck, onSkip, locale, copy: overrides, }) {
|
|
26
|
+
const copy = resolvePodSignInCopy(locale, overrides);
|
|
27
|
+
const titleId = useId();
|
|
28
|
+
const tunnelLabelId = useId();
|
|
29
|
+
const selected = tunnels.find((tunnel) => tunnel.id === selectedTunnelId);
|
|
30
|
+
const fields = selected ? [...selected.parameterFields, ...(selected.credentialField ? [selected.credentialField] : [])] : [];
|
|
31
|
+
const rows = [
|
|
32
|
+
['local', copy.probeLocal],
|
|
33
|
+
['lan', copy.probeLan],
|
|
34
|
+
['wan', copy.probeWan],
|
|
35
|
+
];
|
|
36
|
+
const checking = Object.values(checks).includes('checking');
|
|
37
|
+
const showTunnels = checks.wan === 'failed';
|
|
38
|
+
return (_jsxs("section", { "aria-labelledby": titleId, "data-pod-sign-in": "network-panel", className: "pod-sign-in flex flex-col gap-4 rounded-lg bg-muted p-3", children: [_jsx("h3", { id: titleId, className: "text-sm font-semibold text-foreground", children: copy.networkTitle }), _jsx("ul", { className: "grid grid-cols-3 gap-2", "aria-busy": checking || undefined, children: rows.map(([key, label]) => (_jsxs("li", { "data-probe": key, "data-state": checks[key], className: "flex flex-col gap-1 rounded-lg border border-border bg-card p-2 text-[13px]", children: [_jsx("span", { className: "text-foreground", children: label }), _jsxs("span", { className: "flex items-center gap-1 text-muted-foreground", children: [_jsx(ProbeIcon, { state: checks[key] }), probeLabel(checks[key], copy)] })] }, key))) }), checks.wan === 'ok' && activeTunnelLabel ? (_jsx("p", { role: "status", className: "text-[13px] text-success", children: formatCopy(copy.reachableVia, { tunnel: activeTunnelLabel }) })) : null, showTunnels ? (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("p", { id: tunnelLabelId, className: "text-[13px] font-medium text-foreground", children: copy.tunnelTitle }), _jsx("div", { role: "radiogroup", "aria-labelledby": tunnelLabelId, className: "grid grid-cols-2 gap-2", children: tunnels.map((tunnel) => {
|
|
39
|
+
const isSelected = tunnel.id === selectedTunnelId;
|
|
40
|
+
return (_jsxs("label", { className: cn('flex min-h-11 cursor-pointer items-center justify-center rounded-lg border px-2 text-center text-sm focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-ring', isSelected ? 'border-primary bg-accent text-foreground' : 'border-border bg-card text-foreground'), children: [_jsx("input", { type: "radio", name: "tunnel-provider", className: "sr-only", value: tunnel.id, checked: isSelected, disabled: busy, onChange: () => onSelectTunnel(tunnel.id) }), tunnel.label] }, tunnel.id));
|
|
41
|
+
}) }), fields.map((field) => (_jsx(Field, { label: field.label, children: (fieldProps) => (_jsx("input", { ...fieldProps, name: field.key, type: field.secret ? 'password' : 'text', autoComplete: "off", spellCheck: false, className: fieldClass, value: fieldValues[field.key] ?? '', disabled: busy, onChange: (event) => onFieldChange(field.key, event.target.value) })) }, field.key)))] })) : null, _jsxs("div", { className: "flex flex-col gap-2", children: [showTunnels ? (_jsx(ActionButton, { className: primaryButtonClass, busy: busy, disabled: !selected, onClick: onEnable, children: onSkip ? copy.enableAndCheck : copy.enableAndRecheck })) : null, onSkip ? (_jsx(ActionButton, { variant: "outline", className: outlineButtonClass, disabled: busy, onClick: onSkip, children: copy.skipForNow })) : (_jsx(ActionButton, { variant: "outline", className: outlineButtonClass, disabled: busy || checking, onClick: onRecheck, children: copy.recheck }))] }), _jsx("p", { className: "text-[13px] text-muted-foreground", children: copy.networkFootnote })] }));
|
|
42
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type PodSignInCopy, type PodSignInLocale } from './copy';
|
|
3
|
+
import type { AppIdentity, StorageLocation } from './types';
|
|
4
|
+
export interface RememberedIdentity {
|
|
5
|
+
displayName: string;
|
|
6
|
+
avatarUrl?: string;
|
|
7
|
+
/** Drives the avatar corner badge. */
|
|
8
|
+
storage?: StorageLocation;
|
|
9
|
+
}
|
|
10
|
+
export type PodSignInState = {
|
|
11
|
+
kind: 'restoring';
|
|
12
|
+
identity?: RememberedIdentity;
|
|
13
|
+
} | {
|
|
14
|
+
kind: 'remembered';
|
|
15
|
+
identity: RememberedIdentity;
|
|
16
|
+
busy?: boolean;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'expired';
|
|
19
|
+
identity: RememberedIdentity;
|
|
20
|
+
busy?: boolean;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'choose-service';
|
|
23
|
+
customOpen?: boolean;
|
|
24
|
+
customError?: string;
|
|
25
|
+
busy?: boolean;
|
|
26
|
+
};
|
|
27
|
+
/** C group: one line above the primary action, plus a replaced primary label. */
|
|
28
|
+
export interface PodSignInNotice {
|
|
29
|
+
/** neutral = grey text (e.g. "login cancelled"); warning = with an icon. */
|
|
30
|
+
tone: 'neutral' | 'warning';
|
|
31
|
+
text: string;
|
|
32
|
+
/** Replaces the primary button text, e.g. "Retry" or "Start Xpod and enter". */
|
|
33
|
+
primaryLabel?: string;
|
|
34
|
+
/** Only expandable in developer mode. */
|
|
35
|
+
developerDetail?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface PodSignInProps {
|
|
38
|
+
app: AppIdentity;
|
|
39
|
+
state: PodSignInState;
|
|
40
|
+
notice?: PodSignInNotice;
|
|
41
|
+
locale?: PodSignInLocale;
|
|
42
|
+
copy?: Partial<PodSignInCopy>;
|
|
43
|
+
developerMode?: boolean;
|
|
44
|
+
/** Icon on the A3 primary button. Defaults to the Xpod mark; pass `null` for none. */
|
|
45
|
+
primaryIcon?: ReactNode;
|
|
46
|
+
capabilities?: {
|
|
47
|
+
customService?: boolean;
|
|
48
|
+
register?: boolean;
|
|
49
|
+
};
|
|
50
|
+
/** A1 enter / A2 sign in again / A3 sign in with Xpod / the action a notice relabelled. */
|
|
51
|
+
onPrimary(): void;
|
|
52
|
+
/** A1/A2 "use another account". */
|
|
53
|
+
onUseAnother?(): void;
|
|
54
|
+
onCustomService?(input: string): void;
|
|
55
|
+
onRegister?(): void;
|
|
56
|
+
onToggleCustom?(open: boolean): void;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Application-side sign-in body (A and C groups). One column: source mark,
|
|
60
|
+
* one h1, main content, actions. It renders no request, redirect or storage
|
|
61
|
+
* access; the host's controller maps its state to `state` and `notice`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function PodSignIn({ app, state, notice, locale, copy: copyOverrides, developerMode, primaryIcon, capabilities, onPrimary, onUseAnother, onCustomService, onRegister, onToggleCustom, }: PodSignInProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { TriangleAlert } from 'lucide-react';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Input } from '../input.js';
|
|
5
|
+
import { formatCopy, resolvePodSignInCopy } from './copy.js';
|
|
6
|
+
import { ActionButton, Disclosure, Field, PodAvatar, ScreenLayout, SourceMark, Spinner, outlineButtonClass, primaryButtonClass, textButtonClass, usePastDelay, } from './parts.js';
|
|
7
|
+
import { XpodMark } from './XpodMark.js';
|
|
8
|
+
import { cn } from '../utils.js';
|
|
9
|
+
const RESTORE_REVEAL_DELAY_MS = 300;
|
|
10
|
+
function NoticeLine({ notice, developerMode, copy, }) {
|
|
11
|
+
const [copied, setCopied] = useState(false);
|
|
12
|
+
const line = (_jsxs("span", { className: "inline-flex items-start gap-1.5", children: [notice.tone === 'warning' ? _jsx(TriangleAlert, { "aria-hidden": "true", className: "mt-0.5 h-3.5 w-3.5 shrink-0" }) : null, _jsx("span", { children: notice.text })] }));
|
|
13
|
+
const tone = notice.tone === 'warning' ? 'text-warning' : 'text-muted-foreground';
|
|
14
|
+
const role = notice.tone === 'warning' ? 'alert' : 'status';
|
|
15
|
+
const detail = developerMode ? notice.developerDetail : undefined;
|
|
16
|
+
return (_jsx("div", { role: role, "data-pod-sign-in": "notice", "data-tone": notice.tone, className: cn('text-[13px] leading-5', tone), children: detail ? (_jsxs("details", { children: [_jsx("summary", { title: copy.noticeDetailsToggle, className: "cursor-pointer rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring", children: line }), _jsxs("div", { className: "mt-1 rounded-lg bg-muted p-2 text-foreground", children: [_jsx("pre", { className: "whitespace-pre-wrap break-all font-mono text-xs", children: detail }), _jsx("button", { type: "button", className: "mt-1 h-9 rounded-lg px-2 text-[13px] font-medium text-primary hover:bg-accent focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring", onClick: () => {
|
|
17
|
+
void navigator.clipboard?.writeText(detail).then(() => setCopied(true), () => undefined);
|
|
18
|
+
}, children: copied ? copy.noticeCopied : copy.noticeCopy })] })] })) : line }));
|
|
19
|
+
}
|
|
20
|
+
function CustomServiceForm({ copy, error, busy, onSubmit, }) {
|
|
21
|
+
const [value, setValue] = useState('');
|
|
22
|
+
const submit = (event) => {
|
|
23
|
+
event.preventDefault();
|
|
24
|
+
const input = value.trim();
|
|
25
|
+
if (input && onSubmit)
|
|
26
|
+
onSubmit(input);
|
|
27
|
+
};
|
|
28
|
+
return (_jsxs("form", { onSubmit: submit, className: "flex flex-col gap-3", "data-pod-sign-in": "custom-service", children: [_jsx(Field, { label: copy.customLabel, error: error, children: (fieldProps) => (_jsx(Input, { ...fieldProps, name: "service", type: "text", inputMode: "url", autoComplete: "url", autoCapitalize: "none", spellCheck: false, placeholder: copy.customPlaceholder, value: value, disabled: busy, className: "h-11 rounded-lg bg-card text-sm", onChange: (event) => setValue(event.target.value) })) }), _jsx(ActionButton, { type: "submit", variant: "outline", className: outlineButtonClass, busy: busy, disabled: !value.trim(), children: copy.customSubmit })] }));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Application-side sign-in body (A and C groups). One column: source mark,
|
|
32
|
+
* one h1, main content, actions. It renders no request, redirect or storage
|
|
33
|
+
* access; the host's controller maps its state to `state` and `notice`.
|
|
34
|
+
*/
|
|
35
|
+
export function PodSignIn({ app, state, notice, locale = 'zh-CN', copy: copyOverrides, developerMode = false, primaryIcon, capabilities, onPrimary, onUseAnother, onCustomService, onRegister, onToggleCustom, }) {
|
|
36
|
+
const copy = resolvePodSignInCopy(locale, copyOverrides);
|
|
37
|
+
const pastRestoreDelay = usePastDelay(RESTORE_REVEAL_DELAY_MS);
|
|
38
|
+
const [localCustomOpen, setLocalCustomOpen] = useState(false);
|
|
39
|
+
const source = _jsx(SourceMark, { icon: app.icon, name: app.name });
|
|
40
|
+
if (state.kind === 'restoring') {
|
|
41
|
+
const identity = pastRestoreDelay ? state.identity : undefined;
|
|
42
|
+
return (_jsxs(ScreenLayout, { "data-pod-sign-in-state": "restoring", children: [source, _jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-3 text-center", children: [identity ? (_jsxs(_Fragment, { children: [_jsx(PodAvatar, { name: identity.displayName, avatarUrl: identity.avatarUrl, storage: identity.storage }), _jsx("h1", { className: "text-[17px] font-semibold text-foreground", children: identity.displayName })] })) : (_jsx("h1", { className: "sr-only", children: app.name })), _jsx("div", { role: "status", "aria-live": "polite", children: pastRestoreDelay ? (_jsxs("p", { className: "flex items-center gap-2 text-[13px] text-muted-foreground", children: [_jsx(Spinner, {}), copy.restoring] })) : null })] })] }));
|
|
43
|
+
}
|
|
44
|
+
const busy = state.busy === true;
|
|
45
|
+
const primaryLabel = notice?.primaryLabel
|
|
46
|
+
?? (state.kind === 'remembered'
|
|
47
|
+
? formatCopy(copy.enterApp, { app: app.name })
|
|
48
|
+
: state.kind === 'expired'
|
|
49
|
+
? copy.reauthenticate
|
|
50
|
+
: copy.useXpod);
|
|
51
|
+
const noticeNode = notice ? _jsx(NoticeLine, { notice: notice, developerMode: developerMode, copy: copy }) : null;
|
|
52
|
+
if (state.kind === 'remembered' || state.kind === 'expired') {
|
|
53
|
+
return (_jsxs(ScreenLayout, { "data-pod-sign-in-state": state.kind, actions: (_jsxs(_Fragment, { children: [noticeNode, _jsx(ActionButton, { className: primaryButtonClass, busy: busy, "data-pod-sign-in-primary": "true", onClick: onPrimary, children: primaryLabel }), onUseAnother ? (_jsx(ActionButton, { variant: "ghost", className: cn(textButtonClass, 'w-full'), disabled: busy, onClick: onUseAnother, children: copy.useAnother })) : null] })), children: [source, _jsxs("div", { className: "flex flex-1 flex-col items-center justify-center gap-3 text-center", children: [_jsx(PodAvatar, { name: state.identity.displayName, avatarUrl: state.identity.avatarUrl, storage: state.identity.storage }), _jsx("h1", { className: "text-[17px] font-semibold text-foreground", children: state.identity.displayName }), state.kind === 'expired' ? _jsx("p", { className: "text-sm text-muted-foreground", children: copy.expiredLine }) : null] })] }));
|
|
54
|
+
}
|
|
55
|
+
// choose-service (A3)
|
|
56
|
+
const allowCustom = capabilities?.customService !== false;
|
|
57
|
+
const allowRegister = capabilities?.register !== false && Boolean(onRegister);
|
|
58
|
+
const customOpen = allowCustom && (state.customOpen ?? localCustomOpen);
|
|
59
|
+
const toggleCustom = () => {
|
|
60
|
+
const next = !customOpen;
|
|
61
|
+
setLocalCustomOpen(next);
|
|
62
|
+
onToggleCustom?.(next);
|
|
63
|
+
};
|
|
64
|
+
return (_jsxs(ScreenLayout, { "data-pod-sign-in-state": "choose-service", actions: (_jsxs(_Fragment, { children: [noticeNode, _jsxs(ActionButton, { className: primaryButtonClass, busy: busy, "data-pod-sign-in-primary": "true", onClick: onPrimary, children: [!notice?.primaryLabel && primaryIcon !== null ? _jsx("span", { "aria-hidden": "true", className: "mr-2 flex", children: primaryIcon ?? _jsx(XpodMark, { size: 20 }) }) : null, primaryLabel] }), allowCustom ? (_jsxs(_Fragment, { children: [_jsx(ActionButton, { variant: "outline", className: outlineButtonClass, disabled: busy, "aria-expanded": customOpen, onClick: toggleCustom, children: copy.useOtherSolid }), customOpen ? (_jsx(CustomServiceForm, { copy: copy, error: state.customError, busy: busy, onSubmit: onCustomService })) : null] })) : null, allowRegister ? (_jsxs("p", { className: "text-center text-[13px] text-muted-foreground", children: [copy.noAccount, _jsx("button", { type: "button", className: "ml-1 h-9 rounded px-1 font-medium text-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring", disabled: busy, onClick: onRegister, children: copy.register })] })) : null] })), children: [source, _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("h1", { className: "text-[17px] font-semibold text-foreground", children: copy.chooseTitle }), _jsx("p", { className: "text-sm leading-[22px] text-muted-foreground", children: copy.chooseLead }), _jsxs("div", { className: "flex flex-col", children: [_jsx(Disclosure, { summary: copy.whatIsWebId, children: copy.whatIsWebIdBody }), _jsx(Disclosure, { summary: copy.whatIsPod, children: copy.whatIsPodBody })] })] })] }));
|
|
65
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { SignInPresentation } from './types';
|
|
3
|
+
export interface PodSignInFrameProps {
|
|
4
|
+
presentation: SignInPresentation;
|
|
5
|
+
ariaLabel: string;
|
|
6
|
+
/**
|
|
7
|
+
* `page` only: the left introduction column. The host supplies it (the
|
|
8
|
+
* application/service adapter), so the shared frame never hardcodes a
|
|
9
|
+
* service name.
|
|
10
|
+
*/
|
|
11
|
+
appIntro?: ReactNode;
|
|
12
|
+
/** The 360px wide body. */
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
/**
|
|
15
|
+
* `dialog` only. `true` (default) draws a modal layer over the document;
|
|
16
|
+
* `false` draws the same card inline, without modal semantics.
|
|
17
|
+
*/
|
|
18
|
+
modal?: boolean;
|
|
19
|
+
/** Escape closes a modal dialog when the host provides this. */
|
|
20
|
+
onClose?: () => void;
|
|
21
|
+
/** With `onClose`, draws a close button in the corner of a modal dialog. */
|
|
22
|
+
closeLabel?: string;
|
|
23
|
+
/** `data-*` attributes placed on the frame element that carries the region / dialog role. */
|
|
24
|
+
dataAttributes?: Record<`data-${string}`, string | undefined>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Outer frame of every sign-in surface. `window` fills the host window,
|
|
28
|
+
* `dialog` is a 400px layer, `page` is two columns. All three share the same
|
|
29
|
+
* 360px body, so a state looks the same wherever the host places it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function PodSignInFrame({ presentation, ariaLabel, appIntro, children, modal, onClose, closeLabel, dataAttributes, }: PodSignInFrameProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import { cn } from '../utils.js';
|
|
4
|
+
const FOCUSABLE = [
|
|
5
|
+
'a[href]',
|
|
6
|
+
'button:not([disabled])',
|
|
7
|
+
'input:not([disabled])',
|
|
8
|
+
'select:not([disabled])',
|
|
9
|
+
'textarea:not([disabled])',
|
|
10
|
+
'summary',
|
|
11
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
12
|
+
].join(',');
|
|
13
|
+
/** Marks the screen's single primary action so the first Tab can reach it. */
|
|
14
|
+
const PRIMARY_SELECTOR = '[data-pod-sign-in-primary]';
|
|
15
|
+
/**
|
|
16
|
+
* A candidate only counts if a user can actually reach it: hidden, disabled,
|
|
17
|
+
* unrendered and collapsed-detail content must be skipped, or `focus()` on it
|
|
18
|
+
* silently fails and the Tab index never advances (keyboard dead end).
|
|
19
|
+
*/
|
|
20
|
+
function isReachable(el) {
|
|
21
|
+
if (el.hidden || el.closest('[hidden]'))
|
|
22
|
+
return false;
|
|
23
|
+
if (el.matches(':disabled'))
|
|
24
|
+
return false;
|
|
25
|
+
const style = el.ownerDocument.defaultView?.getComputedStyle(el);
|
|
26
|
+
if (style && (style.display === 'none' || style.visibility === 'hidden'))
|
|
27
|
+
return false;
|
|
28
|
+
for (let node = el.parentElement; node; node = node.parentElement) {
|
|
29
|
+
if (node.tagName !== 'DETAILS' || node.open)
|
|
30
|
+
continue;
|
|
31
|
+
const summary = Array.from(node.children).find((child) => child.tagName === 'SUMMARY');
|
|
32
|
+
if (!summary || !(summary === el || summary.contains(el)))
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Focus lands on the dialog itself when it opens (one Tab reaches the primary
|
|
39
|
+
* action), Tab is kept inside, Escape closes, and focus returns to the opener.
|
|
40
|
+
*/
|
|
41
|
+
function useModalFocus(active, onClose) {
|
|
42
|
+
const ref = useRef(null);
|
|
43
|
+
const onCloseRef = useRef(onClose);
|
|
44
|
+
onCloseRef.current = onClose;
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!active)
|
|
47
|
+
return undefined;
|
|
48
|
+
const opener = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
49
|
+
ref.current?.focus();
|
|
50
|
+
const onKeyDown = (event) => {
|
|
51
|
+
if (event.key === 'Escape' && onCloseRef.current) {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
onCloseRef.current();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (event.key !== 'Tab')
|
|
57
|
+
return;
|
|
58
|
+
const root = ref.current;
|
|
59
|
+
if (!root)
|
|
60
|
+
return;
|
|
61
|
+
const items = Array.from(root.querySelectorAll(FOCUSABLE)).filter(isReachable);
|
|
62
|
+
if (items.length === 0) {
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
root.focus();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const index = items.indexOf(document.activeElement);
|
|
68
|
+
// Spec §5.1: focus starts on the dialog itself, so the first forward Tab
|
|
69
|
+
// goes to the screen's primary action instead of whatever happens to be
|
|
70
|
+
// first in DOM order (the close button, or a disclosure summary).
|
|
71
|
+
if (!event.shiftKey && document.activeElement === root) {
|
|
72
|
+
const primary = items.find((item) => item.matches(PRIMARY_SELECTOR));
|
|
73
|
+
if (primary) {
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
primary.focus();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const next = event.shiftKey
|
|
80
|
+
? (index <= 0 ? items.length - 1 : index - 1)
|
|
81
|
+
: (index === items.length - 1 ? 0 : index + 1);
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
items[next]?.focus();
|
|
84
|
+
};
|
|
85
|
+
document.addEventListener('keydown', onKeyDown);
|
|
86
|
+
return () => {
|
|
87
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
88
|
+
opener?.focus();
|
|
89
|
+
};
|
|
90
|
+
}, [active]);
|
|
91
|
+
return ref;
|
|
92
|
+
}
|
|
93
|
+
const bodyClass = 'flex min-h-0 w-full max-w-[360px] flex-1 flex-col';
|
|
94
|
+
/**
|
|
95
|
+
* Outer frame of every sign-in surface. `window` fills the host window,
|
|
96
|
+
* `dialog` is a 400px layer, `page` is two columns. All three share the same
|
|
97
|
+
* 360px body, so a state looks the same wherever the host places it.
|
|
98
|
+
*/
|
|
99
|
+
export function PodSignInFrame({ presentation, ariaLabel, appIntro, children, modal = true, onClose, closeLabel, dataAttributes, }) {
|
|
100
|
+
const isModal = presentation === 'dialog' && modal;
|
|
101
|
+
const dialogRef = useModalFocus(isModal, onClose);
|
|
102
|
+
const intro = appIntro;
|
|
103
|
+
if (presentation === 'window') {
|
|
104
|
+
return (_jsx("div", { role: "region", "aria-label": ariaLabel, "data-pod-sign-in-frame": "window", ...dataAttributes, className: "pod-sign-in flex h-full min-h-[480px] w-full min-w-[320px] justify-center overflow-hidden bg-background text-foreground", children: _jsx("div", { className: bodyClass, children: children }) }));
|
|
105
|
+
}
|
|
106
|
+
if (presentation === 'page') {
|
|
107
|
+
return (_jsxs("div", { role: "region", "aria-label": ariaLabel, "data-pod-sign-in-frame": "page", ...dataAttributes, className: "pod-sign-in grid min-h-[100dvh] w-full bg-background text-foreground md:grid-cols-2", children: [intro ? (_jsx("aside", { "data-pod-sign-in": "intro", className: "hidden flex-col justify-center gap-6 bg-[hsl(var(--sunken))] px-12 py-10 md:flex", children: intro })) : null, _jsx("div", { className: cn('flex min-w-0 items-center justify-center px-4 py-8', intro ? '' : 'md:col-span-2'), children: _jsx("div", { className: bodyClass, children: children }) })] }));
|
|
108
|
+
}
|
|
109
|
+
const card = (_jsxs("div", { ref: dialogRef, role: isModal ? 'dialog' : 'region', "aria-modal": isModal ? true : undefined, "aria-label": ariaLabel, tabIndex: -1, "data-pod-sign-in-frame": "dialog", ...dataAttributes, className: cn('pod-sign-in relative flex max-h-[90dvh] w-[min(400px,calc(100vw-2rem))] flex-col overflow-hidden rounded-xl border border-border bg-card text-card-foreground shadow-lg focus:outline-none motion-safe:animate-in motion-safe:fade-in-0'), children: [isModal && onClose && closeLabel ? (_jsx("button", { type: "button", "aria-label": closeLabel, className: "absolute right-2 top-2 flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:bg-accent", onClick: onClose, children: _jsx("span", { "aria-hidden": "true", children: "\u00D7" }) })) : null, _jsx("div", { className: cn(bodyClass, 'mx-auto'), children: children })] }));
|
|
110
|
+
return isModal ? (_jsx("div", { className: "fixed inset-0 z-[var(--layer-modal)] flex items-center justify-center bg-background/80 p-4", children: card })) : (_jsx("div", { className: "flex w-full items-center justify-center p-4", children: card }));
|
|
111
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { StorageLocationKind } from './types';
|
|
2
|
+
export interface StorageBadgeProps {
|
|
3
|
+
kind: StorageLocationKind;
|
|
4
|
+
/** Announced by screen readers and shown on hover. */
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
/** Avatar corner badge: neutral icon for where the data lives (cloud or edge). */
|
|
8
|
+
export declare function StorageBadge({ kind, label }: StorageBadgeProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Cloud, Laptop } from 'lucide-react';
|
|
3
|
+
/** Avatar corner badge: neutral icon for where the data lives (cloud or edge). */
|
|
4
|
+
export function StorageBadge({ kind, label }) {
|
|
5
|
+
const Icon = kind === 'cloud' ? Cloud : Laptop;
|
|
6
|
+
return (_jsx("span", { role: "img", "aria-label": label, title: label, "data-storage-kind": kind, className: "flex h-[18px] w-[18px] items-center justify-center rounded-full border border-background bg-card text-foreground", children: _jsx(Icon, { "aria-hidden": "true", className: "h-3 w-3" }) }));
|
|
7
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SVGProps } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Xpod product mark (brand asset `xpod-app.svg`). It is the default icon of the
|
|
4
|
+
* sign-in service side; application icons always come from the host.
|
|
5
|
+
*/
|
|
6
|
+
export declare function XpodMark({ size, ...props }: {
|
|
7
|
+
size?: number;
|
|
8
|
+
} & Omit<SVGProps<SVGSVGElement>, 'width' | 'height'>): import("react").JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Xpod product mark (brand asset `xpod-app.svg`). It is the default icon of the
|
|
4
|
+
* sign-in service side; application icons always come from the host.
|
|
5
|
+
*/
|
|
6
|
+
export function XpodMark({ size = 24, ...props }) {
|
|
7
|
+
return (_jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 100 100", width: size, height: size, "aria-hidden": "true", focusable: "false", ...props, children: [_jsx("rect", { x: "10", y: "10", width: "80", height: "80", rx: "18", fill: "#563E84" }), _jsxs("g", { fill: "#F7F4ED", children: [_jsx("path", { d: "M31 24H52V45H73V73Q73 76 70 76H31Q28 76 28 73V27Q28 24 31 24Z" }), _jsx("path", { d: "M58 24L76 39H58Z" })] })] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { LucideIcon } from 'lucide-react';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import type { PodSignInCopy } from './copy';
|
|
4
|
+
import type { StorageLocationKind } from './types';
|
|
5
|
+
/** ok = healthy; unreachable = other devices cannot reach it; stopped = device online, Xpod not running. */
|
|
6
|
+
export type DeviceStatus = 'ok' | 'unreachable' | 'stopped' | 'offline';
|
|
7
|
+
/** One place a Pod can live: Xpod Cloud, or an edge device. Presentation data only. */
|
|
8
|
+
export interface DeviceSummary {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
kind: StorageLocationKind;
|
|
12
|
+
/** e.g. `node-7f3a.undefineds.co` */
|
|
13
|
+
address?: string;
|
|
14
|
+
podCount?: number;
|
|
15
|
+
status: DeviceStatus;
|
|
16
|
+
/** Overrides the default cloud / computer icon. */
|
|
17
|
+
icon?: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
export declare function deviceStatusLabel(status: DeviceStatus, copy: PodSignInCopy): string;
|
|
20
|
+
export declare function deviceKindLabel(kind: StorageLocationKind, copy: PodSignInCopy): string;
|
|
21
|
+
/** Icon, name, kind, address and a status dot with its word (never colour alone). */
|
|
22
|
+
export declare function DeviceIdentity({ device, copy, showStatus }: {
|
|
23
|
+
device: DeviceSummary;
|
|
24
|
+
copy: PodSignInCopy;
|
|
25
|
+
showStatus?: boolean;
|
|
26
|
+
}): import("react").JSX.Element;
|
|
27
|
+
export declare const sectionClass = "pod-sign-in flex flex-col gap-3 text-foreground";
|
|
28
|
+
export declare const listRowClass = "flex min-h-14 items-center gap-3 rounded-lg border border-border bg-card px-3 py-2";
|
|
29
|
+
export declare const sectionTitleClass = "text-[17px] font-semibold text-foreground";
|
|
30
|
+
/** Section title row: an 18px icon, the title, and one sentence saying what the section is for. */
|
|
31
|
+
export declare function SectionHeader({ id, icon: Icon, title, hint, actions }: {
|
|
32
|
+
id: string;
|
|
33
|
+
icon: LucideIcon;
|
|
34
|
+
title: string;
|
|
35
|
+
hint: string;
|
|
36
|
+
actions?: ReactNode;
|
|
37
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Cloud, Laptop } from 'lucide-react';
|
|
3
|
+
import { cn } from '../utils.js';
|
|
4
|
+
import { Hostname } from './parts.js';
|
|
5
|
+
const statusDotClass = {
|
|
6
|
+
ok: 'bg-success',
|
|
7
|
+
unreachable: 'bg-warning',
|
|
8
|
+
stopped: 'bg-muted-foreground',
|
|
9
|
+
offline: 'bg-border',
|
|
10
|
+
};
|
|
11
|
+
export function deviceStatusLabel(status, copy) {
|
|
12
|
+
switch (status) {
|
|
13
|
+
case 'ok': return copy.deviceStatusOk;
|
|
14
|
+
case 'unreachable': return copy.deviceStatusUnreachable;
|
|
15
|
+
case 'stopped': return copy.deviceStatusStopped;
|
|
16
|
+
case 'offline': return copy.deviceStatusOffline;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function deviceKindLabel(kind, copy) {
|
|
20
|
+
return kind === 'cloud' ? copy.deviceCloud : copy.deviceEdge;
|
|
21
|
+
}
|
|
22
|
+
/** Icon, name, kind, address and a status dot with its word (never colour alone). */
|
|
23
|
+
export function DeviceIdentity({ device, copy, showStatus = true }) {
|
|
24
|
+
const Icon = device.kind === 'cloud' ? Cloud : Laptop;
|
|
25
|
+
return (_jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-3 text-left", "data-device-id": device.id, children: [_jsx("span", { "aria-hidden": "true", className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground", children: device.icon ?? _jsx(Icon, { className: "h-4 w-4" }) }), _jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [_jsx("span", { className: "truncate text-sm font-medium text-foreground", children: device.name }), _jsx("span", { className: "shrink-0 rounded bg-muted px-1.5 text-xs text-muted-foreground", children: deviceKindLabel(device.kind, copy) })] }), _jsxs("span", { className: "flex min-w-0 flex-wrap items-center gap-x-2 text-xs text-muted-foreground", children: [device.address ? _jsx(Hostname, { className: "truncate", children: device.address }) : null, device.podCount !== undefined ? _jsx("span", { children: copy.podCount.replace('{count}', String(device.podCount)) }) : null, showStatus ? (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx("span", { "aria-hidden": "true", className: cn('h-2 w-2 rounded-full', statusDotClass[device.status]) }), deviceStatusLabel(device.status, copy)] })) : null] })] })] }));
|
|
26
|
+
}
|
|
27
|
+
export const sectionClass = 'pod-sign-in flex flex-col gap-3 text-foreground';
|
|
28
|
+
export const listRowClass = 'flex min-h-14 items-center gap-3 rounded-lg border border-border bg-card px-3 py-2';
|
|
29
|
+
export const sectionTitleClass = 'text-[17px] font-semibold text-foreground';
|
|
30
|
+
/** Section title row: an 18px icon, the title, and one sentence saying what the section is for. */
|
|
31
|
+
export function SectionHeader({ id, icon: Icon, title, hint, actions }) {
|
|
32
|
+
return (_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [_jsx(Icon, { "aria-hidden": "true", className: "mt-0.5 h-[18px] w-[18px] shrink-0 text-primary" }), _jsxs("div", { className: "min-w-0", children: [_jsx("h2", { id: id, className: sectionTitleClass, children: title }), _jsx("p", { className: "text-[13px] text-muted-foreground", children: hint })] })] }), actions] }));
|
|
33
|
+
}
|