Canvas Design System
Main Site Tokens

Planner Library Rail

The Planner's left column: search, category navigation, and the draggable activity palette.

When to Use

Use when: You're building the Planner's left column, or a surface that needs the same search → browse → drag flow. Every activity goes through Planner Card at sm, so the palette and the canvas speak the same card language.
Don't use when: Your sidebar needs regions beyond search, categories, and item sections. This is a layout shell with a fixed anatomy — compose your own <aside> and reuse ws_planner_card() directly instead of forcing it.

Variants

Canonical

All three regions: a search field with its keyboard hint, a category list with dots and counts, and grouped sections of draggable cards. Expect a column of 260–320px.

<?= ws_planner_library_rail([
    'search'     => ['placeholder' => 'Search 2,418 exercises…', 'kbd' => '⌘K', 'name' => 'q'],
    'categories' => [
        'title' => 'Library',
        'count' => '2,418',
        'items' => [
            ['label' => 'Strategy', 'color' => $cat['color'], 'count' => 284, 'active' => true],
        ],
    ],
    'sections'   => [
        ['title' => 'Strategy · drag to add', 'meta' => 'top 5', 'items' => [
            ['title' => 'Lightning Decision Jam', 'type' => 'exercise',
             'duration' => '45m', 'meta' => '6-12 ppl'],
        ]],
    ],
]) ?>
info Category colours come from the categories table per record — they're data, not phase tokens, which is why they aren't red. Pass $cat['color'] straight through rather than hardcoding.

Minimal — no categories

Search plus item sections. Use it when the rail is already scoped to one category and the nav would be redundant. Passing null for search hides that region too.

<?= ws_planner_library_rail([
    'search'   => ['placeholder' => 'Search icebreakers…'],
    'sections' => [['title' => 'Fast icebreakers', 'items' => $items]],
]) ?>

States

StateBehavior
Active categoryactive => true applies the phase colour, swaps the dot to white, and sets aria-current="page".
Category as link vs buttonWith an href the row is an <a>; without one it's a <button> carrying data-category-id and data-category-name for your click handler.
Section collapsed / expandedSection headers toggle via data-rail-toggle, with aria-expanded and aria-controls kept in sync. Your JS owns the toggle.
Show more / lessLong sections emit data-rail-show-more with data-label-more and data-label-less, so the button text swaps without hardcoding strings in JS.
Card statesHover, disabled, and the swap affordance all belong to Planner Card.
Empty sectionRenders the header with no items. Filter empty sections out before passing them.

Real-World Usage

The Planner's real left column, built from the category and exercise tables. Items are forwarded wholesale to Planner Card, so any card option can travel in the item array — including attrs for the drag payload.

<?= ws_planner_library_rail([
    'search'     => ['placeholder' => "Search {$total} exercises…", 'kbd' => '⌘K', 'name' => 'q',
                     'value' => $_GET['q'] ?? ''],
    'categories' => [
        'title' => 'Library',
        'count' => number_format($total),
        'items' => array_map(fn($c) => [
            'label'  => $c['name'],
            'color'  => $c['color'],
            'count'  => $c['exercise_count'],
            'active' => $c['id'] === $activeCategoryId,
            'id'     => $c['id'],
        ], $categories),
    ],
    'sections'   => [[
        'title' => $activeCategoryName . ' · drag to add',
        'meta'  => 'top ' . count($exercises),
        'items' => array_map(fn($e) => [
            'title'    => $e['name'],
            'type'     => 'exercise',
            'duration' => (int) $e['duration'],
            'meta'     => $e['group_size'],
            'attrs'    => ['data-exercise-id' => $e['id']],   // the drag payload
        ], $exercises),
    ]],
]) ?>

Options

OptionTypePurpose
searcharray|null['placeholder', 'kbd', 'name', 'value']. null hides the region. kbd is the shortcut hint shown in the field.
categoriesarray|null['title', 'count', 'items'], each item ['label', 'color', 'count?', 'active?', 'href?', 'id?']. null hides the nav.
sectionsarray[['title', 'meta?', 'items']]. Each item is forwarded to ws_planner_card() with size=sm and draggable=true, so every card option is valid.
idstringElement ID
classstringAdditional CSS classes
attrsarrayExtra HTML attributes

Accessibility

The best-wired component in the library — treat its markup as the reference for the other rails.

ConcernBehavior
LandmarksThe search region carries role="search", making it a named landmark screen-reader users can jump to.
ListsCategory and item groups use role="list" and role="listitem", so counts and positions are announced.
Current categoryaria-current="page" on the active row — the state is conveyed without relying on the colour swap.
Collapsible sectionsHeaders pair aria-expanded with aria-controls pointing at {sectionId}-body, the correct disclosure pattern.
Search fieldLabelled by aria-label taken from the placeholder, so it has a name even though there's no visible label.
Decorative iconsCategory dots and chevrons carry aria-hidden="true".
Drag to addKnown gap: dragging a card to the canvas is pointer-only. A keyboard user can reach and focus a palette card but cannot place it. Provide an "Add to agenda" action as an equivalent path — this is the rail's most significant accessibility limitation.

Tokens

The rail is a layout shell, so most of its visual weight comes from the cards inside it. Its own rules draw on the shared surface, border, ink, and spacing scales; the activity accents come from --planner-* by way of Planner Card, and category dots take their colour from record data rather than a token.

Rail width is governed by --app-rail-width (320px) in the app shell — see the Planner guide. Don't hardcode a width on the container.

CSS Classes

Note the prefix is .ws-planner-rail, not .ws-planner-library-rail — the helper and the class don't share a name.

ClassPurpose
.ws-planner-railRail container
.ws-planner-rail__search / __search-icon / __search-kbdSearch region and its adornments
.ws-planner-rail__categories / __cat-gridCategory nav wrappers
.ws-planner-rail__cat-btn / -name / -count / -iconA category row and its parts
.ws-planner-rail__cat-dotColour dot, tinted from record data
.ws-planner-rail__h / __h--toggle / __h-left / __h-right / __h-icon / __h-chevronSection header and its disclosure control
.ws-planner-rail__bodyCollapsible section body, targeted by aria-controls
.ws-planner-rail__itemsCard list
.ws-planner-rail__filterFilter control row
.ws-planner-rail__instructionsHelper text, e.g. "drag to add"
.ws-planner-rail__footer / -btn / -label / -btn-iconRail footer actions

Files

FilePurpose
includes/components/helpers.phpws_planner_library_rail() helper function
includes/components/planner-library-rail.phpTemplate — regions, ARIA wiring, and the card loop
includes/components/planner-card.phpRenders every activity in the rail
includes/components/components.cssStyles (.ws-planner-rail rules)
css/planner/components/_sidebar.cssPlanner-side rail styling