@drawnagency/primitives 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/auth/index.js +26 -3
  2. package/dist/chunk-2VTPWODA.js +60 -0
  3. package/dist/chunk-CS7F6IOY.js +39 -0
  4. package/dist/chunk-HOJAF4VD.js +264 -0
  5. package/dist/chunk-IP6ODLXX.js +341 -0
  6. package/dist/chunk-T4BJ6RSB.js +58 -0
  7. package/dist/chunk-UKEVUCIZ.js +200 -0
  8. package/dist/chunk-UMSFICAC.js +36 -0
  9. package/dist/index.js +156 -4
  10. package/dist/lib/index.js +62 -12
  11. package/dist/lib/sanitize.d.ts.map +1 -1
  12. package/dist/media/index.js +36 -9
  13. package/dist/schemas/index.js +52 -7
  14. package/package.json +5 -4
  15. package/src/lib/sanitize.ts +6 -2
  16. package/dist/auth/cookies.js +0 -44
  17. package/dist/auth/errors.js +0 -10
  18. package/dist/auth/security.js +0 -48
  19. package/dist/auth/types.js +0 -1
  20. package/dist/components/brandguide/ColorSwatchSettings.js +0 -10
  21. package/dist/components/brandguide/Colors.js +0 -79
  22. package/dist/components/brandguide/DoDontList.js +0 -22
  23. package/dist/components/brandguide/DoDontMediaGrid.js +0 -5
  24. package/dist/components/editor/AudiencePicker.js +0 -24
  25. package/dist/components/editor/DeleteButton.js +0 -6
  26. package/dist/components/editor/DragHandle.js +0 -8
  27. package/dist/components/editor/InsertButton.js +0 -7
  28. package/dist/components/editor/SectionWrapper.js +0 -135
  29. package/dist/components/editor/SettingsButton.js +0 -6
  30. package/dist/components/editor/SettingsForm.js +0 -64
  31. package/dist/components/editor/StatusBadge.js +0 -10
  32. package/dist/components/editor/StatusPicker.js +0 -30
  33. package/dist/components/editor/index.js +0 -7
  34. package/dist/components/primitives/CustomParagraph.js +0 -24
  35. package/dist/components/primitives/EditableGrid.js +0 -90
  36. package/dist/components/primitives/EditableList.js +0 -54
  37. package/dist/components/primitives/EditablePlainText.js +0 -52
  38. package/dist/components/primitives/EditableRichText.js +0 -86
  39. package/dist/components/primitives/HeadingSection.js +0 -7
  40. package/dist/components/primitives/IconPicker.js +0 -21
  41. package/dist/components/primitives/LinkPopover.js +0 -48
  42. package/dist/components/primitives/MediaSettingsForms.js +0 -42
  43. package/dist/components/primitives/ResolvedMedia.js +0 -9
  44. package/dist/components/primitives/RichTextToolbar.js +0 -26
  45. package/dist/components/primitives/tiptap-presets.js +0 -44
  46. package/dist/components/primitives/useEditableCollection.js +0 -61
  47. package/dist/components/primitives/useEditablePlainText.js +0 -27
  48. package/dist/components/primitives/useEditableRichText.js +0 -52
  49. package/dist/components/sections/Button/CTAButton.js +0 -18
  50. package/dist/components/sections/Button/index.js +0 -28
  51. package/dist/components/sections/Colors/index.js +0 -34
  52. package/dist/components/sections/DoDontList/index.js +0 -33
  53. package/dist/components/sections/DoDontMediaGrid/index.js +0 -41
  54. package/dist/components/sections/IconList/IconList.js +0 -131
  55. package/dist/components/sections/IconList/IconListSettings.js +0 -22
  56. package/dist/components/sections/IconList/index.js +0 -27
  57. package/dist/components/sections/LinkHeading/index.js +0 -15
  58. package/dist/components/sections/MediaGrid/MediaGrid.js +0 -62
  59. package/dist/components/sections/MediaGrid/index.js +0 -35
  60. package/dist/components/sections/Prose/Prose.js +0 -11
  61. package/dist/components/sections/Prose/index.js +0 -15
  62. package/dist/components/sections/SectionLayout.js +0 -15
  63. package/dist/components/sections/SplitContent/SplitContent.js +0 -31
  64. package/dist/components/sections/SplitContent/SplitContentSettings.js +0 -17
  65. package/dist/components/sections/SplitContent/index.js +0 -27
  66. package/dist/components/sections/SubHeading/index.js +0 -18
  67. package/dist/components/sections/SubSubHeading/index.js +0 -18
  68. package/dist/components/sections/ViewRenderer.js +0 -13
  69. package/dist/components/sections/register-schemas.js +0 -15
  70. package/dist/components/sections/register.js +0 -15
  71. package/dist/components/shared/Button.js +0 -27
  72. package/dist/components/shared/Checkbox.js +0 -10
  73. package/dist/components/shared/ColorPicker.js +0 -5
  74. package/dist/components/shared/ErrorBoundary.js +0 -30
  75. package/dist/components/shared/FontPicker.js +0 -190
  76. package/dist/components/shared/FormLabel.js +0 -5
  77. package/dist/components/shared/IconButton.js +0 -16
  78. package/dist/components/shared/Input.js +0 -8
  79. package/dist/components/shared/Navigation.js +0 -71
  80. package/dist/components/shared/PasswordInput.js +0 -11
  81. package/dist/components/shared/Popover.js +0 -33
  82. package/dist/components/shared/PopoverItem.js +0 -6
  83. package/dist/components/shared/Select.js +0 -9
  84. package/dist/components/shared/Textarea.js +0 -8
  85. package/dist/components/shared/Toggle.js +0 -5
  86. package/dist/components/shared/Tooltip.js +0 -8
  87. package/dist/components/shared/icons.js +0 -23
  88. package/dist/components/shell/AudienceAddForm.js +0 -43
  89. package/dist/components/shell/AudienceRow.js +0 -74
  90. package/dist/components/shell/EditorContext.js +0 -24
  91. package/dist/components/shell/EditorLoginForm.js +0 -46
  92. package/dist/components/shell/EditorModal.js +0 -43
  93. package/dist/components/shell/EditorModalContext.js +0 -20
  94. package/dist/components/shell/EditorShell.js +0 -483
  95. package/dist/components/shell/MediaLibraryContext.js +0 -5
  96. package/dist/components/shell/MediaLibraryModal.js +0 -145
  97. package/dist/components/shell/ProcessingIndicator.js +0 -15
  98. package/dist/components/shell/SectionSkeleton.js +0 -22
  99. package/dist/components/shell/SectionTypePicker.js +0 -15
  100. package/dist/components/shell/SiteSettingsDisplay.js +0 -28
  101. package/dist/components/shell/SiteSettingsModal.js +0 -40
  102. package/dist/components/shell/SiteSettingsUsers.js +0 -87
  103. package/dist/components/shell/SiteSettingsViewerAccess.js +0 -94
  104. package/dist/components/shell/ViewerLoginForm.js +0 -40
  105. package/dist/data/google-fonts.json +0 -7718
  106. package/dist/hooks/index.js +0 -6
  107. package/dist/hooks/useActiveHeadings.js +0 -99
  108. package/dist/hooks/useEditorPersistence.js +0 -73
  109. package/dist/hooks/useEditorPublish.js +0 -145
  110. package/dist/hooks/useFocusTrap.js +0 -51
  111. package/dist/hooks/useMediaPipeline.js +0 -253
  112. package/dist/hooks/useResolvedMedia.js +0 -39
  113. package/dist/lib/cn.js +0 -5
  114. package/dist/lib/contrast.js +0 -11
  115. package/dist/lib/dexie.js +0 -236
  116. package/dist/lib/events.js +0 -15
  117. package/dist/lib/google-fonts.js +0 -11
  118. package/dist/lib/grid.js +0 -7
  119. package/dist/lib/icons.js +0 -27
  120. package/dist/lib/loader.js +0 -57
  121. package/dist/lib/nav.js +0 -58
  122. package/dist/lib/registry.js +0 -64
  123. package/dist/lib/safeRedirect.js +0 -11
  124. package/dist/lib/sanitize.js +0 -6
  125. package/dist/lib/timestamp.js +0 -28
  126. package/dist/media/github.js +0 -60
  127. package/dist/media/queue.js +0 -116
  128. package/dist/media/resolve.js +0 -50
  129. package/dist/media/types.js +0 -1
  130. package/dist/media/utils.js +0 -41
  131. package/dist/media/videoPoster.js +0 -44
  132. package/dist/media/worker.js +0 -73
  133. package/dist/schemas/audience.js +0 -19
  134. package/dist/schemas/auth.js +0 -22
  135. package/dist/schemas/media-grid-options.js +0 -7
  136. package/dist/schemas/media.js +0 -28
  137. package/dist/schemas/sections.js +0 -12
  138. package/dist/schemas/shared.js +0 -71
  139. package/dist/schemas/site-config.js +0 -26
@@ -1,15 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useRef, useState } from "react";
3
- import { Loader2 } from "lucide-react";
4
- import { cn } from "../../lib/cn";
5
- export function ProcessingIndicator({ items }) {
6
- const [open, setOpen] = useState(false);
7
- const buttonRef = useRef(null);
8
- const activeItems = items.filter((i) => i.state === "active");
9
- const queuedItems = items.filter((i) => i.state === "queued");
10
- const errorItems = items.filter((i) => i.state === "error");
11
- const visibleCount = activeItems.length + queuedItems.length + errorItems.length;
12
- if (visibleCount === 0)
13
- return null;
14
- return (_jsxs("div", { className: "relative inline-flex", children: [_jsxs("button", { ref: buttonRef, type: "button", onClick: () => setOpen((o) => !o), className: cn("relative inline-flex items-center gap-1.5 rounded-md border border-base-200 bg-base px-3 py-1.5 text-sm font-medium text-base-contrast transition-colors hover:bg-base-accent"), "aria-label": "Processing status", children: [_jsx(Loader2, { size: 14, className: "animate-spin text-primary" }), _jsx("span", { className: "text-xs font-semibold tabular-nums", children: visibleCount })] }), open && (_jsx("div", { className: "absolute bottom-full left-0 z-50 mb-2 w-64 rounded-lg border border-base-200 bg-base shadow-lg", children: _jsxs("div", { className: "flex flex-col divide-y divide-base-200", children: [activeItems.map((item) => (_jsxs("div", { className: "flex flex-col gap-1.5 px-3 py-2", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "truncate text-xs font-medium text-base-contrast", children: item.originalName }), _jsxs("span", { className: "ml-2 shrink-0 text-xs text-base-contrast-light", children: [item.percent, "%"] })] }), _jsx("div", { className: "h-1 overflow-hidden rounded-full bg-base-accent", children: _jsx("div", { className: "h-full rounded-full bg-primary transition-all", style: { width: `${item.percent}%` } }) })] }, item.id))), errorItems.map((item) => (_jsxs("div", { className: "flex flex-col gap-0.5 px-3 py-2", children: [_jsx("span", { className: "truncate text-xs font-medium text-red-600", children: item.originalName }), item.error && (_jsx("span", { className: "text-xs text-red-500", children: item.error }))] }, item.id))), queuedItems.length > 0 && (_jsx("div", { className: "px-3 py-2", children: _jsxs("span", { className: "text-xs text-base-contrast-light", children: ["+", queuedItems.length, " queued"] }) }))] }) }))] }));
15
- }
@@ -1,22 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect } from "react";
3
- import { SectionTypePicker } from "./SectionTypePicker";
4
- import { SectionLayout } from "../sections/SectionLayout";
5
- import { cn } from "../../lib/cn";
6
- export function SectionSkeleton({ types, onSelect, onDismiss }) {
7
- const [isVisible, setIsVisible] = useState(false);
8
- useEffect(() => {
9
- // Trigger animation on next frame
10
- requestAnimationFrame(() => setIsVisible(true));
11
- }, []);
12
- useEffect(() => {
13
- function handleKeyDown(e) {
14
- if (e.key === "Escape") {
15
- onDismiss();
16
- }
17
- }
18
- document.addEventListener("keydown", handleKeyDown);
19
- return () => document.removeEventListener("keydown", handleKeyDown);
20
- }, [onDismiss]);
21
- return (_jsx(SectionLayout, { type: "skeleton", status: "draft", children: _jsxs("div", { className: cn("relative transition-all duration-200", isVisible ? "opacity-100" : "opacity-0 -translate-y-2"), children: [_jsx("div", { className: "flex h-20 items-center justify-center rounded-lg bg-base-accent", children: _jsx("span", { className: "text-sm text-base-contrast-light/50", children: "New section" }) }), _jsx("div", { className: "relative flex justify-center", children: _jsx(SectionTypePicker, { types: types, onSelect: onSelect, onClose: onDismiss }) })] }) }));
22
- }
@@ -1,15 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef } from "react";
3
- export function SectionTypePicker({ types, onSelect, onClose }) {
4
- const panelRef = useRef(null);
5
- useEffect(() => {
6
- function handleMouseDown(e) {
7
- if (panelRef.current && !panelRef.current.contains(e.target)) {
8
- onClose();
9
- }
10
- }
11
- document.addEventListener("mousedown", handleMouseDown);
12
- return () => document.removeEventListener("mousedown", handleMouseDown);
13
- }, [onClose]);
14
- return (_jsx("div", { ref: panelRef, className: "absolute z-50 mt-1 w-64 rounded-lg border border-base-200 bg-base p-2 shadow-lg", children: types.map(({ type, label, icon: Icon }) => (_jsxs("button", { className: "cursor-pointer flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm text-base-contrast-light hover:bg-base-accent hover:text-base-contrast", onClick: () => onSelect(type), children: [_jsx("span", { className: "flex h-6 w-6 items-center justify-center text-base", children: _jsx(Icon, {}) }), _jsx("span", { children: label })] }, type))) }));
15
- }
@@ -1,28 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ColorPicker } from "../shared/ColorPicker";
3
- import { FontPicker } from "../shared/FontPicker";
4
- import { Input } from "../shared/Input";
5
- import { Select } from "../shared/Select";
6
- import { FormLabel } from "../shared/FormLabel";
7
- import { deriveContrast } from "../../lib/contrast";
8
- import { buildGoogleFontsUrl } from "../../lib/google-fonts";
9
- const darkModeOptions = [
10
- { label: "Light", value: "light" },
11
- { label: "Dark", value: "dark" },
12
- { label: "Optional (viewer toggle)", value: "optional" },
13
- ];
14
- export function SiteSettingsDisplay({ siteConfig, onChange }) {
15
- function update(patch) {
16
- onChange({ ...siteConfig, ...patch });
17
- }
18
- function handleColorChange(color) {
19
- const contrast = deriveContrast(color);
20
- onChange({ ...siteConfig, primaryColor: color, primaryContrast: contrast });
21
- }
22
- function handleFontChange(field, family) {
23
- const next = { ...siteConfig, [field]: family };
24
- next.googleFontsUrl = buildGoogleFontsUrl(next.headingFont, next.bodyFont);
25
- onChange(next);
26
- }
27
- return (_jsxs("div", { className: "space-y-5", children: [_jsx(Input, { label: "Site name", value: siteConfig.siteName, onChange: (value) => update({ siteName: value }), placeholder: "Brand Portal" }), _jsxs("div", { children: [_jsx(FormLabel, { children: "Primary color" }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(ColorPicker, { value: siteConfig.primaryColor, onChange: handleColorChange, label: "Primary color" }), _jsx("span", { className: "text-sm text-base-contrast-light", children: siteConfig.primaryColor })] })] }), _jsx(Select, { label: "Dark mode", value: siteConfig.darkMode, onChange: (value) => update({ darkMode: value }), options: darkModeOptions }), _jsx(FontPicker, { label: "Heading font", value: siteConfig.headingFont, onChange: (family) => handleFontChange("headingFont", family) }), _jsx(FontPicker, { label: "Body font", value: siteConfig.bodyFont, onChange: (family) => handleFontChange("bodyFont", family) })] }));
28
- }
@@ -1,40 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from "react";
3
- import { EditorModal } from "./EditorModal";
4
- import { Button } from "../shared/Button";
5
- import { SiteSettingsViewerAccess } from "./SiteSettingsViewerAccess";
6
- import { SiteSettingsDisplay } from "./SiteSettingsDisplay";
7
- import { SiteSettingsUsers } from "./SiteSettingsUsers";
8
- import { cn } from "../../lib/cn";
9
- export function SiteSettingsModal({ isOpen, onClose, siteConfig, onSiteConfigChange, onAudiencesChange, capabilities, currentUser }) {
10
- const tabs = [
11
- { id: "users", label: "Users", show: capabilities.userManagement },
12
- { id: "viewer-access", label: "Viewer Access", show: true },
13
- { id: "display", label: "Display", show: true },
14
- ];
15
- const visibleTabs = tabs.filter((t) => t.show);
16
- const [activeTab, setActiveTab] = useState(visibleTabs[0]?.id ?? "display");
17
- const [signOutError, setSignOutError] = useState(null);
18
- async function handleSignOut() {
19
- setSignOutError(null);
20
- try {
21
- const res = await fetch("/api/auth/sign-out", { method: "POST" });
22
- if (!res.ok) {
23
- setSignOutError("Sign out failed");
24
- return;
25
- }
26
- window.location.assign("/edit/login");
27
- }
28
- catch {
29
- setSignOutError("Network error during sign out");
30
- }
31
- }
32
- const identityLabel = currentUser?.email
33
- ? currentUser.email
34
- : currentUser
35
- ? (currentUser.role === "owner" ? "Owner" : "Editor")
36
- : null;
37
- return (_jsx(EditorModal, { isOpen: isOpen, onClose: onClose, title: "Site Settings", size: "settings", noPadding: true, children: _jsxs("div", { className: "flex flex-1 flex-col overflow-hidden", children: [_jsx("div", { className: "border-b border-base-200", children: _jsx("div", { className: "flex px-6", role: "tablist", children: visibleTabs.map((tab) => (_jsx("button", { role: "tab", "aria-selected": activeTab === tab.id, onClick: () => setActiveTab(tab.id), className: cn("cursor-pointer px-4 py-2 text-sm font-medium border-b-2 -mb-px", activeTab === tab.id
38
- ? "border-brand text-brand"
39
- : "border-transparent text-base-contrast-light hover:text-base-contrast"), children: tab.label }, tab.id))) }) }), _jsxs("div", { "data-testid": "site-settings-tab-panel", className: "flex flex-1 flex-col overflow-y-auto px-6 py-4", children: [activeTab === "users" && _jsx(SiteSettingsUsers, { currentUser: currentUser }), activeTab === "viewer-access" && (_jsx(SiteSettingsViewerAccess, { audienceManagement: capabilities.audienceManagement, passwordToggle: capabilities.passwordToggle, onAudiencesChange: onAudiencesChange })), activeTab === "display" && (_jsx(SiteSettingsDisplay, { siteConfig: siteConfig, onChange: onSiteConfigChange }))] }), currentUser && (_jsxs("div", { className: "flex items-center justify-between gap-3 border-t border-base-200 px-6 py-3", children: [_jsxs("div", { className: "text-xs text-base-contrast-light", children: ["Signed in as", " ", _jsx("span", { className: "font-medium text-base-contrast", children: identityLabel }), currentUser.email && (_jsxs("span", { className: "ml-1 capitalize", children: ["(", currentUser.role, ")"] })), signOutError && (_jsxs("span", { className: "ml-2 text-red-600", children: ["\u2014 ", signOutError] }))] }), _jsx(Button, { type: "button", variant: "destructive", onClick: handleSignOut, children: "Sign out" })] }))] }) }));
40
- }
@@ -1,87 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Fragment, useState, useEffect } from "react";
3
- import { ChevronDown } from "lucide-react";
4
- import { Button } from "../shared/Button";
5
- export function SiteSettingsUsers({ currentUser }) {
6
- const [users, setUsers] = useState([]);
7
- const [loading, setLoading] = useState(true);
8
- const [inviteEmail, setInviteEmail] = useState("");
9
- const [inviteRole, setInviteRole] = useState("editor");
10
- const [error, setError] = useState(null);
11
- const [confirmingRevoke, setConfirmingRevoke] = useState(null);
12
- async function loadUsers() {
13
- try {
14
- const res = await fetch("/api/auth/users");
15
- if (!res.ok) {
16
- setError("Failed to load users");
17
- setUsers([]);
18
- return;
19
- }
20
- const data = await res.json();
21
- setUsers(data.users);
22
- }
23
- catch {
24
- setError("Network error while loading users");
25
- setUsers([]);
26
- }
27
- finally {
28
- setLoading(false);
29
- }
30
- }
31
- useEffect(() => {
32
- loadUsers();
33
- }, []);
34
- async function handleInvite(e) {
35
- e.preventDefault();
36
- setError(null);
37
- try {
38
- const res = await fetch("/api/auth/users", {
39
- method: "POST",
40
- headers: { "Content-Type": "application/json" },
41
- body: JSON.stringify({ email: inviteEmail, role: inviteRole }),
42
- });
43
- if (!res.ok) {
44
- const data = await res.json().catch(() => ({}));
45
- setError(data.error || "Invite failed");
46
- return;
47
- }
48
- setInviteEmail("");
49
- await loadUsers();
50
- }
51
- catch {
52
- setError("Network error while inviting");
53
- }
54
- }
55
- function handleRevoke(userId) {
56
- setError(null);
57
- const ownerCount = users.filter((u) => u.role === "owner").length;
58
- const user = users.find((u) => u.id === userId);
59
- if (user?.role === "owner" && ownerCount <= 1) {
60
- setError("Cannot remove the last owner");
61
- return;
62
- }
63
- setConfirmingRevoke(userId);
64
- }
65
- async function confirmRevoke(userId) {
66
- setConfirmingRevoke(null);
67
- try {
68
- const res = await fetch("/api/auth/users", {
69
- method: "DELETE",
70
- headers: { "Content-Type": "application/json" },
71
- body: JSON.stringify({ userId }),
72
- });
73
- if (!res.ok) {
74
- const data = await res.json().catch(() => ({}));
75
- setError(data.error || "Revoke failed");
76
- return;
77
- }
78
- await loadUsers();
79
- }
80
- catch {
81
- setError("Network error while removing user");
82
- }
83
- }
84
- if (loading)
85
- return _jsx("p", { className: "text-sm text-base-contrast-light", children: "Loading users..." });
86
- return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsxs("div", { className: "grid grid-cols-[1fr_7rem_5rem] text-sm", children: [_jsx("div", { className: "flex items-center border-b border-base-200 pr-2 pb-2 text-base-contrast-light", children: "Email" }), _jsx("div", { className: "flex items-center border-b border-base-200 pr-2 pb-2 text-base-contrast-light", children: "Role" }), _jsx("div", { className: "border-b border-base-200 pb-2" }), users.map((user) => (_jsxs(Fragment, { children: [_jsx("div", { className: "flex items-center border-b border-base-200 py-2 pr-2 text-base-contrast", children: user.email }), _jsx("div", { className: "flex items-center border-b border-base-200 py-2 pr-2 capitalize text-base-contrast-light", children: user.role }), _jsx("div", { className: "flex items-center justify-end border-b border-base-200 py-2", children: currentUser?.email === user.email ? (_jsx("span", { className: "text-xs text-base-contrast-light", children: "\u2014" })) : confirmingRevoke === user.id ? (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx(Button, { type: "button", variant: "ghost", tone: "destructive", onClick: () => confirmRevoke(user.id), children: "Confirm" }), _jsx(Button, { type: "button", variant: "ghost", onClick: () => setConfirmingRevoke(null), children: "Cancel" })] })) : (_jsx(Button, { type: "button", variant: "ghost", tone: "destructive", onClick: () => handleRevoke(user.id), children: "Remove" })) })] }, user.id)))] }), _jsxs("form", { onSubmit: handleInvite, className: "grid grid-cols-[1fr_7rem_5rem] text-sm", children: [_jsx("div", { className: "pr-2", children: _jsx("input", { type: "email", placeholder: "Email address", value: inviteEmail, onChange: (e) => setInviteEmail(e.target.value), required: true, "aria-label": "Email address", className: "w-full rounded border border-base-200 bg-base px-3 py-2 text-sm text-base-contrast focus:border-base-contrast focus:outline-none focus:ring-1 focus:ring-base-contrast" }) }), _jsx("div", { className: "pr-2", children: _jsxs("div", { className: "relative", children: [_jsxs("select", { value: inviteRole, onChange: (e) => setInviteRole(e.target.value), "aria-label": "Role", className: "w-full appearance-none rounded border border-base-200 bg-base py-2 pr-8 pl-3 text-sm text-base-contrast focus:border-base-contrast focus:outline-none focus:ring-1 focus:ring-base-contrast", children: [_jsx("option", { value: "editor", children: "Editor" }), _jsx("option", { value: "owner", children: "Owner" })] }), _jsx(ChevronDown, { size: 14, className: "pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-base-contrast-light" })] }) }), _jsx(Button, { type: "submit", variant: "primary", size: "md", className: "w-full", children: "Invite" })] }), error && _jsx("p", { className: "text-sm text-red-600", children: error })] }));
87
- }
@@ -1,94 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect, useCallback } from "react";
3
- import { Button } from "../shared/Button";
4
- import { Toggle } from "../shared/Toggle";
5
- import { AudienceRow } from "./AudienceRow";
6
- import { AudienceAddForm } from "./AudienceAddForm";
7
- import { cn } from "../../lib/cn";
8
- export function SiteSettingsViewerAccess({ audienceManagement, passwordToggle, onAudiencesChange }) {
9
- const [audiences, setAudiences] = useState([]);
10
- const [passwordEnabled, setPasswordEnabled] = useState(true);
11
- const [loading, setLoading] = useState(true);
12
- const [showAddForm, setShowAddForm] = useState(false);
13
- const [toggleError, setToggleError] = useState(null);
14
- const loadAudiences = useCallback(async () => {
15
- try {
16
- const res = await fetch("/api/auth/audiences");
17
- const data = await res.json();
18
- const list = data.audiences ?? [];
19
- setAudiences(list);
20
- onAudiencesChange(list);
21
- setPasswordEnabled(data.passwordEnabled ?? true);
22
- }
23
- finally {
24
- setLoading(false);
25
- }
26
- }, [onAudiencesChange]);
27
- useEffect(() => {
28
- loadAudiences();
29
- }, [loadAudiences]);
30
- async function handlePasswordToggle(enabled) {
31
- setToggleError(null);
32
- const previous = passwordEnabled;
33
- setPasswordEnabled(enabled);
34
- try {
35
- const res = await fetch("/api/auth/password-enabled", {
36
- method: "POST",
37
- headers: { "Content-Type": "application/json" },
38
- body: JSON.stringify({ enabled }),
39
- });
40
- if (!res.ok) {
41
- const body = await res.json().catch(() => ({}));
42
- setToggleError(body.error ?? "Update failed");
43
- setPasswordEnabled(previous);
44
- }
45
- }
46
- catch {
47
- setToggleError("Network error");
48
- setPasswordEnabled(previous);
49
- }
50
- }
51
- async function handleAdd(data) {
52
- const res = await fetch("/api/auth/audiences", {
53
- method: "POST",
54
- headers: { "Content-Type": "application/json" },
55
- body: JSON.stringify(data),
56
- });
57
- if (!res.ok) {
58
- const err = await res.json().catch(() => ({}));
59
- throw new Error(err.error ?? "Add failed");
60
- }
61
- setShowAddForm(false);
62
- await loadAudiences();
63
- }
64
- async function handleSave(name, patch) {
65
- const res = await fetch("/api/auth/audiences", {
66
- method: "PATCH",
67
- headers: { "Content-Type": "application/json" },
68
- body: JSON.stringify({ name, ...patch }),
69
- });
70
- if (!res.ok) {
71
- const err = await res.json().catch(() => ({}));
72
- throw new Error(err.error ?? "Save failed");
73
- }
74
- await loadAudiences();
75
- }
76
- async function handleDelete(name) {
77
- const res = await fetch("/api/auth/audiences", {
78
- method: "DELETE",
79
- headers: { "Content-Type": "application/json" },
80
- body: JSON.stringify({ name }),
81
- });
82
- if (!res.ok) {
83
- const err = await res.json().catch(() => ({}));
84
- throw new Error(err.error ?? "Delete failed");
85
- }
86
- await loadAudiences();
87
- }
88
- if (loading) {
89
- return _jsx("p", { className: "text-sm text-base-contrast-light", children: "Loading audiences..." });
90
- }
91
- return (_jsxs("div", { className: "space-y-4", children: [!audienceManagement && (_jsx("p", { className: "text-xs text-base-contrast-light", children: "This site is running in password-only mode. A developer can configure the environment variables for Admin, Editor, and Viewer details." })), passwordToggle && (_jsxs("div", { className: "space-y-2 rounded border border-base-200 p-3", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Toggle, { checked: passwordEnabled, onChange: handlePasswordToggle, label: "Require password to view site" }), _jsx("span", { className: "text-sm font-medium", children: "Require password to view site" })] }), _jsx("p", { className: "text-xs text-base-contrast-light", children: passwordEnabled
92
- ? "Viewers must select an audience and enter a password to see any content."
93
- : "Site is fully public. All published sections are visible to anyone." }), toggleError && _jsx("p", { className: "text-xs text-red-600", children: toggleError })] })), _jsxs("div", { className: cn("space-y-2", !passwordEnabled && "opacity-60"), children: [!passwordEnabled && (_jsx("p", { className: "mb-2 text-xs text-base-contrast-light", children: "Audiences are saved but not enforced while the password is disabled." })), audiences.map((a) => (_jsx(AudienceRow, { audience: a, isDefault: a.isDefault, canMutate: audienceManagement, onSave: (patch) => handleSave(a.name, patch), onDelete: () => handleDelete(a.name) }, a.name))), audiences.length === 0 && (_jsx("p", { className: "text-sm text-base-contrast-light", children: "No audiences configured." }))] }), audienceManagement && (showAddForm ? (_jsx(AudienceAddForm, { onSubmit: handleAdd, onCancel: () => setShowAddForm(false) })) : (_jsx(Button, { variant: "secondary", onClick: () => setShowAddForm(true), children: "Add audience" })))] }));
94
- }
@@ -1,40 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from "react";
3
- import { PasswordInput } from "../shared/PasswordInput";
4
- import { Button } from "../shared/Button";
5
- import { safeRedirect } from "../../lib/safeRedirect";
6
- export function ViewerLoginForm({ audiences, next }) {
7
- const [audience, setAudience] = useState(audiences.length === 1 ? audiences[0].name : "");
8
- const [password, setPassword] = useState("");
9
- const [error, setError] = useState(null);
10
- const [loading, setLoading] = useState(false);
11
- async function handleSubmit(e) {
12
- e.preventDefault();
13
- setLoading(true);
14
- setError(null);
15
- const body = { audience, password };
16
- if (next)
17
- body.next = next;
18
- try {
19
- const response = await fetch("/api/auth/verify-audience", {
20
- method: "POST",
21
- headers: { "Content-Type": "application/json" },
22
- body: JSON.stringify(body),
23
- });
24
- if (!response.ok) {
25
- const data = await response.json().catch(() => ({}));
26
- setError(data.error || "Access denied");
27
- return;
28
- }
29
- const data = await response.json().catch(() => ({}));
30
- window.location.assign(safeRedirect(data.redirectTo, "/"));
31
- }
32
- catch {
33
- setError("Network error");
34
- }
35
- finally {
36
- setLoading(false);
37
- }
38
- }
39
- return (_jsx("div", { className: "mx-auto max-w-sm", children: _jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { htmlFor: "audience", className: "mb-1.5 block text-sm font-medium text-base-contrast", children: "Audience" }), _jsxs("select", { id: "audience", value: audience, onChange: (e) => setAudience(e.target.value), required: true, className: "w-full rounded border border-base-200 px-3 py-2 text-sm", children: [audiences.length > 1 && _jsx("option", { value: "", children: "Select audience..." }), audiences.map((a) => (_jsx("option", { value: a.name, children: a.displayName }, a.name)))] })] }), _jsx(PasswordInput, { label: "Password", value: password, onChange: setPassword, autoComplete: "current-password" }), error && (_jsx("p", { className: "text-sm text-red-600", children: error })), _jsx(Button, { type: "submit", variant: "brand", size: "md", disabled: loading || !audience, isLoading: loading, loadingLabel: "Verifying...", className: "w-full", children: "Enter" })] }) }));
40
- }