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.
@@ -1,8 +1,9 @@
1
- import type { ReactElement } from "react";
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
- export function AssetThumb({ asset, className }) {
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, 320), alt: "", loading: "lazy" });
8
- if (asset.kind === "video") {
9
- return asset.posterId
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, gapcursor: false, trailingNode: 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: [_jsx("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%" } }) }), 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
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
  }
@@ -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.57";
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.57";
2
+ export const SMOODLY_VERSION = "0.0.58";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoodly",
3
- "version": "0.0.57",
3
+ "version": "0.0.58",
4
4
  "description": "Open-source, code-first CMS for Astro and Next.js: define a component once, it becomes a typed, editable building block.",
5
5
  "license": "MIT",
6
6
  "engines": {