@owlmeans/web-panel 0.1.18-rc.25 → 0.1.18-rc.27
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/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/web-panel/SKILL.md +11 -1
- package/build/auth/modules.d.ts +1 -1
- package/build/auth/modules.d.ts.map +1 -1
- package/build/components/login/screen.d.ts.map +1 -1
- package/build/components/login/screen.js +23 -3
- package/build/components/login/screen.js.map +1 -1
- package/build/components/login/terms.d.ts.map +1 -1
- package/build/components/login/terms.js +4 -1
- package/build/components/login/terms.js.map +1 -1
- package/build/modules.d.ts +1 -1
- package/build/modules.d.ts.map +1 -1
- package/package.json +1 -1
- package/probe.d.mts +2 -0
- package/probe.d.mts.map +1 -0
- package/probe2.d.mts +2 -0
- package/probe2.d.mts.map +1 -0
- package/src/components/login/screen.tsx +36 -5
- package/src/components/login/terms.tsx +5 -2
- package/tests/harness/mount.tsx +7 -1
- package/tests/login.spec.ts +74 -1
- package/build/.gitkeep +0 -0
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/web-panel",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-
|
|
4
|
+
"version": "0.1.18-rc.27",
|
|
5
|
+
"generatedAt": "2026-09-01T12:30:48.603Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -8,7 +8,7 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/web-panel
|
|
9
9
|
|
|
10
10
|
**Layer:** Web (React)
|
|
11
|
-
**Install:** `"@owlmeans/web-panel": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/web-panel": "^0.1.18-rc.27"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
## Key Exports
|
|
14
14
|
|
|
@@ -18,6 +18,7 @@ user-invocable: false
|
|
|
18
18
|
| `NavLayout` | The application shell — header, section menu, screen menu, content, footer |
|
|
19
19
|
| `TopNav` / `SideNav` / `Footer` | The shell's pieces, mountable on their own |
|
|
20
20
|
| `Toaster` | The application's toast surface — mounted once, in the layout |
|
|
21
|
+
| `LoginScreen` / `LocalizedLoginScreen` / `appendLoginScreen` | The identity-provider choice screen — see `login-methods` |
|
|
21
22
|
| `components` submodule | shadcn/Radix panel/form components |
|
|
22
23
|
| Re-exports from `@owlmeans/client-panel` | Cross-platform panel primitives, incl. `usePanelNav` and the `PanelNav*` types |
|
|
23
24
|
| `main`, `exports`, `context`, `modules`, `types` | Wiring helpers |
|
|
@@ -174,6 +175,15 @@ export const MainLayout: FC<PropsWithChildren> = ({ children }) => <>
|
|
|
174
175
|
- `sonner` is a dependency of this package, so nothing is required of the consumer — but an app
|
|
175
176
|
raising its own toasts should declare `sonner` too, at a range that resolves to the same copy.
|
|
176
177
|
|
|
178
|
+
## Subpath: `./consent`
|
|
179
|
+
|
|
180
|
+
`PanelCookieConsent` and `PanelCookiePolicy` — `@owlmeans/web-consent`'s components bound to this
|
|
181
|
+
app's language and translations, falling through to the package's own seven-language bundle for
|
|
182
|
+
every key the app has not overridden. See the `consent` skill.
|
|
183
|
+
|
|
184
|
+
A re-export does not move Tailwind class strings, so a consumer still adds an `@source` for
|
|
185
|
+
`@owlmeans/web-consent/build` as well as for this package's.
|
|
186
|
+
|
|
177
187
|
## Consumer setup — the `@` contract and Tailwind
|
|
178
188
|
|
|
179
189
|
`web-panel` emits `@/components/ui/*` and `@/lib/utils` verbatim; the app's bundler resolves `@` to
|
package/build/auth/modules.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const modules:
|
|
1
|
+
export declare const modules: any[];
|
|
2
2
|
//# sourceMappingURL=modules.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modules.d.ts","sourceRoot":"","sources":["../../src/auth/modules.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,OAAO,
|
|
1
|
+
{"version":3,"file":"modules.d.ts","sourceRoot":"","sources":["../../src/auth/modules.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,OAAO,OAAuB,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screen.d.ts","sourceRoot":"","sources":["../../../src/components/login/screen.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"screen.d.ts","sourceRoot":"","sources":["../../../src/components/login/screen.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAQ/B,OAAO,KAAK,EAAe,gBAAgB,EAAE,MAAM,6BAA6B,CAAA;AAWhF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,gBAAgB,CA6F5C,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,EAAE,EAAE,CAAC,gBAAgB,CAOrD,CAAA"}
|
|
@@ -6,6 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
|
|
|
6
6
|
import { cn } from '@/lib/utils';
|
|
7
7
|
import { useI18nLib } from '@owlmeans/client-i18n';
|
|
8
8
|
import { useLoginMethods } from '@owlmeans/client-panel/auth';
|
|
9
|
+
import { loginAttemptError } from '@owlmeans/client-auth/login';
|
|
9
10
|
import { LoginMethodIcon } from './icons.js';
|
|
10
11
|
import { LoginTerms } from './terms.js';
|
|
11
12
|
import { LoginCredit } from './credit.js';
|
|
@@ -33,12 +34,31 @@ export const LoginScreen = props => {
|
|
|
33
34
|
...(props.methods != null ? { methods: props.methods } : {}),
|
|
34
35
|
});
|
|
35
36
|
const Logo = props.Logo;
|
|
36
|
-
|
|
37
|
+
const attemptError = model.busy == null ? loginAttemptError(model.outcome) : null;
|
|
38
|
+
// The viewport height is set INLINE, and deliberately.
|
|
39
|
+
//
|
|
40
|
+
// A percentage minimum (`min-h-full`) resolves against a parent that has a height, and this
|
|
41
|
+
// screen is rendered straight out of the dispatcher into whatever the app has around it — which
|
|
42
|
+
// almost never carries one, so the box collapsed to its content and the card sat at the top of
|
|
43
|
+
// an empty page. The fix is a viewport unit; the reason it is not a utility class is that this
|
|
44
|
+
// package's classes reach a consuming app's stylesheet only through a `@source` scan of its
|
|
45
|
+
// `src`, and a class NEW to that stylesheet is the one thing the scan can be stale about. Every
|
|
46
|
+
// other utility here already exists in any app that renders anything; a missing `min-h-dvh`
|
|
47
|
+
// rule, by contrast, is invisible — the screen looks nearly right and is silently uncentred.
|
|
48
|
+
//
|
|
49
|
+
// `props.style` still wins, because a class-based override no longer can.
|
|
50
|
+
return _jsx("div", { "data-login-screen": true, style: { minHeight: '100dvh', ...props.style }, className: cn('flex w-full items-center justify-center p-4', props.className), children: _jsxs(Card, { className: cn('w-full max-w-sm', props.containerClassName), children: [_jsxs(CardHeader, { className: "items-center gap-2 text-center", children: [Logo != null && _jsx("div", { className: "flex justify-center pb-2", children: isValidElement(Logo) ? Logo : typeof Logo === 'function'
|
|
37
51
|
? _jsx(Logo, { className: "h-10 w-auto" }) : Logo }), _jsx(CardTitle, { className: "text-xl", children: props.title ?? t('login.title', 'Sign in') }), _jsx(CardDescription, { children: props.subtitle ?? t('login.subtitle', 'Choose how you would like to continue.') })] }), _jsxs(CardContent, { className: "flex flex-col gap-4", children: [model.methods.length < 1
|
|
38
52
|
? _jsx("p", { role: "status", className: "text-sm text-muted-foreground text-center", children: t('login.empty', 'No sign-in method is configured for this application.') })
|
|
39
|
-
: _jsx("div", { className: "flex flex-col gap-2", children: model.methods.map((method) => _jsxs(Button, { type: "button", "data-login-method": method.id, variant: VARIANT[method.emphasis ?? 'secondary'] ?? 'outline', "aria-disabled": model.blocked, "data-blocked": model.blocked ? 'true' : undefined,
|
|
53
|
+
: _jsx("div", { className: "flex flex-col gap-2", children: model.methods.map((method) => _jsxs(Button, { type: "button", "data-login-method": method.id, variant: VARIANT[method.emphasis ?? 'secondary'] ?? 'outline', "aria-disabled": model.blocked, "data-blocked": model.blocked ? 'true' : undefined,
|
|
54
|
+
// `cursor-pointer` explicitly: this screen renders through the CONSUMER's vendored
|
|
55
|
+
// `@/components/ui/button`, and an app whose shadcn copy predates the cursor rule
|
|
56
|
+
// shows an arrow over the one control on the page. Stating it here makes the screen
|
|
57
|
+
// behave the same whichever copy it lands on.
|
|
58
|
+
className: cn('w-full justify-center gap-2 cursor-pointer', model.blocked && 'opacity-60'), autoFocus: method.id === model.primary?.id, onClick: () => model.select(method), children: [model.busy === method.id
|
|
40
59
|
? _jsx(Loader2, { className: "size-4 animate-spin" })
|
|
41
|
-
: _jsx(LoginMethodIcon, { name: method.icon, className: "size-4" }), method.label ?? t(`login.method.${method.i18nKey ?? method.id}`, method.id)] }, method.id)) }), model.terms.required && _jsx(LoginTerms, { model: model.terms, translate: t }),
|
|
60
|
+
: _jsx(LoginMethodIcon, { name: method.icon, className: "size-4" }), method.label ?? t(`login.method.${method.i18nKey ?? method.id}`, method.id)] }, method.id)) }), model.terms.required && _jsx(LoginTerms, { model: model.terms, translate: t }), (model.error ?? attemptError) != null
|
|
61
|
+
&& _jsx("p", { role: "alert", "data-login-error": true, className: "text-sm text-destructive text-center", children: model.error ?? t(attemptError, 'Sign-in did not complete. Please try again.') })] }), _jsx("div", { className: "px-6", children: props.footer ?? _jsx(LoginCredit, { model: model.credit, translate: t }) })] }) });
|
|
42
62
|
};
|
|
43
63
|
/**
|
|
44
64
|
* {@link LoginScreen} bound to the application's own translations.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screen.js","sourceRoot":"","sources":["../../../src/components/login/screen.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AAC/C,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAChG,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;
|
|
1
|
+
{"version":3,"file":"screen.js","sourceRoot":"","sources":["../../../src/components/login/screen.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"terms.d.ts","sourceRoot":"","sources":["../../../src/components/login/terms.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAa,MAAM,OAAO,CAAA;AAE1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAElE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,eAAe,CAAA;IACtB,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IACxD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAuBD,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC,eAAe,
|
|
1
|
+
{"version":3,"file":"terms.d.ts","sourceRoot":"","sources":["../../../src/components/login/terms.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAa,MAAM,OAAO,CAAA;AAE1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAElE,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,eAAe,CAAA;IACtB,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,MAAM,CAAA;IACxD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAuBD,eAAO,MAAM,UAAU,EAAE,EAAE,CAAC,eAAe,CA0C1C,CAAA"}
|
|
@@ -19,7 +19,10 @@ const interpolate = (sentence, slots) => sentence
|
|
|
19
19
|
});
|
|
20
20
|
export const LoginTerms = ({ model, translate, className }) => {
|
|
21
21
|
const link = (href, label) => _jsx("a", { href: href, target: "_blank", rel: "noreferrer noopener", className: "underline underline-offset-2 hover:text-foreground", children: label });
|
|
22
|
-
|
|
22
|
+
// Centred, like every other row in the card. The checkbox stays at the start of the sentence
|
|
23
|
+
// rather than above it, so `justify-center` centres the pair and `text-center` centres the
|
|
24
|
+
// wrapped lines within it.
|
|
25
|
+
return _jsxs("div", { className: cn('flex flex-col items-center gap-1.5 text-center', className), children: [_jsxs("label", { className: "flex items-start justify-center gap-2 text-sm text-muted-foreground cursor-pointer", children: [_jsx("input", { type: "checkbox", "data-login-terms": true, className: "mt-0.5 size-4 shrink-0 accent-primary", checked: model.accepted, "aria-invalid": model.attempted && !model.accepted, onChange: event => model.accept(event.target.checked) }), _jsx("span", { children: interpolate(translate('login.terms.agreement', 'I have read and agree to the {{terms}} and the {{privacy}}.'), {
|
|
23
26
|
terms: link(model.urls.terms, translate('login.terms.terms', 'Terms & Conditions')),
|
|
24
27
|
privacy: link(model.urls.privacy, translate('login.terms.privacy', 'Privacy Policy')),
|
|
25
28
|
cookies: model.urls.cookies != null
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"terms.js","sourceRoot":"","sources":["../../../src/components/login/terms.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAShC;;;;;;;;GAQG;AACH,MAAM,WAAW,GAAG,CAClB,QAAgB,EAAE,KAAgC,EACrC,EAAE,CAAC,QAAQ;KACvB,KAAK,CAAC,mBAAmB,CAAC;KAC1B,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;IACnB,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAE9C,OAAO,KAAK,IAAI,IAAI;QAClB,CAAC,CAAC,yBAAmB,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,IAA/B,KAAK,CAAkC;QACpD,CAAC,CAAC,yBAAmB,IAAI,IAAZ,KAAK,CAAe,CAAA;AACrC,CAAC,CAAC,CAAA;AAEJ,MAAM,CAAC,MAAM,UAAU,GAAwB,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,EAAE;IACjF,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,KAAa,EAAa,EAAE,CACtD,YAAG,IAAI,EAAE,IAAI,EAAE,MAAM,EAAC,QAAQ,EAAC,GAAG,EAAC,qBAAqB,EACtD,SAAS,EAAC,oDAAoD,YAAE,KAAK,GAAK,CAAA;IAE9E,OAAO,eAAK,SAAS,EAAE,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"terms.js","sourceRoot":"","sources":["../../../src/components/login/terms.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAShC;;;;;;;;GAQG;AACH,MAAM,WAAW,GAAG,CAClB,QAAgB,EAAE,KAAgC,EACrC,EAAE,CAAC,QAAQ;KACvB,KAAK,CAAC,mBAAmB,CAAC;KAC1B,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;IACnB,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAE9C,OAAO,KAAK,IAAI,IAAI;QAClB,CAAC,CAAC,yBAAmB,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,IAA/B,KAAK,CAAkC;QACpD,CAAC,CAAC,yBAAmB,IAAI,IAAZ,KAAK,CAAe,CAAA;AACrC,CAAC,CAAC,CAAA;AAEJ,MAAM,CAAC,MAAM,UAAU,GAAwB,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,EAAE;IACjF,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,KAAa,EAAa,EAAE,CACtD,YAAG,IAAI,EAAE,IAAI,EAAE,MAAM,EAAC,QAAQ,EAAC,GAAG,EAAC,qBAAqB,EACtD,SAAS,EAAC,oDAAoD,YAAE,KAAK,GAAK,CAAA;IAE9E,6FAA6F;IAC7F,2FAA2F;IAC3F,2BAA2B;IAC3B,OAAO,eAAK,SAAS,EAAE,EAAE,CAAC,gDAAgD,EAAE,SAAS,CAAC,aACpF,iBAAO,SAAS,EAAC,oFAAoF,aAOnG,gBACE,IAAI,EAAC,UAAU,4BACf,SAAS,EAAC,uCAAuC,EACjD,OAAO,EAAE,KAAK,CAAC,QAAQ,kBACT,KAAK,CAAC,SAAS,IAAI,CAAC,KAAK,CAAC,QAAQ,EAChD,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,GACrD,EACF,yBACG,WAAW,CACV,SAAS,CAAC,uBAAuB,EAC/B,6DAA6D,CAAC,EAChE;4BACE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,SAAS,CAAC,mBAAmB,EAAE,oBAAoB,CAAC,CAAC;4BACnF,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,qBAAqB,EAAE,gBAAgB,CAAC,CAAC;4BACrF,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,OAAO,IAAI,IAAI;gCACjC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC,qBAAqB,EAAE,eAAe,CAAC,CAAC;gCAC7E,CAAC,CAAC,IAAI;yBACT,CACF,GACI,IACD,EACP,KAAK,CAAC,SAAS,IAAI,CAAC,KAAK,CAAC,QAAQ,IAAI,YAAG,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,0BAA0B,YACxF,SAAS,CAAC,sBAAsB,EAC/B,2EAA2E,CAAC,GAC5E,IACA,CAAA;AACR,CAAC,CAAA"}
|
package/build/modules.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const modules:
|
|
1
|
+
export declare const modules: any[];
|
|
2
2
|
//# sourceMappingURL=modules.d.ts.map
|
package/build/modules.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"modules.d.ts","sourceRoot":"","sources":["../src/modules.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,OAAO,
|
|
1
|
+
{"version":3,"file":"modules.d.ts","sourceRoot":"","sources":["../src/modules.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,OAAO,OAAuB,CAAA"}
|
package/package.json
CHANGED
package/probe.d.mts
ADDED
package/probe.d.mts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probe.d.mts","sourceRoot":"","sources":["probe.mjs"],"names":[],"mappings":""}
|
package/probe2.d.mts
ADDED
package/probe2.d.mts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"probe2.d.mts","sourceRoot":"","sources":["probe2.mjs"],"names":[],"mappings":""}
|
|
@@ -6,6 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
|
|
|
6
6
|
import { cn } from '@/lib/utils'
|
|
7
7
|
import { useI18nLib } from '@owlmeans/client-i18n'
|
|
8
8
|
import { useLoginMethods } from '@owlmeans/client-panel/auth'
|
|
9
|
+
import { loginAttemptError } from '@owlmeans/client-auth/login'
|
|
9
10
|
import type { LoginMethod, LoginScreenProps } from '@owlmeans/client-auth/login'
|
|
10
11
|
import { LoginMethodIcon } from './icons.js'
|
|
11
12
|
import { LoginTerms } from './terms.js'
|
|
@@ -37,8 +38,25 @@ export const LoginScreen: FC<LoginScreenProps> = props => {
|
|
|
37
38
|
})
|
|
38
39
|
|
|
39
40
|
const Logo = props.Logo
|
|
41
|
+
const attemptError = model.busy == null ? loginAttemptError(model.outcome) : null
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
// The viewport height is set INLINE, and deliberately.
|
|
44
|
+
//
|
|
45
|
+
// A percentage minimum (`min-h-full`) resolves against a parent that has a height, and this
|
|
46
|
+
// screen is rendered straight out of the dispatcher into whatever the app has around it — which
|
|
47
|
+
// almost never carries one, so the box collapsed to its content and the card sat at the top of
|
|
48
|
+
// an empty page. The fix is a viewport unit; the reason it is not a utility class is that this
|
|
49
|
+
// package's classes reach a consuming app's stylesheet only through a `@source` scan of its
|
|
50
|
+
// `src`, and a class NEW to that stylesheet is the one thing the scan can be stale about. Every
|
|
51
|
+
// other utility here already exists in any app that renders anything; a missing `min-h-dvh`
|
|
52
|
+
// rule, by contrast, is invisible — the screen looks nearly right and is silently uncentred.
|
|
53
|
+
//
|
|
54
|
+
// `props.style` still wins, because a class-based override no longer can.
|
|
55
|
+
return <div
|
|
56
|
+
data-login-screen
|
|
57
|
+
style={{ minHeight: '100dvh', ...props.style }}
|
|
58
|
+
className={cn('flex w-full items-center justify-center p-4', props.className)}
|
|
59
|
+
>
|
|
42
60
|
<Card className={cn('w-full max-w-sm', props.containerClassName)}>
|
|
43
61
|
<CardHeader className="items-center gap-2 text-center">
|
|
44
62
|
{Logo != null && <div className="flex justify-center pb-2">
|
|
@@ -68,7 +86,14 @@ export const LoginScreen: FC<LoginScreenProps> = props => {
|
|
|
68
86
|
// why.
|
|
69
87
|
aria-disabled={model.blocked}
|
|
70
88
|
data-blocked={model.blocked ? 'true' : undefined}
|
|
71
|
-
|
|
89
|
+
// `cursor-pointer` explicitly: this screen renders through the CONSUMER's vendored
|
|
90
|
+
// `@/components/ui/button`, and an app whose shadcn copy predates the cursor rule
|
|
91
|
+
// shows an arrow over the one control on the page. Stating it here makes the screen
|
|
92
|
+
// behave the same whichever copy it lands on.
|
|
93
|
+
className={cn(
|
|
94
|
+
'w-full justify-center gap-2 cursor-pointer',
|
|
95
|
+
model.blocked && 'opacity-60'
|
|
96
|
+
)}
|
|
72
97
|
autoFocus={method.id === model.primary?.id}
|
|
73
98
|
onClick={() => model.select(method)}
|
|
74
99
|
>
|
|
@@ -81,9 +106,15 @@ export const LoginScreen: FC<LoginScreenProps> = props => {
|
|
|
81
106
|
|
|
82
107
|
{model.terms.required && <LoginTerms model={model.terms} translate={t} />}
|
|
83
108
|
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
|
|
109
|
+
{/*
|
|
110
|
+
A thrown message first, because it names the actual fault; otherwise whatever the
|
|
111
|
+
finished outcome means for someone still looking at this screen. An attempt that ends
|
|
112
|
+
without moving the document MUST say so — silence here reads as a dead button.
|
|
113
|
+
*/}
|
|
114
|
+
{(model.error ?? attemptError) != null
|
|
115
|
+
&& <p role="alert" data-login-error className="text-sm text-destructive text-center">
|
|
116
|
+
{model.error ?? t(attemptError as string, 'Sign-in did not complete. Please try again.')}
|
|
117
|
+
</p>}
|
|
87
118
|
</CardContent>
|
|
88
119
|
|
|
89
120
|
<div className="px-6">
|
|
@@ -34,8 +34,11 @@ export const LoginTerms: FC<LoginTermsProps> = ({ model, translate, className })
|
|
|
34
34
|
<a href={href} target="_blank" rel="noreferrer noopener"
|
|
35
35
|
className="underline underline-offset-2 hover:text-foreground">{label}</a>
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// Centred, like every other row in the card. The checkbox stays at the start of the sentence
|
|
38
|
+
// rather than above it, so `justify-center` centres the pair and `text-center` centres the
|
|
39
|
+
// wrapped lines within it.
|
|
40
|
+
return <div className={cn('flex flex-col items-center gap-1.5 text-center', className)}>
|
|
41
|
+
<label className="flex items-start justify-center gap-2 text-sm text-muted-foreground cursor-pointer">
|
|
39
42
|
{/*
|
|
40
43
|
A NATIVE checkbox, deliberately. `web-panel` ships no `checkbox` primitive, and requiring
|
|
41
44
|
every consumer to vendor one plus its Radix peer to render a sign-in screen would be a
|
package/tests/harness/mount.tsx
CHANGED
|
@@ -150,7 +150,13 @@ ensureLoginService(context as never).registerMethodSource({
|
|
|
150
150
|
alias: 'harness',
|
|
151
151
|
list: () => [
|
|
152
152
|
method('primary', { emphasis: 'primary', icon: 'google', label: 'Continue with Google' }),
|
|
153
|
-
method
|
|
153
|
+
// A method that finishes without moving the document — the shape of a relying party that
|
|
154
|
+
// could not build an authorization URL. The screen must SAY so; silence reads as a dead
|
|
155
|
+
// button, which is exactly the bug this pins.
|
|
156
|
+
method('secondary', {
|
|
157
|
+
order: 20, label: 'Sign in with a key',
|
|
158
|
+
start: async () => { started.push('secondary'); return LoginOutcome.Passed },
|
|
159
|
+
}),
|
|
154
160
|
method('operator', { order: 900, restricted: true, label: 'Secret key' }),
|
|
155
161
|
],
|
|
156
162
|
})
|
package/tests/login.spec.ts
CHANGED
|
@@ -44,7 +44,80 @@ describe('@owlmeans/web-panel — the sign-in screen', () => {
|
|
|
44
44
|
const credit = await page.locator('[data-login-credit]').textContent()
|
|
45
45
|
expect(credit).toContain('Powered by OwlMeans')
|
|
46
46
|
expect(credit).toContain('Harness')
|
|
47
|
-
|
|
47
|
+
// A copyright NOTICE, not a name: the mark and the year are what make it one.
|
|
48
|
+
expect(credit).toContain(`© ${new Date().getFullYear()} Acme`)
|
|
49
|
+
} finally {
|
|
50
|
+
await close()
|
|
51
|
+
}
|
|
52
|
+
}, TIMEOUT)
|
|
53
|
+
|
|
54
|
+
test('centres the card in the viewport, rather than stacking it at the top', async () => {
|
|
55
|
+
const { page, close } = await open()
|
|
56
|
+
try {
|
|
57
|
+
const screen = await page.locator('[data-login-screen]').boundingBox()
|
|
58
|
+
const viewport = page.viewportSize()
|
|
59
|
+
|
|
60
|
+
// A percentage minimum height resolves against a parent that HAS a height, and the
|
|
61
|
+
// dispatcher renders this screen into a chain with none — so `min-h-full` collapsed the
|
|
62
|
+
// box to its content and left the card at the top of an otherwise empty page.
|
|
63
|
+
expect(screen?.height).toBeGreaterThanOrEqual((viewport?.height ?? 0) - 1)
|
|
64
|
+
|
|
65
|
+
// The height on its own is not the requirement — where the CARD ends up is. Asserted from
|
|
66
|
+
// the card's own centre so the test fails on a lost `items-center` too, not only on a lost
|
|
67
|
+
// height, and against the SCREEN box rather than the window because this harness mounts the
|
|
68
|
+
// screen inside the app shell, one header down. A dispatcher renders it as the whole page,
|
|
69
|
+
// where the two coincide.
|
|
70
|
+
const card = await page.locator('[data-login-screen] > *').first().boundingBox()
|
|
71
|
+
const cardCentre = (card?.y ?? 0) + (card?.height ?? 0) / 2
|
|
72
|
+
const screenCentre = (screen?.y ?? 0) + (screen?.height ?? 0) / 2
|
|
73
|
+
|
|
74
|
+
expect(Math.abs(cardCentre - screenCentre)).toBeLessThanOrEqual(2)
|
|
75
|
+
} finally {
|
|
76
|
+
await close()
|
|
77
|
+
}
|
|
78
|
+
}, TIMEOUT)
|
|
79
|
+
|
|
80
|
+
test('an attempt that goes nowhere SAYS so, instead of reading as a dead button', async () => {
|
|
81
|
+
const { page, close } = await open()
|
|
82
|
+
try {
|
|
83
|
+
await page.locator('[data-login-terms]').check()
|
|
84
|
+
expect(await page.locator('[data-login-error]').count()).toBe(0)
|
|
85
|
+
|
|
86
|
+
// `secondary` finishes as `Passed` — it ran, and the document did not move. That is a valid
|
|
87
|
+
// answer to a dispatcher, which has a continuation, and a dead end on a screen.
|
|
88
|
+
await page.locator('[data-login-method="secondary"]').click()
|
|
89
|
+
await page.waitForSelector('[data-login-error]', { timeout: 10_000 })
|
|
90
|
+
|
|
91
|
+
expect(await page.evaluate(() => (window as never as { __loginStarted: string[] }).__loginStarted))
|
|
92
|
+
.toContain('secondary')
|
|
93
|
+
expect(await page.locator('[data-login-error]').textContent()).toBeTruthy()
|
|
94
|
+
} finally {
|
|
95
|
+
await close()
|
|
96
|
+
}
|
|
97
|
+
}, TIMEOUT)
|
|
98
|
+
|
|
99
|
+
test('offers the methods with a pointer cursor', async () => {
|
|
100
|
+
const { page, close } = await open()
|
|
101
|
+
try {
|
|
102
|
+
// The screen renders through the CONSUMER's vendored button, and an older shadcn copy has
|
|
103
|
+
// no cursor rule — so the one control on the page showed an arrow.
|
|
104
|
+
const cursor = await page.locator('[data-login-method]').first()
|
|
105
|
+
.evaluate(node => window.getComputedStyle(node).cursor)
|
|
106
|
+
|
|
107
|
+
expect(cursor).toBe('pointer')
|
|
108
|
+
} finally {
|
|
109
|
+
await close()
|
|
110
|
+
}
|
|
111
|
+
}, TIMEOUT)
|
|
112
|
+
|
|
113
|
+
test('centres the terms confirmation, like every other row in the card', async () => {
|
|
114
|
+
const { page, close } = await open()
|
|
115
|
+
try {
|
|
116
|
+
const align = await page.locator('[data-login-terms]').evaluate(
|
|
117
|
+
node => window.getComputedStyle(node.closest('label') as Element).textAlign
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
expect(align).toBe('center')
|
|
48
121
|
} finally {
|
|
49
122
|
await close()
|
|
50
123
|
}
|
package/build/.gitkeep
DELETED
|
File without changes
|