Media library
Library and editor
No media assets. Upload an image to begin.
Loading media…
Media could not be loaded.
Product media
Trail pack frontPrimary1600 × 900 · PNG
Trail pack detail512 × 512 · PNG
Packaging draft180 × 180 · PNG
0 selected
Describe the selected asset for people who cannot see it.
Choose a local image; this provider-free example does not upload it.
open System
open FSharp.ViewEngine
open Acme.Components.Primitives
open Acme.Components.Application
open type Html
open type Svg
open type Datastar
type ShellDestination =
| LedgerHome
| LedgerAccounts
| LedgerAccount of int
| LedgerTransaction of int
| LedgerCreateAccount
| LedgerReports
| LedgerSettings
| TreasuryHome
| TreasuryPeriod of string
| TreasuryTransactions
| TreasuryPayees
| TreasuryAccounts
let private shellDestinationKey = function
| LedgerHome -> "ledger-home"
| LedgerAccounts -> "ledger-accounts"
| LedgerAccount id -> $"ledger-account-{id}"
| LedgerTransaction id -> $"ledger-transaction-{id}"
| LedgerCreateAccount -> "ledger-create-account"
| LedgerReports -> "ledger-reports"
| LedgerSettings -> "ledger-settings"
| TreasuryHome -> "treasury-home"
| TreasuryPeriod region -> $"treasury-period-{region.ToLowerInvariant()}"
| TreasuryTransactions -> "treasury-transactions"
| TreasuryPayees -> "treasury-payees"
| TreasuryAccounts -> "treasury-accounts"
let shellDestinationUrl destination =
$"/components/page-examples/account-management?destination={shellDestinationKey destination}"
let mediaLibraryExample =
let library =
MediaLibrary.create "product-media" "Product media" "assetIds"
[ MediaAsset.create "trail-front" "Trail pack front" "/social-card.png" "Blue trail pack shown from the front" LedgerAccounts
|> MediaAsset.withDetail "1600 × 900 · PNG"
|> MediaAsset.primary
MediaAsset.create "trail-detail" "Trail pack detail" "/android-chrome-512x512.png" "Close view of the trail pack straps" LedgerAccounts
|> MediaAsset.withDetail "512 × 512 · PNG"
MediaAsset.create "trail-draft" "Packaging draft" "/apple-touch-icon.png" "Draft packaging artwork" LedgerAccounts
|> MediaAsset.withDetail "180 × 180 · PNG" ]
|> MediaLibrary.withSelected [ "trail-front" ]
|> MediaLibrary.render shellDestinationUrl
BulkActions.create "media-bulk-actions" "product-media" "Selected media actions"
[ BulkAction.create "Set primary" "Primary media updated in this resettable demo." (fun keys -> "document.getElementById('media-edit-feedback').textContent = 'Primary asset: ' + " + keys + ".at(0)")
|> BulkAction.primary
BulkAction.create "Remove" "Selected media removed in this resettable demo." (fun keys -> "document.getElementById('media-edit-feedback').textContent = 'Remove requested for: ' + " + keys + ".join(', ')")
|> BulkAction.destructive ]
library
|> BulkActions.render
|> fun libraryWithActions ->
div {
_dataSignals "{mediastate: 'ready'}"
_class "grid gap-4"
div {
_class "flex flex-wrap gap-2"
for state, label in [ "empty", "Show empty"; "pending", "Simulate loading"; "error", "Simulate error"; "ready", "Restore media" ] do
button {
_type "button"
_dataOn ("click", $"$mediastate = '{state}'")
_class "rounded-[var(--fve-radius-control)] px-3 py-2 text-sm font-semibold ring-1 ring-[var(--fve-border)]"
label
}
}
p {
_dataShow "$mediastate == 'empty'"
_class "rounded-[var(--fve-radius-panel)] bg-[var(--fve-neutral-subtle)] p-4 text-sm text-[var(--fve-muted-text)]"
"No media assets. Upload an image to begin."
}
p {
_dataShow "$mediastate == 'pending'"
_role "status"
_ariaLive "polite"
_class "rounded-[var(--fve-radius-panel)] bg-[var(--fve-neutral-subtle)] p-4 text-sm text-[var(--fve-muted-text)]"
"Loading media…"
}
div {
_dataShow "$mediastate == 'error'"
_role "alert"
_class "flex flex-wrap items-center justify-between gap-3 rounded-[var(--fve-radius-panel)] bg-[var(--fve-critical-subtle)] p-4 text-sm text-[var(--fve-critical-text)]"
span { "Media could not be loaded." }
button { _type "button"; _dataOn ("click", "$mediastate = 'ready'"); _class "rounded-[var(--fve-radius-control)] px-3 py-2 font-semibold ring-1 ring-[var(--fve-critical-ring)]"; "Retry media" }
}
div {
_dataShow "$mediastate == 'ready'"
_class "grid gap-4"
libraryWithActions
Textarea.create "altText" "Alt text"
|> Textarea.withId "media-alt-text"
|> Textarea.withValue "Blue trail pack shown from the front"
|> Textarea.withDescription "Describe the selected asset for people who cannot see it."
|> Textarea.render
Button.create "Save alt text"
|> Button.withAttributes [ _dataOn ("click", "(() => { const value = document.getElementById('media-alt-text').value; document.querySelectorAll('#product-media input:checked').forEach(input => input.closest('li').querySelector('img').alt = value); document.getElementById('media-edit-feedback').textContent = 'Alt text updated for selected media.' })()") ]
|> Button.render
FileSelection.create "media-replacement" "replacementImage" "Replacement image"
|> FileSelection.withDescription "Choose a local image; this provider-free example does not upload it."
|> FileSelection.withAccept "image/*"
|> FileSelection.render
Button.create "Apply demo replacement"
|> Button.withAttributes [ _dataOn ("click", "document.querySelectorAll('#product-media input:checked').forEach(input => input.closest('li').querySelector('img').src = '/favicon-32x32.png'); document.getElementById('media-edit-feedback').textContent = 'Demo replacement applied to selected media.'") ]
|> Button.render
p { _id "media-edit-feedback"; _role "status"; _ariaLive "polite"; _class "text-sm text-[var(--fve-muted-text)]" }
}
}