panel-frame-ui 0.0.0-stage → 0.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zakai Hamilton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,82 @@
1
- # Temporary Holding Version
1
+ # Panel Frame
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Panel Frame is a React UI package for the shared dashboards and account screens in the Visitoring, Perminister, Nanogres, and Postparticle applications.
4
+
5
+ The package standardizes structure and interaction patterns while letting each application provide its own brand, route links, account actions, and semantic theme colors.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pnpm add panel-frame-ui
11
+ ```
12
+
13
+ Import the stylesheet once from the application's root stylesheet or root layout:
14
+
15
+ ```css
16
+ @import "panel-frame-ui/styles.css";
17
+ ```
18
+
19
+ Then map the app's existing theme values to the `--pf-*` variables. At minimum, set `--pf-background`, `--pf-surface`, `--pf-text`, `--pf-muted`, `--pf-border`, `--pf-accent`, and `--pf-accent-contrast`. The package has sensible defaults for other semantic colors.
20
+
21
+ ## Dashboard shell
22
+
23
+ The shell owns the left sidebar, mobile drawer, toolbar, and right-hand page area. Pass app-owned navigation links and controls as slots so Next.js `Link`, authorization rules, and product-specific actions stay in the application.
24
+
25
+ ```tsx
26
+ import { DashboardShell } from "panel-frame-ui";
27
+
28
+ <DashboardShell
29
+ brand={
30
+ <a className="pf-brand" href="/dashboard">
31
+ Product
32
+ </a>
33
+ }
34
+ navigation={
35
+ <a className="pf-sidebar-link" href="/dashboard" aria-current="page">
36
+ Overview
37
+ </a>
38
+ }
39
+ toolbar={<AccountActions />}
40
+ >
41
+ <PageContent />
42
+ </DashboardShell>;
43
+ ```
44
+
45
+ Use `pf-sidebar-group`, `pf-sidebar-group-label`, and `pf-sidebar-link` for a consistent navigation hierarchy and active state.
46
+ Set `contentAsMain={false}` when the page content already provides its own `<main>` landmark.
47
+
48
+ ## Authentication shell
49
+
50
+ ```tsx
51
+ import { AuthCard, AuthShell } from "panel-frame-ui";
52
+
53
+ <AuthShell brand={<Brand />} aside={<AuthIntro />} utility={<ThemePicker />}>
54
+ <AuthCard>
55
+ <LoginForm />
56
+ </AuthCard>
57
+ </AuthShell>;
58
+ ```
59
+
60
+ The app owns all form actions, identity-provider links, and recovery URLs. Panel Frame only provides the common frame and visual primitives.
61
+
62
+ ## Components
63
+
64
+ The package exports `DashboardShell`, `AuthShell`, `AuthCard`, `Button`, `Card`, `Badge`, `Alert`, `Field`, `TextField`, `SelectField`, `EmptyState`, `Skeleton`, `PageHeader`, and `ThemeSelect`.
65
+
66
+ React and React DOM are peer dependencies. The package does not depend on Next.js; pass framework links as React children in the app-owned navigation.
67
+
68
+ ## Repository checks
69
+
70
+ Run `pnpm health` to build the package, lint and format-check the source, run component tests, and check type safety, unused code, duplication, and package exports. GitHub Actions runs those checks on pushes and pull requests, along with OSV-Scanner for dependency vulnerabilities, Gitleaks for secrets, and actionlint for workflow syntax.
71
+
72
+ The package is ESM-only. Are The Types Wrong? checks the JavaScript entry point; Publint checks the stylesheet export and published files.
73
+
74
+ ## Releases
75
+
76
+ This repository uses Changesets. Add a changeset with `pnpm changeset` when a change is ready for release. Merging it to `main` opens or updates a version pull request. Merging that version pull request publishes the package and creates a GitHub release. Versioning permissions and npm publishing permissions are separated into different workflow jobs.
77
+
78
+ The npm package is published publicly with npm trusted publishing from `.github/workflows/release.yml`. For the first publish, use Node 22.14 or later and npm 11.5.1 or later, run `pnpm install --frozen-lockfile` and `pnpm health`, then publish version `0.1.0` with `npm publish --access public` from an npm account you control. Then, in the package's npm settings, add a GitHub Actions trusted publisher for user `zakaihamilton`, repository `panel-frame-ui`, and workflow `release.yml`, and allow direct `npm publish`. Enable GitHub Actions to create pull requests in the repository settings. GitHub may ask a repository writer to approve the health workflow on the automatically created version pull request before its checks run.
79
+
80
+ ## License
81
+
82
+ Panel Frame UI is available under the MIT License. See [LICENSE](./LICENSE).
@@ -0,0 +1,11 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ export type AuthShellProps = {
3
+ brand: ReactNode;
4
+ aside: ReactNode;
5
+ utility?: ReactNode;
6
+ children: ReactNode;
7
+ footer?: ReactNode;
8
+ };
9
+ export declare function AuthShell({ brand, aside, utility, children, footer, }: AuthShellProps): import("react").JSX.Element;
10
+ export declare function AuthCard({ className, ...props }: HTMLAttributes<HTMLElement>): import("react").JSX.Element;
11
+ //# sourceMappingURL=auth-shell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-shell.d.ts","sourceRoot":"","sources":["../src/auth-shell.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvD,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,SAAS,CAAC;IACjB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;IACpB,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB,CAAC;AAEF,wBAAgB,SAAS,CAAC,EACxB,KAAK,EACL,KAAK,EACL,OAAO,EACP,QAAQ,EACR,MAAM,GACP,EAAE,cAAc,+BAgBhB;AAED,wBAAgB,QAAQ,CAAC,EACvB,SAAc,EACd,GAAG,KAAK,EACT,EAAE,cAAc,CAAC,WAAW,CAAC,+BAE7B"}
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function AuthShell({ brand, aside, utility, children, footer, }) {
3
+ return (_jsx("main", { className: "pf-auth-shell", children: _jsxs("div", { className: "pf-auth-layout", children: [_jsxs("aside", { className: "pf-auth-aside", children: [_jsx("div", { className: "pf-auth-brand", children: brand }), _jsx("div", { className: "pf-auth-aside-content", children: aside }), footer ? _jsx("div", { className: "pf-auth-aside-footer", children: footer }) : null] }), _jsxs("section", { "aria-label": "Account access", className: "pf-auth-main", children: [utility ? _jsx("div", { className: "pf-auth-utility", children: utility }) : null, _jsx("div", { className: "pf-auth-content", children: children })] })] }) }));
4
+ }
5
+ export function AuthCard({ className = "", ...props }) {
6
+ return _jsx("section", { className: `pf-auth-card ${className}`.trim(), ...props });
7
+ }
@@ -0,0 +1,13 @@
1
+ import { type ReactNode } from "react";
2
+ export type DashboardShellProps = {
3
+ brand: ReactNode;
4
+ navigation: ReactNode;
5
+ sidebarContext?: ReactNode;
6
+ sidebarFooter?: ReactNode;
7
+ toolbar?: ReactNode;
8
+ sidebarVisible?: boolean;
9
+ contentAsMain?: boolean;
10
+ children: ReactNode;
11
+ };
12
+ export declare function DashboardShell({ brand, navigation, sidebarContext, sidebarFooter, toolbar, sidebarVisible, contentAsMain, children, }: DashboardShellProps): import("react").JSX.Element;
13
+ //# sourceMappingURL=dashboard-shell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-shell.d.ts","sourceRoot":"","sources":["../src/dashboard-shell.tsx"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,SAAS,CAAC;IACjB,UAAU,EAAE,SAAS,CAAC;IACtB,cAAc,CAAC,EAAE,SAAS,CAAC;IAC3B,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAKF,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,UAAU,EACV,cAAc,EACd,aAAa,EACb,OAAO,EACP,cAAqB,EACrB,aAAoB,EACpB,QAAQ,GACT,EAAE,mBAAmB,+BA0JrB"}
@@ -0,0 +1,73 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState, } from "react";
4
+ const focusableSelector = 'a[href], button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])';
5
+ export function DashboardShell({ brand, navigation, sidebarContext, sidebarFooter, toolbar, sidebarVisible = true, contentAsMain = true, children, }) {
6
+ const [isMobile, setIsMobile] = useState(false);
7
+ const [mobileOpen, setMobileOpen] = useState(false);
8
+ const sidebarRef = useRef(null);
9
+ const triggerRef = useRef(null);
10
+ useEffect(() => {
11
+ const media = window.matchMedia("(max-width: 780px)");
12
+ const update = () => {
13
+ setIsMobile(media.matches);
14
+ if (!media.matches)
15
+ setMobileOpen(false);
16
+ };
17
+ update();
18
+ media.addEventListener("change", update);
19
+ return () => media.removeEventListener("change", update);
20
+ }, []);
21
+ useEffect(() => {
22
+ if (!isMobile || !mobileOpen)
23
+ return;
24
+ const previousOverflow = document.body.style.overflow;
25
+ document.body.style.overflow = "hidden";
26
+ const focusable = () => Array.from(sidebarRef.current?.querySelectorAll(focusableSelector) ??
27
+ []).filter((element) => !element.hasAttribute("hidden"));
28
+ focusable()[0]?.focus();
29
+ const onKeyDown = (event) => {
30
+ if (event.key === "Escape") {
31
+ event.preventDefault();
32
+ setMobileOpen(false);
33
+ triggerRef.current?.focus();
34
+ return;
35
+ }
36
+ if (event.key !== "Tab")
37
+ return;
38
+ const elements = focusable();
39
+ const first = elements[0];
40
+ const last = elements.at(-1);
41
+ if (!first || !last) {
42
+ event.preventDefault();
43
+ }
44
+ else if (event.shiftKey && document.activeElement === first) {
45
+ event.preventDefault();
46
+ last.focus();
47
+ }
48
+ else if (!event.shiftKey && document.activeElement === last) {
49
+ event.preventDefault();
50
+ first.focus();
51
+ }
52
+ };
53
+ document.addEventListener("keydown", onKeyDown);
54
+ return () => {
55
+ document.body.style.overflow = previousOverflow;
56
+ document.removeEventListener("keydown", onKeyDown);
57
+ };
58
+ }, [isMobile, mobileOpen]);
59
+ function closeAfterNavigation(event) {
60
+ if (isMobile &&
61
+ event.target instanceof Element &&
62
+ event.target.closest("a[href]")) {
63
+ setMobileOpen(false);
64
+ }
65
+ }
66
+ function closeDrawer() {
67
+ setMobileOpen(false);
68
+ triggerRef.current?.focus();
69
+ }
70
+ return (_jsxs("div", { className: "pf-dashboard-shell", "data-mobile-open": mobileOpen ? "true" : "false", "data-sidebar-visible": sidebarVisible ? "true" : "false", children: [_jsx("a", { className: "pf-skip-link", href: "#pf-main-content", children: "Skip to main content" }), _jsxs("aside", { ref: sidebarRef, "aria-hidden": (isMobile && !mobileOpen) || (!isMobile && !sidebarVisible)
71
+ ? true
72
+ : undefined, "aria-label": "Primary navigation", "aria-modal": isMobile && mobileOpen ? true : undefined, className: "pf-sidebar", id: "pf-dashboard-sidebar", inert: (isMobile && !mobileOpen) || (!isMobile && !sidebarVisible), onClick: closeAfterNavigation, role: isMobile && mobileOpen ? "dialog" : undefined, tabIndex: -1, children: [_jsx("div", { className: "pf-sidebar-brand", children: brand }), sidebarContext ? (_jsx("div", { className: "pf-sidebar-context", children: sidebarContext })) : null, _jsx("div", { className: "pf-sidebar-navigation", children: navigation }), _jsx("div", { className: "pf-sidebar-spacer" }), sidebarFooter ? (_jsx("div", { className: "pf-sidebar-footer", children: sidebarFooter })) : null] }), isMobile && mobileOpen ? (_jsx("button", { "aria-label": "Close dashboard navigation", className: "pf-sidebar-backdrop", onClick: closeDrawer, tabIndex: -1, type: "button" })) : null, _jsxs("div", { className: "pf-dashboard-main", children: [_jsxs("header", { className: "pf-dashboard-toolbar", children: [_jsx("button", { "aria-controls": "pf-dashboard-sidebar", "aria-expanded": mobileOpen, "aria-label": mobileOpen ? "Close navigation" : "Open navigation", className: "pf-mobile-menu-button", onClick: () => setMobileOpen((open) => !open), ref: triggerRef, type: "button", children: _jsx("span", { "aria-hidden": "true", children: "\u2630" }) }), toolbar ? _jsx("div", { className: "pf-toolbar-content", children: toolbar }) : null] }), contentAsMain ? (_jsx("main", { className: "pf-dashboard-content", id: "pf-main-content", tabIndex: -1, children: children })) : (_jsx("div", { className: "pf-dashboard-content", id: "pf-main-content", tabIndex: -1, children: children }))] })] }));
73
+ }
@@ -0,0 +1,5 @@
1
+ export { AuthCard, AuthShell, type AuthShellProps } from "./auth-shell.js";
2
+ export { DashboardShell, type DashboardShellProps } from "./dashboard-shell.js";
3
+ export { Alert, Badge, Button, Card, EmptyState, Field, PageHeader, SelectField, Skeleton, TextField, type ButtonVariant, } from "./primitives.js";
4
+ export { ThemeSelect, type ThemeChoice } from "./theme-select.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC3E,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAChF,OAAO,EACL,KAAK,EACL,KAAK,EACL,MAAM,EACN,IAAI,EACJ,UAAU,EACV,KAAK,EACL,UAAU,EACV,WAAW,EACX,QAAQ,EACR,SAAS,EACT,KAAK,aAAa,GACnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { AuthCard, AuthShell } from "./auth-shell.js";
2
+ export { DashboardShell } from "./dashboard-shell.js";
3
+ export { Alert, Badge, Button, Card, EmptyState, Field, PageHeader, SelectField, Skeleton, TextField, } from "./primitives.js";
4
+ export { ThemeSelect } from "./theme-select.js";
@@ -0,0 +1,49 @@
1
+ import { type ButtonHTMLAttributes, type HTMLAttributes, type InputHTMLAttributes, type ReactNode, type SelectHTMLAttributes } from "react";
2
+ export type ButtonVariant = "primary" | "secondary" | "quiet" | "danger";
3
+ export declare function Button({ variant, className, type, ...props }: ButtonHTMLAttributes<HTMLButtonElement> & {
4
+ variant?: ButtonVariant;
5
+ }): import("react").JSX.Element;
6
+ export declare function Card({ className, ...props }: HTMLAttributes<HTMLElement>): import("react").JSX.Element;
7
+ export declare function Badge({ tone, className, ...props }: HTMLAttributes<HTMLSpanElement> & {
8
+ tone?: "neutral" | "success" | "warning" | "danger" | "accent";
9
+ }): import("react").JSX.Element;
10
+ export declare function Alert({ tone, className, ...props }: HTMLAttributes<HTMLDivElement> & {
11
+ tone?: "info" | "success" | "warning" | "error";
12
+ }): import("react").JSX.Element;
13
+ export declare function Field({ label, hint, error, id, children, }: {
14
+ label: string;
15
+ hint?: string;
16
+ error?: string;
17
+ id?: string;
18
+ children: (props: {
19
+ id: string;
20
+ "aria-describedby"?: string;
21
+ "aria-invalid"?: true;
22
+ }) => ReactNode;
23
+ }): import("react").JSX.Element;
24
+ export declare function TextField({ label, hint, error, id, ...inputProps }: Omit<InputHTMLAttributes<HTMLInputElement>, "id"> & {
25
+ label: string;
26
+ hint?: string;
27
+ error?: string;
28
+ id?: string;
29
+ }): import("react").JSX.Element;
30
+ export declare function SelectField({ label, hint, error, id, children, ...selectProps }: Omit<SelectHTMLAttributes<HTMLSelectElement>, "id"> & {
31
+ label: string;
32
+ hint?: string;
33
+ error?: string;
34
+ id?: string;
35
+ children: ReactNode;
36
+ }): import("react").JSX.Element;
37
+ export declare function EmptyState({ title, description, action, }: {
38
+ title: string;
39
+ description?: ReactNode;
40
+ action?: ReactNode;
41
+ }): import("react").JSX.Element;
42
+ export declare function Skeleton({ className, ...props }: HTMLAttributes<HTMLDivElement>): import("react").JSX.Element;
43
+ export declare function PageHeader({ eyebrow, title, description, actions, }: {
44
+ eyebrow?: ReactNode;
45
+ title: ReactNode;
46
+ description?: ReactNode;
47
+ actions?: ReactNode;
48
+ }): import("react").JSX.Element;
49
+ //# sourceMappingURL=primitives.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../src/primitives.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId, } from "react";
3
+ export function Button({ variant = "primary", className = "", type = "button", ...props }) {
4
+ return (_jsx("button", { className: `pf-button pf-button-${variant} ${className}`.trim(), type: type, ...props }));
5
+ }
6
+ export function Card({ className = "", ...props }) {
7
+ return _jsx("section", { className: `pf-card ${className}`.trim(), ...props });
8
+ }
9
+ export function Badge({ tone = "neutral", className = "", ...props }) {
10
+ return (_jsx("span", { className: `pf-badge pf-badge-${tone} ${className}`.trim(), ...props }));
11
+ }
12
+ export function Alert({ tone = "info", className = "", ...props }) {
13
+ return (_jsx("div", { className: `pf-alert pf-alert-${tone} ${className}`.trim(), role: tone === "error" ? "alert" : "status", ...props }));
14
+ }
15
+ export function Field({ label, hint, error, id, children, }) {
16
+ const generatedId = useId();
17
+ const fieldId = id ?? generatedId;
18
+ const hintId = hint ? `${fieldId}-hint` : undefined;
19
+ const errorId = error ? `${fieldId}-error` : undefined;
20
+ const describedBy = [hintId, errorId].filter(Boolean).join(" ") || undefined;
21
+ return (_jsxs("label", { className: "pf-field", htmlFor: fieldId, children: [_jsx("span", { className: "pf-field-label", children: label }), children({
22
+ id: fieldId,
23
+ "aria-describedby": describedBy,
24
+ "aria-invalid": error ? true : undefined,
25
+ }), hint ? (_jsx("span", { className: "pf-field-hint", id: hintId, children: hint })) : null, error ? (_jsx("span", { className: "pf-field-error", id: errorId, children: error })) : null] }));
26
+ }
27
+ export function TextField({ label, hint, error, id, ...inputProps }) {
28
+ return (_jsx(Field, { label: label, hint: hint, error: error, id: id, children: (fieldProps) => (_jsx("input", { className: "pf-input", ...fieldProps, ...inputProps })) }));
29
+ }
30
+ export function SelectField({ label, hint, error, id, children, ...selectProps }) {
31
+ return (_jsx(Field, { label: label, hint: hint, error: error, id: id, children: (fieldProps) => (_jsx("select", { className: "pf-input pf-select", ...fieldProps, ...selectProps, children: children })) }));
32
+ }
33
+ export function EmptyState({ title, description, action, }) {
34
+ return (_jsxs("section", { className: "pf-empty-state", children: [_jsx("h2", { children: title }), description ? (_jsx("div", { className: "pf-empty-description", children: description })) : null, action ? _jsx("div", { className: "pf-empty-action", children: action }) : null] }));
35
+ }
36
+ export function Skeleton({ className = "", ...props }) {
37
+ return (_jsx("div", { "aria-hidden": "true", className: `pf-skeleton ${className}`.trim(), ...props }));
38
+ }
39
+ export function PageHeader({ eyebrow, title, description, actions, }) {
40
+ return (_jsxs("header", { className: "pf-page-header", children: [_jsxs("div", { className: "pf-page-heading", children: [eyebrow ? _jsx("p", { className: "pf-page-eyebrow", children: eyebrow }) : null, _jsx("h1", { children: title }), description ? (_jsx("p", { className: "pf-page-description", children: description })) : null] }), actions ? _jsx("div", { className: "pf-page-actions", children: actions }) : null] }));
41
+ }
@@ -0,0 +1,7 @@
1
+ import type { SelectHTMLAttributes } from "react";
2
+ export type ThemeChoice = "system" | "light" | "dark";
3
+ export declare function ThemeSelect({ value, onChange, className, ...props }: Omit<SelectHTMLAttributes<HTMLSelectElement>, "value" | "onChange"> & {
4
+ value: ThemeChoice;
5
+ onChange: (value: ThemeChoice) => void;
6
+ }): import("react").JSX.Element;
7
+ //# sourceMappingURL=theme-select.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-select.d.ts","sourceRoot":"","sources":["../src/theme-select.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAElD,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;AAEtD,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,SAAc,EACd,GAAG,KAAK,EACT,EAAE,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC,GAAG;IACvE,KAAK,EAAE,WAAW,CAAC;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,CAAC;CACxC,+BAiBA"}
@@ -0,0 +1,4 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function ThemeSelect({ value, onChange, className = "", ...props }) {
3
+ return (_jsxs("label", { className: `pf-theme-select ${className}`.trim(), children: [_jsx("span", { children: "Theme" }), _jsxs("select", { "aria-label": "Color theme", className: "pf-input", onChange: (event) => onChange(event.target.value), value: value, ...props, children: [_jsx("option", { value: "system", children: "System" }), _jsx("option", { value: "light", children: "Light" }), _jsx("option", { value: "dark", children: "Dark" })] })] }));
4
+ }
package/package.json CHANGED
@@ -1,6 +1,82 @@
1
1
  {
2
2
  "name": "panel-frame-ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Shared dashboard and account UI for Panel Frame projects",
5
+ "license": "MIT",
6
+ "engines": {
7
+ "node": ">=22.14.0"
8
+ },
9
+ "packageManager": "pnpm@12.16.1",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/zakaihamilton/panel-frame-ui.git"
13
+ },
14
+ "homepage": "https://github.com/zakaihamilton/panel-frame-ui#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/zakaihamilton/panel-frame-ui/issues"
17
+ },
18
+ "type": "module",
19
+ "sideEffects": [
20
+ "./styles.css"
21
+ ],
22
+ "files": [
23
+ "dist",
24
+ "styles.css",
25
+ "styles.css.d.ts",
26
+ "README.md",
27
+ "LICENSE",
28
+ "CHANGELOG.md"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "main": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js"
39
+ },
40
+ "./styles.css": {
41
+ "types": "./styles.css.d.ts",
42
+ "default": "./styles.css"
43
+ }
44
+ },
45
+ "scripts": {
46
+ "build": "tsc -p tsconfig.json",
47
+ "lint": "eslint .",
48
+ "format": "prettier --write .",
49
+ "format:check": "prettier --check .",
50
+ "test": "vitest run",
51
+ "prepack": "pnpm run build",
52
+ "health": "pnpm run build && pnpm run lint && pnpm run format:check && pnpm run test && repnix check",
53
+ "health:dead-code": "knip",
54
+ "health:duplication": "jscpd src",
55
+ "health:package:publint": "publint",
56
+ "health:package:types": "attw --pack ."
57
+ },
58
+ "peerDependencies": {
59
+ "react": ">=19.0.0 <20.0.0",
60
+ "react-dom": ">=19.0.0 <20.0.0"
61
+ },
62
+ "devDependencies": {
63
+ "@arethetypeswrong/cli": "^0.18.5",
64
+ "@changesets/cli": "^3.0.3",
65
+ "@eslint/js": "^9.39.5",
66
+ "@testing-library/react": "^16.3.3",
67
+ "@types/react": "^19.3.0",
68
+ "eslint": "^9.39.5",
69
+ "eslint-config-prettier": "^10.1.8",
70
+ "eslint-plugin-react-hooks": "^7.1.1",
71
+ "globals": "^17.14.0",
72
+ "jscpd": "^5.4.3",
73
+ "jsdom": "^31.0.0",
74
+ "knip": "^6.44.2",
75
+ "prettier": "^3.9.10",
76
+ "publint": "^0.3.25",
77
+ "repnix": "^1.1.0",
78
+ "typescript": "^6.0.3",
79
+ "typescript-eslint": "^8.72.2",
80
+ "vitest": "^5.1.0"
81
+ }
82
+ }
package/styles.css ADDED
@@ -0,0 +1,782 @@
1
+ :root {
2
+ --pf-background: #f5f7f6;
3
+ --pf-surface: #ffffff;
4
+ --pf-surface-raised: #ffffff;
5
+ --pf-text: #202b28;
6
+ --pf-muted: #65716d;
7
+ --pf-border: #dfe6e2;
8
+ --pf-accent: #236b55;
9
+ --pf-accent-contrast: #ffffff;
10
+ --pf-danger: #a44040;
11
+ --pf-danger-surface: #fff0ed;
12
+ --pf-success: #276147;
13
+ --pf-success-surface: #e7f3eb;
14
+ --pf-warning: #795920;
15
+ --pf-warning-surface: #fbf1d8;
16
+ --pf-sidebar-width: 256px;
17
+ --pf-content-width: 1440px;
18
+ --pf-radius-sm: 7px;
19
+ --pf-radius-md: 11px;
20
+ --pf-radius-lg: 16px;
21
+ --pf-shadow: 0 14px 38px rgb(23 43 34 / 8%);
22
+ --pf-focus: var(--pf-accent);
23
+ --pf-motion-fast: 150ms;
24
+ --pf-ease: cubic-bezier(0.2, 0.8, 0.2, 1);
25
+ }
26
+
27
+ .pf-dashboard-shell,
28
+ .pf-dashboard-shell *,
29
+ .pf-auth-shell,
30
+ .pf-auth-shell *,
31
+ .pf-component {
32
+ box-sizing: border-box;
33
+ }
34
+
35
+ .pf-dashboard-shell {
36
+ min-height: 100vh;
37
+ display: grid;
38
+ grid-template-columns: var(--pf-sidebar-width) minmax(0, 1fr);
39
+ color: var(--pf-text);
40
+ background: var(--pf-background);
41
+ font-family: var(--pf-font, inherit);
42
+ }
43
+
44
+ .pf-dashboard-shell[data-sidebar-visible="false"] {
45
+ grid-template-columns: 0 minmax(0, 1fr);
46
+ }
47
+
48
+ .pf-dashboard-shell[data-sidebar-visible="false"] .pf-sidebar {
49
+ width: 0;
50
+ padding-right: 0;
51
+ padding-left: 0;
52
+ overflow: hidden;
53
+ border: 0;
54
+ opacity: 0;
55
+ pointer-events: none;
56
+ }
57
+
58
+ .pf-skip-link {
59
+ position: fixed;
60
+ z-index: 100;
61
+ top: 12px;
62
+ left: 12px;
63
+ padding: 10px 14px;
64
+ border-radius: var(--pf-radius-sm);
65
+ color: var(--pf-accent-contrast);
66
+ background: var(--pf-accent);
67
+ transform: translateY(-160%);
68
+ }
69
+
70
+ .pf-skip-link:focus {
71
+ transform: translateY(0);
72
+ }
73
+
74
+ .pf-sidebar {
75
+ position: sticky;
76
+ top: 0;
77
+ z-index: 30;
78
+ display: flex;
79
+ width: var(--pf-sidebar-width);
80
+ height: 100vh;
81
+ min-height: 520px;
82
+ flex-direction: column;
83
+ padding: 20px 14px 14px;
84
+ overflow-y: auto;
85
+ border-right: 1px solid var(--pf-border);
86
+ color: var(--pf-text);
87
+ background: var(--pf-surface);
88
+ }
89
+
90
+ .pf-sidebar-brand {
91
+ display: flex;
92
+ min-height: 46px;
93
+ align-items: center;
94
+ padding: 0 10px 17px;
95
+ }
96
+
97
+ .pf-sidebar-brand > a,
98
+ .pf-brand {
99
+ display: inline-flex;
100
+ min-width: 0;
101
+ align-items: center;
102
+ gap: 10px;
103
+ color: inherit;
104
+ font-size: 17px;
105
+ font-weight: 720;
106
+ letter-spacing: -0.035em;
107
+ text-decoration: none;
108
+ }
109
+
110
+ .pf-sidebar-context {
111
+ margin: 0 0 16px;
112
+ }
113
+
114
+ .pf-visitoring-switchers {
115
+ display: grid;
116
+ gap: 8px;
117
+ }
118
+
119
+ .pf-visitoring-switchers > :where(a, form, div) {
120
+ min-width: 0;
121
+ }
122
+
123
+ .pf-sidebar-navigation {
124
+ display: grid;
125
+ align-content: start;
126
+ gap: 4px;
127
+ }
128
+
129
+ .pf-sidebar-group {
130
+ display: grid;
131
+ gap: 4px;
132
+ margin: 12px 0 8px;
133
+ }
134
+
135
+ .pf-sidebar-group-label {
136
+ margin: 0 10px 5px;
137
+ color: var(--pf-muted);
138
+ font-size: 11px;
139
+ font-weight: 700;
140
+ letter-spacing: 0.11em;
141
+ text-transform: uppercase;
142
+ }
143
+
144
+ .pf-sidebar-link {
145
+ display: flex;
146
+ min-height: 40px;
147
+ align-items: center;
148
+ gap: 11px;
149
+ padding: 8px 10px;
150
+ border: 1px solid transparent;
151
+ border-radius: var(--pf-radius-sm);
152
+ color: var(--pf-muted);
153
+ font-size: 13px;
154
+ font-weight: 590;
155
+ line-height: 1.35;
156
+ text-decoration: none;
157
+ transition:
158
+ color var(--pf-motion-fast) ease,
159
+ background-color var(--pf-motion-fast) ease,
160
+ border-color var(--pf-motion-fast) ease;
161
+ }
162
+
163
+ .pf-sidebar-link:hover {
164
+ color: var(--pf-text);
165
+ background: color-mix(in srgb, var(--pf-accent) 7%, var(--pf-surface));
166
+ }
167
+
168
+ .pf-sidebar-link[aria-current="page"],
169
+ .pf-sidebar-link[data-active="true"] {
170
+ border-color: color-mix(in srgb, var(--pf-accent) 18%, var(--pf-border));
171
+ color: var(--pf-accent);
172
+ background: color-mix(in srgb, var(--pf-accent) 10%, var(--pf-surface));
173
+ font-weight: 700;
174
+ }
175
+
176
+ .pf-sidebar-link :where(svg, [data-icon]) {
177
+ width: 18px;
178
+ height: 18px;
179
+ flex: 0 0 auto;
180
+ }
181
+
182
+ .pf-sidebar-spacer {
183
+ flex: 1 0 18px;
184
+ }
185
+
186
+ .pf-sidebar-footer {
187
+ display: grid;
188
+ gap: 10px;
189
+ padding: 12px 8px 2px;
190
+ border-top: 1px solid var(--pf-border);
191
+ }
192
+
193
+ .pf-dashboard-main {
194
+ min-width: 0;
195
+ min-height: 100vh;
196
+ }
197
+
198
+ .pf-dashboard-toolbar {
199
+ position: sticky;
200
+ z-index: 20;
201
+ top: 0;
202
+ display: flex;
203
+ min-height: 58px;
204
+ align-items: center;
205
+ justify-content: flex-end;
206
+ gap: 12px;
207
+ padding: 8px clamp(18px, 3vw, 42px);
208
+ border-bottom: 1px solid var(--pf-border);
209
+ background: color-mix(in srgb, var(--pf-background) 92%, transparent);
210
+ backdrop-filter: blur(14px);
211
+ }
212
+
213
+ .pf-toolbar-content {
214
+ display: flex;
215
+ min-width: 0;
216
+ align-items: center;
217
+ gap: 10px;
218
+ }
219
+
220
+ .pf-toolbar-title {
221
+ margin-right: auto;
222
+ color: var(--pf-muted);
223
+ font-size: 13px;
224
+ font-weight: 650;
225
+ }
226
+
227
+ .pf-account-label {
228
+ overflow: hidden;
229
+ color: var(--pf-muted);
230
+ font-size: 12px;
231
+ text-overflow: ellipsis;
232
+ white-space: nowrap;
233
+ }
234
+
235
+ .pf-sign-out {
236
+ width: 100%;
237
+ border: 0;
238
+ font: inherit;
239
+ text-align: left;
240
+ background: transparent;
241
+ cursor: pointer;
242
+ }
243
+
244
+ .pf-mobile-menu-button {
245
+ display: none;
246
+ width: 40px;
247
+ height: 40px;
248
+ align-items: center;
249
+ justify-content: center;
250
+ border: 1px solid var(--pf-border);
251
+ border-radius: var(--pf-radius-sm);
252
+ color: var(--pf-text);
253
+ background: var(--pf-surface);
254
+ font: inherit;
255
+ cursor: pointer;
256
+ }
257
+
258
+ .pf-dashboard-content {
259
+ width: min(100%, var(--pf-content-width));
260
+ min-width: 0;
261
+ margin: 0 auto;
262
+ padding: clamp(20px, 3vw, 42px);
263
+ }
264
+
265
+ .pf-dashboard-shell
266
+ :where(a, button, input, select, textarea, [tabindex]):focus-visible,
267
+ .pf-auth-shell
268
+ :where(a, button, input, select, textarea, [tabindex]):focus-visible,
269
+ .pf-component
270
+ :where(a, button, input, select, textarea, [tabindex]):focus-visible {
271
+ outline: 3px solid color-mix(in srgb, var(--pf-focus) 72%, transparent);
272
+ outline-offset: 3px;
273
+ }
274
+
275
+ .pf-sidebar-backdrop {
276
+ display: none;
277
+ }
278
+
279
+ .pf-auth-shell {
280
+ display: grid;
281
+ min-height: 100vh;
282
+ place-items: stretch center;
283
+ padding: clamp(16px, 4vw, 56px);
284
+ color: var(--pf-text);
285
+ background: var(--pf-background);
286
+ font-family: var(--pf-font, inherit);
287
+ }
288
+
289
+ .pf-auth-layout {
290
+ display: grid;
291
+ width: min(1120px, 100%);
292
+ min-height: min(740px, calc(100vh - 2 * clamp(16px, 4vw, 56px)));
293
+ grid-template-columns: minmax(0, 1fr) minmax(390px, 0.84fr);
294
+ overflow: hidden;
295
+ border: 1px solid var(--pf-border);
296
+ border-radius: var(--pf-radius-lg);
297
+ background: var(--pf-surface);
298
+ box-shadow: var(--pf-shadow);
299
+ }
300
+
301
+ .pf-auth-aside {
302
+ display: flex;
303
+ min-width: 0;
304
+ flex-direction: column;
305
+ padding: clamp(28px, 5vw, 58px);
306
+ background: color-mix(in srgb, var(--pf-accent) 7%, var(--pf-background));
307
+ }
308
+
309
+ .pf-auth-brand {
310
+ display: flex;
311
+ min-height: 44px;
312
+ align-items: center;
313
+ font-size: 18px;
314
+ font-weight: 740;
315
+ }
316
+
317
+ .pf-auth-aside-content {
318
+ display: grid;
319
+ flex: 1;
320
+ align-content: center;
321
+ gap: 16px;
322
+ padding: 38px 0;
323
+ }
324
+
325
+ .pf-auth-aside-content h1,
326
+ .pf-auth-aside-content h2 {
327
+ max-width: 540px;
328
+ margin: 0;
329
+ font-size: clamp(2rem, 4vw, 3.5rem);
330
+ line-height: 1.05;
331
+ letter-spacing: -0.055em;
332
+ }
333
+
334
+ .pf-auth-aside-content p {
335
+ max-width: 490px;
336
+ margin: 0;
337
+ color: var(--pf-muted);
338
+ font-size: 16px;
339
+ line-height: 1.7;
340
+ }
341
+
342
+ .pf-auth-aside-footer {
343
+ color: var(--pf-muted);
344
+ font-size: 13px;
345
+ }
346
+
347
+ .pf-auth-main {
348
+ position: relative;
349
+ display: flex;
350
+ min-width: 0;
351
+ flex-direction: column;
352
+ justify-content: center;
353
+ padding: clamp(22px, 4vw, 52px);
354
+ }
355
+
356
+ .pf-auth-utility {
357
+ position: absolute;
358
+ top: 18px;
359
+ right: 18px;
360
+ }
361
+
362
+ .pf-auth-content {
363
+ width: min(100%, 460px);
364
+ margin: 0 auto;
365
+ }
366
+
367
+ .pf-auth-card {
368
+ padding: clamp(22px, 3vw, 34px);
369
+ border: 1px solid var(--pf-border);
370
+ border-radius: var(--pf-radius-md);
371
+ color: var(--pf-text);
372
+ background: var(--pf-surface-raised);
373
+ box-shadow: var(--pf-shadow);
374
+ }
375
+
376
+ .pf-auth-card > :first-child {
377
+ margin-top: 0;
378
+ }
379
+
380
+ .pf-auth-card > :last-child {
381
+ margin-bottom: 0;
382
+ }
383
+
384
+ .pf-button {
385
+ display: inline-flex;
386
+ min-height: 40px;
387
+ align-items: center;
388
+ justify-content: center;
389
+ gap: 9px;
390
+ padding: 9px 15px;
391
+ border: 1px solid transparent;
392
+ border-radius: var(--pf-radius-sm);
393
+ font: inherit;
394
+ font-size: 13px;
395
+ font-weight: 680;
396
+ line-height: 1.2;
397
+ text-decoration: none;
398
+ cursor: pointer;
399
+ transition:
400
+ background-color var(--pf-motion-fast) ease,
401
+ border-color var(--pf-motion-fast) ease,
402
+ color var(--pf-motion-fast) ease,
403
+ transform var(--pf-motion-fast) ease;
404
+ }
405
+
406
+ .pf-button:disabled {
407
+ cursor: not-allowed;
408
+ opacity: 0.55;
409
+ }
410
+
411
+ .pf-button-primary {
412
+ color: var(--pf-accent-contrast);
413
+ background: var(--pf-accent);
414
+ }
415
+
416
+ .pf-button-primary:hover:not(:disabled) {
417
+ filter: brightness(0.94);
418
+ }
419
+
420
+ .pf-button-secondary {
421
+ border-color: var(--pf-border);
422
+ color: var(--pf-text);
423
+ background: var(--pf-surface);
424
+ }
425
+
426
+ .pf-button-quiet {
427
+ border-color: color-mix(in srgb, var(--pf-accent) 20%, var(--pf-border));
428
+ color: var(--pf-accent);
429
+ background: color-mix(in srgb, var(--pf-accent) 8%, var(--pf-surface));
430
+ }
431
+
432
+ .pf-button-danger {
433
+ border-color: color-mix(in srgb, var(--pf-danger) 24%, var(--pf-border));
434
+ color: var(--pf-danger);
435
+ background: var(--pf-danger-surface);
436
+ }
437
+
438
+ .pf-card {
439
+ min-width: 0;
440
+ padding: 20px;
441
+ border: 1px solid var(--pf-border);
442
+ border-radius: var(--pf-radius-md);
443
+ background: var(--pf-surface);
444
+ }
445
+
446
+ .pf-badge {
447
+ display: inline-flex;
448
+ min-height: 23px;
449
+ align-items: center;
450
+ padding: 3px 8px;
451
+ border: 1px solid var(--pf-border);
452
+ border-radius: 999px;
453
+ color: var(--pf-muted);
454
+ background: var(--pf-surface);
455
+ font-size: 11px;
456
+ font-weight: 700;
457
+ line-height: 1.2;
458
+ }
459
+
460
+ .pf-badge-success,
461
+ .pf-alert-success {
462
+ border-color: color-mix(in srgb, var(--pf-success) 22%, var(--pf-border));
463
+ color: var(--pf-success);
464
+ background: var(--pf-success-surface);
465
+ }
466
+
467
+ .pf-badge-warning,
468
+ .pf-alert-warning {
469
+ border-color: color-mix(in srgb, var(--pf-warning) 22%, var(--pf-border));
470
+ color: var(--pf-warning);
471
+ background: var(--pf-warning-surface);
472
+ }
473
+
474
+ .pf-badge-danger,
475
+ .pf-alert-error {
476
+ border-color: color-mix(in srgb, var(--pf-danger) 24%, var(--pf-border));
477
+ color: var(--pf-danger);
478
+ background: var(--pf-danger-surface);
479
+ }
480
+
481
+ .pf-badge-accent {
482
+ border-color: color-mix(in srgb, var(--pf-accent) 20%, var(--pf-border));
483
+ color: var(--pf-accent);
484
+ background: color-mix(in srgb, var(--pf-accent) 8%, var(--pf-surface));
485
+ }
486
+
487
+ .pf-alert {
488
+ padding: 12px 14px;
489
+ border: 1px solid var(--pf-border);
490
+ border-radius: var(--pf-radius-sm);
491
+ color: var(--pf-text);
492
+ background: var(--pf-surface);
493
+ font-size: 13px;
494
+ line-height: 1.55;
495
+ }
496
+
497
+ .pf-alert-info {
498
+ border-color: color-mix(in srgb, var(--pf-accent) 20%, var(--pf-border));
499
+ color: var(--pf-accent);
500
+ background: color-mix(in srgb, var(--pf-accent) 7%, var(--pf-surface));
501
+ }
502
+
503
+ .pf-field {
504
+ display: grid;
505
+ gap: 7px;
506
+ color: var(--pf-text);
507
+ font-size: 13px;
508
+ font-weight: 620;
509
+ }
510
+
511
+ .pf-field-label {
512
+ line-height: 1.35;
513
+ }
514
+
515
+ .pf-input {
516
+ width: 100%;
517
+ min-height: 41px;
518
+ padding: 9px 11px;
519
+ border: 1px solid var(--pf-border);
520
+ border-radius: var(--pf-radius-sm);
521
+ color: var(--pf-text);
522
+ background: var(--pf-surface);
523
+ font: inherit;
524
+ font-size: 14px;
525
+ }
526
+
527
+ .pf-input[aria-invalid="true"] {
528
+ border-color: var(--pf-danger);
529
+ }
530
+
531
+ .pf-input::placeholder {
532
+ color: var(--pf-muted);
533
+ opacity: 0.8;
534
+ }
535
+
536
+ .pf-field-hint,
537
+ .pf-field-error {
538
+ color: var(--pf-muted);
539
+ font-size: 12px;
540
+ font-weight: 450;
541
+ line-height: 1.45;
542
+ }
543
+
544
+ .pf-field-error {
545
+ color: var(--pf-danger);
546
+ }
547
+
548
+ .pf-select {
549
+ cursor: pointer;
550
+ }
551
+
552
+ .pf-empty-state {
553
+ display: grid;
554
+ justify-items: center;
555
+ gap: 9px;
556
+ max-width: 560px;
557
+ margin: 48px auto;
558
+ padding: 30px 22px;
559
+ text-align: center;
560
+ }
561
+
562
+ .pf-empty-state h2 {
563
+ margin: 0;
564
+ font-size: 20px;
565
+ letter-spacing: -0.025em;
566
+ }
567
+
568
+ .pf-empty-description {
569
+ max-width: 460px;
570
+ color: var(--pf-muted);
571
+ font-size: 14px;
572
+ line-height: 1.65;
573
+ }
574
+
575
+ .pf-empty-action {
576
+ margin-top: 6px;
577
+ }
578
+
579
+ .pf-skeleton {
580
+ min-height: 12px;
581
+ border-radius: 6px;
582
+ background: color-mix(in srgb, var(--pf-muted) 12%, var(--pf-surface));
583
+ animation: pf-skeleton-pulse 1.4s ease-in-out infinite;
584
+ }
585
+
586
+ .pf-page-header {
587
+ display: flex;
588
+ align-items: flex-end;
589
+ justify-content: space-between;
590
+ gap: 18px;
591
+ margin: 0 0 22px;
592
+ }
593
+
594
+ .pf-page-heading h1 {
595
+ margin: 5px 0 0;
596
+ font-size: clamp(1.7rem, 3vw, 2.6rem);
597
+ line-height: 1.12;
598
+ letter-spacing: -0.05em;
599
+ }
600
+
601
+ .pf-page-eyebrow {
602
+ margin: 0;
603
+ color: var(--pf-muted);
604
+ font-size: 12px;
605
+ font-weight: 700;
606
+ letter-spacing: 0.09em;
607
+ text-transform: uppercase;
608
+ }
609
+
610
+ .pf-page-description {
611
+ max-width: 720px;
612
+ margin: 8px 0 0;
613
+ color: var(--pf-muted);
614
+ font-size: 14px;
615
+ line-height: 1.6;
616
+ }
617
+
618
+ .pf-page-actions {
619
+ display: flex;
620
+ flex: 0 0 auto;
621
+ flex-wrap: wrap;
622
+ gap: 8px;
623
+ }
624
+
625
+ .pf-theme-select {
626
+ display: inline-flex;
627
+ align-items: center;
628
+ gap: 8px;
629
+ color: var(--pf-muted);
630
+ font-size: 12px;
631
+ }
632
+
633
+ .pf-theme-select .pf-input {
634
+ width: auto;
635
+ min-height: 34px;
636
+ padding: 5px 9px;
637
+ font-size: 12px;
638
+ }
639
+
640
+ @keyframes pf-skeleton-pulse {
641
+ 50% {
642
+ opacity: 0.55;
643
+ }
644
+ }
645
+
646
+ @media (max-width: 780px) {
647
+ .pf-dashboard-shell {
648
+ display: block;
649
+ }
650
+
651
+ .pf-dashboard-shell[data-sidebar-visible="false"] {
652
+ grid-template-columns: minmax(0, 1fr);
653
+ }
654
+
655
+ .pf-sidebar {
656
+ position: fixed;
657
+ inset: 0 auto 0 0;
658
+ width: min(var(--pf-sidebar-width), calc(100vw - 56px));
659
+ height: 100dvh;
660
+ min-height: 0;
661
+ transform: translateX(-105%);
662
+ visibility: hidden;
663
+ transition:
664
+ transform 220ms var(--pf-ease),
665
+ visibility 220ms;
666
+ }
667
+
668
+ .pf-dashboard-shell[data-sidebar-visible="false"] .pf-sidebar {
669
+ width: min(var(--pf-sidebar-width), calc(100vw - 56px));
670
+ padding: 20px 14px 14px;
671
+ border-right: 1px solid var(--pf-border);
672
+ opacity: 1;
673
+ pointer-events: auto;
674
+ }
675
+
676
+ .pf-dashboard-shell[data-mobile-open="true"] .pf-sidebar {
677
+ transform: translateX(0);
678
+ visibility: visible;
679
+ }
680
+
681
+ .pf-sidebar-backdrop {
682
+ position: fixed;
683
+ z-index: 25;
684
+ inset: 0;
685
+ display: block;
686
+ border: 0;
687
+ background: rgb(0 0 0 / 38%);
688
+ }
689
+
690
+ .pf-mobile-menu-button {
691
+ display: inline-flex;
692
+ flex: 0 0 auto;
693
+ margin-right: auto;
694
+ }
695
+
696
+ .pf-dashboard-toolbar {
697
+ min-height: 56px;
698
+ padding: 7px 14px;
699
+ }
700
+
701
+ .pf-dashboard-content {
702
+ padding: 20px 16px 32px;
703
+ }
704
+
705
+ .pf-auth-layout {
706
+ min-height: calc(100vh - 32px);
707
+ grid-template-columns: minmax(0, 1fr);
708
+ }
709
+
710
+ .pf-auth-aside {
711
+ min-height: 190px;
712
+ padding: 22px 24px;
713
+ }
714
+
715
+ .pf-auth-aside-content {
716
+ gap: 9px;
717
+ padding: 24px 0 4px;
718
+ }
719
+
720
+ .pf-auth-aside-content h1,
721
+ .pf-auth-aside-content h2 {
722
+ font-size: clamp(1.65rem, 8vw, 2.35rem);
723
+ }
724
+
725
+ .pf-auth-aside-footer {
726
+ display: none;
727
+ }
728
+
729
+ .pf-auth-main {
730
+ padding: 24px;
731
+ }
732
+
733
+ .pf-auth-utility {
734
+ top: 10px;
735
+ right: 10px;
736
+ }
737
+ }
738
+
739
+ @media (max-width: 520px) {
740
+ .pf-auth-shell {
741
+ padding: 0;
742
+ }
743
+
744
+ .pf-auth-layout {
745
+ min-height: 100vh;
746
+ border: 0;
747
+ border-radius: 0;
748
+ }
749
+
750
+ .pf-auth-aside {
751
+ min-height: 150px;
752
+ padding: 18px 20px;
753
+ }
754
+
755
+ .pf-auth-main {
756
+ padding: 22px 18px;
757
+ }
758
+
759
+ .pf-auth-card {
760
+ padding: 20px;
761
+ }
762
+
763
+ .pf-page-header {
764
+ align-items: flex-start;
765
+ flex-direction: column;
766
+ }
767
+
768
+ .pf-page-actions {
769
+ width: 100%;
770
+ }
771
+ }
772
+
773
+ @media (prefers-reduced-motion: reduce) {
774
+ .pf-dashboard-shell *,
775
+ .pf-auth-shell *,
776
+ .pf-component * {
777
+ scroll-behavior: auto !important;
778
+ animation-duration: 0.01ms !important;
779
+ animation-iteration-count: 1 !important;
780
+ transition-duration: 0.01ms !important;
781
+ }
782
+ }
@@ -0,0 +1,3 @@
1
+ declare const stylesheet: string;
2
+
3
+ export default stylesheet;