Scheduling workspace
https://fve.meiermade.com/components/page-examples/scheduling?state=ready&view=month&range=-294 Workspace
Northwind Outdoor
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
}
let section (heading: string) (content: HtmlElement) =
Section.create (SectionHeader.create heading |> SectionHeader.withDivider) content
|> Section.render id
let details (fields: (string * string) list) =
DescriptionList.create [ for label, value in fields -> DetailField.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 ->
EmptyState.create "Nothing to show yet" "Change the current selection or return to the populated workspace."
|> EmptyState.withActions (link (queryUrl page { query with state = Ready }) "Return to workspace")
|> EmptyState.render
| Failed ->
Notice.create
"workspace-error"
"This view could not be loaded"
(p { "Your selection is preserved. Try loading it again." })
|> Notice.withTone Tone.Critical
|> 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 content =
let product, workspaceName, destinations =
match page with
| DependencyGraph
| ExecutionDetail ->
"Relay", "Customer analytics", [ DependencyGraph, "Dependencies"; ExecutionDetail, "Executions" ]
| FinancialReporting -> "Ledger", "Northwind Studio", [ FinancialReporting, "Overview" ]
| Messaging -> "Gather", "Northwind Outdoor", [ Messaging, "Messages" ]
| _ ->
"Fieldwork", "Northwind Outdoor", [ Operations, "Overview"; Scheduling, "Schedule"; MediaManagement, "Photos" ]
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.withMobileContext (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
|> PageHeader.withActions (ActionCluster.create (slug page + "-actions") actions)
let topBar =
PageTopBar.create ()
|> PageTopBar.withContent (
div {
_class "flex min-h-[var(--fve-shell-bar-min-height)] items-center px-4 sm:px-6 lg:px-8"
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.withTopBar topBar
|> Page.withWidth PageWidth.Full
|> Page.withBodyLayout (
if canvas then
PageBodyLayout.Canvas
else
PageBodyLayout.Padded
)
|> Page.render id
AppShell.create (slug page + "-shell") navigation body
|> 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 photographs"
(link
(queryUrl MediaManagement { defaultQuery with item = event.photo })
"View matching photograph")
link (url Scheduling) "Back to schedule"
},
event.name
| None ->
let view =
match query.view with
| "day" -> CalendarView.Day
| "week" -> CalendarView.Week
| "year" -> CalendarView.Year
| _ -> CalendarView.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 step direction =
let target =
match view with
| CalendarView.Day -> start.AddDays direction
| CalendarView.Week -> start.AddDays(direction * 7)
| CalendarView.Month -> start.AddMonths direction
| CalendarView.Year -> start.AddYears direction
target.DayNumber - today.DayNumber
Calendar.create
"Schedule"
view
start
[ for event in scheduledEvents ->
CalendarEvent.create
event.id
event.name
event.date
(eventUrl event)
|> CalendarEvent.withTime event.start event.finish
|> CalendarEvent.withDetail event.people ]
|> Calendar.withToday today (queryUrl Scheduling { query with range = "0" })
|> Calendar.withSelectedDate start
|> Calendar.withDateDestination (fun date -> queryUrl Scheduling { query with view = "day"; range = string (date.DayNumber - today.DayNumber) })
|> (if step -1 >= -366 then Calendar.withPrevious (queryUrl Scheduling { query with range = string (step -1) }) else id)
|> (if step 1 <= 366 then Calendar.withNext (queryUrl Scheduling { query with range = string (step 1) }) else id)
|> Calendar.withViewDestinations
[ for value, label in
[ CalendarView.Day, "day"
CalendarView.Week, "week"
CalendarView.Month, "month"
CalendarView.Year, "year" ] ->
value, queryUrl Scheduling { query with view = label } ]
|> Calendar.render id,
"Schedule"
workspace
Scheduling
heading
"Northwind Outdoor · Times in America/New_York"
false
[ ApplicationAction.link (url Operations) "Overview" ]
(stateContent Scheduling query content)
div { _class "h-[44rem]"; scheduling defaultQuery }
|> Browser.create
|> Browser.withAddress "https://fve.meiermade.com/components/page-examples/scheduling?state=ready"
|> Fixture.browser "page-workspace" "Workspace" "/components/page-examples/scheduling?state=ready"
|> Fixture.render