@guard-rail/core 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -1
- package/dist/cli/doctor.js +138 -3
- package/dist/cli/doctor.js.map +1 -1
- package/dist/core/csp.d.ts +32 -0
- package/dist/core/csp.js +26 -0
- package/dist/core/csp.js.map +1 -0
- package/dist/index.client.d.ts +1 -0
- package/dist/index.client.js +3 -0
- package/dist/index.client.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/react/Admin.d.ts +5 -3
- package/dist/react/Admin.js +29 -41
- package/dist/react/Admin.js.map +1 -1
- package/dist/react/AuthDrawer.js +22 -34
- package/dist/react/AuthDrawer.js.map +1 -1
- package/dist/react/GRail/client.js +27 -47
- package/dist/react/GRail/client.js.map +1 -1
- package/dist/react/MediaPicker.js +12 -16
- package/dist/react/MediaPicker.js.map +1 -1
- package/dist/react/Repeater/client.js +12 -13
- package/dist/react/Repeater/client.js.map +1 -1
- package/dist/react/Team.d.ts +1 -7
- package/dist/react/Team.js +21 -46
- package/dist/react/Team.js.map +1 -1
- package/dist/react/guardrail.css +563 -0
- package/package.json +11 -10
- package/dist/react/styles.d.ts +0 -504
- package/dist/react/styles.js +0 -500
- package/dist/react/styles.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/react/GRail/client.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useRouter } from \"next/navigation\";\nimport { saveOverride } from \"../../server/actions\";\nimport { GRailRender } from \"./render\";\nimport { useGRail } from \"../Provider\";\nimport { useAnchorRect } from \"../useAnchorRect\";\nimport { CheckIcon, CloseIcon, OpenIcon, PencilIcon, SpinnerIcon } from \"../Icons\";\nimport { openHref } from \"../openHref\";\nimport { S } from \"../styles\";\nimport { MediaPicker } from \"../MediaPicker\";\nimport type { HTMLKeys, OverrideRow, OverrideValue } from \"../../core/types\";\n\nexport function GRailClient({\n as,\n initialOverride,\n ...htmlProps\n}: {\n as?: HTMLKeys;\n initialOverride: OverrideRow;\n [key: string]: unknown;\n}) {\n const { isEditMode } = useGRail();\n const [editing, setEditing] = useState(false);\n const [saving, setSaving] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [row, setRow] = useState<OverrideRow>(initialOverride);\n\n const elementRef = useRef<HTMLElement | null>(null);\n const isImage = row.value.type === \"image\";\n // Image fields keep a rect for their whole time in edit mode (their marker\n // needs it); everything else only while actively editing.\n const rect = useAnchorRect(elementRef, editing || (isEditMode && isImage), {\n // Only the idle marker case (isImage, not actively editing) is gated —\n // an image mid-edit tracks unconditionally, same as text/link chrome.\n gateByVisibility: isImage && !editing,\n });\n\n /**\n * The text the element was rendered with when this edit session began.\n *\n * While editing, the DOM owns the text and React must not touch it. Feeding\n * state back into a contentEditable's children makes React replace the text\n * node on every keystroke, which resets the caret to offset 0 — typing then\n * appears reversed and backspace jumps to the start. Holding this value\n * constant means the children prop never changes, so React leaves the node\n * alone and the caret stays put.\n */\n const sessionTextRef = useRef<string>(initialOverride.value.text ?? \"\");\n /** Live DOM text, updated without re-rendering. Read on save. */\n const draftTextRef = useRef<string>(initialOverride.value.text ?? \"\");\n\n const isInline = row.value.type === \"text\" || row.value.type === \"button\";\n\n const beginEdit = () => {\n sessionTextRef.current = row.value.text ?? \"\";\n draftTextRef.current = row.value.text ?? \"\";\n setError(null);\n setEditing(true);\n };\n\n const updateAttribute = (key: string, value: string) =>\n setRow((prev) => ({ ...prev, value: { ...prev.value, [key]: value } }));\n\n const patchValue = (patch: Partial<OverrideValue>) =>\n setRow((prev) => ({ ...prev, value: { ...prev.value, ...patch } }));\n\n const handleSave = async () => {\n // Inline text lives in the DOM, not in state, so read it back here.\n const next: OverrideRow = isInline\n ? { ...row, value: { ...row.value, text: draftTextRef.current } }\n : row;\n\n setSaving(true);\n setError(null);\n const res = await saveOverride(next);\n setSaving(false);\n\n if (!res.success) {\n setError(res.error ?? \"Save failed.\");\n return;\n }\n setRow(next);\n setEditing(false);\n };\n\n const handleCancel = () => {\n setRow(initialOverride);\n draftTextRef.current = initialOverride.value.text ?? \"\";\n if (isInline && elementRef.current) {\n elementRef.current.textContent = initialOverride.value.text ?? \"\";\n }\n setError(null);\n setEditing(false);\n };\n\n // Clicking away from an active edit cancels it, the same as Escape. Anything\n // inside the field itself (repositioning the caret) or inside the anchored\n // toolbar/panel (both carry data-guardrail) is not \"away\" — checked via\n // closest() rather than React's onClick so a stopPropagation() inside the\n // chrome (image/link panel inputs) reliably suppresses this too, since that\n // call stops the native event before it ever reaches this document listener.\n useEffect(() => {\n if (!editing) return;\n const handlePointerDown = (e: MouseEvent) => {\n const target = e.target as Node;\n if (elementRef.current?.contains(target)) return;\n if (target instanceof Element && target.closest(\"[data-guardrail]\")) return;\n handleCancel();\n };\n document.addEventListener(\"mousedown\", handlePointerDown);\n return () => document.removeEventListener(\"mousedown\", handlePointerDown);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [editing]);\n\n if (!isEditMode) {\n return <GRailRender as={as} {...htmlProps} override={row.value} />;\n }\n\n // Frozen during the session so React never rewrites the live text node.\n const renderedValue =\n editing && isInline\n ? { ...row.value, text: sessionTextRef.current }\n : row.value;\n\n return (\n <>\n <GRailRender\n as={as}\n {...htmlProps}\n ref={elementRef}\n override={renderedValue}\n style={{\n ...(htmlProps.style as React.CSSProperties),\n // Images are left alone — no outline, position, or z-index — and\n // get a separate ImageMarker instead. Restyling the <img> itself\n // can't work for a background image with content layered over it.\n ...(isImage\n ? { cursor: \"pointer\" }\n : {\n outline: editing ? \"2px solid #2563eb\" : \"1px dashed #60a5fa\",\n outlineOffset: 2,\n cursor: editing ? \"text\" : \"pointer\",\n }),\n }}\n onClick={(e: React.MouseEvent) => {\n if (editing) return;\n e.preventDefault();\n // preventDefault only blocks the browser's native action (following\n // an href); it does not stop the click from bubbling. A field\n // rendered inside a Link would still trigger the Link's own onClick\n // and navigate anyway — stopPropagation is what actually claims the\n // click for editing instead. (Use `editable={false}` on fields\n // meant to stay pure navigation, e.g. inside a card that links to\n // that content's own page.)\n e.stopPropagation();\n beginEdit();\n }}\n // Spread only for inline text: images and links would receive invalid\n // DOM props (contentEditable on next/image, etc).\n {...(isInline\n ? {\n contentEditable: editing,\n suppressContentEditableWarning: true,\n onInput: (e: React.FormEvent<HTMLElement>) => {\n // Ref only — setState here re-renders and moves the caret.\n draftTextRef.current = e.currentTarget.textContent ?? \"\";\n },\n onKeyDown: (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n void handleSave();\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n handleCancel();\n }\n },\n }\n : {})}\n />\n\n {/*\n Portaled to <body>, not rendered next to the field. The chrome is\n position:fixed with a high z-index, but z-index only ranks within the\n nearest stacking context — rendered inline, a field inside any ancestor\n that forms one (a z-indexed or transformed hero, say) traps its own\n popup there, where unrelated content stacked higher elsewhere on the\n page can paint straight over it. Also sidesteps a transformed ancestor\n silently re-anchoring position:fixed to itself instead of the viewport.\n */}\n {rect &&\n (isImage || editing) &&\n createPortal(\n <>\n {isImage && (\n <ImageMarker rect={rect} editing={editing} onEdit={beginEdit} />\n )}\n {editing && (\n <Chrome\n rect={rect}\n isInline={isInline}\n row={row}\n error={error}\n saving={saving}\n onChange={updateAttribute}\n onPatch={patchValue}\n onSave={handleSave}\n onCancel={handleCancel}\n />\n )}\n </>,\n document.body,\n )}\n </>\n );\n}\n\n/**\n * Dashed outline plus an \"Edit\" handle for an image field, positioned from the\n * image's measured rect rather than attached to the image. Everything here is\n * portaled to <body> by the caller, so it ranks above page content whatever the\n * image's own ancestors do, and never shifts the developer's layout.\n */\nfunction ImageMarker({\n rect,\n editing,\n onEdit,\n}: {\n rect: { top: number; left: number; bottom: number; width: number };\n editing: boolean;\n onEdit: () => void;\n}) {\n const height = rect.bottom - rect.top;\n // Collapsed (display:none, zero-size) or scrolled fully out of view.\n if (rect.width <= 0 || height <= 0) return null;\n if (typeof window !== \"undefined\" && (rect.bottom < 0 || rect.top > window.innerHeight)) {\n return null;\n }\n\n const HANDLE_H = 26;\n // A logo-sized image would be mostly covered by its own handle, so small\n // ones get it just outside (above, or below when there's no room above).\n const small = height < HANDLE_H * 2 || rect.width < 120;\n let handleTop: number;\n if (small) {\n handleTop = rect.top - HANDLE_H - 4 >= 4 ? rect.top - HANDLE_H - 4 : rect.bottom + 4;\n } else {\n // Stays on screen for a tall background that's partly scrolled past, but\n // never leaves the image's own bounds.\n handleTop = Math.min(\n Math.max(rect.top + 6, 6),\n Math.max(rect.top + 6, rect.bottom - HANDLE_H - 6),\n );\n }\n const handleLeft = small ? rect.left : rect.left + 6;\n\n return (\n <>\n <div\n data-guardrail\n style={{\n ...S.markerOutline,\n top: rect.top - 2,\n left: rect.left - 2,\n width: rect.width + 4,\n height: height + 4,\n border: editing ? \"2px solid #2563eb\" : \"1px dashed #60a5fa\",\n }}\n />\n {!editing && (\n <button\n type=\"button\"\n data-guardrail\n style={{ ...S.markerHandle, top: handleTop, left: handleLeft }}\n aria-label=\"Edit image\"\n title=\"Edit image\"\n onClick={(e) => {\n // React bubbles synthetic events through portals to the field's\n // *React* ancestors — a field inside a <Link> would otherwise\n // navigate on this click, same reason the field's own onClick\n // claims its click.\n e.preventDefault();\n e.stopPropagation();\n onEdit();\n }}\n >\n <PencilIcon /> Edit\n </button>\n )}\n </>\n );\n}\n\nfunction Chrome({\n rect,\n isInline,\n row,\n error,\n saving,\n onChange,\n onPatch,\n onSave,\n onCancel,\n}: {\n rect: { top: number; left: number; bottom: number; width: number };\n isInline: boolean;\n row: OverrideRow;\n error: string | null;\n saving: boolean;\n onChange: (key: string, value: string) => void;\n onPatch: (patch: Partial<OverrideValue>) => void;\n onSave: () => void;\n onCancel: () => void;\n}) {\n const { storage } = useGRail();\n const router = useRouter();\n const [pickerOpen, setPickerOpen] = useState(false);\n const GAP = 8;\n // Flip above the element when there is no room below it.\n const below = rect.bottom + GAP;\n const flip = typeof window !== \"undefined\" && below > window.innerHeight - 120;\n\n // The panel is anchored at the field's left edge, so a field near the right\n // of the viewport (a nav link, say) would push it — and its Save button —\n // off-screen. Pull it back so its full width stays visible. 260 mirrors\n // anchoredPanel's minWidth; the inline bar is small enough not to need it.\n const PANEL_MIN_WIDTH = 260;\n const maxLeft =\n typeof window !== \"undefined\"\n ? window.innerWidth - (isInline ? 0 : PANEL_MIN_WIDTH) - 8\n : Infinity;\n\n const position: React.CSSProperties = {\n left: Math.max(8, Math.min(rect.left, maxLeft)),\n ...(flip ? { top: Math.max(8, rect.top - GAP - 44) } : { top: below }),\n };\n\n const actions = (\n <>\n <button\n style={S.iconBtnPrimary}\n onClick={onSave}\n disabled={saving}\n title=\"Save (Enter)\"\n aria-label=\"Save\"\n >\n {saving ? <SpinnerIcon /> : <CheckIcon />}\n </button>\n <button\n style={S.iconBtn}\n onClick={onCancel}\n disabled={saving}\n title=\"Cancel (Esc)\"\n aria-label=\"Cancel\"\n >\n <CloseIcon />\n </button>\n </>\n );\n\n // Text and buttons edit in place, so they only need the two controls.\n if (isInline) {\n return (\n <div data-guardrail style={{ ...S.anchoredBar, ...position }}>\n {actions}\n {error && <span style={{ ...S.error, margin: \"0 6px\" }}>{error}</span>}\n </div>\n );\n }\n\n // Images and links carry attributes with no on-page representation.\n return (\n <div\n data-guardrail\n style={{ ...S.anchoredPanel, ...position }}\n onClick={(e) => e.stopPropagation()}\n >\n {row.value.type === \"image\" && (\n <>\n {row.value.src && (\n // Plain img: previews must not depend on remotePatterns.\n <img src={row.value.src} alt=\"\" style={S.previewThumb} />\n )}\n\n {storage && (\n <button\n type=\"button\"\n style={S.secondaryBtn}\n onClick={() => setPickerOpen(true)}\n >\n {row.value.src ? \"Replace image\" : \"Choose image\"}\n </button>\n )}\n\n <label style={S.label}>Alt text</label>\n <input\n style={S.field}\n value={row.value.alt || \"\"}\n placeholder=\"Describe the image for screen readers\"\n onChange={(e) => onChange(\"alt\", e.target.value)}\n />\n\n {/* Only without storage: there is no picker then, so a URL is the one\n way to set an image. A typed URL has unknown proportions, so any\n stored dimensions are cleared. */}\n {!storage && (\n <>\n <label style={S.label}>Image URL</label>\n <input\n style={S.field}\n value={row.value.src || \"\"}\n onChange={(e) =>\n onPatch({ src: e.target.value, width: undefined, height: undefined })\n }\n />\n </>\n )}\n\n {pickerOpen && (\n <MediaPicker\n current={row.value.src}\n onClose={() => setPickerOpen(false)}\n onSelect={(image) =>\n onPatch({ src: image.src, width: image.width, height: image.height })\n }\n />\n )}\n </>\n )}\n\n {row.value.type === \"link\" && (\n <>\n <label style={S.label}>Link text</label>\n <input\n style={S.field}\n value={row.value.text || \"\"}\n onChange={(e) => onChange(\"text\", e.target.value)}\n />\n <label style={S.label}>URL</label>\n <input\n style={S.field}\n value={row.value.href || \"\"}\n onChange={(e) => onChange(\"href\", e.target.value)}\n />\n </>\n )}\n\n {error && <div style={{ ...S.error, marginBottom: 8 }}>{error}</div>}\n <div\n style={{\n display: \"flex\",\n gap: 4,\n alignItems: \"center\",\n justifyContent: row.value.type === \"link\" ? \"space-between\" : \"flex-end\",\n }}\n >\n {/* The field's own click opens this editor, so an editable link can't\n be followed by clicking it — this is how an editor gets to the page. */}\n {row.value.type === \"link\" && (\n <button\n type=\"button\"\n style={{\n ...S.secondaryBtn,\n width: \"auto\",\n marginBottom: 0,\n display: \"inline-flex\",\n alignItems: \"center\",\n gap: 6,\n }}\n disabled={!row.value.href?.trim()}\n onClick={() => openHref(row.value.href ?? \"\", (path) => router.push(path))}\n title=\"Open this link\"\n aria-label=\"Open link\"\n >\n <OpenIcon /> Open\n </button>\n )}\n <div style={{ display: \"flex\", gap: 4 }}>{actions}</div>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/react/GRail/client.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useEffect, useRef, useState } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useRouter } from \"next/navigation\";\nimport { saveOverride } from \"../../server/actions\";\nimport { GRailRender } from \"./render\";\nimport { useGRail } from \"../Provider\";\nimport { useAnchorRect } from \"../useAnchorRect\";\nimport { CheckIcon, CloseIcon, OpenIcon, PencilIcon, SpinnerIcon } from \"../Icons\";\nimport { openHref } from \"../openHref\";\nimport { MediaPicker } from \"../MediaPicker\";\nimport type { HTMLKeys, OverrideRow, OverrideValue } from \"../../core/types\";\n\nexport function GRailClient({\n as,\n initialOverride,\n ...htmlProps\n}: {\n as?: HTMLKeys;\n initialOverride: OverrideRow;\n [key: string]: unknown;\n}) {\n const { isEditMode } = useGRail();\n const [editing, setEditing] = useState(false);\n const [saving, setSaving] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [row, setRow] = useState<OverrideRow>(initialOverride);\n\n const elementRef = useRef<HTMLElement | null>(null);\n const isImage = row.value.type === \"image\";\n // Image fields keep a rect for their whole time in edit mode (their marker\n // needs it); everything else only while actively editing.\n const rect = useAnchorRect(elementRef, editing || (isEditMode && isImage), {\n // Only the idle marker case (isImage, not actively editing) is gated —\n // an image mid-edit tracks unconditionally, same as text/link chrome.\n gateByVisibility: isImage && !editing,\n });\n\n /**\n * The text the element was rendered with when this edit session began.\n *\n * While editing, the DOM owns the text and React must not touch it. Feeding\n * state back into a contentEditable's children makes React replace the text\n * node on every keystroke, which resets the caret to offset 0 — typing then\n * appears reversed and backspace jumps to the start. Holding this value\n * constant means the children prop never changes, so React leaves the node\n * alone and the caret stays put.\n */\n const sessionTextRef = useRef<string>(initialOverride.value.text ?? \"\");\n /** Live DOM text, updated without re-rendering. Read on save. */\n const draftTextRef = useRef<string>(initialOverride.value.text ?? \"\");\n\n const isInline = row.value.type === \"text\" || row.value.type === \"button\";\n\n const beginEdit = () => {\n sessionTextRef.current = row.value.text ?? \"\";\n draftTextRef.current = row.value.text ?? \"\";\n setError(null);\n setEditing(true);\n };\n\n const updateAttribute = (key: string, value: string) =>\n setRow((prev) => ({ ...prev, value: { ...prev.value, [key]: value } }));\n\n const patchValue = (patch: Partial<OverrideValue>) =>\n setRow((prev) => ({ ...prev, value: { ...prev.value, ...patch } }));\n\n const handleSave = async () => {\n // Inline text lives in the DOM, not in state, so read it back here.\n const next: OverrideRow = isInline\n ? { ...row, value: { ...row.value, text: draftTextRef.current } }\n : row;\n\n setSaving(true);\n setError(null);\n const res = await saveOverride(next);\n setSaving(false);\n\n if (!res.success) {\n setError(res.error ?? \"Save failed.\");\n return;\n }\n setRow(next);\n setEditing(false);\n };\n\n const handleCancel = () => {\n setRow(initialOverride);\n draftTextRef.current = initialOverride.value.text ?? \"\";\n if (isInline && elementRef.current) {\n elementRef.current.textContent = initialOverride.value.text ?? \"\";\n }\n setError(null);\n setEditing(false);\n };\n\n // Clicking away from an active edit cancels it, the same as Escape. Anything\n // inside the field itself (repositioning the caret) or inside the anchored\n // toolbar/panel (both carry data-guardrail) is not \"away\" — checked via\n // closest() rather than React's onClick so a stopPropagation() inside the\n // chrome (image/link panel inputs) reliably suppresses this too, since that\n // call stops the native event before it ever reaches this document listener.\n useEffect(() => {\n if (!editing) return;\n const handlePointerDown = (e: MouseEvent) => {\n const target = e.target as Node;\n if (elementRef.current?.contains(target)) return;\n if (target instanceof Element && target.closest(\"[data-guardrail]\")) return;\n handleCancel();\n };\n document.addEventListener(\"mousedown\", handlePointerDown);\n return () => document.removeEventListener(\"mousedown\", handlePointerDown);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [editing]);\n\n if (!isEditMode) {\n return <GRailRender as={as} {...htmlProps} override={row.value} />;\n }\n\n // Frozen during the session so React never rewrites the live text node.\n const renderedValue =\n editing && isInline\n ? { ...row.value, text: sessionTextRef.current }\n : row.value;\n\n return (\n <>\n <GRailRender\n as={as}\n {...htmlProps}\n ref={elementRef}\n override={renderedValue}\n // The outline and cursor come from guardrail.css, keyed off these\n // attributes — not an inline style, which a strict CSP refuses in\n // server-rendered markup (and which would be repeated on every field).\n // Images are left alone — no outline, position, or z-index — and get a\n // separate ImageMarker instead. Restyling the <img> itself can't work\n // for a background image with content layered over it.\n data-gr-field={isImage ? \"image\" : \"text\"}\n {...(editing && !isImage ? { \"data-gr-editing\": \"\" } : {})}\n onClick={(e: React.MouseEvent) => {\n if (editing) return;\n e.preventDefault();\n // preventDefault only blocks the browser's native action (following\n // an href); it does not stop the click from bubbling. A field\n // rendered inside a Link would still trigger the Link's own onClick\n // and navigate anyway — stopPropagation is what actually claims the\n // click for editing instead. (Use `editable={false}` on fields\n // meant to stay pure navigation, e.g. inside a card that links to\n // that content's own page.)\n e.stopPropagation();\n beginEdit();\n }}\n // Spread only for inline text: images and links would receive invalid\n // DOM props (contentEditable on next/image, etc).\n {...(isInline\n ? {\n contentEditable: editing,\n suppressContentEditableWarning: true,\n onInput: (e: React.FormEvent<HTMLElement>) => {\n // Ref only — setState here re-renders and moves the caret.\n draftTextRef.current = e.currentTarget.textContent ?? \"\";\n },\n onKeyDown: (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault();\n void handleSave();\n } else if (e.key === \"Escape\") {\n e.preventDefault();\n handleCancel();\n }\n },\n }\n : {})}\n />\n\n {/*\n Portaled to <body>, not rendered next to the field. The chrome is\n position:fixed with a high z-index, but z-index only ranks within the\n nearest stacking context — rendered inline, a field inside any ancestor\n that forms one (a z-indexed or transformed hero, say) traps its own\n popup there, where unrelated content stacked higher elsewhere on the\n page can paint straight over it. Also sidesteps a transformed ancestor\n silently re-anchoring position:fixed to itself instead of the viewport.\n */}\n {rect &&\n (isImage || editing) &&\n createPortal(\n <>\n {isImage && (\n <ImageMarker rect={rect} editing={editing} onEdit={beginEdit} />\n )}\n {editing && (\n <Chrome\n rect={rect}\n isInline={isInline}\n row={row}\n error={error}\n saving={saving}\n onChange={updateAttribute}\n onPatch={patchValue}\n onSave={handleSave}\n onCancel={handleCancel}\n />\n )}\n </>,\n document.body,\n )}\n </>\n );\n}\n\n/**\n * Dashed outline plus an \"Edit\" handle for an image field, positioned from the\n * image's measured rect rather than attached to the image. Everything here is\n * portaled to <body> by the caller, so it ranks above page content whatever the\n * image's own ancestors do, and never shifts the developer's layout.\n */\nfunction ImageMarker({\n rect,\n editing,\n onEdit,\n}: {\n rect: { top: number; left: number; bottom: number; width: number };\n editing: boolean;\n onEdit: () => void;\n}) {\n const height = rect.bottom - rect.top;\n // Collapsed (display:none, zero-size) or scrolled fully out of view.\n if (rect.width <= 0 || height <= 0) return null;\n if (typeof window !== \"undefined\" && (rect.bottom < 0 || rect.top > window.innerHeight)) {\n return null;\n }\n\n const HANDLE_H = 26;\n // A logo-sized image would be mostly covered by its own handle, so small\n // ones get it just outside (above, or below when there's no room above).\n const small = height < HANDLE_H * 2 || rect.width < 120;\n let handleTop: number;\n if (small) {\n handleTop = rect.top - HANDLE_H - 4 >= 4 ? rect.top - HANDLE_H - 4 : rect.bottom + 4;\n } else {\n // Stays on screen for a tall background that's partly scrolled past, but\n // never leaves the image's own bounds.\n handleTop = Math.min(\n Math.max(rect.top + 6, 6),\n Math.max(rect.top + 6, rect.bottom - HANDLE_H - 6),\n );\n }\n const handleLeft = small ? rect.left : rect.left + 6;\n\n return (\n <>\n <div\n data-guardrail\n className=\"gr-marker-outline\"\n {...(editing ? { \"data-gr-editing\": \"\" } : {})}\n style={{\n top: rect.top - 2,\n left: rect.left - 2,\n width: rect.width + 4,\n height: height + 4,\n }}\n />\n {!editing && (\n <button\n type=\"button\"\n data-guardrail\n className=\"gr-marker-handle\"\n style={{ top: handleTop, left: handleLeft }}\n aria-label=\"Edit image\"\n title=\"Edit image\"\n onClick={(e) => {\n // React bubbles synthetic events through portals to the field's\n // *React* ancestors — a field inside a <Link> would otherwise\n // navigate on this click, same reason the field's own onClick\n // claims its click.\n e.preventDefault();\n e.stopPropagation();\n onEdit();\n }}\n >\n <PencilIcon /> Edit\n </button>\n )}\n </>\n );\n}\n\nfunction Chrome({\n rect,\n isInline,\n row,\n error,\n saving,\n onChange,\n onPatch,\n onSave,\n onCancel,\n}: {\n rect: { top: number; left: number; bottom: number; width: number };\n isInline: boolean;\n row: OverrideRow;\n error: string | null;\n saving: boolean;\n onChange: (key: string, value: string) => void;\n onPatch: (patch: Partial<OverrideValue>) => void;\n onSave: () => void;\n onCancel: () => void;\n}) {\n const { storage } = useGRail();\n const router = useRouter();\n const [pickerOpen, setPickerOpen] = useState(false);\n const GAP = 8;\n // Flip above the element when there is no room below it.\n const below = rect.bottom + GAP;\n const flip = typeof window !== \"undefined\" && below > window.innerHeight - 120;\n\n // The panel is anchored at the field's left edge, so a field near the right\n // of the viewport (a nav link, say) would push it — and its Save button —\n // off-screen. Pull it back so its full width stays visible. 260 mirrors\n // anchoredPanel's minWidth; the inline bar is small enough not to need it.\n const PANEL_MIN_WIDTH = 260;\n const maxLeft =\n typeof window !== \"undefined\"\n ? window.innerWidth - (isInline ? 0 : PANEL_MIN_WIDTH) - 8\n : Infinity;\n\n const position: React.CSSProperties = {\n left: Math.max(8, Math.min(rect.left, maxLeft)),\n ...(flip ? { top: Math.max(8, rect.top - GAP - 44) } : { top: below }),\n };\n\n const actions = (\n <>\n <button\n className=\"gr-icon-btn-primary\"\n onClick={onSave}\n disabled={saving}\n title=\"Save (Enter)\"\n aria-label=\"Save\"\n >\n {saving ? <SpinnerIcon /> : <CheckIcon />}\n </button>\n <button\n className=\"gr-icon-btn\"\n onClick={onCancel}\n disabled={saving}\n title=\"Cancel (Esc)\"\n aria-label=\"Cancel\"\n >\n <CloseIcon />\n </button>\n </>\n );\n\n // Text and buttons edit in place, so they only need the two controls.\n if (isInline) {\n return (\n <div data-guardrail className=\"gr-anchored-bar\" style={position}>\n {actions}\n {error && <span className=\"gr-error gr-mx-6\">{error}</span>}\n </div>\n );\n }\n\n // Images and links carry attributes with no on-page representation.\n return (\n <div\n data-guardrail\n className=\"gr-anchored-panel\"\n style={position}\n onClick={(e) => e.stopPropagation()}\n >\n {row.value.type === \"image\" && (\n <>\n {row.value.src && (\n // Plain img: previews must not depend on remotePatterns.\n <img src={row.value.src} alt=\"\" className=\"gr-preview-thumb\" />\n )}\n\n {storage && (\n <button\n type=\"button\"\n className=\"gr-secondary-btn\"\n onClick={() => setPickerOpen(true)}\n >\n {row.value.src ? \"Replace image\" : \"Choose image\"}\n </button>\n )}\n\n <label className=\"gr-label\">Alt text</label>\n <input\n className=\"gr-field\"\n value={row.value.alt || \"\"}\n placeholder=\"Describe the image for screen readers\"\n onChange={(e) => onChange(\"alt\", e.target.value)}\n />\n\n {/* Only without storage: there is no picker then, so a URL is the one\n way to set an image. A typed URL has unknown proportions, so any\n stored dimensions are cleared. */}\n {!storage && (\n <>\n <label className=\"gr-label\">Image URL</label>\n <input\n className=\"gr-field\"\n value={row.value.src || \"\"}\n onChange={(e) =>\n onPatch({ src: e.target.value, width: undefined, height: undefined })\n }\n />\n </>\n )}\n\n {pickerOpen && (\n <MediaPicker\n current={row.value.src}\n onClose={() => setPickerOpen(false)}\n onSelect={(image) =>\n onPatch({ src: image.src, width: image.width, height: image.height })\n }\n />\n )}\n </>\n )}\n\n {row.value.type === \"link\" && (\n <>\n <label className=\"gr-label\">Link text</label>\n <input\n className=\"gr-field\"\n value={row.value.text || \"\"}\n onChange={(e) => onChange(\"text\", e.target.value)}\n />\n <label className=\"gr-label\">URL</label>\n <input\n className=\"gr-field\"\n value={row.value.href || \"\"}\n onChange={(e) => onChange(\"href\", e.target.value)}\n />\n </>\n )}\n\n {error && <div className=\"gr-error gr-mb-8\">{error}</div>}\n <div\n className={\n row.value.type === \"link\"\n ? \"gr-flex gr-gap-4 gr-items-center gr-justify-between\"\n : \"gr-flex gr-gap-4 gr-items-center gr-justify-end\"\n }\n >\n {/* The field's own click opens this editor, so an editable link can't\n be followed by clicking it — this is how an editor gets to the page. */}\n {row.value.type === \"link\" && (\n <button\n type=\"button\"\n className=\"gr-secondary-btn gr-w-auto gr-mb-0 gr-inline-flex gr-items-center gr-gap-6\"\n disabled={!row.value.href?.trim()}\n onClick={() => openHref(row.value.href ?? \"\", (path) => router.push(path))}\n title=\"Open this link\"\n aria-label=\"Open link\"\n >\n <OpenIcon /> Open\n </button>\n )}\n <div className=\"gr-flex gr-gap-4\">{actions}</div>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -3,7 +3,6 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import { createAssetUpload, listAssets } from "../server/actions";
|
|
5
5
|
import { CloseIcon, SpinnerIcon } from "./Icons";
|
|
6
|
-
import { S, v } from "./styles";
|
|
7
6
|
import { prepareImage } from "./resizeImage";
|
|
8
7
|
function measure(url) {
|
|
9
8
|
return new Promise((resolve) => {
|
|
@@ -81,35 +80,35 @@ function MediaPicker({
|
|
|
81
80
|
if (file) void upload(file);
|
|
82
81
|
};
|
|
83
82
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
84
|
-
/* @__PURE__ */ jsx("div", { "data-guardrail": true,
|
|
83
|
+
/* @__PURE__ */ jsx("div", { "data-guardrail": true, className: "gr-picker-scrim", onClick: onClose }),
|
|
85
84
|
/* @__PURE__ */ jsxs(
|
|
86
85
|
"div",
|
|
87
86
|
{
|
|
88
87
|
"data-guardrail": true,
|
|
89
88
|
role: "dialog",
|
|
90
89
|
"aria-label": "Choose an image",
|
|
91
|
-
|
|
90
|
+
className: "gr-picker",
|
|
92
91
|
onClick: (e) => e.stopPropagation(),
|
|
93
92
|
children: [
|
|
94
|
-
/* @__PURE__ */ jsxs("header", {
|
|
95
|
-
/* @__PURE__ */ jsx("h2", {
|
|
93
|
+
/* @__PURE__ */ jsxs("header", { className: "gr-drawer-head", children: [
|
|
94
|
+
/* @__PURE__ */ jsx("h2", { className: "gr-drawer-title", children: "Choose an image" }),
|
|
96
95
|
/* @__PURE__ */ jsx(
|
|
97
96
|
"button",
|
|
98
97
|
{
|
|
99
|
-
|
|
98
|
+
className: "gr-icon-btn gr-text-muted",
|
|
100
99
|
onClick: onClose,
|
|
101
100
|
"aria-label": "Close",
|
|
102
101
|
children: /* @__PURE__ */ jsx(CloseIcon, {})
|
|
103
102
|
}
|
|
104
103
|
)
|
|
105
104
|
] }),
|
|
106
|
-
/* @__PURE__ */ jsxs("div", {
|
|
105
|
+
/* @__PURE__ */ jsxs("div", { className: "gr-drawer-body", children: [
|
|
107
106
|
/* @__PURE__ */ jsxs(
|
|
108
107
|
"div",
|
|
109
108
|
{
|
|
110
109
|
role: "button",
|
|
111
110
|
tabIndex: 0,
|
|
112
|
-
|
|
111
|
+
className: dragging ? "gr-dropzone gr-dropzone-active" : "gr-dropzone",
|
|
113
112
|
onClick: () => !uploading && inputRef.current?.click(),
|
|
114
113
|
onKeyDown: (e) => {
|
|
115
114
|
if (e.key === "Enter" || e.key === " ") inputRef.current?.click();
|
|
@@ -125,7 +124,7 @@ function MediaPicker({
|
|
|
125
124
|
onFiles(e.dataTransfer.files);
|
|
126
125
|
},
|
|
127
126
|
children: [
|
|
128
|
-
uploading ? /* @__PURE__ */ jsxs("span", {
|
|
127
|
+
uploading ? /* @__PURE__ */ jsxs("span", { className: "gr-inline-flex gr-gap-8 gr-items-center", children: [
|
|
129
128
|
/* @__PURE__ */ jsx(SpinnerIcon, {}),
|
|
130
129
|
" Uploading\u2026"
|
|
131
130
|
] }) : "Drop an image here, or click to upload",
|
|
@@ -145,18 +144,15 @@ function MediaPicker({
|
|
|
145
144
|
]
|
|
146
145
|
}
|
|
147
146
|
),
|
|
148
|
-
error && /* @__PURE__ */ jsx("div", {
|
|
149
|
-
assets === null ? /* @__PURE__ */ jsx("p", {
|
|
147
|
+
error && /* @__PURE__ */ jsx("div", { className: "gr-error gr-mb-12", children: error }),
|
|
148
|
+
assets === null ? /* @__PURE__ */ jsx("p", { className: "gr-fs-13 gr-text-muted", children: "Loading\u2026" }) : assets.length === 0 ? /* @__PURE__ */ jsx("p", { className: "gr-fs-13 gr-text-muted", children: "No images uploaded yet." }) : /* @__PURE__ */ jsx("div", { className: "gr-picker-grid", children: assets.map((asset) => /* @__PURE__ */ jsx(
|
|
150
149
|
"button",
|
|
151
150
|
{
|
|
152
151
|
type: "button",
|
|
153
152
|
title: asset.name,
|
|
154
|
-
|
|
155
|
-
...S.pickerTile,
|
|
156
|
-
...asset.url === current ? S.pickerTileActive : {}
|
|
157
|
-
},
|
|
153
|
+
className: asset.url === current ? "gr-picker-tile gr-picker-tile-active" : "gr-picker-tile",
|
|
158
154
|
onClick: async () => choose(asset.url, await measure(asset.url)),
|
|
159
|
-
children: /* @__PURE__ */ jsx("img", { src: asset.url, alt: "", loading: "lazy",
|
|
155
|
+
children: /* @__PURE__ */ jsx("img", { src: asset.url, alt: "", loading: "lazy", className: "gr-picker-thumb" })
|
|
160
156
|
},
|
|
161
157
|
asset.id
|
|
162
158
|
)) })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/react/MediaPicker.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { createAssetUpload, listAssets } from \"../server/actions\";\nimport { CloseIcon, SpinnerIcon } from \"./Icons\";\nimport { S, v } from \"./styles\";\nimport { prepareImage } from \"./resizeImage\";\nimport type { GRAsset } from \"../core/types\";\n\nexport interface PickedImage {\n src: string;\n width?: number;\n height?: number;\n}\n\ntype Size = { width?: number; height?: number };\n\n/**\n * Natural size is read in the browser, so no dimensions need storing on the\n * storage side and every adapter gets them for free.\n */\nfunction measure(url: string): Promise<Size> {\n return new Promise((resolve) => {\n const img = new Image();\n img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight });\n img.onerror = () => resolve({});\n img.src = url;\n });\n}\n\nexport function MediaPicker({\n current,\n onSelect,\n onClose,\n}: {\n current?: string;\n onSelect: (image: PickedImage) => void;\n onClose: () => void;\n}) {\n const [assets, setAssets] = useState<GRAsset[] | null>(null);\n const [uploading, setUploading] = useState(false);\n const [dragging, setDragging] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n void listAssets().then(setAssets);\n }, []);\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== \"Escape\") return;\n e.stopPropagation();\n onClose();\n };\n window.addEventListener(\"keydown\", onKey, true);\n return () => window.removeEventListener(\"keydown\", onKey, true);\n }, [onClose]);\n\n const choose = useCallback(\n (src: string, size: Size) => {\n onSelect({ src, ...size });\n onClose();\n },\n [onSelect, onClose],\n );\n\n const upload = async (file: File) => {\n if (uploading) return;\n setError(null);\n setUploading(true);\n try {\n // Scaled first, so the size check and the upload both see the file\n // that will actually be stored — an oversized phone photo that shrinks\n // under the cap is accepted.\n const prepared = await prepareImage(file);\n const payload = prepared.file;\n\n const res = await createAssetUpload({\n name: payload.name,\n contentType: payload.type,\n size: payload.size,\n });\n if (!res.success) {\n setError(res.error);\n return;\n }\n const { target } = res;\n\n // Straight to storage — the bytes never touch the Next.js server.\n const put = await fetch(target.url, {\n method: target.method,\n headers: target.headers,\n body: payload,\n });\n if (!put.ok) {\n setError(`Upload failed (${put.status}).`);\n return;\n }\n\n setAssets((prev) => [target.asset, ...(prev ?? [])]);\n choose(target.asset.url, { width: prepared.width, height: prepared.height });\n } catch {\n setError(\"Upload failed. Check your connection and try again.\");\n } finally {\n setUploading(false);\n }\n };\n\n const onFiles = (files: FileList | null | undefined) => {\n const file = files?.[0];\n if (file) void upload(file);\n };\n\n return (\n <>\n <div data-guardrail style={S.pickerScrim} onClick={onClose} />\n <div\n data-guardrail\n role=\"dialog\"\n aria-label=\"Choose an image\"\n style={S.picker}\n onClick={(e) => e.stopPropagation()}\n >\n <header style={S.drawerHead}>\n <h2 style={S.drawerTitle}>Choose an image</h2>\n <button\n style={{ ...S.iconBtn, color: v(\"muted\") }}\n onClick={onClose}\n aria-label=\"Close\"\n >\n <CloseIcon />\n </button>\n </header>\n\n <div style={S.drawerBody}>\n <div\n role=\"button\"\n tabIndex={0}\n style={{ ...S.dropzone, ...(dragging ? S.dropzoneActive : {}) }}\n onClick={() => !uploading && inputRef.current?.click()}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") inputRef.current?.click();\n }}\n onDragOver={(e) => {\n e.preventDefault();\n setDragging(true);\n }}\n onDragLeave={() => setDragging(false)}\n onDrop={(e) => {\n e.preventDefault();\n setDragging(false);\n onFiles(e.dataTransfer.files);\n }}\n >\n {uploading ? (\n <span style={{ display: \"inline-flex\", gap: 8, alignItems: \"center\" }}>\n <SpinnerIcon /> Uploading…\n </span>\n ) : (\n \"Drop an image here, or click to upload\"\n )}\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/png,image/jpeg,image/webp,image/gif,image/avif\"\n hidden\n onChange={(e) => {\n onFiles(e.target.files);\n e.target.value = \"\";\n }}\n />\n </div>\n\n {error && <div style={{ ...S.error, marginBottom: 12 }}>{error}</div>}\n\n {assets === null ? (\n <p style={{ fontSize: 13, color: v(\"muted\") }}>Loading…</p>\n ) : assets.length === 0 ? (\n <p style={{ fontSize: 13, color: v(\"muted\") }}>No images uploaded yet.</p>\n ) : (\n <div style={S.pickerGrid}>\n {assets.map((asset) => (\n <button\n key={asset.id}\n type=\"button\"\n title={asset.name}\n style={{\n ...S.pickerTile,\n ...(asset.url === current ? S.pickerTileActive : {}),\n }}\n onClick={async () => choose(asset.url, await measure(asset.url))}\n >\n {/* Plain img: the admin UI must not depend on next/image's\n remotePatterns being configured. */}\n <img src={asset.url} alt=\"\" loading=\"lazy\" style={S.pickerThumb} />\n </button>\n ))}\n </div>\n )}\n </div>\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/react/MediaPicker.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { createAssetUpload, listAssets } from \"../server/actions\";\nimport { CloseIcon, SpinnerIcon } from \"./Icons\";\nimport { prepareImage } from \"./resizeImage\";\nimport type { GRAsset } from \"../core/types\";\n\nexport interface PickedImage {\n src: string;\n width?: number;\n height?: number;\n}\n\ntype Size = { width?: number; height?: number };\n\n/**\n * Natural size is read in the browser, so no dimensions need storing on the\n * storage side and every adapter gets them for free.\n */\nfunction measure(url: string): Promise<Size> {\n return new Promise((resolve) => {\n const img = new Image();\n img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight });\n img.onerror = () => resolve({});\n img.src = url;\n });\n}\n\nexport function MediaPicker({\n current,\n onSelect,\n onClose,\n}: {\n current?: string;\n onSelect: (image: PickedImage) => void;\n onClose: () => void;\n}) {\n const [assets, setAssets] = useState<GRAsset[] | null>(null);\n const [uploading, setUploading] = useState(false);\n const [dragging, setDragging] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n void listAssets().then(setAssets);\n }, []);\n\n useEffect(() => {\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== \"Escape\") return;\n e.stopPropagation();\n onClose();\n };\n window.addEventListener(\"keydown\", onKey, true);\n return () => window.removeEventListener(\"keydown\", onKey, true);\n }, [onClose]);\n\n const choose = useCallback(\n (src: string, size: Size) => {\n onSelect({ src, ...size });\n onClose();\n },\n [onSelect, onClose],\n );\n\n const upload = async (file: File) => {\n if (uploading) return;\n setError(null);\n setUploading(true);\n try {\n // Scaled first, so the size check and the upload both see the file\n // that will actually be stored — an oversized phone photo that shrinks\n // under the cap is accepted.\n const prepared = await prepareImage(file);\n const payload = prepared.file;\n\n const res = await createAssetUpload({\n name: payload.name,\n contentType: payload.type,\n size: payload.size,\n });\n if (!res.success) {\n setError(res.error);\n return;\n }\n const { target } = res;\n\n // Straight to storage — the bytes never touch the Next.js server.\n const put = await fetch(target.url, {\n method: target.method,\n headers: target.headers,\n body: payload,\n });\n if (!put.ok) {\n setError(`Upload failed (${put.status}).`);\n return;\n }\n\n setAssets((prev) => [target.asset, ...(prev ?? [])]);\n choose(target.asset.url, { width: prepared.width, height: prepared.height });\n } catch {\n setError(\"Upload failed. Check your connection and try again.\");\n } finally {\n setUploading(false);\n }\n };\n\n const onFiles = (files: FileList | null | undefined) => {\n const file = files?.[0];\n if (file) void upload(file);\n };\n\n return (\n <>\n <div data-guardrail className=\"gr-picker-scrim\" onClick={onClose} />\n <div\n data-guardrail\n role=\"dialog\"\n aria-label=\"Choose an image\"\n className=\"gr-picker\"\n onClick={(e) => e.stopPropagation()}\n >\n <header className=\"gr-drawer-head\">\n <h2 className=\"gr-drawer-title\">Choose an image</h2>\n <button\n className=\"gr-icon-btn gr-text-muted\"\n onClick={onClose}\n aria-label=\"Close\"\n >\n <CloseIcon />\n </button>\n </header>\n\n <div className=\"gr-drawer-body\">\n <div\n role=\"button\"\n tabIndex={0}\n className={dragging ? \"gr-dropzone gr-dropzone-active\" : \"gr-dropzone\"}\n onClick={() => !uploading && inputRef.current?.click()}\n onKeyDown={(e) => {\n if (e.key === \"Enter\" || e.key === \" \") inputRef.current?.click();\n }}\n onDragOver={(e) => {\n e.preventDefault();\n setDragging(true);\n }}\n onDragLeave={() => setDragging(false)}\n onDrop={(e) => {\n e.preventDefault();\n setDragging(false);\n onFiles(e.dataTransfer.files);\n }}\n >\n {uploading ? (\n <span className=\"gr-inline-flex gr-gap-8 gr-items-center\">\n <SpinnerIcon /> Uploading…\n </span>\n ) : (\n \"Drop an image here, or click to upload\"\n )}\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/png,image/jpeg,image/webp,image/gif,image/avif\"\n hidden\n onChange={(e) => {\n onFiles(e.target.files);\n e.target.value = \"\";\n }}\n />\n </div>\n\n {error && <div className=\"gr-error gr-mb-12\">{error}</div>}\n\n {assets === null ? (\n <p className=\"gr-fs-13 gr-text-muted\">Loading…</p>\n ) : assets.length === 0 ? (\n <p className=\"gr-fs-13 gr-text-muted\">No images uploaded yet.</p>\n ) : (\n <div className=\"gr-picker-grid\">\n {assets.map((asset) => (\n <button\n key={asset.id}\n type=\"button\"\n title={asset.name}\n className={asset.url === current ? \"gr-picker-tile gr-picker-tile-active\" : \"gr-picker-tile\"}\n onClick={async () => choose(asset.url, await measure(asset.url))}\n >\n {/* Plain img: the admin UI must not depend on next/image's\n remotePatterns being configured. */}\n <img src={asset.url} alt=\"\" loading=\"lazy\" className=\"gr-picker-thumb\" />\n </button>\n ))}\n </div>\n )}\n </div>\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
SpinnerIcon,
|
|
12
12
|
TrashIcon
|
|
13
13
|
} from "../Icons";
|
|
14
|
-
import { S, v } from "../styles";
|
|
15
14
|
function RepeaterClient({
|
|
16
15
|
repeaterId,
|
|
17
16
|
routed,
|
|
@@ -56,15 +55,15 @@ function RepeaterClient({
|
|
|
56
55
|
};
|
|
57
56
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
58
57
|
items.length === 0 && children,
|
|
59
|
-
items.map(({ id, href, node }, index) => /* @__PURE__ */ jsxs("div", {
|
|
60
|
-
/* @__PURE__ */ jsxs("div", {
|
|
58
|
+
items.map(({ id, href, node }, index) => /* @__PURE__ */ jsxs("div", { className: "gr-repeater-item", children: [
|
|
59
|
+
/* @__PURE__ */ jsxs("div", { className: "gr-repeater-toolbar", children: [
|
|
61
60
|
href && // Clicking the item's fields edits them rather than following
|
|
62
61
|
// any link around them, so this is the way to its own page. A
|
|
63
62
|
// same-tab router push: the session keeps edit mode on.
|
|
64
63
|
/* @__PURE__ */ jsx(
|
|
65
64
|
"button",
|
|
66
65
|
{
|
|
67
|
-
|
|
66
|
+
className: "gr-icon-btn",
|
|
68
67
|
onClick: () => router.push(href),
|
|
69
68
|
"aria-label": "Open page",
|
|
70
69
|
title: "Open this item's page",
|
|
@@ -74,7 +73,7 @@ function RepeaterClient({
|
|
|
74
73
|
/* @__PURE__ */ jsx(
|
|
75
74
|
"button",
|
|
76
75
|
{
|
|
77
|
-
|
|
76
|
+
className: "gr-icon-btn",
|
|
78
77
|
disabled: pending || index === 0,
|
|
79
78
|
onClick: () => move(id, -1),
|
|
80
79
|
"aria-label": "Move up",
|
|
@@ -85,7 +84,7 @@ function RepeaterClient({
|
|
|
85
84
|
/* @__PURE__ */ jsx(
|
|
86
85
|
"button",
|
|
87
86
|
{
|
|
88
|
-
|
|
87
|
+
className: "gr-icon-btn",
|
|
89
88
|
disabled: pending || index === items.length - 1,
|
|
90
89
|
onClick: () => move(id, 1),
|
|
91
90
|
"aria-label": "Move down",
|
|
@@ -96,7 +95,7 @@ function RepeaterClient({
|
|
|
96
95
|
/* @__PURE__ */ jsx(
|
|
97
96
|
"button",
|
|
98
97
|
{
|
|
99
|
-
|
|
98
|
+
className: "gr-icon-btn gr-text-danger",
|
|
100
99
|
disabled: pending,
|
|
101
100
|
onClick: () => {
|
|
102
101
|
if (confirm("Remove this item and its content?")) {
|
|
@@ -111,12 +110,12 @@ function RepeaterClient({
|
|
|
111
110
|
] }),
|
|
112
111
|
node
|
|
113
112
|
] }, id)),
|
|
114
|
-
error && /* @__PURE__ */ jsx("div", {
|
|
115
|
-
/* @__PURE__ */ jsx("div", {
|
|
113
|
+
error && /* @__PURE__ */ jsx("div", { className: "gr-error gr-mt-8", children: error }),
|
|
114
|
+
/* @__PURE__ */ jsx("div", { className: "gr-repeater-add-row", children: adding && routed ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
116
115
|
/* @__PURE__ */ jsx(
|
|
117
116
|
"input",
|
|
118
117
|
{
|
|
119
|
-
|
|
118
|
+
className: "gr-repeater-slug-input",
|
|
120
119
|
placeholder: "url-slug",
|
|
121
120
|
value: slug,
|
|
122
121
|
autoFocus: true,
|
|
@@ -127,11 +126,11 @@ function RepeaterClient({
|
|
|
127
126
|
}
|
|
128
127
|
}
|
|
129
128
|
),
|
|
130
|
-
/* @__PURE__ */ jsx("button", {
|
|
129
|
+
/* @__PURE__ */ jsx("button", { className: "gr-primary-btn", disabled: pending, onClick: submitAdd, children: pending ? /* @__PURE__ */ jsx(SpinnerIcon, {}) : "Add" }),
|
|
131
130
|
/* @__PURE__ */ jsx(
|
|
132
131
|
"button",
|
|
133
132
|
{
|
|
134
|
-
|
|
133
|
+
className: "gr-cancel-btn",
|
|
135
134
|
disabled: pending,
|
|
136
135
|
onClick: () => {
|
|
137
136
|
setAdding(false);
|
|
@@ -144,7 +143,7 @@ function RepeaterClient({
|
|
|
144
143
|
"button",
|
|
145
144
|
{
|
|
146
145
|
type: "button",
|
|
147
|
-
|
|
146
|
+
className: "gr-repeater-add-btn",
|
|
148
147
|
disabled: pending,
|
|
149
148
|
onClick: () => routed ? setAdding(true) : submitAdd(),
|
|
150
149
|
children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/react/Repeater/client.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useState, useTransition } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from \"../../server/repeaterActions\";\nimport {\n ArrowDownIcon,\n ArrowUpIcon,\n OpenIcon,\n PlusIcon,\n SpinnerIcon,\n TrashIcon,\n} from \"../Icons\";\
|
|
1
|
+
{"version":3,"sources":["../../../src/react/Repeater/client.tsx"],"sourcesContent":["\"use client\";\n\nimport React, { useState, useTransition } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { addRepeaterItem, removeRepeaterItem, reorderRepeaterItems } from \"../../server/repeaterActions\";\nimport {\n ArrowDownIcon,\n ArrowUpIcon,\n OpenIcon,\n PlusIcon,\n SpinnerIcon,\n TrashIcon,\n} from \"../Icons\";\n\ninterface RenderedItem {\n id: string;\n /** The item's own page, when the repeater was told where its route lives. */\n href?: string;\n /** Already rendered server-side — this component only wraps it in chrome. */\n node: React.ReactNode;\n}\n\nexport function RepeaterClient({\n repeaterId,\n routed,\n items,\n children,\n}: {\n repeaterId: string;\n routed?: boolean;\n items: RenderedItem[];\n children?: React.ReactNode;\n}) {\n const router = useRouter();\n const [error, setError] = useState<string | null>(null);\n const [adding, setAdding] = useState(false);\n const [slug, setSlug] = useState(\"\");\n const [pending, startTransition] = useTransition();\n\n /**\n * There is deliberately no local copy of `items`. Each item's node can\n * contain <GRail> fields, which only exist as a resolved server render —\n * they cannot be reproduced by holding data in client state and re-invoking\n * a component function in the browser. router.refresh() re-runs <GRepeater>\n * on the server and delivers freshly rendered nodes as new props, the same\n * pattern used for the login flow in Admin.tsx.\n */\n const run = (fn: () => Promise<{ success: boolean; error?: string }>) =>\n startTransition(async () => {\n setError(null);\n const res = await fn();\n if (!res.success) setError(res.error ?? \"Something went wrong.\");\n router.refresh();\n });\n\n const move = (id: string, dir: -1 | 1) => {\n const ids = items.map((i) => i.id);\n const index = ids.indexOf(id);\n const target = index + dir;\n if (target < 0 || target >= ids.length) return;\n const a = ids[index]!;\n const b = ids[target]!;\n ids[index] = b;\n ids[target] = a;\n run(() => reorderRepeaterItems(repeaterId, ids));\n };\n\n const submitAdd = () => {\n if (routed && !slug.trim()) return;\n run(async () => {\n const res = await addRepeaterItem(\n repeaterId,\n routed ? { slug: slug.trim() } : undefined,\n );\n if (res.success) {\n setAdding(false);\n setSlug(\"\");\n }\n return res;\n });\n };\n\n return (\n <>\n {items.length === 0 && children}\n\n {items.map(({ id, href, node }, index) => (\n <div key={id} className=\"gr-repeater-item\">\n <div className=\"gr-repeater-toolbar\">\n {href && (\n // Clicking the item's fields edits them rather than following\n // any link around them, so this is the way to its own page. A\n // same-tab router push: the session keeps edit mode on.\n <button\n className=\"gr-icon-btn\"\n onClick={() => router.push(href)}\n aria-label=\"Open page\"\n title=\"Open this item's page\"\n >\n <OpenIcon />\n </button>\n )}\n <button\n className=\"gr-icon-btn\"\n disabled={pending || index === 0}\n onClick={() => move(id, -1)}\n aria-label=\"Move up\"\n title=\"Move up\"\n >\n <ArrowUpIcon />\n </button>\n <button\n className=\"gr-icon-btn\"\n disabled={pending || index === items.length - 1}\n onClick={() => move(id, 1)}\n aria-label=\"Move down\"\n title=\"Move down\"\n >\n <ArrowDownIcon />\n </button>\n <button\n className=\"gr-icon-btn gr-text-danger\"\n disabled={pending}\n onClick={() => {\n if (confirm(\"Remove this item and its content?\")) {\n run(() => removeRepeaterItem(repeaterId, id));\n }\n }}\n aria-label=\"Remove\"\n title=\"Remove\"\n >\n <TrashIcon />\n </button>\n </div>\n {node}\n </div>\n ))}\n\n {error && <div className=\"gr-error gr-mt-8\">{error}</div>}\n\n <div className=\"gr-repeater-add-row\">\n {adding && routed ? (\n <>\n <input\n className=\"gr-repeater-slug-input\"\n placeholder=\"url-slug\"\n value={slug}\n autoFocus\n onChange={(e) => setSlug(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Enter\") submitAdd();\n if (e.key === \"Escape\") setAdding(false);\n }}\n />\n <button className=\"gr-primary-btn\" disabled={pending} onClick={submitAdd}>\n {pending ? <SpinnerIcon /> : \"Add\"}\n </button>\n <button\n className=\"gr-cancel-btn\"\n disabled={pending}\n onClick={() => {\n setAdding(false);\n setSlug(\"\");\n }}\n >\n Cancel\n </button>\n </>\n ) : (\n <button\n type=\"button\"\n className=\"gr-repeater-add-btn\"\n disabled={pending}\n onClick={() => (routed ? setAdding(true) : submitAdd())}\n >\n <PlusIcon /> Add {routed ? \"item\" : \"card\"}\n </button>\n )}\n </div>\n </>\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/react/Team.d.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Keyframes cannot be expressed as inline styles, so the drawer ships the few
|
|
5
|
-
* it needs. Scoped to [data-guardrail-team] to stay clear of app styles, and
|
|
6
|
-
* disabled under prefers-reduced-motion.
|
|
7
|
-
*/
|
|
8
|
-
declare const ANIMATION_CSS = "\n@keyframes gr-fade-in { from { opacity: 0 } to { opacity: 1 } }\n@keyframes gr-slide-in {\n from { transform: translateX(100%) }\n to { transform: translateX(0) }\n}\n[data-guardrail-scrim] { animation: gr-fade-in 160ms ease-out both }\n[data-guardrail-team] { animation: gr-slide-in 220ms cubic-bezier(0.32, 0.72, 0, 1) both }\n[data-guardrail-team] [data-gr-row] { animation: gr-fade-in 240ms ease-out both }\n@keyframes gr-fade-out { from { opacity: 1 } to { opacity: 0 } }\n@keyframes gr-slide-out {\n from { transform: translateX(0) }\n to { transform: translateX(100%) }\n}\n[data-guardrail-scrim][data-closing] { animation: gr-fade-out 180ms ease-in both }\n[data-guardrail-team][data-closing] { animation: gr-slide-out 180ms cubic-bezier(0.4, 0, 1, 1) both }\n@media (prefers-reduced-motion: reduce) {\n [data-guardrail-scrim], [data-guardrail-team], [data-guardrail-team] [data-gr-row] {\n animation: none\n }\n}\n";
|
|
9
3
|
declare function TeamDrawer({ onClose }: {
|
|
10
4
|
onClose: () => void;
|
|
11
5
|
}): react_jsx_runtime.JSX.Element;
|
|
12
6
|
|
|
13
|
-
export {
|
|
7
|
+
export { TeamDrawer };
|
package/dist/react/Team.js
CHANGED
|
@@ -10,30 +10,7 @@ import {
|
|
|
10
10
|
} from "../server/actions";
|
|
11
11
|
import { useGRail } from "./Provider";
|
|
12
12
|
import { CloseIcon, KeyIcon } from "./Icons";
|
|
13
|
-
import { S, v } from "./styles";
|
|
14
13
|
const ROLES = ["admin", "editor"];
|
|
15
|
-
const ANIMATION_CSS = `
|
|
16
|
-
@keyframes gr-fade-in { from { opacity: 0 } to { opacity: 1 } }
|
|
17
|
-
@keyframes gr-slide-in {
|
|
18
|
-
from { transform: translateX(100%) }
|
|
19
|
-
to { transform: translateX(0) }
|
|
20
|
-
}
|
|
21
|
-
[data-guardrail-scrim] { animation: gr-fade-in 160ms ease-out both }
|
|
22
|
-
[data-guardrail-team] { animation: gr-slide-in 220ms cubic-bezier(0.32, 0.72, 0, 1) both }
|
|
23
|
-
[data-guardrail-team] [data-gr-row] { animation: gr-fade-in 240ms ease-out both }
|
|
24
|
-
@keyframes gr-fade-out { from { opacity: 1 } to { opacity: 0 } }
|
|
25
|
-
@keyframes gr-slide-out {
|
|
26
|
-
from { transform: translateX(0) }
|
|
27
|
-
to { transform: translateX(100%) }
|
|
28
|
-
}
|
|
29
|
-
[data-guardrail-scrim][data-closing] { animation: gr-fade-out 180ms ease-in both }
|
|
30
|
-
[data-guardrail-team][data-closing] { animation: gr-slide-out 180ms cubic-bezier(0.4, 0, 1, 1) both }
|
|
31
|
-
@media (prefers-reduced-motion: reduce) {
|
|
32
|
-
[data-guardrail-scrim], [data-guardrail-team], [data-guardrail-team] [data-gr-row] {
|
|
33
|
-
animation: none
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
`;
|
|
37
14
|
function TeamDrawer({ onClose }) {
|
|
38
15
|
const { role, email: currentEmail } = useGRail();
|
|
39
16
|
const isAdmin = role === "admin";
|
|
@@ -76,14 +53,13 @@ function TeamDrawer({ onClose }) {
|
|
|
76
53
|
return () => window.removeEventListener("keydown", onKey);
|
|
77
54
|
}, [requestClose]);
|
|
78
55
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
79
|
-
/* @__PURE__ */ jsx("style", { href: "guardrail-team-animation", precedence: "default", children: ANIMATION_CSS }),
|
|
80
56
|
/* @__PURE__ */ jsx(
|
|
81
57
|
"div",
|
|
82
58
|
{
|
|
83
59
|
"data-guardrail": true,
|
|
84
60
|
"data-guardrail-scrim": true,
|
|
85
61
|
...closing ? { "data-closing": "" } : {},
|
|
86
|
-
|
|
62
|
+
className: "gr-scrim",
|
|
87
63
|
onClick: requestClose
|
|
88
64
|
}
|
|
89
65
|
),
|
|
@@ -96,27 +72,27 @@ function TeamDrawer({ onClose }) {
|
|
|
96
72
|
onAnimationEnd: (e) => {
|
|
97
73
|
if (closing && e.target === e.currentTarget) onClose();
|
|
98
74
|
},
|
|
99
|
-
|
|
75
|
+
className: "gr-drawer",
|
|
100
76
|
role: "dialog",
|
|
101
77
|
"aria-label": "Team",
|
|
102
78
|
children: [
|
|
103
|
-
/* @__PURE__ */ jsxs("header", {
|
|
104
|
-
/* @__PURE__ */ jsx("h2", {
|
|
105
|
-
/* @__PURE__ */ jsx("button", {
|
|
79
|
+
/* @__PURE__ */ jsxs("header", { className: "gr-drawer-head", children: [
|
|
80
|
+
/* @__PURE__ */ jsx("h2", { className: "gr-drawer-title", children: "Team" }),
|
|
81
|
+
/* @__PURE__ */ jsx("button", { className: "gr-icon-btn gr-text-muted", onClick: requestClose, "aria-label": "Close", children: /* @__PURE__ */ jsx(CloseIcon, {}) })
|
|
106
82
|
] }),
|
|
107
|
-
/* @__PURE__ */ jsxs("div", {
|
|
108
|
-
error && /* @__PURE__ */ jsx("div", {
|
|
109
|
-
notice && /* @__PURE__ */ jsx("div", {
|
|
110
|
-
loading ? /* @__PURE__ */ jsx("p", {
|
|
111
|
-
/* @__PURE__ */ jsxs("span", {
|
|
83
|
+
/* @__PURE__ */ jsxs("div", { className: "gr-drawer-body", children: [
|
|
84
|
+
error && /* @__PURE__ */ jsx("div", { className: "gr-error gr-mb-12", children: error }),
|
|
85
|
+
notice && /* @__PURE__ */ jsx("div", { className: "gr-fs-12 gr-text-muted gr-mb-12", role: "status", children: notice }),
|
|
86
|
+
loading ? /* @__PURE__ */ jsx("p", { className: "gr-fs-13 gr-text-muted", children: "Loading\u2026" }) : members.map((m) => /* @__PURE__ */ jsxs("div", { "data-gr-row": true, className: "gr-member-row", children: [
|
|
87
|
+
/* @__PURE__ */ jsxs("span", { className: "gr-member-email", title: m.email, children: [
|
|
112
88
|
m.email,
|
|
113
89
|
m.email === currentEmail && " (you)"
|
|
114
90
|
] }),
|
|
115
|
-
m.status === "pending" && /* @__PURE__ */ jsx("span", {
|
|
91
|
+
m.status === "pending" && /* @__PURE__ */ jsx("span", { className: "gr-badge", children: "Invited" }),
|
|
116
92
|
isAdmin ? /* @__PURE__ */ jsx(
|
|
117
93
|
"select",
|
|
118
94
|
{
|
|
119
|
-
|
|
95
|
+
className: "gr-select",
|
|
120
96
|
value: m.role,
|
|
121
97
|
disabled: pending,
|
|
122
98
|
onChange: (e) => run(
|
|
@@ -124,11 +100,11 @@ function TeamDrawer({ onClose }) {
|
|
|
124
100
|
),
|
|
125
101
|
children: ROLES.map((r) => /* @__PURE__ */ jsx("option", { value: r, children: r }, r))
|
|
126
102
|
}
|
|
127
|
-
) : /* @__PURE__ */ jsx("span", {
|
|
103
|
+
) : /* @__PURE__ */ jsx("span", { className: "gr-fs-12 gr-text-muted", children: m.role }),
|
|
128
104
|
isAdmin && /* @__PURE__ */ jsx(
|
|
129
105
|
"button",
|
|
130
106
|
{
|
|
131
|
-
|
|
107
|
+
className: "gr-icon-btn gr-text-muted gr-size-22",
|
|
132
108
|
disabled: pending,
|
|
133
109
|
onClick: () => run(() => sendPasswordResetAction(m.email)),
|
|
134
110
|
"aria-label": `Send password reset to ${m.email}`,
|
|
@@ -139,7 +115,7 @@ function TeamDrawer({ onClose }) {
|
|
|
139
115
|
isAdmin && /* @__PURE__ */ jsx(
|
|
140
116
|
"button",
|
|
141
117
|
{
|
|
142
|
-
|
|
118
|
+
className: "gr-icon-btn gr-text-muted gr-size-22",
|
|
143
119
|
disabled: pending,
|
|
144
120
|
onClick: () => run(() => removeMember(m.email)),
|
|
145
121
|
"aria-label": `Remove ${m.email}`,
|
|
@@ -149,7 +125,7 @@ function TeamDrawer({ onClose }) {
|
|
|
149
125
|
)
|
|
150
126
|
] }, m.email)),
|
|
151
127
|
isAdmin && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
152
|
-
/* @__PURE__ */ jsx("p", {
|
|
128
|
+
/* @__PURE__ */ jsx("p", { className: "gr-section-label", children: "Invite someone" }),
|
|
153
129
|
/* @__PURE__ */ jsxs(
|
|
154
130
|
"form",
|
|
155
131
|
{
|
|
@@ -165,7 +141,7 @@ function TeamDrawer({ onClose }) {
|
|
|
165
141
|
/* @__PURE__ */ jsx(
|
|
166
142
|
"input",
|
|
167
143
|
{
|
|
168
|
-
|
|
144
|
+
className: "gr-field",
|
|
169
145
|
type: "email",
|
|
170
146
|
placeholder: "name@company.com",
|
|
171
147
|
value: inviteEmail,
|
|
@@ -173,22 +149,22 @@ function TeamDrawer({ onClose }) {
|
|
|
173
149
|
required: true
|
|
174
150
|
}
|
|
175
151
|
),
|
|
176
|
-
/* @__PURE__ */ jsxs("div", {
|
|
152
|
+
/* @__PURE__ */ jsxs("div", { className: "gr-flex gr-gap-8", children: [
|
|
177
153
|
/* @__PURE__ */ jsx(
|
|
178
154
|
"select",
|
|
179
155
|
{
|
|
180
|
-
|
|
156
|
+
className: "gr-select gr-flex-1",
|
|
181
157
|
value: inviteRole,
|
|
182
158
|
onChange: (e) => setInviteRole(e.target.value),
|
|
183
159
|
children: ROLES.map((r) => /* @__PURE__ */ jsx("option", { value: r, children: r }, r))
|
|
184
160
|
}
|
|
185
161
|
),
|
|
186
|
-
/* @__PURE__ */ jsx("button", {
|
|
162
|
+
/* @__PURE__ */ jsx("button", { className: "gr-primary-btn", type: "submit", disabled: pending, children: pending ? "Working\u2026" : "Invite" })
|
|
187
163
|
] })
|
|
188
164
|
]
|
|
189
165
|
}
|
|
190
166
|
),
|
|
191
|
-
/* @__PURE__ */ jsxs("p", {
|
|
167
|
+
/* @__PURE__ */ jsxs("p", { className: "gr-fs-11 gr-text-subtle gr-mt-10", children: [
|
|
192
168
|
"New people get an email to set a password. Invites and resets go out through your ",
|
|
193
169
|
/* @__PURE__ */ jsx("code", { children: "onInvite" }),
|
|
194
170
|
" / ",
|
|
@@ -204,7 +180,6 @@ function TeamDrawer({ onClose }) {
|
|
|
204
180
|
] });
|
|
205
181
|
}
|
|
206
182
|
export {
|
|
207
|
-
ANIMATION_CSS,
|
|
208
183
|
TeamDrawer
|
|
209
184
|
};
|
|
210
185
|
//# sourceMappingURL=Team.js.map
|