v3: dump edit balances its two columns — the dump itself (preview, refresh, thumbnail, URL or replacement file) on the left, its presentation (title, why, categories, visibility) on the right, actions in a full-width footer bar so both columns end level; "Why?" stretches to absorb a taller preview on desktop

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-09-19 21:32:18 +00:00
parent e9ebc3fee6
commit 084ebcb627
2 changed files with 193 additions and 148 deletions

View File

@@ -95,8 +95,8 @@
.dump-post-header, .dump-post-header,
.dump-rich-content, .dump-rich-content,
.form-page-header, .form-page-header,
.dump-edit-preview, .dump-edit-panel,
.form-page .form, .dump-edit-footer,
.playlist-detail-header, .playlist-detail-header,
.comment-node-inner { .comment-node-inner {
box-shadow: var(--panel-ring), var(--card-shadow); box-shadow: var(--panel-ring), var(--card-shadow);
@@ -3032,20 +3032,38 @@ body.has-player .chat-fab {
animation: page-enter 0.2s ease both; animation: page-enter 0.2s ease both;
} }
/* The edit form spans the page grid and lays out its own panels: the dump
itself (preview, thumbnail, source) beside its presentation (title, why,
categories, visibility), with a full-width action bar underneath so both
columns end on the same line. Only the header and footer are rounded. */
.dump-edit-form {
display: grid;
grid-template-columns: 1fr;
gap: inherit;
}
@media (min-width: 860px) { @media (min-width: 860px) {
.form-page--two-col { .form-page--two-col,
.dump-edit-form {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} }
.form-page--two-col .form-page-header { .form-page--two-col .form-page-header,
.dump-edit-form,
.dump-edit-footer {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
.form-page--two-col .dump-edit-preview { /* "Why?" soaks up whatever height the source column adds, so a tall
border-radius: 0 0 0 var(--radius-xl); preview doesn't leave a hole under the details. */
.dump-edit-panel--details .form-field:has(.mention-textarea-wrap) {
flex: 1 1 auto;
} }
/* Higher specificity than the generic `.form-page .form` below so the .dump-edit-panel--details .mention-textarea-wrap {
form's bottom-left stays square where it meets the preview. */ flex: 1 1 auto;
.form-page.form-page--two-col .form { display: flex;
border-radius: 0 0 var(--radius-xl) 0; flex-direction: column;
}
.dump-edit-panel--details .mention-textarea-wrap textarea {
flex: 1 1 auto;
} }
} }
@@ -3074,13 +3092,31 @@ body.has-player .chat-fab {
word-break: break-word; word-break: break-word;
} }
.dump-edit-preview { .dump-edit-panel {
display: flex;
flex-direction: column;
gap: 1rem;
min-width: 0;
padding: 1.25rem; padding: 1.25rem;
background: var(--color-surface); background: var(--color-surface);
}
.dump-edit-preview {
overflow: hidden; overflow: hidden;
}
.dump-edit-footer {
padding: 0.875rem 1.25rem;
background: var(--color-surface);
border-radius: 0 0 var(--radius-xl) var(--radius-xl); border-radius: 0 0 var(--radius-xl) var(--radius-xl);
} }
.dump-edit-footer .form-actions {
padding-top: 0;
border-top: 0;
margin-top: 0;
}
.dump-edit-refresh { .dump-edit-refresh {
margin-top: 0.75rem; margin-top: 0.75rem;
} }
@@ -3097,12 +3133,6 @@ body.has-player .chat-fab {
gap: 0.75rem; gap: 0.75rem;
} }
.form-page .form {
background: var(--color-surface);
border-radius: 0 0 var(--radius-xl) var(--radius-xl);
padding: 1.25rem;
}
.form-actions { .form-actions {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -214,41 +214,42 @@ export function DumpEdit() {
<h1 className="form-page-title">{dump.title}</h1> <h1 className="form-page-title">{dump.title}</h1>
</div> </div>
<div className="dump-edit-preview">
{dump.kind === "file"
? <FilePreview dump={dump} />
: dump.richContent
? <RichContentCard richContent={dump.richContent} />
: dump.url && (
<a
href={dump.url}
target="_blank"
rel="noopener noreferrer"
className="dump-url-link"
>
{dump.url}
</a>
)}
{dump.kind === "url" && (
<button
type="button"
className="btn-secondary dump-edit-refresh"
onClick={handleRefreshMetadata}
disabled={refreshing}
>
{refreshing
? <Trans>Refreshing…</Trans>
: <Trans>Refresh metadata</Trans>}
</button>
)}
{refreshError && (
<p className="dump-edit-refresh-error">{refreshError}</p>
)}
</div>
<DumpEditForm <DumpEditForm
key={dump.id} key={dump.id}
dump={dump} dump={dump}
preview={
<div className="dump-edit-preview">
{dump.kind === "file"
? <FilePreview dump={dump} />
: dump.richContent
? <RichContentCard richContent={dump.richContent} />
: dump.url && (
<a
href={dump.url}
target="_blank"
rel="noopener noreferrer"
className="dump-url-link"
>
{dump.url}
</a>
)}
{dump.kind === "url" && (
<button
type="button"
className="btn-secondary dump-edit-refresh"
onClick={handleRefreshMetadata}
disabled={refreshing}
>
{refreshing
? <Trans>Refreshing…</Trans>
: <Trans>Refresh metadata</Trans>}
</button>
)}
{refreshError && (
<p className="dump-edit-refresh-error">{refreshError}</p>
)}
</div>
}
currentThumbnailSrc={currentThumbnailSrc} currentThumbnailSrc={currentThumbnailSrc}
thumbUploading={thumbUploading} thumbUploading={thumbUploading}
onThumbnailChange={handleThumbnailChange} onThumbnailChange={handleThumbnailChange}
@@ -272,6 +273,7 @@ export function DumpEdit() {
interface DumpEditFormProps { interface DumpEditFormProps {
dump: Dump; dump: Dump;
preview: React.ReactNode;
currentThumbnailSrc: string | null; currentThumbnailSrc: string | null;
thumbUploading: boolean; thumbUploading: boolean;
onThumbnailChange: (file: File) => void; onThumbnailChange: (file: File) => void;
@@ -290,6 +292,7 @@ interface EditValues {
function DumpEditForm({ function DumpEditForm({
dump, dump,
preview,
currentThumbnailSrc, currentThumbnailSrc,
thumbUploading, thumbUploading,
onThumbnailChange, onThumbnailChange,
@@ -367,111 +370,123 @@ function DumpEditForm({
return ( return (
<FormProvider {...form}> <FormProvider {...form}>
<form className="form" onSubmit={onSubmit}> <form className="form dump-edit-form" onSubmit={onSubmit}>
<FormError title={t`Could not save`} /> {
/* Left: the dumped thing itself — what it looks like and where it
comes from. Right: how it's presented and shared. */
}
<div className="dump-edit-panel dump-edit-panel--source">
{preview}
<div className="form-field"> <div className="form-field">
<span className="form-label"> <span className="form-label">
<Trans>Thumbnail</Trans> <Trans>Thumbnail</Trans>
</span> </span>
<div className="dump-edit-thumbnail-row"> <div className="dump-edit-thumbnail-row">
<ImagePicker <ImagePicker
src={currentThumbnailSrc} src={currentThumbnailSrc}
size={64} size={64}
onChange={onThumbnailChange} onChange={onThumbnailChange}
uploading={thumbUploading} uploading={thumbUploading}
/> />
{dump.thumbnailMime && ( {dump.thumbnailMime && (
<button <button
type="button" type="button"
className="btn-border" className="btn-border"
onClick={onThumbnailReset} onClick={onThumbnailReset}
disabled={thumbUploading} disabled={thumbUploading}
> >
<Trans>Reset to default</Trans> <Trans>Reset to default</Trans>
</button> </button>
)} )}
</div>
</div> </div>
{dump.kind === "url"
? (
<TextField<EditValues>
name="url"
type="url"
label={t`URL`}
placeholder="https://..."
rules={{ required: true }}
/>
)
: (
<div className="form-field">
<span className="form-label">
<Trans>Replace file</Trans>
</span>
<p className="dump-file-notice">
<strong>{dump.fileName}</strong>
{dump.fileSize != null && ` — ${formatBytes(dump.fileSize)}`}
</p>
<FileField<EditValues>
name="newFile"
hint={t`Drop a replacement here`}
showLimit={false}
/>
</div>
)}
</div> </div>
<TextField<EditValues> <div className="dump-edit-panel dump-edit-panel--details">
name="title" <FormError title={t`Could not save`} />
label={t`Title`}
labelAction={metaTitle
? (
<button
type="button"
className="form-label-action"
onClick={() =>
form.setValue("title", metaTitle, { shouldDirty: true })}
disabled={currentTitle.trim() === metaTitle}
>
<Trans>Reset to default</Trans>
</button>
)
: undefined}
maxLength={VALIDATION.DUMP_TITLE_MAX}
rules={{ required: true }}
/>
{dump.kind === "url" <TextField<EditValues>
? ( name="title"
<TextField<EditValues> label={t`Title`}
name="url" labelAction={metaTitle
type="url" ? (
label={t`URL`} <button
placeholder="https://..." type="button"
rules={{ required: true }} className="form-label-action"
/> onClick={() =>
) form.setValue("title", metaTitle, { shouldDirty: true })}
: ( disabled={currentTitle.trim() === metaTitle}
<div className="form-field"> >
<span className="form-label"> <Trans>Reset to default</Trans>
<Trans>Replace file</Trans> </button>
</span> )
<p className="dump-file-notice"> : undefined}
<strong>{dump.fileName}</strong> maxLength={VALIDATION.DUMP_TITLE_MAX}
{dump.fileSize != null && ` — ${formatBytes(dump.fileSize)}`} rules={{ required: true }}
</p> />
<FileField<EditValues>
name="newFile" <RichTextField<EditValues>
hint={t`Drop a replacement here`} name="comment"
showLimit={false} label={t`Why?`}
/> placeholder={t`What makes it worth it?`}
rows={3}
maxLength={VALIDATION.DUMP_COMMENT_MAX}
rules={{ maxLength: VALIDATION.DUMP_COMMENT_MAX }}
/>
<CategorySelect
selected={selectedCategoryIds}
onToggle={toggleCategory}
/>
<VisibilityToggle<EditValues> name="isPublic" />
</div>
<div className="dump-edit-footer">
<div className="form-actions">
<button
type="button"
onClick={onRequestDelete}
className="btn-danger"
>
<Trans>Delete dump</Trans>
</button>
<div className="form-actions-right">
<Link to={dumpUrl(dump)} className="form-cancel">
<Trans>Cancel</Trans>
</Link>
<SubmitButton>
<Trans>Save</Trans>
</SubmitButton>
</div> </div>
)}
<RichTextField<EditValues>
name="comment"
label={t`Why?`}
placeholder={t`What makes it worth it?`}
rows={3}
maxLength={VALIDATION.DUMP_COMMENT_MAX}
rules={{ maxLength: VALIDATION.DUMP_COMMENT_MAX }}
/>
<CategorySelect
selected={selectedCategoryIds}
onToggle={toggleCategory}
/>
<VisibilityToggle<EditValues> name="isPublic" />
<div className="form-actions">
<button
type="button"
onClick={onRequestDelete}
className="btn-danger"
>
<Trans>Delete dump</Trans>
</button>
<div className="form-actions-right">
<Link to={dumpUrl(dump)} className="form-cancel">
<Trans>Cancel</Trans>
</Link>
<SubmitButton>
<Trans>Save</Trans>
</SubmitButton>
</div> </div>
</div> </div>
</form> </form>