@hadoku/task-ui-components 0.0.0-stage → 4.20.3
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 +21 -0
- package/README.md +917 -2
- package/dist/app-header.css +288 -0
- package/dist/bento.css +143 -0
- package/dist/button.css +227 -0
- package/dist/components/AppHeader.d.ts +61 -0
- package/dist/components/AppHeader.d.ts.map +1 -0
- package/dist/components/AppHeader.js +23 -0
- package/dist/components/AppHeader.js.map +1 -0
- package/dist/components/BentoCard.d.ts +23 -0
- package/dist/components/BentoCard.d.ts.map +1 -0
- package/dist/components/BentoCard.js +49 -0
- package/dist/components/BentoCard.js.map +1 -0
- package/dist/components/BentoGrid.d.ts +24 -0
- package/dist/components/BentoGrid.d.ts.map +1 -0
- package/dist/components/BentoGrid.js +22 -0
- package/dist/components/BentoGrid.js.map +1 -0
- package/dist/components/Button.d.ts +28 -0
- package/dist/components/Button.d.ts.map +1 -0
- package/dist/components/Button.js +51 -0
- package/dist/components/Button.js.map +1 -0
- package/dist/components/ConnectedSettings.d.ts +53 -0
- package/dist/components/ConnectedSettings.d.ts.map +1 -0
- package/dist/components/ConnectedSettings.js +302 -0
- package/dist/components/ConnectedSettings.js.map +1 -0
- package/dist/components/ConnectedThemePicker.d.ts +52 -0
- package/dist/components/ConnectedThemePicker.d.ts.map +1 -0
- package/dist/components/ConnectedThemePicker.js +32 -0
- package/dist/components/ConnectedThemePicker.js.map +1 -0
- package/dist/components/ContextMenu.d.ts +22 -0
- package/dist/components/ContextMenu.d.ts.map +1 -0
- package/dist/components/ContextMenu.js +23 -0
- package/dist/components/ContextMenu.js.map +1 -0
- package/dist/components/HadokuThemePicker.d.ts +27 -0
- package/dist/components/HadokuThemePicker.d.ts.map +1 -0
- package/dist/components/HadokuThemePicker.js +19 -0
- package/dist/components/HadokuThemePicker.js.map +1 -0
- package/dist/components/LoadingSkeleton.d.ts +14 -0
- package/dist/components/LoadingSkeleton.d.ts.map +1 -0
- package/dist/components/LoadingSkeleton.js +62 -0
- package/dist/components/LoadingSkeleton.js.map +1 -0
- package/dist/components/Modal.d.ts +39 -0
- package/dist/components/Modal.d.ts.map +1 -0
- package/dist/components/Modal.js +106 -0
- package/dist/components/Modal.js.map +1 -0
- package/dist/components/NumberInput.d.ts +26 -0
- package/dist/components/NumberInput.d.ts.map +1 -0
- package/dist/components/NumberInput.js +131 -0
- package/dist/components/NumberInput.js.map +1 -0
- package/dist/components/Select.d.ts +52 -0
- package/dist/components/Select.d.ts.map +1 -0
- package/dist/components/Select.js +281 -0
- package/dist/components/Select.js.map +1 -0
- package/dist/components/SettingsModal.d.ts +69 -0
- package/dist/components/SettingsModal.d.ts.map +1 -0
- package/dist/components/SettingsModal.js +141 -0
- package/dist/components/SettingsModal.js.map +1 -0
- package/dist/components/ThemeIcons.d.ts +26 -0
- package/dist/components/ThemeIcons.d.ts.map +1 -0
- package/dist/components/ThemeIcons.js +74 -0
- package/dist/components/ThemeIcons.js.map +1 -0
- package/dist/components/ThemePicker.d.ts +28 -0
- package/dist/components/ThemePicker.d.ts.map +1 -0
- package/dist/components/ThemePicker.js +101 -0
- package/dist/components/ThemePicker.js.map +1 -0
- package/dist/components/Toast.d.ts +13 -0
- package/dist/components/Toast.d.ts.map +1 -0
- package/dist/components/Toast.js +70 -0
- package/dist/components/Toast.js.map +1 -0
- package/dist/components/Toaster.d.ts +19 -0
- package/dist/components/Toaster.d.ts.map +1 -0
- package/dist/components/Toaster.js +19 -0
- package/dist/components/Toaster.js.map +1 -0
- package/dist/components/Tooltip.d.ts +49 -0
- package/dist/components/Tooltip.d.ts.map +1 -0
- package/dist/components/Tooltip.js +143 -0
- package/dist/components/Tooltip.js.map +1 -0
- package/dist/context-menu.css +11 -0
- package/dist/controls.css +137 -0
- package/dist/hooks/useReducedMotion.d.ts +7 -0
- package/dist/hooks/useReducedMotion.d.ts.map +1 -0
- package/dist/hooks/useReducedMotion.js +25 -0
- package/dist/hooks/useReducedMotion.js.map +1 -0
- package/dist/hooks/useToast.d.ts +18 -0
- package/dist/hooks/useToast.d.ts.map +1 -0
- package/dist/hooks/useToast.js +28 -0
- package/dist/hooks/useToast.js.map +1 -0
- package/dist/icon-btn.css +64 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/menuPlacement.d.ts +16 -0
- package/dist/lib/menuPlacement.d.ts.map +1 -0
- package/dist/lib/menuPlacement.js +19 -0
- package/dist/lib/menuPlacement.js.map +1 -0
- package/dist/lib/selectMenus.d.ts +30 -0
- package/dist/lib/selectMenus.d.ts.map +1 -0
- package/dist/lib/selectMenus.js +236 -0
- package/dist/lib/selectMenus.js.map +1 -0
- package/dist/lib/settingsClient.d.ts +120 -0
- package/dist/lib/settingsClient.d.ts.map +1 -0
- package/dist/lib/settingsClient.js +140 -0
- package/dist/lib/settingsClient.js.map +1 -0
- package/dist/lib/themeScope.d.ts +19 -0
- package/dist/lib/themeScope.d.ts.map +1 -0
- package/dist/lib/themeScope.js +10 -0
- package/dist/lib/themeScope.js.map +1 -0
- package/dist/lib/titleTooltips.d.ts +38 -0
- package/dist/lib/titleTooltips.d.ts.map +1 -0
- package/dist/lib/titleTooltips.js +166 -0
- package/dist/lib/titleTooltips.js.map +1 -0
- package/dist/lib/tooltipPlacement.d.ts +14 -0
- package/dist/lib/tooltipPlacement.d.ts.map +1 -0
- package/dist/lib/tooltipPlacement.js +34 -0
- package/dist/lib/tooltipPlacement.js.map +1 -0
- package/dist/modal.css +129 -0
- package/dist/number-input.css +131 -0
- package/dist/scrollbar.css +165 -0
- package/dist/select.css +251 -0
- package/dist/styles.css +1866 -0
- package/dist/task-ui-components.css +1866 -0
- package/dist/theme-picker.css +219 -0
- package/dist/toaster.css +159 -0
- package/dist/tooltip.css +103 -0
- package/dist/types/index.d.ts +37 -0
- package/dist/types/index.d.ts.map +1 -0
- package/package.json +60 -4
- package/src/app-header.css +288 -0
- package/src/bento.css +143 -0
- package/src/button.css +227 -0
- package/src/components/AppHeader.tsx +87 -0
- package/src/components/BentoCard.tsx +72 -0
- package/src/components/BentoGrid.tsx +48 -0
- package/src/components/Button.tsx +81 -0
- package/src/components/ConnectedSettings.tsx +465 -0
- package/src/components/ConnectedThemePicker.tsx +79 -0
- package/src/components/ContextMenu.tsx +48 -0
- package/src/components/HadokuThemePicker.tsx +39 -0
- package/src/components/LoadingSkeleton.tsx +84 -0
- package/src/components/Modal.tsx +171 -0
- package/src/components/NumberInput.tsx +161 -0
- package/src/components/Select.tsx +359 -0
- package/src/components/SettingsModal.tsx +235 -0
- package/src/components/ThemeIcons.tsx +127 -0
- package/src/components/ThemePicker.tsx +149 -0
- package/src/components/Toast.tsx +92 -0
- package/src/components/Toaster.tsx +43 -0
- package/src/components/Tooltip.tsx +212 -0
- package/src/context-menu.css +11 -0
- package/src/controls.css +137 -0
- package/src/hooks/useReducedMotion.ts +36 -0
- package/src/hooks/useToast.ts +57 -0
- package/src/icon-btn.css +64 -0
- package/src/index.ts +140 -0
- package/src/lib/menuPlacement.ts +31 -0
- package/src/lib/selectMenus.ts +303 -0
- package/src/lib/settingsClient.ts +377 -0
- package/src/lib/themeScope.ts +23 -0
- package/src/lib/titleTooltips.ts +236 -0
- package/src/lib/tooltipPlacement.ts +59 -0
- package/src/modal.css +129 -0
- package/src/number-input.css +131 -0
- package/src/scrollbar.css +165 -0
- package/src/select.css +251 -0
- package/src/theme-picker.css +219 -0
- package/src/toaster.css +159 -0
- package/src/tooltip.css +103 -0
- package/src/types/index.ts +43 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConnectedSettings.js","sources":["../../src/components/ConnectedSettings.tsx"],"sourcesContent":["/**\n * ConnectedSettings — the unified per-user settings control for the hadoku\n * ecosystem: a gear button that opens a popout housing everything that is a\n * property of the PERSON (not the app):\n * - current access tier (read-only, friendly label)\n * - display name (inline edit → POST /session/name)\n * - global content-visibility level (tiered pill → PUT /prefs/api/v1/content-level)\n * - switch key (an inline sign-in form, on the site's own sign-in client)\n *\n * \"Connected\" (parallel to ConnectedThemePicker): it self-wires to the\n * hadoku.me edge-router via same-origin fetches (see lib/settingsClient), so\n * dropping `<ConnectedSettings />` into any app under hadoku.me needs zero\n * props. Identity is resolved via whoami() unless the host passes `userType` /\n * `name` (avoids a redundant fetch when the app already knows).\n *\n * All of that is resolved AT MOUNT via `prefetchSettings()`, never on\n * gear-click — opening the popout must cost zero requests. See the effect\n * below, and `e2e/settings-prefetch.spec.ts` in hadoku-task, which fails if the\n * fetch moves back behind the click.\n *\n * Mirrors ThemePicker's controlled-popout pattern: owns its own `open` state +\n * a full-viewport overlay for click-outside dismiss. The content pill renders\n * `maxLevel` segments (friend ⇒ 3, admin ⇒ 4) and is hidden for public callers.\n * maxLevel comes from the server so the tier ceiling is never trusted from the\n * client.\n */\nimport React, { useCallback, useEffect, useRef, useState } from 'react'\nimport { SettingsIcon } from './ThemeIcons'\nimport {\n prefetchSettings,\n setContentLevel,\n setDisplayName,\n loadSignIn,\n switchKeyHref,\n type ContentLevelState,\n type Tier\n} from '../lib/settingsClient'\nimport '../app-header.css'\n\nexport interface ConnectedSettingsProps {\n /** Caller's tier. Omit to self-resolve via whoami().\n *\n * This exists for apps that CANNOT reach `/session/whoami` — conjure sits\n * behind a path-prefixed shim that serves nothing outside `/conjure`, so\n * whoami 404s and the tier would fall back to guest. It is an identity\n * hint, not a way to present a different tier than the edge assigns: every\n * gate that matters is enforced server-side off the edge-stamped tier. */\n userType?: Tier\n /** Display name. Omit to self-resolve via whoami(). Same rationale. */\n name?: string | null\n /** Notify the host (e.g. a \"Signed in as …\" label) of a rename. */\n onNameChange?: (name: string | null) => void\n /** App-specific settings rendered as a final section in the same popout, so\n * an app with its own preferences keeps ONE unified gear instead of a second\n * control. Shown under an \"App preferences\" divider.\n *\n * This slot is deliberately unconstrained — put whatever the app needs in\n * it. The constraint is only that it lands AFTER the four canonical rows and\n * cannot change them. */\n children?: React.ReactNode\n}\n\n/** Rejection sink for fire-and-forget settings actions. The client helpers\n * already resolve to null on failure (state reflects it), so this only trips\n * on truly unexpected throws — log rather than swallow silently. */\nfunction reportErr(err: unknown) {\n // task-ui-components v2 dropped its logger; keep the package dependency-free.\n console.error('[ConnectedSettings] action failed', (err as Error)?.message ?? err)\n}\n\nconst TIER_LABEL: Record<Tier, string> = {\n admin: 'Administrator',\n wife: 'Wife',\n service: 'Service',\n friend: 'Friend',\n public: 'Guest'\n}\n\nexport function ConnectedSettings({\n userType: userTypeProp,\n name: nameProp,\n onNameChange,\n children\n}: ConnectedSettingsProps) {\n const [open, setOpen] = useState(false)\n const rootRef = useRef<HTMLDivElement>(null)\n\n // Identity — from props if supplied, else resolved lazily via whoami().\n const [userType, setUserType] = useState<Tier>(userTypeProp ?? 'public')\n const [resolvedName, setResolvedName] = useState<string | null>(nameProp ?? null)\n\n // Name editing\n const [nameDraft, setNameDraft] = useState(resolvedName ?? '')\n const [editingName, setEditingName] = useState(false)\n const [nameSaving, setNameSaving] = useState(false)\n const [displayName, setLocalName] = useState<string | null>(resolvedName)\n\n // Key swap. The draft lives only in this state and the input: it is never\n // logged, stored or sent anywhere but the site's sign-in client.\n const [keyFormOpen, setKeyFormOpen] = useState(false)\n const [keyDraft, setKeyDraft] = useState('')\n const [keyBusy, setKeyBusy] = useState(false)\n const [keyError, setKeyError] = useState<string | null>(null)\n\n // Content level\n const [content, setContent] = useState<ContentLevelState | null>(null)\n const [levelSaving, setLevelSaving] = useState(false)\n\n // One flag for the whole prefetch, not one per request — the panel body\n // reveals when everything has settled, so nothing pops in a beat late. In\n // practice the user never sees it: the prefetch starts at mount and is long\n // done by the time the gear is clicked. It only shows if they click within\n // the first moments of the page.\n const [resolving, setResolving] = useState(true)\n\n // Everyone who is SIGNED IN, expressed as \"not public\" rather than as a list\n // of tiers. The list form (`=== 'admin' || === 'friend' || === 'service'`)\n // is an exact-match allowlist: it silently excluded `wife` the moment that\n // tier existed, which is the failure the rank model is meant to prevent.\n // There is no rank helper in this bundle, but \"above public\" needs none.\n const showContentPill = userType !== 'public'\n\n // Keep local identity in sync when the host controls it via props.\n useEffect(() => {\n if (userTypeProp !== undefined) setUserType(userTypeProp)\n }, [userTypeProp])\n useEffect(() => {\n if (nameProp !== undefined) {\n setResolvedName(nameProp)\n setLocalName(nameProp)\n setNameDraft(nameProp ?? '')\n }\n }, [nameProp])\n\n // Resolve everything the panel shows AT MOUNT — not on open.\n //\n // This used to be gated on `open`, which made the gear the trigger for\n // whoami + content-level (167ms / 186ms on prod, concurrent) — a round trip\n // the user waited out with the panel already on screen and blank. Nothing\n // about that data depends on the click:\n // it's the signed-in user's own identity, settled at boot and unchanged for\n // the life of the page. So it rides along with the app's boot traffic, and\n // clicking the gear costs zero requests. `e2e/settings-prefetch.spec.ts` in\n // hadoku-task pins that.\n //\n // The client memoises across callers, so mounting this in several places (or\n // remounting it) does not multiply requests, and on hadoku.me the identity\n // half is free — it awaits the whoami the shell already had in flight.\n useEffect(() => {\n let live = true\n prefetchSettings({ userType: userTypeProp, name: nameProp })\n .then(({ identity, content: state }) => {\n if (!live) return\n // Props stay authoritative: the host passed them because it can see an\n // identity this component can't resolve for itself.\n if (userTypeProp === undefined) {\n setUserType(identity.userType)\n setResolvedName(identity.name)\n setLocalName(identity.name)\n setNameDraft(identity.name ?? '')\n }\n if (state) setContent(state)\n })\n .catch(reportErr)\n .finally(() => {\n if (live) setResolving(false)\n })\n return () => {\n live = false\n }\n // Hints are read once, on the first mount that gets there — the prefetch is\n // page-scoped and a later prop change cannot un-issue it. Identity changes\n // arrive through the sync effects above, and a key swap reloads the page.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [])\n\n // Escape closes.\n useEffect(() => {\n if (!open) return\n const onKey = (e: KeyboardEvent) => {\n if (e.key === 'Escape') setOpen(false)\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [open])\n\n const saveName = useCallback(async () => {\n const trimmed = nameDraft.trim()\n if (!trimmed || trimmed === displayName) {\n setEditingName(false)\n return\n }\n setNameSaving(true)\n const stored = await setDisplayName(trimmed)\n setNameSaving(false)\n if (stored !== null) {\n setLocalName(stored)\n setNameDraft(stored)\n onNameChange?.(stored)\n }\n setEditingName(false)\n }, [nameDraft, displayName, onNameChange])\n\n // Open the form AND start loading the site's sign-in client, which begins its\n // fingerprint collection as it evaluates — so it is ready by submit. Where the\n // client cannot load at all (not on hadoku.me), the page-level /auth flow is\n // the only way to sign in, so go there.\n const openKeyForm = useCallback(() => {\n setKeyFormOpen(true)\n setKeyError(null)\n loadSignIn()\n .then(signIn => {\n if (!signIn) window.location.assign(switchKeyHref())\n })\n .catch(reportErr)\n }, [])\n\n const submitKey = useCallback(async () => {\n setKeyBusy(true)\n setKeyError(null)\n const signIn = await loadSignIn()\n if (!signIn) {\n window.location.assign(switchKeyHref())\n return\n }\n try {\n await signIn(keyDraft)\n } catch (err) {\n // A refused key is an inline error, never a redirect.\n setKeyError((err as Error)?.message || 'Invalid key')\n setKeyBusy(false)\n return\n }\n // The page now acts for a different identity: everything must re-mount.\n // Busy stays on through the reload so the form can't be resubmitted.\n window.location.reload()\n }, [keyDraft])\n\n const pickLevel = useCallback(\n async (level: number) => {\n if (!content || level === content.level || levelSaving) return\n const prev = content\n setContent({ ...content, level }) // optimistic\n setLevelSaving(true)\n const result = await setContentLevel(level)\n setLevelSaving(false)\n setContent(result ?? prev) // adopt server truth, or revert\n },\n [content, levelSaving]\n )\n\n // No caller-supplied class on the root. The four rows below are the\n // platform's, not the app's, and a className here is enough to restyle or\n // hide any of them — which is the drift this component exists to prevent.\n // Apps extend via `children`, never by reaching into this markup.\n return (\n <div className=\"settings-popout\" ref={rootRef}>\n <button\n type=\"button\"\n className=\"app-header__icon-btn settings-toggle-btn\"\n aria-label=\"User settings\"\n aria-expanded={open}\n onClick={() => setOpen(v => !v)}\n >\n <SettingsIcon />\n </button>\n\n {open && <div className=\"settings-popout__overlay\" onClick={() => setOpen(false)} />}\n\n {open && (\n <div className=\"settings-popout__panel\" role=\"dialog\" aria-label=\"User settings\">\n <div className=\"settings-popout__header\">Settings</div>\n\n {/* Access tier. While resolving, the state still says 'public' —\n rendering \"Guest\" there would be a wrong answer that corrects\n itself a moment later, which reads exactly like the lag this\n placeholder exists to remove. */}\n <section className=\"settings-popout__row\">\n <span className=\"settings-popout__label\">Access tier</span>\n {resolving ? (\n <span className=\"settings-popout__value\">…</span>\n ) : (\n <span className={`settings-popout__tier settings-popout__tier--${userType}`}>\n {TIER_LABEL[userType]}\n </span>\n )}\n </section>\n\n {/* Display name */}\n <section className=\"settings-popout__row\">\n <span className=\"settings-popout__label\">Display name</span>\n {resolving ? (\n <span className=\"settings-popout__value\">…</span>\n ) : editingName ? (\n <span className=\"settings-popout__inline\">\n <input\n className=\"settings-popout__input\"\n value={nameDraft}\n maxLength={64}\n autoFocus\n disabled={nameSaving}\n onChange={e => setNameDraft(e.target.value)}\n onKeyDown={e => {\n if (e.key === 'Enter') {\n saveName().catch(reportErr)\n }\n if (e.key === 'Escape') {\n setEditingName(false)\n setNameDraft(displayName ?? '')\n }\n }}\n />\n <button\n type=\"button\"\n className=\"settings-popout__btn settings-popout__btn--primary\"\n disabled={nameSaving}\n onClick={() => {\n saveName().catch(reportErr)\n }}\n >\n {nameSaving ? '…' : 'Save'}\n </button>\n </span>\n ) : (\n <span className=\"settings-popout__inline\">\n <span className=\"settings-popout__value\">{displayName || 'Unnamed'}</span>\n <button\n type=\"button\"\n className=\"settings-popout__btn\"\n onClick={() => {\n setNameDraft(displayName ?? '')\n setEditingName(true)\n }}\n >\n Edit\n </button>\n </span>\n )}\n </section>\n\n {/* Content visibility level. Held back until the first-open fetch\n settles, so it lands in the same paint as the tier and name\n rather than a round trip behind them. `content` is fetched\n concurrently with whoami(), so by then it is already here. */}\n {!resolving && showContentPill && (\n <section className=\"settings-popout__row settings-popout__row--stack\">\n <span className=\"settings-popout__label\">Content visibility</span>\n {content ? (\n <>\n <div\n className=\"settings-popout__pill\"\n role=\"radiogroup\"\n aria-label=\"Content visibility level\"\n >\n {Array.from({ length: content.maxLevel }, (_, i) => i + 1).map(lvl => (\n <button\n key={lvl}\n type=\"button\"\n role=\"radio\"\n aria-checked={lvl === content.level}\n aria-label={`Level ${lvl}`}\n className={`settings-popout__seg${\n lvl <= content.level ? ' settings-popout__seg--filled' : ''\n }`}\n disabled={levelSaving}\n onClick={() => {\n pickLevel(lvl).catch(reportErr)\n }}\n >\n {lvl}\n </button>\n ))}\n </div>\n <span className=\"settings-popout__hint\">\n Higher levels reveal more mature content across apps. Level 1 is\n safe-for-everyone.\n </span>\n </>\n ) : (\n // Not \"Loading…\" — the fetch has already settled by the time\n // this section renders at all, so a null here means it failed.\n <span className=\"settings-popout__hint\">Currently unavailable.</span>\n )}\n </section>\n )}\n\n {/* Switch key. Signs in inline through the SITE's sign-in client\n (the /auth page's own code path), never a post of ours. A real\n <form> with a username + current-password pair, so password\n managers offer and save the key. */}\n {keyFormOpen ? (\n <section className=\"settings-popout__row settings-popout__row--stack\">\n <form\n className=\"settings-popout__key-form\"\n onSubmit={e => {\n e.preventDefault()\n submitKey().catch(reportErr)\n }}\n >\n <input\n type=\"text\"\n name=\"username\"\n autoComplete=\"username\"\n value=\"hadoku\"\n readOnly\n hidden\n />\n <label className=\"settings-popout__label\" htmlFor=\"settings-popout-key\">\n Access key\n </label>\n <span className=\"settings-popout__inline\">\n <input\n id=\"settings-popout-key\"\n className=\"settings-popout__input\"\n type=\"password\"\n name=\"password\"\n autoComplete=\"current-password\"\n autoCapitalize=\"none\"\n spellCheck={false}\n autoFocus\n value={keyDraft}\n disabled={keyBusy}\n aria-invalid={keyError !== null}\n aria-describedby={keyError ? 'settings-popout-key-error' : undefined}\n onChange={e => setKeyDraft(e.target.value)}\n />\n <button\n type=\"submit\"\n className=\"settings-popout__btn settings-popout__btn--primary\"\n disabled={keyBusy}\n >\n {keyBusy ? 'Signing in…' : 'Sign in'}\n </button>\n </span>\n {keyError && (\n <span\n id=\"settings-popout-key-error\"\n className=\"settings-popout__error\"\n role=\"alert\"\n >\n {keyError}\n </span>\n )}\n </form>\n </section>\n ) : (\n <section className=\"settings-popout__row\">\n <span className=\"settings-popout__label\">Access key</span>\n <button type=\"button\" className=\"settings-popout__btn\" onClick={openKeyForm}>\n Switch key\n </button>\n </section>\n )}\n\n {/* App-specific settings (optional) — same popout, one gear. */}\n {children && (\n <section className=\"settings-popout__row settings-popout__row--stack settings-popout__app\">\n {children}\n </section>\n )}\n </div>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConnectedThemePicker - Stateful theme picker component
|
|
3
|
+
* A convenience wrapper around ThemePicker with built-in state management
|
|
4
|
+
*/
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import type { ThemeFamily, DropdownPlacement } from '../types';
|
|
7
|
+
export interface ConnectedThemePickerProps {
|
|
8
|
+
/** Available theme families */
|
|
9
|
+
themeFamilies: ThemeFamily[];
|
|
10
|
+
/** Current active theme */
|
|
11
|
+
currentTheme: string;
|
|
12
|
+
/** Callback when theme is changed */
|
|
13
|
+
onThemeChange: (theme: string) => void;
|
|
14
|
+
/** Optional: Function to get icon for current theme (for toggle button) */
|
|
15
|
+
getThemeIcon?: (theme: string) => React.ReactNode;
|
|
16
|
+
/** Optional: CSS class name for the container */
|
|
17
|
+
className?: string;
|
|
18
|
+
/** Optional callback when settings icon is clicked */
|
|
19
|
+
onSettingsClick?: () => void;
|
|
20
|
+
/** Optional: Direction the dropdown opens. Defaults to 'auto' (edge detection) */
|
|
21
|
+
dropdownPlacement?: DropdownPlacement;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* ConnectedThemePicker - A stateful theme picker with built-in open/close state management
|
|
25
|
+
*
|
|
26
|
+
* This component manages its own isOpen state, making it easier to use than the base ThemePicker.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* import { ConnectedThemePicker } from '@wolffm/task-ui-components'
|
|
31
|
+
* import { THEME_FAMILIES, THEME_ICON_MAP } from '@wolffm/themes'
|
|
32
|
+
* import '@wolffm/task-ui-components/theme-picker.css'
|
|
33
|
+
*
|
|
34
|
+
* function App() {
|
|
35
|
+
* const { theme, setTheme } = useTheme()
|
|
36
|
+
*
|
|
37
|
+
* return (
|
|
38
|
+
* <ConnectedThemePicker
|
|
39
|
+
* themeFamilies={THEME_FAMILIES}
|
|
40
|
+
* currentTheme={theme}
|
|
41
|
+
* onThemeChange={setTheme}
|
|
42
|
+
* getThemeIcon={(theme) => {
|
|
43
|
+
* const Icon = THEME_ICON_MAP[theme]
|
|
44
|
+
* return Icon ? <Icon /> : null
|
|
45
|
+
* }}
|
|
46
|
+
* />
|
|
47
|
+
* )
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare function ConnectedThemePicker({ themeFamilies, currentTheme, onThemeChange, getThemeIcon, className, onSettingsClick, dropdownPlacement }: ConnectedThemePickerProps): React.JSX.Element;
|
|
52
|
+
//# sourceMappingURL=ConnectedThemePicker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConnectedThemePicker.d.ts","sourceRoot":"","sources":["../../src/components/ConnectedThemePicker.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAE9D,MAAM,WAAW,yBAAyB;IACxC,+BAA+B;IAC/B,aAAa,EAAE,WAAW,EAAE,CAAA;IAC5B,2BAA2B;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,qCAAqC;IACrC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,2EAA2E;IAC3E,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC,SAAS,CAAA;IACjD,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sDAAsD;IACtD,eAAe,CAAC,EAAE,MAAM,IAAI,CAAA;IAC5B,kFAAkF;IAClF,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;CACtC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,aAAa,EACb,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,SAAS,EACT,eAAe,EACf,iBAAiB,EAClB,EAAE,yBAAyB,qBAgB3B"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { ThemePicker } from "./ThemePicker.js";
|
|
4
|
+
function ConnectedThemePicker({
|
|
5
|
+
themeFamilies,
|
|
6
|
+
currentTheme,
|
|
7
|
+
onThemeChange,
|
|
8
|
+
getThemeIcon,
|
|
9
|
+
className,
|
|
10
|
+
onSettingsClick,
|
|
11
|
+
dropdownPlacement
|
|
12
|
+
}) {
|
|
13
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
14
|
+
return /* @__PURE__ */ jsx(
|
|
15
|
+
ThemePicker,
|
|
16
|
+
{
|
|
17
|
+
currentTheme,
|
|
18
|
+
isOpen,
|
|
19
|
+
themeFamilies,
|
|
20
|
+
onThemeChange,
|
|
21
|
+
onToggle: () => setIsOpen(!isOpen),
|
|
22
|
+
getThemeIcon,
|
|
23
|
+
className,
|
|
24
|
+
onSettingsClick,
|
|
25
|
+
dropdownPlacement
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
ConnectedThemePicker
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=ConnectedThemePicker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConnectedThemePicker.js","sources":["../../src/components/ConnectedThemePicker.tsx"],"sourcesContent":["/**\r\n * ConnectedThemePicker - Stateful theme picker component\r\n * A convenience wrapper around ThemePicker with built-in state management\r\n */\r\n\r\nimport React from 'react'\r\nimport { ThemePicker } from './ThemePicker'\r\nimport type { ThemeFamily, DropdownPlacement } from '../types'\r\n\r\nexport interface ConnectedThemePickerProps {\r\n /** Available theme families */\r\n themeFamilies: ThemeFamily[]\r\n /** Current active theme */\r\n currentTheme: string\r\n /** Callback when theme is changed */\r\n onThemeChange: (theme: string) => void\r\n /** Optional: Function to get icon for current theme (for toggle button) */\r\n getThemeIcon?: (theme: string) => React.ReactNode\r\n /** Optional: CSS class name for the container */\r\n className?: string\r\n /** Optional callback when settings icon is clicked */\r\n onSettingsClick?: () => void\r\n /** Optional: Direction the dropdown opens. Defaults to 'auto' (edge detection) */\r\n dropdownPlacement?: DropdownPlacement\r\n}\r\n\r\n/**\r\n * ConnectedThemePicker - A stateful theme picker with built-in open/close state management\r\n *\r\n * This component manages its own isOpen state, making it easier to use than the base ThemePicker.\r\n *\r\n * @example\r\n * ```tsx\r\n * import { ConnectedThemePicker } from '@wolffm/task-ui-components'\r\n * import { THEME_FAMILIES, THEME_ICON_MAP } from '@wolffm/themes'\r\n * import '@wolffm/task-ui-components/theme-picker.css'\r\n *\r\n * function App() {\r\n * const { theme, setTheme } = useTheme()\r\n *\r\n * return (\r\n * <ConnectedThemePicker\r\n * themeFamilies={THEME_FAMILIES}\r\n * currentTheme={theme}\r\n * onThemeChange={setTheme}\r\n * getThemeIcon={(theme) => {\r\n * const Icon = THEME_ICON_MAP[theme]\r\n * return Icon ? <Icon /> : null\r\n * }}\r\n * />\r\n * )\r\n * }\r\n * ```\r\n */\r\nexport function ConnectedThemePicker({\r\n themeFamilies,\r\n currentTheme,\r\n onThemeChange,\r\n getThemeIcon,\r\n className,\r\n onSettingsClick,\r\n dropdownPlacement\r\n}: ConnectedThemePickerProps) {\r\n const [isOpen, setIsOpen] = React.useState(false)\r\n\r\n return (\r\n <ThemePicker\r\n currentTheme={currentTheme}\r\n isOpen={isOpen}\r\n themeFamilies={themeFamilies}\r\n onThemeChange={onThemeChange}\r\n onToggle={() => setIsOpen(!isOpen)}\r\n getThemeIcon={getThemeIcon}\r\n className={className}\r\n onSettingsClick={onSettingsClick}\r\n dropdownPlacement={dropdownPlacement}\r\n />\r\n )\r\n}\r\n"],"names":[],"mappings":";;;AAsDO,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA8B;AAC5B,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAS,KAAK;AAEhD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,UAAU,MAAM,UAAU,CAAC,MAAM;AAAA,MACjC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,EAAA;AAGN;"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContextMenu Component
|
|
3
|
+
* Generic context menu with position-based rendering
|
|
4
|
+
*/
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import '../context-menu.css';
|
|
7
|
+
export interface ContextMenuItem {
|
|
8
|
+
/** ReactNode, not string, so callers can put an <Icon> beside the text. */
|
|
9
|
+
label: React.ReactNode;
|
|
10
|
+
onClick: () => void | Promise<void>;
|
|
11
|
+
isDanger?: boolean;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface ContextMenuProps {
|
|
15
|
+
isOpen: boolean;
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
items: ContextMenuItem[];
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function ContextMenu({ isOpen, x, y, items, className }: ContextMenuProps): React.JSX.Element | null;
|
|
22
|
+
//# sourceMappingURL=ContextMenu.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContextMenu.d.ts","sourceRoot":"","sources":["../../src/components/ContextMenu.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,qBAAqB,CAAA;AAE5B,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,KAAK,EAAE,KAAK,CAAC,SAAS,CAAA;IACtB,OAAO,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,OAAO,CAAA;IACf,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,eAAe,EAAE,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,SAA0B,EAAE,EAAE,gBAAgB,4BAuBhG"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
/* empty css */
|
|
3
|
+
function ContextMenu({ isOpen, x, y, items, className = "context-menu" }) {
|
|
4
|
+
if (!isOpen) return null;
|
|
5
|
+
const style = {
|
|
6
|
+
"--context-menu-x": `${x}px`,
|
|
7
|
+
"--context-menu-y": `${y}px`
|
|
8
|
+
};
|
|
9
|
+
return /* @__PURE__ */ jsx("div", { className, style, children: items.map((item, index) => /* @__PURE__ */ jsx(
|
|
10
|
+
"button",
|
|
11
|
+
{
|
|
12
|
+
className: `context-menu-item ${item.isDanger ? "context-menu-item--danger" : ""}`,
|
|
13
|
+
onClick: item.onClick,
|
|
14
|
+
disabled: item.disabled,
|
|
15
|
+
children: item.label
|
|
16
|
+
},
|
|
17
|
+
index
|
|
18
|
+
)) });
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
ContextMenu
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=ContextMenu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContextMenu.js","sources":["../../src/components/ContextMenu.tsx"],"sourcesContent":["/**\n * ContextMenu Component\n * Generic context menu with position-based rendering\n */\n\nimport React from 'react'\nimport '../context-menu.css'\n\nexport interface ContextMenuItem {\n /** ReactNode, not string, so callers can put an <Icon> beside the text. */\n label: React.ReactNode\n onClick: () => void | Promise<void>\n isDanger?: boolean\n disabled?: boolean\n}\n\nexport interface ContextMenuProps {\n isOpen: boolean\n x: number\n y: number\n items: ContextMenuItem[]\n className?: string\n}\n\nexport function ContextMenu({ isOpen, x, y, items, className = 'context-menu' }: ContextMenuProps) {\n if (!isOpen) return null\n\n // Use CSS custom properties for dynamic positioning\n const style = {\n '--context-menu-x': `${x}px`,\n '--context-menu-y': `${y}px`\n } as React.CSSProperties\n\n return (\n <div className={className} style={style}>\n {items.map((item, index) => (\n <button\n key={index}\n className={`context-menu-item ${item.isDanger ? 'context-menu-item--danger' : ''}`}\n onClick={item.onClick}\n disabled={item.disabled}\n >\n {item.label}\n </button>\n ))}\n </div>\n )\n}\n"],"names":[],"mappings":";;AAwBO,SAAS,YAAY,EAAE,QAAQ,GAAG,GAAG,OAAO,YAAY,kBAAoC;AACjG,MAAI,CAAC,OAAQ,QAAO;AAGpB,QAAM,QAAQ;AAAA,IACZ,oBAAoB,GAAG,CAAC;AAAA,IACxB,oBAAoB,GAAG,CAAC;AAAA,EAAA;AAG1B,SACE,oBAAC,SAAI,WAAsB,OACxB,gBAAM,IAAI,CAAC,MAAM,UAChB;AAAA,IAAC;AAAA,IAAA;AAAA,MAEC,WAAW,qBAAqB,KAAK,WAAW,8BAA8B,EAAE;AAAA,MAChF,SAAS,KAAK;AAAA,MACd,UAAU,KAAK;AAAA,MAEd,UAAA,KAAK;AAAA,IAAA;AAAA,IALD;AAAA,EAAA,CAOR,GACH;AAEJ;"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HadokuThemePicker — the platform theme picker, placeable anywhere, and
|
|
3
|
+
* configurable nowhere.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS
|
|
6
|
+
* ---------------
|
|
7
|
+
* Locking the picker inside AppHeader made it uniform, but it also made it
|
|
8
|
+
* unreachable for an app that has no header row. watchpart2 is that app: its
|
|
9
|
+
* UI is an overlay on a video surface, and its picker lives in the chat panel's
|
|
10
|
+
* button strip. Under a header-only API its only options were to grow a header
|
|
11
|
+
* it does not want, or keep hand-rolling — and hand-rolling is what this whole
|
|
12
|
+
* effort removes.
|
|
13
|
+
*
|
|
14
|
+
* So the enforcement is not "you may not PLACE the picker", it is "you may not
|
|
15
|
+
* CONFIGURE it". This component takes no props at all. There is no themeFamilies
|
|
16
|
+
* to pass a different list to, no getThemeIcon to slip a debug logger into, no
|
|
17
|
+
* className to restyle it with, no isOpen to manage differently — those props
|
|
18
|
+
* are exactly how the old ConnectedThemePicker drifted. Everything comes from
|
|
19
|
+
* <HadokuThemeRoot>.
|
|
20
|
+
*
|
|
21
|
+
* AppHeader renders this internally. An app with no header renders it directly.
|
|
22
|
+
* Both get the identical control, because there is no way to ask for a
|
|
23
|
+
* different one.
|
|
24
|
+
*/
|
|
25
|
+
import React from 'react';
|
|
26
|
+
export declare function HadokuThemePicker(): React.JSX.Element;
|
|
27
|
+
//# sourceMappingURL=HadokuThemePicker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HadokuThemePicker.d.ts","sourceRoot":"","sources":["../../src/components/HadokuThemePicker.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,KAAK,MAAM,OAAO,CAAA;AAIzB,wBAAgB,iBAAiB,sBAUhC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ConnectedThemePicker } from "./ConnectedThemePicker.js";
|
|
3
|
+
import { useHadokuTheme } from "@wolffm/themes";
|
|
4
|
+
function HadokuThemePicker() {
|
|
5
|
+
const t = useHadokuTheme();
|
|
6
|
+
return /* @__PURE__ */ jsx(
|
|
7
|
+
ConnectedThemePicker,
|
|
8
|
+
{
|
|
9
|
+
themeFamilies: t.themeFamilies,
|
|
10
|
+
currentTheme: t.theme,
|
|
11
|
+
onThemeChange: t.setTheme,
|
|
12
|
+
getThemeIcon: t.getThemeIcon
|
|
13
|
+
}
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
export {
|
|
17
|
+
HadokuThemePicker
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=HadokuThemePicker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HadokuThemePicker.js","sources":["../../src/components/HadokuThemePicker.tsx"],"sourcesContent":["/**\n * HadokuThemePicker — the platform theme picker, placeable anywhere, and\n * configurable nowhere.\n *\n * WHY THIS EXISTS\n * ---------------\n * Locking the picker inside AppHeader made it uniform, but it also made it\n * unreachable for an app that has no header row. watchpart2 is that app: its\n * UI is an overlay on a video surface, and its picker lives in the chat panel's\n * button strip. Under a header-only API its only options were to grow a header\n * it does not want, or keep hand-rolling — and hand-rolling is what this whole\n * effort removes.\n *\n * So the enforcement is not \"you may not PLACE the picker\", it is \"you may not\n * CONFIGURE it\". This component takes no props at all. There is no themeFamilies\n * to pass a different list to, no getThemeIcon to slip a debug logger into, no\n * className to restyle it with, no isOpen to manage differently — those props\n * are exactly how the old ConnectedThemePicker drifted. Everything comes from\n * <HadokuThemeRoot>.\n *\n * AppHeader renders this internally. An app with no header renders it directly.\n * Both get the identical control, because there is no way to ask for a\n * different one.\n */\nimport React from 'react'\nimport { ConnectedThemePicker } from './ConnectedThemePicker'\nimport { useHadokuTheme } from '@wolffm/themes'\n\nexport function HadokuThemePicker() {\n const t = useHadokuTheme()\n return (\n <ConnectedThemePicker\n themeFamilies={t.themeFamilies}\n currentTheme={t.theme}\n onThemeChange={t.setTheme}\n getThemeIcon={t.getThemeIcon}\n />\n )\n}\n"],"names":[],"mappings":";;;AA4BO,SAAS,oBAAoB;AAClC,QAAM,IAAI,eAAA;AACV,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,eAAe,EAAE;AAAA,MACjB,cAAc,EAAE;AAAA,MAChB,eAAe,EAAE;AAAA,MACjB,cAAc,EAAE;AAAA,IAAA;AAAA,EAAA;AAGtB;"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LoadingSkeleton Component
|
|
3
|
+
* Displays a loading skeleton with theme support
|
|
4
|
+
* Can be customized with different layouts
|
|
5
|
+
*/
|
|
6
|
+
import React from 'react';
|
|
7
|
+
export interface LoadingSkeletonProps {
|
|
8
|
+
isDarkTheme?: boolean;
|
|
9
|
+
className?: string;
|
|
10
|
+
layout?: 'default' | 'minimal' | 'custom';
|
|
11
|
+
customContent?: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
export declare function LoadingSkeleton({ isDarkTheme, className, layout, customContent }: LoadingSkeletonProps): React.JSX.Element;
|
|
14
|
+
//# sourceMappingURL=LoadingSkeleton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LoadingSkeleton.d.ts","sourceRoot":"","sources":["../../src/components/LoadingSkeleton.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AAEzB,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAA;IACzC,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CAChC;AAED,wBAAgB,eAAe,CAAC,EAC9B,WAAmB,EACnB,SAAc,EACd,MAAkB,EAClB,aAAa,EACd,EAAE,oBAAoB,qBA+DtB"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
function LoadingSkeleton({
|
|
3
|
+
isDarkTheme = false,
|
|
4
|
+
className = "",
|
|
5
|
+
layout = "default",
|
|
6
|
+
customContent
|
|
7
|
+
}) {
|
|
8
|
+
if (layout === "custom" && customContent) {
|
|
9
|
+
return /* @__PURE__ */ jsx(
|
|
10
|
+
"div",
|
|
11
|
+
{
|
|
12
|
+
className: `loading-skeleton ${className}`,
|
|
13
|
+
"data-dark-theme": isDarkTheme ? "true" : "false",
|
|
14
|
+
children: customContent
|
|
15
|
+
}
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
if (layout === "minimal") {
|
|
19
|
+
return /* @__PURE__ */ jsx(
|
|
20
|
+
"div",
|
|
21
|
+
{
|
|
22
|
+
className: `loading-skeleton loading-skeleton--minimal ${className}`,
|
|
23
|
+
"data-dark-theme": isDarkTheme ? "true" : "false",
|
|
24
|
+
children: /* @__PURE__ */ jsx("div", { className: "skeleton-spinner" })
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return /* @__PURE__ */ jsx(
|
|
29
|
+
"div",
|
|
30
|
+
{
|
|
31
|
+
className: `loading-skeleton ${className}`,
|
|
32
|
+
"data-dark-theme": isDarkTheme ? "true" : "false",
|
|
33
|
+
children: /* @__PURE__ */ jsxs("div", { className: "loading-skeleton__content", children: [
|
|
34
|
+
/* @__PURE__ */ jsxs("div", { className: "skeleton-header-row", children: [
|
|
35
|
+
/* @__PURE__ */ jsx("div", { className: "skeleton-header" }),
|
|
36
|
+
/* @__PURE__ */ jsx("div", { className: "skeleton-button" })
|
|
37
|
+
] }),
|
|
38
|
+
/* @__PURE__ */ jsxs("div", { className: "skeleton-nav", children: [
|
|
39
|
+
/* @__PURE__ */ jsx("div", { className: "skeleton-nav-item" }),
|
|
40
|
+
/* @__PURE__ */ jsx("div", { className: "skeleton-nav-item" }),
|
|
41
|
+
/* @__PURE__ */ jsx("div", { className: "skeleton-nav-item" })
|
|
42
|
+
] }),
|
|
43
|
+
/* @__PURE__ */ jsx("div", { className: "skeleton-input" }),
|
|
44
|
+
/* @__PURE__ */ jsxs("div", { className: "skeleton-filters", children: [
|
|
45
|
+
/* @__PURE__ */ jsx("div", { className: "skeleton-filter" }),
|
|
46
|
+
/* @__PURE__ */ jsx("div", { className: "skeleton-filter" }),
|
|
47
|
+
/* @__PURE__ */ jsx("div", { className: "skeleton-filter" })
|
|
48
|
+
] }),
|
|
49
|
+
/* @__PURE__ */ jsxs("div", { className: "skeleton-content", children: [
|
|
50
|
+
/* @__PURE__ */ jsx("div", { className: "skeleton-content-header" }),
|
|
51
|
+
/* @__PURE__ */ jsx("div", { className: "skeleton-item" }),
|
|
52
|
+
/* @__PURE__ */ jsx("div", { className: "skeleton-item" }),
|
|
53
|
+
/* @__PURE__ */ jsx("div", { className: "skeleton-item" })
|
|
54
|
+
] })
|
|
55
|
+
] })
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
LoadingSkeleton
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=LoadingSkeleton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LoadingSkeleton.js","sources":["../../src/components/LoadingSkeleton.tsx"],"sourcesContent":["/**\r\n * LoadingSkeleton Component\r\n * Displays a loading skeleton with theme support\r\n * Can be customized with different layouts\r\n */\r\n\r\nimport React from 'react'\r\n\r\nexport interface LoadingSkeletonProps {\r\n isDarkTheme?: boolean\r\n className?: string\r\n layout?: 'default' | 'minimal' | 'custom'\r\n customContent?: React.ReactNode\r\n}\r\n\r\nexport function LoadingSkeleton({\r\n isDarkTheme = false,\r\n className = '',\r\n layout = 'default',\r\n customContent\r\n}: LoadingSkeletonProps) {\r\n if (layout === 'custom' && customContent) {\r\n return (\r\n <div\r\n className={`loading-skeleton ${className}`}\r\n data-dark-theme={isDarkTheme ? 'true' : 'false'}\r\n >\r\n {customContent}\r\n </div>\r\n )\r\n }\r\n\r\n if (layout === 'minimal') {\r\n return (\r\n <div\r\n className={`loading-skeleton loading-skeleton--minimal ${className}`}\r\n data-dark-theme={isDarkTheme ? 'true' : 'false'}\r\n >\r\n <div className=\"skeleton-spinner\"></div>\r\n </div>\r\n )\r\n }\r\n\r\n // Default layout - task app specific but can be overridden\r\n return (\r\n <div\r\n className={`loading-skeleton ${className}`}\r\n data-dark-theme={isDarkTheme ? 'true' : 'false'}\r\n >\r\n <div className=\"loading-skeleton__content\">\r\n {/* Header */}\r\n <div className=\"skeleton-header-row\">\r\n <div className=\"skeleton-header\"></div>\r\n <div className=\"skeleton-button\"></div>\r\n </div>\r\n\r\n {/* Navigation */}\r\n <div className=\"skeleton-nav\">\r\n <div className=\"skeleton-nav-item\"></div>\r\n <div className=\"skeleton-nav-item\"></div>\r\n <div className=\"skeleton-nav-item\"></div>\r\n </div>\r\n\r\n {/* Input */}\r\n <div className=\"skeleton-input\"></div>\r\n\r\n {/* Filters */}\r\n <div className=\"skeleton-filters\">\r\n <div className=\"skeleton-filter\"></div>\r\n <div className=\"skeleton-filter\"></div>\r\n <div className=\"skeleton-filter\"></div>\r\n </div>\r\n\r\n {/* Content */}\r\n <div className=\"skeleton-content\">\r\n <div className=\"skeleton-content-header\"></div>\r\n <div className=\"skeleton-item\"></div>\r\n <div className=\"skeleton-item\"></div>\r\n <div className=\"skeleton-item\"></div>\r\n </div>\r\n </div>\r\n </div>\r\n )\r\n}\r\n"],"names":[],"mappings":";AAeO,SAAS,gBAAgB;AAAA,EAC9B,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,SAAS;AAAA,EACT;AACF,GAAyB;AACvB,MAAI,WAAW,YAAY,eAAe;AACxC,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAW,oBAAoB,SAAS;AAAA,QACxC,mBAAiB,cAAc,SAAS;AAAA,QAEvC,UAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAGP;AAEA,MAAI,WAAW,WAAW;AACxB,WACE;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAW,8CAA8C,SAAS;AAAA,QAClE,mBAAiB,cAAc,SAAS;AAAA,QAExC,UAAA,oBAAC,OAAA,EAAI,WAAU,mBAAA,CAAmB;AAAA,MAAA;AAAA,IAAA;AAAA,EAGxC;AAGA,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,WAAW,oBAAoB,SAAS;AAAA,MACxC,mBAAiB,cAAc,SAAS;AAAA,MAExC,UAAA,qBAAC,OAAA,EAAI,WAAU,6BAEb,UAAA;AAAA,QAAA,qBAAC,OAAA,EAAI,WAAU,uBACb,UAAA;AAAA,UAAA,oBAAC,OAAA,EAAI,WAAU,kBAAA,CAAkB;AAAA,UACjC,oBAAC,OAAA,EAAI,WAAU,kBAAA,CAAkB;AAAA,QAAA,GACnC;AAAA,QAGA,qBAAC,OAAA,EAAI,WAAU,gBACb,UAAA;AAAA,UAAA,oBAAC,OAAA,EAAI,WAAU,oBAAA,CAAoB;AAAA,UACnC,oBAAC,OAAA,EAAI,WAAU,oBAAA,CAAoB;AAAA,UACnC,oBAAC,OAAA,EAAI,WAAU,oBAAA,CAAoB;AAAA,QAAA,GACrC;AAAA,QAGA,oBAAC,OAAA,EAAI,WAAU,iBAAA,CAAiB;AAAA,QAGhC,qBAAC,OAAA,EAAI,WAAU,oBACb,UAAA;AAAA,UAAA,oBAAC,OAAA,EAAI,WAAU,kBAAA,CAAkB;AAAA,UACjC,oBAAC,OAAA,EAAI,WAAU,kBAAA,CAAkB;AAAA,UACjC,oBAAC,OAAA,EAAI,WAAU,kBAAA,CAAkB;AAAA,QAAA,GACnC;AAAA,QAGA,qBAAC,OAAA,EAAI,WAAU,oBACb,UAAA;AAAA,UAAA,oBAAC,OAAA,EAAI,WAAU,0BAAA,CAA0B;AAAA,UACzC,oBAAC,OAAA,EAAI,WAAU,gBAAA,CAAgB;AAAA,UAC/B,oBAAC,OAAA,EAAI,WAAU,gBAAA,CAAgB;AAAA,UAC/B,oBAAC,OAAA,EAAI,WAAU,gBAAA,CAAgB;AAAA,QAAA,EAAA,CACjC;AAAA,MAAA,EAAA,CACF;AAAA,IAAA;AAAA,EAAA;AAGN;"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal Component
|
|
3
|
+
* Generic modal dialog with keyboard shortcuts and customizable actions
|
|
4
|
+
*
|
|
5
|
+
* Styles ship with the package: import '@wolffm/task-ui-components/modal.css'
|
|
6
|
+
*/
|
|
7
|
+
import React from 'react';
|
|
8
|
+
export interface ModalProps {
|
|
9
|
+
isOpen: boolean;
|
|
10
|
+
title: string;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
onConfirm?: () => void | Promise<void>;
|
|
13
|
+
children?: React.ReactNode;
|
|
14
|
+
inputValue?: string;
|
|
15
|
+
onInputChange?: (value: string) => void;
|
|
16
|
+
inputPlaceholder?: string;
|
|
17
|
+
confirmLabel?: string;
|
|
18
|
+
cancelLabel?: string;
|
|
19
|
+
confirmDisabled?: boolean;
|
|
20
|
+
confirmDanger?: boolean;
|
|
21
|
+
showCancel?: boolean;
|
|
22
|
+
showConfirm?: boolean;
|
|
23
|
+
className?: string;
|
|
24
|
+
overlayClassName?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Replaces the whole action row. A wizard has Back/Cancel/Regenerate/Create,
|
|
27
|
+
* which the fixed Cancel+Confirm pair cannot express — and that is what
|
|
28
|
+
* pushes an app to hand-roll its own dialog instead of adopting this one.
|
|
29
|
+
*
|
|
30
|
+
* `null` means *no* row at all (a progress step, a viewer); omitting it means
|
|
31
|
+
* "use the default row". `??` collapses those two, so the check below is
|
|
32
|
+
* against `undefined` explicitly.
|
|
33
|
+
*/
|
|
34
|
+
actions?: React.ReactNode;
|
|
35
|
+
/** Rendered left of the title, e.g. a wizard's Back button. */
|
|
36
|
+
lead?: React.ReactNode;
|
|
37
|
+
}
|
|
38
|
+
export declare function Modal({ isOpen, title, onClose, onConfirm, children, inputValue, onInputChange, inputPlaceholder, confirmLabel, cancelLabel, confirmDisabled, confirmDanger, showCancel, showConfirm, className, overlayClassName, actions, lead }: ModalProps): React.JSX.Element | null;
|
|
39
|
+
//# sourceMappingURL=Modal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../../src/components/Modal.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAmC,MAAM,OAAO,CAAA;AAEvD,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,OAAO,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,IAAI,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,+DAA+D;IAC/D,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;CACvB;AASD,wBAAgB,KAAK,CAAC,EACpB,MAAM,EACN,KAAK,EACL,OAAO,EACP,SAAS,EACT,QAAQ,EACR,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,YAAwB,EACxB,WAAsB,EACtB,eAAuB,EACvB,aAAqB,EACrB,UAAiB,EACjB,WAAkB,EAClB,SAAc,EACd,gBAAqB,EACrB,OAAO,EACP,IAAI,EACL,EAAE,UAAU,4BAwGZ"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId, useRef, useEffect } from "react";
|
|
3
|
+
const escapeStack = [];
|
|
4
|
+
function Modal({
|
|
5
|
+
isOpen,
|
|
6
|
+
title,
|
|
7
|
+
onClose,
|
|
8
|
+
onConfirm,
|
|
9
|
+
children,
|
|
10
|
+
inputValue,
|
|
11
|
+
onInputChange,
|
|
12
|
+
inputPlaceholder,
|
|
13
|
+
confirmLabel = "Confirm",
|
|
14
|
+
cancelLabel = "Cancel",
|
|
15
|
+
confirmDisabled = false,
|
|
16
|
+
confirmDanger = false,
|
|
17
|
+
showCancel = true,
|
|
18
|
+
showConfirm = true,
|
|
19
|
+
className = "",
|
|
20
|
+
overlayClassName = "",
|
|
21
|
+
actions,
|
|
22
|
+
lead
|
|
23
|
+
}) {
|
|
24
|
+
const titleId = useId();
|
|
25
|
+
const cardRef = useRef(null);
|
|
26
|
+
const onCloseRef = useRef(onClose);
|
|
27
|
+
onCloseRef.current = onClose;
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!isOpen) return;
|
|
30
|
+
const id = Symbol("modal");
|
|
31
|
+
escapeStack.push(id);
|
|
32
|
+
const onKey = (e) => {
|
|
33
|
+
if (e.key !== "Escape") return;
|
|
34
|
+
if (escapeStack[escapeStack.length - 1] !== id) return;
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
onCloseRef.current();
|
|
37
|
+
};
|
|
38
|
+
document.addEventListener("keydown", onKey);
|
|
39
|
+
return () => {
|
|
40
|
+
document.removeEventListener("keydown", onKey);
|
|
41
|
+
const at = escapeStack.lastIndexOf(id);
|
|
42
|
+
if (at >= 0) escapeStack.splice(at, 1);
|
|
43
|
+
};
|
|
44
|
+
}, [isOpen]);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!isOpen) return;
|
|
47
|
+
const card = cardRef.current;
|
|
48
|
+
if (!card) return;
|
|
49
|
+
const focusable = card.querySelector(
|
|
50
|
+
'input:not([type=hidden]), textarea, select, button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])'
|
|
51
|
+
);
|
|
52
|
+
(focusable ?? card).focus();
|
|
53
|
+
}, [isOpen]);
|
|
54
|
+
if (!isOpen) return null;
|
|
55
|
+
return /* @__PURE__ */ jsx("div", { className: `modal-overlay ${overlayClassName}`.trim(), onClick: onClose, children: /* @__PURE__ */ jsxs(
|
|
56
|
+
"div",
|
|
57
|
+
{
|
|
58
|
+
ref: cardRef,
|
|
59
|
+
className: `modal-card ${className}`.trim(),
|
|
60
|
+
onClick: (e) => e.stopPropagation(),
|
|
61
|
+
role: "dialog",
|
|
62
|
+
"aria-modal": "true",
|
|
63
|
+
"aria-labelledby": titleId,
|
|
64
|
+
tabIndex: -1,
|
|
65
|
+
children: [
|
|
66
|
+
/* @__PURE__ */ jsxs("div", { className: "modal-head", children: [
|
|
67
|
+
lead,
|
|
68
|
+
/* @__PURE__ */ jsx("h3", { id: titleId, children: title })
|
|
69
|
+
] }),
|
|
70
|
+
children,
|
|
71
|
+
onInputChange && /* @__PURE__ */ jsx(
|
|
72
|
+
"input",
|
|
73
|
+
{
|
|
74
|
+
type: "text",
|
|
75
|
+
className: "modal-input",
|
|
76
|
+
value: inputValue || "",
|
|
77
|
+
onChange: (e) => onInputChange(e.target.value),
|
|
78
|
+
placeholder: inputPlaceholder,
|
|
79
|
+
onKeyDown: (e) => {
|
|
80
|
+
if (e.key === "Enter" && !confirmDisabled && onConfirm) {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
void onConfirm();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
),
|
|
87
|
+
actions !== void 0 ? actions : /* @__PURE__ */ jsxs("div", { className: "modal-actions", children: [
|
|
88
|
+
showCancel && /* @__PURE__ */ jsx("button", { className: "modal-button", onClick: onClose, children: cancelLabel }),
|
|
89
|
+
showConfirm && onConfirm && /* @__PURE__ */ jsx(
|
|
90
|
+
"button",
|
|
91
|
+
{
|
|
92
|
+
className: `modal-button ${confirmDanger ? "modal-button--danger" : "modal-button--primary"}`,
|
|
93
|
+
onClick: () => void onConfirm(),
|
|
94
|
+
disabled: confirmDisabled,
|
|
95
|
+
children: confirmLabel
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
] })
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
) });
|
|
102
|
+
}
|
|
103
|
+
export {
|
|
104
|
+
Modal
|
|
105
|
+
};
|
|
106
|
+
//# sourceMappingURL=Modal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Modal.js","sources":["../../src/components/Modal.tsx"],"sourcesContent":["/**\r\n * Modal Component\r\n * Generic modal dialog with keyboard shortcuts and customizable actions\r\n *\r\n * Styles ship with the package: import '@wolffm/task-ui-components/modal.css'\r\n */\r\n\r\nimport React, { useEffect, useId, useRef } from 'react'\r\n\r\nexport interface ModalProps {\r\n isOpen: boolean\r\n title: string\r\n onClose: () => void\r\n onConfirm?: () => void | Promise<void>\r\n children?: React.ReactNode\r\n inputValue?: string\r\n onInputChange?: (value: string) => void\r\n inputPlaceholder?: string\r\n confirmLabel?: string\r\n cancelLabel?: string\r\n confirmDisabled?: boolean\r\n confirmDanger?: boolean\r\n showCancel?: boolean\r\n showConfirm?: boolean\r\n className?: string\r\n overlayClassName?: string\r\n /**\r\n * Replaces the whole action row. A wizard has Back/Cancel/Regenerate/Create,\r\n * which the fixed Cancel+Confirm pair cannot express — and that is what\r\n * pushes an app to hand-roll its own dialog instead of adopting this one.\r\n *\r\n * `null` means *no* row at all (a progress step, a viewer); omitting it means\r\n * \"use the default row\". `??` collapses those two, so the check below is\r\n * against `undefined` explicitly.\r\n */\r\n actions?: React.ReactNode\r\n /** Rendered left of the title, e.g. a wizard's Back button. */\r\n lead?: React.ReactNode\r\n}\r\n\r\n/**\r\n * Open dialogs, outermost first. Each mounted-and-open Modal pushes a token\r\n * and pops it on close, so the handler can tell whether it is the one the\r\n * keypress is meant for.\r\n */\r\nconst escapeStack: symbol[] = []\r\n\r\nexport function Modal({\r\n isOpen,\r\n title,\r\n onClose,\r\n onConfirm,\r\n children,\r\n inputValue,\r\n onInputChange,\r\n inputPlaceholder,\r\n confirmLabel = 'Confirm',\r\n cancelLabel = 'Cancel',\r\n confirmDisabled = false,\r\n confirmDanger = false,\r\n showCancel = true,\r\n showConfirm = true,\r\n className = '',\r\n overlayClassName = '',\r\n actions,\r\n lead\r\n}: ModalProps) {\r\n const titleId = useId()\r\n const cardRef = useRef<HTMLDivElement>(null)\r\n\r\n // Escape closes from anywhere in the dialog, not just from a focused input.\r\n // Bound to the document rather than the card so it works before anything\r\n // inside has been focused — previously the handler hung off the optional\r\n // text input, so a dialog without one could not be dismissed by keyboard.\r\n //\r\n // Only the TOP-MOST open dialog answers, which is what makes a confirm\r\n // nested inside another modal usable: the parent's document listener is\r\n // still live underneath it, so without the stack one Escape closes both.\r\n // onClose is read through a ref so a caller passing an unmemoised handler\r\n // cannot re-run this effect and shuffle itself back to the top of the stack.\r\n const onCloseRef = useRef(onClose)\r\n onCloseRef.current = onClose\r\n useEffect(() => {\r\n if (!isOpen) return\r\n const id = Symbol('modal')\r\n escapeStack.push(id)\r\n const onKey = (e: KeyboardEvent) => {\r\n if (e.key !== 'Escape') return\r\n if (escapeStack[escapeStack.length - 1] !== id) return\r\n e.preventDefault()\r\n onCloseRef.current()\r\n }\r\n document.addEventListener('keydown', onKey)\r\n return () => {\r\n document.removeEventListener('keydown', onKey)\r\n const at = escapeStack.lastIndexOf(id)\r\n if (at >= 0) escapeStack.splice(at, 1)\r\n }\r\n }, [isOpen])\r\n\r\n // Move focus into the dialog on open, so the keyboard starts inside it.\r\n useEffect(() => {\r\n if (!isOpen) return\r\n const card = cardRef.current\r\n if (!card) return\r\n const focusable = card.querySelector<HTMLElement>(\r\n 'input:not([type=hidden]), textarea, select, button:not([disabled]), [href], [tabindex]:not([tabindex=\"-1\"])'\r\n )\r\n ;(focusable ?? card).focus()\r\n }, [isOpen])\r\n\r\n if (!isOpen) return null\r\n\r\n return (\r\n <div className={`modal-overlay ${overlayClassName}`.trim()} onClick={onClose}>\r\n <div\r\n ref={cardRef}\r\n className={`modal-card ${className}`.trim()}\r\n onClick={e => e.stopPropagation()}\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n aria-labelledby={titleId}\r\n tabIndex={-1}\r\n >\r\n <div className=\"modal-head\">\r\n {lead}\r\n <h3 id={titleId}>{title}</h3>\r\n </div>\r\n\r\n {children}\r\n\r\n {onInputChange && (\r\n <input\r\n type=\"text\"\r\n className=\"modal-input\"\r\n value={inputValue || ''}\r\n onChange={e => onInputChange(e.target.value)}\r\n placeholder={inputPlaceholder}\r\n onKeyDown={e => {\r\n if (e.key === 'Enter' && !confirmDisabled && onConfirm) {\r\n e.preventDefault()\r\n void onConfirm()\r\n }\r\n }}\r\n />\r\n )}\r\n\r\n {actions !== undefined ? (\r\n actions\r\n ) : (\r\n <div className=\"modal-actions\">\r\n {showCancel && (\r\n <button className=\"modal-button\" onClick={onClose}>\r\n {cancelLabel}\r\n </button>\r\n )}\r\n {showConfirm && onConfirm && (\r\n <button\r\n className={`modal-button ${confirmDanger ? 'modal-button--danger' : 'modal-button--primary'}`}\r\n onClick={() => void onConfirm()}\r\n disabled={confirmDisabled}\r\n >\r\n {confirmLabel}\r\n </button>\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n </div>\r\n )\r\n}\r\n"],"names":[],"mappings":";;AA6CA,MAAM,cAAwB,CAAA;AAEvB,SAAS,MAAM;AAAA,EACpB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,eAAe;AAAA,EACf,cAAc;AAAA,EACd,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,aAAa;AAAA,EACb,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,mBAAmB;AAAA,EACnB;AAAA,EACA;AACF,GAAe;AACb,QAAM,UAAU,MAAA;AAChB,QAAM,UAAU,OAAuB,IAAI;AAY3C,QAAM,aAAa,OAAO,OAAO;AACjC,aAAW,UAAU;AACrB,YAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AACb,UAAM,KAAK,OAAO,OAAO;AACzB,gBAAY,KAAK,EAAE;AACnB,UAAM,QAAQ,CAAC,MAAqB;AAClC,UAAI,EAAE,QAAQ,SAAU;AACxB,UAAI,YAAY,YAAY,SAAS,CAAC,MAAM,GAAI;AAChD,QAAE,eAAA;AACF,iBAAW,QAAA;AAAA,IACb;AACA,aAAS,iBAAiB,WAAW,KAAK;AAC1C,WAAO,MAAM;AACX,eAAS,oBAAoB,WAAW,KAAK;AAC7C,YAAM,KAAK,YAAY,YAAY,EAAE;AACrC,UAAI,MAAM,EAAG,aAAY,OAAO,IAAI,CAAC;AAAA,IACvC;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAGX,YAAU,MAAM;AACd,QAAI,CAAC,OAAQ;AACb,UAAM,OAAO,QAAQ;AACrB,QAAI,CAAC,KAAM;AACX,UAAM,YAAY,KAAK;AAAA,MACrB;AAAA,IAAA;AAED,KAAC,aAAa,MAAM,MAAA;AAAA,EACvB,GAAG,CAAC,MAAM,CAAC;AAEX,MAAI,CAAC,OAAQ,QAAO;AAEpB,SACE,oBAAC,SAAI,WAAW,iBAAiB,gBAAgB,GAAG,KAAA,GAAQ,SAAS,SACnE,UAAA;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,KAAK;AAAA,MACL,WAAW,cAAc,SAAS,GAAG,KAAA;AAAA,MACrC,SAAS,CAAA,MAAK,EAAE,gBAAA;AAAA,MAChB,MAAK;AAAA,MACL,cAAW;AAAA,MACX,mBAAiB;AAAA,MACjB,UAAU;AAAA,MAEV,UAAA;AAAA,QAAA,qBAAC,OAAA,EAAI,WAAU,cACZ,UAAA;AAAA,UAAA;AAAA,UACD,oBAAC,MAAA,EAAG,IAAI,SAAU,UAAA,MAAA,CAAM;AAAA,QAAA,GAC1B;AAAA,QAEC;AAAA,QAEA,iBACC;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,WAAU;AAAA,YACV,OAAO,cAAc;AAAA,YACrB,UAAU,CAAA,MAAK,cAAc,EAAE,OAAO,KAAK;AAAA,YAC3C,aAAa;AAAA,YACb,WAAW,CAAA,MAAK;AACd,kBAAI,EAAE,QAAQ,WAAW,CAAC,mBAAmB,WAAW;AACtD,kBAAE,eAAA;AACF,qBAAK,UAAA;AAAA,cACP;AAAA,YACF;AAAA,UAAA;AAAA,QAAA;AAAA,QAIH,YAAY,SACX,UAEA,qBAAC,OAAA,EAAI,WAAU,iBACZ,UAAA;AAAA,UAAA,kCACE,UAAA,EAAO,WAAU,gBAAe,SAAS,SACvC,UAAA,aACH;AAAA,UAED,eAAe,aACd;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WAAW,gBAAgB,gBAAgB,yBAAyB,uBAAuB;AAAA,cAC3F,SAAS,MAAM,KAAK,UAAA;AAAA,cACpB,UAAU;AAAA,cAET,UAAA;AAAA,YAAA;AAAA,UAAA;AAAA,QACH,EAAA,CAEJ;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA,GAGN;AAEJ;"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NumberInput Component
|
|
3
|
+
* A themed number field with its own stepper, for the OS spinner arrows that
|
|
4
|
+
* no CSS can style (controls.css removes them from a bare <input type=number>).
|
|
5
|
+
*
|
|
6
|
+
* It is a real <input type="number"> with every input prop passed through, so
|
|
7
|
+
* it works controlled (`value` + `onChange`) or uncontrolled (`defaultValue`),
|
|
8
|
+
* submits in a form, and keeps native min/max/step validation and the arrow
|
|
9
|
+
* keys. The buttons call the input's own stepUp()/stepDown() and then fire
|
|
10
|
+
* `input` and `change`, so `onChange` sees an ordinary edit: React's value
|
|
11
|
+
* tracker compares against what React last wrote, and stepUp() goes around it.
|
|
12
|
+
*
|
|
13
|
+
* Holding a button repeats, like the native spinner. The buttons are not tab
|
|
14
|
+
* stops — the arrow keys on the field already step — but they are labelled.
|
|
15
|
+
*/
|
|
16
|
+
import React from 'react';
|
|
17
|
+
import '../number-input.css';
|
|
18
|
+
export type NumberInputSize = 'sm' | 'md' | 'lg';
|
|
19
|
+
export interface NumberInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type' | 'size'> {
|
|
20
|
+
size?: NumberInputSize;
|
|
21
|
+
/** Labels for the two buttons; default "Increase" / "Decrease". */
|
|
22
|
+
increaseLabel?: string;
|
|
23
|
+
decreaseLabel?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare const NumberInput: React.ForwardRefExoticComponent<NumberInputProps & React.RefAttributes<HTMLInputElement>>;
|
|
26
|
+
//# sourceMappingURL=NumberInput.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NumberInput.d.ts","sourceRoot":"","sources":["../../src/components/NumberInput.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAA;AAEvE,OAAO,qBAAqB,CAAA;AAE5B,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAA;AAEhD,MAAM,WAAW,gBACf,SAAQ,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC1E,IAAI,CAAC,EAAE,eAAe,CAAA;IACtB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAMD,eAAO,MAAM,WAAW,2FA8HvB,CAAA"}
|