@devxcrew/ui 1.0.81 → 1.1.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.
@@ -2,14 +2,26 @@
2
2
 
3
3
  ## Version State
4
4
 
5
- Current version: 1.0.81
5
+ Current version: 1.1.0
6
6
 
7
- Release tag: v-1.0.81
7
+ Release tag: v-1.1.0
8
8
 
9
- Changelog label: v 1.0.81
9
+ Changelog label: v 1.1.0
10
10
 
11
11
  History starts with this standalone package baseline. CXApp business history is not copied.
12
12
 
13
+ ## v-1.1.0
14
+
15
+ ### [v 1.1.0] 2026-10-02 1:36 pm - Shared public pages and three identity desks
16
+
17
+ #### Database Changes
18
+
19
+ - Database update: No (manual).
20
+
21
+ #### App Codebase Changes
22
+
23
+ - Added configurable public pages, three desk login forms, session-aware desk shells and logout through the app-local identity API.
24
+
13
25
  ## v-1.0.81
14
26
 
15
27
  ### [v 1.0.81] 2026-10-02 12:08 pm - Devxcrew replacement release
@@ -10,6 +10,8 @@ Own reusable controls, accessible interaction primitives, layout wrappers, styli
10
10
 
11
11
  ## Public exports
12
12
 
13
+ layouts/identity-portal provides shared public pages, three login pages and desk shells. Apps supply PublicApp configuration, public policy content and business desk content. The portal uses the app-local /api/auth and /api/desks HTTP endpoints. It stores no credentials or tokens in browser storage. Server-side authorization remains mandatory.
14
+
13
15
  | Export | Responsibility |
14
16
  | --------------------------------- | --------------------------------------------------------------------------------- |
15
17
  | components/* | Radix-based controls, inputs, dialogs, tables, charts, loading states and editors |
@@ -5,6 +5,7 @@ export * from "./layouts/admin-layout";
5
5
  export * from "./layouts/application-layout";
6
6
  export * from "./layouts/app-layout";
7
7
  export * from "./layouts/auth-layout";
8
+ export * from "./layouts/identity-portal";
8
9
  export * from "./layouts/super-layout";
9
10
  export * from "./layouts/web-layout";
10
11
  export * from "./components/aspect-ratio";
package/dist/src/index.js CHANGED
@@ -5,6 +5,7 @@ export * from "./layouts/admin-layout";
5
5
  export * from "./layouts/application-layout";
6
6
  export * from "./layouts/app-layout";
7
7
  export * from "./layouts/auth-layout";
8
+ export * from "./layouts/identity-portal";
8
9
  export * from "./layouts/super-layout";
9
10
  export * from "./layouts/web-layout";
10
11
  export * from "./components/aspect-ratio";
@@ -0,0 +1,15 @@
1
+ import { type ReactNode } from "react";
2
+ type Desk = "sa" | "admin" | "tenant";
3
+ export type PublicApp = {
4
+ name: string;
5
+ applicationId: string;
6
+ version: string;
7
+ mode: "single" | "multi";
8
+ clientId: string | null;
9
+ };
10
+ export declare function IdentityPortal({ app, deskContent, publicContent }: {
11
+ app: PublicApp;
12
+ deskContent?: Partial<Record<Desk, ReactNode>>;
13
+ publicContent?: Partial<Record<"about" | "privacy" | "terms", ReactNode>>;
14
+ }): import("react").JSX.Element;
15
+ export {};
@@ -0,0 +1,115 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { AuthLayout } from "./auth-layout";
4
+ import { AppLayout } from "./app-layout";
5
+ import { Button } from "../components/button";
6
+ import { Input } from "../components/input";
7
+ import { Label } from "../components/label";
8
+ const desks = [
9
+ { id: "tenant", label: "Client desk", description: "Access your company workspace." },
10
+ { id: "admin", label: "Administration", description: "Manage application operations." },
11
+ { id: "sa", label: "Super Admin", description: "Manage application administration." }
12
+ ];
13
+ export function IdentityPortal({ app, deskContent, publicContent }) {
14
+ const segments = window.location.pathname.split("/").filter(Boolean);
15
+ const desk = desks.find((item) => item.id === segments[0]);
16
+ if (desk && segments.length === 2 && segments[1] === "login")
17
+ return _jsx(LoginPage, { app: app, desk: desk.id });
18
+ if (desk && segments.length === 1)
19
+ return (_jsx(DeskPage, { app: app, desk: desk.id, children: deskContent?.[desk.id] }));
20
+ const page = segments[0];
21
+ if (segments.length > 1 || (page && !["login", "about", "privacy", "terms"].includes(page)))
22
+ return (_jsxs(PublicShell, { app: app, children: [_jsx("h1", { className: "text-3xl font-semibold", children: "Page not found" }), _jsx("a", { href: "/", children: "Return home" })] }));
23
+ if (page === "about" || page === "privacy" || page === "terms")
24
+ return (_jsxs(PublicShell, { app: app, children: [_jsx("h1", { className: "text-3xl font-semibold", children: page === "about" ? `About ${app.name}` : page === "privacy" ? "Privacy" : "Terms" }), publicContent?.[page] ?? (_jsx("p", { children: "Contact your application administrator for this information." }))] }));
25
+ return (_jsxs(PublicShell, { app: app, children: [_jsxs("div", { className: "space-y-3", children: [_jsx("p", { className: "text-sm font-medium text-muted-foreground", children: app.name }), _jsx("h1", { className: "text-4xl font-semibold tracking-tight", children: "Your workspace, connected." }), _jsx("p", { className: "text-lg text-muted-foreground", children: "Choose your desk to sign in." })] }), _jsx("div", { className: "grid gap-6 sm:grid-cols-3", children: desks.map((item) => (_jsxs("div", { className: "space-y-4 border-t pt-5", children: [_jsx("h2", { className: "text-xl font-semibold", children: item.label }), _jsx("p", { className: "text-muted-foreground", children: item.description }), _jsx(Button, { asChild: true, children: _jsxs("a", { href: `/${item.id}/login`, children: ["Open ", item.label] }) })] }, item.id))) })] }));
26
+ }
27
+ function PublicShell({ app, children }) {
28
+ return (_jsxs("div", { className: "min-h-screen bg-background text-foreground", children: [_jsx("header", { className: "border-b", children: _jsxs("nav", { "aria-label": "Public navigation", className: "mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-4 px-6 py-5", children: [_jsx("a", { className: "text-xl font-semibold", href: "/", children: app.name }), _jsxs("div", { className: "flex gap-5", children: [_jsx("a", { href: "/about", children: "About" }), _jsx("a", { href: "/login", children: "Sign in" })] })] }) }), _jsx("main", { className: "mx-auto flex max-w-6xl flex-col gap-12 px-6 py-16", children: children }), _jsxs("footer", { className: "mx-auto flex max-w-6xl gap-6 px-6 py-8 text-sm", children: [_jsx("a", { href: "/privacy", children: "Privacy" }), _jsx("a", { href: "/terms", children: "Terms" })] })] }));
29
+ }
30
+ function LoginPage({ app, desk }) {
31
+ const label = desks.find((item) => item.id === desk).label;
32
+ const [error, setError] = useState("");
33
+ const [busy, setBusy] = useState(false);
34
+ async function submit(event) {
35
+ event.preventDefault();
36
+ setError("");
37
+ setBusy(true);
38
+ const form = new FormData(event.currentTarget);
39
+ try {
40
+ const response = await fetch(`/api/auth/${desk}/login`, {
41
+ method: "POST",
42
+ credentials: "same-origin",
43
+ headers: { "Content-Type": "application/json" },
44
+ body: JSON.stringify({
45
+ email: form.get("email"),
46
+ password: form.get("password"),
47
+ clientId: app.clientId ?? form.get("clientId")
48
+ })
49
+ });
50
+ const result = await response.json();
51
+ if (!response.ok)
52
+ throw new Error(result.message ?? "Sign-in failed. Try again.");
53
+ window.location.assign(`/${desk}`);
54
+ }
55
+ catch (failure) {
56
+ setError(failure instanceof Error ? failure.message : "Sign-in failed.");
57
+ setBusy(false);
58
+ }
59
+ }
60
+ return (_jsx(AuthLayout, { brandName: app.name, title: `${label} login`, surface: desk, description: `Sign in to ${label}.`, afterCard: _jsx("a", { href: "/", children: "Return home" }), children: _jsxs("form", { onSubmit: submit, className: "space-y-5", children: [_jsx("h2", { className: "text-lg font-semibold", children: label }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "email", children: "Email" }), _jsx(Input, { id: "email", name: "email", type: "email", autoComplete: "username", required: true, maxLength: 255 })] }), _jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "password", children: "Password" }), _jsx(Input, { id: "password", name: "password", type: "password", autoComplete: "current-password", required: true, maxLength: 256 })] }), !app.clientId && (_jsxs("div", { className: "space-y-2", children: [_jsx(Label, { htmlFor: "clientId", children: "Client code" }), _jsx(Input, { id: "clientId", name: "clientId", autoComplete: "organization", required: true, maxLength: 64 })] })), error && (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: error })), _jsx(Button, { type: "submit", className: "w-full", disabled: busy, children: busy ? "Signing in…" : "Sign in" })] }) }));
61
+ }
62
+ function DeskPage({ app, desk, children }) {
63
+ const [session, setSession] = useState(null);
64
+ const [error, setError] = useState("");
65
+ useEffect(() => {
66
+ const controller = new AbortController();
67
+ async function load() {
68
+ try {
69
+ const response = await fetch(`/api/desks/${desk}`, {
70
+ signal: controller.signal,
71
+ credentials: "same-origin",
72
+ cache: "no-store"
73
+ });
74
+ if (response.status === 401) {
75
+ window.location.replace(`/${desk}/login`);
76
+ return;
77
+ }
78
+ const result = await response.json();
79
+ if (!response.ok)
80
+ throw new Error(result.message ?? "This desk is unavailable.");
81
+ setSession(result.data);
82
+ }
83
+ catch (failure) {
84
+ if (!controller.signal.aborted)
85
+ setError(failure instanceof Error ? failure.message : "This desk is unavailable.");
86
+ }
87
+ }
88
+ void load();
89
+ const refresh = window.setInterval(() => void load(), 60000);
90
+ return () => {
91
+ controller.abort();
92
+ window.clearInterval(refresh);
93
+ };
94
+ }, [desk]);
95
+ async function logout() {
96
+ try {
97
+ const response = await fetch("/api/auth/logout", {
98
+ method: "POST",
99
+ credentials: "same-origin"
100
+ });
101
+ if (!response.ok)
102
+ throw new Error("Sign-out failed. Try again.");
103
+ window.location.assign(`/${desk}/login`);
104
+ }
105
+ catch (failure) {
106
+ setError(failure instanceof Error ? failure.message : "Sign-out failed.");
107
+ }
108
+ }
109
+ const label = desks.find((item) => item.id === desk).label;
110
+ if (error)
111
+ return (_jsxs(PublicShell, { app: app, children: [_jsx("h1", { className: "text-2xl font-semibold", children: label }), _jsx("p", { role: "alert", children: error }), _jsx("a", { href: `/${desk}/login`, children: "Return to sign in" })] }));
112
+ if (!session)
113
+ return (_jsx(PublicShell, { app: app, children: _jsx("p", { role: "status", children: "Loading your desk\u2026" }) }));
114
+ return (_jsx(AppLayout, { brand: { title: app.name, subtitle: label, href: `/${desk}` }, title: app.name, headerTitle: label, menuItems: [], workspaceItems: [], userMenuItems: [], homeHref: `/${desk}`, versionLabel: `v${app.version}`, children: _jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-4", children: [_jsxs("div", { children: [_jsxs("h1", { className: "text-2xl font-semibold", children: ["Welcome, ", session.name] }), _jsx("p", { className: "text-muted-foreground", children: session.email })] }), _jsx(Button, { variant: "outline", onClick: () => void logout(), children: "Sign out" })] }), children ?? _jsx("p", { children: "Your application modules will appear in this workspace." })] }) }));
115
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devxcrew/ui",
3
- "version": "1.0.81",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {