Files
RenewCMS/docs/design/media-field.md
T

13 KiB
Raw Blame History

Media field type

Board: RenewCMS Screens Schema.dc.html. Frames changed: SchemaFrame.dc.html, MediaFrame.dc.html. Spec data: schema-spec.js (states) and media-field-tables.js (tables, appended to the board). No new tokens, no new colors, radius 0.

1. Behaviour

  • New field type media, name "Media", icon lucide:image, description "One image from your media library".
  • Stored value: id of a library image, or null. The file is never copied. The field always shows the current library image.
  • Options: label, key, required. No default, unique, min, max or choices. The inspector hides them.
  • Required: the entry can't be saved while the field is empty (422 invalid_entry, rule required).
  • A saved field's type never changes. Key and type are locked after the first save (existing rule).
  • Adding a required media field to a type with entries is refused, because a media field can't have a default. The review step shows this before the server is asked.
  • Image deleted while entries point at it: the entry keeps the id. The control shows the missing state. Saving is refused only if the field is required.
  • Entries count as uses of an image in Media. Deleting a used image is allowed after a warning.
  • Variants: the editor preview uses the original. No variant select in the field. Nothing about variants in the builder.
  • Galleries (several images) are out of scope and not drawn.

2. New component: MediaFieldControl

Props: label, required, modelValue (image id or null), image (filename, width, height, size, or missing: true), disabled, error. Emits update:modelValue. Opens the picker. Nuxt UI v4: UFormField (label, help, error, required), UButton outline ("Pick an image", "Change image", "Choose another image"), UButton ghost ("Remove", "Clear"), UModal on desktop and bottom UDrawer on mobile for the picker, UTooltip for the list placeholder and the badge, UIcon lucide:image, image-off, unlink, image-plus. The thumbnail is a plain button.

States and measurements (SchemaFrame MCtl):

State Drawing
Empty Dashed box (1px dashed --ui-border-accented), padding 12, 40 px icon square (--ui-bg-elevated), "No image selected.", button "Pick an image" (icon image-plus). Mobile: stacked, 44 px button
Filled Solid 1px --ui-border-accented box, padding 12. Thumbnail 160 × 120 (mobile: full width, 16:9). Filename weight 500, then "1600 × 900 · 240 KB" 12/18 muted. Buttons "Change image" (outline, icon image) and "Remove" (ghost, icon x). Help line "One image from your library."
Hover (desktop) Border --ui-text-muted. Bottom bar 28 px, --ui-bg-inverted with --ui-text-inverted, icon image 14 and "Change image". No hover on mobile
Focus 2 px --ui-primary outline, offset 2, on the thumbnail button
Error Box border --ui-error plus inset 1px --ui-error, error line with circle-alert 14: "Pick an image for Thumbnail." Focus ring on the first button (Pick an image, or Choose another image)
Missing Dashed 160 × 120 (mobile 16:9) placeholder with image-off 24. Text "This image is no longer in the library" (weight 500). Help: optional "You can still save. The entry keeps the link until you change it." / required "Thumbnail is required. Choose another image to save." Buttons "Choose another image" (outline) and "Clear" (ghost)
Saving Thumbnail at 60%, buttons at 40%, all form fields disabled, Save reads "Saving…"
Read-only (fields changed) Same as saving, plus warning alert "The fields of Events changed" and button "Reload form"

Label is the field label, plus " *" when required (existing form rule; "(optional)" is not used).

3. Per screen

3.1 Schema list (group 1), state "Custom type: read-only view"

Fields table of a custom type (Label, Key, Type, Rules) in the built-in table style. Thumbnail row: image icon, key thumbnail, type "Media", rules "One image from the library". Header "Events" with badge "Read only" (lock). Alert: "You can read these fields here" / "Only an owner or admin can change a content type." The built-in Media type in Schema is unchanged. Decision needed: no route shows a custom type read-only today.

3.2 Schema builder (group 2), 3 states

  • "Media field selected": saved Thumbnail row selected (index 7). Inspector: Label, Key (locked, "Used in the API, so it can't change."), Type tiles (seven, Media selected, others dimmed with "Can't change after saving"), Required switch ("Required" / "An entry can't be saved with this field empty."). Hidden for media: Unique values, Default value, limits, choices. Hint (info-soft block, info icon): "Holds one image from the media library. Entries link to the image instead of keeping a copy, so they always show its current version."
  • "New media field, seven types": new row with "New" badge, key editable ("The name used in the API. Starts with a letter. Lowercase letters, digits and underscores."), type tiles all enabled. State chip "Unsaved changes".
  • "Media field, required": row shows the existing Required marker.
  • Field row: grip, 28 px icon square (image), label, key (mono), badges. Mobile: inspector in the existing 92% sheet (title "Field: Thumbnail", footer "Done").
  • Type tile grid now has seven tiles (3 columns desktop inspector, 2 in sheets).

3.3 Review (group 3), 2 states

Dialog "Review changes to Events" / "Apply them to save the content type." Summary "Add 1 field" / "Events has 12 entries. Each one is checked against the new fields when you apply." The info bar in the builder behind reads "12 entries use this content type…".

  • Refused: row "Add Thumbnail", warning flag "Can't be applied", text "Media, required. A media field can't have a default, so the 12 existing entries would have no image. Make Thumbnail optional to add it." Note "Back to editing keeps your other changes." Buttons "Back to editing", "Apply changes" (disabled). This departs from the existing review (Apply is never disabled): decision.
  • Clean: row "Add Thumbnail", "Media, optional. 12 entries are unaffected." Apply enabled.

3.4 Field types (group 4), state "Media"

Per-type frame like the others: header icon image, "Media", "One image from your media library", type: "media". Columns: inspector options, entry form control (filled with hover, then empty), list cell (Filled thumbnail, Empty "–", Image missing), errors by rule, filter (later). Errors by rule: required 422 "Pick an image for Thumbnail."; type 422 "Thumbnail must be an image from the library."; min, max, options, unique: "Not used for media." Filter widget: "Thumbnail: has an image" with Any / Has an image / No image, tagged Later.

3.5 Entry list (group 5), 3 states

Columns shown: Title, Thumbnail, Starts on, Category (user choice). Header "Thumbnail", no sort icon, column 80 px. Cell: 40 px square thumbnail; "–" when empty; dashed 40 px square with unlink icon, accessible name "Image no longer in the library", when the image is gone. Row height stays 48 px (thumbnail fits with 4 px above and below). Mobile card: 32 px thumbnail leads the card (empty: "–", missing: dashed square), card min height 68 px. Columns menu lists Thumbnail with the image icon ("4 of 6", "Choose up to 6 fields to show. 4 selected." on mobile). Tooltip on the missing placeholder (desktop): "Image no longer in the library".

3.6 Entry editor (group 6), 10 states

Control sits after Description, at the end of the form (schema order; see conflicts). States: empty optional, empty required, filled, hover, focus, validation error (status chip "Not saved"), image no longer in the library, missing + required (error), saving, read-only. The board scrolls the form so the control is visible in the frame; the real form is not scrolled.

3.7 Picker (group 10), 8 states, MediaFrame field mode

Existing picker (Upload and Library tabs, single selection, search "Search by filename", "Newest first · 48 images", Load more, 4 columns desktop and 3 on mobile, square tiles). Differences:

  • Title "Choose an image".
  • Primary "Use this image" (disabled until a tile is chosen). When "No image" is chosen it reads "Remove image".
  • No alt text field, no variant select. Selected strip shows thumbnail, filename, "dimensions · size".
  • Current image tile badge "Current" (replaces "In this article").
  • Optional fields only: row "No image" / "Leave this field empty." at the top of the Library tab. Selected: sand fill, primary border, check.
  • Empty: "Your library is empty" / "Images you upload appear here, ready to use in any article or entry." / "Upload an image".
  • Failed: "Couldn't load your library" / "Check your connection and try again." / "Try again".
  • Loading: skeleton tiles after 150 ms.
  • Upload tab: unchanged flow (crop, resize); the new image lands in the library and is selected.
  • Desktop dialog 640 wide, mobile bottom sheet.

3.8 Media library (group 11), 4 states

  • Badge "Used in N" = articles + entries. The tooltip (hover or focus) reads "Used in entries: 3" and, if articles also use it, "Used in articles: 2" on the first line. Example: bathroom-tiles.png, 3 entries, "Used in 3".
  • List view: same badge in the list row and mobile card.
  • Detail panel (lime-plaster.jpg, 2 articles, 5 entries): preview is shorter when the image has entries; existing "Used in 2 articles" list; new section "Used in entries" with up to 3 rows (type name muted, entry title as link, arrow-up-right) and "and 2 more". When there are entries the empty-articles line reads "No article shows it." (not "safe to delete").
  • Delete: title "Delete an image that's in use?" (existing in-use dialog), body "lime-plaster.jpg" is used in 5 entries and 2 articles. Those entry fields will show "This image is no longer in the library", and the articles will show a broken image. This can't be undone. with the entries and articles lists; "Cancel" and destructive "Delete image".

4. Copy added (exact)

Media · One image from your media library · Holds one image from the media library. Entries link to the image instead of keeping a copy, so they always show its current version. · No image selected. · Pick an image · Change image · Remove · Choose another image · Clear · One image from your library. · This image is no longer in the library · You can still save. The entry keeps the link until you change it. · Thumbnail is required. Choose another image to save. · Pick an image for Thumbnail. · Thumbnail must be an image from the library. · Not used for media. · Image no longer in the library · Choose an image · Use this image · Remove image · Current · No image · Leave this field empty. · Used in entries · and 2 more · Used in entries: 3 · Can't be applied · Media, required. A media field can't have a default, so the 12 existing entries would have no image. Make Thumbnail optional to add it. · Media, optional. 12 entries are unaffected. · The Thumbnail field was changed after you opened this form. Reload the form to continue. What you typed here can't be saved against the old fields. Sentence case, "image" never "asset", buttons are verbs.

5. API and backend

  • Schema API accepts type media with label, key, required. Value: image id or null.
  • 422 invalid_entry: required → "Pick an image for Thumbnail."; type (id not in the library on a changed value) → "Thumbnail must be an image from the library."
  • Entry and list responses include, per media field, filename, width, height, size and a thumbnail URL, or missing: true.
  • GET /v1/media/{id}/usage returns articles and entries (content type name, key, entry id, title), a total and the first few items. GET /v1/media/ list items carry the combined use count.
  • DELETE /v1/media/{id} stays allowed for used images. Entries keep the id.
  • A new required media field on a type with entries is refused (schema_incompatible). The client shows the refusal first.

HTTP to copy (picker and field), from states.md: 400 "That request wasn't valid. Check the fields and try again."; 401/403 global; 404 "That item no longer exists."; 409 "This changed somewhere else. Reload and try again."; 413 "That file is too large."; 429 "Too many requests. Try again in a few minutes."; 500/network "Something went wrong on our side. Nothing was changed. Try again in a moment."

6. Accessibility

  • Control is role="group" labelled by the field label. Required: aria-required on the main button.
  • Names: "Pick an image for Thumbnail"; thumbnail "Change image, lime-plaster-workshop.jpg, 1600 by 900, 240 KB"; "Remove image from Thumbnail"; "Clear Thumbnail"; missing placeholder role="img" "Image no longer in the library".
  • Error: error text has an id. The main button gets aria-invalid="true" and aria-describedby (error id, then help id). Focus moves to the first invalid control on a refused save.
  • Order: label, thumbnail, Change image, Remove, help or error. The thumbnail duplicates Change image; use tabindex="-1" on it if one stop is preferred.
  • One polite live region: "Image selected: lime-plaster-workshop.jpg", "Image removed from Thumbnail.", "Thumbnail cleared."
  • Focus: Use this image → Change image. Remove or Clear → Pick an image. Cancel or Esc → the opener.
  • Picker: tiles are buttons with aria-pressed, names "stone-steps.jpg, 310 KB, Current". "No image" has aria-pressed. "Use this image" uses aria-disabled.
  • Contrast: Current badge and selected rows use --rc-on-sand; hover bar 18.9 (inverted pair). 44 px targets on mobile. No hover on mobile.