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:
64
src/App.css
64
src/App.css
@@ -95,8 +95,8 @@
|
||||
.dump-post-header,
|
||||
.dump-rich-content,
|
||||
.form-page-header,
|
||||
.dump-edit-preview,
|
||||
.form-page .form,
|
||||
.dump-edit-panel,
|
||||
.dump-edit-footer,
|
||||
.playlist-detail-header,
|
||||
.comment-node-inner {
|
||||
box-shadow: var(--panel-ring), var(--card-shadow);
|
||||
@@ -3032,20 +3032,38 @@ body.has-player .chat-fab {
|
||||
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) {
|
||||
.form-page--two-col {
|
||||
.form-page--two-col,
|
||||
.dump-edit-form {
|
||||
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;
|
||||
}
|
||||
.form-page--two-col .dump-edit-preview {
|
||||
border-radius: 0 0 0 var(--radius-xl);
|
||||
/* "Why?" soaks up whatever height the source column adds, so a tall
|
||||
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
|
||||
form's bottom-left stays square where it meets the preview. */
|
||||
.form-page.form-page--two-col .form {
|
||||
border-radius: 0 0 var(--radius-xl) 0;
|
||||
.dump-edit-panel--details .mention-textarea-wrap {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
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;
|
||||
}
|
||||
|
||||
.dump-edit-preview {
|
||||
.dump-edit-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
min-width: 0;
|
||||
padding: 1.25rem;
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.dump-edit-preview {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dump-edit-footer {
|
||||
padding: 0.875rem 1.25rem;
|
||||
background: var(--color-surface);
|
||||
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 {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
@@ -3097,12 +3133,6 @@ body.has-player .chat-fab {
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -214,41 +214,42 @@ export function DumpEdit() {
|
||||
<h1 className="form-page-title">{dump.title}</h1>
|
||||
</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
|
||||
key={dump.id}
|
||||
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}
|
||||
thumbUploading={thumbUploading}
|
||||
onThumbnailChange={handleThumbnailChange}
|
||||
@@ -272,6 +273,7 @@ export function DumpEdit() {
|
||||
|
||||
interface DumpEditFormProps {
|
||||
dump: Dump;
|
||||
preview: React.ReactNode;
|
||||
currentThumbnailSrc: string | null;
|
||||
thumbUploading: boolean;
|
||||
onThumbnailChange: (file: File) => void;
|
||||
@@ -290,6 +292,7 @@ interface EditValues {
|
||||
|
||||
function DumpEditForm({
|
||||
dump,
|
||||
preview,
|
||||
currentThumbnailSrc,
|
||||
thumbUploading,
|
||||
onThumbnailChange,
|
||||
@@ -367,111 +370,123 @@ function DumpEditForm({
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<form className="form" onSubmit={onSubmit}>
|
||||
<FormError title={t`Could not save`} />
|
||||
<form className="form dump-edit-form" onSubmit={onSubmit}>
|
||||
{
|
||||
/* 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">
|
||||
<span className="form-label">
|
||||
<Trans>Thumbnail</Trans>
|
||||
</span>
|
||||
<div className="dump-edit-thumbnail-row">
|
||||
<ImagePicker
|
||||
src={currentThumbnailSrc}
|
||||
size={64}
|
||||
onChange={onThumbnailChange}
|
||||
uploading={thumbUploading}
|
||||
/>
|
||||
{dump.thumbnailMime && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn-border"
|
||||
onClick={onThumbnailReset}
|
||||
disabled={thumbUploading}
|
||||
>
|
||||
<Trans>Reset to default</Trans>
|
||||
</button>
|
||||
)}
|
||||
<div className="form-field">
|
||||
<span className="form-label">
|
||||
<Trans>Thumbnail</Trans>
|
||||
</span>
|
||||
<div className="dump-edit-thumbnail-row">
|
||||
<ImagePicker
|
||||
src={currentThumbnailSrc}
|
||||
size={64}
|
||||
onChange={onThumbnailChange}
|
||||
uploading={thumbUploading}
|
||||
/>
|
||||
{dump.thumbnailMime && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn-border"
|
||||
onClick={onThumbnailReset}
|
||||
disabled={thumbUploading}
|
||||
>
|
||||
<Trans>Reset to default</Trans>
|
||||
</button>
|
||||
)}
|
||||
</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>
|
||||
|
||||
<TextField<EditValues>
|
||||
name="title"
|
||||
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 }}
|
||||
/>
|
||||
<div className="dump-edit-panel dump-edit-panel--details">
|
||||
<FormError title={t`Could not save`} />
|
||||
|
||||
{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}
|
||||
/>
|
||||
<TextField<EditValues>
|
||||
name="title"
|
||||
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 }}
|
||||
/>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
)}
|
||||
|
||||
<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>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user