3e9b97ed8b
`member` and `anon` were told apart inside the office by the client and by nothing else. `access.ts` picked an `officeDepth`, `createOfficeScene` built a presence layer at full depth — and then nobody ever called `setPresence`, so both tiers rendered the identical empty room. A tier that changes nothing you can see is not a tier, and `routes/markers.ts` had already written down why one drawn only in the browser is worse than none: it is a UI hiding a control over a body the API hands to whoever asks. So the refusal happens on the server now. `GET /api/v1/offices/:id/presence` is the one route that always takes a session, whatever else the deployment is configured for. `markers.ts` serves its feed to anonymous callers when no feed is configured, on the grounds that there is nothing there to protect; that reasoning does not transfer, and the difference is the whole point — a marker is a company at an address and a presence is a person at a desk. The ordering inside the handler is the security property, not a detail. It resolves the viewer *before* it looks at the id, so an anonymous caller gets an identical 401 for a real office, a private one and one that was never created. Check the office first and 404-for-unknown against 401-for-known tells them apart perfectly, which is the enumeration oracle CONTRACT.md §6 forbids, wearing a different status code. Three requests and one `deepEqual` hold that down. `TERA_PRESENCE_DIR` is a second directory rather than a `people` field on the pack, and that is the design. `types.ts` says a `Presence` binds to a `seatId` and never to a coordinate so the geometry can be published while the people cannot — which buys nothing if both live in one file, because an operator who wants a public floorplan then has to strip the roster out by hand, and the first time they forget the leak is permanent. Two directories makes the safe thing the default thing. An office with no roster is 200 and empty, never 404: "no such office" and "nobody has told me who is in this one" are different problems with different fixes, and one 404 sends an operator after the wrong one. On the client, occupancy arrives after the room is on screen rather than before — the building is worth looking at while a second request is in flight. An API that answers is believed, including when it answers with nobody; an office where everyone has gone home is a real fact and overwriting it with invented people to liven up the demo is the one thing this must never do. An API that does not answer falls back to a fabricated roster, exactly as the markers do, because a clone with no server is the flagship case and a member shown the same empty room as a stranger has been told the tier means something when it does not. Those twenty-five people are invented and the page says so. `sample.ts` says it to a reader of the source; `#office-badge` now says "Sample occupancy — these people are invented" to the person looking at the room, and it is not suppressed when a real deployment's API merely happened to be down — that is exactly the case where a member would otherwise read invented names as their colleagues. Fabricated names at real desks look like a staff list, and a screenshot of one must not be possible to take without the caption. The floor plan marks the occupied desks, one colour for everybody where the scene has four: at three device pixels a hue is a guess. The plan answers "is anyone there" and the room answers "who, and what are they doing". Hovering a desk names them, and the readout reads as an address getting more specific — metres, then room, then person. server: 127 tests pass, 11 of them new. Client typechecks and builds; the office chunk absorbed the plan renderer and the entry chunk moved 2.3 kB for the sample roster. Checked in the browser at office.lumbridgecorp.com: FULL VIEW, the badge, figures at the benches, dots on the plan, and "3.7, 16.7 m · Alcatraz · Clementine Roux" under the pointer.
445 lines
17 KiB
TypeScript
445 lines
17 KiB
TypeScript
/**
|
|
* Fabricated demo data, so that a clone of this repo has something on it.
|
|
*
|
|
* **Everything in this file is invented.** The companies do not exist, have
|
|
* never existed, and are named the way they are — Wobbegong, Nonsuch, Pennyfarthing
|
|
* — specifically so that nobody can mistake one for a real business. The
|
|
* coordinates were typed by hand from a general sense of where San Francisco's
|
|
* neighbourhoods are; none of them is anybody's address, and none of them came
|
|
* out of a geocoder.
|
|
*
|
|
* That last point is the licence rule and not a stylistic preference. No real
|
|
* company data ships in this repo, for two reasons that happen to want the same
|
|
* thing. The privacy one is obvious: pipeline status — who is talking to whom,
|
|
* who said no — is private, and a public repo is the wrong place for it. The
|
|
* licence one is sharper and is the subject of ARCHITECTURE.md §3.2 and
|
|
* CONTRACT.md §8: real positions are *geocoded*, and a geocoder built on
|
|
* OpenStreetMap returns ODbL data. Serving a snapshot of those coordinates is
|
|
* Publicly Using a Derivative Database, which drags share-alike onto everything
|
|
* served alongside it, whether or not the rows live in the repo. So real markers
|
|
* arrive at runtime over the API — see `http.ts` — carrying a provenance field
|
|
* the server checks, and the repo itself ships this: fiction, which owes nobody
|
|
* anything.
|
|
*
|
|
* The demo is worth having anyway. A map with no pins on it teaches nobody what
|
|
* the thing is for, and half the interesting behaviour in `markers.ts` — the
|
|
* ghost treatment for a marker that has no real position yet — is invisible
|
|
* without data that exercises it.
|
|
*/
|
|
|
|
import { regionOf, syntheticRoutes, type SimRoute } from "../engine/flights.ts";
|
|
import type { Presence } from "../interiors/types.ts";
|
|
import type { City, Marker, MarkerPalette } from "../engine/types.ts";
|
|
|
|
/**
|
|
* A small pipeline, as colours.
|
|
*
|
|
* Five states is about the fewest that still shows why `colorKey` is opaque to
|
|
* the engine: none of these words means anything to `markers.ts`, which looks
|
|
* the key up here and draws whatever it finds. A public sector map would supply
|
|
* an entirely different set against the same renderer.
|
|
*/
|
|
export const SAMPLE_PALETTE: MarkerPalette = {
|
|
watching: 0x7f8b99,
|
|
applied: 0x4f9cf2,
|
|
talking: 0x3fbf9a,
|
|
offer: 0xf2b134,
|
|
closed: 0xd2544f,
|
|
};
|
|
|
|
/**
|
|
* Where the unplaced markers float: out over the bay, east of the Ferry
|
|
* Building, in a short arc.
|
|
*
|
|
* A marker with `located: false` has no address yet, and the honest thing to do
|
|
* with a position you do not have is to not pretend you have one. Open water is
|
|
* the clearest way to say that on a map — nothing is there, the pin visibly
|
|
* stands on nothing, and it cannot be misread as a building. `markers.ts` gives
|
|
* these a different silhouette and a lower opacity as well, so the tell does not
|
|
* rest on placement alone.
|
|
*/
|
|
const UNPLACED_LNG = -122.3665;
|
|
|
|
/**
|
|
* Twenty-two invented companies across San Francisco.
|
|
*
|
|
* Placed by neighbourhood rather than by street: Hayes Valley and SoMa are
|
|
* crowded because that is the fact the map is usually drawing attention to, the
|
|
* Bayview and the Richmond have one each, and three have no position at all.
|
|
* The clustering is the point — a marker layer that only ever gets evenly
|
|
* scattered test data hides every overlap problem it has.
|
|
*/
|
|
export const SAMPLE_MARKERS: Marker[] = [
|
|
// Hayes Valley and the bowl below Buena Vista.
|
|
{
|
|
id: "sample-thimbleway",
|
|
label: "Thimbleway Systems",
|
|
colorKey: "talking",
|
|
lat: 37.7768,
|
|
lng: -122.4243,
|
|
located: true,
|
|
blurb: "Invented. Model-serving, allegedly, in a Victorian with bad wiring.",
|
|
},
|
|
{
|
|
id: "sample-nonsuch",
|
|
label: "Nonsuch Cartography Co.",
|
|
colorKey: "applied",
|
|
lat: 37.7752,
|
|
lng: -122.4262,
|
|
located: true,
|
|
blurb: "Invented. Maps of places that are not there.",
|
|
},
|
|
{
|
|
id: "sample-marmalade",
|
|
label: "Marmalade Interchange",
|
|
colorKey: "watching",
|
|
lat: 37.7781,
|
|
lng: -122.4218,
|
|
located: true,
|
|
blurb: "Invented. Moves data between two formats nobody uses.",
|
|
},
|
|
|
|
// SoMa, where the grid turns forty-six degrees.
|
|
{
|
|
id: "sample-kettle-anvil",
|
|
label: "Kettle & Anvil Compute",
|
|
colorKey: "offer",
|
|
lat: 37.7805,
|
|
lng: -122.4051,
|
|
located: true,
|
|
blurb: "Invented. Sells the shovels, or claims to.",
|
|
},
|
|
{
|
|
id: "sample-ninth-pelican",
|
|
label: "Ninth Pelican Labs",
|
|
colorKey: "applied",
|
|
lat: 37.7784,
|
|
lng: -122.4009,
|
|
located: true,
|
|
blurb: "Invented. There were never eight others.",
|
|
},
|
|
{
|
|
id: "sample-brassbound",
|
|
label: "Brassbound Telemetry",
|
|
colorKey: "closed",
|
|
lat: 37.7822,
|
|
lng: -122.4074,
|
|
located: true,
|
|
blurb: "Invented. Went quiet after the second call.",
|
|
},
|
|
|
|
// The Financial District and Jackson Square.
|
|
{
|
|
id: "sample-grimsby-doone",
|
|
label: "Grimsby & Doone Photonics",
|
|
colorKey: "watching",
|
|
lat: 37.7941,
|
|
lng: -122.4008,
|
|
located: true,
|
|
blurb: "Invented. Two surnames and a laser.",
|
|
},
|
|
{
|
|
id: "sample-tugboat",
|
|
label: "Tugboat Actuarial",
|
|
colorKey: "applied",
|
|
lat: 37.7958,
|
|
lng: -122.4032,
|
|
located: true,
|
|
blurb: "Invented. Insurance for things that have already happened.",
|
|
},
|
|
|
|
// Mission Bay: landfill, then biotech.
|
|
{
|
|
id: "sample-fogbank",
|
|
label: "Fogbank Freight",
|
|
colorKey: "talking",
|
|
lat: 37.7709,
|
|
lng: -122.3918,
|
|
located: true,
|
|
blurb: "Invented. Logistics, in a building younger than most of the staff.",
|
|
},
|
|
{
|
|
id: "sample-bittern",
|
|
label: "Bittern & Sons Biologics",
|
|
colorKey: "watching",
|
|
lat: 37.7688,
|
|
lng: -122.3894,
|
|
located: true,
|
|
blurb: "Invented. No sons.",
|
|
},
|
|
|
|
// The Mission, flat and sunny.
|
|
{
|
|
id: "sample-unlikely-weather",
|
|
label: "Bureau of Unlikely Weather",
|
|
colorKey: "offer",
|
|
lat: 37.7602,
|
|
lng: -122.4151,
|
|
located: true,
|
|
blurb: "Invented. Forecasts nobody asked for.",
|
|
},
|
|
{
|
|
id: "sample-perpetual-bagel",
|
|
label: "Perpetual Bagel Works",
|
|
colorKey: "closed",
|
|
lat: 37.7574,
|
|
lng: -122.4192,
|
|
located: true,
|
|
blurb: "Invented. The name was the whole pitch.",
|
|
},
|
|
|
|
// Potrero Hill and Dogpatch, the old industrial edge.
|
|
{
|
|
id: "sample-wobbegong",
|
|
label: "Wobbegong Robotics",
|
|
colorKey: "talking",
|
|
lat: 37.7589,
|
|
lng: -122.4002,
|
|
located: true,
|
|
blurb: "Invented. Named after a carpet shark, for reasons never explained.",
|
|
},
|
|
{
|
|
id: "sample-sourdough-semi",
|
|
label: "Sourdough Semiconductor",
|
|
colorKey: "applied",
|
|
lat: 37.7597,
|
|
lng: -122.3881,
|
|
located: true,
|
|
blurb: "Invented. A fab in a city with no fabs.",
|
|
},
|
|
{
|
|
id: "sample-pennyfarthing",
|
|
label: "Pennyfarthing Power",
|
|
colorKey: "watching",
|
|
lat: 37.7564,
|
|
lng: -122.3973,
|
|
located: true,
|
|
blurb: "Invented. Batteries, uphill.",
|
|
},
|
|
|
|
// The north side and the hills.
|
|
{
|
|
id: "sample-lamplighter",
|
|
label: "Lamplighter Aerostatics",
|
|
colorKey: "watching",
|
|
lat: 37.8004,
|
|
lng: -122.4086,
|
|
located: true,
|
|
blurb: "Invented. Airships, which are always about to come back.",
|
|
},
|
|
{
|
|
id: "sample-halfpenny",
|
|
label: "Halfpenny Optics",
|
|
colorKey: "applied",
|
|
lat: 37.7929,
|
|
lng: -122.4147,
|
|
located: true,
|
|
blurb: "Invented. Lenses, four hundred feet above the water they look at.",
|
|
},
|
|
|
|
// One each in the parts of the city the map usually forgets.
|
|
{
|
|
id: "sample-cormorant",
|
|
label: "Cormorant Freight Systems",
|
|
colorKey: "talking",
|
|
lat: 37.7357,
|
|
lng: -122.3908,
|
|
located: true,
|
|
blurb: "Invented. The only pin south of Islais Creek, which is the point.",
|
|
},
|
|
{
|
|
id: "sample-tidewrack",
|
|
label: "Tidewrack Instruments",
|
|
colorKey: "watching",
|
|
lat: 37.7802,
|
|
lng: -122.4638,
|
|
located: true,
|
|
blurb: "Invented. Sensors, in the fog, on purpose.",
|
|
},
|
|
|
|
// Three with no position yet. See `UNPLACED_LNG`.
|
|
{
|
|
id: "sample-quibble",
|
|
label: "Quibble Quantum",
|
|
colorKey: "applied",
|
|
lat: 37.7965,
|
|
lng: UNPLACED_LNG,
|
|
located: false,
|
|
blurb: "Invented, and unplaced: no address on file, so the map does not invent one.",
|
|
},
|
|
{
|
|
id: "sample-antelope-foundry",
|
|
label: "Antelope Foundry",
|
|
colorKey: "watching",
|
|
lat: 37.7905,
|
|
lng: UNPLACED_LNG,
|
|
located: false,
|
|
blurb: "Invented, and unplaced.",
|
|
},
|
|
{
|
|
id: "sample-mudlark",
|
|
label: "Mudlark Instruments",
|
|
colorKey: "closed",
|
|
lat: 37.7845,
|
|
lng: UNPLACED_LNG,
|
|
located: false,
|
|
blurb: "Invented, and unplaced.",
|
|
},
|
|
];
|
|
|
|
/**
|
|
* Sample traffic over the Bay Area, for when the API is not there to send a
|
|
* flight plan.
|
|
*
|
|
* The corridors are roughly the real ones — arrivals down the peninsula from
|
|
* the north, departures turning out over the Pacific, a slow light aircraft
|
|
* crossing the bay — because that is what makes the sky read as this city's sky
|
|
* rather than as random motion. The callsigns are not: no real operator uses
|
|
* these prefixes, which keeps a demo from looking like a feed of actual
|
|
* traffic. Nothing here is observed, and `flights.ts` explains at length why
|
|
* this project ships a simulator instead of a client for somebody's live data.
|
|
*
|
|
* The unqualified name is a leftover and is kept because the app imports it.
|
|
* Every leg in it is over San Francisco, which is only the right answer for one
|
|
* of the two cities in this build; `sampleRoutesFor` is the entry point that
|
|
* knows the difference.
|
|
*/
|
|
export const SAMPLE_ROUTES: SimRoute[] = [
|
|
{ callsign: "NIMBUS 4", from: [37.95, -122.36], to: [37.66, -122.4], fromAlt: 2400, toAlt: 500, duration: 190 },
|
|
{ callsign: "NIMBUS 17", from: [37.93, -122.31], to: [37.65, -122.38], fromAlt: 2100, toAlt: 450, duration: 210 },
|
|
{ callsign: "PELICAN 2", from: [37.64, -122.39], to: [37.9, -122.62], fromAlt: 700, toAlt: 5200, duration: 165 },
|
|
{ callsign: "PELICAN 31", from: [37.7, -122.21], to: [37.88, -122.55], fromAlt: 1800, toAlt: 6100, duration: 230 },
|
|
{ callsign: "CORMORANT 8", from: [37.62, -122.6], to: [37.95, -122.28], fromAlt: 6800, toAlt: 8200, duration: 260 },
|
|
{ callsign: "KESTREL 5", from: [37.83, -122.56], to: [37.7, -122.22], fromAlt: 1100, toAlt: 1300, duration: 300 },
|
|
{ callsign: "NIMBUS 40", from: [37.96, -122.48], to: [37.63, -122.36], fromAlt: 3100, toAlt: 600, duration: 205 },
|
|
];
|
|
|
|
/**
|
|
* Sample traffic over the Los Angeles basin.
|
|
*
|
|
* The same idea as `SAMPLE_ROUTES` and it exists because that one was being
|
|
* flown over both cities: the SoCal board would come up with a sky whose every
|
|
* aircraft was nearly six hundred kilometres north of it, off the edge of the world
|
|
* and therefore invisible. A city that renders an empty sky looks like a city
|
|
* whose flight layer failed.
|
|
*
|
|
* Again the geography is roughly right and the callsigns are invented. LAX runs
|
|
* west almost always, so the arrivals here come in from the east over the
|
|
* basin and the departures go out over the water before turning; Burbank sits
|
|
* up the valley behind the hills, and the light aircraft is following the coast
|
|
* because in this basin that is what they do.
|
|
*/
|
|
export const SAMPLE_SOCAL_ROUTES: SimRoute[] = [
|
|
{ callsign: "CONDOR 6", from: [34.02, -117.45], to: [33.945, -118.36], fromAlt: 3500, toAlt: 400, duration: 330 },
|
|
{ callsign: "CONDOR 21", from: [33.99, -117.29], to: [33.94, -118.33], fromAlt: 4100, toAlt: 450, duration: 360 },
|
|
{ callsign: "AVOCET 12", from: [33.945, -118.42], to: [34.15, -118.84], fromAlt: 500, toAlt: 5200, duration: 190 },
|
|
{ callsign: "AVOCET 30", from: [33.68, -117.87], to: [33.34, -118.4], fromAlt: 600, toAlt: 4800, duration: 210 },
|
|
{ callsign: "CURLEW 3", from: [33.36, -117.3], to: [33.69, -117.86], fromAlt: 4200, toAlt: 500, duration: 235 },
|
|
{ callsign: "TOWHEE 9", from: [34.33, -118.82], to: [34.2, -118.36], fromAlt: 3000, toAlt: 450, duration: 175 },
|
|
{ callsign: "CONDOR 44", from: [34.36, -118.66], to: [33.32, -117.28], fromAlt: 9200, toAlt: 9800, duration: 300 },
|
|
{ callsign: "SANDPIPER 4", from: [33.6, -118.02], to: [34.03, -118.62], fromAlt: 900, toAlt: 1100, duration: 330 },
|
|
];
|
|
|
|
/**
|
|
* The right sample sky for a city, and something defensible for a city nobody
|
|
* has drawn one for.
|
|
*
|
|
* Keyed on `city.id` rather than on position because the hand-placed corridors
|
|
* are the whole value here: knowing that arrivals come down the peninsula and
|
|
* that LAX departs to the west is knowledge about two named places, and there
|
|
* is no way to derive it from a bounding box. What *can* be derived is legs
|
|
* that are at least in the right region, which is what `syntheticRoutes` does
|
|
* and what any third city gets until somebody sits down with a chart.
|
|
*/
|
|
export function sampleRoutesFor(city: Pick<City, "id" | "center" | "bounds">): SimRoute[] {
|
|
if (city.id === "sf") return SAMPLE_ROUTES;
|
|
if (city.id === "socal") return SAMPLE_SOCAL_ROUTES;
|
|
return syntheticRoutes(regionOf(city));
|
|
}
|
|
|
|
// ---- The office ------------------------------------------------------------
|
|
|
|
/**
|
|
* A fabricated roster for the reference office, so that "sign in" has something
|
|
* to show in a clone with no server behind it.
|
|
*
|
|
* **Every person here is invented**, by the same rule and for the same reason as
|
|
* the companies above: the names are deliberately unmistakable for anyone's
|
|
* — Quillon, Marchetti, Oyelaran-Pike — and none of them works anywhere. Real
|
|
* occupancy is the most private thing this project touches. It says who was at
|
|
* their desk and, by omission, who was not, which is an attendance record; it
|
|
* arrives at runtime over an authenticated endpoint (`routes/presence.ts`) and
|
|
* it never, ever lands in this repo.
|
|
*
|
|
* The demo is worth having for the same reason the markers are. An empty
|
|
* building teaches nobody what the office is *for*, and the entire presence
|
|
* layer — the figures, the seat binding, the hover card that names somebody —
|
|
* is invisible without data that exercises it. It also makes the tier honest
|
|
* locally: `access.ts` hands a clone `member`, and a member who is shown the
|
|
* same empty room as a stranger has been told the tier means something when it
|
|
* does not.
|
|
*
|
|
* The seat ids are real ids from `offices/lumbridge-hq.ts` and have to be: a
|
|
* presence whose seat is not in the plan is dropped by `presence.ts`, silently
|
|
* and correctly, so a typo here is an invisible absence rather than an error.
|
|
* They are also spread deliberately — two meeting rooms mid-session, a full
|
|
* bench, a half-empty one, three focus booths of which one is taken — because a
|
|
* floor where every desk is occupied reads as a texture rather than as people.
|
|
*/
|
|
export const SAMPLE_PRESENCE: Presence[] = [
|
|
// Reception, and someone waiting.
|
|
{ id: "p-01", seatId: "reception-01", label: "Wren Abaddon", colorKey: "in" },
|
|
{ id: "p-02", seatId: "lobby-01", label: "Visitor", colorKey: "guest" },
|
|
|
|
// The eng bench, full along the window row and thinning behind it.
|
|
{ id: "p-03", seatId: "eng-01", label: "Tobias Quillon", colorKey: "in" },
|
|
{ id: "p-04", seatId: "eng-02", label: "Ines Marchetti", colorKey: "in" },
|
|
{ id: "p-05", seatId: "eng-03", label: "Dara Oyelaran-Pike", colorKey: "in" },
|
|
{ id: "p-06", seatId: "eng-04", label: "Sunniva Holt", colorKey: "in" },
|
|
{ id: "p-07", seatId: "eng-05", label: "Casimir Vane", colorKey: "focus" },
|
|
{ id: "p-08", seatId: "eng-06", label: "Perpetua Nkemelu", colorKey: "in" },
|
|
{ id: "p-09", seatId: "eng-08", label: "Rafferty Osgood", colorKey: "in" },
|
|
{ id: "p-10", seatId: "eng-11", label: "Marisol Thibault", colorKey: "focus" },
|
|
|
|
// Ops, about half in.
|
|
{ id: "p-11", seatId: "ops-01", label: "Anouk Sterling", colorKey: "in" },
|
|
{ id: "p-12", seatId: "ops-02", label: "Emeka Farrow", colorKey: "in" },
|
|
{ id: "p-13", seatId: "ops-05", label: "Bettina Kovač", colorKey: "in" },
|
|
{ id: "p-14", seatId: "ops-07", label: "Xiomara Belfry", colorKey: "focus" },
|
|
{ id: "p-15", seatId: "ops-10", label: "Aurelio Pinsent", colorKey: "in" },
|
|
|
|
// Design.
|
|
{ id: "p-16", seatId: "design-01", label: "Halcyon Reeve", colorKey: "in" },
|
|
{ id: "p-17", seatId: "design-03", label: "Zephyrine Mbeki", colorKey: "in" },
|
|
{ id: "p-18", seatId: "design-04", label: "Ptolemy Sandoval", colorKey: "in" },
|
|
|
|
// One focus booth of three, which is the honest ratio.
|
|
{ id: "p-19", seatId: "booth-02", label: "Ottoline Grieve", colorKey: "focus" },
|
|
|
|
// Alcatraz, mid-session — six of ten, which is what a real meeting looks like.
|
|
{ id: "p-20", seatId: "alcatraz-01", label: "Cormac Dellwood", colorKey: "meeting" },
|
|
{ id: "p-21", seatId: "alcatraz-02", label: "Solveig Amankwah", colorKey: "meeting" },
|
|
{ id: "p-22", seatId: "alcatraz-03", label: "Ignatius Pell", colorKey: "meeting" },
|
|
{ id: "p-23", seatId: "alcatraz-06", label: "Rosalind Achebe", colorKey: "meeting" },
|
|
{ id: "p-24", seatId: "alcatraz-07", label: "Vasco Underhill", colorKey: "meeting" },
|
|
{ id: "p-25", seatId: "alcatraz-08", label: "Clementine Roux", colorKey: "meeting" },
|
|
];
|
|
|
|
/**
|
|
* `colorKey` -> colour, for the fabricated roster.
|
|
*
|
|
* Four states and no more, because the palette is the legend whether or not one
|
|
* is drawn: a viewer can tell "at their desk" from "heads-down" from "in a
|
|
* meeting" at a glance, and a fifth shade would be a distinction nobody could
|
|
* name. Amber for focus is the interface's own accent doing what it does
|
|
* everywhere else in this app — marking the thing you are meant to notice —
|
|
* and the guest green is the only hue that is not in the office's own materials,
|
|
* because a visitor is the one person in the building who is not part of it.
|
|
*/
|
|
export const SAMPLE_PRESENCE_PALETTE: Record<string, number> = {
|
|
in: 0x6f9ec4,
|
|
focus: 0xf2b134,
|
|
meeting: 0xc4796f,
|
|
guest: 0x7fb886,
|
|
};
|