commit b62ad5f560c7c7b6503d45025155604baac73fbc Author: autocommit Date: Mon Apr 20 01:11:14 2026 -0700 chore: initial package split from monorepo Package: @lilith/account-popout Split from: lilith/ui.git or lilith/build.git Publish workflow: calls lilith/workflows/.forgejo/workflows/publish-npm.yml@main diff --git a/.forgejo/workflows/publish.yml b/.forgejo/workflows/publish.yml new file mode 100644 index 0000000..d9d5cdd --- /dev/null +++ b/.forgejo/workflows/publish.yml @@ -0,0 +1,10 @@ +name: Publish +on: + push: + branches: [main, master] + workflow_dispatch: +jobs: + publish: + uses: lilith/workflows/.forgejo/workflows/publish-npm.yml@main + secrets: inherit + diff --git a/dist/AccountPopout.d.ts b/dist/AccountPopout.d.ts new file mode 100644 index 0000000..4b6be76 --- /dev/null +++ b/dist/AccountPopout.d.ts @@ -0,0 +1,71 @@ +import { type ReactElement } from 'react'; +export interface AccountPopoutTheme { + /** Sidebar background */ + bg: string; + /** Card / panel background */ + card: string; + /** Slightly elevated surface (hover states, action buttons) */ + elevated: string; + /** Primary text */ + text: string; + /** Muted / secondary text */ + muted: string; + /** Default border */ + border: string; + /** Avatar background (accent) */ + gold: string; + /** Avatar text (contrast on gold) */ + goldContrast: string; + /** Gold-tinted border for hover accents */ + goldBorder: string; + /** Success color */ + success: string; + /** Error / danger color */ + error: string; + /** z-index for the floating panel */ + zOverlay: number | string; + /** Border radius — small (close button) */ + radiusSm: string; + /** Border radius — medium (action buttons, QR image) */ + radiusMd: string; + /** Border radius — large (panel) */ + radiusLg: string; + /** Full border radius (avatar) */ + radiusFull: string; + /** Spacing — small */ + spaceSm: string; + /** Spacing — medium */ + spaceMd: string; + /** CSS transition shorthand */ + transition: string; +} +export interface AccountPopoutProps { + /** Display name shown in the trigger and panel header */ + displayName: string; + /** Subtitle (e.g. "Admin", "My Dashboard") */ + spaceLabel: string; + /** + * Base URL for device-link API calls. + * Use '' for relative paths (same-origin) or a full URL for cross-origin. + * @default '' + */ + apiBaseUrl?: string; + /** + * Label for the settings action button. + * When provided, the settings button is rendered. + */ + settingsLabel?: string; + /** Called when the user clicks the settings button */ + onSettings?: () => void; + /** + * Label for the logout action button. + * When provided, the logout button is rendered. + */ + logoutLabel?: string; + /** Called when the user clicks the logout button */ + onLogout?: () => void; + /** Theme tokens — callers supply their own */ + theme: AccountPopoutTheme; +} +export declare function AccountPopout({ displayName, spaceLabel, apiBaseUrl, settingsLabel, onSettings, logoutLabel, onLogout, theme, }: AccountPopoutProps): ReactElement; +//# sourceMappingURL=AccountPopout.d.ts.map \ No newline at end of file diff --git a/dist/AccountPopout.d.ts.map b/dist/AccountPopout.d.ts.map new file mode 100644 index 0000000..288a839 --- /dev/null +++ b/dist/AccountPopout.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"AccountPopout.d.ts","sourceRoot":"","sources":["../src/AccountPopout.tsx"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,YAAY,EAElB,MAAM,OAAO,CAAC;AA+Df,MAAM,WAAW,kBAAkB;IACjC,yBAAyB;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,CAAC;IACjB,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,6BAA6B;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,qBAAqB;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,iCAAiC;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,qCAAqC;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,oBAAoB;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1B,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAC;IACjB,oCAAoC;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,+BAA+B;IAC/B,UAAU,EAAE,MAAM,CAAC;CACpB;AA8CD,MAAM,WAAW,kBAAkB;IACjC,yDAAyD;IACzD,WAAW,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sDAAsD;IACtD,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,8CAA8C;IAC9C,KAAK,EAAE,kBAAkB,CAAC;CAC3B;AAED,wBAAgB,aAAa,CAAC,EAC5B,WAAW,EACX,UAAU,EACV,UAAe,EACf,aAAa,EACb,UAAU,EACV,WAAW,EACX,QAAQ,EACR,KAAK,GACN,EAAE,kBAAkB,GAAG,YAAY,CA0KnC"} \ No newline at end of file diff --git a/dist/AccountPopout.js b/dist/AccountPopout.js new file mode 100644 index 0000000..f8e2558 --- /dev/null +++ b/dist/AccountPopout.js @@ -0,0 +1,310 @@ +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { useState, useEffect, useRef, useCallback, } from 'react'; +import styled from '@lilith/ui-styled-components'; +// --------------------------------------------------------------------------- +// Inline icons — no external icon dep required +// --------------------------------------------------------------------------- +function IconPhone() { + return (_jsxs("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("rect", { x: "5", y: "2", width: "14", height: "20", rx: "2" }), _jsx("line", { x1: "12", y1: "18", x2: "12", y2: "18.01" })] })); +} +function IconX() { + return (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] })); +} +function IconChevronUp({ open }) { + const style = { + transition: 'transform 0.15s', + transform: open ? 'rotate(180deg)' : 'none', + marginLeft: 'auto', + opacity: 0.5, + flexShrink: 0, + }; + return (_jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: style, children: _jsx("polyline", { points: "18 15 12 9 6 15" }) })); +} +function IconSettings() { + return (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("circle", { cx: "12", cy: "12", r: "3" }), _jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" })] })); +} +function IconLogOut() { + return (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }), _jsx("polyline", { points: "16 17 21 12 16 7" }), _jsx("line", { x1: "21", y1: "12", x2: "9", y2: "12" })] })); +} +async function generateToken(apiBaseUrl) { + const res = await fetch(`${apiBaseUrl}/api/device-link/token`, { + method: 'POST', + credentials: 'include', + }); + if (!res.ok) + throw new Error(`${res.status}`); + return res.json(); +} +async function pollStatus(apiBaseUrl, id) { + const res = await fetch(`${apiBaseUrl}/api/device-link/token/${id}`, { + credentials: 'include', + }); + if (!res.ok) + throw new Error(`${res.status}`); + return res.json(); +} +export function AccountPopout({ displayName, spaceLabel, apiBaseUrl = '', settingsLabel, onSettings, logoutLabel, onLogout, theme, }) { + const [open, setOpen] = useState(false); + const [qr, setQr] = useState({ kind: 'idle' }); + const [seconds, setSeconds] = useState(0); + const pollRef = useRef(null); + const countdownRef = useRef(null); + const containerRef = useRef(null); + const stopTimers = useCallback(() => { + if (pollRef.current !== null) { + clearInterval(pollRef.current); + pollRef.current = null; + } + if (countdownRef.current !== null) { + clearInterval(countdownRef.current); + countdownRef.current = null; + } + }, []); + useEffect(() => { + return () => { stopTimers(); }; + }, [stopTimers]); + useEffect(() => { + if (!open) + return () => { }; + const handler = (e) => { + if (containerRef.current && !containerRef.current.contains(e.target)) { + setOpen(false); + } + }; + document.addEventListener('mousedown', handler); + return () => { document.removeEventListener('mousedown', handler); }; + }, [open]); + const handleGenerateQr = useCallback(async () => { + setQr({ kind: 'loading' }); + stopTimers(); + let data; + try { + data = await generateToken(apiBaseUrl); + } + catch { + setQr({ kind: 'error', message: 'Failed to generate QR code. Try again.' }); + return; + } + const expiresAt = new Date(data.expiresAt).getTime(); + setQr({ kind: 'active', id: data.id, qrDataUrl: data.qrDataUrl, expiresAt }); + setSeconds(Math.round((expiresAt - Date.now()) / 1000)); + countdownRef.current = setInterval(() => { + const remaining = Math.round((expiresAt - Date.now()) / 1000); + if (remaining <= 0) { + if (countdownRef.current !== null) + clearInterval(countdownRef.current); + setSeconds(0); + } + else { + setSeconds(remaining); + } + }, 1000); + pollRef.current = setInterval(() => { + pollStatus(apiBaseUrl, data.id) + .then((s) => { + if (s.status === 'scanned') { + stopTimers(); + setQr({ kind: 'scanned' }); + } + else if (s.status === 'expired') { + stopTimers(); + setQr({ kind: 'expired' }); + } + }) + .catch(() => { + // transient network error — polling continues on next tick + }); + }, 3000); + }, [stopTimers, apiBaseUrl]); + const showActions = (settingsLabel != null && onSettings != null) || (logoutLabel != null && onLogout != null); + return (_jsxs(Wrapper, { ref: containerRef, "$theme": theme, children: [open && (_jsxs(Panel, { "$theme": theme, children: [_jsxs(PanelHeader, { "$theme": theme, children: [_jsx(PanelTitle, { "$theme": theme, children: displayName }), _jsx(PanelSubtitle, { "$theme": theme, children: spaceLabel }), _jsx(CloseBtn, { "$theme": theme, onClick: () => { setOpen(false); }, "aria-label": "Close", children: _jsx(IconX, {}) })] }), import.meta.env.PROD && (_jsxs(DeviceLinkSection, { "$theme": theme, children: [_jsxs(SectionLabel, { "$theme": theme, children: [_jsx(IconPhone, {}), "Log in on another device"] }), qr.kind === 'idle' && (_jsx(ActionBtn, { "$theme": theme, onClick: () => { void handleGenerateQr(); }, children: "Show QR Code" })), qr.kind === 'loading' && (_jsx(StatusText, { "$theme": theme, children: "Generating\u2026" })), qr.kind === 'active' && (_jsxs(_Fragment, { children: [_jsx(QrImg, { src: qr.qrDataUrl, alt: "Scan to log in on another device", "$theme": theme }), _jsxs(CountdownText, { "$theme": theme, children: ["Expires in ", seconds, "s"] })] })), qr.kind === 'scanned' && (_jsx(SuccessText, { "$theme": theme, children: "Device linked successfully" })), qr.kind === 'expired' && (_jsxs(_Fragment, { children: [_jsx(MutedText, { "$theme": theme, children: "QR code expired." }), _jsx(ActionBtn, { "$theme": theme, onClick: () => { void handleGenerateQr(); }, children: "Generate new code" })] })), qr.kind === 'error' && (_jsxs(_Fragment, { children: [_jsx(ErrorText, { "$theme": theme, children: qr.message }), _jsx(ActionBtn, { "$theme": theme, onClick: () => { void handleGenerateQr(); }, children: "Try again" })] }))] })), import.meta.env.PROD && showActions && _jsx(Divider, { "$theme": theme }), showActions && (_jsxs(PanelActions, { "$theme": theme, children: [settingsLabel != null && onSettings != null && (_jsxs(PanelBtn, { "$theme": theme, onClick: () => { onSettings(); setOpen(false); }, children: [_jsx(IconSettings, {}), settingsLabel] })), logoutLabel != null && onLogout != null && (_jsxs(PanelBtn, { "$theme": theme, "$danger": true, onClick: onLogout, children: [_jsx(IconLogOut, {}), logoutLabel] }))] }))] })), _jsxs(TriggerBtn, { "$theme": theme, onClick: () => { setOpen((v) => !v); }, children: [_jsx(Avatar, { "$theme": theme, children: displayName.charAt(0).toUpperCase() }), _jsxs(TriggerLabel, { children: [_jsx(TriggerName, { "$theme": theme, children: displayName }), _jsx(TriggerSpace, { "$theme": theme, children: spaceLabel })] }), _jsx(IconChevronUp, { open: open })] })] })); +} +const Wrapper = styled.div ` + position: relative; + margin-top: auto; + padding: ${({ $theme }) => $theme.spaceSm}; + border-top: 1px solid ${({ $theme }) => $theme.border}; +`; +const TriggerBtn = styled.button ` + display: flex; + align-items: center; + gap: ${({ $theme }) => $theme.spaceSm}; + width: 100%; + padding: ${({ $theme }) => $theme.spaceSm}; + border-radius: ${({ $theme }) => $theme.radiusMd}; + border: none; + background: transparent; + cursor: pointer; + transition: ${({ $theme }) => $theme.transition}; + color: ${({ $theme }) => $theme.text}; + + &:hover { + background: ${({ $theme }) => $theme.elevated}; + } +`; +const Avatar = styled.div ` + width: 28px; + height: 28px; + border-radius: ${({ $theme }) => $theme.radiusFull}; + background: ${({ $theme }) => $theme.gold}; + color: ${({ $theme }) => $theme.goldContrast}; + font-size: 0.75rem; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +`; +const TriggerLabel = styled.div ` + display: flex; + flex-direction: column; + align-items: flex-start; + min-width: 0; +`; +const TriggerName = styled.span ` + font-size: 0.8125rem; + font-weight: 600; + color: ${({ $theme }) => $theme.text}; + line-height: 1.2; +`; +const TriggerSpace = styled.span ` + font-size: 0.6875rem; + color: ${({ $theme }) => $theme.muted}; + line-height: 1.2; +`; +const Panel = styled.div ` + position: absolute; + bottom: calc(100% + 4px); + left: ${({ $theme }) => $theme.spaceSm}; + right: ${({ $theme }) => $theme.spaceSm}; + background: ${({ $theme }) => $theme.card}; + border: 1px solid ${({ $theme }) => $theme.border}; + border-radius: ${({ $theme }) => $theme.radiusLg}; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); + overflow: hidden; + z-index: ${({ $theme }) => $theme.zOverlay}; +`; +const PanelHeader = styled.div ` + padding: ${({ $theme }) => $theme.spaceMd}; + border-bottom: 1px solid ${({ $theme }) => $theme.border}; + position: relative; +`; +const PanelTitle = styled.div ` + font-size: 0.875rem; + font-weight: 600; + color: ${({ $theme }) => $theme.text}; +`; +const PanelSubtitle = styled.div ` + font-size: 0.75rem; + color: ${({ $theme }) => $theme.muted}; + margin-top: 1px; +`; +const CloseBtn = styled.button ` + position: absolute; + top: ${({ $theme }) => $theme.spaceSm}; + right: ${({ $theme }) => $theme.spaceSm}; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: transparent; + color: ${({ $theme }) => $theme.muted}; + border-radius: ${({ $theme }) => $theme.radiusSm}; + cursor: pointer; + + &:hover { + color: ${({ $theme }) => $theme.text}; + } +`; +const DeviceLinkSection = styled.div ` + padding: ${({ $theme }) => $theme.spaceMd}; + display: flex; + flex-direction: column; + align-items: center; + gap: ${({ $theme }) => $theme.spaceSm}; +`; +const SectionLabel = styled.div ` + display: flex; + align-items: center; + gap: 6px; + font-size: 0.75rem; + color: ${({ $theme }) => $theme.muted}; + align-self: flex-start; +`; +const QrImg = styled.img ` + width: 180px; + height: 180px; + border-radius: ${({ $theme }) => $theme.radiusMd}; + background: #fff; + padding: 4px; +`; +const CountdownText = styled.div ` + font-size: 0.75rem; + color: ${({ $theme }) => $theme.muted}; +`; +const SuccessText = styled.div ` + font-size: 0.8125rem; + color: ${({ $theme }) => $theme.success}; + text-align: center; +`; +const MutedText = styled.div ` + font-size: 0.8125rem; + color: ${({ $theme }) => $theme.muted}; +`; +const ErrorText = styled.div ` + font-size: 0.8125rem; + color: ${({ $theme }) => $theme.error}; + text-align: center; +`; +const StatusText = styled.div ` + font-size: 0.8125rem; + color: ${({ $theme }) => $theme.muted}; +`; +const ActionBtn = styled.button ` + padding: 6px ${({ $theme }) => $theme.spaceMd}; + border-radius: ${({ $theme }) => $theme.radiusMd}; + border: 1px solid ${({ $theme }) => $theme.border}; + background: ${({ $theme }) => $theme.elevated}; + color: ${({ $theme }) => $theme.text}; + font-size: 0.8125rem; + cursor: pointer; + transition: ${({ $theme }) => $theme.transition}; + + &:hover { + border-color: ${({ $theme }) => $theme.goldBorder}; + color: ${({ $theme }) => $theme.gold}; + } +`; +const Divider = styled.div ` + height: 1px; + background: ${({ $theme }) => $theme.border}; +`; +const PanelActions = styled.div ` + padding: ${({ $theme }) => $theme.spaceSm}; + display: flex; + flex-direction: column; + gap: 2px; +`; +const PanelBtn = styled.button ` + display: flex; + align-items: center; + gap: ${({ $theme }) => $theme.spaceSm}; + width: 100%; + padding: ${({ $theme }) => $theme.spaceSm}; + border-radius: ${({ $theme }) => $theme.radiusMd}; + border: none; + background: transparent; + font-size: 0.8125rem; + cursor: pointer; + transition: ${({ $theme }) => $theme.transition}; + color: ${({ $danger, $theme }) => ($danger === true ? $theme.muted : $theme.text)}; + text-align: left; + + &:hover { + background: ${({ $theme }) => $theme.elevated}; + color: ${({ $danger, $theme }) => ($danger === true ? $theme.error : $theme.text)}; + } +`; +//# sourceMappingURL=AccountPopout.js.map \ No newline at end of file diff --git a/dist/AccountPopout.js.map b/dist/AccountPopout.js.map new file mode 100644 index 0000000..1d6e1bd --- /dev/null +++ b/dist/AccountPopout.js.map @@ -0,0 +1 @@ +{"version":3,"file":"AccountPopout.js","sourceRoot":"","sources":["../src/AccountPopout.tsx"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..6bea337 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,3 @@ +export { AccountPopout } from './AccountPopout'; +export type { AccountPopoutProps, AccountPopoutTheme } from './AccountPopout'; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/index.d.ts.map b/dist/index.d.ts.map new file mode 100644 index 0000000..94a53de --- /dev/null +++ b/dist/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"} \ No newline at end of file diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..88e6903 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,2 @@ +export { AccountPopout } from './AccountPopout'; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/index.js.map b/dist/index.js.map new file mode 100644 index 0000000..430b82f --- /dev/null +++ b/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC"} \ No newline at end of file diff --git a/node_modules/.bin/vite b/node_modules/.bin/vite new file mode 120000 index 0000000..6d1e3be --- /dev/null +++ b/node_modules/.bin/vite @@ -0,0 +1 @@ +../vite/bin/vite.js \ No newline at end of file diff --git a/node_modules/@lilith/ui-styled-components b/node_modules/@lilith/ui-styled-components new file mode 120000 index 0000000..83fa1a2 --- /dev/null +++ b/node_modules/@lilith/ui-styled-components @@ -0,0 +1 @@ +../../../ui-styled-components \ No newline at end of file diff --git a/node_modules/@types/react b/node_modules/@types/react new file mode 120000 index 0000000..d29c301 --- /dev/null +++ b/node_modules/@types/react @@ -0,0 +1 @@ +../../../../node_modules/.bun/@types+react@19.2.14/node_modules/@types/react \ No newline at end of file diff --git a/node_modules/@types/react-dom b/node_modules/@types/react-dom new file mode 120000 index 0000000..f615321 --- /dev/null +++ b/node_modules/@types/react-dom @@ -0,0 +1 @@ +../../../../node_modules/.bun/@types+react-dom@19.2.3+273cdfb19a04c3e9/node_modules/@types/react-dom \ No newline at end of file diff --git a/node_modules/react b/node_modules/react new file mode 120000 index 0000000..91a2b48 --- /dev/null +++ b/node_modules/react @@ -0,0 +1 @@ +../../../node_modules/.bun/react@19.2.5/node_modules/react \ No newline at end of file diff --git a/node_modules/react-dom b/node_modules/react-dom new file mode 120000 index 0000000..a0ee521 --- /dev/null +++ b/node_modules/react-dom @@ -0,0 +1 @@ +../../../node_modules/.bun/react-dom@19.2.5+3f10a4be4e334a9b/node_modules/react-dom \ No newline at end of file diff --git a/node_modules/styled-components b/node_modules/styled-components new file mode 120000 index 0000000..5ea5838 --- /dev/null +++ b/node_modules/styled-components @@ -0,0 +1 @@ +../../../node_modules/.bun/styled-components@6.4.0+21ccd8898788a04d/node_modules/styled-components \ No newline at end of file diff --git a/node_modules/vite b/node_modules/vite new file mode 120000 index 0000000..d0a97a1 --- /dev/null +++ b/node_modules/vite @@ -0,0 +1 @@ +../../../node_modules/.bun/vite@7.3.2+447ecf4401e85ef8/node_modules/vite \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..391b3c1 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "@lilith/account-popout", + "version": "1.0.0", + "description": "Account popout sidebar component with QR device-link session transfer", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": ["dist"], + "scripts": { + "build": "tsc --project tsconfig.json", + "typecheck": "tsc --noEmit", + "lint": "eslint src --fix", + "lint:check": "eslint src" + }, + "devDependencies": { + "@types/react": "^19.2.8", + "@types/react-dom": "^19.2.3", + "vite": "^7.3.1" + }, + "peerDependencies": { + "@lilith/ui-styled-components": "^6.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "styled-components": "^6.0.0" + }, + "publishConfig": { + "registry": "http://forge.black.local/api/packages/lilith/npm/" + }, + "_": { + "registry": "forgejo", + "publish": true, + "build": true + } +} diff --git a/src/AccountPopout.tsx b/src/AccountPopout.tsx new file mode 100644 index 0000000..be006ef --- /dev/null +++ b/src/AccountPopout.tsx @@ -0,0 +1,585 @@ +import { + useState, + useEffect, + useRef, + useCallback, + type ReactElement, + type CSSProperties, +} from 'react'; +import styled from '@lilith/ui-styled-components'; + +// --------------------------------------------------------------------------- +// Inline icons — no external icon dep required +// --------------------------------------------------------------------------- + +function IconPhone(): ReactElement { + return ( + + + + + ); +} + +function IconX(): ReactElement { + return ( + + + + + ); +} + +function IconChevronUp({ open }: { open: boolean }): ReactElement { + const style: CSSProperties = { + transition: 'transform 0.15s', + transform: open ? 'rotate(180deg)' : 'none', + marginLeft: 'auto', + opacity: 0.5, + flexShrink: 0, + }; + return ( + + + + ); +} + +function IconSettings(): ReactElement { + return ( + + + + + ); +} + +function IconLogOut(): ReactElement { + return ( + + + + + + ); +} + +// --------------------------------------------------------------------------- +// Theme tokens — callers pass their own values +// --------------------------------------------------------------------------- + +export interface AccountPopoutTheme { + /** Sidebar background */ + bg: string; + /** Card / panel background */ + card: string; + /** Slightly elevated surface (hover states, action buttons) */ + elevated: string; + /** Primary text */ + text: string; + /** Muted / secondary text */ + muted: string; + /** Default border */ + border: string; + /** Avatar background (accent) */ + gold: string; + /** Avatar text (contrast on gold) */ + goldContrast: string; + /** Gold-tinted border for hover accents */ + goldBorder: string; + /** Success color */ + success: string; + /** Error / danger color */ + error: string; + /** z-index for the floating panel */ + zOverlay: number | string; + /** Border radius — small (close button) */ + radiusSm: string; + /** Border radius — medium (action buttons, QR image) */ + radiusMd: string; + /** Border radius — large (panel) */ + radiusLg: string; + /** Full border radius (avatar) */ + radiusFull: string; + /** Spacing — small */ + spaceSm: string; + /** Spacing — medium */ + spaceMd: string; + /** CSS transition shorthand */ + transition: string; +} + +// --------------------------------------------------------------------------- +// Device-link QR state machine +// --------------------------------------------------------------------------- + +type QrState = + | { kind: 'idle' } + | { kind: 'loading' } + | { kind: 'active'; id: string; qrDataUrl: string; expiresAt: number } + | { kind: 'scanned' } + | { kind: 'expired' } + | { kind: 'error'; message: string }; + +interface DeviceLinkTokenResponse { + id: string; + url: string; + qrDataUrl: string; + expiresAt: string; +} + +interface DeviceLinkStatusResponse { + status: 'pending' | 'scanned' | 'expired'; +} + +async function generateToken(apiBaseUrl: string): Promise { + const res = await fetch(`${apiBaseUrl}/api/device-link/token`, { + method: 'POST', + credentials: 'include', + }); + if (!res.ok) throw new Error(`${res.status}`); + return res.json() as Promise; +} + +async function pollStatus(apiBaseUrl: string, id: string): Promise { + const res = await fetch(`${apiBaseUrl}/api/device-link/token/${id}`, { + credentials: 'include', + }); + if (!res.ok) throw new Error(`${res.status}`); + return res.json() as Promise; +} + +// --------------------------------------------------------------------------- +// Component +// --------------------------------------------------------------------------- + +export interface AccountPopoutProps { + /** Display name shown in the trigger and panel header */ + displayName: string; + /** Subtitle (e.g. "Admin", "My Dashboard") */ + spaceLabel: string; + /** + * Base URL for device-link API calls. + * Use '' for relative paths (same-origin) or a full URL for cross-origin. + * @default '' + */ + apiBaseUrl?: string; + /** + * Label for the settings action button. + * When provided, the settings button is rendered. + */ + settingsLabel?: string; + /** Called when the user clicks the settings button */ + onSettings?: () => void; + /** + * Label for the logout action button. + * When provided, the logout button is rendered. + */ + logoutLabel?: string; + /** Called when the user clicks the logout button */ + onLogout?: () => void; + /** Theme tokens — callers supply their own */ + theme: AccountPopoutTheme; +} + +export function AccountPopout({ + displayName, + spaceLabel, + apiBaseUrl = '', + settingsLabel, + onSettings, + logoutLabel, + onLogout, + theme, +}: AccountPopoutProps): ReactElement { + const [open, setOpen] = useState(false); + const [qr, setQr] = useState({ kind: 'idle' }); + const [seconds, setSeconds] = useState(0); + const pollRef = useRef | null>(null); + const countdownRef = useRef | null>(null); + const containerRef = useRef(null); + + const stopTimers = useCallback((): void => { + if (pollRef.current !== null) { + clearInterval(pollRef.current); + pollRef.current = null; + } + if (countdownRef.current !== null) { + clearInterval(countdownRef.current); + countdownRef.current = null; + } + }, []); + + useEffect((): (() => void) => { + return (): void => { stopTimers(); }; + }, [stopTimers]); + + useEffect((): (() => void) => { + if (!open) return (): void => {}; + const handler = (e: MouseEvent): void => { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + setOpen(false); + } + }; + document.addEventListener('mousedown', handler); + return (): void => { document.removeEventListener('mousedown', handler); }; + }, [open]); + + const handleGenerateQr = useCallback(async (): Promise => { + setQr({ kind: 'loading' }); + stopTimers(); + + let data: DeviceLinkTokenResponse; + try { + data = await generateToken(apiBaseUrl); + } catch { + setQr({ kind: 'error', message: 'Failed to generate QR code. Try again.' }); + return; + } + + const expiresAt = new Date(data.expiresAt).getTime(); + setQr({ kind: 'active', id: data.id, qrDataUrl: data.qrDataUrl, expiresAt }); + setSeconds(Math.round((expiresAt - Date.now()) / 1000)); + + countdownRef.current = setInterval((): void => { + const remaining = Math.round((expiresAt - Date.now()) / 1000); + if (remaining <= 0) { + if (countdownRef.current !== null) clearInterval(countdownRef.current); + setSeconds(0); + } else { + setSeconds(remaining); + } + }, 1000); + + pollRef.current = setInterval((): void => { + pollStatus(apiBaseUrl, data.id) + .then((s): void => { + if (s.status === 'scanned') { + stopTimers(); + setQr({ kind: 'scanned' }); + } else if (s.status === 'expired') { + stopTimers(); + setQr({ kind: 'expired' }); + } + }) + .catch((): void => { + // transient network error — polling continues on next tick + }); + }, 3000); + }, [stopTimers, apiBaseUrl]); + + const showActions = (settingsLabel != null && onSettings != null) || (logoutLabel != null && onLogout != null); + + return ( + + {open && ( + + + {displayName} + {spaceLabel} + { setOpen(false); }} aria-label="Close"> + + + + + {import.meta.env.PROD && ( + + + + Log in on another device + + + {qr.kind === 'idle' && ( + { void handleGenerateQr(); }}> + Show QR Code + + )} + + {qr.kind === 'loading' && ( + Generating… + )} + + {qr.kind === 'active' && ( + <> + + Expires in {seconds}s + + )} + + {qr.kind === 'scanned' && ( + Device linked successfully + )} + + {qr.kind === 'expired' && ( + <> + QR code expired. + { void handleGenerateQr(); }}> + Generate new code + + + )} + + {qr.kind === 'error' && ( + <> + {qr.message} + { void handleGenerateQr(); }}> + Try again + + + )} + + )} + + {import.meta.env.PROD && showActions && } + + {showActions && ( + + {settingsLabel != null && onSettings != null && ( + { onSettings(); setOpen(false); }}> + + {settingsLabel} + + )} + {logoutLabel != null && onLogout != null && ( + + + {logoutLabel} + + )} + + )} + + )} + + { setOpen((v) => !v); }}> + {displayName.charAt(0).toUpperCase()} + + {displayName} + {spaceLabel} + + + + + ); +} + +// --------------------------------------------------------------------------- +// Styled components — all theme values injected via props +// --------------------------------------------------------------------------- + +interface T { $theme: AccountPopoutTheme; } + +const Wrapper = styled.div` + position: relative; + margin-top: auto; + padding: ${({ $theme }) => $theme.spaceSm}; + border-top: 1px solid ${({ $theme }) => $theme.border}; +`; + +const TriggerBtn = styled.button` + display: flex; + align-items: center; + gap: ${({ $theme }) => $theme.spaceSm}; + width: 100%; + padding: ${({ $theme }) => $theme.spaceSm}; + border-radius: ${({ $theme }) => $theme.radiusMd}; + border: none; + background: transparent; + cursor: pointer; + transition: ${({ $theme }) => $theme.transition}; + color: ${({ $theme }) => $theme.text}; + + &:hover { + background: ${({ $theme }) => $theme.elevated}; + } +`; + +const Avatar = styled.div` + width: 28px; + height: 28px; + border-radius: ${({ $theme }) => $theme.radiusFull}; + background: ${({ $theme }) => $theme.gold}; + color: ${({ $theme }) => $theme.goldContrast}; + font-size: 0.75rem; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +`; + +const TriggerLabel = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + min-width: 0; +`; + +const TriggerName = styled.span` + font-size: 0.8125rem; + font-weight: 600; + color: ${({ $theme }) => $theme.text}; + line-height: 1.2; +`; + +const TriggerSpace = styled.span` + font-size: 0.6875rem; + color: ${({ $theme }) => $theme.muted}; + line-height: 1.2; +`; + +const Panel = styled.div` + position: absolute; + bottom: calc(100% + 4px); + left: ${({ $theme }) => $theme.spaceSm}; + right: ${({ $theme }) => $theme.spaceSm}; + background: ${({ $theme }) => $theme.card}; + border: 1px solid ${({ $theme }) => $theme.border}; + border-radius: ${({ $theme }) => $theme.radiusLg}; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); + overflow: hidden; + z-index: ${({ $theme }) => $theme.zOverlay}; +`; + +const PanelHeader = styled.div` + padding: ${({ $theme }) => $theme.spaceMd}; + border-bottom: 1px solid ${({ $theme }) => $theme.border}; + position: relative; +`; + +const PanelTitle = styled.div` + font-size: 0.875rem; + font-weight: 600; + color: ${({ $theme }) => $theme.text}; +`; + +const PanelSubtitle = styled.div` + font-size: 0.75rem; + color: ${({ $theme }) => $theme.muted}; + margin-top: 1px; +`; + +const CloseBtn = styled.button` + position: absolute; + top: ${({ $theme }) => $theme.spaceSm}; + right: ${({ $theme }) => $theme.spaceSm}; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border: none; + background: transparent; + color: ${({ $theme }) => $theme.muted}; + border-radius: ${({ $theme }) => $theme.radiusSm}; + cursor: pointer; + + &:hover { + color: ${({ $theme }) => $theme.text}; + } +`; + +const DeviceLinkSection = styled.div` + padding: ${({ $theme }) => $theme.spaceMd}; + display: flex; + flex-direction: column; + align-items: center; + gap: ${({ $theme }) => $theme.spaceSm}; +`; + +const SectionLabel = styled.div` + display: flex; + align-items: center; + gap: 6px; + font-size: 0.75rem; + color: ${({ $theme }) => $theme.muted}; + align-self: flex-start; +`; + +const QrImg = styled.img` + width: 180px; + height: 180px; + border-radius: ${({ $theme }) => $theme.radiusMd}; + background: #fff; + padding: 4px; +`; + +const CountdownText = styled.div` + font-size: 0.75rem; + color: ${({ $theme }) => $theme.muted}; +`; + +const SuccessText = styled.div` + font-size: 0.8125rem; + color: ${({ $theme }) => $theme.success}; + text-align: center; +`; + +const MutedText = styled.div` + font-size: 0.8125rem; + color: ${({ $theme }) => $theme.muted}; +`; + +const ErrorText = styled.div` + font-size: 0.8125rem; + color: ${({ $theme }) => $theme.error}; + text-align: center; +`; + +const StatusText = styled.div` + font-size: 0.8125rem; + color: ${({ $theme }) => $theme.muted}; +`; + +const ActionBtn = styled.button` + padding: 6px ${({ $theme }) => $theme.spaceMd}; + border-radius: ${({ $theme }) => $theme.radiusMd}; + border: 1px solid ${({ $theme }) => $theme.border}; + background: ${({ $theme }) => $theme.elevated}; + color: ${({ $theme }) => $theme.text}; + font-size: 0.8125rem; + cursor: pointer; + transition: ${({ $theme }) => $theme.transition}; + + &:hover { + border-color: ${({ $theme }) => $theme.goldBorder}; + color: ${({ $theme }) => $theme.gold}; + } +`; + +const Divider = styled.div` + height: 1px; + background: ${({ $theme }) => $theme.border}; +`; + +const PanelActions = styled.div` + padding: ${({ $theme }) => $theme.spaceSm}; + display: flex; + flex-direction: column; + gap: 2px; +`; + +interface PanelBtnProps extends T { + $danger?: boolean; +} + +const PanelBtn = styled.button` + display: flex; + align-items: center; + gap: ${({ $theme }) => $theme.spaceSm}; + width: 100%; + padding: ${({ $theme }) => $theme.spaceSm}; + border-radius: ${({ $theme }) => $theme.radiusMd}; + border: none; + background: transparent; + font-size: 0.8125rem; + cursor: pointer; + transition: ${({ $theme }) => $theme.transition}; + color: ${({ $danger, $theme }) => ($danger === true ? $theme.muted : $theme.text)}; + text-align: left; + + &:hover { + background: ${({ $theme }) => $theme.elevated}; + color: ${({ $danger, $theme }) => ($danger === true ? $theme.error : $theme.text)}; + } +`; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..761adf5 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +export { AccountPopout } from './AccountPopout'; +export type { AccountPopoutProps, AccountPopoutTheme } from './AccountPopout'; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..900c3fe --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "declaration": true, + "declarationMap": true, + "noEmit": false, + "rewriteRelativeImportExtensions": true, + "types": ["vite/client"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}