Concept F — build handoff
Everything the next step needs: what to build, where, in what order, and what is still undecided. The prototype in the other tab is the behavioural reference — where this document and the prototype disagree, the prototype is a draft and this document is the intent.
Status: direction settled with Sam on 2026-09-26. Not a proposal — the side rail is being replaced. Concepts A–E (two-pane rail, launcher, task-grouping) were considered and rejected; they are not recorded here because none of them is coming back.
1 · The navigation model
Both navigation levels sit on top. There is no left column, so every page runs the
full width. The trade is ~104 px of permanent vertical chrome for ~530 px of width back on
every list page — the right way round for this product, because list pages are its core surface
(docs/08-list-page.md) and they are wide, not tall.
| Level | What it is | Where it renders | Component |
|---|---|---|---|
| L1 | Module | Dark bar, 56 px, top | mise-module-bar new |
| L2 | Section of a module | White strip, 46 px, under L1 | mise-tab-nav extend |
| L3 | Child of a section | Named inside its L2 item; siblings in a dropdown off it | same, via the new children input |
| L4a | A view of one page | In-page tabs, with counts | mise-tab-nav (as used today) |
| L4b | A record's own parts | Inside the record, inline-start | mise-section-nav unchanged |
Four levels is the maximum the product has (confirmed 2026-09-26), so there is no fifth mechanism to design.
The join — the one visual rule that carries the idea
The active module tab has border-radius: 10px 10px 0 0 and no bottom
radius, stretches to the bottom edge of the dark bar, and is painted the same colour as the
section strip beneath it. The two merge into one shape, and the reader sees one control with
a module name attached to a list of its sections — not two stacked strips. Everything else about F
survives a sloppy implementation; this does not.
surface.
The obvious implementation — paint both with the page surface — is correct in light mode and breaks
in dark. Mise's dark surface is #101413 and the rail is #00201b: nearly the
same value, and the page is the darker of the two, so the active tab sinks into the bar
instead of lifting out of it and the join disappears. Give the join its own semantic — the page
surface in light, a raised container in dark (tonal elevation, which is how Mise expresses lift
anyway) — and push the bar a step darker so the tab always rises out of it. The strip does not have
to match the page body; there is a border between them regardless. It has to match the tab.
Check this in dark mode first: it is the state where a wrong choice is invisible in review and
obvious to a user.Why L4a is tabs and not filter chips
The five unhandled-invoices routes load the same component and differ only by
extraFilter today (SUB_TAB_CONFIG in
unhandled-invoices.page.ts) — which argues for chips. They stay tabs
anyway, because a view may grow its own columns or actions and one already has:
delivery-note carries showSendBackToUnhandled: true. Chips promise "same
view, different filter", and that promise is already broken. What they gain is counts,
which they have never shown.
mise-section-nav). Nothing goes in a left column that is not inside a record.2 · Design-system work (Cobalt.DesignSystem.v1)
Three contracts change or appear. Follow docs/07-agent-workflow.md: contract first with
acceptance criteria, then the component, then Storybook evidence in all four theme/density/direction
combinations. All of it obeys the existing hard rules — tokens only, logical properties only
(ADR-007), no theme/density/direction branching in component logic, i18n resolved app-side (ADR-013),
RouterLink for internal navigation (ADR-024).
2.1 mise-module-bar new
A new L4-chrome pattern; needs an RFC under docs/rfc/ per CONTRIBUTING.md,
extending RFC-004's shell layer. Presentational, like every other nav pattern: it renders a resolved
model and emits intents; it never routes, fetches, resolves i18n or owns session state.
selector: 'mise-module-bar'
inputs:
company: MiseNavCompany | null = null // { name, initials, role?, state? }
modules: readonly MiseNavModule[] = [] // same shape mise-nav already takes
activeKey: string | null = null // app-derived from the router
maxVisible: number = 7 // the rest fold into the overflow menu
tools: readonly MiseNavTool[] = [] // search, pins
user: MiseNavUser | null = null
userMenu: readonly MiseNavMenuItem[] = []
collapsed: boolean = false // true at the medium tier: tabs -> one picker
loading: boolean = false
ariaLabel: string = '' // <nav> landmark name (ADR-013)
companyAriaLabel: string = ''
iconTemplate: TemplateRef<{ $implicit: string }> | null = null
outputs:
companyClick: void
toolClick: string
userAction: string
Rules
- Landmark. A
<nav>named byariaLabel. - Company block sits inline-start, before the modules — it is what scopes
everything to its right. A button emitting
companyClick; the app opens its own menu. Whencompany.stateis set (overdue, grace) the chip shows the word under the name, never colour alone (status rule,docs/09-status-taxonomy.md). - Modules are internal
routerLinkanchors; the active one carriesaria-current="page"and the join described above. BeyondmaxVisiblethey fold into an overflow menu (CDK menu) labelled "N more". - The active module is never in the overflow. If it falls past the cut it is promoted into the last visible slot and whatever it displaced goes into the menu. Letting the trigger carry the active module's name instead is not enough: the bar then has no active tab, so the join is gone, the section strip below belongs to nothing visible, and the two bars stop reading as one control. The tab order shifting by one is the cheaper loss.
- Tools (search, pins) and the user block sit inline-end. Menus
are CDK menus, as in
mise-nav. - Collapsed (medium tier) replaces the tab row with a single picker naming the
active module. The app passes the effective state so bar and shell never desync — the same
contract
mise-navhas today. - Theming. Reuse the existing
--nav-*set, mapped from the primary palette ramp exactly asnav.ts:319maps it, so the bar stays dark in both themes. Do not invent a second dark-chrome token set. The one addition is the join surface above, plus a darker--nav-bgin dark mode — the side rail never needed either, because a rail is a column beside the page rather than a bar the page has to rise out of.
2.2 mise-tab-nav extend
The section strip is a mise-tab-nav with two additions. Both are optional, so every
existing consumer is untouched.
interface MiseTabItem {
label: string;
routerLink?: string | readonly unknown[];
href?: string;
testId?: string;
count?: number; // NEW — badge, tabular numerals
children?: readonly MiseTabItem[]; // NEW — opens a CDK menu instead of navigating
}
- An item with
childrenis a menu trigger, not a link: it renders a chevron, opens a CDK menu, and takes the active treatment when any child is active. It does not navigate on its own — picking a child does. - The active section is weight and ink, never an underline. The active module
tab directly above is a filled shape hanging down into this strip; a heavy rule a few pixels
beneath it puts two competing horizontal marks in the same place, and the strip starts reading
as a second tab bar rather than the continuation of the one above. 750 weight on
on-surfaceagainst 600 onon-surface-variantcarries it, andaria-current="page"carries it for assistive tech. The in-page level-4 tabs keep their underline — they sit inside the page with nothing above them to compete with, and there the underline is the conventional affordance. - The active item names its active child, as
Supplier invoices › Unhandled invoices: the parent in--mise-color-on-surface-variant, a chevron separator, the child inon-surfaceat 750, the whole compound reading as one item. Without this the strip stops a level short of where the reader actually is — it shows the group while the page is the child, so nothing on screen but theh1says which child, and a sibling cannot be told apart from another. Inactive items, and active items whose children are unresolved, render the label alone. - The menu marks the active child (
aria-current) and is keyboard-complete from CDK. countrenders as a badge. On the active item it takes the emphasis treatment; on the others it is neutral. Zero renders nothing, not "0".- The strip scrolls horizontally when it overflows, with an edge fade. It must never wrap.
- Items must not shrink.
flex: noneon every item. Without it a flex row squashes its children and clips the text instead of letting the container scroll. This is the horizontal twin of the lesson the vertical rail already wrote down (nav.contract.md, #8332) — same failure, same one-line cause. - It must pan by every input the tier actually has. Touch scrolls natively, with
momentum — do not reimplement that. A wheel does not scroll sideways, so translate vertical wheel
delta to
scrollLeft. A mouse or pen has neither, so drag to pan: engage past a 6 px threshold, capture the pointer, and swallow exactly one click on release so panning never navigates by accident. Engage none of it unless the strip actually overflows. - It must say that it overflows, and bring the active item into view after every navigation. Otherwise you land on a page whose own tab is off-screen and nothing tells you there is more. CSS cannot ask whether a flex row overflowed, so the component sets a flag on resize and on model change and the fade keys off it.
Drill-down below the expanded tier
Scrolling is the fallback, not the answer. Below 60 rem the strip drills: being inside a section that has children shows only that section and its children, behind a back control that returns to the full sibling list.
expanded Dashboard · Supplier invoices ▾ · My attest 3 · Accounting · Reports · Analytics · Settings ▸ Attests
narrow [← Settings] │ Cost centres · Invoice periods · Watch lists · Suppliers · …
- The back control is one wide target carrying the section name, not a bare chevron — the affordance and the "where am I" are the same thing, and it clears 44 px.
- Tapping a parent at this tier drills without navigating. The point is to browse before committing; navigating on the parent tap would defeat it. At the expanded tier the same tap opens the dropdown, because there is room to keep the siblings.
- Back changes the view, never the route. You stay on Attests while you look at the other sections, and can walk into a different one from there.
- Which level is showing is state, not style — a media query cannot express it. The component reads the tier and re-derives on a route change and on crossing the boundary, so a drilled strip never survives a resize to desktop width.
- The drilled list is a nav of links; the back control is a
<button>.
Two levels is the whole depth, so this never becomes a stack to unwind — there is no second back press to design.
2.3 mise-command-palette new
An overlay search over a supplied index. Presentational: the app builds the index and executes the choice.
inputs:
groups: readonly { kind: string; items: readonly MisePaletteItem[] }[] = []
loading: boolean = false
placeholder: string = '' // resolved app-side (ADR-013)
outputs:
queryChange: string // debounced; the app re-supplies groups
choose: MisePaletteItem
secondary: MisePaletteItem // Shift+Enter — the app decides it means "pin"
- Keyboard:
↑↓move,Enterchoose,Shift+Entersecondary,Escclose. The trigger keystroke (⌘K/Ctrl+K) belongs to the app, not the component. - Matching text is highlighted; the component never ranks — the app supplies the order.
- At the compact tier it is full-screen rather than a centred dialog.
2.4 mise-app-shell extend
≤60rem
(app-shell.ts:82). F has no nav column — it has two stacked rows above the content. The
shell needs a second layout mode (orientation: 'inline-start' | 'block-start'), or F
ends up fighting the grid. Ship this with the module bar, not after it.2.5 mise-nav retire, later
Keep it working and contracted until the feature flag in §6 is removed for a full release. Then
deprecate it per SUPPORT.md. Do not delete it while the rollback path still matters.
2.6 mise-section-nav unchanged
No code change. Its scope narrows: it is for a record's own sections over a narrow body,
never a module or section index. The contract's "settings hub" wording should be tightened so the
next person does not reach for it the way invoices-shell.page.ts did.
2.7 Tokens
No new tokens and no new breakpoints. tokens/core/dimension.json
already defines compact 599 / medium 959 / expanded 1439 / content 1680, and
60 rem — the shell's current collapse point — is the medium/expanded seam. Two component tokens
for the two bar heights are reasonable; anything more means the design drifted.
3 · App work (Cobalt.WEB.v3)
3.1 core/module-map.ts
The map gains a third level and two display fields. It stays CONFIG, NOT CODE — the whole point of that file is that a nav change is a one-line edit.
export interface ModuleNavChild {
labelKey: string;
path: string;
v3Native?: boolean;
moduleCodeIndex?: number;
permission?: string;
/** NEW — L3. A child with grandchildren renders as a dropdown in the section strip
* and never navigates itself; the first visible grandchild is its default target. */
children?: readonly ModuleNavGrandchild[];
/** NEW — badge source key, resolved by the shell (see 3.5). Never a literal number. */
countKey?: string;
}
Concrete edits, all of them mechanical:
- InvoiceIQ.
invoiceIQ/invoicesgains three children —unhandled-invoices,archive,documents— and itslabelKeybecomesK.supplierInvoices, notK.invoice. That is the label the route itself already uses (invoices.routes.ts,data.breadcrumb), and the rail's "Invoice" was the odd one out. - InvoiceIQ settings. Its eight lookups become children of
invoiceIQ/settings, which deletes the second left rail inside that page. - KitchenFlow.
inventoryandpurchasinggain their children. - Administration. Its 21 children regroup under six parents — Overview, Company, People, Catalogue, Finance, System. Provisional, and shipping that way (Sam, 2026-09-27): mark them provisional in the map and let someone who knows the work correct them. What the contract fixes is the mechanism, not the names — regrouping is a one-line config change, and Administration is the case that justifies the group dropdown at all, so blocking on names would leave it without a worked example.
module-map.spec.tsalready asserts the administration set explicitly because a new entry that forgetsv3Nativesilently becomes a dead v2 deep-link. Extend the same assertion to the new level — a grandchild that forgets it has the same failure mode.
3.2 The shell
shell/aside/aside.component.tsbecomesshell/module-bar/. Most of it survives verbatim:isModuleVisible/visibleChildrenfiltering, Transloco resolution,matchesRoutePrefix, thematchMedia('(max-width: 60rem)')watcher, the company/user/tool view models. What changes is which component it feeds.- A new
shell/section-strip/resolves the active module's children (and grandchildren) intoMiseTabItem[]. - The breadcrumb header goes away. The two bars are the trail. Page identity moves to the page title block.
--baz-shell-header-block-size is not only
the header's height — styles.scss ties
--mise-side-sheet-inset-block-start to it so the right side-sheet docks flush under the
bar (board 7494). Deleting the header without re-pointing that token silently misaligns every
side sheet in the app. Re-point it at the module bar + section strip height.3.3 Active-route resolution
One rule, applied at three levels, reusing today's matchesRoutePrefix (exact match or
a /-delimited descendant — never a bare substring):
- The module is the one whose route, or any descendant route, prefixes the current URL.
- The section is the child that prefixes it; a section with grandchildren is active when any grandchild is.
- The grandchild is the exact match, and it names the page title.
A route that resolves to no module is a bug, not a blank bar: log it and fall back to Home.
3.4 Pins
A small PinsService over an ordered list of paths. They must be per user and
per company — a pin to a Wholesale page is meaningless in a company without Wholesale, so
filter on read against the same moduleCodeIndexes gate. There is no user-preference
endpoint for this yet; the prototype uses localStorage and that is an acceptable interim,
but it is a real API ask, not an oversight (file it on the WEB.v3 board with the
DS(sam): convention if the DS has to carry it).
3.5 The palette index and the counts
- Pages come from the module map, already filtered by company modules and
permission— so the palette can never offer a page the user cannot open. - Records are a debounced API search; they are the only part that is not local.
- Companies come from the session store, which already preloads the list.
- Actions are out of scope for the first wave. Ship the group empty rather than faking it.
- Counts (
countKey) resolve through one small registry so the map stays config. Only add a count where a number already exists — unhandled invoices, my attest, open sales orders. A count that needs a new endpoint is a separate ask, and a wrong count is worse than no count.
3.6 Pages that lose their in-page navigation
features/invoice-iq/pages/invoices/invoices-shell.page.ts— delete itsMiseSectionNav; the three sections move to the strip. The routes do not change.features/invoice-iq/pages/settings/— same, for its left rail.unhandled/unhandled-shell.page.tskeeps itsmise-tab-nav. It only gains counts.
4 · Responsive contract
| Tier | Width | L1 | L2/L3 | Page |
|---|---|---|---|---|
| Large | ≥ 1440 | As expanded. Both bars stay full-bleed. | Body caps at content-max 1680. Chrome must never look inset. |
|
| Expanded | 960–1439 | Full tab row + overflow. Under ~1100 the search button drops its label. | Strip in full. | Reference layout. |
| Medium tablet | 600–959 | Tabs fold into one module picker; company chip → avatar. | Strip drills: inside a section with children it shows that section and its children behind a back control, instead of scrolling the full sibling list. | Priority columns. Filter drawer always an overlay (ADR-014). Chrome ≈ 88 px. |
| Compact phone | ≤ 599 | No top chrome at all. No header, no section strip. L1, L2 and L3 all live in one full-screen sheet opened from a bottom bar: Start · Search · Menu. The sheet carries the account too, because with the header gone there is nowhere else for it. It does not carry the company switcher — see below. | Rows become cards (ADR-014). L4 tabs stay tabs, scrolling. | |
A hamburger in the top-inline-start corner is the worst-reachable point on a phone, which is why the compact tier moves navigation to the bottom rather than shrinking the desktop shape. A record's section nav becomes a tab row at this tier — the single place mobile departs from desktop F.
- The sibling switch. Unhandled → Archive is one tap on the strip and four through the sheet. If phone users really move between siblings, the drill-down strip earns its 100 px back and should return.
- Company context and switching. The company switcher is a PC and tablet control only — the phone has a different navigation system and identity switching is not part of it. That is a deliberate call, and it has two consequences worth stating rather than discovering: a multi-company user cannot change company from a phone at all, and with no top chrome and no company card in the sheet, nothing in the navigation says which company they are acting for. Posting an invoice into the wrong company is a real and expensive mistake, so the phone home screen should name the current company — that is page content, not chrome, and it keeps the signal without reopening the header.
- Phone landscape is 844 × 390 — the medium tier, not the compact one. So turning a phone sideways swaps the bottom bar for a module picker, and the thumb-reachable navigation the compact tier was designed around disappears. It is also only 390 px tall, so 104 px of chrome is 27% of the screen. Decide deliberately: either the compact tier keys off height as well as width, or landscape phone is accepted as a rare, degraded case. Do not let it be an accident.
- Tablet landscape is 1024 × 768 — the expanded tier, so it gets the full desktop layout including the module tab row. That one is a happy accident and needs no work, but it means the tablet design only ever appears in portrait.
5 · Accessibility contract
- Both bars are
<nav>landmarks with distinct accessible names ("Modules", "Sections") — two unnamed navs is a failure, not a nitpick. - Every nav is a nav of links in native DOM order. No roving tabindex: that is
the correct model for links, and it is what
mise-tab-navalready does. - The active item carries
aria-current="page", derived from the router, never a managed input. - A section with children is a
<button aria-expanded>opening a CDK menu — arrow/Home/End/Esc come free. - Touch targets ≥ 44 px at the compact and medium tiers.
- The palette is a modal dialog: focus moves in on open, is trapped, and returns to the trigger on close. Results are announced as the count changes.
- Depth is indent, weight and ink — never a third font size. The phone menu listed modules, their sections and their children at three different sizes on top of three different weights and three different inks: three signals doing one job, so the list read as accidental rather than hierarchical. One scale, with each size given a job (15 for anything tapped at the top level, 14 for the nested levels, smaller only for supporting text), and let indent carry the depth.
- A menu trigger must look like the other menu triggers. The account control was
a flat circle of initials on a bar the same colour — no border, no chevron, nothing saying it was
a control at all, let alone one that opens a menu. The company chip at the other end of the same
bar had all three. Matching them is what makes it read without a label: a marker, a pill, a
chevron, at both ends. Every trigger also carries
aria-haspopupand a livearia-expanded, and the menu it opens names the person or company it belongs to — two initials are not an identity. - Do not reserve a column for an icon only one row uses. A leading tick gutter
puts a permanent empty channel down a whole menu to mark one item that is already marked — the
selected row carries a filled surface and
aria-current, which is both the stronger visual signal and the one assistive tech actually reads. Same for the lock glyph on a blocked row when a "Locked" chip already says the word. Reserve space for what every row has, not for what one row might. - A dismiss control that needs its own row is the wrong dismiss control. The
phone menu's ✕ sat alone above the content and cost a whole row on the shortest screen in the
product. The fix was not to shrink it: the sheet now stops above the bottom bar, the bar stays
on top, and Menu becomes the toggle — its icon turns to ✕, it carries
aria-expanded, and the row disappears. The bar you opened it from is the obvious place to close it, and Start and Search stay reachable the whole time. Inside it, the company switcher goes back to the menu rather than closing outright: a sub-task should return to where it was opened from, not dump you on the page. - A control has to look like one. The company switcher was a line of text with the words "tap to switch" on it and a bare chevron next to a close button — the copy doing the work the affordance should do. It is now a full-width button with its own surface, border and picker chevron, so the words can describe the company instead of the interface. Anywhere a label has to explain that something is tappable, the control is the thing that is wrong.
- The palette is a different control on a phone, not a smaller one. There is no Esc, no arrow keys and no ⇧⏎, so the keyboard-hint footer is a lie that eats the one strip of screen a thumb can reach — it goes, and an on-screen Cancel beside the field replaces it, because the bottom bar it was opened from is now covered. The active-row highlight also goes: it means "this is what Enter will open", and on touch nothing moves it and nothing presses Enter, so it reads as a selected row that is not one. It returns the moment an arrow key is pressed. Rows stack to two lines — the trail is routinely longer than the label it belongs to — and the field is 16 px or iOS zooms the whole page on focus.
- Status words never rely on colour alone — "Payment overdue", "Locked", not a red dot.
[hidden]must actually hide. The UA rule behind it is weaker than any class that setsdisplay, so a component that toggleshiddenand also stylesdisplay: flexon the same element shows regardless — silently, and only in the state nobody screenshots. Either ship[hidden] { display: none }at the app's style root or never togglehiddenon a flex/grid container.- RTL comes from logical properties only (ADR-007), including the overflow menu's anchor and the
strip's scroll fade. No
left/right.
6 · Build order
Four pieces. Each is useful on its own, and none is a one-way door.
- Section strip — extend
mise-tab-navwithchildrenandcount, then land it inside one module: InvoiceIQ › Supplier invoices. It deletes that page's inner left rail immediately, which is a visible win with no shell change at all. - Palette + pins — additive. Ships on today's rail and stays correct whatever happens to the chrome.
- Module bar + shell orientation — the new chrome. Behind a feature flag, with the rail still buildable, because this is the only irreversible-looking step.
- Retire the rail — once the flag has been on for a full release. Then deprecate
mise-navperSUPPORT.md.
Steps 1 and 2 are worth shipping even if step 3 never happens.
7 · Defects to fix
Both were found while designing this and both are worth fixing regardless of what the navigation ends up looking like.
Company switch can land on a dead route
shell/company-switch.service.ts:58 re-navigates to the same URL after a switch,
unconditionally. Companies enable different modules, so switching into one that lacks the current
module leaves the user on a route it cannot serve.
Rule to adopt: keep the route when the new company has the module; otherwise go to Home and say why ("Wholesale isn't enabled for X — showing Home."). The prototype implements this; the shipped code does not.
The card fallback fires at two different widths
ADR-014 says rows collapse to cards "below the medium breakpoint" (≤ 959 px), but
data-table.ts implements it as a 32 rem container query
(CARD_BREAKPOINT_REM = 32, 512 px) — so a 768 px tablet keeps the table. The
prototype and §4 follow the code, because a table with priority columns reads better on a tablet
than a column of cards. Pick one and make the ADR and the implementation agree.
8 · Open questions
- How many modules does a real company have enabled? Every module row is gated by
moduleCodeIndexes, so ten is the internal worst case, not the customer's view. Under about seven the overflow menu never opens and F is clean. One query over the company-module table settles it, and it is the only number that could still change the design. - Who owns grouping Administration's 21 children? The six groups here are a designer's guess. They need someone who knows what administrators actually do.
- Where do pins live? Per-user storage does not exist in the shell. Interim
localStorageis fine for wave 2; a real endpoint is an API ask.
9 · Acceptance criteria
Per the Mise convention, every contract ends with criteria. These are the cross-cutting ones — each component contract adds its own.
- Tokens only; no raw hex, no hardcoded px for spacing, radius or type; logical properties throughout.
- The active module tab and the section strip render as one continuous shape at every tier where both are visible — verified in light and dark.
- No page in the app has a left navigation column except a record detail.
- Administration is reachable to all 21 children in at most two interactions from any page.
- ⌘K finds every page the current user can open in the current company, and no page they cannot.
- A company switch never leaves the user on a route the new company cannot serve.
- At 390 px there is no horizontal page scroll and every nav target is ≥ 44 px.
- Keyboard-only: reach any L1, L2, L3 and L4 destination, and open and dismiss every menu.
- Screen reader: both bars announce as named navigation landmarks; the current page announces as current.
- Storybook evidence attached for all four theme/density/direction combinations.
- The side sheet still docks flush under the chrome (the
--baz-shell-header-block-sizegotcha in §3.2).
10 · File map
| Path | What happens |
|---|---|
| Cobalt.DesignSystem.v1 | |
docs/rfc/RFC-0NN-module-bar.md | New — extends RFC-004's shell layer |
docs/contracts/module-bar.contract.md | New |
docs/contracts/command-palette.contract.md | New |
docs/contracts/tab-nav.contract.md | children + count |
docs/contracts/app-shell.contract.md | Second orientation |
docs/contracts/section-nav.contract.md | Narrow the stated scope to record detail |
docs/contracts/nav.contract.md | Mark superseded once the flag retires |
docs/adr/ADR-014-…md | Reconcile the card-fallback width with the code |
angular/projects/mise/src/lib/patterns/… | module-bar, command-palette, tab-nav, app-shell |
| Cobalt.WEB.v3 | |
src/app/core/module-map.ts | L3 children, countKey, the Supplier-invoices rename |
src/app/core/module-map.spec.ts | Extend the explicit assertions to L3 |
src/app/shell/aside/ | → shell/module-bar/ + shell/section-strip/ |
src/app/shell/header/header.component.ts | Breadcrumb retires; re-point the side-sheet token |
src/app/shell/layout/shell-layout.component.ts | Shell orientation |
src/app/shell/company-switch.service.ts | §7 fix |
src/app/shell/preferences.service.ts | asideCollapsed is meaningless; pins land near here |
…/invoice-iq/pages/invoices/invoices-shell.page.ts | Drop the section-nav column |
…/invoice-iq/pages/settings/ | Drop the left rail |
e2e/shell/shell-nav-clickable.spec.ts | Rewrite — it asserts the rail |
e2e/admin/admin-nav-targets.spec.ts | Rewrite for the grouped strip |
| Cobalt.Coordination | |
mockups/nav/ | This file and its README — the behavioural reference |
A hazard of the mockups/ house convention
mise.css is inlined whole into this file, so every class name in it is in
scope — including short ones a mockup reaches for naturally: .sec,
.tab, .tabs, .app, .main, .lead,
.strong, .msg. Reusing one means Mise's rules and the mockup's rules
fight, and the loser is whichever is written first or less specifically. Every class this prototype
owns is therefore prefixed f-. Worth the same rule in the other mockups under
mockups/; at least worth knowing before the next unexplained layout bug.
If you read only one thing
Build the section strip first (§6.1) inside InvoiceIQ › Supplier invoices. It is small, it deletes a second left navigation on a real page, it needs no shell change, and it proves the one mechanism that everything else in F depends on.