Planner
GanttToolbarGanttGanttQuickAdd
The Planner shows every live campaign and planned flight, grouped by ad account. A flight’s name is in its bar and in a chip down the Name column, under its account. Press an account to collapse it, and drag the sidebar’s edge to resize it. Scroll to either edge and the range grows. Change the zoom, from its chip or a step at a time with − and +, and the day in the middle of the view stays in the middle. View settings turns key dates on; scroll on to November for the first of them. The view picker switches between the three views.
GanttViewPicker · GanttZoomSwitch · GanttTodayButton · GanttCount · GanttKeyDatesMenu · GanttKeyDates · GanttGroupRow · GanttRow · GanttBar · GanttHandle
Gantt
The five flights at each zoom: a live bar with its pacing fill and a phase band, a campaign with no end date, a planned flight in outline with its resize handles, a drafted move as a dashed second bar, and a campaign over pace. A day is 40 pixels wide at Days, then 24, 12, 4, 2 and 1. The header reads months over days, months over numbered weeks, quarters over months twice, then years over quarters twice. Weekends are shaded as far as Months. From Half-year out the bars are under 40 pixels, too short to hold a name, so each name sits after its bar. At Years the twelve-day planned flight is a stub.
Days · Weeks · Months · Quarters · Half-year · Years
Days
Weeks
Months
Quarters
Half-year
Years
Gantt
At the Quarters zoom a day is four pixels wide. The six-day planned flight is an 18 pixel bar: under 40 pixels a bar has no room for its name, so the name sits after it with the last day under it, and from 16 pixels up it keeps its resize handles. The one-day flight is too short for a bar and is an 8 pixel stub with the same name and day after it. The second account is collapsed, so it is one row tall and draws no bars. The line beside today’s is a plan’s last day.
short bar · stub · collapsed GanttGroupRow · GanttMarker
GanttKeyDates
Key dates are the periods a plan is made round. A run of days is a tinted band down the calendar and one day is a dashed line, both behind the bars and over the weekend shading, and neither takes the pointer: a bar over Black Friday is dragged as it is anywhere else. The names sit in a strip under the header, one row tall. Two that would meet stack on a second line, as Christmas Day and Boxing Day do at Weeks. With both lines taken a name is cut short, and where there is still no room the names fold into a +n chip, as they do at Years. Every chip says its name and its days in a tooltip. The teal band is a date a workspace added in a colour it picked; the rest wear the neutral tint.
Weeks · Months · Years
Weeks
Months
Years
GanttBarGanttRowGanttArrowsGanttLinkLineGanttInsertionLineGanttRowSpace
Drag a bar and after four pixels it leaves its row. A copy follows the pointer with no snapping, and the pill on the header steps a day at a time, naming the days the bar would land on. Going forward the day advances at half a day’s travel; coming back it holds until four pixels before the previous day’s edge. Drag a bar up or down and its account’s rows slide apart as the pointer crosses each one’s middle, a line marks where it would land, and it stays on the row you drop it on. A drag that goes further up or down than across holds the bar’s dates, and the pill shows them unchanged. Drag a planned flight (the dashed bars) into the other account and it lands at the line there and keeps its dates; a live campaign stays in its own account. On the drop the copy travels to its row before the bar shows. Brand search has no flight dates, so it does not lift: a click opens it. Drag either end to resize, a day for each day boundary crossed, down to one day. Escape puts the bar back. With a bar focused, the arrow keys move it a day, or a week with Shift. The two diamonds are milestones: one day each, moved and placed like a bar, with no ends to resize. The arrows are dependencies, drawn from the lane that goes first to the lane that waits on it, and red while the two overlap; moving a bar moves no other bar, and its arrows are hidden while it is in the air. Put the pointer on a bar and two dots appear beside it: drag from the one at its end to a bar that comes after it, or from the one at its start to a bar that comes before it. A bar is always in front of an arrow that crosses its row, so it can be picked up anywhere on its face. Put the pointer on an arrow and its head becomes a button that removes the link.
lift · landing · shift · active · milestone · antennas · linkTarget · Gantt pill · GanttHandle
GanttBarGanttRowGanttInsertionLineGanttRowSpace
Four moments of a drag on the planned flight, held still. Lifted: the pointer is two and a half days along, so the bar sits between days and the pill names the flight three days later. Resizing: the end is three days on, the bar has stepped with it and both handles show. On another row: carried up past the two campaigns, which have slid down a row each to open the slot at the top, where the line is; the drag went up, so the pill shows the dates unchanged. Over the other account: its second row has slid down to open a slot under the line, and the account it left has closed over its row. Here the lifted bar is drawn in place; in a drag it is a copy in a layer above the page.
lifted bar and pill · resize and pill · rows slid apart and the line · a slot in another group
Lifted
Resizing
On another row
Over another account
GanttBarGanttArrowsGanttLinkLine
Milestones and dependencies, held still. Milestones: a 16 pixel diamond in the middle of its day with its name beside it, its date under the name and a hairline down the canvas; one at rest, one in a colour somebody gave it, and one being dragged. Routes: an arrow leaves the end of the lane that goes first with a 5 pixel stub, turns through 10 pixel corners and arrives at the start of the lane that waits, at mid-height, in a 5 by 8 pixel arrowhead. Two bars on one row are joined by a straight line; a successor that starts later turns once, down or up, with corners that shrink where there is little room, down to a square turn for a flight that starts the day after; and an arrow crosses into another account, behind any bar on the rows between. Conflict: the successor starts before the predecessor ends, so the arrow and its head are red, and only then does it double back through the gap between rows. Under a pointer: the arrow and both bars’ borders take the selection colour and the arrowhead becomes the remove button. Antennas: the two dots a link is dragged from, beside the top of a bar's start and the foot of its end and inside its row, on a neutral bar and on a coloured one. Linking: the line from an antenna to the pointer, and the bar it would land on in the selection border and the selected fill. Here the line is drawn in place; in a drag it is in the layer above the page, cut off at the sidebar and the header.
milestone · arrow routes · conflict · hot · antennas · linkTarget
Milestones
Routes
Conflict
Under a pointer
Antennas
Linking
PLANNER_COLOURSplannerColourVars
The fifteen colours a person can give a bar, on the light page and on the dark ground. Each is a fill, the ink written on it and a hover shade, drawn here as the handle at the bar’s end. The ink is the dark neutral in both modes, and the token build stops on any that reads below 4.5:1 on its fill or on its hover shade.
--category-<name> · -foreground · -hover
light
- Red
- Orange
- Amber
- Yellow
- Lime
- Green
- Teal
- Aqua
- Blue
- Indigo
- Purple
- Magenta
- Pink
- Rose
- Slate
dark
- Red
- Orange
- Amber
- Yellow
- Lime
- Green
- Teal
- Aqua
- Blue
- Indigo
- Purple
- Magenta
- Pink
- Rose
- Slate
GanttBarGanttBarDot
The neutral bar, then a bar in each colour, with everything a bar carries drawn on it. The pacing fill is the colour’s hover shade with a rule of its ink where the spend reached, and the state word is in that ink. The dashed outline is a drafted move waiting at the gate, and the two bands under the bar are the planned flight and a phase. A bar with room for its name and none for the word has an 8 pixel dot in the state’s colour before the name. A bar under 40 pixels has the dot with its name, after the bar.
colour · pacing fill · pending outline · planned band · phase band · state dot
GanttColourPickerGanttBar menu
The picker is a row of radios, one per colour and one for none, each with its name, and the lane’s own is checked. It is in a bar’s menu, which a right-click opens, and in the campaign’s and the plan item’s peek. A pick saves at once. On this page it changes the bar below and nothing else.
RadioGroup · ContextMenu · ContextMenuRadioItem
Teal is picked. Right-click the bar for the same swatches in its menu.
GanttColourBySwitch
The toolbar’s View settings carry Colour by. Custom is each lane’s own colour, kept for the whole workspace. Platform, Status and Pace paint every bar from that field for the person looking and write nothing; in the app the choice is kept in the URL as colour.
DropdownMenu · DropdownMenuRadioGroup
GanttQuickAdd
The row at the foot of the Gantt. On a plan page it reads “New plan item” and opens the new item dialog with the typed name; on the Planner it reads “New plan” and goes to a new plan.
KanbanProviderBoardColumnBoardCard
The Board view has one column per platform. Drag a plan item to another platform or another place in its column; a campaign stays on its platform, and a click opens its peek. A card is the lane’s four rows (account, flight, pace and budget), and a lane with a colour wears it as a strip on its leading edge. The sum under a column adds each card’s daily budget: a campaign’s today, a planned item’s on its first day.
KanbanHeader · BoardColumnHeader · KanbanCards · KanbanCard · BoardCard
A$565.00sum
A$231.43sum
A$100.00sum
The set
These are the Admin Kit’s Gantt and kanban, ported onto the package’s Base UI primitives and tokens in packages/ui/components/planner/. The set takes groups, rows, bars and cards as props, draws them at one of six zooms, from Days at 40 pixels a day to Years at one, and reports what the reader did: a press on a bar or a handle, an edge reached while scrolling, a card dropped in a column. A row is 40 pixels tall, with a 34 pixel bar 3 pixels down, and the lane’s name is in its bar and in a chip in the row’s left cell. The left column is headed Name and is 250 pixels wide until you drag its edge. A group is a head row above its rows, and it collapses to its head when you press its name. The header is two 20 pixel tiers, a month over its days, so it lines up with a table’s header row. A bar under 40 pixels wide has its name after it, and a flight too short for a bar at all is an 8 pixel stub with the name after it. Today is a line down the calendar, and the app draws a plan’s last day the same way. It knows nothing of campaigns, plans or drafts. The accounts, flights and budgets above are invented for the page, and the day it treats as today is fixed. The toolbar is two rows on the CRM table’s toolbar: the view picker, View settings, the page’s buttons and the zoom chip and Today, the filters and the count. The board is the CRM board’s column and card, with the flight’s four rows (account, flight, pace and budget) and the sum under each column.
The set draws a drag and leaves the pointer to the caller. A bar given lift is hidden in its row and drawn again in a layer above the page, at the point the caller names. Each Gantt has its own layer, the size of the calendar a reader can see, so a copy is cut off at the sidebar, the header and the edges of its Gantt, and its name stays at the left edge of what shows. The header takes pill and marks the days the bar would land on. drag.ts turns a pointer’s travel into days. A press becomes a drag at four pixels. A moved bar’s day advances at half a day’s travel and, on the way back, holds until four pixels before the previous day’s edge. A resized edge steps a day for each day boundary the pointer crosses.
A bar is placed on a row by hand, and the set draws that too. ganttDragAxis decides what a move is about once the pointer is eight pixels from the press: its row if it has gone further up or down than across, and the caller then holds its dates. ganttSlotIndex names the slot under the pointer, which changes as the pointer crosses the middle of each row, and ganttRowShifts says how far each row slides to open it. A GanttRow given shift slides by transform over 0.15 seconds, GanttInsertionLine sits on the boundary the bar would land on, and GanttRowSpace holds a group open for a row coming from another group. On the drop the caller sets landing, and the copy travels to where the bar now sits over 0.12 seconds before onLanded is called. Reduced motion does both at once.
A GanttBar given milestone draws a 16 pixel diamond in the middle of its day in place of a bar, with its label beside it in medium weight, the day under the label and a hairline down the canvas in the milestone’s colour. It takes lift, colour and menu as a bar does and has no handles.
Dependencies are three parts. GanttArrows is one svg over the Gantt’s body, rendered after the groups, that draws an arrow from the bar with one linkId to the bar with another. It measures the bars where they are drawn, so an arrow crosses groups, follows a bar that is moved, resized or reordered and a row that slides, and is left out while either bar is lifted or its group is collapsed. arrows.ts has the route as a pure function, ganttArrow: a 5 pixel stub out of the predecessor’s end, right-angle turns with 10 pixel corners, and a 5 by 8 pixel arrowhead on the successor’s start at mid-height. A successor that starts after its predecessor ends always gets that one turn: where the stub and two 10 pixel corners do not fit, the corners shrink and then the stub, down to a square turn in the 6 pixels between a flight and the one that starts the next day, so no two arcs ever share a run (ganttCornerRadii). Only a successor that starts before its predecessor ends gets the doubled-back route through the gap between rows. Each line has a 5 pixel wide invisible twin as its hit area. Both are clipped to what is not a bar (ganttClipAround), so an arrow that crosses a row is drawn behind its bar and never takes a press meant for the bar, its handles or an antenna. An arrow the caller marks conflict is drawn in the destructive colour; ganttDependencyConflict says when that is. Under a pointer or the focus the arrow and both bars’ borders take the selection colour and the arrowhead becomes a round remove button, which Enter, Space, Delete and Backspace all press.
A bar given antennas shows the two dots a link is dragged from: one out from the top of its start for what comes before it and one out from the foot of its end for what comes after. Both sit beside the bar and inside its own row, so they show on a Gantt’s first row and under a group’s header as on any other, and a short bar’s name starts past the one at its end. A milestone’s go over its start and under its end, where its row has room. Each is a 7 pixel dot on a short curved stalk, shown while a pointer is over the bar, while it has the focus and while it is active, and it takes the pointer only while it shows. A press on one reaches onAntennaPointerDown with its side. GanttLinkLine is the line of the link in hand, drawn in the lift layer so it is cut off at the sidebar and the header as a lifted bar is, and it follows the pointer through moveTo without a render. The bar the link would land on takes linkTarget: the selection border and the selected fill.
A bar takes one of fifteen colours through colour. The colour is its fill, the colour’s ink is what is written on it, and its handles and a pointer over it take the colour’s hover shade. The bar sets --lane-fill, --lane-ink and --lane-hover on itself, so a fill or a band drawn inside reads the same colour. The values are the categorical tokens on the colour page. GanttColourPicker is the row of swatches, as a radio group on a page and as radio items in the menu a bar opens through menu. GanttBarDot is the 8 pixel dot a bar carries where it has no room for its state word. GanttColourBySwitch is the toolbar’s View settings button, and PlannerColourEdge is the 3 pixel strip a board card wears.
GanttKeyDates marks dates down the whole calendar. It takes dates, each with an id, a name, two days and an optional colour, and is rendered once as the Gantt’s first child. A run of days is a band over its days whole and a single day is a one pixel dashed line down the middle of its day, at every zoom; a band never draws thinner than two pixels. Both go into the Gantt’s backdrop, over the weekend shading and under every row, and take no pointer events, so a bar, a handle, an antenna and an arrow over one are under the pointer as they are anywhere else. A lifted bar, the axis pill and the arrows draw over them. The names go into a strip pinned under the header, 48 pixels tall, in a row of its own, each a chip 20 tall: group headers stick below it and a lifted bar is cut off at its bottom edge. key-dates.ts has the arithmetic as pure functions. ganttKeyDateGeometry places a band or a line on the range’s track. layoutGanttKeyDateChips places the chips so that no two overlap: each starts where its mark starts and is as wide as its name, or as its band when the band is wider, so a long period’s name follows the reader along it. A chip goes on the first of two lines where it fits whole; with both lines taken, the chip before it is cut short, never under 44 pixels; and with no room left the names fold into a +n chip that lists them in its tooltip. A date with a colour takes the categorical token at reduced opacity for its band and whole for its chip, and the rest take a neutral tint. GanttKeyDatesMenu is the key dates part of the toolbar’s View settings, for GanttColourBySwitch’s children: a checkbox item drawn as a switch that leaves the menu open, the region as a submenu of radio items, and an item that opens the caller’s own dates. The set knows no calendar; which dates there are is the caller’s.
Import from the barrel:
import {
Gantt,
GanttBar,
GanttRow,
KanbanProvider,
} from "@tempio/ui/components/planner";In the app
The app binds the set in apps/app/components/hub/timeline/. It reads the lanes with getTimelineOutline, which returns the names, flights and links at once and hands back the pacing figures as a second read, groups the lanes by ad account, and turns a drag into days with timeline-drag.ts. getTimeline is the two joined. The Planner at /timeline and each plan page at /plans/<id> render the same component, with Timeline, List and Board behind the toolbar’s view picker. On /timeline the stage (the toolbar and the view’s empty frame) draws first, then the lanes’ names and flights, and the pacing figures follow once they are read; a plan page is the same read narrowed to one plan, as its Plan tab, beside Brief, Scenarios, Playbooks and Activity.
A drag on a live campaign’s bar drafts a change request. The new dates wait as a dashed second bar and go live when you approve the request, or when a rule you wrote does. A plan item’s bar moves as soon as you drop it, and the move goes into the audit log; a toast names the item and offers Undo, a ghost button, which runs the same move back. The drop of a live campaign asks first, in an alert dialog. On the board a plan item can move to another platform; a campaign stays on its own.
A bar dropped on another row stays there. The drop saves through setLaneOrder, which runs the service the planner_order_set tool runs: one lane, and the lane it now sits before or after. The order is one number per placed lane in planner_lane_position, audited and never a change request, and getTimeline returns each account’s lanes in it, with the lanes nobody has placed after them by start date. The rows take the new order at once and go back, with the refusal’s sentence in a toast, if the save is refused. A plan item dropped in another account on its platform lands at the line there, changes account through upsertPlanItemAction and keeps its dates; a live campaign is placed among its own account’s rows. A lane with no flight dates and an item in a materialized plan do not lift: they keep the pointer cursor, open their peek on a click, and say why in their tooltip.
A plan item with kind set to milestone is drawn as the set’s milestone. The new item form’s Flight and Milestone choice sets it, and media_plan_item holds a milestone to one day and no budget. getTimeline returns it as a plan item lane with itemKind, and everything that adds a plan up leaves it out.
A link is one row in plan_dependency, with a campaign or a plan item at each end. The antenna drag and the two links in a bar’s menu both save through addDependency, which runs the service the plan_dependency_add tool runs. The arrow is drawn at once and is taken away, with the refusal’s sentence in a toast, if the save is refused: a lane from another workspace, a pair already linked, a lane waiting on itself, or a loop, whose refusal names the lanes in it. An arrow’s button and the remove buttons in a lane’s peek go through removeDependency, with Undo in the toast. getTimeline returns the links whose two ends are both among its lanes. A link is planning metadata, audited and never a change request, and it moves no flight: the arrow’s colour is what reports an overlap.
Key dates come from two places. apps/app/lib/ads/key-dates.ts is a calendar of rules with no table behind it: each period is a function from a year to its days, for Australia, New Zealand, the United States, the United Kingdom and Canada, so keyDatesFor(region, range) is right for any year. Lunar New Year is the one that reads a table, 2024 to 2035. The workspace’s own dates are rows in planner_key_date, written by the “Manage key dates” dialog through saveKeyDate and removeKeyDate, which run the service the planner_key_date_upsert and planner_key_date_remove tools run: audited, never a change request. The dialog’s list and the band take a change at once and go back, with the refusal’s sentence in the dialog, if the save is refused. The timeline works the two lists out again for the days drawn as the range grows, and passes them to GanttKeyDates. The switch is keydates=1 in the URL and the region is region, which is left out while it is the workspace’s own: where its ad accounts report and bill, then the reader’s locale, then the United States. planner_key_dates_list returns the same calendar to an agent.
A right-click on a bar, or the swatches in its peek, sets the lane’s colour through setLaneColour, which runs the service the planner_colour_set tool runs. The bar takes the colour at once and goes back to what it had, with the refusal’s sentence in a toast, if the save is refused. The colour is one row per lane in planner_lane_colour and never a change request. Colour by in the toolbar’s View settings paints the bars from COLOUR_BY_TABLE in apps/app/lib/ads/lane-colour-by.ts and is kept in the URL as colour. On the board the colour is a strip on the card’s leading edge, and in the list an 8 pixel dot after the name, the quieter of the marks tried, since both already carry a round pace dot.