smoodly 0.0.57 → 0.0.58
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/dist/admin/files.generated.js +1 -1
- package/dist/admin/forms/MediaGrid.d.ts +3 -2
- package/dist/admin/forms/MediaGrid.js +37 -7
- package/dist/admin/forms/RichtextElementCard.js +2 -1
- package/dist/admin/forms/RichtextImageCard.js +2 -1
- package/dist/admin/forms/richtext-extensions.js +7 -2
- package/dist/admin/shell/MediaScreen.js +2 -2
- package/dist/admin/ui/theme.js +14 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
2
|
import type { AssetRecord } from "../../media.ts";
|
|
3
|
-
export declare function AssetThumb({ asset, className }: {
|
|
3
|
+
export declare function AssetThumb({ asset, className, width }: {
|
|
4
4
|
asset: AssetRecord;
|
|
5
5
|
className?: string;
|
|
6
|
+
width?: 320 | 640;
|
|
6
7
|
}): ReactElement;
|
|
7
8
|
export declare function MediaGrid({ items, selectedId, onSelect }: {
|
|
8
9
|
items: AssetRecord[];
|
|
@@ -1,15 +1,45 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// The library grid the picker and the Media screen share: one card per
|
|
4
|
+
// asset — an image's thumbnail (a small cut where the service cuts, spec
|
|
5
|
+
// 2026-10-18 media on R2 and Images §9), a video's poster, a file's chip.
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
3
7
|
import { formatBytes } from "../shell/media-list.js";
|
|
8
|
+
import { useAssets } from "./assets-context.js";
|
|
4
9
|
import { adminImageSrc } from "./crop-editor.js";
|
|
5
|
-
|
|
10
|
+
/** A video's picture (2026-11-13): its poster, the frame the upload grabbed, drawn as the image it is — a cut where
|
|
11
|
+
* the service cuts, and nothing of the video's bytes fetched for a card. A video with no poster (the browser that
|
|
12
|
+
* uploaded it could not decode it, or its poster is gone) asks the browser for its opening frame instead: the
|
|
13
|
+
* `#t` fragment is what makes Safari draw one, and a browser that cannot decode it leaves the "Video" chip showing. */
|
|
14
|
+
function VideoThumb({ asset, className, width }) {
|
|
15
|
+
const assets = useAssets();
|
|
16
|
+
const [poster, setPoster] = useState(null);
|
|
17
|
+
const [failed, setFailed] = useState(false);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
let live = true;
|
|
20
|
+
setPoster(null);
|
|
21
|
+
setFailed(false);
|
|
22
|
+
if (asset.posterId)
|
|
23
|
+
void assets.get(asset.posterId).then((p) => { if (live) {
|
|
24
|
+
if (p && p.status === "ready")
|
|
25
|
+
setPoster(p);
|
|
26
|
+
else
|
|
27
|
+
setFailed(true);
|
|
28
|
+
} });
|
|
29
|
+
return () => { live = false; };
|
|
30
|
+
}, [assets, asset.posterId]);
|
|
31
|
+
if (poster && !failed)
|
|
32
|
+
return _jsx("img", { className: className, src: adminImageSrc(poster, width), alt: "", loading: "lazy", onError: () => setFailed(true) });
|
|
33
|
+
// The poster is on its way: an empty box, never a flash of the chip.
|
|
34
|
+
if (asset.posterId && !failed)
|
|
35
|
+
return _jsx("span", { className: className });
|
|
36
|
+
return (_jsxs("span", { className: `${className ?? ""} sm-thumb--chip sm-thumb--video`, children: ["Video", _jsx("video", { src: `${asset.url}#t=0.1`, muted: true, playsInline: true, preload: "metadata", tabIndex: -1 })] }));
|
|
37
|
+
}
|
|
38
|
+
export function AssetThumb({ asset, className, width = 320 }) {
|
|
6
39
|
if (asset.kind === "image")
|
|
7
|
-
return _jsx("img", { className: className, src: adminImageSrc(asset,
|
|
8
|
-
if (asset.kind === "video")
|
|
9
|
-
return asset
|
|
10
|
-
? _jsx("video", { className: className, src: asset.url, muted: true, playsInline: true, preload: "metadata" })
|
|
11
|
-
: _jsx("span", { className: `${className ?? ""} sm-thumb--chip`, children: "Video" });
|
|
12
|
-
}
|
|
40
|
+
return _jsx("img", { className: className, src: adminImageSrc(asset, width), alt: "", loading: "lazy" });
|
|
41
|
+
if (asset.kind === "video")
|
|
42
|
+
return _jsx(VideoThumb, { asset: asset, className: className, width: width });
|
|
13
43
|
return _jsx("span", { className: `${className ?? ""} sm-thumb--chip`, children: asset.filename.split(".").pop()?.toUpperCase() ?? "File" });
|
|
14
44
|
}
|
|
15
45
|
export function MediaGrid({ items, selectedId, onSelect }) {
|
|
@@ -15,6 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
15
15
|
// takes the broken stripe and lists its stored fields read-only; a
|
|
16
16
|
// registered one the field's toolset leaves out says so in its head.
|
|
17
17
|
import { useState } from "react";
|
|
18
|
+
import { GripVertical } from "lucide-react";
|
|
18
19
|
import { NodeViewWrapper } from "@tiptap/react";
|
|
19
20
|
import { isHidden } from "../../conditions.js";
|
|
20
21
|
import { IconButton, SectionLabel, cx } from "../ui/primitives.js";
|
|
@@ -45,5 +46,5 @@ export function RichtextElementCard({ node, editor, getPos, updateAttributes, de
|
|
|
45
46
|
const scope = { fields, settings };
|
|
46
47
|
const visibleFields = Object.entries(element?.fields ?? {}).filter(([, d]) => !isHidden(d, { self: fields, ...scope }));
|
|
47
48
|
const visibleSettings = Object.entries(element?.settings ?? {}).filter(([, d]) => !isHidden(d, { self: settings, ...scope }));
|
|
48
|
-
return (_jsxs(NodeViewWrapper, { className: cx(cardClass(selected, form.canvas), state.unknown && "sm-rtcard--broken"), "data-theme": form.canvas ? "light" : undefined, children: [_jsxs("div", { className: "sm-rtcard__head", "data-drag-handle": true, onClick: select, children: [_jsx("span", { className: "sm-rtcard__icon", "aria-hidden": "true", children: element?.icon ?? "▤" }), _jsx("span", { className: "sm-rtcard__title", children: element?.title ?? `Unknown element "${name}"` }), state.outside && _jsx("span", { className: "sm-hint", children: "Not in this field's toolset" }), _jsx(IconButton, { label: open ? "Collapse" : "Expand", onClick: (e) => { e.stopPropagation(); setOpen((o) => !o); }, children: open ? "▾" : "▸" }), _jsx(IconButton, { label: "Delete element", onClick: (e) => { e.stopPropagation(); deleteNode(); }, children: "\u00D7" })] }), open && !element && (_jsx("div", { className: "sm-rtcard__body", contentEditable: false, children: Object.entries(fields).map(([key, value]) => (_jsxs("div", { className: "sm-orphans__row", children: [_jsx("code", { children: key }), _jsx("span", { className: "sm-orphans__value", children: typeof value === "string" ? value : JSON.stringify(value) })] }, key))) })), open && element && (_jsxs("div", { className: "sm-rtcard__body", contentEditable: false, children: [visibleFields.map(([key, d]) => (_jsx(FieldWidget, { fieldKey: key, descriptor: d, value: fields[key], errors: form.errors, path: `${base}.${key}`, scope: scope, onChange: (v) => updateAttributes({ fields: { ...fields, [key]: v } }) }, key))), visibleSettings.length > 0 && _jsx(SectionLabel, { caps: true, children: "Settings" }), visibleSettings.map(([key, d]) => (_jsx(FieldWidget, { fieldKey: key, descriptor: d, value: settings[key], errors: form.errors, path: `${base}.settings.${key}`, scope: scope, onChange: (v) => updateAttributes({ settings: { ...settings, [key]: v } }) }, `setting-${key}`)))] }))] }));
|
|
49
|
+
return (_jsxs(NodeViewWrapper, { className: cx(cardClass(selected, form.canvas), state.unknown && "sm-rtcard--broken"), "data-theme": form.canvas ? "light" : undefined, children: [_jsxs("div", { className: "sm-rtcard__head", "data-drag-handle": true, onClick: select, children: [_jsx("span", { className: "sm-rtcard__grip", title: "Drag to reorder", "aria-hidden": "true", children: _jsx(GripVertical, { size: 14, strokeWidth: 1.75 }) }), _jsx("span", { className: "sm-rtcard__icon", "aria-hidden": "true", children: element?.icon ?? "▤" }), _jsx("span", { className: "sm-rtcard__title", children: element?.title ?? `Unknown element "${name}"` }), state.outside && _jsx("span", { className: "sm-hint", children: "Not in this field's toolset" }), _jsx(IconButton, { label: open ? "Collapse" : "Expand", onClick: (e) => { e.stopPropagation(); setOpen((o) => !o); }, children: open ? "▾" : "▸" }), _jsx(IconButton, { label: "Delete element", onClick: (e) => { e.stopPropagation(); deleteNode(); }, children: "\u00D7" })] }), open && !element && (_jsx("div", { className: "sm-rtcard__body", contentEditable: false, children: Object.entries(fields).map(([key, value]) => (_jsxs("div", { className: "sm-orphans__row", children: [_jsx("code", { children: key }), _jsx("span", { className: "sm-orphans__value", children: typeof value === "string" ? value : JSON.stringify(value) })] }, key))) })), open && element && (_jsxs("div", { className: "sm-rtcard__body", contentEditable: false, children: [visibleFields.map(([key, d]) => (_jsx(FieldWidget, { fieldKey: key, descriptor: d, value: fields[key], errors: form.errors, path: `${base}.${key}`, scope: scope, onChange: (v) => updateAttributes({ fields: { ...fields, [key]: v } }) }, key))), visibleSettings.length > 0 && _jsx(SectionLabel, { caps: true, children: "Settings" }), visibleSettings.map(([key, d]) => (_jsx(FieldWidget, { fieldKey: key, descriptor: d, value: settings[key], errors: form.errors, path: `${base}.settings.${key}`, scope: scope, onChange: (v) => updateAttributes({ settings: { ...settings, [key]: v } }) }, `setting-${key}`)))] }))] }));
|
|
49
50
|
}
|
|
@@ -9,6 +9,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
9
9
|
// store there; the bound renderer applies them. Attribute edits go
|
|
10
10
|
// through updateAttributes so the outer history sees them.
|
|
11
11
|
import { useEffect, useState } from "react";
|
|
12
|
+
import { GripVertical } from "lucide-react";
|
|
12
13
|
import { NodeViewWrapper } from "@tiptap/react";
|
|
13
14
|
import { useAssets } from "./assets-context.js";
|
|
14
15
|
import { MediaPicker } from "./MediaPicker.js";
|
|
@@ -43,5 +44,5 @@ export function RichtextImageCard({ node, editor, getPos, updateAttributes, dele
|
|
|
43
44
|
updateAttributes({ image: value.alt ? { asset: record.id, alt: value.alt } : { asset: record.id } });
|
|
44
45
|
};
|
|
45
46
|
const editable = asset !== null && asset.kind === "image" && !!asset.width && !!asset.height;
|
|
46
|
-
return (_jsxs(NodeViewWrapper, { className: cardClass(selected, form.canvas), "data-theme": form.canvas ? "light" : undefined, "data-drag-handle": true, children: [_jsxs("div", { className: "sm-rtcard__head", onClick: select, children: [_jsx("span", { className: "sm-rtcard__icon", "aria-hidden": "true", children: "\u25A3" }), _jsx("span", { className: "sm-rtcard__title", children: "Image" }), _jsxs("span", { className: "sm-field__actions", children: [editable && _jsx("button", { type: "button", className: "sm-field__action", onClick: () => setCrop(true), children: "Edit" }), _jsx("button", { type: "button", className: "sm-field__action", onClick: () => setPicker(true), children: "Replace" }), _jsx("button", { type: "button", className: "sm-field__action", onClick: () => deleteNode(), children: "Remove" })] })] }), _jsxs("div", { className: "sm-rtcard__body", contentEditable: false, children: [asset && id ? _jsx(ImagePreview, { asset: asset, value: { ...value, asset: id }, height: 160 }) : _jsx("div", { className: "sm-rtcard__thumb", style: { height: 80 } }), caption !== "" && _jsx("span", { className: "sm-rtcard__caption", children: caption })] }), picker && _jsx(MediaPicker, { kinds: ["image"], onPick: replace, onClose: () => setPicker(false) }), crop && asset && id && (_jsx(CropEditor, { asset: asset, descriptor: { type: "image" }, value: { ...value, asset: id }, caption: caption, onSave: (next, cap) => { updateAttributes({ image: next, caption: cap ?? "" }); setCrop(false); }, onClose: () => setCrop(false) }))] }));
|
|
47
|
+
return (_jsxs(NodeViewWrapper, { className: cardClass(selected, form.canvas), "data-theme": form.canvas ? "light" : undefined, "data-drag-handle": true, children: [_jsxs("div", { className: "sm-rtcard__head", onClick: select, children: [_jsx("span", { className: "sm-rtcard__grip", title: "Drag to reorder", "aria-hidden": "true", children: _jsx(GripVertical, { size: 14, strokeWidth: 1.75 }) }), _jsx("span", { className: "sm-rtcard__icon", "aria-hidden": "true", children: "\u25A3" }), _jsx("span", { className: "sm-rtcard__title", children: "Image" }), _jsxs("span", { className: "sm-field__actions", children: [editable && _jsx("button", { type: "button", className: "sm-field__action", onClick: () => setCrop(true), children: "Edit" }), _jsx("button", { type: "button", className: "sm-field__action", onClick: () => setPicker(true), children: "Replace" }), _jsx("button", { type: "button", className: "sm-field__action", onClick: () => deleteNode(), children: "Remove" })] })] }), _jsxs("div", { className: "sm-rtcard__body", contentEditable: false, children: [asset && id ? _jsx(ImagePreview, { asset: asset, value: { ...value, asset: id }, height: 160 }) : _jsx("div", { className: "sm-rtcard__thumb", style: { height: 80 } }), caption !== "" && _jsx("span", { className: "sm-rtcard__caption", children: caption })] }), picker && _jsx(MediaPicker, { kinds: ["image"], onPick: replace, onClose: () => setPicker(false) }), crop && asset && id && (_jsx(CropEditor, { asset: asset, descriptor: { type: "image" }, value: { ...value, asset: id }, caption: caption, onSave: (next, cap) => { updateAttributes({ image: next, caption: cap ?? "" }); setCrop(false); }, onClose: () => setCrop(false) }))] }));
|
|
47
48
|
}
|
|
@@ -4,7 +4,12 @@
|
|
|
4
4
|
// and keyboard shortcuts here, the toolbar in RichtextToolbar, paste in
|
|
5
5
|
// richtext-paste. StarterKit supplies document, paragraph, text, hard
|
|
6
6
|
// break, list item, list keymap and undo/redo; every gated node or mark
|
|
7
|
-
// is switched off there and added back extended.
|
|
7
|
+
// is switched off there and added back extended. The gap cursor stays on:
|
|
8
|
+
// a card is an atom, so beside one that opens or closes the document it is
|
|
9
|
+
// the only place a caret can stand (2026-11-13: a field holding one card
|
|
10
|
+
// took no text, and typing replaced the selected card).
|
|
11
|
+
// The drop cursor too: a card is dragged by its head, and the line says
|
|
12
|
+
// where it lands; its look is the theme's (.sm-dropcursor), in both mounts.
|
|
8
13
|
import { Extension, textblockTypeInputRule } from "@tiptap/core";
|
|
9
14
|
import StarterKit from "@tiptap/starter-kit";
|
|
10
15
|
import Heading from "@tiptap/extension-heading";
|
|
@@ -41,7 +46,7 @@ export function richtextExtensions(allowed, hooks) {
|
|
|
41
46
|
heading: false, bold: false, italic: false, link: false, blockquote: false,
|
|
42
47
|
bulletList: false, orderedList: false,
|
|
43
48
|
code: false, codeBlock: false, strike: false, underline: false, horizontalRule: false,
|
|
44
|
-
dropcursor: false,
|
|
49
|
+
dropcursor: { color: false, width: 2, class: "sm-dropcursor" }, trailingNode: false,
|
|
45
50
|
}),
|
|
46
51
|
Heading.extend({
|
|
47
52
|
// Exact `#` counts, and only the allowed levels; `# ` does nothing when h1 is forbidden.
|
|
@@ -7,7 +7,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
7
7
|
import { useCallback, useEffect, useState } from "react";
|
|
8
8
|
import { useAssets } from "../forms/assets-context.js";
|
|
9
9
|
import { uploadFile } from "../forms/upload.js";
|
|
10
|
-
import { MediaGrid } from "../forms/MediaGrid.js";
|
|
10
|
+
import { AssetThumb, MediaGrid } from "../forms/MediaGrid.js";
|
|
11
11
|
import { Banner, Button, Field, Segmented, TopBar, UploadProgress } from "../ui/primitives.js";
|
|
12
12
|
import { confirm } from "../ui/confirm.js";
|
|
13
13
|
import { adminImageSrc } from "../forms/crop-editor.js";
|
|
@@ -93,7 +93,7 @@ export function MediaScreen({ initialItems = [], initialSelected = null, initial
|
|
|
93
93
|
setSelected(null);
|
|
94
94
|
load();
|
|
95
95
|
};
|
|
96
|
-
return (_jsxs(_Fragment, { children: [_jsx(TopBar, { left: _jsxs(_Fragment, { children: [_jsx("span", { className: "sm-title", children: "Media" }), _jsx("span", { className: "sm-count", children: items.length })] }), right: _jsxs("div", { className: "sm-picker-bar", children: [_jsx("input", { className: "sm-input", placeholder: "Search media", value: search, onChange: (e) => setSearch(e.target.value), style: { width: 220 } }), _jsx(Segmented, { options: KIND_FILTERS, value: filter, onChange: setFilter }), _jsxs("label", { className: "sm-btn sm-btn--primary", children: [busy ? "Uploading…" : "Upload", _jsx("input", { type: "file", accept: acceptFor(["image", "video", "file"], assets.media.accept), hidden: true, disabled: busy, onChange: (e) => { void onFiles(e.target.files); } })] })] }) }), error && _jsx(Banner, { tone: "error", onDismiss: () => setError(null), children: error }), progress !== null && _jsx("div", { className: "sm-mediascreen__upload", children: _jsx(UploadProgress, { fraction: progress }) }), _jsxs("div", { className: "sm-mediascreen", style: selected ? undefined : { gridTemplateColumns: "1fr" }, children: [_jsx("div", { className: "sm-mediascreen__grid", children: _jsx(MediaGrid, { items: items, selectedId: selected?.id, onSelect: (a) => setSelected(a.id === selected?.id ? null : a) }) }), selected && (_jsxs("aside", { className: "sm-mediascreen__inspector", children: [
|
|
96
|
+
return (_jsxs(_Fragment, { children: [_jsx(TopBar, { left: _jsxs(_Fragment, { children: [_jsx("span", { className: "sm-title", children: "Media" }), _jsx("span", { className: "sm-count", children: items.length })] }), right: _jsxs("div", { className: "sm-picker-bar", children: [_jsx("input", { className: "sm-input", placeholder: "Search media", value: search, onChange: (e) => setSearch(e.target.value), style: { width: 220 } }), _jsx(Segmented, { options: KIND_FILTERS, value: filter, onChange: setFilter }), _jsxs("label", { className: "sm-btn sm-btn--primary", children: [busy ? "Uploading…" : "Upload", _jsx("input", { type: "file", accept: acceptFor(["image", "video", "file"], assets.media.accept), hidden: true, disabled: busy, onChange: (e) => { void onFiles(e.target.files); } })] })] }) }), error && _jsx(Banner, { tone: "error", onDismiss: () => setError(null), children: error }), progress !== null && _jsx("div", { className: "sm-mediascreen__upload", children: _jsx(UploadProgress, { fraction: progress }) }), _jsxs("div", { className: "sm-mediascreen", style: selected ? undefined : { gridTemplateColumns: "1fr" }, children: [_jsx("div", { className: "sm-mediascreen__grid", children: _jsx(MediaGrid, { items: items, selectedId: selected?.id, onSelect: (a) => setSelected(a.id === selected?.id ? null : a) }) }), selected && (_jsxs("aside", { className: "sm-mediascreen__inspector", children: [_jsxs("div", { className: "sm-media__thumb", style: { aspectRatio: selected.width && selected.height ? `${selected.width} / ${selected.height}` : "4 / 3" }, children: [selected.kind === "image" && _jsx("img", { src: adminImageSrc(selected, 640), alt: "", style: { left: 0, top: 0, width: "100%", height: "100%" } }), selected.kind === "video" && _jsx(AssetThumb, { asset: selected, className: "sm-mediacard__img", width: 640 })] }), META.map((key) => (_jsx(Field, { label: key === "alt" ? "Alt text" : key[0].toUpperCase() + key.slice(1), children: _jsx("input", { className: "sm-input", value: meta[key], onChange: (e) => setMeta((m) => ({ ...m, [key]: e.target.value })), onBlur: () => { void saveMeta(key); } }) }, key))), _jsxs("div", { className: "sm-facts", children: [_jsx("span", { children: "File" }), _jsx("span", { children: selected.filename }), _jsx("span", { children: "Type" }), _jsx("span", { children: selected.mime }), _jsx("span", { children: "Size" }), _jsx("span", { children: formatBytes(selected.size) }), selected.width && selected.height && _jsxs(_Fragment, { children: [_jsx("span", { children: "Dimensions" }), _jsxs("span", { children: [selected.width, "\u00D7", selected.height] })] }), selected.duration !== null && _jsxs(_Fragment, { children: [_jsx("span", { children: "Duration" }), _jsxs("span", { children: [Math.round(selected.duration), " s"] })] })] }), _jsxs("div", { className: "sm-usage", children: [_jsx("span", { className: "sm-label", children: usageLabel(usage.length) }), usage.map((u, i) => u.href
|
|
97
97
|
? _jsxs(RouteLink, { href: u.href, children: [u.title, " \u00B7 ", u.sourceField] }, `${u.sourceId}-${i}`)
|
|
98
98
|
: _jsxs("span", { children: [u.title, " \u00B7 ", u.sourceField] }, `${u.sourceId}-${i}`))] }), _jsx(Button, { variant: "secondary", disabled: usage.length > 0, title: usage.length > 0 ? "Remove it from those places first." : undefined, onClick: () => { void remove(); }, children: "Delete" })] }))] })] }));
|
|
99
99
|
}
|
package/dist/admin/ui/theme.js
CHANGED
|
@@ -494,6 +494,17 @@ html:has(.sm-admin),body:has(.sm-admin){overscroll-behavior:none}
|
|
|
494
494
|
}
|
|
495
495
|
.sm-floatbar .sm-richtext__link{padding:0 4px 4px}
|
|
496
496
|
.sm-floatbar .sm-richtext__menu{position:static;width:300px}
|
|
497
|
+
/* The gap cursor, the caret beside a card with no text line next to it:
|
|
498
|
+
TipTap's own rule draws it black and reaches the admin document only,
|
|
499
|
+
so both mounts take it from here, in the text's colour. */
|
|
500
|
+
.sm-richtext__content .ProseMirror-gapcursor,.sm-inline-richtext .ProseMirror-gapcursor{display:none;pointer-events:none;position:absolute;margin:0}
|
|
501
|
+
.sm-richtext__content .ProseMirror-gapcursor:after,.sm-inline-richtext .ProseMirror-gapcursor:after{content:"";display:block;position:absolute;top:-2px;width:20px;border-top:1px solid currentColor;animation:sm-gapcursor-blink 1.1s steps(2,start) infinite}
|
|
502
|
+
.sm-richtext__content.ProseMirror-focused .ProseMirror-gapcursor,.sm-inline-richtext.ProseMirror-focused .ProseMirror-gapcursor{display:block}
|
|
503
|
+
@keyframes sm-gapcursor-blink{to{visibility:hidden}}
|
|
504
|
+
/* Where a dragged card lands, and the grip that says a card's head drags. */
|
|
505
|
+
.sm-dropcursor{background:var(--accent,currentColor);border-radius:1px}
|
|
506
|
+
.sm-rtcard__grip{flex:none;display:grid;place-items:center;width:14px;margin-left:-4px;color:var(--text-subtle,currentColor);opacity:.7}
|
|
507
|
+
.sm-rtcard__head:hover .sm-rtcard__grip{opacity:1}
|
|
497
508
|
/* The canvas editor root: ProseMirror's base rules (TipTap injects its
|
|
498
509
|
stylesheet into the admin document, never the iframe) and nothing of the
|
|
499
510
|
widget's typography — the site's CSS styles the content. */
|
|
@@ -1068,6 +1079,9 @@ html:has(.sm-admin),body:has(.sm-admin){overscroll-behavior:none}
|
|
|
1068
1079
|
.sm-mediacard__thumb{display:block;aspect-ratio:4/3;border-radius:var(--radius-sm);overflow:hidden;background:var(--surface-inset)}
|
|
1069
1080
|
.sm-mediacard__img{width:100%;height:100%;object-fit:cover;display:block}
|
|
1070
1081
|
.sm-thumb--chip{display:grid;place-items:center;width:100%;height:100%;font-weight:600;color:var(--text-muted);font-size:var(--text-sm)}
|
|
1082
|
+
/* A video with no poster: the browser's own opening frame over the chip, which stays where it cannot draw one. */
|
|
1083
|
+
.sm-thumb--video{position:relative}
|
|
1084
|
+
.sm-thumb--video>video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;pointer-events:none}
|
|
1071
1085
|
.sm-mediacard__name{font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1072
1086
|
.sm-mediacard__meta{font-size:var(--text-sm);color:var(--text-muted)}
|
|
1073
1087
|
.sm-media{display:flex;flex-direction:column;gap:6px}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SMOODLY_VERSION = "0.0.
|
|
1
|
+
export declare const SMOODLY_VERSION = "0.0.58";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/write-version.mjs from package.json — do not edit.
|
|
2
|
-
export const SMOODLY_VERSION = "0.0.
|
|
2
|
+
export const SMOODLY_VERSION = "0.0.58";
|
package/package.json
CHANGED