https://fve.meiermade.com/components/page-examples/scheduling?state=error&item=lesson-201-default&view=month This view could not be loaded
Your selection is preserved. Try loading it again.
open System
open FSharp.ViewEngine
open Acme.Components.Primitives
open Acme.Components.Application
open Acme.Components.Documentation
open type Html
open type Svg
open type Datastar
type ExamplePage =
| DependencyGraph
| ExecutionDetail
| FinancialReporting
| Messaging
| Operations
| Scheduling
| MediaManagement
type ReviewState =
| Setup
| Ready
| Loading
| Empty
| Failed
type ExampleQuery =
{ state: ReviewState
item: string
view: string
range: string }
let defaultQuery =
{ state = Ready
item = ""
view = ""
range = "" }
let slug =
function
| DependencyGraph -> "dependency-graph"
| ExecutionDetail -> "execution-detail"
| FinancialReporting -> "financial-reporting"
| Messaging -> "messaging"
| Operations -> "operations-dashboard"
| Scheduling -> "scheduling"
| MediaManagement -> "media-management"
let title =
function
| DependencyGraph -> "Dependency graph"
| ExecutionDetail -> "Execution detail"
| FinancialReporting -> "Financial reporting"
| Messaging -> "Messaging"
| Operations -> "Operations dashboard"
| Scheduling -> "Scheduling"
| MediaManagement -> "Media management"
let url page =
"/components/page-examples/" + slug page
let stateKey =
function
| Setup -> "setup"
| Ready -> "ready"
| Loading -> "loading"
| Empty -> "empty"
| Failed -> "error"
let queryUrl page query =
let pairs =
[ "state", stateKey query.state
"item", query.item
"view", query.view
"range", query.range ]
url page
+ "?"
+ (pairs
|> List.filter (snd >> String.IsNullOrEmpty >> not)
|> List.map (fun (key, value) -> key + "=" + Uri.EscapeDataString value)
|> String.concat "&")
let link (href: string) (label: string) : HtmlElement =
a {
_href href
_class
"font-medium text-[var(--fve-brand-text)] underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-[var(--fve-brand-ring)]"
label
}
// Ordinary consumer-authored anchors for compact page-header destinations.
let pageLink (href:string) (label:string) =
a {
_href href
_class "inline-flex min-h-8 items-center justify-center rounded-[var(--fve-radius-control)] bg-[var(--fve-surface)] px-3 py-1.5 text-sm font-medium leading-5 text-[var(--fve-text)] ring-1 ring-inset ring-[var(--fve-border)] hover:bg-[var(--fve-surface-hover)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--fve-brand-ring)]"
label
}
let section (heading: string) (content: HtmlElement) =
Section.create (SectionHeader.create heading |> SectionHeader.withDivider) content
|> Section.render
let details (fields: (string * string) list) =
DescriptionList.create [ for label, value in fields -> DescriptionListItem.text label value ]
|> DescriptionList.withColumns DescriptionListColumns.Three
|> DescriptionList.render
let reviewStates page =
(if page = Operations then [ Setup, "Setup" ] else [])
@ [ Ready, "Populated"; Loading, "Loading"; Empty, "Empty"; Failed, "Error" ]
let stateContent page query content =
match query.state with
| Setup
| Ready -> content
| Loading ->
div {
_class "p-8"
LoadingIndicator.create ("Loading " + (title page).ToLowerInvariant())
|> LoadingIndicator.withVisibleLabel
|> LoadingIndicator.render
}
| Empty ->
let heading, description, action =
match page with
| DependencyGraph -> "No dependencies found", "There are no dependencies for this selection.", "View dependencies"
| ExecutionDetail -> "No execution selected", "Choose an execution to inspect its timing and logs.", "View latest execution"
| FinancialReporting -> "No activity for this period", "Choose a period with recorded balances and transactions.", "View recent activity"
| Messaging -> "No messages in this conversation", "Choose a conversation to read its messages.", "View conversations"
| Operations -> "No sessions scheduled", "Check the schedule for upcoming sessions.", "View schedule"
| Scheduling -> "No sessions in this period", "Return to the current period to see upcoming sessions.", "View current schedule"
| MediaManagement -> "No media in this selection", "Browse the library to choose an asset.", "Browse media"
EmptyState.create heading description
|> EmptyState.withActions (link (queryUrl (if page = Operations then Scheduling else page) { query with state = Ready }) action)
|> EmptyState.render
| Failed ->
Notice.create
"workspace-error"
"This view could not be loaded"
(p { "Your selection is preserved. Try loading it again." })
|> Notice.withColor NoticeColor.Error
|> Notice.withActions (link (queryUrl page { query with state = Ready }) "Try again")
|> Notice.render
/// Place durable workspace identity above the scrollable destination list.
let sideNavWorkspace (name: string) =
div {
p {
_class "text-xs font-semibold uppercase tracking-wide text-[var(--fve-muted-text)]"
"Workspace"
}
p {
_class "mt-1 truncate text-sm font-semibold"
name
}
}
let workspace page heading subtitle canvas (actions:HtmlElement list) content =
let product, workspaceName, destinations =
match page with
| DependencyGraph
| ExecutionDetail ->
"Relay", "Customer analytics", [ DependencyGraph, "Dependencies"; ExecutionDetail, "Executions" ]
| FinancialReporting -> "Ledger", "Meier Made", [ FinancialReporting, "Overview" ]
| Messaging -> "Gather", "Northwind Outdoor", [ Messaging, "Messages" ]
| _ ->
"Fieldwork", "Northwind Outdoor", [ Operations, "Overview"; Scheduling, "Schedule"; MediaManagement, "Media" ]
let items =
[ for destination, label in destinations -> SideNavItem.create destination label ]
let navigation =
SideNav.create
(slug page + "-nav")
(product + " navigation")
(SideNavHeader.create product)
[ SideNavSection.ungrouped items ]
|> SideNav.withCurrent page
|> SideNav.withWidth SideNavWidth.Narrow
|> SideNav.withContext (sideNavWorkspace workspaceName)
|> SideNav.withFooter (
div {
_class "flex min-w-0 items-center gap-3"
Avatar.create "Andy Meier" "AM" |> Avatar.render
p {
_class "truncate text-sm font-medium"
"Andy Meier"
}
}
)
let header =
PageHeader.create heading
|> PageHeader.withSubtitle subtitle
|> (if List.isEmpty actions then id else PageHeader.withActions (div { _id (slug page + "-actions"); _class "flex flex-wrap items-center gap-3"; for action in actions do action }))
let topBar =
PageTopBar.create ()
|> PageTopBar.withContent (
div {
_class "flex min-w-0 flex-1 items-center"
Breadcrumbs.create
(slug page + "-breadcrumbs")
(product + " breadcrumb")
[ BreadcrumbItem.create page product; BreadcrumbItem.create page heading ]
|> Breadcrumbs.render url
}
)
let body =
Page.create header content
|> Page.withWidth PageWidth.Full
|> Page.withBodyLayout (
if canvas then
PageBodyLayout.Canvas
else
PageBodyLayout.Padded
)
|> Page.render
let framedBody =
div {
_class "flex h-full min-h-0 flex-col"
div { _class "hidden shrink-0 md:block"; topBar |> PageTopBar.render }
div { _class "min-h-0 flex-1"; body }
}
AppShell.create (slug page + "-shell") navigation framedBody
|> AppShell.withTheme (
ComponentsTheme.sky
|> ComponentsTheme.withDensity Density.Compact
|> ComponentsTheme.withControlSize ControlSize.Small)
|> AppShell.withBoundary AppShellBoundary.Container
|> AppShell.asPreview (product + " workspace")
|> AppShell.render url
type ScheduledEvent =
{ id: string
name: string
date: DateOnly
time: string
start: TimeOnly
finish: TimeOnly
people: string
kind: string
photo: string }
let scheduledEvents =
[ { id = "lesson-201"
name = "Coastal trail lesson"
date = DateOnly(2026, 9, 17)
time = "10:00–11:30 AM"
start = TimeOnly(10, 0)
finish = TimeOnly(11, 30)
people = "Maya and Sam · Instructor: Andy Meier"
kind = "Lesson"
photo = "photo-303" }
{ id = "camp-017"
name = "Beginner riding camp"
date = DateOnly(2026, 9, 17)
time = "10:30 AM–12:00 PM"
start = TimeOnly(10, 30)
finish = TimeOnly(12, 0)
people = "3 riders · Instructor: Jordan"
kind = "Camp"
photo = "photo-301" }
{ id = "lesson-202"
name = "Cornering fundamentals"
date = DateOnly(2026, 9, 18)
time = "9:00–10:00 AM"
start = TimeOnly(9, 0)
finish = TimeOnly(10, 0)
people = "Riley · Instructor: Andy Meier"
kind = "Lesson"
photo = "photo-302" }
{ id = "ride-019"
name = "Open track practice"
date = DateOnly(2026, 9, 19)
time = "1:00–3:00 PM"
start = TimeOnly(13, 0)
finish = TimeOnly(15, 0)
people = "6 riders · Supervisor: Jordan"
kind = "Practice"
photo = "photo-304" }
{ id = "return-020"
name = "Equipment return"
date = DateOnly(2026, 9, 24)
time = "4:00–4:30 PM"
start = TimeOnly(16, 0)
finish = TimeOnly(16, 30)
people = "Equipment team · Andy Meier"
kind = "Equipment"
photo = "photo-304" } ]
let eventUrl event =
queryUrl Scheduling { defaultQuery with item = event.id }
let scheduling query =
let event = scheduledEvents |> List.tryFind (fun event -> event.id = query.item)
let content, heading =
match event with
| Some event ->
div {
_class "grid gap-8"
details
[ "Date", event.date.ToString("MMMM d, yyyy", Globalization.CultureInfo.InvariantCulture)
"Time", event.time
"Type", event.kind
"Participants", event.people
"Location", "Coastal training grounds"
"Status", "Confirmed" ]
section
"Session preparation"
(ul {
_class "list-disc space-y-2 pl-5 text-sm"
li { "Check helmets and protective equipment before riding." }
li { "Meet at the east entrance 15 minutes before the session." }
li { "Water and a shaded rest area are available beside the track." }
})
section
"Session assets"
(link
(queryUrl MediaManagement { defaultQuery with item = event.photo })
"View matching asset")
link (url Scheduling) "Back to schedule"
},
event.name
| None ->
let view =
match query.view with
| "day" | "week" | "year" -> query.view
| _ -> "month"
let today = DateOnly(2026, 9, 17)
let offset =
match Int32.TryParse query.range with
| true, value -> Math.Clamp(value, -366, 366)
| _ -> 0
let start = today.AddDays offset
let startValue = start.ToString("yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture)
let step direction =
let target =
match view with
| "day" -> start.AddDays direction
| "week" -> start.AddDays(direction * 7)
| "year" -> start.AddYears direction
| _ -> start.AddMonths direction
target.DayNumber - today.DayNumber
let events =
[ for event in scheduledEvents ->
CalendarEvent.create event.id event.name event.date (eventUrl event)
|> CalendarEvent.withTime event.start event.finish
|> CalendarEvent.withDetail event.people ]
let dateDestination (date:DateOnly) = queryUrl Scheduling { query with view = "day"; range = string (date.DayNumber - today.DayNumber) }
let todayDestination = queryUrl Scheduling { query with range = "0" }
let previous = queryUrl Scheduling { query with range = string (step -1) }
let next = queryUrl Scheduling { query with range = string (step 1) }
let calendar =
match view with
| "day" ->
DayCalendar.create "Daily sessions" start events
|> DayCalendar.withToday today todayDestination
|> DayCalendar.withSelectedDate start
|> DayCalendar.withDateDestination dateDestination
|> (if step -1 >= -366 then DayCalendar.withPrevious previous else id)
|> (if step 1 <= 366 then DayCalendar.withNext next else id)
|> DayCalendar.render id
| "week" ->
WeekCalendar.create "Weekly sessions" start events
|> WeekCalendar.withToday today todayDestination
|> WeekCalendar.withSelectedDate start
|> WeekCalendar.withDateDestination dateDestination
|> (if step -1 >= -366 then WeekCalendar.withPrevious previous else id)
|> (if step 1 <= 366 then WeekCalendar.withNext next else id)
|> WeekCalendar.render id
| "year" ->
YearCalendar.create "Annual sessions" start events
|> YearCalendar.withToday today todayDestination
|> YearCalendar.withSelectedDate start
|> YearCalendar.withDateDestination dateDestination
|> (if step -1 >= -366 then YearCalendar.withPrevious previous else id)
|> (if step 1 <= 366 then YearCalendar.withNext next else id)
|> YearCalendar.render id
| _ ->
MonthCalendar.create "Monthly sessions" start events
|> MonthCalendar.withToday today todayDestination
|> MonthCalendar.withSelectedDate start
|> MonthCalendar.withDateDestination dateDestination
|> (if step -1 >= -366 then MonthCalendar.withPrevious previous else id)
|> (if step 1 <= 366 then MonthCalendar.withNext next else id)
|> MonthCalendar.render id
Html.section {
_ariaLabel "Schedule"
_attr ("data-view", view)
_class "grid min-w-0 gap-4"
nav {
_ariaLabel "Schedule calendar view"
_class "flex flex-wrap gap-1"
for value, label in [ "day", "Day"; "week", "Week"; "month", "Month"; "year", "Year" ] do
a {
_href (queryUrl Scheduling { query with view = value })
if view = value then _ariaCurrent "page"
_class "inline-flex min-h-8 items-center rounded-[var(--fve-radius-control)] px-3 py-1 text-sm font-medium no-underline hover:bg-[var(--fve-surface-hover)] focus-visible:outline-2 focus-visible:outline-[var(--fve-brand-ring)] aria-[current=page]:bg-[var(--fve-brand-subtle)] aria-[current=page]:text-[var(--fve-brand-text)]"
label
}
}
div {
_id ("scheduling-calendar-" + view + "-" + startValue)
_dataInit $"el.fveCalendarObserver?.disconnect(); const cell = el.querySelector('[data-date=\"{startValue}\"]'); const region = cell?.closest('[role=region]'); if (cell && region) {{ const observer = new ResizeObserver(() => {{ if (!el.isConnected) {{ observer.disconnect(); return }}; if (!region.clientWidth) return; const c = cell.getBoundingClientRect(), r = region.getBoundingClientRect(); const hours = region.querySelector('.fve-calendar-hours')?.clientWidth || 0; region.scrollLeft += c.left - r.left - Math.max(hours, (region.clientWidth - c.width) / 2) }}); el.fveCalendarObserver = observer; observer.observe(region) }}"
calendar
}
},
"Schedule"
workspace
Scheduling
heading
"Eastern time"
false
[]
(stateContent Scheduling query content)
div { _class "h-[44rem]"; scheduling defaultQuery }
|> Browser.create
|> Browser.withAddress "https://fve.meiermade.com/components/page-examples/scheduling?state=ready"
|> Browser.render
|> Fixture.create "page-workspace" "Workspace" "/components/page-examples/scheduling?state=ready"
|> Fixture.withFullscreenContent (scheduling defaultQuery)
|> Fixture.render