@joanroucoux/cairn-ui 0.11.2 → 0.11.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/fesm2022/joanroucoux-cairn-ui-async.mjs +8 -2
- package/fesm2022/joanroucoux-cairn-ui-async.mjs.map +1 -1
- package/fesm2022/joanroucoux-cairn-ui-row.mjs +2 -2
- package/fesm2022/joanroucoux-cairn-ui-row.mjs.map +1 -1
- package/fesm2022/joanroucoux-cairn-ui-table.mjs +1 -1
- package/fesm2022/joanroucoux-cairn-ui-table.mjs.map +1 -1
- package/mcp-manifest.json +1 -1
- package/package.json +1 -1
- package/types/joanroucoux-cairn-ui-async.d.ts +1 -1
- package/types/joanroucoux-cairn-ui-table.d.ts +1 -1
package/mcp-manifest.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"schemaVersion":1,"package":{"name":"@joanroucoux/cairn-ui","version":"0.11.2","peerDependencies":{"@angular/common":"^22.0.0","@angular/core":"^22.0.0"}},"storybookUrl":"https://joanroucoux.github.io/cairn-ui/","setup":"Import three stylesheets, in order, and register the package as a Tailwind source (templates in `node_modules` are\nnot scanned by default):\n\n```css\n@import 'tailwindcss';\n@import '@joanroucoux/cairn-ui/styles/tokens.css';\n@import '@joanroucoux/cairn-ui/styles/theme.css';\n@import '@joanroucoux/cairn-ui/styles/motion.css';\n@source '../node_modules/@joanroucoux/cairn-ui';\n```\n\n`fonts.css` self-hosts Rubik and must not be imported through the Tailwind entry above: its `@font-face` rules use\n`url(./fonts/...)` paths relative to the file, and Tailwind's CSS-first pipeline does not rebase them. Add it to the\n`styles` array of `angular.json` instead:\n\n```json\n\"styles\": [\"src/styles.css\", \"node_modules/@joanroucoux/cairn-ui/styles/fonts.css\"]\n```\n\nAn application that does not use Tailwind can import `@joanroucoux/cairn-ui/styles/tokens.css` alone: it is a pure\ntoken sheet.","entries":[{"name":"action-bar","importPath":"@joanroucoux/cairn-ui/action-bar","declarations":[{"kind":"component","className":"UiActionBar","selector":"ui-action-bar","description":"Bar of actions fixed above the tab bar on iPhone, hidden from `64rem`, where the same actions\nlive in the page itself. Its children share the width equally. While it is on the page it\npublishes its height as `--action-bar-height` on the root element, which `ui-toaster` clears.","examples":["<ui-action-bar>\n <button ui-button variant=\"outline\" size=\"tall\">Sell</button>\n <button ui-button size=\"tall\">Buy</button>\n</ui-action-bar>"],"inputs":[],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Surfaces/Action bar","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/surfaces-action-bar--docs","summary":"Bar of actions fixed above the tab bar on iPhone, such as Sell and Buy on a holding's\ndetail: the primary actions, within thumb reach. It sits `--tab-bar-height` (52px plus the safe area) above the\nbottom edge, so it clears `ui-tab-bar`, draws a hairline on top over the page background, pads with the\ngutter, and splits its width equally between its children. It is hidden from `64rem`, where the same\nactions live in the page.\n\nPad the scrolling content by the bar's height (about 73px) so its end is not hidden behind it.\n\nWhile it is on the page the bar publishes its height as `--action-bar-height` on the root element:\n`ui-toaster` adds it to its offset, so a confirmation sits 8px above the bar and never covers the actions.","whenToUse":"* For the one or two actions that define a detail screen on a phone.","whenNotToUse":"* On a desktop, where the bar is hidden: place the actions in the page.\n* For navigation. That is the [Tab bar](?path=/docs/data-display-tab-bar--docs).","accessibility":"* A plain container: the actions inside keep their native button semantics and focus order.\n* Use `size=\"tall\"` on the buttons so each reaches a 48px touch target.","args":[]}]},{"name":"alert","importPath":"@joanroucoux/cairn-ui/alert","declarations":[{"kind":"component","className":"UiAlert","selector":"ui-alert","description":"Inline message explaining a failure or a consequence, or static help with `info`: an icon, an optional heading and a text.\nIt fades in when it appears; set `[fadeIn]=\"false\"` on an alert that is part of the page as it\nopens.","examples":["<ui-alert heading=\"Sign-in failed\">Try again.</ui-alert>"],"inputs":[{"name":"variant","type":"'error' | 'warning' | 'info'","required":false,"default":"'error'","twoWay":false},{"name":"heading","type":"string | undefined","required":false,"twoWay":false},{"name":"fadeIn","type":"boolean","required":false,"default":"true","attribute":true,"twoWay":false}],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[{"name":"ALERT_VARIANTS","type":"readonly string[]","values":["error","warning","info"]}],"types":[{"name":"AlertVariant","type":"'error' | 'warning' | 'info'"}],"docs":[{"title":"Feedback/Alert","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/feedback-alert--docs","summary":"Inline message that explains why something failed, or what an action is about to cost: a\nLucide icon in the `--negative` color, an optional heading and a text.","whenToUse":"* To report a failed submission next to the form that caused it, such as a sign-in error.\n* To explain a step with static help, such as what a manual entry cannot verify (`info` variant).\n* To warn, right above a confirmation, about an irreversible consequence (`warning` variant).","whenNotToUse":"* For a failed data load inside a block. Use [Async](?path=/docs/feedback-async--docs), which offers a retry.\n* For a label on a row. Use [Badge](?path=/docs/data-display-badge--docs).","accessibility":"* The `error` variant is `role=\"alert\"`, announced as soon as it appears. The `warning` variant is\n `role=\"status\"`, announced politely.\n* The `info` variant is `role=\"note\"`: read in place, never announced.\n* The icon is decorative and hidden from assistive technology: the text carries the message.","args":[{"name":"variant","description":"`error` is a bordered card with the circle icon. `warning` is a bare inline notice with the triangle icon. `info` is a `--muted` box with the circle-i icon in the text color, a static note."},{"name":"heading","description":"Optional heading. The text below it turns muted."},{"name":"text","description":"Projected text content."},{"name":"fadeIn","description":"Fades the alert in over `--duration-fast` when it appears (default). Turn it off for an alert that is part of the page as it opens, such as an error state after a load: that one shows without motion."}]}]},{"name":"amount","importPath":"@joanroucoux/cairn-ui/amount","declarations":[{"kind":"component","className":"UiAmount","selector":"ui-amount","description":"A number or an amount of money, formatted for the locale and masked when the app hides amounts.","examples":["<ui-amount [value]=\"total\" currency=\"EUR\" numeric=\"proportional\" />\n<ui-amount [value]=\"dayChange\" currency=\"EUR\" signed whenMasked=\"hide\" />"],"inputs":[{"name":"value","type":"number | null | undefined","required":false,"twoWay":false},{"name":"currency","type":"string | undefined","required":false,"twoWay":false},{"name":"locale","type":"string","required":false,"default":"inject(LOCALE_ID)","twoWay":false},{"name":"signed","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"fractionDigits","type":"number","required":false,"default":"2","twoWay":false},{"name":"numeric","type":"'tabular' | 'proportional'","required":false,"default":"'tabular'","twoWay":false},{"name":"whenMasked","type":"'dots' | 'hide'","required":false,"default":"'dots'","twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[{"name":"formatAmount","signature":"formatAmount(value: number | null | undefined, options: AmountFormatOptions, masked?: boolean): string","description":"Formats a number or an amount for the locale. A missing value is an em dash, a masked one four bullets.","examples":[]}],"constants":[{"name":"AMOUNT_MASKINGS","type":"readonly string[]","values":["dots","hide"],"description":"Available masking behaviors."},{"name":"AMOUNT_NUMERICS","type":"readonly string[]","values":["tabular","proportional"],"description":"Available figure styles."},{"name":"UI_AMOUNT_MASKED","type":"InjectionToken<Signal<boolean>>","description":"App-wide \"amounts are hidden\" state. Defaults to never masked; the app overrides it."}],"types":[{"name":"AmountMasking","type":"'dots' | 'hide'"},{"name":"AmountNumeric","type":"'tabular' | 'proportional'"},{"name":"AmountFormatOptions","type":"{\n locale: string;\n currency?: string;\n signed?: boolean;\n fractionDigits?: number;\n}"}],"docs":[{"title":"Data display/Amount","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-amount--docs","summary":"Formats a number or an amount of money for the locale: two decimals, the currency symbol\nafter the figure with a no-break space, the typographic minus (U+2212) on a negative value, an\nexplicit plus on a signed gain. A missing value renders as an em dash. Screens never format an\namount themselves: this is the only place that does.\n\nMasking comes from the `UI_AMOUNT_MASKED` injection token, a signal the app provides (Cairn's\n“Hide amounts” setting). By default nothing is masked.","whenToUse":"* For every euro amount, quantity, average cost or price shown to the user.\n* For the one dominant figure on a screen, with `numeric=\"proportional\"` so its digits are not\n forced into a tabular grid that was designed for lists.","whenNotToUse":"* For a percentage: percentages stay visible even when amounts are masked, so they never go\n through `ui-amount`.\n* For a signed change that sits right next to its own percentage. Pass `whenMasked=\"hide\"` so the\n pair disappears together instead of leaving a masked half next to a visible one.","accessibility":"* A masked value still renders as text (four bullets, or nothing with `whenMasked=\"hide\"`, which\n sets the native `hidden` attribute): a screen reader reads it as it is, so a screen that masks an\n amount must not rely on it alone to convey meaning.","args":[{"name":"value","description":"The number to format. `null` or `undefined` render as an em dash."},{"name":"currency","description":"ISO 4217 code. Omitted for a plain number, such as a quantity."},{"name":"signed","description":"Prefixes a gain with `+` and a loss with the typographic minus."},{"name":"numeric","description":"`tabular` aligns figures in lists and tables. `proportional` suits the one dominant figure."},{"name":"whenMasked","description":"`dots` shows four bullets in place of the figure. `hide` removes the element entirely."},{"name":"fractionDigits","description":"Decimal places. Defaults to two."}]}]},{"name":"async","importPath":"@joanroucoux/cairn-ui/async","declarations":[{"kind":"component","className":"UiAsync","selector":"ui-async","description":"The three states of a block that depends on a service call, next to the ready content. During the\nfirst 150 ms of a load the `[asyncLoading]` content is laid out but invisible and hidden from\nassistive technology, so the block already holds its skeleton's space.","examples":["<ui-async [state]=\"totalState()\" errorTitle=\"The total couldn’t be loaded\" errorMessage=\"The server didn’t respond.\" retryLabel=\"Try again\" (retry)=\"reload()\">\n <span asyncLoading><ui-skeleton shape=\"figure\" /></span>\n <span asyncEmpty>Add a holding to see your total.</span>\n <span>{{ total() }}</span>\n</ui-async>"],"inputs":[{"name":"state","type":"'loading' | 'error' | 'empty' | 'ready'","required":true,"twoWay":false},{"name":"errorTitle","type":"string | undefined","required":false,"twoWay":false},{"name":"errorMessage","type":"string | undefined","required":false,"twoWay":false},{"name":"retryLabel","type":"string | undefined","required":false,"twoWay":false},{"name":"retryIcon","type":"boolean","required":false,"default":"true","attribute":true,"twoWay":false,"description":"Draws the rotate icon in the retry button. Only the `inline` variant honors `false`."},{"name":"variant","type":"'elevated' | 'plain' | 'emphasis' | 'inline'","required":false,"default":"'elevated'","twoWay":false},{"name":"align","type":"'start' | 'center' | 'auto'","required":false,"default":"'start'","twoWay":false},{"name":"card","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"fill","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[{"name":"retry","type":"void"}],"slots":["[asyncLoading]","[asyncEmpty]","*"]}],"services":[],"functions":[{"name":"delayedState","signature":"delayedState(source: Signal<AsyncState>): Signal<AsyncState | null>","description":"Turns the live state of a call into the state to draw: `loading` only appears once the call has\nlasted 150 ms and, once shown, stays at least 400 ms. Until then the previous state stays on\nscreen; `null` means no state to draw yet (a first load under 150 ms), which `ui-async` renders as an invisible skeleton. Call it in an injection context.","examples":["protected readonly shown = delayedState(this.state);"]}],"constants":[{"name":"ASYNC_ALIGNS","type":"readonly string[]","values":["start","center","auto"],"description":"Alignments of the error block; `auto` is centered from 64rem."},{"name":"ASYNC_STATES","type":"readonly string[]","values":["loading","error","empty","ready"],"description":"Available async block states."},{"name":"ASYNC_VARIANTS","type":"readonly string[]","values":["elevated","plain","emphasis","inline"],"description":"Looks of the error block."}],"types":[{"name":"AsyncAlign","type":"'start' | 'center' | 'auto'"},{"name":"AsyncState","type":"'loading' | 'error' | 'empty' | 'ready'"},{"name":"AsyncVariant","type":"'elevated' | 'plain' | 'emphasis' | 'inline'"}],"docs":[{"title":"Feedback/Async","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/feedback-async--docs","summary":"The three states of a block that depends on a service call: a loading skeleton, an error\nshown in place with a retry button, or an empty state. Whichever state is active, the block's own\ntitle stays visible above it: that title is not part of this component, the caller renders it.\n\nOne call, one block, its three states: a call that fails never erases the rest of the screen.\n\nThe error block has four looks. `elevated` (the default) is a tinted box with the token line heights.\n`plain` has no surface and the normal line height, for a block that sits in a card or a table the\nscreen already draws. `emphasis` is a heading, a longer message and a primary retry button, for a\nlist that is the whole page. `inline` is one row, message left and retry right, for a failure\ninside a form. `align` centers the block (`auto`: from 64rem), `card` draws a card around it and\n`fill` makes it fill a host the screen sizes.","whenToUse":"* Around any content that comes from an HTTP call: a total, a chart, a list.\n* When two blocks on the same screen depend on different calls: give each its own `ui-async`, so\n one failing never hides the other.","whenNotToUse":"* Around content that never depends on a call, such as static labels or client-side computed\n values.\n* For a full-page error. `ui-async` covers one block; a screen where every block failed shows its\n own page-level error instead.","accessibility":"* The host carries `aria-busy=\"true\"` while loading.\n* The error block is `role=\"alert\"` inside an `aria-live=\"polite\"` region, so assistive technology\n announces it as soon as it appears.","args":[{"name":"title","description":"The block's own title, rendered by the caller above the three states."},{"name":"state","description":"Which slot is currently shown."},{"name":"errorTitle","description":"Error state title, next to the block name."},{"name":"errorMessage","description":"Error state detail sentence."},{"name":"retryLabel","description":"Label of the retry button."},{"name":"retryIcon","description":"Draws the rotate icon in the retry button (default). Turn it off for a text-only retry, as in a per-source search error. Only the `inline` variant honors it."},{"name":"variant","description":"Look of the error block: `elevated` tinted box, `plain` no surface, `emphasis` heading and primary retry, `inline` one row."},{"name":"align","description":"Alignment of the error block. `auto` is the start below 64rem and centered from 64rem."},{"name":"card","description":"The error block draws its own card (background, container radius, hairline)."},{"name":"fill","description":"The error block fills its host and centers vertically; the host sets the minimum height."},{"name":"retry","description":"Emitted when the retry button is pressed. Only that block reloads."}]}]},{"name":"avatar","importPath":"@joanroucoux/cairn-ui/avatar","declarations":[{"kind":"component","className":"UiAvatar","selector":"ui-avatar","description":"Initials in a disc, standing in for a person.","examples":["<ui-avatar initials=\"JR\" label=\"My account\" />"],"inputs":[{"name":"initials","type":"string","required":true,"twoWay":false},{"name":"label","type":"string","required":true,"twoWay":false},{"name":"size","type":"'sm' | 'md' | 'lg' | 'auto'","required":false,"default":"'md'","twoWay":false}],"outputs":[],"slots":[]},{"kind":"directive","className":"UiAvatarLink","selector":"a[uiAvatarLink], button[uiAvatarLink]","description":"Round hit area around a `ui-avatar` that links somewhere. On the current page\n(`aria-current=\"page\"`, set by `routerLinkActive` through `ariaCurrentWhenActive`) it holds a\nsoft halo.","examples":["<a uiAvatarLink routerLink=\"/profile\"><ui-avatar initials=\"JR\" label=\"Profile\" size=\"auto\" /></a>"],"inputs":[],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[{"name":"AVATAR_SIZES","type":"readonly string[]","values":["sm","md","lg","auto"],"description":"Available avatar sizes."}],"types":[{"name":"AvatarSize","type":"'sm' | 'md' | 'lg' | 'auto'"}],"docs":[{"title":"Data display/Avatar","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-avatar--docs","summary":"Initials in a disc, standing in for a person.","whenToUse":"* As the account entry in the shell. Cairn is a single user application, so exactly one avatar is on\n screen at a time.","whenNotToUse":"* As a decorative shape. It carries an image role and an accessible name, both of which cost a\n screen reader something to announce.\n* To display an uploaded picture. This component draws initials only.","accessibility":"* The host carries `role=\"img\"` and takes its accessible name from `label`.\n* The initials are `aria-hidden`, so a screen reader announces the real name instead of spelling\n out two letters.","args":[{"name":"initials","description":"Two-letter (or so) glyph rendered inside the disc. Purely decorative: hidden from assistive tech."},{"name":"label","description":"Accessible name, announced instead of the initials. Usually the full name."},{"name":"size","description":"Disc diameter: `sm` 32 px, `md` 34 px (iPhone header), `lg` 56 px (Profil), `auto` 34 px then 32 px from 64rem (the app header)."}]}]},{"name":"back-link","importPath":"@joanroucoux/cairn-ui/back-link","declarations":[{"kind":"component","className":"UiBackLink","selector":"a[ui-back-link], button[ui-back-link]","description":"Link back to the parent screen: a chevron and the name of the destination. On a `button`, it\ngoes back inside the current view, such as from a form to the search that led to it.","examples":["<a ui-back-link routerLink=\"/accounts\">Accounts</a>"],"inputs":[{"name":"size","type":"'md' | 'sm' | 'header' | 'inline'","required":false,"default":"'md'","twoWay":false}],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[{"name":"BACK_LINK_SIZES","type":"readonly string[]","values":["md","sm","header","inline"],"description":"Available back link sizes."}],"types":[{"name":"BackLinkSize","type":"'md' | 'sm' | 'header' | 'inline'"}],"docs":[{"title":"Navigation/Back link","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/navigation-back-link--docs","summary":"Link back to the parent screen: a chevron and the name of the destination.","whenToUse":"* On a detail or form screen reached from a list, to return to that list.\n* `md` heads a screen on iPhone; `sm` is the quieter desktop breadcrumb in a header.","whenNotToUse":"* For navigation between sibling destinations. `ui-nav-item` and `ui-tab-bar` cover that.\n* For undoing an action. That is a button.","accessibility":"* A native `<a>`: it is announced as a link named by its label, which should name the destination\n (“Accounts”), not the gesture (“Back”).\n* The chevron is decorative and hidden from assistive technologies.\n* `md`, `sm` and `header` add no margin: place them with `-mx-2` or `-ml-1` so the chevron lines up\n with the content edge while the hit area stays full size. `inline` carries its own outset (-8px top, -4px\n bottom, -6px left, at the start of a flex column): put it at the top of a form column as it is.","args":[{"name":"size","description":"`md` is 44 px high in the foreground color (screen header on iPhone); `sm` is 36 px, muted, with a hover glow (desktop header); `header` has the `md` metrics with a 24 px chevron and a focus ring drawn inside (a screen header); `inline` is the 14 px 500 muted back step of a form, 44 px high (36 px with a mouse), 18 px chevron, hover to the foreground color, on a `button`, with its own outset at the top of a form column."},{"name":"label","description":"Projected text: the name of the destination."}]}]},{"name":"badge","importPath":"@joanroucoux/cairn-ui/badge","declarations":[{"kind":"component","className":"UiBadge","selector":"ui-badge","description":"Small status descriptor for counts, states or labels, such as an instrument's asset class.","examples":["<ui-badge>ETF</ui-badge>"],"inputs":[{"name":"variant","type":"'neutral' | 'outline'","required":false,"default":"'neutral'","twoWay":false},{"name":"size","type":"'md' | 'sm'","required":false,"default":"'md'","twoWay":false}],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[{"name":"BADGE_SIZES","type":"readonly string[]","values":["md","sm"]},{"name":"BADGE_VARIANTS","type":"readonly string[]","values":["neutral","outline"],"description":"Available badge variants."}],"types":[{"name":"BadgeSize","type":"'md' | 'sm'"},{"name":"BadgeVariant","type":"'neutral' | 'outline'"}],"docs":[{"title":"Data display/Badge","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-badge--docs","summary":"Small descriptor that labels the thing next to it, such as a source's sync status or an\naccount's envelope type.","whenToUse":"* To qualify a row or a heading with a short, stable piece of metadata.\n* When the label is a single word or two. A badge is read at a glance, not parsed.","whenNotToUse":"* For a signed figure that moves, such as a gain or a loss. Use [Delta](?path=/docs/data-display-delta--docs).\n* For a message the user has to act on. A badge is a label, not a notification.","accessibility":"* Always give the badge text. Its variant color is a reinforcement, never the only signal for the\n state it names.\n* The badge is inert. Nothing inside it should be focusable or clickable.","args":[{"name":"variant","description":"`neutral` sits on the `--muted` surface. `outline` is quieter still, for a state that should not compete with the content around it."},{"name":"size","description":"`md` is 24 px high, `sm` 22 px for dense rows."},{"name":"label","description":"Projected text content."}]}]},{"name":"button","importPath":"@joanroucoux/cairn-ui/button","declarations":[{"kind":"component","className":"UiButton","selector":"button[ui-button], a[ui-button]","description":"Styled native button or anchor, applied as an attribute so the host keeps every native behavior.","examples":["<button ui-button variant=\"destructive\">Delete</button>\n<button ui-button size=\"icon\" variant=\"ghost\" aria-label=\"More actions\">...</button>"],"inputs":[{"name":"variant","type":"'primary' | 'outline' | 'ghost' | 'destructive' | 'quiet' | 'quiet-destructive' | 'outline-destructive' | 'quiet-glow' | 'tonal'","required":false,"default":"'primary'","twoWay":false},{"name":"size","type":"'compact' | 'slim' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'tall' | 'block' | 'icon' | 'icon-sm'","required":false,"default":"'md'","twoWay":false},{"name":"loading","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"busy","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[{"name":"BUTTON_SIZES","type":"readonly string[]","values":["compact","slim","xs","sm","md","lg","xl","xxl","tall","block","icon","icon-sm"],"description":"Available button sizes."},{"name":"BUTTON_VARIANTS","type":"readonly string[]","values":["primary","outline","ghost","destructive","quiet","quiet-destructive","outline-destructive","quiet-glow","tonal"],"description":"Available button variants."}],"types":[{"name":"ButtonSize","type":"'compact' | 'slim' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'tall' | 'block' | 'icon' | 'icon-sm'"},{"name":"ButtonVariant","type":"'primary' | 'outline' | 'ghost' | 'destructive' | 'quiet' | 'quiet-destructive' | 'outline-destructive' | 'quiet-glow' | 'tonal'"}],"docs":[{"title":"Inputs/Button","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-button--docs","summary":"Clickable element that triggers an action. Its variant tells the user how consequential\nthat action is, so a screen reads at a glance.","whenToUse":"* For any action taken on the current screen: submitting a form, opening a dialog, refreshing quotes.\n* On an `<a>`, only when the action genuinely is a navigation the browser performs itself, such as a\n file download.","whenNotToUse":"* For ordinary navigation between screens. A link should look like a link, and its role should stay\n a link.\n* More than one `primary` per screen. If every action is emphasized, none of them is.\n* For a destructive action without a confirmation step. Pair `destructive` with a dialog.","accessibility":"* The host stays a native `<button>` or `<a>`, so focus, activation with Enter or Space, and the\n disabled state all come from the platform rather than from an ARIA imitation.\n* `size=\"md\"` is the 40/44px touch target Cairn applies everywhere: 40px under a mouse\n (`pointer: fine`), 44px on a touch screen.\n* `size=\"icon\"` has no visible text: it still needs a name of its own, through `aria-label`.\n* A `loading` button is marked `aria-busy` and `aria-disabled`, stays focusable, and swallows a\n click instead of letting a slow first response turn into a double submission.","args":[{"name":"variant","description":"How much emphasis the action carries. `primary` for the expected action, `destructive` for an irreversible one, `outline` and `ghost` for everything else. `quiet` is a muted icon button that fills `--soft` on hover and press (row actions). `quiet-glow` is the same icon button hovering to `--glow` with no press fill (a close cross). `quiet-destructive` turns negative on hover. `outline-destructive` is an outline with negative text. `tonal` is a filled `--muted` pill that becomes a plain text button with a glow hover from 64rem."},{"name":"size","description":"`md` is the 40/44px touch target used everywhere by default. `xs` (32px), `sm` (36px), `lg` (44px) and `xl` (50px) are fixed heights. `compact` (32px, 28px from 64rem), `slim` (44px, then 32px), `xxl` (52px, then 44px), `tall` (48px, then 40px) and `block` (50px, then 44px) step down at 64rem. `icon` is square and `icon-sm` is 44px on touch, 36px with a mouse; both need an `aria-label`."},{"name":"disabled","description":"Native `disabled`, so the button leaves the tab order instead of only looking inactive."},{"name":"loading","description":"Marks the button busy, keeps its width, and blocks a second click while an action is in flight."},{"name":"busy","description":"Marks the button busy like `loading`, without the spinner: the label alone says what is happening (“Signing in…”)."},{"name":"label","description":"Projected text content."}]}]},{"name":"card","importPath":"@joanroucoux/cairn-ui/card","declarations":[{"kind":"component","className":"UiCard","selector":"ui-card","description":"Surface that groups related content: a summary block, a table, a side panel. `surface` limits the\ncard styling to one side of `64rem`. `variant=\"inset\"` is the `--background` box that sits inside\na card or a dialog.","examples":["<ui-card variant=\"elevated\" padding=\"sm\">Total</ui-card>\n<ui-card variant=\"inset\" border=\"hairline\" padding=\"xs\">Results</ui-card>\n<ui-card padding=\"none\" clip>Table that touches the edges, clipped to the corners</ui-card>\n<ui-card surface=\"max-lg\">Card on an iPhone, plain on a desktop</ui-card>"],"inputs":[{"name":"variant","type":"'default' | 'elevated' | 'inset'","required":false,"default":"'default'","twoWay":false},{"name":"padding","type":"'none' | 'xs' | 'sm' | 'md' | 'list' | 'rows' | 'recap' | 'panel'","required":false,"default":"'md'","twoWay":false},{"name":"surface","type":"'always' | 'lg' | 'max-lg'","required":false,"default":"'always'","twoWay":false},{"name":"border","type":"'auto' | 'none' | 'border' | 'hairline'","required":false,"default":"'auto'","twoWay":false},{"name":"clip","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[{"name":"CARD_PADDINGS","type":"readonly string[]","values":["none","xs","sm","md","list","rows","recap","panel"],"description":"Available card paddings."},{"name":"CARD_SURFACES","type":"readonly string[]","values":["always","lg","max-lg"],"description":"Where the card surface (fill, border, radius) and its padding apply."},{"name":"CARD_VARIANTS","type":"readonly string[]","values":["default","elevated","inset"],"description":"Available card variants."}],"types":[{"name":"CardPadding","type":"'none' | 'xs' | 'sm' | 'md' | 'list' | 'rows' | 'recap' | 'panel'"},{"name":"CardSurface","type":"'always' | 'lg' | 'max-lg'"},{"name":"CardVariant","type":"'default' | 'elevated' | 'inset'"}],"docs":[{"title":"Surfaces/Card","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/surfaces-card--docs","summary":"Surface that groups related content, such as a summary block, a table or a side panel.","whenToUse":"* To draw a boundary around content that belongs together and can be read on its own.\n* With `padding=\"none\"` when the content manages its own spacing, which is what a table does.\n* With `surface=\"lg\"` or `\"max-lg\"` when the same block is a card at one width only, such as a detail that is a card beside the list on a desktop and a plain screen on an iPhone.","whenNotToUse":"* As a spacing utility. If nothing is being grouped, a card adds a border for no reason.\n* Nested inside another card of the same variant. Two identical surfaces stacked read as one.","accessibility":"* The card is a plain visual container. It carries no role and no landmark.\n* When it groups a distinct section of a screen, give the content inside it a heading, so the\n section is reachable from a screen reader's heading list.","args":[{"name":"variant","description":"`default` for a primary surface. `elevated` sits on the same tokens as a table's zebra rows, for content that should read as secondary to a `default` card next to it. `inset` is the `--background` box with the control radius that nests inside a card or a dialog."},{"name":"border","description":"Inset 1px ring. `auto` (default) follows the variant: `border` for `default`, `hairline` for `elevated`, `none` for `inset`. Set it to override: a hairline results box, a bordered picked title."},{"name":"padding","description":"Inner spacing; `xs` is 4 px, `sm` 12 px, `md` follows `--inset-card` (16 px, 24 px from 1 024 px), `recap` is 4 px / 12 px for a before/after recap whose rows carry their own 9 px, `panel` is 10 x 12 px, then 12 x 16 px from 1 024 px, `list` keeps a narrow side padding for rows that carry their own inset, `rows` is the 4 px / `--inset-card` inset of a ruled list such as facts, `none` when the content manages its own (e.g. a table)."},{"name":"clip","description":"Clips the content to the rounded corners (`overflow-hidden`). Off by default, so focus rings and popovers of the content are not cut. Use it with `padding=\"none\"` when a table or list touches the edges and its hovered or selected last row must follow the corners."},{"name":"surface","description":"Where the fill, the border, the radius and the padding apply: `always` (default), `lg` (from 64rem, plain below), or `max-lg` (below 64rem, plain from it). The block stays one element, so a detail screen can be a card on a desktop and a plain page on an iPhone."},{"name":"label","description":"Projected content."}]}]},{"name":"choice-chips","importPath":"@joanroucoux/cairn-ui/choice-chips","declarations":[{"kind":"component","className":"UiChoiceChips","selector":"ui-choice-chips","description":"Exclusive choice among a handful of named options, shown as wrapping pills. Binds to Signal Forms\nwith `[formField]`.","examples":["<ui-choice-chips label=\"Wrapper\" [options]=\"wrappers\" [(value)]=\"wrapper\" />","<ui-choice-chips label=\"Wrapper\" [options]=\"wrappers\" [formField]=\"form.wrapper\" />"],"inputs":[{"name":"options","type":"ChoiceChipOption[]","required":true,"twoWay":false},{"name":"value","type":"string","required":true,"twoWay":true},{"name":"label","type":"string | undefined","required":false,"twoWay":false},{"name":"ariaLabel","type":"string | undefined","required":false,"twoWay":false},{"name":"ariaLabelledby","type":"string | undefined","required":false,"twoWay":false},{"name":"surface","type":"'background' | 'card'","required":false,"default":"'background'","twoWay":false},{"name":"disabled","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"errors","type":"readonly UiControlError[]","required":false,"default":"[]","twoWay":false},{"name":"invalid","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"required","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"touched","type":"boolean","required":false,"default":"false","twoWay":false}],"outputs":[{"name":"touch","type":"void"}],"slots":[]}],"services":[],"functions":[],"constants":[],"types":[{"name":"ChoiceChipOption","type":"{\n value: string;\n label: string;\n}"}],"docs":[{"title":"Inputs/Choice chips","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-choice-chips--docs","summary":"Exclusive choice among a handful of named options, shown as wrapping pills, such as an\naccount wrapper, a category or a frequency. The selection is a `model()`, bound with `[(value)]`\nor with Signal Forms' `[formField]`, which fills `disabled`, `errors`, `invalid`, `required` and `touched` too.","whenToUse":"* When every option is worth showing at once and their labels are words rather than a short code,\n about three to ten.\n* In a form, where the choice takes effect on submit.","whenNotToUse":"* For a numeric range or a view switch applied immediately. Use\n [Segmented](?path=/docs/inputs-segmented--docs).\n* For a long or growing list. Use [Select](?path=/docs/inputs-select--docs).\n* For several choices at once: this is a single choice.","accessibility":"* Implements the [ARIA radio group pattern](https://www.w3.org/WAI/ARIA/apg/patterns/radio/).\n* A roving `tabindex` keeps a single stop in the tab order: the selected chip, or the first one\n when nothing matches.\n* Arrow Left, Right, Up and Down move the selection and the focus together, wrapping at both ends.\n Home and End jump to the first and last chip. Space and Enter select the focused chip.\n* Name the group with `label` (shown above the chips), or with `ariaLabel` or `ariaLabelledby`\n when the label lives elsewhere.","args":[{"name":"options","description":"Ordered `{ value, label }` list of choices."},{"name":"label","description":"Visible label above the chips. It also names the `radiogroup`."},{"name":"ariaLabel","description":"Accessible name for the group when there is no visible `label`."},{"name":"value","description":"Selected option value. Two-way bound via `[(value)]`."},{"name":"surface","description":"Fill of the unselected chips, to match the surface they sit on."},{"name":"disabled","description":"Dims every chip and removes them from interaction."}]}]},{"name":"control","importPath":"@joanroucoux/cairn-ui/control","declarations":[],"services":[],"functions":[],"constants":[{"name":"UI_CONTROL","type":"InjectionToken<UiControl>","description":"Resolves to the styled control projected into a field. A control provides it and declares the\n`errors` and `touched` inputs that Angular's `[formField]` fills."}],"types":[{"name":"UiControlError","type":"{ readonly message?: string }","description":"The part of a validation error this library reads. Angular's own `ValidationError` satisfies it."},{"name":"UiControl","type":"{\n readonly errors: Signal<readonly UiControlError[]>;\n readonly touched: Signal<boolean>;\n}","description":"What a styled control offers the field wrapped around it."}],"docs":[]},{"name":"delta","importPath":"@joanroucoux/cairn-ui/delta","declarations":[{"kind":"component","className":"UiDelta","selector":"ui-delta","description":"Signed amount whose sign carries meaning: a gain, a loss, or an unknown.\n\nA null value is not a zero: it renders the unknown state, not a gain of zero. Color never\ncarries the sign on its own: the projected text must already contain a + or a minus sign.","examples":["<ui-delta [value]=\"holding.dayChangeEur\" unknownLabel=\"Not available\">\n {{ holding.dayChangeEur | signedMoney }}\n</ui-delta>"],"inputs":[{"name":"value","type":"number | null | undefined","required":true,"twoWay":false},{"name":"unknownLabel","type":"string","required":true,"twoWay":false},{"name":"emphasis","type":"'text' | 'pill'","required":false,"default":"'text'","twoWay":false},{"name":"size","type":"'label' | 'inherit'","required":false,"default":"'label'","twoWay":false},{"name":"weight","type":"'medium' | 'regular'","required":false,"default":"'medium'","twoWay":false}],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[{"name":"DELTA_EMPHASES","type":"readonly string[]","values":["text","pill"],"description":"Available delta emphases."},{"name":"DELTA_SIZES","type":"readonly string[]","values":["label","inherit"],"description":"Type sizes."},{"name":"DELTA_WEIGHTS","type":"readonly string[]","values":["medium","regular"],"description":"Font weights."}],"types":[{"name":"DeltaEmphasis","type":"'text' | 'pill'"},{"name":"DeltaSize","type":"'label' | 'inherit'"},{"name":"DeltaWeight","type":"'medium' | 'regular'"}],"docs":[{"title":"Data display/Delta","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-delta--docs","summary":"Signed amount whose sign carries meaning: a gain, a loss, or a value that is simply not\nknown.\n\n`value` selects the color and nothing else. The figure itself is projected content, and the caller\nis responsible for formatting it with its own sign, because color can never carry that sign alone.","whenToUse":"* For a figure whose direction matters as much as its magnitude, such as an unrealised gain or a\n daily change.\n* Whenever that figure can legitimately be unknown. Twelve of Cairn's twenty six holdings have no\n cost basis.","whenNotToUse":"* For a plain amount that has no direction, such as a total or a quantity.\n* With `value` set to `0` to stand in for a missing figure. A zero claims a known result of zero,\n which is a different statement. Pass `null`.","accessibility":"* A `null` value renders an em dash marked `aria-hidden` next to a visually hidden\n `unknownLabel`, so the unknown state is announced rather than skipped in silence.\n* The sign always appears in the projected text, so the meaning survives without color.","args":[{"name":"value","description":"Selects the color: positive, negative, neutral, or unknown when `null`. It never affects the text that is displayed."},{"name":"emphasis","description":"`text` colors the figure only. `pill` also draws a neutral chip behind it."},{"name":"size","description":"`label` sets the label size; `inherit` takes the size of the line it sits in."},{"name":"weight","description":"`medium` (500, the default) or `regular` (400) for a figure that sits in running rows."},{"name":"label","description":"Projected content: the formatted figure, already carrying its own sign."}]}]},{"name":"dialog","importPath":"@joanroucoux/cairn-ui/dialog","declarations":[{"kind":"component","className":"UiDialog","selector":"ui-dialog","description":"Modal dialog on the native <dialog>: a bottom sheet under `64rem`, centered above it.\n\n`busy` keeps it open while its action runs: Escape, a backdrop click and a sheet drag do nothing\nand the cross is disabled. The owner disables its own buttons and still closes it through `open`.","examples":["<ui-dialog heading=\"Buy\" description=\"Ferrari\" closeLabel=\"Close\" width=\"528px\" [open]=\"buying()\" (dismissed)=\"buying.set(false)\">\n <p>Body</p>\n <button dialogActions ui-button variant=\"outline\" class=\"max-lg:hidden\" (click)=\"buying.set(false)\">Cancel</button>\n <button dialogActions ui-button>Buy 40 shares</button>\n</ui-dialog>"],"inputs":[{"name":"heading","type":"string","required":true,"twoWay":false},{"name":"description","type":"string | undefined","required":false,"twoWay":false},{"name":"closeLabel","type":"string | undefined","required":false,"twoWay":false},{"name":"variant","type":"'default' | 'confirm'","required":false,"default":"'default'","twoWay":false},{"name":"sheet","type":"'fit' | 'full'","required":false,"default":"'fit'","twoWay":false},{"name":"truncateDescription","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"width","type":"'md' | 'lg' | (string & {}) | undefined","required":false,"twoWay":false},{"name":"open","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"busy","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[{"name":"dismissed","type":"void"},{"name":"closed","type":"'escape' | 'backdrop' | 'cross' | 'drag' | 'programmatic'"}],"slots":["*","[dialogActions]"]}],"services":[],"functions":[],"constants":[{"name":"DIALOG_CLOSE_REASONS","type":"readonly string[]","values":["escape","backdrop","cross","drag","programmatic"],"description":"What started a close, as `closed` reports it."},{"name":"DIALOG_SHEETS","type":"readonly string[]","values":["fit","full"],"description":"`full` makes the sheet under `64rem` fill the screen below the status area; `fit` sizes it to its content."},{"name":"DIALOG_VARIANTS","type":"readonly string[]","values":["default","confirm"],"description":"`confirm` is a short alertdialog: no cross, no hairlines, one 24px padding, 440px wide unless told otherwise."},{"name":"DIALOG_WIDTHS","type":"readonly string[]","values":["md","lg"],"description":"Named dialog widths; any other CSS length is accepted as is."}],"types":[{"name":"DialogCloseReason","type":"'escape' | 'backdrop' | 'cross' | 'drag' | 'programmatic'"},{"name":"DialogSheet","type":"'fit' | 'full'"},{"name":"DialogVariant","type":"'default' | 'confirm'"},{"name":"DialogWidth","type":"'md' | 'lg'"}],"docs":[{"title":"Surfaces/Dialog","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/surfaces-dialog--docs","summary":"Modal dialog built on the native `<dialog>`. The focus trap, the return of focus to the\ntrigger, closing on Escape and the top layer all come from the platform rather than from custom\nJavaScript.\n\nUnder `64rem` it rises from the bottom as a sheet, with a drag handle, 16px padding and its footer\nclear of the home indicator; above that width it is centered with 24px padding. Its body scrolls on\nits own when the content is taller than the screen, so the header and the footer stay in place.\nIts veil is `rgb(0 0 0 / 0.36)`; opened while another modal is open (over a `ui-drawer`), it draws\na lighter `rgb(0 0 0 / 0.24)` over that one's, with nothing to set.\n\nThe header carries the title, an optional subtitle (`description`) and a close cross, which exists\nonly when the consumer passes `closeLabel`. The width is set by the consumer: `md`, `lg` or any CSS\nlength (Add a holding 560px, Buy and New account 480px, a confirmation 440px).\n\nEvery dialog has the same three zones. The header (title, optional subtitle, cross) is padded 16px 16px 16px 24px, with the title and the cross centered on each other, or lined up at the top under a subtitle; the body 20px 24px and is the only zone that scrolls, the footer 16px 24px. On a sheet they are 0 8px 12px 16px, 16px and 12px 16px plus the safe area, under a drag handle. A hairline runs under the header and above the footer, and both come with the cross: without `closeLabel` there is no cross and no hairlines. On a desktop the dialog is centered and never taller than `100dvh - 96px`.\n\nThe `variant` input is `default` or `confirm`: a confirmation (deleting a passkey) is announced as an alertdialog, has no cross and no hairlines, one 24px padding and a 440px width unless `width` says otherwise. The `sheet` input is `fit` (the sheet is as tall as its content) or `full` (it fills the screen below 58px, so the footer stays pinned while the content changes, as in Add a holding). `truncateDescription` keeps the subtitle on one ellipsized line instead of wrapping. A click that starts and ends on the backdrop closes the dialog, like Escape.\n\nContent goes in two slots: the default one for the body, and `[dialogActions]` for the footer\nbuttons. Put the dismissing action first. On a desktop the footer is right-aligned; on a sheet the\nactions stack at full width and 50px, the primary on top, so hide Cancel there with\n`max-lg:hidden` when the sheet has only the primary.\n\nEvery way out goes through one close: Escape, the backdrop, the cross, a drag of the sheet and the\nowner setting `open` back to `false`. `dismissed` fires at once for a close the reader started, never\nfor one the owner asked for. `closed` fires once per close, after the exit transition has played,\nwith what started it: `escape`, `backdrop`, `cross`, `drag` or `programmatic`. When there is no\ntransition to wait for, it fires at once. Reopening during the exit, or destroying the dialog, drops it.\n\nA consumer that mounts the dialog under `@if` keeps it mounted until `closed`. It sets `open` to\n`false` (on `dismissed`, or after a save), and only removes the wrapper and reports to its own\nparent in `(closed)`. Removing the wrapper together with `open = false`, or on `dismissed`, cuts the\nexit: the `<dialog>` leaves the page before the exit has played.\n\n```html\n@if (selling()) {\n <ui-dialog heading=\"Sell\" [open]=\"open()\" (dismissed)=\"open.set(false)\" (closed)=\"selling.set(false); sold.emit()\">\n ...\n </ui-dialog>\n}\n```\n\nOn a desktop the dialog fades and scales up from `--enter-scale` in `--duration-base` and leaves in\n`--duration-exit`, on `--ease-out`. As a sheet the panel only slides, from `translateY(100%)`, on\n`--ease-sheet`, while the veil fades. The handle and the header of a sheet can be dragged down: the\npanel follows the finger, closes past 30 % of its height or on a release faster than 0.5 px/ms, and\notherwise springs back on `--ease-sheet` in `--duration-base`. The body scrolls and never drags.\n\n`busy` keeps the dialog open while its action runs, so the outcome stays on screen: Escape, a click on the\nbackdrop and a drag of the sheet do nothing, the cross is disabled and the dialog carries `aria-busy`. The\nowner disables its own buttons and still closes it by setting `open` to `false`.\nWith `prefers-reduced-motion: reduce` the sheet fades instead of sliding.","whenToUse":"* When an answer is needed before anything else can continue, such as confirming a deletion.\n* To edit a short, self contained form without leaving the current screen.","whenNotToUse":"* For a task the user repeats often. Interrupting the same flow again and again wears thin.\n* For information that is not urgent, or that the user may want to keep visible while working.\n* To hold a long form. If the content scrolls inside the dialog, it belongs on a screen.","accessibility":"* `aria-labelledby` and `aria-describedby` are wired from `heading` and `description`. Never set\n them by hand.\n* Follows the [W3C dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) through\n the native element, including the initial focus move and the focus return on close.\n* The close cross is named by `closeLabel`, which the consumer translates. Without it there is no\n cross, and Escape stays the way out.\n* Every way out is handled the same: Escape, the cross, the backdrop, a drag of the sheet and the\n owner setting `open` back to `false`.","args":[]}]},{"name":"donut","importPath":"@joanroucoux/cairn-ui/donut","declarations":[{"kind":"component","className":"UiDonut","selector":"ui-donut","description":"A ring chart with a legend: slices rank from largest to smallest, and hovering, focusing or\nactivating a legend row highlights its slice.","examples":["<ui-donut [slices]=\"byAssetClass()\" label=\"By asset class\" othersLabel=\"Other\" [valueFormat]=\"formatEur\" [shareFormat]=\"formatShare\" (sliceSelect)=\"goToHoldings($event)\" />"],"inputs":[{"name":"slices","type":"DonutSlice[]","required":true,"twoWay":false},{"name":"label","type":"string","required":true,"twoWay":false},{"name":"othersLabel","type":"string","required":true,"twoWay":false},{"name":"valueFormat","type":"(value: number) => string","required":false,"default":"identityValue","twoWay":false},{"name":"shareFormat","type":"(share: number) => string","required":false,"default":"identityShare","twoWay":false},{"name":"legendHref","type":"((id: string) => string | null) | undefined","required":false,"twoWay":false}],"outputs":[{"name":"sliceSelect","type":"string"}],"slots":[]}],"services":[],"functions":[],"constants":[],"types":[{"name":"DonutSlice","type":"{\n id: string;\n label: string;\n value: number;\n sublabel?: string;\n}"}],"docs":[{"title":"Data display/Donut","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-donut--docs","summary":"A ring chart with its legend: slices come from `slices`, ranked largest first on the neutral gray\nramp (`--ramp-1` to `--ramp-6`). Beyond six slices, the five largest keep their rank and the rest\nmerge into one “Other” slice on `--ramp-6`, whose legend sub-label lists the merged names, while the\nring's `aria-label` still names every original slice.\n\nHovering, focusing or touching a legend row (or its arc) grows that arc by 4 % around the ring center, dims the other arcs to half opacity, and shows its name, share\nand value at the center; activating a row (click or Enter) emits `sliceSelect` with its id, for the\ncaller to navigate with. With `legendHref`, rows are real links: a plain click is canceled and emitted as `sliceSelect` so a router can navigate, while Ctrl/Cmd/Shift-click and middle-click keep the browser's behavior. Formatting is the caller's business: `valueFormat` and `shareFormat` both\narrive as inputs, so masking amounts is a matter of a formatter that returns bullets; shares stay\nvisible either way.","whenToUse":"* A breakdown by one dimension (asset class, account, wrapper) where the parts sum to a whole.","whenNotToUse":"* Comparing two breakdowns side by side with a shared color meaning: the ramp is assigned by rank,\n not by category, so the same color means different things in two different rings.\n* A value that is not a share of a total, such as a trend over time. Use `ui-line-chart` instead.","accessibility":"* The `<svg>` is `role=\"img\"`, named by `label` followed by every slice and its share, so the full\n detail survives even when several slices are grouped into “Other” visually.\n* Every legend row is a `<button>`, or a link when `legendHref` returns a URL for it: reachable by Tab, activated with Enter (and Space on a button), and its own text\n already carries the name, the sub-label, the share and the value: nothing depends on color alone.","args":[{"name":"slices","description":"Slices to rank and draw, each `{ id, label, value, sublabel? }`."},{"name":"label","description":"The ring’s accessible name, e.g. “By asset class”."},{"name":"othersLabel","description":"Label of the grouped slice beyond the sixth, e.g. “Other”."},{"name":"valueFormat","description":"Formats a slice's value, for the legend and the center."},{"name":"shareFormat","description":"Formats a slice's share (0..1), for the legend and the center."},{"name":"legendHref","description":"Maps a slice id to a URL. A row with a URL becomes a link, a row without one (or without the input) stays a button."},{"name":"sliceSelect","description":"Emitted with a slice id when its legend row is activated."}]}]},{"name":"drawer","importPath":"@joanroucoux/cairn-ui/drawer","declarations":[{"kind":"component","className":"UiDrawer","selector":"ui-drawer","description":"Modal side panel on the native <dialog>, against the right edge at full height, for a detail opened from a list.\n\nWithout `heading` it draws no header: the content brings its own title and close button, and `label` names it.\nWith both, `label` names the panel and the heading stays its visible title.","examples":["<ui-drawer heading=\"Northwind World\" closeLabel=\"Close the detail\" [open]=\"open()\" (dismissed)=\"open.set(false)\">\n <p>Body</p>\n</ui-drawer>"],"inputs":[{"name":"heading","type":"string | undefined","required":false,"twoWay":false},{"name":"description","type":"string | undefined","required":false,"twoWay":false},{"name":"closeLabel","type":"string | undefined","required":false,"twoWay":false},{"name":"label","type":"string | undefined","required":false,"twoWay":false},{"name":"width","type":"string","required":false,"default":"'440px'","twoWay":false},{"name":"open","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"busy","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[{"name":"dismissed","type":"void"},{"name":"closed","type":"'escape' | 'backdrop' | 'cross' | 'programmatic'"}],"slots":["*"]}],"services":[],"functions":[],"constants":[{"name":"DRAWER_CLOSE_REASONS","type":"readonly string[]","values":["escape","backdrop","cross","programmatic"],"description":"What started a close, as `closed` reports it."}],"types":[{"name":"DrawerCloseReason","type":"'escape' | 'backdrop' | 'cross' | 'programmatic'"}],"docs":[{"title":"Surfaces/Drawer","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/surfaces-drawer--docs","summary":"Modal side panel built on the native `<dialog>`, against the right edge of the window at full\nheight, for the detail of a row opened from a list on a desktop. The focus trap, the inert page, Escape and the\nreturn of focus to the opener come from the platform.\n\nIt is 440px wide (`width` takes any CSS length), on `--card`, with a shadow on its left edge and 24px of padding;\na `rgb(0 0 0 / 0.36)` veil covers the rest of the window. The panel scrolls on its own and keeps the scroll from\nchaining to the page (`overscroll-behavior: contain`); the page is not locked, so the list behind keeps its\ncolumns and its scroll position.\n\nWith `heading` it draws a header: the title, an optional `description` under it, and a 36px close cross when\n`closeLabel` is set. Without `heading` it draws nothing of its own: the content brings its title row and its\nclose button, and `label` gives the panel its accessible name. With both, `label` names the panel and the heading\nstays its visible title. The content is laid out in a column with a 24px gap.\n\nEvery way out goes through one close: Escape, a click that starts and ends on the veil, the cross and the owner\nsetting `open` back to `false`. `dismissed` fires at once for a close the reader started, never for one the owner\nasked for; `closed` fires once, after the exit has played, with `escape`, `backdrop`, `cross` or\n`programmatic`. `busy` keeps it open while an action runs, like `ui-dialog`.\n\nThe panel enters with opacity and a 24px slide from the right over `--duration-base` on `--ease-out`, while the\nveil fades in; it leaves the reverse way over `--duration-exit`. Under `prefers-reduced-motion: reduce` it only\nfades.\n\nA dialog opened from the drawer opens over it, and the drawer stays open underneath. `ui-dialog` sees the modal\nalready open and draws its own lighter veil, `rgb(0 0 0 / 0.24)`, over the drawer's: there is nothing to set.\nEscape closes the dialog first, then the drawer. Closing or submitting the dialog leaves the drawer open, and focus\ngoes back to the drawer button that opened the dialog: the owner only toggles the dialog's `open`.\n\n```html\n<ui-drawer heading=\"Northwind World\" closeLabel=\"Close the detail\" [open]=\"detail()\" (dismissed)=\"detail.set(false)\">\n ...\n <button ui-button (click)=\"buying.set(true)\">Buy</button>\n</ui-drawer>\n<ui-dialog heading=\"Buy\" [open]=\"buying()\" (dismissed)=\"buying.set(false)\">\n ...\n <button dialogActions ui-button (click)=\"buy()\">Buy 10 shares</button>\n</ui-dialog>\n```\n\nAfter a delete or a sale of everything, the holding is gone: set both `open` to `false`, then move focus to the list.","whenToUse":"* For the detail of a row on a desktop, when the list behind must keep its place.","whenNotToUse":"* On a phone: below `64rem` the detail is a screen of its own. The drawer is not responsive.\n* For a task that needs an answer before anything else: use `ui-dialog`.","accessibility":"* Named by `label` when it is set, otherwise by its heading through `aria-labelledby`; described by\n `description`. Set `label` when the panel's role reads better than the row's name (“Holding detail”): the\n heading stays its visible title. Never set the ARIA attributes by hand.\n* Follows the [W3C dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) through the native\n element: focus moves into the panel on open and goes back to the opener on close.\n* The cross is named by `closeLabel`, which the consumer translates.","args":[]}]},{"name":"empty","importPath":"@joanroucoux/cairn-ui/empty","declarations":[{"kind":"component","className":"UiEmpty","selector":"ui-empty","description":"Centered empty state: a title, a hint and an action slot, such as a search that matches nothing.","examples":["<ui-empty heading=\"No holding matches “zzz”\" hint=\"Search covers the name and the ISIN.\">\n <button ui-button variant=\"outline\" size=\"md\" type=\"button\">Add a holding</button>\n</ui-empty>"],"inputs":[{"name":"heading","type":"string","required":true,"twoWay":false},{"name":"hint","type":"string","required":false,"default":"''","twoWay":false}],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Feedback/Empty","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/feedback-empty--docs","summary":"A centered empty state: a title, a hint and an action slot.","whenToUse":"* For a list or a search that has nothing to show and can offer the next step: a catalog search that matches nothing, an account with no holding.","whenNotToUse":"* For a block that failed to load or is loading. Use [Async](?path=/docs/feedback-async--docs).","accessibility":"* The title and the hint are plain text, read in order, before the projected action.\n* Project a real `<button>` or `<a>` as the action.","args":[{"name":"heading","description":"The title of the state."},{"name":"hint","description":"The line under the title. Omitted when empty."}]}]},{"name":"external-link","importPath":"@joanroucoux/cairn-ui/external-link","declarations":[{"kind":"component","className":"UiExternalLink","selector":"a[ui-external-link]","description":"Link to another site: the label followed by an external-link icon.","examples":["<a ui-external-link href=\"https://www.contoso.com\" target=\"_blank\" rel=\"noopener\">Page on contoso.com</a>"],"inputs":[],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Navigation/External link","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/navigation-external-link--docs","summary":"A link that leaves the application: its label followed by an external-link icon.","whenToUse":"* For a link to another site, such as the issuer's page for a holding.","whenNotToUse":"* For navigation inside the application. Use a plain link, [Row](?path=/docs/data-display-row--docs) or\n [Nav item](?path=/docs/data-display-nav-item--docs).","accessibility":"* A native `<a>`. Add `target=\"_blank\"` with `rel=\"noopener\"` when it opens a new tab, and name the\n destination in the label (\"Fiche sur amundietf.fr\").\n* The icon is decorative and hidden from assistive technologies.\n* The target is 44 px high on touch and the text height with a mouse, which underlines on hover.","args":[{"name":"label","description":"Projected text: names the destination."}]}]},{"name":"fact","importPath":"@joanroucoux/cairn-ui/fact","declarations":[{"kind":"component","className":"UiFact","selector":"div[ui-fact]","description":"One fact of a `dl[uiFacts]`: a label, its value (the projected content) and an optional sub-line\nunder the value.","examples":["<div ui-fact label=\"Price\" sub=\"24/09 · Yahoo\" subTone=\"stale\">28,64 €</div>"],"inputs":[{"name":"label","type":"string","required":true,"twoWay":false},{"name":"sub","type":"string | undefined","required":false,"twoWay":false},{"name":"subTone","type":"'subtle' | 'stale'","required":false,"default":"'subtle'","twoWay":false},{"name":"size","type":"'md' | 'sm' | 'auto' | 'recap'","required":false,"default":"'md'","twoWay":false}],"outputs":[],"slots":["*"]},{"kind":"directive","className":"UiFacts","selector":"dl[uiFacts]","description":"The list that holds `div[ui-fact]` rows, as a native description list.","examples":["<dl uiFacts>\n <div ui-fact label=\"Quantity\">500</div>\n <div ui-fact label=\"Price\" sub=\"24/09 · Yahoo\">28,64 €</div>\n</dl>"],"inputs":[{"name":"ruled","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[{"name":"FACT_SIZES","type":"readonly string[]","values":["md","sm","auto","recap"],"description":"Available fact row sizes."},{"name":"FACT_SUB_TONES","type":"readonly string[]","values":["subtle","stale"],"description":"Color of the line under a fact value. `stale` flags an out-of-date figure."}],"types":[{"name":"FactSize","type":"'md' | 'sm' | 'auto' | 'recap'"},{"name":"FactSubTone","type":"'subtle' | 'stale'"}],"docs":[{"title":"Data display/Facts","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-facts--docs","summary":"A list of facts about one thing: a label on the left, its value on the right, an optional\nline under the value, and a hairline between rows. Built on a native description list.","whenToUse":"* On a detail screen, for the figures that describe the item: quantity, average cost, price with its\n date and source, identifier with its class.","whenNotToUse":"* For tabular data with several columns. Use the [Table](?path=/docs/data-display-table--docs).\n* For a list of things the user can open. Use [Row](?path=/docs/data-display-row--docs).","accessibility":"* `dl[uiFacts]` holds `div[ui-fact]` children, each exposing a `dt` (the label) and a `dd` (the\n value and its sub-line), which is the markup assistive technologies expect for term and value pairs.\n* The sub-line color (`stale`) is never the only signal: the line says what is out of date.\n* `size=\"auto\"` is `md` below 64rem and `sm` from it, as the Holdings detail does, so one list serves both widths.\n* `size=\"md\"` (12 px rows, body value) is the iPhone detail inside a card; `sm` (10 px rows, label\n value) is the desktop detail card. The card around the iPhone list is page layout.","args":[{"name":"size","description":"`md` is a 12 px row with a body value; `sm` a 10 px row with a label-size value and no hairline between rows; `recap` a 9 px row with a label-size value and a hairline between rows, for the before/after recap of a trade dialog; `auto` is `md` below 64rem and `sm` from it, so one list serves both widths."},{"name":"subTone","description":"Color of the line under a value: `subtle`, or `stale` for an out-of-date figure."},{"name":"ruled","description":"Also draws the hairline above the first row."}]}]},{"name":"field","importPath":"@joanroucoux/cairn-ui/field","declarations":[{"kind":"component","className":"UiField","selector":"ui-field","description":"Label, control, hint and error as one unit. The projected native control is found and wired with\nthe ARIA attributes. The message comes from `error`, which wins, or else from the control's own\nvalidation state once touched.","examples":["<ui-field label=\"Quantity\" hint=\"Leave empty if unknown\">\n <input uiInput type=\"number\" [formField]=\"form.quantity\" />\n</ui-field>","<ui-field label=\"ISIN or name\" [error]=\"blank() ? 'Enter an ISIN' : undefined\">\n <input uiInput type=\"search\" />\n</ui-field>","<ui-field label=\"Search holdings\" labelHidden>\n <input uiInput type=\"search\" placeholder=\"Instrument or account\" />\n</ui-field>"],"inputs":[{"name":"label","type":"string","required":true,"twoWay":false},{"name":"labelHidden","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"hint","type":"string | undefined","required":false,"twoWay":false},{"name":"error","type":"string | undefined","required":false,"twoWay":false},{"name":"unit","type":"string | undefined","required":false,"twoWay":false},{"name":"optional","type":"string | undefined","required":false,"twoWay":false},{"name":"invalid","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":["[uiFieldLeading]","*","[uiFieldTrailing]"]},{"kind":"directive","className":"UiFieldLeading","selector":"[uiFieldLeading]","description":"Marks an element, typically an 18px svg icon, as the leading content of a `ui-field`, inside the\ncontrol's left edge. Decorative, so hidden from assistive technologies.","examples":["<ui-field label=\"Search\" labelHidden>\n <svg uiFieldLeading width=\"18\" height=\"18\" viewBox=\"0 0 24 24\">...</svg>\n <input uiInput type=\"search\" />\n</ui-field>"],"inputs":[],"outputs":[],"slots":[]},{"kind":"directive","className":"UiFieldTrailing","selector":"[uiFieldTrailing]","description":"Marks an element, typically an icon button, as the trailing action of a `ui-field`, on the\ncontrol's right edge. A toggle should carry `aria-pressed`. The slot shares the right edge with\n`unit`: set one or the other, not both.","examples":["<ui-field label=\"Password\">\n <input uiInput type=\"password\" size=\"xl\" />\n <button uiFieldTrailing type=\"button\" aria-label=\"Show the password\" [attr.aria-pressed]=\"shown()\">...</button>\n</ui-field>"],"inputs":[],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Inputs/Field","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-field--docs","summary":"Label, hint and error message wrapped around a projected control as a single unit.\n\nThe control stays a plain native element such as `uiInput` or `uiSelect`. The field finds it in\nits own projected content and wires the ARIA attributes onto it, instead of replacing it with a\ncustom component that would have to reimplement every native forms behavior.\n\n`unit` renders a unit such as `EUR` or `shares` inside the control's right edge. It is decorative\ntext, not part of the control's value.\n\n`uiFieldLeading` marks an element, typically an 18px svg icon, as the control's leading content. It\nsits 12px from the left edge, in `--muted-foreground`, and the control's text is inset by 38px\n(12px padding, 18px icon, 8px gap) to clear it. It is decorative and hidden from assistive technologies.\n\nA field shows a message from one of two places. `error` is the caller's own, and always wins. Where\nnone is set, the field shows the first message among the projected control's `errors`, once that\ncontrol is touched: Angular's `[formField]` fills a `uiInput`, `uiSelect` or `uiTextarea` with its\nvalidation state on its own, so a form field needs no binding here at all.","whenToUse":"* Around every form control the user is expected to fill in.\n* Whenever a control needs a hint or can show a validation error.\n* With `labelHidden`, around a control whose purpose the page already makes plain, such as a\n search box above the list it filters. The label is still what a screen reader announces.","whenNotToUse":"* Around more than one control. One field wires one control.","accessibility":"* The label's `for` points at the control's id, generated when the control has none, so clicking\n the label focuses the control.\n* `labelHidden` hides the label from sight only: it stays in the accessibility tree and keeps\n naming the control. A placeholder is not a substitute for it, it disappears as soon as the user types.\n* `hint` and `error` are joined into the control's `aria-describedby`.\n* A non empty `error` marks the control `aria-invalid` and renders the message with `role=\"alert\"`,\n so a screen reader announces it as soon as it appears.\n* A control outside a field shows no message. It has nowhere to put one.","args":[{"name":"label","description":"The field's label, wired to the control via `for`/`id`."},{"name":"labelHidden","description":"Hides the label from sight while it keeps naming the control for assistive technologies."},{"name":"hint","description":"Optional helper text below the control, joined into `aria-describedby`."},{"name":"error","description":"Optional validation message. When set, also marks the control `aria-invalid` and renders with `role=\"alert\"`."},{"name":"optional","description":"Lighter suffix after the label, such as `(optional)`. It stays part of the control accessible name."},{"name":"invalid","description":"Marks the control `aria-invalid` without any message, for a failure the form reports elsewhere. A message from `error` or the control marks it too."},{"name":"unit","description":"Unit shown inside the control's right edge, such as `EUR` or `shares`."}]}]},{"name":"filter-chips","importPath":"@joanroucoux/cairn-ui/filter-chips","declarations":[{"kind":"component","className":"UiFilterChips","selector":"ui-filter-chips","description":"A row of single-choice filter pills, each with an optional count, that narrows a list in place.\nOn touch it scrolls on one line; with a fine pointer it wraps. An element marked `uiChipsLeading` is\nprojected first, outside the chips' group, followed by a vertical rule.","examples":["<ui-filter-chips ariaLabel=\"Filter by class\" [options]=\"classes()\" [(value)]=\"selectedClass\" />"],"inputs":[{"name":"options","type":"FilterChipOption[]","required":true,"twoWay":false},{"name":"value","type":"string","required":true,"twoWay":true},{"name":"ariaLabel","type":"string","required":true,"twoWay":false}],"outputs":[],"slots":["[uiChipsLeading]"]}],"services":[],"functions":[],"constants":[],"types":[{"name":"FilterChipOption","type":"{\n value: string;\n label: string;\n count?: number | string | null;\n}"}],"docs":[{"title":"Inputs/Filter chips","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-filter-chips--docs","summary":"A row of single-choice filter pills that narrows a list in place, each with an optional\ncount: an asset class, a status. The choice is a `model()`, bound with `[(value)]`. One option usually\nstands for “no filter” (such as “All”). On a touch screen the row stays on one line and scrolls\nsideways, with no visible scrollbar, bleeding to the screen edges through the `--gutter` token; each\npill is 34 px tall inside a 44 px target. With a fine pointer the pills are 32 px and wrap.","whenToUse":"* To filter a list that is already on screen, with the effect applied immediately and the result\n count visible on each option.","whenNotToUse":"* A choice made in a form and applied on submit. Use [Choice chips](?path=/docs/inputs-choice-chips--docs).\n* A view switch between a few panels. Use [Segmented](?path=/docs/inputs-segmented--docs).\n* More than about ten options. Use [Select](?path=/docs/inputs-select--docs).","accessibility":"* The row is a `role=\"group\"` named by `ariaLabel`; every chip is a `<button>` with `aria-pressed`\n telling which one is active, so a screen reader announces \"Pressed\" with the label and the count.\n* Every chip is a tab stop. Enter and Space select it. The scroller never needs a pointer: focusing\n a chip scrolls it into view.\n* Place it in a container padded by `--gutter`: on touch the row bleeds to the container's edges.\n* The focus ring is drawn inside the pill on touch and outside it with a fine pointer.","args":[{"name":"options","description":"Ordered `{ value, label, count? }` list. A `count` of `null` or none is left out."},{"name":"ariaLabel","description":"Accessible name of the group."},{"name":"value","description":"Value of the active chip. Two-way bound via `[(value)]`."}]}]},{"name":"group-header","importPath":"@joanroucoux/cairn-ui/group-header","declarations":[{"kind":"component","className":"UiGroupHeader","selector":"ui-group-header","description":"Header of a card on a phone: the name, its meta underneath, the projected total on the right.\n\nWith `collapsible` the header is a button inside the heading, with a chevron that turns when the\ngroup is folded and a press scale. `expanded` is a model: the page keeps the fold memory and hides\nthe card body itself. `toggleDisabled` keeps the button focusable and announced, but a click\nchanges nothing (a filter holds the group open). `controls` is the id of the body it folds.","examples":["<ui-group-header name=\"Northwind PEA\" meta=\"PEA · 3 holdings\" collapsible [(expanded)]=\"open\">\n <ui-amount [value]=\"total\" />\n</ui-group-header>"],"inputs":[{"name":"name","type":"string","required":true,"twoWay":false},{"name":"meta","type":"string | undefined","required":false,"twoWay":false},{"name":"collapsible","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"expanded","type":"boolean","required":false,"default":"true","twoWay":true},{"name":"toggleDisabled","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"controls","type":"string | undefined","required":false,"twoWay":false}],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Data display/Group header","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-group-header--docs","summary":"The header of a grouped card on a phone: the name, its meta underneath, the total on the right\n(projected, usually a `ui-amount`). It sits on the bottom line of the block, 44px tall at least.\n\nWith `collapsible` it is one button inside the heading (`h2 > button[aria-expanded]`) with a 20px chevron that\nturns to -90 degrees when folded, and a press scale. `expanded` is a `model()`: bind `[(expanded)]`, keep the fold\nmemory in the page and hide the card body yourself, with `hidden` rather than removing it. `toggleDisabled` keeps\nthe button focusable and announced while a click does nothing, for a group a filter holds open. `controls` is the id\nof the body it folds.","whenToUse":"* To head a card of rows on a phone, folded or not, where the table band of a desktop screen would not fit.","whenNotToUse":"* On desktop, where `td[ui-group-cell]` heads a group of table rows.","accessibility":"* The button sits inside the heading, so the heading structure survives. `aria-expanded` carries the state, and\n `aria-disabled` the held-open one, which keeps the button reachable by keyboard.\n* The chevron is decorative and hidden from assistive technology.","args":[{"name":"name","description":"The group name, title weight."},{"name":"meta","description":"The line under the name, muted."},{"name":"collapsible","description":"Makes the header a button that folds the group."},{"name":"expanded","description":"Two-way model: whether the group is open."},{"name":"toggleDisabled","description":"Keeps the button announced but ignores a click."}]}]},{"name":"icon-toggle","importPath":"@joanroucoux/cairn-ui/icon-toggle","declarations":[{"kind":"directive","className":"UiIconToggle","selector":"button[uiIconToggle]","description":"Round icon button that switches something on and off, such as the header eye that hides the amounts.\nThe state lives in `aria-pressed`, which also draws the soft fill. Only the press scale transitions:\nthe fill changes at once.","examples":["<button uiIconToggle aria-label=\"Hide amounts\" [attr.aria-pressed]=\"hidden\"><svg>...</svg></button>"],"inputs":[],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Inputs/Icon toggle","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-icon-toggle--docs","summary":"Round icon button that switches something on and off. It is the header eye that hides the\namounts: the icon changes from an eye to a crossed-out eye and the button takes a soft fill.","whenToUse":"* For an action taken at a precise moment and reversed later, when it must stay one gesture away from every\n screen.","whenNotToUse":"* For a setting that is not toggled every day: use a `ui-switch` in a form.\n* For an action that is not reversible, or that has no on/off state: use a `ui-button size=\"icon\"`.","accessibility":"* The host stays a native `<button>`, with `type=\"button\"` unless one is set.\n* It has no visible text: give it an `aria-label`. The state is `aria-pressed`, which the consumer sets; when the\n label also changes with the state (“Hide” / “Show”), a screen reader reads both.\n* The pressed fill is the `--soft` token, drawn as a background image so a fine-pointer hover (`--glow`) never\n replaces it. The fill changes at once: only the press scale transitions.\n* 44px on a touch screen, 40px from 64rem.","args":[]}]},{"name":"input","importPath":"@joanroucoux/cairn-ui/input","declarations":[{"kind":"directive","className":"UiInput","selector":"input[uiInput]","description":"Styled native text input. The host stays a plain `<input>`, so every forms flavor and native\nattribute keeps working. Declares the `errors` and `touched` inputs Angular's `[formField]` fills,\nso a surrounding `ui-field` can show the message.","examples":["<input uiInput type=\"email\" placeholder=\"you@contoso.com\" />"],"inputs":[{"name":"size","type":"'sm' | 'md' | 'lg' | 'xl'","required":false,"default":"'md'","twoWay":false},{"name":"surface","type":"'background' | 'card'","required":false,"default":"'background'","twoWay":false},{"name":"errors","type":"readonly UiControlError[]","required":false,"default":"[]","twoWay":false},{"name":"touched","type":"boolean","required":false,"default":"false","twoWay":false}],"outputs":[],"slots":[]},{"kind":"directive","className":"UiTextarea","selector":"textarea[uiTextarea]","description":"Styled native textarea. It takes no size: it grows from a minimum height. Declares the `errors`\nand `touched` inputs Angular's `[formField]` fills, so a surrounding `ui-field` can show the message.","examples":["<textarea uiTextarea maxlength=\"280\"></textarea>"],"inputs":[{"name":"errors","type":"readonly UiControlError[]","required":false,"default":"[]","twoWay":false},{"name":"touched","type":"boolean","required":false,"default":"false","twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[{"name":"CONTROL_SIZES","type":"readonly string[]","values":["sm","md","lg","xl"],"description":"Available control sizes."},{"name":"CONTROL_SURFACES","type":"readonly string[]","values":["background","card"]}],"types":[{"name":"ControlSize","type":"'sm' | 'md' | 'lg' | 'xl'"},{"name":"ControlSurface","type":"'background' | 'card'"}],"docs":[{"title":"Inputs/Input","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-input--docs","summary":"Styled native `<input>` and `<textarea>`.\n\nBoth are attributes rather than wrapper components, so the host stays the real element. Every\nflavour of Angular forms, from `ngModel` to Signal Forms, and every native attribute keep working\nwithout any plumbing. `uiTextarea` shares its classes with `uiInput` and takes no size, because a\ntextarea grows from a floor instead of sitting at a fixed height.","whenToUse":"* For free text and numbers the user types, wrapped in a\n [Field](?path=/docs/inputs-field--docs) that labels it.","whenNotToUse":"* For a choice among known options. Use [Select](?path=/docs/inputs-select--docs) for a list, or\n [Segmented](?path=/docs/inputs-segmented--docs) for a handful of mutually exclusive values.","accessibility":"* The control always needs an accessible name, either from the label of a surrounding\n `<ui-field>` or from an explicit `aria-label`.\n* A placeholder is not a label. It disappears as soon as the user types.\n* `size=\"md\"` is the 44px touch target, as it is for every other control in the library.","args":[{"name":"type","description":"Native `type` attribute, forwarded as-is."},{"name":"placeholder","description":"Native placeholder text."},{"name":"disabled","description":"Native `disabled`."},{"name":"size","description":"`md` is the 44px touch target used by default; `sm` is 36px; `lg` is 48px below 64rem, then the touch target (sheet forms); `xl` is 48px, then 44px (sign-in)."}]}]},{"name":"line-chart","importPath":"@joanroucoux/cairn-ui/line-chart","declarations":[{"kind":"component","className":"UiLineChart","selector":"ui-line-chart","description":"Value over time: a monotone curve with a dashed line at the starting value and a tooltip.\n\nAxis labels are never repeated: a tick whose `axisFormat` text equals the previous drawn label is\nnot rendered, so only the last edge label can drop.\n\nA new series animates from the drawn curve. With `rangeKey` bound, only a series that comes with\na new key does: a new series under the same key (a reload) is drawn at once. The key may change\nbefore its series arrives.","examples":["<ui-line-chart [points]=\"points\" label=\"Net worth over one month\" startLabel=\"Since\" [valueFormat]=\"formatEur\" />"],"inputs":[{"name":"points","type":"ChartPoint[]","required":true,"twoWay":false},{"name":"label","type":"string","required":true,"twoWay":false},{"name":"valueFormat","type":"(value: number) => string","required":false,"default":"identityValue","twoWay":false},{"name":"tooltipFormat","type":"((point: ChartPoint) => string) | undefined","required":false,"twoWay":false},{"name":"deltaFormat","type":"(delta: number) => string","required":false,"default":"identityDelta","twoWay":false},{"name":"deltaSuffix","type":"string","required":false,"default":"''","twoWay":false},{"name":"timeFormat","type":"(time: number) => string","required":false,"default":"identityTime","twoWay":false},{"name":"axisFormat","type":"(time: number) => string","required":false,"default":"identityTime","twoWay":false},{"name":"startLabel","type":"string","required":false,"default":"''","twoWay":false},{"name":"timeColumnLabel","type":"string","required":false,"default":"'Time'","twoWay":false},{"name":"valueColumnLabel","type":"string","required":false,"default":"'Value'","twoWay":false},{"name":"sparkline","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"tooltip","type":"'compact' | 'large' | 'auto'","required":false,"default":"'compact'","twoWay":false},{"name":"tooltipDelta","type":"boolean","required":false,"default":"true","attribute":true,"twoWay":false},{"name":"axisGap","type":"number | 'auto'","required":false,"default":"12","twoWay":false},{"name":"axisTicks","type":"3 | 5 | 'auto'","required":false,"default":"'auto'","twoWay":false},{"name":"rangeKey","type":"string | null","required":false,"default":"null","twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[],"types":[{"name":"AxisTicks","type":"3 | 5 | 'auto'"},{"name":"ChartPoint","type":"{\n t: number;\n v: number;\n}"},{"name":"TooltipSize","type":"'compact' | 'large'"}],"docs":[{"title":"Data display/Line chart","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-line-chart--docs","summary":"A single series' value over time: a monotone curve (Fritsch-Carlson, the same interpolation\nas d3's `curveMonotoneX`) that never overshoots the data between two points, a dashed reference\nline at the range's starting value with its “Start” label, a focus/hover tooltip with the change\nsince the start, and a shape transition when `points` changes to a new range.\n\nThe transition runs only for a new `points` array: mounting, the first measured size and any later\nresize draw at once. It lasts `--duration-base` with an ease-out, restarts from the shape on screen\nwhen the range changes again, and moves the end dot along the curve. The “Start” label and the\ndashed line fade out when it starts and back in at their new place when it ends; the tooltip waits\nfor the end. Under `prefers-reduced-motion` the new series shows at once.\n\nBind `rangeKey` (the range's id, `1M`, `1Y`) and only a series that comes with a new key\ninterpolates: the same range reloaded after a buy, or a refresh of its prices, redraws at once with\nno fade, as MOUVEMENT.md animates the curve on a range change only. The key may change before its\nseries arrives; the interpolation runs when the series does. Without `rangeKey`, every new series\ninterpolates.\n\nFormatting and locale are the consumer's business: `valueFormat`, `deltaFormat`, `timeFormat` and\n`axisFormat` all arrive as inputs, so masking amounts is a matter of passing a formatter that\nreturns bullets instead of digits: the curve's shape stays visible either way.\n\nThe host fills its parent's height (`block h-full`); size the chart by sizing its container.","whenToUse":"* Portfolio or instrument value over a selectable range, one series at a time.\n* A short history under a coarse `axisFormat` (years on a 5-year range): a tick label equal to the\n previous drawn one is skipped, so the axis never reads “2026 2026 2026”.","whenNotToUse":"* A compact inline figure with its own axis and tooltip. Pass `sparkline` for the 80 x 24 form\n (curve only, no axis, reference line, dot or tooltip, and out of the tab order).\n* Comparing two or more series at once: the reference line and the tooltip both assume a single\n series.","accessibility":"* The `<svg>` is `role=\"img\"` named by `label`, and focusable: Arrow Left/Right move the focused\n point one at a time, Home/End jump to the first/last point, Escape clears it.\n* Every point is also available as a `sr-only` `<table>`, headed by `timeColumnLabel` and\n `valueColumnLabel`, so the data survives without the SVG.\n* `touch-none` on the plot: a tap shows the tooltip and a drag moves it, while the page still scrolls\n everywhere else. Release or cancel hides it.","args":[{"name":"points","description":"Series of `{ t, v }` points, ordered by time."},{"name":"label","description":"The SVG's accessible name (`role=\"img\"`)."},{"name":"startLabel","description":"Prefix of the reference-line label, e.g. `Start`. Empty hides the label."},{"name":"timeColumnLabel","description":"Header of the screen-reader table's time column."},{"name":"valueColumnLabel","description":"Header of the screen-reader table's value column."},{"name":"tooltip","description":"`compact` (default) sits 12px above the plot with label and caption sizes; `large` is the desktop Dashboard tooltip, at the top of the plot with body and label sizes; `auto` is compact below 64rem and large from 64rem up, with no breakpoint in the consumer."},{"name":"deltaSuffix","description":"Text after the tooltip change, kept when masked."},{"name":"tooltipDelta","description":"Shows the change since the start in the tooltip. Defaults to true."},{"name":"axisGap","description":"Pixels between the plot and the axis row, 12 by default and 16 on the desktop Dashboard, or \"auto\" for 12 below 64rem and 16 from 64rem up. The box is plot + gap + 17."},{"name":"axisTicks","description":"3 draws start, middle and end only (Holdings), 5 draws five labels, auto (default) draws 3 below 40rem and 5 above (Dashboard)."},{"name":"sparkline","description":"Draws the curve alone at 1.5px, with no reference line, dot, axis or tooltip, and out of the tab order. Size it with the container."},{"name":"valueFormat","description":"Formats a point value for the start label, the screen-reader table and, without tooltipFormat, the tooltip."},{"name":"tooltipFormat","description":"Formats a whole point for the tooltip value line, when it reads more than the value (a position value and its unit price). Falls back to valueFormat."},{"name":"rangeKey","description":"Id of the range the series belongs to. When bound, a new series interpolates only if it comes with a new key; under the same key it redraws at once, with no fade. Null (default) interpolates every new series."},{"name":"deltaFormat","description":"Formats the signed change since the first point, for the tooltip."},{"name":"timeFormat","description":"Formats a point's time for the tooltip and the table."},{"name":"axisFormat","description":"Formats a tick time for the axis."}]}]},{"name":"menu","importPath":"@joanroucoux/cairn-ui/menu","declarations":[{"kind":"component","className":"UiMenu","selector":"ui-menu","description":"Secondary actions menu, opened from its trigger through the native Popover API. With `sheet`, below\n64rem it opens as a bottom action sheet instead of beside its trigger, and `heading` names what\nthe actions apply to. `width` fixes the popover width in pixels; without it the menu fits its content.","examples":["<button ui-button size=\"icon\" aria-label=\"More\" [uiMenuTrigger]=\"menu\">...</button>\n<ui-menu #menu label=\"Holding actions\">\n <button uiMenuItem (click)=\"edit()\">Edit</button>\n <button uiMenuItem destructive (click)=\"remove()\">Delete the holding</button>\n</ui-menu>"],"inputs":[{"name":"label","type":"string","required":true,"twoWay":false},{"name":"sheet","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"heading","type":"string | undefined","required":false,"twoWay":false},{"name":"width","type":"number | undefined","required":false,"twoWay":false}],"outputs":[],"slots":["*"]},{"kind":"directive","className":"UiMenuItem","selector":"button[uiMenuItem]","description":"One action of a `ui-menu`; `destructive` paints it in `--negative`. Giving it `checked` makes it a radio\nchoice (`menuitemradio`): the current one carries a check mark and `aria-checked=\"true\"`.","examples":["<button uiMenuItem destructive (click)=\"remove()\">Delete the holding</button>\n<button uiMenuItem [checked]=\"account() === 'pea'\" (click)=\"account.set('pea')\">Northwind PEA</button>"],"inputs":[{"name":"destructive","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"checked","type":"boolean | undefined","required":false,"twoWay":false}],"outputs":[],"slots":[]},{"kind":"directive","className":"UiMenuTrigger","selector":"[uiMenuTrigger]","description":"Opens a `ui-menu` from a trigger element and wires its ARIA attributes.","examples":["<button ui-button size=\"icon\" aria-label=\"More\" [uiMenuTrigger]=\"menu\">...</button>"],"inputs":[{"name":"uiMenuTrigger","type":"UiMenu","required":true,"twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Surfaces/Menu","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/surfaces-menu--docs","summary":"Secondary actions menu, built on the native Popover API: light dismiss (Escape, a click\noutside), stacking and the top layer all come from the platform. It opens below its trigger,\naligned to the trigger's right edge, and flips above when there is no room below.\n\nWith `sheet`, below 64rem it opens instead as a bottom action sheet above the tab bar, with a\nbackdrop, and `heading` names what the actions apply to. `width` fixes the popover width in pixels,\n**outer width, padding included**: the mockup's `width: 210px` and `200px` are content-box, so pass\n`218` and `208`. Without it the menu fits its content.","whenToUse":"* For the secondary actions of a row: edit, delete, duplicate.\n* When there are too many actions to show as buttons, or when one of them is destructive and\n should not sit one accidental tap away from the primary action.","whenNotToUse":"* For a single action. A button says more with less.\n* For navigation between screens. `ui-nav-item` and `ui-tab-bar` cover that.","accessibility":"* `heading` is hidden from assistive technologies, so `label` must name the target as well, for\n example “Actions for Northwind PEA”.\n* The trigger carries `aria-haspopup=\"menu\"`, `aria-expanded` and `aria-controls`.\n* Opening moves focus to the first item; the arrow keys move between items and wrap; Home and End\n jump to the first and the last. Escape and a click outside close the menu, and focus returns to\n the trigger either way.\n* A destructive item is colored `--negative`, never conveyed by color alone: its label already\n says what it does (“Delete the holding”).","args":[{"name":"sheet","description":"Below 64rem, opens as a bottom action sheet above the tab bar, with a backdrop, instead of as a popover beside the trigger. From 64rem it is a popover either way."},{"name":"heading","description":"Title of the sheet naming what the actions apply to (an account name). Shown only with `sheet`, below 64rem, and hidden from assistive technologies: `label` must name the target too."}]}]},{"name":"meter","importPath":"@joanroucoux/cairn-ui/meter","declarations":[{"kind":"component","className":"UiMeter","selector":"ui-meter","description":"Proportion of a whole, drawn as a single bar. One bar per category, never a stacked strip.","examples":["<ui-meter [value]=\"0.463\" label=\"Funds\" valueText=\"46.3% - 128,656.00 EUR\" [tone]=\"1\" />"],"inputs":[{"name":"value","type":"number","required":true,"twoWay":false},{"name":"label","type":"string","required":true,"twoWay":false},{"name":"valueText","type":"string","required":true,"twoWay":false},{"name":"tone","type":"1 | 2 | 3 | 4 | 5 | 6","required":false,"default":"1","twoWay":false},{"name":"size","type":"'md' | 'sm'","required":false,"default":"'md'","twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[{"name":"METER_SIZES","type":"readonly string[]","values":["md","sm"],"description":"Track thicknesses."},{"name":"METER_TONES","type":"readonly number[]","values":[1,2,3,4,5,6],"description":"Ramp step used to paint the fill."}],"types":[{"name":"MeterSize","type":"'md' | 'sm'"},{"name":"MeterTone","type":"1 | 2 | 3 | 4 | 5 | 6"}],"docs":[{"title":"Data display/Meter","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-meter--docs","summary":"Proportion of a whole, drawn as a single bar. In Cairn it carries an allocation\ncategory's share of the portfolio.\n\nAlways one bar per category, never a stacked strip. The palette is monochrome and has no hue to\ntell segments of a strip apart, so length has to do that work alone, and it only reads as a\ncomparison when every bar starts at the same edge.","whenToUse":"* To compare parts of a whole, one bar per part, each with its own label.\n* For a static proportion the user reads. It is a display, not a control.","whenNotToUse":"* For progress toward completion, such as an upload. That is a progress bar, and it carries a\n different meaning.\n* As the only place a figure appears. The bar shows a relationship, not a value.","accessibility":"* Renders `role=\"meter\"` with `aria-valuemin`, `aria-valuemax`, `aria-valuenow` and\n `aria-valuetext`.\n* `valueText` should read as a full phrase carrying both the share and the amount, so a screen\n reader announces exactly what a sighted user reads next to the bar.\n* `tone` is decorative. Neighboring ramp steps separate by at least 1.36:1, which distinguishes\n them without ever identifying a category on its own.","args":[{"name":"amount","description":"Amount shown on the label line, formatted by the caller."},{"name":"share","description":"Share of the total shown under the bar, formatted by the caller."},{"name":"value","description":"Fill proportion, 0 to 1. Also sets `aria-valuenow`."},{"name":"label","description":"Accessible name for the meter, usually the category name."},{"name":"valueText","description":"Phrase announced in place of the raw number, carrying both the share and the amount."},{"name":"size","description":"Track thickness: `md` is 6px (the default), `sm` is 4px for dense lists such as the wrappers."},{"name":"tone","description":"Step of the neutral ramp used to paint the fill, `1` being the most prominent and `6` the quietest."}]}]},{"name":"motion","importPath":"@joanroucoux/cairn-ui/motion","declarations":[{"kind":"directive","className":"UiFlipItem","selector":"[uiFlipItem]","description":"Marks an element as an item of the nearest `uiFlipList`, at any depth. Once one item is marked,\nthe list follows its marked items only.","examples":["<tr uiTr uiFlipItem animate.leave=\"ui-leave-fade\">...</tr>"],"inputs":[],"outputs":[],"slots":[]},{"kind":"directive","className":"UiFlipList","selector":"[uiFlipList]","description":"Slides the items of a list into place when one is removed or inserted (FLIP). Pair it with\n`animate.leave=\"ui-leave-fade\"` on the item so it fades out first. Nothing moves under\n`prefers-reduced-motion`.\n\nThe items are the direct children of the list. Mark them with `uiFlipItem` instead when they sit\ndeeper, so a removal also slides the groups below. Only an item added or removed starts a slide;\na change inside an item does not.","examples":["<ul uiFlipList>"],"inputs":[],"outputs":[],"slots":[]},{"kind":"directive","className":"UiHighlight","selector":"[uiHighlight]","description":"Draws the eye to an element that just changed: scrolls it into view when off screen, flashes the\n`--soft` fill, then fades back. Each new non-null token plays once, so pass a fresh value per\nevent (an object or a counter), not a boolean. A non-null token present at first render plays on\narrival. Can sit on the same element as `animate.enter`: it waits for that fade to finish.\n`highlighted` fires when the flash starts, after any scroll and enter fade, so a toast can follow it.\n\nGive the element `scroll-margin-top` and `scroll-margin-bottom` equal to what is pinned above and\nbelow it (header, tab bar, action bar), or a row under them counts as on screen. For an arrival\non a page opened at the element, scroll to it first (`scrollIntoView` in `afterNextRender`) and\nset the token afterwards, otherwise it scrolls smoothly from where the page is.","examples":["<a ui-row [uiHighlight]=\"changed() === holding.id ? token() : null\" routerLink=\"/holdings/1\">...</a>"],"inputs":[{"name":"uiHighlight","type":"unknown","required":false,"default":"null","twoWay":false}],"outputs":[{"name":"highlighted","type":"void"}],"slots":[]}],"services":[],"functions":[{"name":"injectReducedMotion","signature":"injectReducedMotion(): Signal<boolean>","description":"A signal that follows the user's `prefers-reduced-motion` preference. Call it in an injection\ncontext.","examples":["protected readonly reduced = injectReducedMotion();"]},{"name":"holdTransitionsUntilRendered","signature":"holdTransitionsUntilRendered(): void","description":"Holds the transitions of the host and of everything inside it until two frames after its first\nrender, so bound classes do not animate on page open. Call it in an injection context. A host\nwhose view is never refreshed (a detached change detector) never gets its transitions back.","examples":["constructor() {\n holdTransitionsUntilRendered();\n}"]}],"constants":[],"types":[],"docs":[{"title":"Foundations/Motion classes","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/foundations-motion-classes--docs","summary":"The enter and leave classes shipped in `styles/motion.css`, for Angular's `animate.enter` and\n`animate.leave`. A consumer imports that stylesheet after `theme.css` and writes\n`animate.enter=\"ui-enter-fade\"`; it needs no styling of its own.","whenToUse":"* `ui-enter-fade`: opacity over `--duration-fast`, for a message that appears.\n* `ui-enter-fade-up`: opacity and a 4px rise over `--duration-fast`, for content that arrives in place.\n* `ui-leave-fade`: opacity over `--duration-exit`, for every exit.","whenNotToUse":"* On page open: nothing animates when a screen first renders.\n* For a layout change: only `transform` and `opacity` animate.","accessibility":"* Under `prefers-reduced-motion: reduce` the rise becomes a plain fade, and view transitions are off.","args":[]},{"title":"Foundations/Flip list","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/foundations-flip-list--docs","summary":"`[uiFlipList]` on a list container slides the remaining items into place when one is removed or\ninserted. It measures every item before and after the change (FLIP), from the top of the list itself, then\nanimates the shift in `translateY` over `--duration-base` with `--ease-out`. The list needs no\n`position: relative`, and a list that moved on the page in the meantime slides by the gap only.\n\nThe items are the direct children of the list, unless some are marked `uiFlipItem`: the list then follows\nthe marked items at any depth. Put `uiFlipList` around every group of a page and `uiFlipItem` on each group\nheading and row, so a removal also slides the groups below. An item inside another item (a row inside a card)\nmoves by its own share, the card carrying the rest.","whenToUse":"* On the list of a deletable item: a holding, a passkey. Put `animate.leave=\"ui-leave-fade\"` on the item so it fades out first, then the others move up.\n* On a page made of groups (one `tbody` or card per account): `uiFlipList` on the table or the page column, `uiFlipItem` on the headings and rows.\n* When an item comes back in its place: the ones after it slide down.","whenNotToUse":"* To reorder or filter a long list: only items added or removed start a slide, and only vertically.\n* On a change inside an item (a row that grows): the layout changes at once, nothing slides.","accessibility":"* Under `prefers-reduced-motion: reduce` nothing moves: the list closes up at once.","args":[]},{"title":"Foundations/Highlight","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/foundations-highlight--docs","summary":"`[uiHighlight]=\"token\"` draws the eye to what just changed: it scrolls the element into view when it is\noff screen, holds the `--soft` fill for 200 ms, then fades back to the element's own background over\n`--duration-highlight` with `--ease-out`. It is the one place where `background-color` animates.","whenToUse":"* After Buy, Sell, Edit or Add, on the row that changed.\n* On arrival at a precise place, on the heading of the account the user came for.\n* On `ui-row`, `tr[uiTr]` (it paints the cells), `td[ui-group-cell]` (it paints the band) or any plain element.\n* Give the element `scroll-margin-top` and `scroll-margin-bottom` equal to what is pinned above and below it\n (header, tab bar, action bar): a row hidden under them counts as off screen and is scrolled out.\n* For an arrival, scroll to the element at once first (`scrollIntoView` in `afterNextRender`), then set the\n token: a token present at first render scrolls smoothly from where the page is.\n* On a new row with `animate.enter`, on the same element: the highlight starts once the enter animation has\n finished, so the row fades in, then lights up.\n* To follow the highlight with a toast, listen to `(highlighted)`: it fires once the flash starts, after the scroll\n and the enter animation, and never for a highlight a newer token replaced before it started.","whenNotToUse":"* To signal a price going up or down: nothing flashes green or red.\n* On page open without a cause: a null token does nothing.","accessibility":"* Under `prefers-reduced-motion: reduce` the scroll is instant and the fade lasts 600 ms.\n* The highlight is decoration only: it carries no information a screen reader would miss.","args":[]},{"title":"Foundations/Transitions on mount","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/foundations-transitions-on-mount--docs","summary":"Nothing animates when a screen opens, even when the router lays the page out before its first\nchange detection. Opening a route creates its elements first and binds their classes on the next\ntick; a layout read in between (the router's scroll to the top, a view transition capturing the\npage) styles the elements without their classes, and the classes then arrive as a change. The\ncomponents below hold their transitions (and their descendants') until two frames after their first\nrender, so their fill, color, thumb and switch track are simply there.","whenToUse":"* `holdTransitionsUntilRendered()`, in the constructor of a component or directive of your own whose\n bound classes carry a transition. `styles/motion.css` holds the matching rule.","whenNotToUse":"* In a view whose change detector is detached: the hold ends after its first render, which never comes.\n* For an element that should animate as it appears: use `animate.enter` or `@starting-style`.","accessibility":"* No effect on semantics: it only delays when transitions start to apply.","args":[]}]},{"name":"nav-item","importPath":"@joanroucoux/cairn-ui/nav-item","declarations":[{"kind":"component","className":"UiNavItem","selector":"a[ui-nav-item]","description":"One destination of the sidebar navigation.","examples":["<a ui-nav-item [active]=\"url === '/'\" routerLink=\"/\">\n <lucide-icon navIcon name=\"house\" />\n Portfolio\n</a>"],"inputs":[{"name":"active","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":["[navIcon]","*"]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Data display/Nav item","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-nav-item--docs","summary":"One destination of the sidebar navigation shown from `64rem`: Portfolio, Holdings,\nAllocation, Accounts. `ui-tab-bar` is its counterpart below that width and reuses the same active,\nhover, rest and focus states.\n\nThe icon is projected in `[navIcon]`; cairn-ui carries no icon family of its own, so the app sizes\nand colors it (Lucide, stroke 1.75, following the active state's text color through `currentColor`).","whenToUse":"* For each entry of the sidebar's primary navigation.","whenNotToUse":"* For a secondary action within a page. Use `ui-row` or `ui-menu` instead.","accessibility":"* `active` sets `aria-current=\"page\"`, following the [current page pattern](https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/).\n* Renders on a native `<a>`, so keyboard focus and navigation come from the platform.","args":[{"name":"active","description":"Sets `aria-current=\"page\"` and the soft background, at once (no fade)."},{"name":"label","description":"Projected label."}]}]},{"name":"result-group","importPath":"@joanroucoux/cairn-ui/result-group","declarations":[{"kind":"component","className":"UiResultGroup","selector":"ui-result-group","description":"One source of a search: a two-tone heading, then its rows, or a skeleton while it answers, a\nmessage when it has nothing, or an inline error with a retry.","examples":["<ui-result-group label=\"Yahoo Finance\" hint=\"live prices\" [state]=\"state()\"\n errorMessage=\"Yahoo Finance didn’t respond.\" retryLabel=\"Try again\" (retry)=\"search()\">\n <button ui-row size=\"dense\" type=\"button\">...</button>\n <span resultGroupMessage>No results from Yahoo Finance.</span>\n</ui-result-group>"],"inputs":[{"name":"label","type":"string","required":true,"twoWay":false},{"name":"hint","type":"string | undefined","required":false,"twoWay":false},{"name":"state","type":"'loading' | 'error' | 'empty' | 'ready'","required":false,"default":"'ready'","twoWay":false},{"name":"errorMessage","type":"string | undefined","required":false,"twoWay":false},{"name":"retryLabel","type":"string | undefined","required":false,"twoWay":false}],"outputs":[{"name":"retry","type":"void"}],"slots":[]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Data display/Result group","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-result-group--docs","summary":"One source of a title search: a heading in two tones (the source in 600 `--foreground`, then what its\nprice is in `--subtle-foreground`), and under it the rows the source answered, a two-row skeleton while it\nanswers, a message when it has nothing, or an inline error with a text-only retry.\n\nIt wraps [Async](?path=/docs/feedback-async--docs) in its `inline` variant: the group reloads alone, the other\nsources stay on screen. Project the rows as the default content (`ui-row size=\"dense\"`, an unavailable\nforeign-currency row last) and the empty message with the `resultGroupMessage` attribute.","whenToUse":"* In a search that asks several sources at once, one group per source, inside a bordered list.","whenNotToUse":"* For a single list: use rows directly. For a failed block that is not a search result, use `ui-async`.","accessibility":"* The host is `role=\"group\"` named by its heading.\n* The loading state sets `aria-busy`; the error is `role=\"alert\"` so a failing source is announced.","args":[{"name":"label","description":"The source, in 600 `--foreground`. It names the group."},{"name":"hint","description":"What its price is, in `--subtle-foreground` after the label."},{"name":"state","description":"`ready` shows the projected rows, `loading` the two-row skeleton, `empty` the `resultGroupMessage`, `error` the message and the retry."},{"name":"errorMessage","description":"Sentence shown when the source failed."},{"name":"retryLabel","description":"Text-only retry button label."},{"name":"retry","description":"Emitted when the retry is pressed. Only this group reloads."}]}]},{"name":"row","importPath":"@joanroucoux/cairn-ui/row","declarations":[{"kind":"directive","className":"UiListRow","selector":"[uiListRow]","description":"Non-interactive row of a list, separated from the next by a hairline: a passkey with its delete\nbutton, a device, a setting. Put it on an `li` or a `div`. Use `ui-row` instead when the whole\nrow is a link or an action.","examples":["<ul>\n <li uiListRow>\n <span class=\"flex-1\">MacBook Air</span>\n <button ui-button variant=\"quiet-destructive\" size=\"icon-sm\" aria-label=\"Delete MacBook Air\">...</button>\n </li>\n</ul>"],"inputs":[{"name":"ruled","type":"boolean","required":false,"default":"true","attribute":true,"twoWay":false}],"outputs":[],"slots":[]},{"kind":"component","className":"UiRow","selector":"a[ui-row], button[ui-row]","description":"Clickable row of a list: a holding, an account, a search result. With `trailing=\"chevron\"` it is a\nflush link row: no side padding, a 16 px chevron at the end, the text dimming on hover.","examples":["<a ui-row [selected]=\"holding.id === openId()\" [routerLink]=\"['/holdings', holding.id]\">\n {{ holding.name }}\n</a>"],"inputs":[{"name":"selected","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"busy","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"unavailable","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"size","type":"'md' | 'lg' | 'xl' | 'card' | 'dense'","required":false,"default":"'md'","twoWay":false},{"name":"padding","type":"'md' | 'sm' | 'none'","required":false,"default":"'md'","twoWay":false},{"name":"gap","type":"'default' | 'sm'","required":false,"default":"'default'","twoWay":false},{"name":"trailing","type":"'none' | 'chevron'","required":false,"default":"'none'","twoWay":false}],"outputs":[],"slots":["*"]},{"kind":"directive","className":"UiRowGroup","selector":"[uiRowGroup]","description":"Groups link rows under a hairline, as the \"other ways\" links under a search list. Put it on a `div`\naround `ui-row` with `trailing=\"chevron\"`.","examples":["<div uiRowGroup>\n <button ui-row type=\"button\" trailing=\"chevron\">Enter manually</button>\n</div>"],"inputs":[],"outputs":[],"slots":[]},{"kind":"directive","className":"UiRowItem","selector":"[uiRowItem]","description":"Lays out a `ui-row` with a trailing action beside it, such as a \"more\" icon button.","examples":["<div uiRowItem>\n <a ui-row size=\"xl\" padding=\"sm\" routerLink=\"/accounts/1\">...</a>\n <button ui-button variant=\"quiet\" size=\"icon-sm\" aria-label=\"Account actions\">...</button>\n</div>"],"inputs":[],"outputs":[],"slots":[]},{"kind":"directive","className":"UiRowTile","selector":"[uiRowTile]","description":"The filled tile that leads a row, holding an icon. Put it on a `span` as the first child of a\n`ui-row` or a `uiListRow`.","examples":["<a ui-row size=\"xl\" padding=\"sm\" href=\"/export\">\n <span uiRowTile><svg lucideDownload [size]=\"18\" /></span>\n Export\n</a>"],"inputs":[],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[{"name":"ROW_GAPS","type":"readonly string[]","values":["default","sm"]},{"name":"ROW_PADDINGS","type":"readonly string[]","values":["md","sm","none"]},{"name":"ROW_SIZES","type":"readonly string[]","values":["md","lg","xl","card","dense"]},{"name":"ROW_TRAILINGS","type":"readonly string[]","values":["none","chevron"]}],"types":[{"name":"RowGap","type":"'default' | 'sm'"},{"name":"RowPadding","type":"'md' | 'sm' | 'none'"},{"name":"RowSize","type":"'md' | 'lg' | 'xl' | 'card' | 'dense'"},{"name":"RowTrailing","type":"'none' | 'chevron'"}],"docs":[{"title":"Data display/Row","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-row--docs","summary":"A clickable row for lists: holdings, accounts, search results. It replaces the per-row cards and\ntext \"Edit\" / \"Delete\" buttons the previous version used.\n\nApplies to a native `<a>` for navigation or a `<button>` for an in-place action, never a `<div>`\nwith a click handler, so keyboard and assistive technology support come for free.","whenToUse":"* For every row of a list the user can open: a holding, an account, a search result.","whenNotToUse":"* For a row that is not clickable. A plain row of text does not need this component. A static list with a hairline between rows, such as the passkeys of a profile, takes `uiListRow` on its `li`: 72px (68px from 64rem), 12px gap, a hairline under each row.\n* For a primary page action, such as \"Add a holding\". Use `ui-button`.","accessibility":"* `selected` sets `aria-current=\"true\"`, for the row whose detail is open next to it.\n* `busy` sets `aria-busy` and `aria-disabled`, shows a spinner in the trailing slot and swallows the clicks while the row keeps its focus.\n* `unavailable` sets `aria-disabled=\"true\"`, grays the row to half opacity, shows a not-allowed cursor, drops hover and press, and swallows the clicks. The row stays focusable, so a screen reader finds it and reads why it cannot be picked: put the reason in the row.\n* Follows the native element's own semantics: an `<a>` for navigation, a `<button>` for an action.","args":[{"name":"selected","description":"Sets `aria-current=\"true\"` and the soft background."},{"name":"busy","description":"An action started from the row is in flight: a spinner turns in the trailing slot, the row sets `aria-busy` and `aria-disabled`, swallows clicks and keeps its focus, so it cannot be activated twice."},{"name":"unavailable","description":"Grays the row (half opacity), forbids the cursor, drops hover and press and swallows clicks. Sets `aria-disabled=\"true\"`."},{"name":"size","description":"Minimum height and gap: `md` 56 px, `lg` 60 px (10 px gap), `xl` 72 px on touch and 68 px with a mouse (12 px gap), `card` 68 px (8 px gap, the rows of a card), `dense` 56 px at every width (12 px gap, result lists)."},{"name":"gap","description":"Space between the row children: the size default, or `sm` for 8 px on any size."},{"name":"trailing","description":"`chevron` makes a flush link row: a 16 px chevron in `--subtle-foreground` at the end, no side padding, a minimum height of `--row-min`, and the text turns `--muted-foreground` on hover instead of the glow. `size`, `padding` and `gap` are ignored."},{"name":"padding","description":"Side padding: `md` 10 px, `sm` 8 px, `none` 0."}]}]},{"name":"segmented","importPath":"@joanroucoux/cairn-ui/segmented","declarations":[{"kind":"component","className":"UiSegmented","selector":"ui-segmented","description":"Exclusive choice within a small, known set: a time range, a unit, a mode.","examples":["<ui-segmented [options]=\"ranges\" label=\"Time range\" [(value)]=\"range\" />"],"inputs":[{"name":"options","type":"SegmentedOption[]","required":true,"twoWay":false},{"name":"label","type":"string","required":true,"twoWay":false},{"name":"showLabel","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"value","type":"string","required":true,"twoWay":true},{"name":"size","type":"'md' | 'sm'","required":false,"default":"'md'","twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[{"name":"SEGMENTED_SIZES","type":"readonly string[]","values":["md","sm"]}],"types":[{"name":"SegmentedOption","type":"{\n value: string;\n label: string;\n}"},{"name":"SegmentedSize","type":"'md' | 'sm'"}],"docs":[{"title":"Inputs/Segmented","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-segmented--docs","summary":"Exclusive choice within a small, known set, such as a chart's time range, a unit or a\nmode. The selection is a `model()`, bound with `[(value)]`.","whenToUse":"* When every option fits on one line and all of them are worth showing at once, which in practice\n means about two to six.\n* When picking an option applies it immediately, with no confirmation step.","whenNotToUse":"* For more options than fit, or for a list that grows. Use\n [Select](?path=/docs/inputs-select--docs).\n* For actions rather than a selection. A row of buttons is not a segmented control.\n* For a choice that only takes effect once a form is submitted, where a radio group reads more\n honestly.","accessibility":"* Implements the [ARIA radio group pattern](https://www.w3.org/WAI/ARIA/apg/patterns/radio/), since\n no native element covers this.\n* A roving `tabindex` keeps a single stop in the tab order: the selected option, or the first one\n when nothing matches.\n* Arrow Left, Right, Up and Down move the selection and the focus together, wrapping at both ends.\n* `label` names the group. Without it, a screen reader announces the options with nothing to tie\n them to.","args":[{"name":"label","description":"Accessible name for the group (`aria-label` on the `radiogroup`)."},{"name":"showLabel","description":"Draws `label` above the track (label size, 500, muted, 6px gap) and names the group by it with `aria-labelledby` instead of `aria-label`."},{"name":"value","description":"Currently selected option value. Two-way bound via `[(value)]`."},{"name":"size","description":"Option height: `md` is 40 px on touch and 32 px with a mouse (Dashboard, Profile); `sm` is 40 px, then 30 px with a mouse (the Holdings detail range)."},{"name":"options","description":"Ordered `{ value, label }` list of choices."}]}]},{"name":"select","importPath":"@joanroucoux/cairn-ui/select","declarations":[{"kind":"directive","className":"UiSelect","selector":"select[uiSelect]","description":"Styled native select, drop-down list included where the browser allows it. Declares the `errors`\nand `touched` inputs Angular's `[formField]` fills, so a surrounding `ui-field` can show the message.","examples":["<select uiSelect><option value=\"pea\">PEA</option></select>"],"inputs":[{"name":"size","type":"'sm' | 'md' | 'lg' | 'xl'","required":false,"default":"'md'","twoWay":false},{"name":"errors","type":"readonly UiControlError[]","required":false,"default":"[]","twoWay":false},{"name":"touched","type":"boolean","required":false,"default":"false","twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Inputs/Select","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-select--docs","summary":"Styled native `<select>`, drop-down list included.\n\nIt is never rebuilt as a listbox. Where the browser supports a customizable select, the control\nopts into it, so the list carries the same surface, border, radius and spacing as the rest of the\nlibrary rather than being drawn by the operating system. Where it does not, the platform draws the\nlist as it always has.\n\nThe opt-in stops at `pointer: fine`, since on a phone the native picker is what makes the control\nusable at all.\n\nThe list always opens downwards. Where there is not enough room below it shrinks to the space left\nand scrolls, rather than flipping above the control.","whenToUse":"* To choose one value from a list that is too long to display in full, such as an account or an\n instrument.","whenNotToUse":"* For a handful of options that all deserve to be visible. Use\n [Segmented](?path=/docs/inputs-segmented--docs).\n* For a binary on or off. A checkbox or a switch says it in one interaction instead of two.","accessibility":"* The control needs an accessible name, either from the label of a surrounding `<ui-field>` or\n from an explicit `aria-label`.\n* Keyboard support, including typeahead, arrow keys and Home and End, comes from the platform.\n* A first option with an empty value works as the placeholder, so the unselected state is a real\n value rather than an invisible one.","args":[{"name":"size","description":"`md` is the 44px touch target used by default."},{"name":"disabled","description":"Native `disabled`."}]}]},{"name":"select-pill","importPath":"@joanroucoux/cairn-ui/select-pill","declarations":[{"kind":"component","className":"UiSelectPill","selector":"ui-select-pill","description":"A pill that opens a `ui-menu` of exclusive choices, to head a row of filter chips: outlined at rest with a\nchevron, solid primary when `active`, where a separate 14 px cross replaces the chevron and emits `cleared`.\nIt is 34 px tall inside a 44 px target on touch and 32 px with a fine pointer; its focus ring is an outline\n2 px outside the pill; the cross has a 44 px hit area on touch (36 px with a fine pointer). `contextLabel`, when\ngiven, is read before the label while active so a screen reader hears what the account is. Clearing moves focus\nback to the trigger. `disabled` dims it like a disabled button, opens nothing and draws the chevron even when `active`. The projected text is the pill label and the projected `ui-menu` is what it opens.","examples":["<ui-select-pill uiChipsLeading [active]=\"!!account()\" clearLabel=\"Clear the account\" (cleared)=\"account.set('')\">\n {{ accountName() }}\n <ui-menu label=\"Account\" sheet>\n <button uiMenuItem [checked]=\"!account()\" (click)=\"account.set('')\">All accounts</button>\n </ui-menu>\n</ui-select-pill>"],"inputs":[{"name":"active","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"disabled","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"clearLabel","type":"string","required":true,"twoWay":false},{"name":"contextLabel","type":"string | undefined","required":false,"twoWay":false}],"outputs":[{"name":"cleared","type":"void"}],"slots":["*","ui-menu"]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Inputs/Select pill","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-select-pill--docs","summary":"Compact pill that opens a [Menu](?path=/docs/surfaces-menu--docs) of exclusive choices, to head a\nrow of [Filter chips](?path=/docs/inputs-filter-chips--docs), for a filter with too many values for chips, such as an\naccount. The projected text is the pill label and the projected `ui-menu` is what it opens, as a sheet on a phone\nlike the `…` menus. At rest it is outlined on the card surface with a chevron; once a value other than the \"all\"\nchoice is picked (`[active]=\"true\"`) it is solid primary and a separate 14 px cross replaces the chevron: it\nemits `cleared`, so the list goes back to every value in one tap. It is 34 px tall inside a 44 px target on touch,\n32 px with a fine pointer.","whenToUse":"* To filter a list by one value among several, next to the chips of another filter.","whenNotToUse":"* A choice made in a form. Use [Select](?path=/docs/inputs-select--docs).\n* A handful of values that all deserve to be visible. Use [Filter chips](?path=/docs/inputs-filter-chips--docs).","accessibility":"* The trigger is named by its label, and `aria-haspopup`, `aria-expanded` and `aria-controls` come from the\n menu. Give the menu its own `label`; its choices are `menuitemradio` items, the current one `aria-checked`.\n* The cross is a real button named by `clearLabel`, which the app passes in its own language, and it is a\n tab stop of its own, outside the trigger.\n* The trigger's focus ring sits 2 px outside the pill; the cross's is a 24 px circle around its icon, in the pill's\n text color so it shows on the solid pill. Both are outlines, so they show in forced colors; the pill keeps a\n border there.","args":[{"name":"active","description":"Solid primary pill with a cross in place of the chevron, for a value other than \"all\"."},{"name":"disabled","description":"Dims the pill like a disabled button and opens nothing; the cross is not drawn and the chevron stays, even when `active`. For a list still loading or that failed to load."},{"name":"clearLabel","description":"Accessible name of the cross, in the language of the app. Required."},{"name":"cleared","description":"Emits when the cross is pressed. Only drawn when `active`."}]}]},{"name":"skeleton","importPath":"@joanroucoux/cairn-ui/skeleton","declarations":[{"kind":"component","className":"UiSkeleton","selector":"ui-skeleton","description":"Decorative, pulsing placeholder for content that is still loading.","examples":["<ui-skeleton shape=\"figure\" />\n<ui-skeleton [lines]=\"3\" />"],"inputs":[{"name":"shape","type":"'text' | 'figure' | 'row' | 'chart' | 'ring' | 'dot'","required":false,"default":"'text'","twoWay":false},{"name":"lines","type":"number","required":false,"default":"1","twoWay":false},{"name":"height","type":"number","required":false,"default":"12","twoWay":false},{"name":"size","type":"number","required":false,"default":"232","twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[{"name":"SKELETON_SHAPES","type":"readonly string[]","values":["text","figure","row","chart","ring","dot"],"description":"Available skeleton shapes."}],"types":[{"name":"SkeletonShape","type":"'text' | 'figure' | 'row' | 'chart' | 'ring' | 'dot'"}],"docs":[{"title":"Feedback/Skeleton","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/feedback-skeleton--docs","summary":"Placeholder standing in for content that has not arrived yet, at the shape of the content it\nreplaces: text, a dominant figure, a list row, a chart, a ring, or a legend dot.\n\n`shape=\"text\"` (the default) draws one bar per `lines`; above one line, the last bar is shortened so\nthe block reads as the tail of a paragraph rather than another full width line. `figure` is the 260 by\n40 dominant figure and `row` a list row. `chart` fills its host from a 200px floor, so the\nscreen sizes the box (200 on the phone, 300 and more on the desktop). `ring` takes `size`\n(232 on the phone, 240 on the desktop) with a 30px band. `dot` is the 12px disc of a legend entry. None of them reads `lines` or `height`.","whenToUse":"* While a first load is in flight, sized and positioned like the content it replaces, so nothing\n jumps when the real content lands.","whenNotToUse":"* For a refresh of content already on screen. Replacing what the user is reading with gray bars\n loses their place.\n* For a wait with no known shape. A skeleton promises a layout it then has to deliver.","accessibility":"* The whole block is `aria-hidden`, so a screen reader never announces a row of decorative bars.\n* The announcement is the calling screen's job: it owns the `role=\"status\"` sentence that says\n content is loading. This component only paints.\n* The pulse reads its duration from `--pulse-duration`, which `tokens.css` sets to `0ms` under\n `prefers-reduced-motion: reduce`: it stops there with no branching in this component.","args":[{"name":"shape","description":"The shape of the content being stood in for. `text` is the only one `lines` and `height` affect."},{"name":"lines","description":"Number of bars, `shape=\"text\"` only. Above 1, the last one is narrower."},{"name":"size","description":"Diameter in pixels, `shape=\"ring\"` only. 232 on the phone, 240 on the desktop; the band is always 30px."},{"name":"height","description":"Bar height in pixels, `shape=\"text\"` only. Match the text being stood in for."}]}]},{"name":"stale-link","importPath":"@joanroucoux/cairn-ui/stale-link","declarations":[{"kind":"directive","className":"UiStaleLink","selector":"a[uiStaleLink]","description":"Link in the stale tone, for a caption about data that is late or left out of a total, such as\n\"1 holding without a price, not counted\". `chevron` adds a trailing chevron, `size` is `caption` (the\ndefault) or `label`.","examples":["<a uiStaleLink chevron size=\"label\" routerLink=\"/holdings\">1 stale price</a>"],"inputs":[{"name":"chevron","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"size","type":"'caption' | 'label'","required":false,"default":"'caption'","twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[{"name":"STALE_LINK_SIZES","type":"readonly string[]","values":["caption","label"],"description":"Available stale link sizes."}],"types":[{"name":"StaleLinkSize","type":"'caption' | 'label'"}],"docs":[{"title":"Navigation/Stale link","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/navigation-stale-link--docs","summary":"A link in the stale tone, for a caption about data that is late or left out of a total.","whenToUse":"* For a caption that leads to the holdings concerned: \"1 stale price\", \"1 holding without a price, not counted\".\n* `size=\"label\"` (14/20) where the link sits in a 14px line, as the dashboard total draws \"1 stale price ›\"; the default `caption` (12/17) elsewhere.","whenNotToUse":"* For a caption that leads nowhere. Use the stale tone of [Facts](?path=/docs/data-display-facts--docs) or of the table sub line.\n* For a link that leaves the application. Use [External link](?path=/docs/navigation-external-link--docs).","accessibility":"* A native `<a>`: the caption names the destination. The optional chevron is a decoration drawn by CSS, so it is not read.\n* Underlined on hover, with a 2px ring on keyboard focus.\n* The text keeps its size (12/17, or 14/20 with `size=\"label\"`), but a `::before` pseudo-element extends the hit area to 44px high (40px with a mouse), centered on the text.","args":[{"name":"chevron","description":"Adds a trailing chevron."},{"name":"label","description":"Projected text: names what is late or left out."},{"name":"size","description":"Text size: `caption` 12/17 (default) or `label` 14/20, both at weight 500. The hit area stays 44px high (40px with a mouse)."}]}]},{"name":"stat","importPath":"@joanroucoux/cairn-ui/stat","declarations":[{"kind":"component","className":"UiStat","selector":"ui-stat","description":"Label, figure and a named period of change: a `hero` for a screen's single headline number, or a\nsmaller `tile` repeated in a grid. The figure, its delta and its caption are projected, so the\ncomponent does no formatting. The `hero` figure does not use `tabular-nums`.","examples":["<ui-stat label=\"Net worth\" size=\"hero\">\n <span uiStatValue>128,656 EUR</span>\n <ui-delta uiStatDelta [value]=\"dayChange()\" unknownLabel=\"Not available\">+316.54 EUR</ui-delta>\n <span uiStatCaption>today</span>\n</ui-stat>"],"inputs":[{"name":"label","type":"string","required":true,"twoWay":false},{"name":"size","type":"'hero' | 'tile'","required":false,"default":"'tile'","twoWay":false}],"outputs":[],"slots":["[uiStatValue]","[uiStatDelta]","[uiStatCaption]","[uiStatAside]"]}],"services":[],"functions":[],"constants":[{"name":"STAT_SIZES","type":"readonly string[]","values":["hero","tile"],"description":"Available stat sizes."}],"types":[{"name":"StatSize","type":"'hero' | 'tile'"}],"docs":[{"title":"Data display/Stat","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-stat--docs","summary":"Label, figure and a named period of change, at two sizes: a `hero` for a screen's single\nheadline number, or a smaller `tile` repeated in a grid.\n\nThe figure goes in `[uiStatValue]`, already formatted by the caller. `[uiStatDelta]` takes a\n`ui-delta` carrying the change, `[uiStatCaption]` names the period it covers (such as \"over 1\nmonth\"), and `[uiStatAside]` is an optional slot for a compact visual such as a sparkline. All\nthree are optional; only the value is required.","whenToUse":"* For the single most important figure on a screen, at `size=\"hero\"`.\n* For one figure among several of the same kind, such as one tile per account, at the default\n `size=\"tile\"`.","whenNotToUse":"* For a figure with no period attached and nothing changing over time. A plain value in a\n `ui-card` is enough.","accessibility":"* The label and the figure are a `dt`/`dd` pair inside a `dl`, so the association survives without\n extra ARIA.\n* The `hero` figure is at least 48px with proportional digits, never `tabular-nums`: a lone\n headline number is not a column of aligned figures.","args":[{"name":"value","description":"The formatted figure, projected into the value slot."},{"name":"delta","description":"The formatted, signed change, projected into the delta slot."},{"name":"label","description":"The stat's label, in sentence case and without a trailing colon."},{"name":"size","description":"`hero` for a screen's single headline figure, `tile` for one among several."}]}]},{"name":"switch","importPath":"@joanroucoux/cairn-ui/switch","declarations":[{"kind":"directive","className":"UiSwitch","selector":"input[type=checkbox][uiSwitch]","description":"Native checkbox styled as an iOS-style switch, exposed to assistive technology as `role=\"switch\"`.","examples":["<input type=\"checkbox\" uiSwitch [checked]=\"hidden()\" (change)=\"toggle()\" aria-label=\"Hide amounts\" />"],"inputs":[],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Inputs/Switch","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-switch--docs","summary":"A native checkbox styled as a track and a sliding knob, exposed to assistive technology as\n`role=\"switch\"`. Being a real checkbox, it works with a plain `[checked]` / `(change)` pair or with\nAngular forms: no custom `ControlValueAccessor` is involved.","whenToUse":"* For a setting that takes effect immediately, with no separate \"Save\" step, such as \"Hide amounts\".","whenNotToUse":"* For a choice that still needs confirming. A switch implies the change already happened.\n* For a choice between more than two options. Use `ui-segmented` instead.","accessibility":"* `role=\"switch\"` overrides the native checkbox role; the checked state stays native, so a screen\n reader announces it the same way it would a real switch.\n* Always give it an accessible name, either `aria-label` or a `<label>` referencing its `id`.\n* Toggles on a click and on Space, both native to the checkbox input.","args":[{"name":"checked","description":"The native checked state."},{"name":"disabled","description":"Dims the switch to opacity 0.4 and blocks interaction."},{"name":"label","description":"Accessible name, wired through `aria-label`."}]}]},{"name":"tab-bar","importPath":"@joanroucoux/cairn-ui/tab-bar","declarations":[{"kind":"component","className":"UiTab","selector":"a[ui-tab]","description":"One destination of `ui-tab-bar`.","examples":["<a ui-tab active routerLink=\"/\"><lucide-icon tabIcon name=\"house\" />Portfolio</a>"],"inputs":[{"name":"active","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":["[tabIcon]","*"]},{"kind":"component","className":"UiTabBar","selector":"nav[ui-tab-bar]","description":"Bottom navigation bar for iPhone. While it is on the page it publishes its height as\n`--tab-bar-height` on the root element, which `ui-action-bar` and `ui-toaster` sit above: use it\nfor anything else that must clear the bar.","examples":["<nav ui-tab-bar>\n <a ui-tab [active]=\"url === '/'\" routerLink=\"/\"><lucide-icon tabIcon name=\"house\" />Portfolio</a>\n</nav>"],"inputs":[],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Data display/Tab bar","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-tab-bar--docs","summary":"Bottom navigation bar shown below `64rem`: 52px of tabs plus `env(safe-area-inset-bottom)`,\nnever a fixed 34px, so it sits above the home indicator on every device. `ui-nav-item` is its\nsidebar counterpart on the desktop layout.\n\nThe component only lays out its tabs; the app positions the bar itself (`fixed` or `sticky`) and\npads the page content by the same height so nothing hides behind it. While it is on the page the bar\npublishes that height as `--tab-bar-height` on the root element, so the app pads with\n`pb-(--tab-bar-height)` instead of restating 52px; `ui-action-bar` and `ui-toaster` sit above it.","whenToUse":"* As the primary navigation on a narrow viewport, mirroring the sidebar's destinations exactly.","whenNotToUse":"* Above `64rem`, where the sidebar (`ui-nav-item`) carries navigation instead.","accessibility":"* The active tab carries `aria-current=\"page\"` and is shown in 600 weight on `--foreground`; an\n inactive tab stays on `--muted-foreground`.\n* Renders on native `<a>` elements inside a `<nav>`, so keyboard focus and navigation come from the\n platform.","args":[]}]},{"name":"table","importPath":"@joanroucoux/cairn-ui/table","declarations":[{"kind":"directive","className":"UiCellSub","selector":"[uiCellSub]","description":"A second line under a cell's main text. `narrow` shows it only under 1024px, for the data held\nback by `secondary` columns.","examples":["<span uiCellSub narrow>10 × 421,26 € · Avg. cost 388,10 €</span>\n<span uiCellSub tone=\"stale\">Price as of 24/09</span>"],"inputs":[{"name":"narrow","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"tone","type":"'subtle' | 'stale' | 'inherit'","required":false,"default":"'subtle'","twoWay":false}],"outputs":[],"slots":[]},{"kind":"directive","className":"UiGroup","selector":"tbody[uiGroup]","description":"Body of one group of rows. `collapsed` hides every `tr[uiTr]` but the group row with `hidden`: the rows\nstay in the DOM, so a `uiFlipList` around the table sees no item added or removed and slides\nnothing. A plain `tr` without `uiTr` is not hidden.","examples":["<tbody uiGroup [collapsed]=\"!open()\">"],"inputs":[{"name":"collapsed","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":[]},{"kind":"component","className":"UiGroupCell","selector":"td[ui-group-cell]","description":"The band that opens a group of rows: name, meta beside it, the projected total on the right.\nSpans the whole row through `colspan`.\n\nWith `collapsible` the band is a button inside the heading, with a chevron that turns when the\ngroup is folded. `expanded` is a model: the page keeps the fold memory and sets `collapsed` on the\n`tbody[uiGroup]`. `toggleDisabled` keeps the button focusable and announced, but a click changes\nnothing (a filter holds the group open). `controls` is the id of the group it folds.","examples":["<tr uiTr group><td ui-group-cell colspan=\"4\" name=\"Brokerage\" meta=\"Savings plan\">...total...</td></tr>\n<tr uiTr group><td ui-group-cell collapsible [(expanded)]=\"open\" controls=\"brokerage\" colspan=\"4\" name=\"Brokerage\">...</td></tr>"],"inputs":[{"name":"name","type":"string","required":true,"twoWay":false},{"name":"meta","type":"string | undefined","required":false,"twoWay":false},{"name":"size","type":"'md' | 'lg'","required":false,"default":"'md'","twoWay":false},{"name":"collapsible","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"expanded","type":"boolean","required":false,"default":"true","twoWay":true},{"name":"toggleDisabled","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"controls","type":"string | undefined","required":false,"twoWay":false}],"outputs":[],"slots":["*"]},{"kind":"directive","className":"UiRowAction","selector":"[uiRowAction]","description":"A control in another cell of a stretched-link row: keeps it clickable above the row-wide link.","examples":["<td uiTd><button uiRowAction type=\"button\">Enter a price</button></td>"],"inputs":[],"outputs":[],"slots":[]},{"kind":"directive","className":"UiRowLink","selector":"a[uiRowLink], button[uiRowLink]","description":"Makes a link or button the way into its row: by default it stretches over the whole row, so the\nrow is one target. With `[stretch]=\"false\"` only the name is the target. `current` marks the open\nholding. Other controls in the row need `uiRowAction` to stay clickable.","examples":["<td uiTd primary><a uiRowLink href=\"/holdings/42\">Ferrari</a></td>\n<td uiTd primary><a uiRowLink [stretch]=\"false\" href=\"/accounts/3\">Brokerage</a></td>"],"inputs":[{"name":"stretch","type":"boolean","required":false,"default":"true","attribute":true,"twoWay":false},{"name":"current","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":[]},{"kind":"directive","className":"UiTable","selector":"table[uiTable]","description":"Styled native table. `row` sets the height of every body row, `rule` draws or drops the hairline\nunder the header, `spaced` leaves a gap between the header and the first row.","examples":["<table uiTable row=\"60\" [rule]=\"false\">"],"inputs":[{"name":"row","type":"'48' | '52' | '60'","required":false,"default":"'48'","twoWay":false},{"name":"rule","type":"boolean","required":false,"default":"true","attribute":true,"twoWay":false},{"name":"spaced","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":[]},{"kind":"directive","className":"UiTd","selector":"td[uiTd]","description":"Body cell. Same inputs as `UiTh` bar `tall` and `width`, which must be set on both to match.","examples":["<td uiTd numeric secondary>12</td>"],"inputs":[{"name":"numeric","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"primary","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"secondary","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"from","type":"'md' | 'lg' | null","required":false,"default":"null","twoWay":false}],"outputs":[],"slots":[]},{"kind":"directive","className":"UiTh","selector":"th[uiTh]","description":"Column header. `numeric` right-aligns, `secondary` (or `from`) hides the column below a\nbreakpoint, `primary` makes it the column that absorbs the free width, `tall` is a 40px header,\n`width` fixes the column width.","examples":["<th uiTh numeric secondary width=\"108px\">Quantity</th>"],"inputs":[{"name":"width","type":"string | null","required":false,"default":"null","twoWay":false},{"name":"numeric","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"primary","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"secondary","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"tall","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"from","type":"'md' | 'lg' | null","required":false,"default":"null","twoWay":false}],"outputs":[],"slots":[]},{"kind":"directive","className":"UiTr","selector":"tr[uiTr]","description":"Body or group row. `selected` takes the soft fill, `interactive` (a row that opens something) also\npresses to it. `group` marks the row that holds a group band: it has no hover state.","examples":["<tr uiTr [selected]=\"open\">...\n<tr uiTr group><td ui-group-cell name=\"Brokerage\">..."],"inputs":[{"name":"group","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"selected","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"interactive","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[{"name":"CELL_BREAKPOINTS","type":"readonly string[]","values":["md","lg"],"description":"Breakpoints a secondary column can be held back until."},{"name":"GROUP_SIZES","type":"readonly string[]","values":["md","lg"],"description":"Group band sizes."},{"name":"SUB_TONES","type":"readonly string[]","values":["subtle","stale","inherit"],"description":"Tones of a subtitle: subtle (default), `--stale` at 500, or the color of the cell."},{"name":"TABLE_ROWS","type":"readonly string[]","values":["48","52","60"],"description":"Body row heights."}],"types":[{"name":"CellBreakpoint","type":"'md' | 'lg'"},{"name":"GroupSize","type":"'md' | 'lg'"},{"name":"SubTone","type":"'subtle' | 'stale' | 'inherit'"},{"name":"TableRow","type":"'48' | '52' | '60'"}],"docs":[{"title":"Data display/Table","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-table--docs","summary":"Styled native `<table>`. The rows and cells stay plain `<tr>` and `<td>`, with `uiTable`,\n`uiTh`, `uiTd` and `uiTr` applied as attributes. The header caption reads once, centered, in the subtle color.\nTable options: `row` (48, 52 or 60px body rows), `[rule]=\"false\"` (no hairline under the header, as on Holdings)\nand `spaced` (4px between the header and the first row, as on Accounts). A row glows on hover;\n`selected` takes the soft fill.\n\n`numeric` right aligns a column and lines its digits up, on the `<th>` and on every `<td>`. `primary` marks the\none column that absorbs the free width and truncates; give the others a `width` on their header.\n`td[ui-group-cell]` is the muted band that opens a group (`size=\"lg\"` is the 48px band of Holdings); wrap the\ngroup in `<tbody uiGroup>` for its 4px tail. Its total is a `ui-amount`. Its heading takes focus with no ring: `h2[data-group-heading]`.\n\n`a[uiRowLink]` or `button[uiRowLink]` in the first cell makes the whole row one target, with a row-wide ring\n(Holdings). `[stretch]=\"false\"` keeps the target on the name, underlined on hover with its own ring\n(Accounts). `current` sets `aria-current`. A control in another cell takes `uiRowAction`. A row\nthat opens something is `interactive` (it presses to the soft fill); one with no action keeps a plain name,\n`<td uiTd primary class=\"truncate font-medium\">`.\n\nUnder 1024px, mark Quantity, Avg. cost and Price `secondary` on their header and cells: they are hidden, and a\n`<span uiCellSub narrow>` carries them as the row subtitle. `uiCellSub` takes a `tone`: subtle, stale or\ninherit. Amounts in a subtitle go through `ui-amount`.","whenToUse":"* For data with more than one dimension, where a row and a column both mean something.","whenNotToUse":"* For layout. A table announces relationships that a layout does not have.\n* For a single list of values, where a list carries the same content with less structure.","accessibility":"* The semantics come from the real `<table>`, so rows, columns and header association work without any ARIA.\n* `selected` sets `aria-selected` on the row; put `current` on its link so the open holding is announced.\n* A column held back by `secondary` must be held back on its header and its cells together. Its content stays\n available in the subtitle.","args":[]},{"title":"Data display/Table groups","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-table-groups--docs","summary":"Collapsible group bands of `uiTable`: `td[ui-group-cell] collapsible` is a button inside the heading\n(`h2 > button[aria-expanded]`) with an 18px chevron that turns to -90 degrees in `--duration-fast`. Bind\n`[(expanded)]`, keep the fold memory in the page and set `[collapsed]` on the `tbody[uiGroup]`: its `tr[uiTr]` rows\nget `hidden` and stay in the DOM, so a `uiFlipList` around the table slides nothing. `toggleDisabled` keeps the\nbutton announced (`aria-disabled`) while a click does nothing, for a group a filter holds open; `controls` is the id\nof the group it folds.","whenToUse":"* To let a long list of grouped rows fold group by group.","whenNotToUse":"* To hide a group a filter or a search keeps open: set `toggleDisabled` and keep `expanded` true.","accessibility":"* The button sits inside the heading and carries `aria-expanded`; `aria-controls` points at the group body.\n* Folded rows are `hidden`, so assistive technology skips them.","args":[{"name":"locked","description":"Sets `toggleDisabled` on every band, as a filter holding the groups open does."}]}]},{"name":"toast","importPath":"@joanroucoux/cairn-ui/toast","declarations":[{"kind":"component","className":"UiToaster","selector":"ui-toaster","description":"Message shown by `UiToasts`: one at a time, no action. A confirmation (`show`) is gone after `--toast-duration`;\nan error (`showError`) stays until its cross is clicked. Centered at the bottom of the content area: from `64rem`\nbetween `--sidebar-width` (0 when unset) and the right edge.\nPlace it once in the app shell.","examples":["<ui-toaster />"],"inputs":[],"outputs":[],"slots":[]}],"services":[{"className":"UiToasts","providedIn":"root","description":"One-slot message queue read by `ui-toaster`: a confirmation that times out, or an error that stays until closed.","examples":["inject(UiToasts).show('Purchase recorded');\ninject(UiToasts).showError('Import failed');"],"members":[{"name":"toast","signature":"toast: Signal<Toast | null>"},{"name":"show","signature":"show(message: string): void"},{"name":"showError","signature":"showError(message: string, closeLabel?: string): void"},{"name":"dismiss","signature":"dismiss(): void"}]}],"functions":[],"constants":[],"types":[{"name":"Toast","type":"| { readonly id: number; readonly text: string; readonly kind: 'success' }\n | { readonly id: number; readonly text: string; readonly kind: 'error'; readonly closeLabel: string }"}],"docs":[{"title":"Feedback/Toast","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/feedback-toast--docs","summary":"A short message after an action. `UiToasts.show(message)` puts a confirmation sentence in the single\nslot of the `<ui-toaster />` the app shell renders once; `UiToasts.showError(message)` puts an error there. A new\nmessage replaces the current one in place, whichever kind. It is an inverted surface (`--primary-foreground` on\n`--primary`, no contour) with a check icon. A confirmation fades out after `--toast-duration` (5 s) and the\ntimer stops while the pointer is over it or focus is inside it. An error has an alert icon, no timer, and stays until\nits cross (labeled by the second argument, \"Close\" by default) is clicked.\n\nBelow `64rem` it is centered 8px above `ui-tab-bar` (its `--tab-bar-height`, 52px plus the safe area), or 8px above\n`ui-action-bar` when one is on the page (the bar publishes its height as `--action-bar-height`); from `64rem` it is\ncentered 24px from the bottom between `--sidebar-width` (0 when the app does not set it) and the right edge. It enters\nwith opacity and an 8px rise over `--duration-base`, and leaves with opacity over `--duration-exit`. Under\n`prefers-reduced-motion: reduce` it only fades.\n\nThe toaster is a `popover=\"manual\"` shown in the top layer, and shown again every time a message arrives, so a\nmessage stays above an open `ui-drawer`. A modal that opens while a message is visible does not raise it: show the\ntoast once the dialog has closed, so it never covers the dialog's main button. Elements behind a modal are inert, so\nthe pause and the cross do not work over an open modal.","whenToUse":"* To confirm that a save, a deletion or an import went through.","whenNotToUse":"* For anything that needs an answer: use `ui-alert` or a dialog.\n* For an error a field can show: put it on the field. The error variant is for a failure with no place of its own.\n* To offer an action. A toast carries a sentence only, never a button.","accessibility":"* The `role=\"status\"` region is in the DOM while empty, so a screen reader announces the first message,\n error included.\n* Keep the sentence short: a confirmation disappears on its own. The error starts with the problem.","args":[]}]}],"tokens":[{"name":"--background","group":"color","light":"#f7f8f8","dark":"#0a0b0b","overrides":[],"role":"Page background"},{"name":"--foreground","group":"color","light":"#111414","dark":"#edefee","overrides":[],"role":"Default text on background"},{"name":"--card","group":"color","light":"#ffffff","dark":"#151717","overrides":[],"role":"Card and panel surface"},{"name":"--card-foreground","group":"color","light":"#111414","dark":"#edefee","overrides":[],"role":"Text on card"},{"name":"--elevated","group":"color","light":"#f1f3f2","dark":"#101111","overrides":[],"role":"Raised surface: menu, chart tooltip, elevated card"},{"name":"--border","group":"color","light":"#e2e5e4","dark":"#242726","overrides":[],"role":"Borders and dividers"},{"name":"--hairline","group":"color","light":"#edefee","dark":"#1e2020","overrides":[],"role":"Row separators inside a card"},{"name":"--muted-foreground","group":"color","light":"#5c6462","dark":"#989d9b","overrides":[],"role":"Secondary text"},{"name":"--subtle-foreground","group":"color","light":"#626a68","dark":"#8f9593","overrides":[],"role":"Tertiary text and unknown values"},{"name":"--primary","group":"color","light":"#161918","dark":"#f2f4f3","overrides":[],"role":"Primary actions and active states"},{"name":"--primary-to","group":"color","light":"#333937","dark":"#c9cecc","overrides":[],"role":"Far end of the primary gradient, and the primary button on hover"},{"name":"--primary-foreground","group":"color","light":"#fafbfa","dark":"#0a0b0b","overrides":[],"role":"Text on primary"},{"name":"--soft","group":"color","light":"rgb(22 25 24 / 0.07)","dark":"rgb(242 244 243 / 0.1)","overrides":[],"role":"Primary at low opacity: selected row, active nav item, pressed state, delta pill"},{"name":"--glow","group":"color","light":"rgb(22 25 24 / 0.05)","dark":"rgb(242 244 243 / 0.07)","overrides":[],"role":"Fainter primary tint: hover on rows, menu items and nav items"},{"name":"--ring","group":"color","light":"#161918","dark":"#f2f4f3","overrides":[],"role":"Focus outline"},{"name":"--positive","group":"color","light":"#047757","dark":"#3fbf8f","overrides":[],"role":"Gain"},{"name":"--negative","group":"color","light":"#b5413a","dark":"#e96e63","overrides":[],"role":"Loss and destructive actions"},{"name":"--stale","group":"color","light":"#86620d","dark":"#c4922a","overrides":[],"role":"Quote older than the freshness rule allows"},{"name":"--ramp-1","group":"color","light":"#161918","dark":"#f2f4f3","overrides":[],"role":"Allocation bar, largest share"},{"name":"--ramp-2","group":"color","light":"#3d4341","dark":"#c9cecc","overrides":[]},{"name":"--ramp-3","group":"color","light":"#646b69","dark":"#a2a9a6","overrides":[]},{"name":"--ramp-4","group":"color","light":"#8b9290","dark":"#7c8380","overrides":[]},{"name":"--ramp-5","group":"color","light":"#b0b6b4","dark":"#5a605e","overrides":[]},{"name":"--ramp-6","group":"color","light":"#ced3d1","dark":"#3b403e","overrides":[],"role":"Allocation bar, smallest share"},{"name":"--muted","group":"color","light":"#f1f3f2","dark":"#1e2020","overrides":[],"role":"Neutral fill: avatar, badge, group band, meter track, switch track, skeleton, segmented track"},{"name":"--destructive","group":"color","light":"#b8443c","dark":"#a33a33","overrides":[]},{"name":"--destructive-foreground","group":"color","light":"#fafbfa","dark":"#fafbfa","overrides":[]},{"name":"--font-sans","group":"typography","value":"'Rubik', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif","overrides":[],"utility":"font-sans"},{"name":"--numeric","group":"typography","value":"tabular-nums","overrides":[]},{"name":"--text-caption","group":"typography","value":"0.75rem","overrides":[],"utility":"text-caption","role":"Badges, price dates, table headers, the \"when to use\" table you are reading"},{"name":"--text-caption--line-height","group":"typography","value":"1.0625rem","overrides":[]},{"name":"--text-label","group":"typography","value":"0.875rem","overrides":[],"utility":"text-label","role":"Labels, secondary text, table cells, segmented options, buttons"},{"name":"--text-label--line-height","group":"typography","value":"1.25rem","overrides":[]},{"name":"--text-body","group":"typography","value":"1rem","overrides":[],"utility":"text-body","role":"Rows, body copy, inputs: 16px is the floor iOS does not zoom on focus"},{"name":"--text-body--line-height","group":"typography","value":"1.5rem","overrides":[]},{"name":"--text-title","group":"typography","value":"1.25rem","overrides":[],"utility":"text-title","role":"Card and section titles"},{"name":"--text-title--line-height","group":"typography","value":"1.75rem","overrides":[]},{"name":"--text-heading","group":"typography","value":"1.75rem","overrides":[],"utility":"text-heading","role":"The page title in the header"},{"name":"--text-heading--line-height","group":"typography","value":"2.125rem","overrides":[]},{"name":"--text-display","group":"typography","value":"2.5rem","overrides":[],"utility":"text-display","role":"The one dominant figure on a screen, such as net worth"},{"name":"--text-display--line-height","group":"typography","value":"2.75rem","overrides":[]},{"name":"--tracking-display","group":"typography","value":"-0.02em","overrides":[]},{"name":"--tracking-caption","group":"typography","value":"0.01em","overrides":[]},{"name":"--tracking-label","group":"typography","value":"normal","overrides":[]},{"name":"--gutter","group":"layout","value":"1rem","overrides":[{"media":"(min-width: 64rem)","value":"2rem"}]},{"name":"--inset-card","group":"layout","value":"1rem","overrides":[{"media":"(min-width: 64rem)","value":"1.5rem"}]},{"name":"--stack-group","group":"layout","value":"1rem","overrides":[]},{"name":"--stack-section","group":"layout","value":"1rem","overrides":[{"media":"(min-width: 64rem)","value":"1.5rem"}]},{"name":"--row-min","group":"layout","value":"2.75rem","overrides":[{"media":"(pointer: fine)","value":"2.5rem"}]},{"name":"--header-height","group":"layout","value":"3.5rem","overrides":[{"media":"(min-width: 64rem)","value":"4.5rem"}]},{"name":"--radius-control","group":"radius","value":"0.5rem","overrides":[],"utility":"rounded-control"},{"name":"--radius-container","group":"radius","value":"0.75rem","overrides":[],"utility":"rounded-container"},{"name":"--radius-pill","group":"radius","value":"9999px","overrides":[],"utility":"rounded-pill"},{"name":"--duration-press","group":"motion","value":"120ms","overrides":[]},{"name":"--duration-fast","group":"motion","value":"180ms","overrides":[]},{"name":"--duration-base","group":"motion","value":"260ms","overrides":[{"media":"(prefers-reduced-motion: reduce)","value":"150ms"}]},{"name":"--duration-exit","group":"motion","value":"180ms","overrides":[]},{"name":"--ease-out","group":"motion","value":"cubic-bezier(0.23, 1, 0.32, 1)","overrides":[],"utility":"ease-out"},{"name":"--ease-sheet","group":"motion","value":"cubic-bezier(0.32, 0.72, 0, 1)","overrides":[],"utility":"ease-sheet"},{"name":"--press-scale","group":"motion","value":"0.97","overrides":[{"media":"(prefers-reduced-motion: reduce)","value":"1"}]},{"name":"--enter-scale","group":"motion","value":"0.95","overrides":[{"media":"(prefers-reduced-motion: reduce)","value":"1"}]},{"name":"--pulse-duration","group":"motion","value":"1600ms","overrides":[{"media":"(prefers-reduced-motion: reduce)","value":"0ms"}]},{"name":"--duration-spin","group":"motion","value":"800ms","overrides":[{"media":"(prefers-reduced-motion: reduce)","value":"1600ms"}]},{"name":"--duration-highlight","group":"motion","value":"1200ms","overrides":[{"media":"(prefers-reduced-motion: reduce)","value":"600ms"}]},{"name":"--toast-duration","group":"motion","value":"5000ms","overrides":[]},{"name":"--chevron-down","group":"asset","value":"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%235c6462' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E\")","overrides":[{"media":"(prefers-color-scheme: dark)","value":"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23989d9b' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E\")"}]}],"foundations":[{"slug":"overview","title":"Overview","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/foundations-overview--docs","content":"# Cairn UI\n\nCairn UI is the component library behind Cairn, a personal wealth tracker. It ships as a publishable\nAngular package (`projects/ui`) and is developed entirely through this Storybook: there is no demo\napplication, so what you browse here is exactly what a consuming application gets.\n\nThe library is deliberately small. It covers the controls and surfaces Cairn actually uses, and each\none wraps a native element rather than replacing it, so keyboard behavior, forms integration and\nassistive technology support come from the platform instead of being rebuilt.\n\n## Get started\n\n- **Browse the components** in the sidebar. Each one opens on its Docs page: what it is for, when to\n reach for it, its accessibility contract, and a table of every input it accepts.\n- **Try them out.** Every story is live. Change any input under **Controls** in the side panel and\n the story rerenders.\n- **Read the code.** The **Code** panel under a story shows the exact template that produced it.\n- **Switch schemes.** The paintbrush in the toolbar renders any story in light or dark, or leaves it\n on the operating system preference. Themes are explained below.\n\n## Installation\n\n```bash\npnpm add @joanroucoux/cairn-ui\n```\n\nTwo steps are needed beyond installing the package, because it ships CSS that a JavaScript import\ncannot pull in on its own.\n\nImport the token sheet from the application's global stylesheet:\n\n```css\n@import '@joanroucoux/cairn-ui/styles/tokens.css';\n```\n\nComponents are imported one subpath per folder, the package root exports nothing:\n\n```ts\nimport { UiButton } from '@joanroucoux/cairn-ui/button';\n```\n\nThen register the package as a Tailwind source, since Tailwind does not scan `node_modules`:\n\n```css\n@source '../node_modules/@joanroucoux/cairn-ui';\n```\n\n## Themes\n\nEvery token declares both of its values at once with\n[`light-dark()`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark), so the\nactive scheme is decided by the `color-scheme` in effect rather than by a second set of variables.\n\nAn application that does nothing inherits the operating system preference and is already correct in\nboth schemes. An application that offers its own `system | light | dark` setting forces the two\nexplicit cases by stamping `data-theme` on `<html>`:\n\n| Preference | Attribute on <html> | Scheme in effect |\n| --- | --- | --- |\n| `system` | none | whichever one the operating system asks for |\n| `light` | `data-theme=\"light\"` | light |\n| `dark` | `data-theme=\"dark\"` | dark |\n\nLeaving the attribute off is what makes `system` work, so a preference store must remove it rather\nthan write an empty value or a third theme name. That is the whole contract: one attribute, no\nJavaScript theme object, no class list to keep in sync.\n\nSee [Colors](https://joanroucoux.github.io/cairn-ui/?path=/docs/foundations-colors--docs) for every token this resolves and its measured\ncontrast at both ends.\n\n## Contributing a component\n\nComponents live one per folder in `projects/ui/`, each with its implementation, its spec and\nits stories side by side, and each one an entry point of its own (`ng-package.json` and `index.ts`),\nimported as `@joanroucoux/cairn-ui/<folder>`. `badge`, `button`\nand `input` are the reference implementations to copy the shape from: standalone, zoneless, signal\n`input()`s, and Tailwind utilities that read the tokens instead of hardcoding a color."},{"slug":"colors","title":"Colors","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/foundations-colors--docs","content":"# Colors\n\nThe library ships a single token sheet, `styles/tokens.css` (published as `dist/ui/styles/tokens.css`).\n\nCairn is **monochrome**. The brand carries no hue, which leaves semantic color (a gain, a loss, a stale quote) as the\nonly color in the interface. That is the point of the direction rather than a limitation of it: a green figure on a\nscreen where nothing else is colored is impossible to miss.\n\nEvery token declares both of its values with `light-dark()`. The active scheme follows the OS preference, unless the\nhost application forces one by setting `data-theme=\"light\"` or `data-theme=\"dark\"` on `<html>`.\n\n## Measured contrast\n\nThese ratios were computed, not eyeballed. Every text token clears the WCAG AA threshold of 4.5:1 against every ground it sits on:\n`--background`, `--card`, `--elevated`, `--muted`, and the `--soft` highlight over `--background` or `--card` (selected\nrow, active legend row, delta pill). Each row below shows the worst of them, and `tokens.spec.ts` fails below 4.5:1, so a change to a value shows up here rather than only in a number someone has to trust.\n\n| Token | Role | Light | Dark |\n| --- | --- | --- | --- |\n| `--foreground` | Default text on background | 16.62:1 | 15.58:1 |\n| `--muted-foreground` | Secondary text | 4.97:1 | 5.03:1 |\n| `--subtle-foreground` | Tertiary text and unknown values | 4.54:1 | 4.54:1 |\n| `--positive` | Gain | 4.54:1 | 5.97:1 |\n| `--negative` | Loss and destructive actions | 4.54:1 | 4.53:1 |\n| `--stale` | Quote older than the freshness rule allows | 4.56:1 | 4.94:1 |\n| `--primary-foreground` | Text on primary | 11.37:1 | 12.37:1 |\n\n`--positive`, `--negative` and `--stale` are **darker in light mode than in dark mode**. This is not an\ninconsistency: a mid-tone green that reads at 8.5:1 on a near-black ground only reaches 4.07:1 on white, which\nfails. The role is what stays fixed across schemes, never the hex.\n\n## The neutral ramp\n\n`--ramp-1` through `--ramp-6` paint allocation bars. A categorical palette would contradict the monochrome premise,\nso luminance is the only variable left; adjacent steps separate by at least 1.44:1 in dark and 1.36:1 in light.\nBecause each category gets its own bar rather than a segment in a stacked strip, that separation only has to make\nthe steps distinguishable, never identifiable on its own, since the label sits next to every bar.\n\n| Token | Role |\n| --- | --- |\n| `--background` | Page background |\n| `--foreground` | Default text on background |\n| `--card` | Card and panel surface |\n| `--card-foreground` | Text on card |\n| `--elevated` | Raised surface: menu, chart tooltip, elevated card |\n| `--border` | Borders and dividers |\n| `--hairline` | Row separators inside a card |\n| `--muted` | Neutral fill: avatar, badge, group band, meter track, switch track, skeleton, segmented track |\n| `--muted-foreground` | Secondary text |\n| `--subtle-foreground` | Tertiary text and unknown values |\n| `--primary` | Primary actions and active states |\n| `--primary-to` | Far end of the primary gradient, and the primary button on hover |\n| `--primary-foreground` | Text on primary |\n| `--soft` | Primary at low opacity: selected row, active nav item, pressed state, delta pill |\n| `--glow` | Fainter primary tint: hover on rows, menu items and nav items |\n| `--ring` | Focus outline |\n| `--positive` | Gain |\n| `--negative` | Loss and destructive actions |\n| `--stale` | Quote older than the freshness rule allows |\n| `--ramp-1` | Allocation bar, largest share |\n| `--ramp-6` | Allocation bar, smallest share |\n\n> Both columns render from the same custom properties: `light-dark()` resolves against each swatch's own\n> `color-scheme`, which is how the whole system switches themes with a single attribute."},{"slug":"motion","title":"Motion","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/foundations-motion--docs","content":"# Motion\n\nOnly `transform` and `opacity` animate anywhere in Cairn. Never `ease-in`: something that appears or moves always\neases out, so it settles rather than lunges. Exits are faster than entries. Nothing animates on page entry: the\ndashboard opens several times a day, and a screen that replays its own arrival every time reads as slow.\n\n## Durations\n\n| Token | Value | For |\n| ---------------------- | ------ | ------------------------------------------------------------------- |\n| `--duration-press` | 120ms | Press feedback, on pointer down |\n| `--duration-fast` | 180ms | Menus, the segmented control's thumb |\n| `--duration-base` | 260ms | Dialogs, sheets, the drawer, list-to-detail, a chart's range change |\n| `--duration-exit` | 180ms | Every exit |\n| `--duration-spin` | 800ms | One turn of the button spinner (1600ms under reduced motion) |\n| `--duration-highlight` | 1200ms | A freshly edited row fading back (600ms under reduced motion) |\n| `--toast-duration` | 5000ms | How long a confirmation stays (an error stays until closed) |\n\n## Curves\n\n| Token | Value | For |\n| -------------- | -------------------------------- | ---------------------------------------------------------- |\n| `--ease-out` | `cubic-bezier(0.23, 1, 0.32, 1)` | Anything that appears or moves |\n| `--ease-sheet` | `cubic-bezier(0.32, 0.72, 0, 1)` | Sheets rising from the bottom of the screen, and back down |\n\n## Scales\n\n| Token | Value | For |\n| ------------------ | ------ | ------------------------------------------------------------------------------------ |\n| `--press-scale` | 0.97 | A pressed control, on `:active`: `scale(var(--press-scale))` over `--duration-press` |\n| `--enter-scale` | 0.95 | Dialogs and menus, which start at this scale and opacity 0, never at `scale(0)` |\n| `--pulse-duration` | 1600ms | The skeleton's opacity pulse, from 1 to 0.45 and back, alternating |\n\n## Reduced motion\n\n`tokens.css` neutralizes all three under `prefers-reduced-motion: reduce`, with no component-side branching:\n\n```css\n@media (prefers-reduced-motion: reduce) {\n :root {\n --press-scale: 1; /* a press no longer scales */\n --enter-scale: 1; /* a dialog or menu fades in instead of scaling up */\n --duration-base: 150ms; /* slides and scales become short fades */\n --pulse-duration: 0ms; /* the skeleton stops pulsing */\n --duration-highlight: 600ms;\n --duration-spin: 1600ms;\n }\n}\n```\n\nBecause every animated component reads its scale, duration and curve from these custom properties instead of a\nhardcoded value, asking for reduced motion turns every scale into a no-op and every pulse into a static fill without\na single `@media` query inside a component."},{"slug":"typography","title":"Typography","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/foundations-typography--docs","content":"# Typography\n\nThe library self-hosts **Rubik**, the only family used for both the logotype and the interface. Import it once\nalongside the tokens:\n\n```css\n@import 'tailwindcss';\n@import '@joanroucoux/cairn-ui/styles/tokens.css';\n@import '@joanroucoux/cairn-ui/styles/theme.css';\n@import '@joanroucoux/cairn-ui/styles/fonts.css';\n```\n\n`theme.css` registers `font-sans`, so `var(--font-sans)` and the Tailwind default font stack both resolve to Rubik\nwith the same system fallbacks.\n\n## Sizes\n\nExactly six sizes, each with its own line height baked in: `text-{size}` sets both. `theme.css` also removes\nTailwind's default scale, so `text-sm`, `text-lg`, `text-xl`... no longer exist. A size a component does not use is a\nsize that cannot be reached.\n\n| Class | Size / line height | Use |\n| --- | --- | --- |\n| `text-caption` | 12 / 17 | Badges, price dates, table headers, the \"when to use\" table you are reading |\n| `text-label` | 14 / 20 | Labels, secondary text, table cells, segmented options, buttons |\n| `text-body` | 16 / 24 | Rows, body copy, inputs: 16px is the floor iOS does not zoom on focus |\n| `text-title` | 20 / 28 | Card and section titles |\n| `text-heading` | 28 / 34 | The page title in the header |\n| `text-display` | 40 / 44 | The one dominant figure on a screen, such as net worth |\n\n`text-heading` and `text-display` also carry a tighter tracking (`--tracking-display`, -0.02em); `text-caption`\nopens up slightly (`--tracking-caption`, 0.01em) to stay legible at 12px; `text-label` sets a normal tracking\n(`--tracking-label`), so `text-caption lg:text-label` needs no `tracking-normal` to drop the caption's.\n\n## Weights\n\nFour weights, each with a fixed role, never chosen for its own sake:\n\n| Weight | Role |\n| ------ | ---------------------------------- |\n| 400 | Body text |\n| 500 | Labels and buttons |\n| 600 | Titles and the one dominant figure |\n| 700 | The logotype, and nothing else |\n\n## Tabular figures\n\n`--numeric` sets `font-variant-numeric: tabular-nums`. Apply it wherever digits sit in a column and have to line up:\ntables, lists, a figure next to its own delta. The one dominant figure on a screen (a `text-display` net worth, a\n`ui-stat` hero) stays proportional instead: it reads better on its own than a column of tabular digits does."}]}
|
|
1
|
+
{"schemaVersion":1,"package":{"name":"@joanroucoux/cairn-ui","version":"0.11.4","peerDependencies":{"@angular/common":"^22.0.0","@angular/core":"^22.0.0"}},"storybookUrl":"https://joanroucoux.github.io/cairn-ui/","setup":"Import three stylesheets, in order, and register the package as a Tailwind source (templates in `node_modules` are\nnot scanned by default):\n\n```css\n@import 'tailwindcss';\n@import '@joanroucoux/cairn-ui/styles/tokens.css';\n@import '@joanroucoux/cairn-ui/styles/theme.css';\n@import '@joanroucoux/cairn-ui/styles/motion.css';\n@source '../node_modules/@joanroucoux/cairn-ui';\n```\n\n`fonts.css` self-hosts Rubik and must not be imported through the Tailwind entry above: its `@font-face` rules use\n`url(./fonts/...)` paths relative to the file, and Tailwind's CSS-first pipeline does not rebase them. Add it to the\n`styles` array of `angular.json` instead:\n\n```json\n\"styles\": [\"src/styles.css\", \"node_modules/@joanroucoux/cairn-ui/styles/fonts.css\"]\n```\n\nAn application that does not use Tailwind can import `@joanroucoux/cairn-ui/styles/tokens.css` alone: it is a pure\ntoken sheet.","entries":[{"name":"action-bar","importPath":"@joanroucoux/cairn-ui/action-bar","declarations":[{"kind":"component","className":"UiActionBar","selector":"ui-action-bar","description":"Bar of actions fixed above the tab bar on iPhone, hidden from `64rem`, where the same actions\nlive in the page itself. Its children share the width equally. While it is on the page it\npublishes its height as `--action-bar-height` on the root element, which `ui-toaster` clears.","examples":["<ui-action-bar>\n <button ui-button variant=\"outline\" size=\"tall\">Sell</button>\n <button ui-button size=\"tall\">Buy</button>\n</ui-action-bar>"],"inputs":[],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Surfaces/Action bar","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/surfaces-action-bar--docs","summary":"Bar of actions fixed above the tab bar on iPhone, such as Sell and Buy on a holding's\ndetail: the primary actions, within thumb reach. It sits `--tab-bar-height` (52px plus the safe area) above the\nbottom edge, so it clears `ui-tab-bar`, draws a hairline on top over the page background, pads with the\ngutter, and splits its width equally between its children. It is hidden from `64rem`, where the same\nactions live in the page.\n\nPad the scrolling content by the bar's height (about 73px) so its end is not hidden behind it.\n\nWhile it is on the page the bar publishes its height as `--action-bar-height` on the root element:\n`ui-toaster` adds it to its offset, so a confirmation sits 8px above the bar and never covers the actions.","whenToUse":"* For the one or two actions that define a detail screen on a phone.","whenNotToUse":"* On a desktop, where the bar is hidden: place the actions in the page.\n* For navigation. That is the [Tab bar](?path=/docs/data-display-tab-bar--docs).","accessibility":"* A plain container: the actions inside keep their native button semantics and focus order.\n* Use `size=\"tall\"` on the buttons so each reaches a 48px touch target.","args":[]}]},{"name":"alert","importPath":"@joanroucoux/cairn-ui/alert","declarations":[{"kind":"component","className":"UiAlert","selector":"ui-alert","description":"Inline message explaining a failure or a consequence, or static help with `info`: an icon, an optional heading and a text.\nIt fades in when it appears; set `[fadeIn]=\"false\"` on an alert that is part of the page as it\nopens.","examples":["<ui-alert heading=\"Sign-in failed\">Try again.</ui-alert>"],"inputs":[{"name":"variant","type":"'error' | 'warning' | 'info'","required":false,"default":"'error'","twoWay":false},{"name":"heading","type":"string | undefined","required":false,"twoWay":false},{"name":"fadeIn","type":"boolean","required":false,"default":"true","attribute":true,"twoWay":false}],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[{"name":"ALERT_VARIANTS","type":"readonly string[]","values":["error","warning","info"]}],"types":[{"name":"AlertVariant","type":"'error' | 'warning' | 'info'"}],"docs":[{"title":"Feedback/Alert","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/feedback-alert--docs","summary":"Inline message that explains why something failed, or what an action is about to cost: a\nLucide icon in the `--negative` color, an optional heading and a text.","whenToUse":"* To report a failed submission next to the form that caused it, such as a sign-in error.\n* To explain a step with static help, such as what a manual entry cannot verify (`info` variant).\n* To warn, right above a confirmation, about an irreversible consequence (`warning` variant).","whenNotToUse":"* For a failed data load inside a block. Use [Async](?path=/docs/feedback-async--docs), which offers a retry.\n* For a label on a row. Use [Badge](?path=/docs/data-display-badge--docs).","accessibility":"* The `error` variant is `role=\"alert\"`, announced as soon as it appears. The `warning` variant is\n `role=\"status\"`, announced politely.\n* The `info` variant is `role=\"note\"`: read in place, never announced.\n* The icon is decorative and hidden from assistive technology: the text carries the message.","args":[{"name":"variant","description":"`error` is a bordered card with the circle icon. `warning` is a bare inline notice with the triangle icon. `info` is a `--muted` box with the circle-i icon in the text color, a static note."},{"name":"heading","description":"Optional heading. The text below it turns muted."},{"name":"text","description":"Projected text content."},{"name":"fadeIn","description":"Fades the alert in over `--duration-fast` when it appears (default). Turn it off for an alert that is part of the page as it opens, such as an error state after a load: that one shows without motion."}]}]},{"name":"amount","importPath":"@joanroucoux/cairn-ui/amount","declarations":[{"kind":"component","className":"UiAmount","selector":"ui-amount","description":"A number or an amount of money, formatted for the locale and masked when the app hides amounts.","examples":["<ui-amount [value]=\"total\" currency=\"EUR\" numeric=\"proportional\" />\n<ui-amount [value]=\"dayChange\" currency=\"EUR\" signed whenMasked=\"hide\" />"],"inputs":[{"name":"value","type":"number | null | undefined","required":false,"twoWay":false},{"name":"currency","type":"string | undefined","required":false,"twoWay":false},{"name":"locale","type":"string","required":false,"default":"inject(LOCALE_ID)","twoWay":false},{"name":"signed","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"fractionDigits","type":"number","required":false,"default":"2","twoWay":false},{"name":"numeric","type":"'tabular' | 'proportional'","required":false,"default":"'tabular'","twoWay":false},{"name":"whenMasked","type":"'dots' | 'hide'","required":false,"default":"'dots'","twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[{"name":"formatAmount","signature":"formatAmount(value: number | null | undefined, options: AmountFormatOptions, masked?: boolean): string","description":"Formats a number or an amount for the locale. A missing value is an em dash, a masked one four bullets.","examples":[]}],"constants":[{"name":"AMOUNT_MASKINGS","type":"readonly string[]","values":["dots","hide"],"description":"Available masking behaviors."},{"name":"AMOUNT_NUMERICS","type":"readonly string[]","values":["tabular","proportional"],"description":"Available figure styles."},{"name":"UI_AMOUNT_MASKED","type":"InjectionToken<Signal<boolean>>","description":"App-wide \"amounts are hidden\" state. Defaults to never masked; the app overrides it."}],"types":[{"name":"AmountMasking","type":"'dots' | 'hide'"},{"name":"AmountNumeric","type":"'tabular' | 'proportional'"},{"name":"AmountFormatOptions","type":"{\n locale: string;\n currency?: string;\n signed?: boolean;\n fractionDigits?: number;\n}"}],"docs":[{"title":"Data display/Amount","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-amount--docs","summary":"Formats a number or an amount of money for the locale: two decimals, the currency symbol\nafter the figure with a no-break space, the typographic minus (U+2212) on a negative value, an\nexplicit plus on a signed gain. A missing value renders as an em dash. Screens never format an\namount themselves: this is the only place that does.\n\nMasking comes from the `UI_AMOUNT_MASKED` injection token, a signal the app provides (Cairn's\n“Hide amounts” setting). By default nothing is masked.","whenToUse":"* For every euro amount, quantity, average cost or price shown to the user.\n* For the one dominant figure on a screen, with `numeric=\"proportional\"` so its digits are not\n forced into a tabular grid that was designed for lists.","whenNotToUse":"* For a percentage: percentages stay visible even when amounts are masked, so they never go\n through `ui-amount`.\n* For a signed change that sits right next to its own percentage. Pass `whenMasked=\"hide\"` so the\n pair disappears together instead of leaving a masked half next to a visible one.","accessibility":"* A masked value still renders as text (four bullets, or nothing with `whenMasked=\"hide\"`, which\n sets the native `hidden` attribute): a screen reader reads it as it is, so a screen that masks an\n amount must not rely on it alone to convey meaning.","args":[{"name":"value","description":"The number to format. `null` or `undefined` render as an em dash."},{"name":"currency","description":"ISO 4217 code. Omitted for a plain number, such as a quantity."},{"name":"signed","description":"Prefixes a gain with `+` and a loss with the typographic minus."},{"name":"numeric","description":"`tabular` aligns figures in lists and tables. `proportional` suits the one dominant figure."},{"name":"whenMasked","description":"`dots` shows four bullets in place of the figure. `hide` removes the element entirely."},{"name":"fractionDigits","description":"Decimal places. Defaults to two."}]}]},{"name":"async","importPath":"@joanroucoux/cairn-ui/async","declarations":[{"kind":"component","className":"UiAsync","selector":"ui-async","description":"The three states of a block that depends on a service call, next to the ready content. During the\nfirst 150 ms of a load the `[asyncLoading]` content is laid out but invisible, unanimated and hidden from\nassistive technology, so the block already holds its skeleton's space.","examples":["<ui-async [state]=\"totalState()\" errorTitle=\"The total couldn’t be loaded\" errorMessage=\"The server didn’t respond.\" retryLabel=\"Try again\" (retry)=\"reload()\">\n <span asyncLoading><ui-skeleton shape=\"figure\" /></span>\n <span asyncEmpty>Add a holding to see your total.</span>\n <span>{{ total() }}</span>\n</ui-async>"],"inputs":[{"name":"state","type":"'loading' | 'error' | 'empty' | 'ready'","required":true,"twoWay":false},{"name":"errorTitle","type":"string | undefined","required":false,"twoWay":false},{"name":"errorMessage","type":"string | undefined","required":false,"twoWay":false},{"name":"retryLabel","type":"string | undefined","required":false,"twoWay":false},{"name":"retryIcon","type":"boolean","required":false,"default":"true","attribute":true,"twoWay":false,"description":"Draws the rotate icon in the retry button. Only the `inline` variant honors `false`."},{"name":"variant","type":"'elevated' | 'plain' | 'emphasis' | 'inline'","required":false,"default":"'elevated'","twoWay":false},{"name":"align","type":"'start' | 'center' | 'auto'","required":false,"default":"'start'","twoWay":false},{"name":"card","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"fill","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[{"name":"retry","type":"void"}],"slots":["[asyncLoading]","[asyncEmpty]","*"]}],"services":[],"functions":[{"name":"delayedState","signature":"delayedState(source: Signal<AsyncState>): Signal<AsyncState | null>","description":"Turns the live state of a call into the state to draw: `loading` only appears once the call has\nlasted 150 ms and, once shown, stays at least 400 ms. Until then the previous state stays on\nscreen; `null` means no state to draw yet (a first load under 150 ms), which `ui-async` renders as an invisible skeleton. Call it in an injection context.","examples":["protected readonly shown = delayedState(this.state);"]}],"constants":[{"name":"ASYNC_ALIGNS","type":"readonly string[]","values":["start","center","auto"],"description":"Alignments of the error block; `auto` is centered from 64rem."},{"name":"ASYNC_STATES","type":"readonly string[]","values":["loading","error","empty","ready"],"description":"Available async block states."},{"name":"ASYNC_VARIANTS","type":"readonly string[]","values":["elevated","plain","emphasis","inline"],"description":"Looks of the error block."}],"types":[{"name":"AsyncAlign","type":"'start' | 'center' | 'auto'"},{"name":"AsyncState","type":"'loading' | 'error' | 'empty' | 'ready'"},{"name":"AsyncVariant","type":"'elevated' | 'plain' | 'emphasis' | 'inline'"}],"docs":[{"title":"Feedback/Async","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/feedback-async--docs","summary":"The three states of a block that depends on a service call: a loading skeleton, an error\nshown in place with a retry button, or an empty state. Whichever state is active, the block's own\ntitle stays visible above it: that title is not part of this component, the caller renders it.\n\nOne call, one block, its three states: a call that fails never erases the rest of the screen.\n\nThe error block has four looks. `elevated` (the default) is a tinted box with the token line heights.\n`plain` has no surface and the normal line height, for a block that sits in a card or a table the\nscreen already draws. `emphasis` is a heading, a longer message and a primary retry button, for a\nlist that is the whole page. `inline` is one row, message left and retry right, for a failure\ninside a form. `align` centers the block (`auto`: from 64rem), `card` draws a card around it and\n`fill` makes it fill a host the screen sizes.","whenToUse":"* Around any content that comes from an HTTP call: a total, a chart, a list.\n* When two blocks on the same screen depend on different calls: give each its own `ui-async`, so\n one failing never hides the other.","whenNotToUse":"* Around content that never depends on a call, such as static labels or client-side computed\n values.\n* For a full-page error. `ui-async` covers one block; a screen where every block failed shows its\n own page-level error instead.","accessibility":"* The host carries `aria-busy=\"true\"` while loading.\n* The error block is `role=\"alert\"` inside an `aria-live=\"polite\"` region, so assistive technology\n announces it as soon as it appears.","args":[{"name":"title","description":"The block's own title, rendered by the caller above the three states."},{"name":"state","description":"Which slot is currently shown."},{"name":"errorTitle","description":"Error state title, next to the block name."},{"name":"errorMessage","description":"Error state detail sentence."},{"name":"retryLabel","description":"Label of the retry button."},{"name":"retryIcon","description":"Draws the rotate icon in the retry button (default). Turn it off for a text-only retry, as in a per-source search error. Only the `inline` variant honors it."},{"name":"variant","description":"Look of the error block: `elevated` tinted box, `plain` no surface, `emphasis` heading and primary retry, `inline` one row."},{"name":"align","description":"Alignment of the error block. `auto` is the start below 64rem and centered from 64rem."},{"name":"card","description":"The error block draws its own card (background, container radius, hairline)."},{"name":"fill","description":"The error block fills its host and centers vertically; the host sets the minimum height."},{"name":"retry","description":"Emitted when the retry button is pressed. Only that block reloads."}]}]},{"name":"avatar","importPath":"@joanroucoux/cairn-ui/avatar","declarations":[{"kind":"component","className":"UiAvatar","selector":"ui-avatar","description":"Initials in a disc, standing in for a person.","examples":["<ui-avatar initials=\"JR\" label=\"My account\" />"],"inputs":[{"name":"initials","type":"string","required":true,"twoWay":false},{"name":"label","type":"string","required":true,"twoWay":false},{"name":"size","type":"'sm' | 'md' | 'lg' | 'auto'","required":false,"default":"'md'","twoWay":false}],"outputs":[],"slots":[]},{"kind":"directive","className":"UiAvatarLink","selector":"a[uiAvatarLink], button[uiAvatarLink]","description":"Round hit area around a `ui-avatar` that links somewhere. On the current page\n(`aria-current=\"page\"`, set by `routerLinkActive` through `ariaCurrentWhenActive`) it holds a\nsoft halo.","examples":["<a uiAvatarLink routerLink=\"/profile\"><ui-avatar initials=\"JR\" label=\"Profile\" size=\"auto\" /></a>"],"inputs":[],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[{"name":"AVATAR_SIZES","type":"readonly string[]","values":["sm","md","lg","auto"],"description":"Available avatar sizes."}],"types":[{"name":"AvatarSize","type":"'sm' | 'md' | 'lg' | 'auto'"}],"docs":[{"title":"Data display/Avatar","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-avatar--docs","summary":"Initials in a disc, standing in for a person.","whenToUse":"* As the account entry in the shell. Cairn is a single user application, so exactly one avatar is on\n screen at a time.","whenNotToUse":"* As a decorative shape. It carries an image role and an accessible name, both of which cost a\n screen reader something to announce.\n* To display an uploaded picture. This component draws initials only.","accessibility":"* The host carries `role=\"img\"` and takes its accessible name from `label`.\n* The initials are `aria-hidden`, so a screen reader announces the real name instead of spelling\n out two letters.","args":[{"name":"initials","description":"Two-letter (or so) glyph rendered inside the disc. Purely decorative: hidden from assistive tech."},{"name":"label","description":"Accessible name, announced instead of the initials. Usually the full name."},{"name":"size","description":"Disc diameter: `sm` 32 px, `md` 34 px (iPhone header), `lg` 56 px (Profil), `auto` 34 px then 32 px from 64rem (the app header)."}]}]},{"name":"back-link","importPath":"@joanroucoux/cairn-ui/back-link","declarations":[{"kind":"component","className":"UiBackLink","selector":"a[ui-back-link], button[ui-back-link]","description":"Link back to the parent screen: a chevron and the name of the destination. On a `button`, it\ngoes back inside the current view, such as from a form to the search that led to it.","examples":["<a ui-back-link routerLink=\"/accounts\">Accounts</a>"],"inputs":[{"name":"size","type":"'md' | 'sm' | 'header' | 'inline'","required":false,"default":"'md'","twoWay":false}],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[{"name":"BACK_LINK_SIZES","type":"readonly string[]","values":["md","sm","header","inline"],"description":"Available back link sizes."}],"types":[{"name":"BackLinkSize","type":"'md' | 'sm' | 'header' | 'inline'"}],"docs":[{"title":"Navigation/Back link","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/navigation-back-link--docs","summary":"Link back to the parent screen: a chevron and the name of the destination.","whenToUse":"* On a detail or form screen reached from a list, to return to that list.\n* `md` heads a screen on iPhone; `sm` is the quieter desktop breadcrumb in a header.","whenNotToUse":"* For navigation between sibling destinations. `ui-nav-item` and `ui-tab-bar` cover that.\n* For undoing an action. That is a button.","accessibility":"* A native `<a>`: it is announced as a link named by its label, which should name the destination\n (“Accounts”), not the gesture (“Back”).\n* The chevron is decorative and hidden from assistive technologies.\n* `md`, `sm` and `header` add no margin: place them with `-mx-2` or `-ml-1` so the chevron lines up\n with the content edge while the hit area stays full size. `inline` carries its own outset (-8px top, -4px\n bottom, -6px left, at the start of a flex column): put it at the top of a form column as it is.","args":[{"name":"size","description":"`md` is 44 px high in the foreground color (screen header on iPhone); `sm` is 36 px, muted, with a hover glow (desktop header); `header` has the `md` metrics with a 24 px chevron and a focus ring drawn inside (a screen header); `inline` is the 14 px 500 muted back step of a form, 44 px high (36 px with a mouse), 18 px chevron, hover to the foreground color, on a `button`, with its own outset at the top of a form column."},{"name":"label","description":"Projected text: the name of the destination."}]}]},{"name":"badge","importPath":"@joanroucoux/cairn-ui/badge","declarations":[{"kind":"component","className":"UiBadge","selector":"ui-badge","description":"Small status descriptor for counts, states or labels, such as an instrument's asset class.","examples":["<ui-badge>ETF</ui-badge>"],"inputs":[{"name":"variant","type":"'neutral' | 'outline'","required":false,"default":"'neutral'","twoWay":false},{"name":"size","type":"'md' | 'sm'","required":false,"default":"'md'","twoWay":false}],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[{"name":"BADGE_SIZES","type":"readonly string[]","values":["md","sm"]},{"name":"BADGE_VARIANTS","type":"readonly string[]","values":["neutral","outline"],"description":"Available badge variants."}],"types":[{"name":"BadgeSize","type":"'md' | 'sm'"},{"name":"BadgeVariant","type":"'neutral' | 'outline'"}],"docs":[{"title":"Data display/Badge","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-badge--docs","summary":"Small descriptor that labels the thing next to it, such as a source's sync status or an\naccount's envelope type.","whenToUse":"* To qualify a row or a heading with a short, stable piece of metadata.\n* When the label is a single word or two. A badge is read at a glance, not parsed.","whenNotToUse":"* For a signed figure that moves, such as a gain or a loss. Use [Delta](?path=/docs/data-display-delta--docs).\n* For a message the user has to act on. A badge is a label, not a notification.","accessibility":"* Always give the badge text. Its variant color is a reinforcement, never the only signal for the\n state it names.\n* The badge is inert. Nothing inside it should be focusable or clickable.","args":[{"name":"variant","description":"`neutral` sits on the `--muted` surface. `outline` is quieter still, for a state that should not compete with the content around it."},{"name":"size","description":"`md` is 24 px high, `sm` 22 px for dense rows."},{"name":"label","description":"Projected text content."}]}]},{"name":"button","importPath":"@joanroucoux/cairn-ui/button","declarations":[{"kind":"component","className":"UiButton","selector":"button[ui-button], a[ui-button]","description":"Styled native button or anchor, applied as an attribute so the host keeps every native behavior.","examples":["<button ui-button variant=\"destructive\">Delete</button>\n<button ui-button size=\"icon\" variant=\"ghost\" aria-label=\"More actions\">...</button>"],"inputs":[{"name":"variant","type":"'primary' | 'outline' | 'ghost' | 'destructive' | 'quiet' | 'quiet-destructive' | 'outline-destructive' | 'quiet-glow' | 'tonal'","required":false,"default":"'primary'","twoWay":false},{"name":"size","type":"'compact' | 'slim' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'tall' | 'block' | 'icon' | 'icon-sm'","required":false,"default":"'md'","twoWay":false},{"name":"loading","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"busy","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[{"name":"BUTTON_SIZES","type":"readonly string[]","values":["compact","slim","xs","sm","md","lg","xl","xxl","tall","block","icon","icon-sm"],"description":"Available button sizes."},{"name":"BUTTON_VARIANTS","type":"readonly string[]","values":["primary","outline","ghost","destructive","quiet","quiet-destructive","outline-destructive","quiet-glow","tonal"],"description":"Available button variants."}],"types":[{"name":"ButtonSize","type":"'compact' | 'slim' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' | 'tall' | 'block' | 'icon' | 'icon-sm'"},{"name":"ButtonVariant","type":"'primary' | 'outline' | 'ghost' | 'destructive' | 'quiet' | 'quiet-destructive' | 'outline-destructive' | 'quiet-glow' | 'tonal'"}],"docs":[{"title":"Inputs/Button","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-button--docs","summary":"Clickable element that triggers an action. Its variant tells the user how consequential\nthat action is, so a screen reads at a glance.","whenToUse":"* For any action taken on the current screen: submitting a form, opening a dialog, refreshing quotes.\n* On an `<a>`, only when the action genuinely is a navigation the browser performs itself, such as a\n file download.","whenNotToUse":"* For ordinary navigation between screens. A link should look like a link, and its role should stay\n a link.\n* More than one `primary` per screen. If every action is emphasized, none of them is.\n* For a destructive action without a confirmation step. Pair `destructive` with a dialog.","accessibility":"* The host stays a native `<button>` or `<a>`, so focus, activation with Enter or Space, and the\n disabled state all come from the platform rather than from an ARIA imitation.\n* `size=\"md\"` is the 40/44px touch target Cairn applies everywhere: 40px under a mouse\n (`pointer: fine`), 44px on a touch screen.\n* `size=\"icon\"` has no visible text: it still needs a name of its own, through `aria-label`.\n* A `loading` button is marked `aria-busy` and `aria-disabled`, stays focusable, and swallows a\n click instead of letting a slow first response turn into a double submission.","args":[{"name":"variant","description":"How much emphasis the action carries. `primary` for the expected action, `destructive` for an irreversible one, `outline` and `ghost` for everything else. `quiet` is a muted icon button that fills `--soft` on hover and press (row actions). `quiet-glow` is the same icon button hovering to `--glow` with no press fill (a close cross). `quiet-destructive` turns negative on hover. `outline-destructive` is an outline with negative text. `tonal` is a filled `--muted` pill that becomes a plain text button with a glow hover from 64rem."},{"name":"size","description":"`md` is the 40/44px touch target used everywhere by default. `xs` (32px), `sm` (36px), `lg` (44px) and `xl` (50px) are fixed heights. `compact` (32px, 28px from 64rem), `slim` (44px, then 32px), `xxl` (52px, then 44px), `tall` (48px, then 40px) and `block` (50px, then 44px) step down at 64rem. `icon` is square and `icon-sm` is 44px on touch, 36px with a mouse; both need an `aria-label`."},{"name":"disabled","description":"Native `disabled`, so the button leaves the tab order instead of only looking inactive."},{"name":"loading","description":"Marks the button busy, keeps its width, and blocks a second click while an action is in flight."},{"name":"busy","description":"Marks the button busy like `loading`, without the spinner: the label alone says what is happening (“Signing in…”)."},{"name":"label","description":"Projected text content."}]}]},{"name":"card","importPath":"@joanroucoux/cairn-ui/card","declarations":[{"kind":"component","className":"UiCard","selector":"ui-card","description":"Surface that groups related content: a summary block, a table, a side panel. `surface` limits the\ncard styling to one side of `64rem`. `variant=\"inset\"` is the `--background` box that sits inside\na card or a dialog.","examples":["<ui-card variant=\"elevated\" padding=\"sm\">Total</ui-card>\n<ui-card variant=\"inset\" border=\"hairline\" padding=\"xs\">Results</ui-card>\n<ui-card padding=\"none\" clip>Table that touches the edges, clipped to the corners</ui-card>\n<ui-card surface=\"max-lg\">Card on an iPhone, plain on a desktop</ui-card>"],"inputs":[{"name":"variant","type":"'default' | 'elevated' | 'inset'","required":false,"default":"'default'","twoWay":false},{"name":"padding","type":"'none' | 'xs' | 'sm' | 'md' | 'list' | 'rows' | 'recap' | 'panel'","required":false,"default":"'md'","twoWay":false},{"name":"surface","type":"'always' | 'lg' | 'max-lg'","required":false,"default":"'always'","twoWay":false},{"name":"border","type":"'auto' | 'none' | 'border' | 'hairline'","required":false,"default":"'auto'","twoWay":false},{"name":"clip","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[{"name":"CARD_PADDINGS","type":"readonly string[]","values":["none","xs","sm","md","list","rows","recap","panel"],"description":"Available card paddings."},{"name":"CARD_SURFACES","type":"readonly string[]","values":["always","lg","max-lg"],"description":"Where the card surface (fill, border, radius) and its padding apply."},{"name":"CARD_VARIANTS","type":"readonly string[]","values":["default","elevated","inset"],"description":"Available card variants."}],"types":[{"name":"CardPadding","type":"'none' | 'xs' | 'sm' | 'md' | 'list' | 'rows' | 'recap' | 'panel'"},{"name":"CardSurface","type":"'always' | 'lg' | 'max-lg'"},{"name":"CardVariant","type":"'default' | 'elevated' | 'inset'"}],"docs":[{"title":"Surfaces/Card","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/surfaces-card--docs","summary":"Surface that groups related content, such as a summary block, a table or a side panel.","whenToUse":"* To draw a boundary around content that belongs together and can be read on its own.\n* With `padding=\"none\"` when the content manages its own spacing, which is what a table does.\n* With `surface=\"lg\"` or `\"max-lg\"` when the same block is a card at one width only, such as a detail that is a card beside the list on a desktop and a plain screen on an iPhone.","whenNotToUse":"* As a spacing utility. If nothing is being grouped, a card adds a border for no reason.\n* Nested inside another card of the same variant. Two identical surfaces stacked read as one.","accessibility":"* The card is a plain visual container. It carries no role and no landmark.\n* When it groups a distinct section of a screen, give the content inside it a heading, so the\n section is reachable from a screen reader's heading list.","args":[{"name":"variant","description":"`default` for a primary surface. `elevated` sits on the same tokens as a table's zebra rows, for content that should read as secondary to a `default` card next to it. `inset` is the `--background` box with the control radius that nests inside a card or a dialog."},{"name":"border","description":"Inset 1px ring. `auto` (default) follows the variant: `border` for `default`, `hairline` for `elevated`, `none` for `inset`. Set it to override: a hairline results box, a bordered picked title."},{"name":"padding","description":"Inner spacing; `xs` is 4 px, `sm` 12 px, `md` follows `--inset-card` (16 px, 24 px from 1 024 px), `recap` is 4 px / 12 px for a before/after recap whose rows carry their own 9 px, `panel` is 10 x 12 px, then 12 x 16 px from 1 024 px, `list` keeps a narrow side padding for rows that carry their own inset, `rows` is the 4 px / `--inset-card` inset of a ruled list such as facts, `none` when the content manages its own (e.g. a table)."},{"name":"clip","description":"Clips the content to the rounded corners (`overflow-hidden`). Off by default, so focus rings and popovers of the content are not cut. Use it with `padding=\"none\"` when a table or list touches the edges and its hovered or selected last row must follow the corners."},{"name":"surface","description":"Where the fill, the border, the radius and the padding apply: `always` (default), `lg` (from 64rem, plain below), or `max-lg` (below 64rem, plain from it). The block stays one element, so a detail screen can be a card on a desktop and a plain page on an iPhone."},{"name":"label","description":"Projected content."}]}]},{"name":"choice-chips","importPath":"@joanroucoux/cairn-ui/choice-chips","declarations":[{"kind":"component","className":"UiChoiceChips","selector":"ui-choice-chips","description":"Exclusive choice among a handful of named options, shown as wrapping pills. Binds to Signal Forms\nwith `[formField]`.","examples":["<ui-choice-chips label=\"Wrapper\" [options]=\"wrappers\" [(value)]=\"wrapper\" />","<ui-choice-chips label=\"Wrapper\" [options]=\"wrappers\" [formField]=\"form.wrapper\" />"],"inputs":[{"name":"options","type":"ChoiceChipOption[]","required":true,"twoWay":false},{"name":"value","type":"string","required":true,"twoWay":true},{"name":"label","type":"string | undefined","required":false,"twoWay":false},{"name":"ariaLabel","type":"string | undefined","required":false,"twoWay":false},{"name":"ariaLabelledby","type":"string | undefined","required":false,"twoWay":false},{"name":"surface","type":"'background' | 'card'","required":false,"default":"'background'","twoWay":false},{"name":"disabled","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"errors","type":"readonly UiControlError[]","required":false,"default":"[]","twoWay":false},{"name":"invalid","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"required","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"touched","type":"boolean","required":false,"default":"false","twoWay":false}],"outputs":[{"name":"touch","type":"void"}],"slots":[]}],"services":[],"functions":[],"constants":[],"types":[{"name":"ChoiceChipOption","type":"{\n value: string;\n label: string;\n}"}],"docs":[{"title":"Inputs/Choice chips","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-choice-chips--docs","summary":"Exclusive choice among a handful of named options, shown as wrapping pills, such as an\naccount wrapper, a category or a frequency. The selection is a `model()`, bound with `[(value)]`\nor with Signal Forms' `[formField]`, which fills `disabled`, `errors`, `invalid`, `required` and `touched` too.","whenToUse":"* When every option is worth showing at once and their labels are words rather than a short code,\n about three to ten.\n* In a form, where the choice takes effect on submit.","whenNotToUse":"* For a numeric range or a view switch applied immediately. Use\n [Segmented](?path=/docs/inputs-segmented--docs).\n* For a long or growing list. Use [Select](?path=/docs/inputs-select--docs).\n* For several choices at once: this is a single choice.","accessibility":"* Implements the [ARIA radio group pattern](https://www.w3.org/WAI/ARIA/apg/patterns/radio/).\n* A roving `tabindex` keeps a single stop in the tab order: the selected chip, or the first one\n when nothing matches.\n* Arrow Left, Right, Up and Down move the selection and the focus together, wrapping at both ends.\n Home and End jump to the first and last chip. Space and Enter select the focused chip.\n* Name the group with `label` (shown above the chips), or with `ariaLabel` or `ariaLabelledby`\n when the label lives elsewhere.","args":[{"name":"options","description":"Ordered `{ value, label }` list of choices."},{"name":"label","description":"Visible label above the chips. It also names the `radiogroup`."},{"name":"ariaLabel","description":"Accessible name for the group when there is no visible `label`."},{"name":"value","description":"Selected option value. Two-way bound via `[(value)]`."},{"name":"surface","description":"Fill of the unselected chips, to match the surface they sit on."},{"name":"disabled","description":"Dims every chip and removes them from interaction."}]}]},{"name":"control","importPath":"@joanroucoux/cairn-ui/control","declarations":[],"services":[],"functions":[],"constants":[{"name":"UI_CONTROL","type":"InjectionToken<UiControl>","description":"Resolves to the styled control projected into a field. A control provides it and declares the\n`errors` and `touched` inputs that Angular's `[formField]` fills."}],"types":[{"name":"UiControlError","type":"{ readonly message?: string }","description":"The part of a validation error this library reads. Angular's own `ValidationError` satisfies it."},{"name":"UiControl","type":"{\n readonly errors: Signal<readonly UiControlError[]>;\n readonly touched: Signal<boolean>;\n}","description":"What a styled control offers the field wrapped around it."}],"docs":[]},{"name":"delta","importPath":"@joanroucoux/cairn-ui/delta","declarations":[{"kind":"component","className":"UiDelta","selector":"ui-delta","description":"Signed amount whose sign carries meaning: a gain, a loss, or an unknown.\n\nA null value is not a zero: it renders the unknown state, not a gain of zero. Color never\ncarries the sign on its own: the projected text must already contain a + or a minus sign.","examples":["<ui-delta [value]=\"holding.dayChangeEur\" unknownLabel=\"Not available\">\n {{ holding.dayChangeEur | signedMoney }}\n</ui-delta>"],"inputs":[{"name":"value","type":"number | null | undefined","required":true,"twoWay":false},{"name":"unknownLabel","type":"string","required":true,"twoWay":false},{"name":"emphasis","type":"'text' | 'pill'","required":false,"default":"'text'","twoWay":false},{"name":"size","type":"'label' | 'inherit'","required":false,"default":"'label'","twoWay":false},{"name":"weight","type":"'medium' | 'regular'","required":false,"default":"'medium'","twoWay":false}],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[{"name":"DELTA_EMPHASES","type":"readonly string[]","values":["text","pill"],"description":"Available delta emphases."},{"name":"DELTA_SIZES","type":"readonly string[]","values":["label","inherit"],"description":"Type sizes."},{"name":"DELTA_WEIGHTS","type":"readonly string[]","values":["medium","regular"],"description":"Font weights."}],"types":[{"name":"DeltaEmphasis","type":"'text' | 'pill'"},{"name":"DeltaSize","type":"'label' | 'inherit'"},{"name":"DeltaWeight","type":"'medium' | 'regular'"}],"docs":[{"title":"Data display/Delta","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-delta--docs","summary":"Signed amount whose sign carries meaning: a gain, a loss, or a value that is simply not\nknown.\n\n`value` selects the color and nothing else. The figure itself is projected content, and the caller\nis responsible for formatting it with its own sign, because color can never carry that sign alone.","whenToUse":"* For a figure whose direction matters as much as its magnitude, such as an unrealised gain or a\n daily change.\n* Whenever that figure can legitimately be unknown. Twelve of Cairn's twenty six holdings have no\n cost basis.","whenNotToUse":"* For a plain amount that has no direction, such as a total or a quantity.\n* With `value` set to `0` to stand in for a missing figure. A zero claims a known result of zero,\n which is a different statement. Pass `null`.","accessibility":"* A `null` value renders an em dash marked `aria-hidden` next to a visually hidden\n `unknownLabel`, so the unknown state is announced rather than skipped in silence.\n* The sign always appears in the projected text, so the meaning survives without color.","args":[{"name":"value","description":"Selects the color: positive, negative, neutral, or unknown when `null`. It never affects the text that is displayed."},{"name":"emphasis","description":"`text` colors the figure only. `pill` also draws a neutral chip behind it."},{"name":"size","description":"`label` sets the label size; `inherit` takes the size of the line it sits in."},{"name":"weight","description":"`medium` (500, the default) or `regular` (400) for a figure that sits in running rows."},{"name":"label","description":"Projected content: the formatted figure, already carrying its own sign."}]}]},{"name":"dialog","importPath":"@joanroucoux/cairn-ui/dialog","declarations":[{"kind":"component","className":"UiDialog","selector":"ui-dialog","description":"Modal dialog on the native <dialog>: a bottom sheet under `64rem`, centered above it.\n\n`busy` keeps it open while its action runs: Escape, a backdrop click and a sheet drag do nothing\nand the cross is disabled. The owner disables its own buttons and still closes it through `open`.","examples":["<ui-dialog heading=\"Buy\" description=\"Ferrari\" closeLabel=\"Close\" width=\"528px\" [open]=\"buying()\" (dismissed)=\"buying.set(false)\">\n <p>Body</p>\n <button dialogActions ui-button variant=\"outline\" class=\"max-lg:hidden\" (click)=\"buying.set(false)\">Cancel</button>\n <button dialogActions ui-button>Buy 40 shares</button>\n</ui-dialog>"],"inputs":[{"name":"heading","type":"string","required":true,"twoWay":false},{"name":"description","type":"string | undefined","required":false,"twoWay":false},{"name":"closeLabel","type":"string | undefined","required":false,"twoWay":false},{"name":"variant","type":"'default' | 'confirm'","required":false,"default":"'default'","twoWay":false},{"name":"sheet","type":"'fit' | 'full'","required":false,"default":"'fit'","twoWay":false},{"name":"truncateDescription","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"width","type":"'md' | 'lg' | (string & {}) | undefined","required":false,"twoWay":false},{"name":"open","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"busy","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[{"name":"dismissed","type":"void"},{"name":"closed","type":"'escape' | 'backdrop' | 'cross' | 'drag' | 'programmatic'"}],"slots":["*","[dialogActions]"]}],"services":[],"functions":[],"constants":[{"name":"DIALOG_CLOSE_REASONS","type":"readonly string[]","values":["escape","backdrop","cross","drag","programmatic"],"description":"What started a close, as `closed` reports it."},{"name":"DIALOG_SHEETS","type":"readonly string[]","values":["fit","full"],"description":"`full` makes the sheet under `64rem` fill the screen below the status area; `fit` sizes it to its content."},{"name":"DIALOG_VARIANTS","type":"readonly string[]","values":["default","confirm"],"description":"`confirm` is a short alertdialog: no cross, no hairlines, one 24px padding, 440px wide unless told otherwise."},{"name":"DIALOG_WIDTHS","type":"readonly string[]","values":["md","lg"],"description":"Named dialog widths; any other CSS length is accepted as is."}],"types":[{"name":"DialogCloseReason","type":"'escape' | 'backdrop' | 'cross' | 'drag' | 'programmatic'"},{"name":"DialogSheet","type":"'fit' | 'full'"},{"name":"DialogVariant","type":"'default' | 'confirm'"},{"name":"DialogWidth","type":"'md' | 'lg'"}],"docs":[{"title":"Surfaces/Dialog","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/surfaces-dialog--docs","summary":"Modal dialog built on the native `<dialog>`. The focus trap, the return of focus to the\ntrigger, closing on Escape and the top layer all come from the platform rather than from custom\nJavaScript.\n\nUnder `64rem` it rises from the bottom as a sheet, with a drag handle, 16px padding and its footer\nclear of the home indicator; above that width it is centered with 24px padding. Its body scrolls on\nits own when the content is taller than the screen, so the header and the footer stay in place.\nIts veil is `rgb(0 0 0 / 0.36)`; opened while another modal is open (over a `ui-drawer`), it draws\na lighter `rgb(0 0 0 / 0.24)` over that one's, with nothing to set.\n\nThe header carries the title, an optional subtitle (`description`) and a close cross, which exists\nonly when the consumer passes `closeLabel`. The width is set by the consumer: `md`, `lg` or any CSS\nlength (Add a holding 560px, Buy and New account 480px, a confirmation 440px).\n\nEvery dialog has the same three zones. The header (title, optional subtitle, cross) is padded 16px 16px 16px 24px, with the title and the cross centered on each other, or lined up at the top under a subtitle; the body 20px 24px and is the only zone that scrolls, the footer 16px 24px. On a sheet they are 0 8px 12px 16px, 16px and 12px 16px plus the safe area, under a drag handle. A hairline runs under the header and above the footer, and both come with the cross: without `closeLabel` there is no cross and no hairlines. On a desktop the dialog is centered and never taller than `100dvh - 96px`.\n\nThe `variant` input is `default` or `confirm`: a confirmation (deleting a passkey) is announced as an alertdialog, has no cross and no hairlines, one 24px padding and a 440px width unless `width` says otherwise. The `sheet` input is `fit` (the sheet is as tall as its content) or `full` (it fills the screen below 58px, so the footer stays pinned while the content changes, as in Add a holding). `truncateDescription` keeps the subtitle on one ellipsized line instead of wrapping. A click that starts and ends on the backdrop closes the dialog, like Escape.\n\nContent goes in two slots: the default one for the body, and `[dialogActions]` for the footer\nbuttons. Put the dismissing action first. On a desktop the footer is right-aligned; on a sheet the\nactions stack at full width and 50px, the primary on top, so hide Cancel there with\n`max-lg:hidden` when the sheet has only the primary.\n\nEvery way out goes through one close: Escape, the backdrop, the cross, a drag of the sheet and the\nowner setting `open` back to `false`. `dismissed` fires at once for a close the reader started, never\nfor one the owner asked for. `closed` fires once per close, after the exit transition has played,\nwith what started it: `escape`, `backdrop`, `cross`, `drag` or `programmatic`. When there is no\ntransition to wait for, it fires at once. Reopening during the exit, or destroying the dialog, drops it.\n\nA consumer that mounts the dialog under `@if` keeps it mounted until `closed`. It sets `open` to\n`false` (on `dismissed`, or after a save), and only removes the wrapper and reports to its own\nparent in `(closed)`. Removing the wrapper together with `open = false`, or on `dismissed`, cuts the\nexit: the `<dialog>` leaves the page before the exit has played.\n\n```html\n@if (selling()) {\n <ui-dialog heading=\"Sell\" [open]=\"open()\" (dismissed)=\"open.set(false)\" (closed)=\"selling.set(false); sold.emit()\">\n ...\n </ui-dialog>\n}\n```\n\nOn a desktop the dialog fades and scales up from `--enter-scale` in `--duration-base` and leaves in\n`--duration-exit`, on `--ease-out`. As a sheet the panel only slides, from `translateY(100%)`, on\n`--ease-sheet`, while the veil fades. The handle and the header of a sheet can be dragged down: the\npanel follows the finger, closes past 30 % of its height or on a release faster than 0.5 px/ms, and\notherwise springs back on `--ease-sheet` in `--duration-base`. The body scrolls and never drags.\n\n`busy` keeps the dialog open while its action runs, so the outcome stays on screen: Escape, a click on the\nbackdrop and a drag of the sheet do nothing, the cross is disabled and the dialog carries `aria-busy`. The\nowner disables its own buttons and still closes it by setting `open` to `false`.\nWith `prefers-reduced-motion: reduce` the sheet fades instead of sliding.","whenToUse":"* When an answer is needed before anything else can continue, such as confirming a deletion.\n* To edit a short, self contained form without leaving the current screen.","whenNotToUse":"* For a task the user repeats often. Interrupting the same flow again and again wears thin.\n* For information that is not urgent, or that the user may want to keep visible while working.\n* To hold a long form. If the content scrolls inside the dialog, it belongs on a screen.","accessibility":"* `aria-labelledby` and `aria-describedby` are wired from `heading` and `description`. Never set\n them by hand.\n* Follows the [W3C dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) through\n the native element, including the initial focus move and the focus return on close.\n* The close cross is named by `closeLabel`, which the consumer translates. Without it there is no\n cross, and Escape stays the way out.\n* Every way out is handled the same: Escape, the cross, the backdrop, a drag of the sheet and the\n owner setting `open` back to `false`.","args":[]}]},{"name":"donut","importPath":"@joanroucoux/cairn-ui/donut","declarations":[{"kind":"component","className":"UiDonut","selector":"ui-donut","description":"A ring chart with a legend: slices rank from largest to smallest, and hovering, focusing or\nactivating a legend row highlights its slice.","examples":["<ui-donut [slices]=\"byAssetClass()\" label=\"By asset class\" othersLabel=\"Other\" [valueFormat]=\"formatEur\" [shareFormat]=\"formatShare\" (sliceSelect)=\"goToHoldings($event)\" />"],"inputs":[{"name":"slices","type":"DonutSlice[]","required":true,"twoWay":false},{"name":"label","type":"string","required":true,"twoWay":false},{"name":"othersLabel","type":"string","required":true,"twoWay":false},{"name":"valueFormat","type":"(value: number) => string","required":false,"default":"identityValue","twoWay":false},{"name":"shareFormat","type":"(share: number) => string","required":false,"default":"identityShare","twoWay":false},{"name":"legendHref","type":"((id: string) => string | null) | undefined","required":false,"twoWay":false}],"outputs":[{"name":"sliceSelect","type":"string"}],"slots":[]}],"services":[],"functions":[],"constants":[],"types":[{"name":"DonutSlice","type":"{\n id: string;\n label: string;\n value: number;\n sublabel?: string;\n}"}],"docs":[{"title":"Data display/Donut","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-donut--docs","summary":"A ring chart with its legend: slices come from `slices`, ranked largest first on the neutral gray\nramp (`--ramp-1` to `--ramp-6`). Beyond six slices, the five largest keep their rank and the rest\nmerge into one “Other” slice on `--ramp-6`, whose legend sub-label lists the merged names, while the\nring's `aria-label` still names every original slice.\n\nHovering, focusing or touching a legend row (or its arc) grows that arc by 4 % around the ring center, dims the other arcs to half opacity, and shows its name, share\nand value at the center; activating a row (click or Enter) emits `sliceSelect` with its id, for the\ncaller to navigate with. With `legendHref`, rows are real links: a plain click is canceled and emitted as `sliceSelect` so a router can navigate, while Ctrl/Cmd/Shift-click and middle-click keep the browser's behavior. Formatting is the caller's business: `valueFormat` and `shareFormat` both\narrive as inputs, so masking amounts is a matter of a formatter that returns bullets; shares stay\nvisible either way.","whenToUse":"* A breakdown by one dimension (asset class, account, wrapper) where the parts sum to a whole.","whenNotToUse":"* Comparing two breakdowns side by side with a shared color meaning: the ramp is assigned by rank,\n not by category, so the same color means different things in two different rings.\n* A value that is not a share of a total, such as a trend over time. Use `ui-line-chart` instead.","accessibility":"* The `<svg>` is `role=\"img\"`, named by `label` followed by every slice and its share, so the full\n detail survives even when several slices are grouped into “Other” visually.\n* Every legend row is a `<button>`, or a link when `legendHref` returns a URL for it: reachable by Tab, activated with Enter (and Space on a button), and its own text\n already carries the name, the sub-label, the share and the value: nothing depends on color alone.","args":[{"name":"slices","description":"Slices to rank and draw, each `{ id, label, value, sublabel? }`."},{"name":"label","description":"The ring’s accessible name, e.g. “By asset class”."},{"name":"othersLabel","description":"Label of the grouped slice beyond the sixth, e.g. “Other”."},{"name":"valueFormat","description":"Formats a slice's value, for the legend and the center."},{"name":"shareFormat","description":"Formats a slice's share (0..1), for the legend and the center."},{"name":"legendHref","description":"Maps a slice id to a URL. A row with a URL becomes a link, a row without one (or without the input) stays a button."},{"name":"sliceSelect","description":"Emitted with a slice id when its legend row is activated."}]}]},{"name":"drawer","importPath":"@joanroucoux/cairn-ui/drawer","declarations":[{"kind":"component","className":"UiDrawer","selector":"ui-drawer","description":"Modal side panel on the native <dialog>, against the right edge at full height, for a detail opened from a list.\n\nWithout `heading` it draws no header: the content brings its own title and close button, and `label` names it.\nWith both, `label` names the panel and the heading stays its visible title.","examples":["<ui-drawer heading=\"Northwind World\" closeLabel=\"Close the detail\" [open]=\"open()\" (dismissed)=\"open.set(false)\">\n <p>Body</p>\n</ui-drawer>"],"inputs":[{"name":"heading","type":"string | undefined","required":false,"twoWay":false},{"name":"description","type":"string | undefined","required":false,"twoWay":false},{"name":"closeLabel","type":"string | undefined","required":false,"twoWay":false},{"name":"label","type":"string | undefined","required":false,"twoWay":false},{"name":"width","type":"string","required":false,"default":"'440px'","twoWay":false},{"name":"open","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"busy","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[{"name":"dismissed","type":"void"},{"name":"closed","type":"'escape' | 'backdrop' | 'cross' | 'programmatic'"}],"slots":["*"]}],"services":[],"functions":[],"constants":[{"name":"DRAWER_CLOSE_REASONS","type":"readonly string[]","values":["escape","backdrop","cross","programmatic"],"description":"What started a close, as `closed` reports it."}],"types":[{"name":"DrawerCloseReason","type":"'escape' | 'backdrop' | 'cross' | 'programmatic'"}],"docs":[{"title":"Surfaces/Drawer","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/surfaces-drawer--docs","summary":"Modal side panel built on the native `<dialog>`, against the right edge of the window at full\nheight, for the detail of a row opened from a list on a desktop. The focus trap, the inert page, Escape and the\nreturn of focus to the opener come from the platform.\n\nIt is 440px wide (`width` takes any CSS length), on `--card`, with a shadow on its left edge and 24px of padding;\na `rgb(0 0 0 / 0.36)` veil covers the rest of the window. The panel scrolls on its own and keeps the scroll from\nchaining to the page (`overscroll-behavior: contain`); the page is not locked, so the list behind keeps its\ncolumns and its scroll position.\n\nWith `heading` it draws a header: the title, an optional `description` under it, and a 36px close cross when\n`closeLabel` is set. Without `heading` it draws nothing of its own: the content brings its title row and its\nclose button, and `label` gives the panel its accessible name. With both, `label` names the panel and the heading\nstays its visible title. The content is laid out in a column with a 24px gap.\n\nEvery way out goes through one close: Escape, a click that starts and ends on the veil, the cross and the owner\nsetting `open` back to `false`. `dismissed` fires at once for a close the reader started, never for one the owner\nasked for; `closed` fires once, after the exit has played, with `escape`, `backdrop`, `cross` or\n`programmatic`. `busy` keeps it open while an action runs, like `ui-dialog`.\n\nThe panel enters with opacity and a 24px slide from the right over `--duration-base` on `--ease-out`, while the\nveil fades in; it leaves the reverse way over `--duration-exit`. Under `prefers-reduced-motion: reduce` it only\nfades.\n\nA dialog opened from the drawer opens over it, and the drawer stays open underneath. `ui-dialog` sees the modal\nalready open and draws its own lighter veil, `rgb(0 0 0 / 0.24)`, over the drawer's: there is nothing to set.\nEscape closes the dialog first, then the drawer. Closing or submitting the dialog leaves the drawer open, and focus\ngoes back to the drawer button that opened the dialog: the owner only toggles the dialog's `open`.\n\n```html\n<ui-drawer heading=\"Northwind World\" closeLabel=\"Close the detail\" [open]=\"detail()\" (dismissed)=\"detail.set(false)\">\n ...\n <button ui-button (click)=\"buying.set(true)\">Buy</button>\n</ui-drawer>\n<ui-dialog heading=\"Buy\" [open]=\"buying()\" (dismissed)=\"buying.set(false)\">\n ...\n <button dialogActions ui-button (click)=\"buy()\">Buy 10 shares</button>\n</ui-dialog>\n```\n\nAfter a delete or a sale of everything, the holding is gone: set both `open` to `false`, then move focus to the list.","whenToUse":"* For the detail of a row on a desktop, when the list behind must keep its place.","whenNotToUse":"* On a phone: below `64rem` the detail is a screen of its own. The drawer is not responsive.\n* For a task that needs an answer before anything else: use `ui-dialog`.","accessibility":"* Named by `label` when it is set, otherwise by its heading through `aria-labelledby`; described by\n `description`. Set `label` when the panel's role reads better than the row's name (“Holding detail”): the\n heading stays its visible title. Never set the ARIA attributes by hand.\n* Follows the [W3C dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/) through the native\n element: focus moves into the panel on open and goes back to the opener on close.\n* The cross is named by `closeLabel`, which the consumer translates.","args":[]}]},{"name":"empty","importPath":"@joanroucoux/cairn-ui/empty","declarations":[{"kind":"component","className":"UiEmpty","selector":"ui-empty","description":"Centered empty state: a title, a hint and an action slot, such as a search that matches nothing.","examples":["<ui-empty heading=\"No holding matches “zzz”\" hint=\"Search covers the name and the ISIN.\">\n <button ui-button variant=\"outline\" size=\"md\" type=\"button\">Add a holding</button>\n</ui-empty>"],"inputs":[{"name":"heading","type":"string","required":true,"twoWay":false},{"name":"hint","type":"string","required":false,"default":"''","twoWay":false}],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Feedback/Empty","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/feedback-empty--docs","summary":"A centered empty state: a title, a hint and an action slot.","whenToUse":"* For a list or a search that has nothing to show and can offer the next step: a catalog search that matches nothing, an account with no holding.","whenNotToUse":"* For a block that failed to load or is loading. Use [Async](?path=/docs/feedback-async--docs).","accessibility":"* The title and the hint are plain text, read in order, before the projected action.\n* Project a real `<button>` or `<a>` as the action.","args":[{"name":"heading","description":"The title of the state."},{"name":"hint","description":"The line under the title. Omitted when empty."}]}]},{"name":"external-link","importPath":"@joanroucoux/cairn-ui/external-link","declarations":[{"kind":"component","className":"UiExternalLink","selector":"a[ui-external-link]","description":"Link to another site: the label followed by an external-link icon.","examples":["<a ui-external-link href=\"https://www.contoso.com\" target=\"_blank\" rel=\"noopener\">Page on contoso.com</a>"],"inputs":[],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Navigation/External link","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/navigation-external-link--docs","summary":"A link that leaves the application: its label followed by an external-link icon.","whenToUse":"* For a link to another site, such as the issuer's page for a holding.","whenNotToUse":"* For navigation inside the application. Use a plain link, [Row](?path=/docs/data-display-row--docs) or\n [Nav item](?path=/docs/data-display-nav-item--docs).","accessibility":"* A native `<a>`. Add `target=\"_blank\"` with `rel=\"noopener\"` when it opens a new tab, and name the\n destination in the label (\"Fiche sur amundietf.fr\").\n* The icon is decorative and hidden from assistive technologies.\n* The target is 44 px high on touch and the text height with a mouse, which underlines on hover.","args":[{"name":"label","description":"Projected text: names the destination."}]}]},{"name":"fact","importPath":"@joanroucoux/cairn-ui/fact","declarations":[{"kind":"component","className":"UiFact","selector":"div[ui-fact]","description":"One fact of a `dl[uiFacts]`: a label, its value (the projected content) and an optional sub-line\nunder the value.","examples":["<div ui-fact label=\"Price\" sub=\"24/09 · Yahoo\" subTone=\"stale\">28,64 €</div>"],"inputs":[{"name":"label","type":"string","required":true,"twoWay":false},{"name":"sub","type":"string | undefined","required":false,"twoWay":false},{"name":"subTone","type":"'subtle' | 'stale'","required":false,"default":"'subtle'","twoWay":false},{"name":"size","type":"'md' | 'sm' | 'auto' | 'recap'","required":false,"default":"'md'","twoWay":false}],"outputs":[],"slots":["*"]},{"kind":"directive","className":"UiFacts","selector":"dl[uiFacts]","description":"The list that holds `div[ui-fact]` rows, as a native description list.","examples":["<dl uiFacts>\n <div ui-fact label=\"Quantity\">500</div>\n <div ui-fact label=\"Price\" sub=\"24/09 · Yahoo\">28,64 €</div>\n</dl>"],"inputs":[{"name":"ruled","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[{"name":"FACT_SIZES","type":"readonly string[]","values":["md","sm","auto","recap"],"description":"Available fact row sizes."},{"name":"FACT_SUB_TONES","type":"readonly string[]","values":["subtle","stale"],"description":"Color of the line under a fact value. `stale` flags an out-of-date figure."}],"types":[{"name":"FactSize","type":"'md' | 'sm' | 'auto' | 'recap'"},{"name":"FactSubTone","type":"'subtle' | 'stale'"}],"docs":[{"title":"Data display/Facts","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-facts--docs","summary":"A list of facts about one thing: a label on the left, its value on the right, an optional\nline under the value, and a hairline between rows. Built on a native description list.","whenToUse":"* On a detail screen, for the figures that describe the item: quantity, average cost, price with its\n date and source, identifier with its class.","whenNotToUse":"* For tabular data with several columns. Use the [Table](?path=/docs/data-display-table--docs).\n* For a list of things the user can open. Use [Row](?path=/docs/data-display-row--docs).","accessibility":"* `dl[uiFacts]` holds `div[ui-fact]` children, each exposing a `dt` (the label) and a `dd` (the\n value and its sub-line), which is the markup assistive technologies expect for term and value pairs.\n* The sub-line color (`stale`) is never the only signal: the line says what is out of date.\n* `size=\"auto\"` is `md` below 64rem and `sm` from it, as the Holdings detail does, so one list serves both widths.\n* `size=\"md\"` (12 px rows, body value) is the iPhone detail inside a card; `sm` (10 px rows, label\n value) is the desktop detail card. The card around the iPhone list is page layout.","args":[{"name":"size","description":"`md` is a 12 px row with a body value; `sm` a 10 px row with a label-size value and no hairline between rows; `recap` a 9 px row with a label-size value and a hairline between rows, for the before/after recap of a trade dialog; `auto` is `md` below 64rem and `sm` from it, so one list serves both widths."},{"name":"subTone","description":"Color of the line under a value: `subtle`, or `stale` for an out-of-date figure."},{"name":"ruled","description":"Also draws the hairline above the first row."}]}]},{"name":"field","importPath":"@joanroucoux/cairn-ui/field","declarations":[{"kind":"component","className":"UiField","selector":"ui-field","description":"Label, control, hint and error as one unit. The projected native control is found and wired with\nthe ARIA attributes. The message comes from `error`, which wins, or else from the control's own\nvalidation state once touched.","examples":["<ui-field label=\"Quantity\" hint=\"Leave empty if unknown\">\n <input uiInput type=\"number\" [formField]=\"form.quantity\" />\n</ui-field>","<ui-field label=\"ISIN or name\" [error]=\"blank() ? 'Enter an ISIN' : undefined\">\n <input uiInput type=\"search\" />\n</ui-field>","<ui-field label=\"Search holdings\" labelHidden>\n <input uiInput type=\"search\" placeholder=\"Instrument or account\" />\n</ui-field>"],"inputs":[{"name":"label","type":"string","required":true,"twoWay":false},{"name":"labelHidden","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"hint","type":"string | undefined","required":false,"twoWay":false},{"name":"error","type":"string | undefined","required":false,"twoWay":false},{"name":"unit","type":"string | undefined","required":false,"twoWay":false},{"name":"optional","type":"string | undefined","required":false,"twoWay":false},{"name":"invalid","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":["[uiFieldLeading]","*","[uiFieldTrailing]"]},{"kind":"directive","className":"UiFieldLeading","selector":"[uiFieldLeading]","description":"Marks an element, typically an 18px svg icon, as the leading content of a `ui-field`, inside the\ncontrol's left edge. Decorative, so hidden from assistive technologies.","examples":["<ui-field label=\"Search\" labelHidden>\n <svg uiFieldLeading width=\"18\" height=\"18\" viewBox=\"0 0 24 24\">...</svg>\n <input uiInput type=\"search\" />\n</ui-field>"],"inputs":[],"outputs":[],"slots":[]},{"kind":"directive","className":"UiFieldTrailing","selector":"[uiFieldTrailing]","description":"Marks an element, typically an icon button, as the trailing action of a `ui-field`, on the\ncontrol's right edge. A toggle should carry `aria-pressed`. The slot shares the right edge with\n`unit`: set one or the other, not both.","examples":["<ui-field label=\"Password\">\n <input uiInput type=\"password\" size=\"xl\" />\n <button uiFieldTrailing type=\"button\" aria-label=\"Show the password\" [attr.aria-pressed]=\"shown()\">...</button>\n</ui-field>"],"inputs":[],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Inputs/Field","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-field--docs","summary":"Label, hint and error message wrapped around a projected control as a single unit.\n\nThe control stays a plain native element such as `uiInput` or `uiSelect`. The field finds it in\nits own projected content and wires the ARIA attributes onto it, instead of replacing it with a\ncustom component that would have to reimplement every native forms behavior.\n\n`unit` renders a unit such as `EUR` or `shares` inside the control's right edge. It is decorative\ntext, not part of the control's value.\n\n`uiFieldLeading` marks an element, typically an 18px svg icon, as the control's leading content. It\nsits 12px from the left edge, in `--muted-foreground`, and the control's text is inset by 38px\n(12px padding, 18px icon, 8px gap) to clear it. It is decorative and hidden from assistive technologies.\n\nA field shows a message from one of two places. `error` is the caller's own, and always wins. Where\nnone is set, the field shows the first message among the projected control's `errors`, once that\ncontrol is touched: Angular's `[formField]` fills a `uiInput`, `uiSelect` or `uiTextarea` with its\nvalidation state on its own, so a form field needs no binding here at all.","whenToUse":"* Around every form control the user is expected to fill in.\n* Whenever a control needs a hint or can show a validation error.\n* With `labelHidden`, around a control whose purpose the page already makes plain, such as a\n search box above the list it filters. The label is still what a screen reader announces.","whenNotToUse":"* Around more than one control. One field wires one control.","accessibility":"* The label's `for` points at the control's id, generated when the control has none, so clicking\n the label focuses the control.\n* `labelHidden` hides the label from sight only: it stays in the accessibility tree and keeps\n naming the control. A placeholder is not a substitute for it, it disappears as soon as the user types.\n* `hint` and `error` are joined into the control's `aria-describedby`.\n* A non empty `error` marks the control `aria-invalid` and renders the message with `role=\"alert\"`,\n so a screen reader announces it as soon as it appears.\n* A control outside a field shows no message. It has nowhere to put one.","args":[{"name":"label","description":"The field's label, wired to the control via `for`/`id`."},{"name":"labelHidden","description":"Hides the label from sight while it keeps naming the control for assistive technologies."},{"name":"hint","description":"Optional helper text below the control, joined into `aria-describedby`."},{"name":"error","description":"Optional validation message. When set, also marks the control `aria-invalid` and renders with `role=\"alert\"`."},{"name":"optional","description":"Lighter suffix after the label, such as `(optional)`. It stays part of the control accessible name."},{"name":"invalid","description":"Marks the control `aria-invalid` without any message, for a failure the form reports elsewhere. A message from `error` or the control marks it too."},{"name":"unit","description":"Unit shown inside the control's right edge, such as `EUR` or `shares`."}]}]},{"name":"filter-chips","importPath":"@joanroucoux/cairn-ui/filter-chips","declarations":[{"kind":"component","className":"UiFilterChips","selector":"ui-filter-chips","description":"A row of single-choice filter pills, each with an optional count, that narrows a list in place.\nOn touch it scrolls on one line; with a fine pointer it wraps. An element marked `uiChipsLeading` is\nprojected first, outside the chips' group, followed by a vertical rule.","examples":["<ui-filter-chips ariaLabel=\"Filter by class\" [options]=\"classes()\" [(value)]=\"selectedClass\" />"],"inputs":[{"name":"options","type":"FilterChipOption[]","required":true,"twoWay":false},{"name":"value","type":"string","required":true,"twoWay":true},{"name":"ariaLabel","type":"string","required":true,"twoWay":false}],"outputs":[],"slots":["[uiChipsLeading]"]}],"services":[],"functions":[],"constants":[],"types":[{"name":"FilterChipOption","type":"{\n value: string;\n label: string;\n count?: number | string | null;\n}"}],"docs":[{"title":"Inputs/Filter chips","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-filter-chips--docs","summary":"A row of single-choice filter pills that narrows a list in place, each with an optional\ncount: an asset class, a status. The choice is a `model()`, bound with `[(value)]`. One option usually\nstands for “no filter” (such as “All”). On a touch screen the row stays on one line and scrolls\nsideways, with no visible scrollbar, bleeding to the screen edges through the `--gutter` token; each\npill is 34 px tall inside a 44 px target. With a fine pointer the pills are 32 px and wrap.","whenToUse":"* To filter a list that is already on screen, with the effect applied immediately and the result\n count visible on each option.","whenNotToUse":"* A choice made in a form and applied on submit. Use [Choice chips](?path=/docs/inputs-choice-chips--docs).\n* A view switch between a few panels. Use [Segmented](?path=/docs/inputs-segmented--docs).\n* More than about ten options. Use [Select](?path=/docs/inputs-select--docs).","accessibility":"* The row is a `role=\"group\"` named by `ariaLabel`; every chip is a `<button>` with `aria-pressed`\n telling which one is active, so a screen reader announces \"Pressed\" with the label and the count.\n* Every chip is a tab stop. Enter and Space select it. The scroller never needs a pointer: focusing\n a chip scrolls it into view.\n* Place it in a container padded by `--gutter`: on touch the row bleeds to the container's edges.\n* The focus ring is drawn inside the pill on touch and outside it with a fine pointer.","args":[{"name":"options","description":"Ordered `{ value, label, count? }` list. A `count` of `null` or none is left out."},{"name":"ariaLabel","description":"Accessible name of the group."},{"name":"value","description":"Value of the active chip. Two-way bound via `[(value)]`."}]}]},{"name":"group-header","importPath":"@joanroucoux/cairn-ui/group-header","declarations":[{"kind":"component","className":"UiGroupHeader","selector":"ui-group-header","description":"Header of a card on a phone: the name, its meta underneath, the projected total on the right.\n\nWith `collapsible` the header is a button inside the heading, with a chevron that turns when the\ngroup is folded and a press scale. `expanded` is a model: the page keeps the fold memory and hides\nthe card body itself. `toggleDisabled` keeps the button focusable and announced, but a click\nchanges nothing (a filter holds the group open). `controls` is the id of the body it folds.","examples":["<ui-group-header name=\"Northwind PEA\" meta=\"PEA · 3 holdings\" collapsible [(expanded)]=\"open\">\n <ui-amount [value]=\"total\" />\n</ui-group-header>"],"inputs":[{"name":"name","type":"string","required":true,"twoWay":false},{"name":"meta","type":"string | undefined","required":false,"twoWay":false},{"name":"collapsible","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"expanded","type":"boolean","required":false,"default":"true","twoWay":true},{"name":"toggleDisabled","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"controls","type":"string | undefined","required":false,"twoWay":false}],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Data display/Group header","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-group-header--docs","summary":"The header of a grouped card on a phone: the name, its meta underneath, the total on the right\n(projected, usually a `ui-amount`). It sits on the bottom line of the block, 44px tall at least.\n\nWith `collapsible` it is one button inside the heading (`h2 > button[aria-expanded]`) with a 20px chevron that\nturns to -90 degrees when folded, and a press scale. `expanded` is a `model()`: bind `[(expanded)]`, keep the fold\nmemory in the page and hide the card body yourself, with `hidden` rather than removing it. `toggleDisabled` keeps\nthe button focusable and announced while a click does nothing, for a group a filter holds open. `controls` is the id\nof the body it folds.","whenToUse":"* To head a card of rows on a phone, folded or not, where the table band of a desktop screen would not fit.","whenNotToUse":"* On desktop, where `td[ui-group-cell]` heads a group of table rows.","accessibility":"* The button sits inside the heading, so the heading structure survives. `aria-expanded` carries the state, and\n `aria-disabled` the held-open one, which keeps the button reachable by keyboard.\n* The chevron is decorative and hidden from assistive technology.","args":[{"name":"name","description":"The group name, title weight."},{"name":"meta","description":"The line under the name, muted."},{"name":"collapsible","description":"Makes the header a button that folds the group."},{"name":"expanded","description":"Two-way model: whether the group is open."},{"name":"toggleDisabled","description":"Keeps the button announced but ignores a click."}]}]},{"name":"icon-toggle","importPath":"@joanroucoux/cairn-ui/icon-toggle","declarations":[{"kind":"directive","className":"UiIconToggle","selector":"button[uiIconToggle]","description":"Round icon button that switches something on and off, such as the header eye that hides the amounts.\nThe state lives in `aria-pressed`, which also draws the soft fill. Only the press scale transitions:\nthe fill changes at once.","examples":["<button uiIconToggle aria-label=\"Hide amounts\" [attr.aria-pressed]=\"hidden\"><svg>...</svg></button>"],"inputs":[],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Inputs/Icon toggle","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-icon-toggle--docs","summary":"Round icon button that switches something on and off. It is the header eye that hides the\namounts: the icon changes from an eye to a crossed-out eye and the button takes a soft fill.","whenToUse":"* For an action taken at a precise moment and reversed later, when it must stay one gesture away from every\n screen.","whenNotToUse":"* For a setting that is not toggled every day: use a `ui-switch` in a form.\n* For an action that is not reversible, or that has no on/off state: use a `ui-button size=\"icon\"`.","accessibility":"* The host stays a native `<button>`, with `type=\"button\"` unless one is set.\n* It has no visible text: give it an `aria-label`. The state is `aria-pressed`, which the consumer sets; when the\n label also changes with the state (“Hide” / “Show”), a screen reader reads both.\n* The pressed fill is the `--soft` token, drawn as a background image so a fine-pointer hover (`--glow`) never\n replaces it. The fill changes at once: only the press scale transitions.\n* 44px on a touch screen, 40px from 64rem.","args":[]}]},{"name":"input","importPath":"@joanroucoux/cairn-ui/input","declarations":[{"kind":"directive","className":"UiInput","selector":"input[uiInput]","description":"Styled native text input. The host stays a plain `<input>`, so every forms flavor and native\nattribute keeps working. Declares the `errors` and `touched` inputs Angular's `[formField]` fills,\nso a surrounding `ui-field` can show the message.","examples":["<input uiInput type=\"email\" placeholder=\"you@contoso.com\" />"],"inputs":[{"name":"size","type":"'sm' | 'md' | 'lg' | 'xl'","required":false,"default":"'md'","twoWay":false},{"name":"surface","type":"'background' | 'card'","required":false,"default":"'background'","twoWay":false},{"name":"errors","type":"readonly UiControlError[]","required":false,"default":"[]","twoWay":false},{"name":"touched","type":"boolean","required":false,"default":"false","twoWay":false}],"outputs":[],"slots":[]},{"kind":"directive","className":"UiTextarea","selector":"textarea[uiTextarea]","description":"Styled native textarea. It takes no size: it grows from a minimum height. Declares the `errors`\nand `touched` inputs Angular's `[formField]` fills, so a surrounding `ui-field` can show the message.","examples":["<textarea uiTextarea maxlength=\"280\"></textarea>"],"inputs":[{"name":"errors","type":"readonly UiControlError[]","required":false,"default":"[]","twoWay":false},{"name":"touched","type":"boolean","required":false,"default":"false","twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[{"name":"CONTROL_SIZES","type":"readonly string[]","values":["sm","md","lg","xl"],"description":"Available control sizes."},{"name":"CONTROL_SURFACES","type":"readonly string[]","values":["background","card"]}],"types":[{"name":"ControlSize","type":"'sm' | 'md' | 'lg' | 'xl'"},{"name":"ControlSurface","type":"'background' | 'card'"}],"docs":[{"title":"Inputs/Input","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-input--docs","summary":"Styled native `<input>` and `<textarea>`.\n\nBoth are attributes rather than wrapper components, so the host stays the real element. Every\nflavour of Angular forms, from `ngModel` to Signal Forms, and every native attribute keep working\nwithout any plumbing. `uiTextarea` shares its classes with `uiInput` and takes no size, because a\ntextarea grows from a floor instead of sitting at a fixed height.","whenToUse":"* For free text and numbers the user types, wrapped in a\n [Field](?path=/docs/inputs-field--docs) that labels it.","whenNotToUse":"* For a choice among known options. Use [Select](?path=/docs/inputs-select--docs) for a list, or\n [Segmented](?path=/docs/inputs-segmented--docs) for a handful of mutually exclusive values.","accessibility":"* The control always needs an accessible name, either from the label of a surrounding\n `<ui-field>` or from an explicit `aria-label`.\n* A placeholder is not a label. It disappears as soon as the user types.\n* `size=\"md\"` is the 44px touch target, as it is for every other control in the library.","args":[{"name":"type","description":"Native `type` attribute, forwarded as-is."},{"name":"placeholder","description":"Native placeholder text."},{"name":"disabled","description":"Native `disabled`."},{"name":"size","description":"`md` is the 44px touch target used by default; `sm` is 36px; `lg` is 48px below 64rem, then the touch target (sheet forms); `xl` is 48px, then 44px (sign-in)."}]}]},{"name":"line-chart","importPath":"@joanroucoux/cairn-ui/line-chart","declarations":[{"kind":"component","className":"UiLineChart","selector":"ui-line-chart","description":"Value over time: a monotone curve with a dashed line at the starting value and a tooltip.\n\nAxis labels are never repeated: a tick whose `axisFormat` text equals the previous drawn label is\nnot rendered, so only the last edge label can drop.\n\nA new series animates from the drawn curve. With `rangeKey` bound, only a series that comes with\na new key does: a new series under the same key (a reload) is drawn at once. The key may change\nbefore its series arrives.","examples":["<ui-line-chart [points]=\"points\" label=\"Net worth over one month\" startLabel=\"Since\" [valueFormat]=\"formatEur\" />"],"inputs":[{"name":"points","type":"ChartPoint[]","required":true,"twoWay":false},{"name":"label","type":"string","required":true,"twoWay":false},{"name":"valueFormat","type":"(value: number) => string","required":false,"default":"identityValue","twoWay":false},{"name":"tooltipFormat","type":"((point: ChartPoint) => string) | undefined","required":false,"twoWay":false},{"name":"deltaFormat","type":"(delta: number) => string","required":false,"default":"identityDelta","twoWay":false},{"name":"deltaSuffix","type":"string","required":false,"default":"''","twoWay":false},{"name":"timeFormat","type":"(time: number) => string","required":false,"default":"identityTime","twoWay":false},{"name":"axisFormat","type":"(time: number) => string","required":false,"default":"identityTime","twoWay":false},{"name":"startLabel","type":"string","required":false,"default":"''","twoWay":false},{"name":"timeColumnLabel","type":"string","required":false,"default":"'Time'","twoWay":false},{"name":"valueColumnLabel","type":"string","required":false,"default":"'Value'","twoWay":false},{"name":"sparkline","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"tooltip","type":"'compact' | 'large' | 'auto'","required":false,"default":"'compact'","twoWay":false},{"name":"tooltipDelta","type":"boolean","required":false,"default":"true","attribute":true,"twoWay":false},{"name":"axisGap","type":"number | 'auto'","required":false,"default":"12","twoWay":false},{"name":"axisTicks","type":"3 | 5 | 'auto'","required":false,"default":"'auto'","twoWay":false},{"name":"rangeKey","type":"string | null","required":false,"default":"null","twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[],"types":[{"name":"AxisTicks","type":"3 | 5 | 'auto'"},{"name":"ChartPoint","type":"{\n t: number;\n v: number;\n}"},{"name":"TooltipSize","type":"'compact' | 'large'"}],"docs":[{"title":"Data display/Line chart","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-line-chart--docs","summary":"A single series' value over time: a monotone curve (Fritsch-Carlson, the same interpolation\nas d3's `curveMonotoneX`) that never overshoots the data between two points, a dashed reference\nline at the range's starting value with its “Start” label, a focus/hover tooltip with the change\nsince the start, and a shape transition when `points` changes to a new range.\n\nThe transition runs only for a new `points` array: mounting, the first measured size and any later\nresize draw at once. It lasts `--duration-base` with an ease-out, restarts from the shape on screen\nwhen the range changes again, and moves the end dot along the curve. The “Start” label and the\ndashed line fade out when it starts and back in at their new place when it ends; the tooltip waits\nfor the end. Under `prefers-reduced-motion` the new series shows at once.\n\nBind `rangeKey` (the range's id, `1M`, `1Y`) and only a series that comes with a new key\ninterpolates: the same range reloaded after a buy, or a refresh of its prices, redraws at once with\nno fade, as MOUVEMENT.md animates the curve on a range change only. The key may change before its\nseries arrives; the interpolation runs when the series does. Without `rangeKey`, every new series\ninterpolates.\n\nFormatting and locale are the consumer's business: `valueFormat`, `deltaFormat`, `timeFormat` and\n`axisFormat` all arrive as inputs, so masking amounts is a matter of passing a formatter that\nreturns bullets instead of digits: the curve's shape stays visible either way.\n\nThe host fills its parent's height (`block h-full`); size the chart by sizing its container.","whenToUse":"* Portfolio or instrument value over a selectable range, one series at a time.\n* A short history under a coarse `axisFormat` (years on a 5-year range): a tick label equal to the\n previous drawn one is skipped, so the axis never reads “2026 2026 2026”.","whenNotToUse":"* A compact inline figure with its own axis and tooltip. Pass `sparkline` for the 80 x 24 form\n (curve only, no axis, reference line, dot or tooltip, and out of the tab order).\n* Comparing two or more series at once: the reference line and the tooltip both assume a single\n series.","accessibility":"* The `<svg>` is `role=\"img\"` named by `label`, and focusable: Arrow Left/Right move the focused\n point one at a time, Home/End jump to the first/last point, Escape clears it.\n* Every point is also available as a `sr-only` `<table>`, headed by `timeColumnLabel` and\n `valueColumnLabel`, so the data survives without the SVG.\n* `touch-none` on the plot: a tap shows the tooltip and a drag moves it, while the page still scrolls\n everywhere else. Release or cancel hides it.","args":[{"name":"points","description":"Series of `{ t, v }` points, ordered by time."},{"name":"label","description":"The SVG's accessible name (`role=\"img\"`)."},{"name":"startLabel","description":"Prefix of the reference-line label, e.g. `Start`. Empty hides the label."},{"name":"timeColumnLabel","description":"Header of the screen-reader table's time column."},{"name":"valueColumnLabel","description":"Header of the screen-reader table's value column."},{"name":"tooltip","description":"`compact` (default) sits 12px above the plot with label and caption sizes; `large` is the desktop Dashboard tooltip, at the top of the plot with body and label sizes; `auto` is compact below 64rem and large from 64rem up, with no breakpoint in the consumer."},{"name":"deltaSuffix","description":"Text after the tooltip change, kept when masked."},{"name":"tooltipDelta","description":"Shows the change since the start in the tooltip. Defaults to true."},{"name":"axisGap","description":"Pixels between the plot and the axis row, 12 by default and 16 on the desktop Dashboard, or \"auto\" for 12 below 64rem and 16 from 64rem up. The box is plot + gap + 17."},{"name":"axisTicks","description":"3 draws start, middle and end only (Holdings), 5 draws five labels, auto (default) draws 3 below 40rem and 5 above (Dashboard)."},{"name":"sparkline","description":"Draws the curve alone at 1.5px, with no reference line, dot, axis or tooltip, and out of the tab order. Size it with the container."},{"name":"valueFormat","description":"Formats a point value for the start label, the screen-reader table and, without tooltipFormat, the tooltip."},{"name":"tooltipFormat","description":"Formats a whole point for the tooltip value line, when it reads more than the value (a position value and its unit price). Falls back to valueFormat."},{"name":"rangeKey","description":"Id of the range the series belongs to. When bound, a new series interpolates only if it comes with a new key; under the same key it redraws at once, with no fade. Null (default) interpolates every new series."},{"name":"deltaFormat","description":"Formats the signed change since the first point, for the tooltip."},{"name":"timeFormat","description":"Formats a point's time for the tooltip and the table."},{"name":"axisFormat","description":"Formats a tick time for the axis."}]}]},{"name":"menu","importPath":"@joanroucoux/cairn-ui/menu","declarations":[{"kind":"component","className":"UiMenu","selector":"ui-menu","description":"Secondary actions menu, opened from its trigger through the native Popover API. With `sheet`, below\n64rem it opens as a bottom action sheet instead of beside its trigger, and `heading` names what\nthe actions apply to. `width` fixes the popover width in pixels; without it the menu fits its content.","examples":["<button ui-button size=\"icon\" aria-label=\"More\" [uiMenuTrigger]=\"menu\">...</button>\n<ui-menu #menu label=\"Holding actions\">\n <button uiMenuItem (click)=\"edit()\">Edit</button>\n <button uiMenuItem destructive (click)=\"remove()\">Delete the holding</button>\n</ui-menu>"],"inputs":[{"name":"label","type":"string","required":true,"twoWay":false},{"name":"sheet","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"heading","type":"string | undefined","required":false,"twoWay":false},{"name":"width","type":"number | undefined","required":false,"twoWay":false}],"outputs":[],"slots":["*"]},{"kind":"directive","className":"UiMenuItem","selector":"button[uiMenuItem]","description":"One action of a `ui-menu`; `destructive` paints it in `--negative`. Giving it `checked` makes it a radio\nchoice (`menuitemradio`): the current one carries a check mark and `aria-checked=\"true\"`.","examples":["<button uiMenuItem destructive (click)=\"remove()\">Delete the holding</button>\n<button uiMenuItem [checked]=\"account() === 'pea'\" (click)=\"account.set('pea')\">Northwind PEA</button>"],"inputs":[{"name":"destructive","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"checked","type":"boolean | undefined","required":false,"twoWay":false}],"outputs":[],"slots":[]},{"kind":"directive","className":"UiMenuTrigger","selector":"[uiMenuTrigger]","description":"Opens a `ui-menu` from a trigger element and wires its ARIA attributes.","examples":["<button ui-button size=\"icon\" aria-label=\"More\" [uiMenuTrigger]=\"menu\">...</button>"],"inputs":[{"name":"uiMenuTrigger","type":"UiMenu","required":true,"twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Surfaces/Menu","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/surfaces-menu--docs","summary":"Secondary actions menu, built on the native Popover API: light dismiss (Escape, a click\noutside), stacking and the top layer all come from the platform. It opens below its trigger,\naligned to the trigger's right edge, and flips above when there is no room below.\n\nWith `sheet`, below 64rem it opens instead as a bottom action sheet above the tab bar, with a\nbackdrop, and `heading` names what the actions apply to. `width` fixes the popover width in pixels,\n**outer width, padding included**: the mockup's `width: 210px` and `200px` are content-box, so pass\n`218` and `208`. Without it the menu fits its content.","whenToUse":"* For the secondary actions of a row: edit, delete, duplicate.\n* When there are too many actions to show as buttons, or when one of them is destructive and\n should not sit one accidental tap away from the primary action.","whenNotToUse":"* For a single action. A button says more with less.\n* For navigation between screens. `ui-nav-item` and `ui-tab-bar` cover that.","accessibility":"* `heading` is hidden from assistive technologies, so `label` must name the target as well, for\n example “Actions for Northwind PEA”.\n* The trigger carries `aria-haspopup=\"menu\"`, `aria-expanded` and `aria-controls`.\n* Opening moves focus to the first item; the arrow keys move between items and wrap; Home and End\n jump to the first and the last. Escape and a click outside close the menu, and focus returns to\n the trigger either way.\n* A destructive item is colored `--negative`, never conveyed by color alone: its label already\n says what it does (“Delete the holding”).","args":[{"name":"sheet","description":"Below 64rem, opens as a bottom action sheet above the tab bar, with a backdrop, instead of as a popover beside the trigger. From 64rem it is a popover either way."},{"name":"heading","description":"Title of the sheet naming what the actions apply to (an account name). Shown only with `sheet`, below 64rem, and hidden from assistive technologies: `label` must name the target too."}]}]},{"name":"meter","importPath":"@joanroucoux/cairn-ui/meter","declarations":[{"kind":"component","className":"UiMeter","selector":"ui-meter","description":"Proportion of a whole, drawn as a single bar. One bar per category, never a stacked strip.","examples":["<ui-meter [value]=\"0.463\" label=\"Funds\" valueText=\"46.3% - 128,656.00 EUR\" [tone]=\"1\" />"],"inputs":[{"name":"value","type":"number","required":true,"twoWay":false},{"name":"label","type":"string","required":true,"twoWay":false},{"name":"valueText","type":"string","required":true,"twoWay":false},{"name":"tone","type":"1 | 2 | 3 | 4 | 5 | 6","required":false,"default":"1","twoWay":false},{"name":"size","type":"'md' | 'sm'","required":false,"default":"'md'","twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[{"name":"METER_SIZES","type":"readonly string[]","values":["md","sm"],"description":"Track thicknesses."},{"name":"METER_TONES","type":"readonly number[]","values":[1,2,3,4,5,6],"description":"Ramp step used to paint the fill."}],"types":[{"name":"MeterSize","type":"'md' | 'sm'"},{"name":"MeterTone","type":"1 | 2 | 3 | 4 | 5 | 6"}],"docs":[{"title":"Data display/Meter","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-meter--docs","summary":"Proportion of a whole, drawn as a single bar. In Cairn it carries an allocation\ncategory's share of the portfolio.\n\nAlways one bar per category, never a stacked strip. The palette is monochrome and has no hue to\ntell segments of a strip apart, so length has to do that work alone, and it only reads as a\ncomparison when every bar starts at the same edge.","whenToUse":"* To compare parts of a whole, one bar per part, each with its own label.\n* For a static proportion the user reads. It is a display, not a control.","whenNotToUse":"* For progress toward completion, such as an upload. That is a progress bar, and it carries a\n different meaning.\n* As the only place a figure appears. The bar shows a relationship, not a value.","accessibility":"* Renders `role=\"meter\"` with `aria-valuemin`, `aria-valuemax`, `aria-valuenow` and\n `aria-valuetext`.\n* `valueText` should read as a full phrase carrying both the share and the amount, so a screen\n reader announces exactly what a sighted user reads next to the bar.\n* `tone` is decorative. Neighboring ramp steps separate by at least 1.36:1, which distinguishes\n them without ever identifying a category on its own.","args":[{"name":"amount","description":"Amount shown on the label line, formatted by the caller."},{"name":"share","description":"Share of the total shown under the bar, formatted by the caller."},{"name":"value","description":"Fill proportion, 0 to 1. Also sets `aria-valuenow`."},{"name":"label","description":"Accessible name for the meter, usually the category name."},{"name":"valueText","description":"Phrase announced in place of the raw number, carrying both the share and the amount."},{"name":"size","description":"Track thickness: `md` is 6px (the default), `sm` is 4px for dense lists such as the wrappers."},{"name":"tone","description":"Step of the neutral ramp used to paint the fill, `1` being the most prominent and `6` the quietest."}]}]},{"name":"motion","importPath":"@joanroucoux/cairn-ui/motion","declarations":[{"kind":"directive","className":"UiFlipItem","selector":"[uiFlipItem]","description":"Marks an element as an item of the nearest `uiFlipList`, at any depth. Once one item is marked,\nthe list follows its marked items only.","examples":["<tr uiTr uiFlipItem animate.leave=\"ui-leave-fade\">...</tr>"],"inputs":[],"outputs":[],"slots":[]},{"kind":"directive","className":"UiFlipList","selector":"[uiFlipList]","description":"Slides the items of a list into place when one is removed or inserted (FLIP). Pair it with\n`animate.leave=\"ui-leave-fade\"` on the item so it fades out first. Nothing moves under\n`prefers-reduced-motion`.\n\nThe items are the direct children of the list. Mark them with `uiFlipItem` instead when they sit\ndeeper, so a removal also slides the groups below. Only an item added or removed starts a slide;\na change inside an item does not.","examples":["<ul uiFlipList>"],"inputs":[],"outputs":[],"slots":[]},{"kind":"directive","className":"UiHighlight","selector":"[uiHighlight]","description":"Draws the eye to an element that just changed: scrolls it into view when off screen, flashes the\n`--soft` fill, then fades back. Each new non-null token plays once, so pass a fresh value per\nevent (an object or a counter), not a boolean. A non-null token present at first render plays on\narrival. Can sit on the same element as `animate.enter`: it waits for that fade to finish.\n`highlighted` fires when the flash starts, after any scroll and enter fade, so a toast can follow it.\n\nGive the element `scroll-margin-top` and `scroll-margin-bottom` equal to what is pinned above and\nbelow it (header, tab bar, action bar), or a row under them counts as on screen. For an arrival\non a page opened at the element, scroll to it first (`scrollIntoView` in `afterNextRender`) and\nset the token afterwards, otherwise it scrolls smoothly from where the page is.","examples":["<a ui-row [uiHighlight]=\"changed() === holding.id ? token() : null\" routerLink=\"/holdings/1\">...</a>"],"inputs":[{"name":"uiHighlight","type":"unknown","required":false,"default":"null","twoWay":false}],"outputs":[{"name":"highlighted","type":"void"}],"slots":[]}],"services":[],"functions":[{"name":"injectReducedMotion","signature":"injectReducedMotion(): Signal<boolean>","description":"A signal that follows the user's `prefers-reduced-motion` preference. Call it in an injection\ncontext.","examples":["protected readonly reduced = injectReducedMotion();"]},{"name":"holdTransitionsUntilRendered","signature":"holdTransitionsUntilRendered(): void","description":"Holds the transitions of the host and of everything inside it until two frames after its first\nrender, so bound classes do not animate on page open. Call it in an injection context. A host\nwhose view is never refreshed (a detached change detector) never gets its transitions back.","examples":["constructor() {\n holdTransitionsUntilRendered();\n}"]}],"constants":[],"types":[],"docs":[{"title":"Foundations/Motion classes","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/foundations-motion-classes--docs","summary":"The enter and leave classes shipped in `styles/motion.css`, for Angular's `animate.enter` and\n`animate.leave`. A consumer imports that stylesheet after `theme.css` and writes\n`animate.enter=\"ui-enter-fade\"`; it needs no styling of its own.","whenToUse":"* `ui-enter-fade`: opacity over `--duration-fast`, for a message that appears.\n* `ui-enter-fade-up`: opacity and a 4px rise over `--duration-fast`, for content that arrives in place.\n* `ui-leave-fade`: opacity over `--duration-exit`, for every exit.","whenNotToUse":"* On page open: nothing animates when a screen first renders.\n* For a layout change: only `transform` and `opacity` animate.","accessibility":"* Under `prefers-reduced-motion: reduce` the rise becomes a plain fade, and view transitions are off.","args":[]},{"title":"Foundations/Flip list","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/foundations-flip-list--docs","summary":"`[uiFlipList]` on a list container slides the remaining items into place when one is removed or\ninserted. It measures every item before and after the change (FLIP), from the top of the list itself, then\nanimates the shift in `translateY` over `--duration-base` with `--ease-out`. The list needs no\n`position: relative`, and a list that moved on the page in the meantime slides by the gap only.\n\nThe items are the direct children of the list, unless some are marked `uiFlipItem`: the list then follows\nthe marked items at any depth. Put `uiFlipList` around every group of a page and `uiFlipItem` on each group\nheading and row, so a removal also slides the groups below. An item inside another item (a row inside a card)\nmoves by its own share, the card carrying the rest.","whenToUse":"* On the list of a deletable item: a holding, a passkey. Put `animate.leave=\"ui-leave-fade\"` on the item so it fades out first, then the others move up.\n* On a page made of groups (one `tbody` or card per account): `uiFlipList` on the table or the page column, `uiFlipItem` on the headings and rows.\n* When an item comes back in its place: the ones after it slide down.","whenNotToUse":"* To reorder or filter a long list: only items added or removed start a slide, and only vertically.\n* On a change inside an item (a row that grows): the layout changes at once, nothing slides.","accessibility":"* Under `prefers-reduced-motion: reduce` nothing moves: the list closes up at once.","args":[]},{"title":"Foundations/Highlight","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/foundations-highlight--docs","summary":"`[uiHighlight]=\"token\"` draws the eye to what just changed: it scrolls the element into view when it is\noff screen, holds the `--soft` fill for 200 ms, then fades back to the element's own background over\n`--duration-highlight` with `--ease-out`. It is the one place where `background-color` animates.","whenToUse":"* After Buy, Sell, Edit or Add, on the row that changed.\n* On arrival at a precise place, on the heading of the account the user came for.\n* On `ui-row`, `tr[uiTr]` (it paints the cells), `td[ui-group-cell]` (it paints the band) or any plain element.\n* Give the element `scroll-margin-top` and `scroll-margin-bottom` equal to what is pinned above and below it\n (header, tab bar, action bar): a row hidden under them counts as off screen and is scrolled out.\n* For an arrival, scroll to the element at once first (`scrollIntoView` in `afterNextRender`), then set the\n token: a token present at first render scrolls smoothly from where the page is.\n* On a new row with `animate.enter`, on the same element: the highlight starts once the enter animation has\n finished, so the row fades in, then lights up.\n* To follow the highlight with a toast, listen to `(highlighted)`: it fires once the flash starts, after the scroll\n and the enter animation, and never for a highlight a newer token replaced before it started.","whenNotToUse":"* To signal a price going up or down: nothing flashes green or red.\n* On page open without a cause: a null token does nothing.","accessibility":"* Under `prefers-reduced-motion: reduce` the scroll is instant and the fade lasts 600 ms.\n* The highlight is decoration only: it carries no information a screen reader would miss.","args":[]},{"title":"Foundations/Transitions on mount","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/foundations-transitions-on-mount--docs","summary":"Nothing animates when a screen opens, even when the router lays the page out before its first\nchange detection. Opening a route creates its elements first and binds their classes on the next\ntick; a layout read in between (the router's scroll to the top, a view transition capturing the\npage) styles the elements without their classes, and the classes then arrive as a change. The\ncomponents below hold their transitions (and their descendants') until two frames after their first\nrender, so their fill, color, thumb and switch track are simply there.","whenToUse":"* `holdTransitionsUntilRendered()`, in the constructor of a component or directive of your own whose\n bound classes carry a transition. `styles/motion.css` holds the matching rule.","whenNotToUse":"* In a view whose change detector is detached: the hold ends after its first render, which never comes.\n* For an element that should animate as it appears: use `animate.enter` or `@starting-style`.","accessibility":"* No effect on semantics: it only delays when transitions start to apply.","args":[]}]},{"name":"nav-item","importPath":"@joanroucoux/cairn-ui/nav-item","declarations":[{"kind":"component","className":"UiNavItem","selector":"a[ui-nav-item]","description":"One destination of the sidebar navigation.","examples":["<a ui-nav-item [active]=\"url === '/'\" routerLink=\"/\">\n <lucide-icon navIcon name=\"house\" />\n Portfolio\n</a>"],"inputs":[{"name":"active","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":["[navIcon]","*"]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Data display/Nav item","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-nav-item--docs","summary":"One destination of the sidebar navigation shown from `64rem`: Portfolio, Holdings,\nAllocation, Accounts. `ui-tab-bar` is its counterpart below that width and reuses the same active,\nhover, rest and focus states.\n\nThe icon is projected in `[navIcon]`; cairn-ui carries no icon family of its own, so the app sizes\nand colors it (Lucide, stroke 1.75, following the active state's text color through `currentColor`).","whenToUse":"* For each entry of the sidebar's primary navigation.","whenNotToUse":"* For a secondary action within a page. Use `ui-row` or `ui-menu` instead.","accessibility":"* `active` sets `aria-current=\"page\"`, following the [current page pattern](https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/).\n* Renders on a native `<a>`, so keyboard focus and navigation come from the platform.","args":[{"name":"active","description":"Sets `aria-current=\"page\"` and the soft background, at once (no fade)."},{"name":"label","description":"Projected label."}]}]},{"name":"result-group","importPath":"@joanroucoux/cairn-ui/result-group","declarations":[{"kind":"component","className":"UiResultGroup","selector":"ui-result-group","description":"One source of a search: a two-tone heading, then its rows, or a skeleton while it answers, a\nmessage when it has nothing, or an inline error with a retry.","examples":["<ui-result-group label=\"Yahoo Finance\" hint=\"live prices\" [state]=\"state()\"\n errorMessage=\"Yahoo Finance didn’t respond.\" retryLabel=\"Try again\" (retry)=\"search()\">\n <button ui-row size=\"dense\" type=\"button\">...</button>\n <span resultGroupMessage>No results from Yahoo Finance.</span>\n</ui-result-group>"],"inputs":[{"name":"label","type":"string","required":true,"twoWay":false},{"name":"hint","type":"string | undefined","required":false,"twoWay":false},{"name":"state","type":"'loading' | 'error' | 'empty' | 'ready'","required":false,"default":"'ready'","twoWay":false},{"name":"errorMessage","type":"string | undefined","required":false,"twoWay":false},{"name":"retryLabel","type":"string | undefined","required":false,"twoWay":false}],"outputs":[{"name":"retry","type":"void"}],"slots":[]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Data display/Result group","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-result-group--docs","summary":"One source of a title search: a heading in two tones (the source in 600 `--foreground`, then what its\nprice is in `--subtle-foreground`), and under it the rows the source answered, a two-row skeleton while it\nanswers, a message when it has nothing, or an inline error with a text-only retry.\n\nIt wraps [Async](?path=/docs/feedback-async--docs) in its `inline` variant: the group reloads alone, the other\nsources stay on screen. Project the rows as the default content (`ui-row size=\"dense\"`, an unavailable\nforeign-currency row last) and the empty message with the `resultGroupMessage` attribute.","whenToUse":"* In a search that asks several sources at once, one group per source, inside a bordered list.","whenNotToUse":"* For a single list: use rows directly. For a failed block that is not a search result, use `ui-async`.","accessibility":"* The host is `role=\"group\"` named by its heading.\n* The loading state sets `aria-busy`; the error is `role=\"alert\"` so a failing source is announced.","args":[{"name":"label","description":"The source, in 600 `--foreground`. It names the group."},{"name":"hint","description":"What its price is, in `--subtle-foreground` after the label."},{"name":"state","description":"`ready` shows the projected rows, `loading` the two-row skeleton, `empty` the `resultGroupMessage`, `error` the message and the retry."},{"name":"errorMessage","description":"Sentence shown when the source failed."},{"name":"retryLabel","description":"Text-only retry button label."},{"name":"retry","description":"Emitted when the retry is pressed. Only this group reloads."}]}]},{"name":"row","importPath":"@joanroucoux/cairn-ui/row","declarations":[{"kind":"directive","className":"UiListRow","selector":"[uiListRow]","description":"Non-interactive row of a list, separated from the next by a hairline: a passkey with its delete\nbutton, a device, a setting. Put it on an `li` or a `div`. Use `ui-row` instead when the whole\nrow is a link or an action.","examples":["<ul>\n <li uiListRow>\n <span class=\"flex-1\">MacBook Air</span>\n <button ui-button variant=\"quiet-destructive\" size=\"icon-sm\" aria-label=\"Delete MacBook Air\">...</button>\n </li>\n</ul>"],"inputs":[{"name":"ruled","type":"boolean","required":false,"default":"true","attribute":true,"twoWay":false}],"outputs":[],"slots":[]},{"kind":"component","className":"UiRow","selector":"a[ui-row], button[ui-row]","description":"Clickable row of a list: a holding, an account, a search result. With `trailing=\"chevron\"` it is a\nflush link row: no side padding, a 16 px chevron at the end, the text dimming on hover.","examples":["<a ui-row [selected]=\"holding.id === openId()\" [routerLink]=\"['/holdings', holding.id]\">\n {{ holding.name }}\n</a>"],"inputs":[{"name":"selected","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"busy","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"unavailable","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"size","type":"'md' | 'lg' | 'xl' | 'card' | 'dense'","required":false,"default":"'md'","twoWay":false},{"name":"padding","type":"'md' | 'sm' | 'none'","required":false,"default":"'md'","twoWay":false},{"name":"gap","type":"'default' | 'sm'","required":false,"default":"'default'","twoWay":false},{"name":"trailing","type":"'none' | 'chevron'","required":false,"default":"'none'","twoWay":false}],"outputs":[],"slots":["*"]},{"kind":"directive","className":"UiRowGroup","selector":"[uiRowGroup]","description":"Groups link rows under a hairline, as the \"other ways\" links under a search list. Put it on a `div`\naround `ui-row` with `trailing=\"chevron\"`.","examples":["<div uiRowGroup>\n <button ui-row type=\"button\" trailing=\"chevron\">Enter manually</button>\n</div>"],"inputs":[],"outputs":[],"slots":[]},{"kind":"directive","className":"UiRowItem","selector":"[uiRowItem]","description":"Lays out a `ui-row` with a trailing action beside it, such as a \"more\" icon button.","examples":["<div uiRowItem>\n <a ui-row size=\"xl\" padding=\"sm\" routerLink=\"/accounts/1\">...</a>\n <button ui-button variant=\"quiet\" size=\"icon-sm\" aria-label=\"Account actions\">...</button>\n</div>"],"inputs":[],"outputs":[],"slots":[]},{"kind":"directive","className":"UiRowTile","selector":"[uiRowTile]","description":"The filled tile that leads a row, holding an icon. Put it on a `span` as the first child of a\n`ui-row` or a `uiListRow`.","examples":["<a ui-row size=\"xl\" padding=\"sm\" href=\"/export\">\n <span uiRowTile><svg lucideDownload [size]=\"18\" /></span>\n Export\n</a>"],"inputs":[],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[{"name":"ROW_GAPS","type":"readonly string[]","values":["default","sm"]},{"name":"ROW_PADDINGS","type":"readonly string[]","values":["md","sm","none"]},{"name":"ROW_SIZES","type":"readonly string[]","values":["md","lg","xl","card","dense"]},{"name":"ROW_TRAILINGS","type":"readonly string[]","values":["none","chevron"]}],"types":[{"name":"RowGap","type":"'default' | 'sm'"},{"name":"RowPadding","type":"'md' | 'sm' | 'none'"},{"name":"RowSize","type":"'md' | 'lg' | 'xl' | 'card' | 'dense'"},{"name":"RowTrailing","type":"'none' | 'chevron'"}],"docs":[{"title":"Data display/Row","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-row--docs","summary":"A clickable row for lists: holdings, accounts, search results. It replaces the per-row cards and\ntext \"Edit\" / \"Delete\" buttons the previous version used.\n\nApplies to a native `<a>` for navigation or a `<button>` for an in-place action, never a `<div>`\nwith a click handler, so keyboard and assistive technology support come for free.","whenToUse":"* For every row of a list the user can open: a holding, an account, a search result.","whenNotToUse":"* For a row that is not clickable. A plain row of text does not need this component. A static list with a hairline between rows, such as the passkeys of a profile, takes `uiListRow` on its `li`: 72px (68px from 64rem), 12px gap, a hairline under each row.\n* For a primary page action, such as \"Add a holding\". Use `ui-button`.","accessibility":"* `selected` sets `aria-current=\"true\"`, for the row whose detail is open next to it.\n* `busy` sets `aria-busy` and `aria-disabled`, shows a spinner in the trailing slot and swallows the clicks while the row keeps its focus.\n* `unavailable` sets `aria-disabled=\"true\"`, grays the row to half opacity, shows a not-allowed cursor, drops hover and press, and swallows the clicks. The row stays focusable, so a screen reader finds it and reads why it cannot be picked: put the reason in the row.\n* Follows the native element's own semantics: an `<a>` for navigation, a `<button>` for an action.","args":[{"name":"selected","description":"Sets `aria-current=\"true\"` and the soft background."},{"name":"busy","description":"An action started from the row is in flight: a spinner turns in the trailing slot, the row sets `aria-busy` and `aria-disabled`, swallows clicks and keeps its focus, so it cannot be activated twice."},{"name":"unavailable","description":"Grays the row (half opacity), forbids the cursor, drops hover and press and swallows clicks. Sets `aria-disabled=\"true\"`."},{"name":"size","description":"Minimum height and gap: `md` 56 px, `lg` 60 px (10 px gap), `xl` 72 px on touch and 68 px with a mouse (12 px gap), `card` 68 px (8 px gap, the rows of a card), `dense` 56 px at every width (12 px gap, result lists)."},{"name":"gap","description":"Space between the row children: the size default, or `sm` for 8 px on any size."},{"name":"trailing","description":"`chevron` makes a flush link row: a 16 px chevron in `--subtle-foreground` at the end, no side padding, a minimum height of `--row-min`, and the text turns `--muted-foreground` on hover instead of the glow. `size`, `padding` and `gap` are ignored."},{"name":"padding","description":"Side padding: `md` 10 px, `sm` 8 px, `none` 0."}]}]},{"name":"segmented","importPath":"@joanroucoux/cairn-ui/segmented","declarations":[{"kind":"component","className":"UiSegmented","selector":"ui-segmented","description":"Exclusive choice within a small, known set: a time range, a unit, a mode.","examples":["<ui-segmented [options]=\"ranges\" label=\"Time range\" [(value)]=\"range\" />"],"inputs":[{"name":"options","type":"SegmentedOption[]","required":true,"twoWay":false},{"name":"label","type":"string","required":true,"twoWay":false},{"name":"showLabel","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"value","type":"string","required":true,"twoWay":true},{"name":"size","type":"'md' | 'sm'","required":false,"default":"'md'","twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[{"name":"SEGMENTED_SIZES","type":"readonly string[]","values":["md","sm"]}],"types":[{"name":"SegmentedOption","type":"{\n value: string;\n label: string;\n}"},{"name":"SegmentedSize","type":"'md' | 'sm'"}],"docs":[{"title":"Inputs/Segmented","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-segmented--docs","summary":"Exclusive choice within a small, known set, such as a chart's time range, a unit or a\nmode. The selection is a `model()`, bound with `[(value)]`.","whenToUse":"* When every option fits on one line and all of them are worth showing at once, which in practice\n means about two to six.\n* When picking an option applies it immediately, with no confirmation step.","whenNotToUse":"* For more options than fit, or for a list that grows. Use\n [Select](?path=/docs/inputs-select--docs).\n* For actions rather than a selection. A row of buttons is not a segmented control.\n* For a choice that only takes effect once a form is submitted, where a radio group reads more\n honestly.","accessibility":"* Implements the [ARIA radio group pattern](https://www.w3.org/WAI/ARIA/apg/patterns/radio/), since\n no native element covers this.\n* A roving `tabindex` keeps a single stop in the tab order: the selected option, or the first one\n when nothing matches.\n* Arrow Left, Right, Up and Down move the selection and the focus together, wrapping at both ends.\n* `label` names the group. Without it, a screen reader announces the options with nothing to tie\n them to.","args":[{"name":"label","description":"Accessible name for the group (`aria-label` on the `radiogroup`)."},{"name":"showLabel","description":"Draws `label` above the track (label size, 500, muted, 6px gap) and names the group by it with `aria-labelledby` instead of `aria-label`."},{"name":"value","description":"Currently selected option value. Two-way bound via `[(value)]`."},{"name":"size","description":"Option height: `md` is 40 px on touch and 32 px with a mouse (Dashboard, Profile); `sm` is 40 px, then 30 px with a mouse (the Holdings detail range)."},{"name":"options","description":"Ordered `{ value, label }` list of choices."}]}]},{"name":"select","importPath":"@joanroucoux/cairn-ui/select","declarations":[{"kind":"directive","className":"UiSelect","selector":"select[uiSelect]","description":"Styled native select, drop-down list included where the browser allows it. Declares the `errors`\nand `touched` inputs Angular's `[formField]` fills, so a surrounding `ui-field` can show the message.","examples":["<select uiSelect><option value=\"pea\">PEA</option></select>"],"inputs":[{"name":"size","type":"'sm' | 'md' | 'lg' | 'xl'","required":false,"default":"'md'","twoWay":false},{"name":"errors","type":"readonly UiControlError[]","required":false,"default":"[]","twoWay":false},{"name":"touched","type":"boolean","required":false,"default":"false","twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Inputs/Select","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-select--docs","summary":"Styled native `<select>`, drop-down list included.\n\nIt is never rebuilt as a listbox. Where the browser supports a customizable select, the control\nopts into it, so the list carries the same surface, border, radius and spacing as the rest of the\nlibrary rather than being drawn by the operating system. Where it does not, the platform draws the\nlist as it always has.\n\nThe opt-in stops at `pointer: fine`, since on a phone the native picker is what makes the control\nusable at all.\n\nThe list always opens downwards. Where there is not enough room below it shrinks to the space left\nand scrolls, rather than flipping above the control.","whenToUse":"* To choose one value from a list that is too long to display in full, such as an account or an\n instrument.","whenNotToUse":"* For a handful of options that all deserve to be visible. Use\n [Segmented](?path=/docs/inputs-segmented--docs).\n* For a binary on or off. A checkbox or a switch says it in one interaction instead of two.","accessibility":"* The control needs an accessible name, either from the label of a surrounding `<ui-field>` or\n from an explicit `aria-label`.\n* Keyboard support, including typeahead, arrow keys and Home and End, comes from the platform.\n* A first option with an empty value works as the placeholder, so the unselected state is a real\n value rather than an invisible one.","args":[{"name":"size","description":"`md` is the 44px touch target used by default."},{"name":"disabled","description":"Native `disabled`."}]}]},{"name":"select-pill","importPath":"@joanroucoux/cairn-ui/select-pill","declarations":[{"kind":"component","className":"UiSelectPill","selector":"ui-select-pill","description":"A pill that opens a `ui-menu` of exclusive choices, to head a row of filter chips: outlined at rest with a\nchevron, solid primary when `active`, where a separate 14 px cross replaces the chevron and emits `cleared`.\nIt is 34 px tall inside a 44 px target on touch and 32 px with a fine pointer; its focus ring is an outline\n2 px outside the pill; the cross has a 44 px hit area on touch (36 px with a fine pointer). `contextLabel`, when\ngiven, is read before the label while active so a screen reader hears what the account is. Clearing moves focus\nback to the trigger. `disabled` dims it like a disabled button, opens nothing and draws the chevron even when `active`. The projected text is the pill label and the projected `ui-menu` is what it opens.","examples":["<ui-select-pill uiChipsLeading [active]=\"!!account()\" clearLabel=\"Clear the account\" (cleared)=\"account.set('')\">\n {{ accountName() }}\n <ui-menu label=\"Account\" sheet>\n <button uiMenuItem [checked]=\"!account()\" (click)=\"account.set('')\">All accounts</button>\n </ui-menu>\n</ui-select-pill>"],"inputs":[{"name":"active","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"disabled","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"clearLabel","type":"string","required":true,"twoWay":false},{"name":"contextLabel","type":"string | undefined","required":false,"twoWay":false}],"outputs":[{"name":"cleared","type":"void"}],"slots":["*","ui-menu"]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Inputs/Select pill","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-select-pill--docs","summary":"Compact pill that opens a [Menu](?path=/docs/surfaces-menu--docs) of exclusive choices, to head a\nrow of [Filter chips](?path=/docs/inputs-filter-chips--docs), for a filter with too many values for chips, such as an\naccount. The projected text is the pill label and the projected `ui-menu` is what it opens, as a sheet on a phone\nlike the `…` menus. At rest it is outlined on the card surface with a chevron; once a value other than the \"all\"\nchoice is picked (`[active]=\"true\"`) it is solid primary and a separate 14 px cross replaces the chevron: it\nemits `cleared`, so the list goes back to every value in one tap. It is 34 px tall inside a 44 px target on touch,\n32 px with a fine pointer.","whenToUse":"* To filter a list by one value among several, next to the chips of another filter.","whenNotToUse":"* A choice made in a form. Use [Select](?path=/docs/inputs-select--docs).\n* A handful of values that all deserve to be visible. Use [Filter chips](?path=/docs/inputs-filter-chips--docs).","accessibility":"* The trigger is named by its label, and `aria-haspopup`, `aria-expanded` and `aria-controls` come from the\n menu. Give the menu its own `label`; its choices are `menuitemradio` items, the current one `aria-checked`.\n* The cross is a real button named by `clearLabel`, which the app passes in its own language, and it is a\n tab stop of its own, outside the trigger.\n* The trigger's focus ring sits 2 px outside the pill; the cross's is a 24 px circle around its icon, in the pill's\n text color so it shows on the solid pill. Both are outlines, so they show in forced colors; the pill keeps a\n border there.","args":[{"name":"active","description":"Solid primary pill with a cross in place of the chevron, for a value other than \"all\"."},{"name":"disabled","description":"Dims the pill like a disabled button and opens nothing; the cross is not drawn and the chevron stays, even when `active`. For a list still loading or that failed to load."},{"name":"clearLabel","description":"Accessible name of the cross, in the language of the app. Required."},{"name":"cleared","description":"Emits when the cross is pressed. Only drawn when `active`."}]}]},{"name":"skeleton","importPath":"@joanroucoux/cairn-ui/skeleton","declarations":[{"kind":"component","className":"UiSkeleton","selector":"ui-skeleton","description":"Decorative, pulsing placeholder for content that is still loading.","examples":["<ui-skeleton shape=\"figure\" />\n<ui-skeleton [lines]=\"3\" />"],"inputs":[{"name":"shape","type":"'text' | 'figure' | 'row' | 'chart' | 'ring' | 'dot'","required":false,"default":"'text'","twoWay":false},{"name":"lines","type":"number","required":false,"default":"1","twoWay":false},{"name":"height","type":"number","required":false,"default":"12","twoWay":false},{"name":"size","type":"number","required":false,"default":"232","twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[{"name":"SKELETON_SHAPES","type":"readonly string[]","values":["text","figure","row","chart","ring","dot"],"description":"Available skeleton shapes."}],"types":[{"name":"SkeletonShape","type":"'text' | 'figure' | 'row' | 'chart' | 'ring' | 'dot'"}],"docs":[{"title":"Feedback/Skeleton","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/feedback-skeleton--docs","summary":"Placeholder standing in for content that has not arrived yet, at the shape of the content it\nreplaces: text, a dominant figure, a list row, a chart, a ring, or a legend dot.\n\n`shape=\"text\"` (the default) draws one bar per `lines`; above one line, the last bar is shortened so\nthe block reads as the tail of a paragraph rather than another full width line. `figure` is the 260 by\n40 dominant figure and `row` a list row. `chart` fills its host from a 200px floor, so the\nscreen sizes the box (200 on the phone, 300 and more on the desktop). `ring` takes `size`\n(232 on the phone, 240 on the desktop) with a 30px band. `dot` is the 12px disc of a legend entry. None of them reads `lines` or `height`.","whenToUse":"* While a first load is in flight, sized and positioned like the content it replaces, so nothing\n jumps when the real content lands.","whenNotToUse":"* For a refresh of content already on screen. Replacing what the user is reading with gray bars\n loses their place.\n* For a wait with no known shape. A skeleton promises a layout it then has to deliver.","accessibility":"* The whole block is `aria-hidden`, so a screen reader never announces a row of decorative bars.\n* The announcement is the calling screen's job: it owns the `role=\"status\"` sentence that says\n content is loading. This component only paints.\n* The pulse reads its duration from `--pulse-duration`, which `tokens.css` sets to `0ms` under\n `prefers-reduced-motion: reduce`: it stops there with no branching in this component.","args":[{"name":"shape","description":"The shape of the content being stood in for. `text` is the only one `lines` and `height` affect."},{"name":"lines","description":"Number of bars, `shape=\"text\"` only. Above 1, the last one is narrower."},{"name":"size","description":"Diameter in pixels, `shape=\"ring\"` only. 232 on the phone, 240 on the desktop; the band is always 30px."},{"name":"height","description":"Bar height in pixels, `shape=\"text\"` only. Match the text being stood in for."}]}]},{"name":"stale-link","importPath":"@joanroucoux/cairn-ui/stale-link","declarations":[{"kind":"directive","className":"UiStaleLink","selector":"a[uiStaleLink]","description":"Link in the stale tone, for a caption about data that is late or left out of a total, such as\n\"1 holding without a price, not counted\". `chevron` adds a trailing chevron, `size` is `caption` (the\ndefault) or `label`.","examples":["<a uiStaleLink chevron size=\"label\" routerLink=\"/holdings\">1 stale price</a>"],"inputs":[{"name":"chevron","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"size","type":"'caption' | 'label'","required":false,"default":"'caption'","twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[{"name":"STALE_LINK_SIZES","type":"readonly string[]","values":["caption","label"],"description":"Available stale link sizes."}],"types":[{"name":"StaleLinkSize","type":"'caption' | 'label'"}],"docs":[{"title":"Navigation/Stale link","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/navigation-stale-link--docs","summary":"A link in the stale tone, for a caption about data that is late or left out of a total.","whenToUse":"* For a caption that leads to the holdings concerned: \"1 stale price\", \"1 holding without a price, not counted\".\n* `size=\"label\"` (14/20) where the link sits in a 14px line, as the dashboard total draws \"1 stale price ›\"; the default `caption` (12/17) elsewhere.","whenNotToUse":"* For a caption that leads nowhere. Use the stale tone of [Facts](?path=/docs/data-display-facts--docs) or of the table sub line.\n* For a link that leaves the application. Use [External link](?path=/docs/navigation-external-link--docs).","accessibility":"* A native `<a>`: the caption names the destination. The optional chevron is a decoration drawn by CSS, so it is not read.\n* Underlined on hover, with a 2px ring on keyboard focus.\n* The text keeps its size (12/17, or 14/20 with `size=\"label\"`), but a `::before` pseudo-element extends the hit area to 44px high (40px with a mouse), centered on the text.","args":[{"name":"chevron","description":"Adds a trailing chevron."},{"name":"label","description":"Projected text: names what is late or left out."},{"name":"size","description":"Text size: `caption` 12/17 (default) or `label` 14/20, both at weight 500. The hit area stays 44px high (40px with a mouse)."}]}]},{"name":"stat","importPath":"@joanroucoux/cairn-ui/stat","declarations":[{"kind":"component","className":"UiStat","selector":"ui-stat","description":"Label, figure and a named period of change: a `hero` for a screen's single headline number, or a\nsmaller `tile` repeated in a grid. The figure, its delta and its caption are projected, so the\ncomponent does no formatting. The `hero` figure does not use `tabular-nums`.","examples":["<ui-stat label=\"Net worth\" size=\"hero\">\n <span uiStatValue>128,656 EUR</span>\n <ui-delta uiStatDelta [value]=\"dayChange()\" unknownLabel=\"Not available\">+316.54 EUR</ui-delta>\n <span uiStatCaption>today</span>\n</ui-stat>"],"inputs":[{"name":"label","type":"string","required":true,"twoWay":false},{"name":"size","type":"'hero' | 'tile'","required":false,"default":"'tile'","twoWay":false}],"outputs":[],"slots":["[uiStatValue]","[uiStatDelta]","[uiStatCaption]","[uiStatAside]"]}],"services":[],"functions":[],"constants":[{"name":"STAT_SIZES","type":"readonly string[]","values":["hero","tile"],"description":"Available stat sizes."}],"types":[{"name":"StatSize","type":"'hero' | 'tile'"}],"docs":[{"title":"Data display/Stat","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-stat--docs","summary":"Label, figure and a named period of change, at two sizes: a `hero` for a screen's single\nheadline number, or a smaller `tile` repeated in a grid.\n\nThe figure goes in `[uiStatValue]`, already formatted by the caller. `[uiStatDelta]` takes a\n`ui-delta` carrying the change, `[uiStatCaption]` names the period it covers (such as \"over 1\nmonth\"), and `[uiStatAside]` is an optional slot for a compact visual such as a sparkline. All\nthree are optional; only the value is required.","whenToUse":"* For the single most important figure on a screen, at `size=\"hero\"`.\n* For one figure among several of the same kind, such as one tile per account, at the default\n `size=\"tile\"`.","whenNotToUse":"* For a figure with no period attached and nothing changing over time. A plain value in a\n `ui-card` is enough.","accessibility":"* The label and the figure are a `dt`/`dd` pair inside a `dl`, so the association survives without\n extra ARIA.\n* The `hero` figure is at least 48px with proportional digits, never `tabular-nums`: a lone\n headline number is not a column of aligned figures.","args":[{"name":"value","description":"The formatted figure, projected into the value slot."},{"name":"delta","description":"The formatted, signed change, projected into the delta slot."},{"name":"label","description":"The stat's label, in sentence case and without a trailing colon."},{"name":"size","description":"`hero` for a screen's single headline figure, `tile` for one among several."}]}]},{"name":"switch","importPath":"@joanroucoux/cairn-ui/switch","declarations":[{"kind":"directive","className":"UiSwitch","selector":"input[type=checkbox][uiSwitch]","description":"Native checkbox styled as an iOS-style switch, exposed to assistive technology as `role=\"switch\"`.","examples":["<input type=\"checkbox\" uiSwitch [checked]=\"hidden()\" (change)=\"toggle()\" aria-label=\"Hide amounts\" />"],"inputs":[],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Inputs/Switch","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/inputs-switch--docs","summary":"A native checkbox styled as a track and a sliding knob, exposed to assistive technology as\n`role=\"switch\"`. Being a real checkbox, it works with a plain `[checked]` / `(change)` pair or with\nAngular forms: no custom `ControlValueAccessor` is involved.","whenToUse":"* For a setting that takes effect immediately, with no separate \"Save\" step, such as \"Hide amounts\".","whenNotToUse":"* For a choice that still needs confirming. A switch implies the change already happened.\n* For a choice between more than two options. Use `ui-segmented` instead.","accessibility":"* `role=\"switch\"` overrides the native checkbox role; the checked state stays native, so a screen\n reader announces it the same way it would a real switch.\n* Always give it an accessible name, either `aria-label` or a `<label>` referencing its `id`.\n* Toggles on a click and on Space, both native to the checkbox input.","args":[{"name":"checked","description":"The native checked state."},{"name":"disabled","description":"Dims the switch to opacity 0.4 and blocks interaction."},{"name":"label","description":"Accessible name, wired through `aria-label`."}]}]},{"name":"tab-bar","importPath":"@joanroucoux/cairn-ui/tab-bar","declarations":[{"kind":"component","className":"UiTab","selector":"a[ui-tab]","description":"One destination of `ui-tab-bar`.","examples":["<a ui-tab active routerLink=\"/\"><lucide-icon tabIcon name=\"house\" />Portfolio</a>"],"inputs":[{"name":"active","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":["[tabIcon]","*"]},{"kind":"component","className":"UiTabBar","selector":"nav[ui-tab-bar]","description":"Bottom navigation bar for iPhone. While it is on the page it publishes its height as\n`--tab-bar-height` on the root element, which `ui-action-bar` and `ui-toaster` sit above: use it\nfor anything else that must clear the bar.","examples":["<nav ui-tab-bar>\n <a ui-tab [active]=\"url === '/'\" routerLink=\"/\"><lucide-icon tabIcon name=\"house\" />Portfolio</a>\n</nav>"],"inputs":[],"outputs":[],"slots":["*"]}],"services":[],"functions":[],"constants":[],"types":[],"docs":[{"title":"Data display/Tab bar","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-tab-bar--docs","summary":"Bottom navigation bar shown below `64rem`: 52px of tabs plus `env(safe-area-inset-bottom)`,\nnever a fixed 34px, so it sits above the home indicator on every device. `ui-nav-item` is its\nsidebar counterpart on the desktop layout.\n\nThe component only lays out its tabs; the app positions the bar itself (`fixed` or `sticky`) and\npads the page content by the same height so nothing hides behind it. While it is on the page the bar\npublishes that height as `--tab-bar-height` on the root element, so the app pads with\n`pb-(--tab-bar-height)` instead of restating 52px; `ui-action-bar` and `ui-toaster` sit above it.","whenToUse":"* As the primary navigation on a narrow viewport, mirroring the sidebar's destinations exactly.","whenNotToUse":"* Above `64rem`, where the sidebar (`ui-nav-item`) carries navigation instead.","accessibility":"* The active tab carries `aria-current=\"page\"` and is shown in 600 weight on `--foreground`; an\n inactive tab stays on `--muted-foreground`.\n* Renders on native `<a>` elements inside a `<nav>`, so keyboard focus and navigation come from the\n platform.","args":[]}]},{"name":"table","importPath":"@joanroucoux/cairn-ui/table","declarations":[{"kind":"directive","className":"UiCellSub","selector":"[uiCellSub]","description":"A second line under a cell's main text. `narrow` shows it only under 1024px, for the data held\nback by `secondary` columns.","examples":["<span uiCellSub narrow>10 × 421,26 € · Avg. cost 388,10 €</span>\n<span uiCellSub tone=\"stale\">Price as of 24/09</span>"],"inputs":[{"name":"narrow","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"tone","type":"'subtle' | 'stale' | 'inherit'","required":false,"default":"'subtle'","twoWay":false}],"outputs":[],"slots":[]},{"kind":"directive","className":"UiGroup","selector":"tbody[uiGroup]","description":"Body of one group of rows. `collapsed` hides every `tr[uiTr]` but the group row with `hidden`: the rows\nstay in the DOM, so a `uiFlipList` around the table sees no item added or removed and slides\nnothing. A plain `tr` without `uiTr` is not hidden.","examples":["<tbody uiGroup [collapsed]=\"!open()\">"],"inputs":[{"name":"collapsed","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":[]},{"kind":"component","className":"UiGroupCell","selector":"td[ui-group-cell]","description":"The band that opens a group of rows: name, meta beside it, the projected total on the right.\nSpans the whole row through `colspan`.\n\nWith `collapsible` the band is a button inside the heading, with a chevron that turns when the\ngroup is folded. `expanded` is a model: the page keeps the fold memory and sets `collapsed` on the\n`tbody[uiGroup]`. `toggleDisabled` keeps the button focusable and announced, but a click changes\nnothing (a filter holds the group open). `controls` is the id of the group it folds.","examples":["<tr uiTr group><td ui-group-cell colspan=\"4\" name=\"Brokerage\" meta=\"Savings plan\">...total...</td></tr>\n<tr uiTr group><td ui-group-cell collapsible [(expanded)]=\"open\" controls=\"brokerage\" colspan=\"4\" name=\"Brokerage\">...</td></tr>"],"inputs":[{"name":"name","type":"string","required":true,"twoWay":false},{"name":"meta","type":"string | undefined","required":false,"twoWay":false},{"name":"size","type":"'md' | 'lg'","required":false,"default":"'md'","twoWay":false},{"name":"collapsible","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"expanded","type":"boolean","required":false,"default":"true","twoWay":true},{"name":"toggleDisabled","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"controls","type":"string | undefined","required":false,"twoWay":false}],"outputs":[],"slots":["*"]},{"kind":"directive","className":"UiRowAction","selector":"[uiRowAction]","description":"A control in another cell of a stretched-link row: keeps it clickable above the row-wide link.","examples":["<td uiTd><button uiRowAction type=\"button\">Enter a price</button></td>"],"inputs":[],"outputs":[],"slots":[]},{"kind":"directive","className":"UiRowLink","selector":"a[uiRowLink], button[uiRowLink]","description":"Makes a link or button the way into its row: by default it stretches over the whole row, so the\nrow is one target. With `[stretch]=\"false\"` only the name is the target. `current` marks the open\nholding. Other controls in the row need `uiRowAction` to stay clickable.","examples":["<td uiTd primary><a uiRowLink href=\"/holdings/42\">Ferrari</a></td>\n<td uiTd primary><a uiRowLink [stretch]=\"false\" href=\"/accounts/3\">Brokerage</a></td>"],"inputs":[{"name":"stretch","type":"boolean","required":false,"default":"true","attribute":true,"twoWay":false},{"name":"current","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":[]},{"kind":"directive","className":"UiTable","selector":"table[uiTable]","description":"Styled native table. `row` sets the height of every body row, `rule` draws or drops the hairline\nunder the header, `spaced` leaves a gap between the header and the first row.","examples":["<table uiTable row=\"60\" [rule]=\"false\">"],"inputs":[{"name":"row","type":"'48' | '52' | '60'","required":false,"default":"'48'","twoWay":false},{"name":"rule","type":"boolean","required":false,"default":"true","attribute":true,"twoWay":false},{"name":"spaced","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":[]},{"kind":"directive","className":"UiTd","selector":"td[uiTd]","description":"Body cell. Same inputs as `UiTh` bar `tall` and `width`, which must be set on both to match.","examples":["<td uiTd numeric secondary>12</td>"],"inputs":[{"name":"numeric","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"primary","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"secondary","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"from","type":"'md' | 'lg' | null","required":false,"default":"null","twoWay":false}],"outputs":[],"slots":[]},{"kind":"directive","className":"UiTh","selector":"th[uiTh]","description":"Column header. `numeric` right-aligns, `secondary` (or `from`) hides the column below a\nbreakpoint, `primary` makes it the column that absorbs the free width, `tall` is a 40px header,\n`width` fixes the column width.","examples":["<th uiTh numeric secondary width=\"108px\">Quantity</th>"],"inputs":[{"name":"width","type":"string | null","required":false,"default":"null","twoWay":false},{"name":"numeric","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"primary","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"secondary","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"tall","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"from","type":"'md' | 'lg' | null","required":false,"default":"null","twoWay":false}],"outputs":[],"slots":[]},{"kind":"directive","className":"UiTr","selector":"tr[uiTr]","description":"Body or group row. `selected` takes the soft fill, `interactive` (a row that opens something) also\npresses to it. `group` marks the row that holds a group band: it has no hover state.","examples":["<tr uiTr [selected]=\"open\">...\n<tr uiTr group><td ui-group-cell name=\"Brokerage\">..."],"inputs":[{"name":"group","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"selected","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false},{"name":"interactive","type":"boolean","required":false,"default":"false","attribute":true,"twoWay":false}],"outputs":[],"slots":[]}],"services":[],"functions":[],"constants":[{"name":"CELL_BREAKPOINTS","type":"readonly string[]","values":["md","lg"],"description":"Breakpoints a secondary column can be held back until."},{"name":"GROUP_SIZES","type":"readonly string[]","values":["md","lg"],"description":"Group band sizes."},{"name":"SUB_TONES","type":"readonly string[]","values":["subtle","stale","inherit"],"description":"Tones of a subtitle: subtle (default), `--stale` at 500, or the color of the cell."},{"name":"TABLE_ROWS","type":"readonly string[]","values":["48","52","60"],"description":"Body row heights."}],"types":[{"name":"CellBreakpoint","type":"'md' | 'lg'"},{"name":"GroupSize","type":"'md' | 'lg'"},{"name":"SubTone","type":"'subtle' | 'stale' | 'inherit'"},{"name":"TableRow","type":"'48' | '52' | '60'"}],"docs":[{"title":"Data display/Table","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-table--docs","summary":"Styled native `<table>`. The rows and cells stay plain `<tr>` and `<td>`, with `uiTable`,\n`uiTh`, `uiTd` and `uiTr` applied as attributes. The header caption reads once, centered, in the subtle color.\nTable options: `row` (48, 52 or 60px body rows), `[rule]=\"false\"` (no hairline under the header, as on Holdings)\nand `spaced` (4px between the header and the first row, as on Accounts). A row glows on hover;\n`selected` takes the soft fill.\n\n`numeric` right aligns a column and lines its digits up, on the `<th>` and on every `<td>`. `primary` marks the\none column that absorbs the free width and truncates; give the others a `width` on their header.\n`td[ui-group-cell]` is the muted band that opens a group (`size=\"lg\"` is the 48px band of Holdings); wrap the\ngroup in `<tbody uiGroup>` for its 4px tail. Its total is a `ui-amount`. Its heading takes focus with no ring: `h2[data-group-heading]`.\n\n`a[uiRowLink]` or `button[uiRowLink]` in the first cell makes the whole row one target, with a row-wide ring\n(Holdings). `[stretch]=\"false\"` keeps the target on the name, underlined on hover with its own ring\n(Accounts). `current` sets `aria-current`. A control in another cell takes `uiRowAction`. A row\nthat opens something is `interactive` (it presses to the soft fill); one with no action keeps a plain name,\n`<td uiTd primary class=\"truncate font-medium\">`.\n\nUnder 1024px, mark Quantity, Avg. cost and Price `secondary` on their header and cells: they are hidden, and a\n`<span uiCellSub narrow>` carries them as the row subtitle. `uiCellSub` takes a `tone`: subtle, stale or\ninherit. Amounts in a subtitle go through `ui-amount`.","whenToUse":"* For data with more than one dimension, where a row and a column both mean something.","whenNotToUse":"* For layout. A table announces relationships that a layout does not have.\n* For a single list of values, where a list carries the same content with less structure.","accessibility":"* The semantics come from the real `<table>`, so rows, columns and header association work without any ARIA.\n* `selected` sets `aria-selected` on the row; put `current` on its link so the open holding is announced.\n* A column held back by `secondary` must be held back on its header and its cells together. Its content stays\n available in the subtitle.","args":[]},{"title":"Data display/Table groups","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/data-display-table-groups--docs","summary":"Collapsible group bands of `uiTable`: `td[ui-group-cell] collapsible` is a button inside the heading\n(`h2 > button[aria-expanded]`) with an 18px chevron that turns to -90 degrees in `--duration-fast`. Bind\n`[(expanded)]`, keep the fold memory in the page and set `[collapsed]` on the `tbody[uiGroup]`: its `tr[uiTr]` rows\nget `hidden` and stay in the DOM, so a `uiFlipList` around the table slides nothing. `toggleDisabled` keeps the\nbutton announced (`aria-disabled`) while a click does nothing, for a group a filter holds open; `controls` is the id\nof the group it folds.","whenToUse":"* To let a long list of grouped rows fold group by group.","whenNotToUse":"* To hide a group a filter or a search keeps open: set `toggleDisabled` and keep `expanded` true.","accessibility":"* The button sits inside the heading and carries `aria-expanded`; `aria-controls` points at the group body.\n* Folded rows are `hidden`, so assistive technology skips them.","args":[{"name":"locked","description":"Sets `toggleDisabled` on every band, as a filter holding the groups open does."}]}]},{"name":"toast","importPath":"@joanroucoux/cairn-ui/toast","declarations":[{"kind":"component","className":"UiToaster","selector":"ui-toaster","description":"Message shown by `UiToasts`: one at a time, no action. A confirmation (`show`) is gone after `--toast-duration`;\nan error (`showError`) stays until its cross is clicked. Centered at the bottom of the content area: from `64rem`\nbetween `--sidebar-width` (0 when unset) and the right edge.\nPlace it once in the app shell.","examples":["<ui-toaster />"],"inputs":[],"outputs":[],"slots":[]}],"services":[{"className":"UiToasts","providedIn":"root","description":"One-slot message queue read by `ui-toaster`: a confirmation that times out, or an error that stays until closed.","examples":["inject(UiToasts).show('Purchase recorded');\ninject(UiToasts).showError('Import failed');"],"members":[{"name":"toast","signature":"toast: Signal<Toast | null>"},{"name":"show","signature":"show(message: string): void"},{"name":"showError","signature":"showError(message: string, closeLabel?: string): void"},{"name":"dismiss","signature":"dismiss(): void"}]}],"functions":[],"constants":[],"types":[{"name":"Toast","type":"| { readonly id: number; readonly text: string; readonly kind: 'success' }\n | { readonly id: number; readonly text: string; readonly kind: 'error'; readonly closeLabel: string }"}],"docs":[{"title":"Feedback/Toast","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/feedback-toast--docs","summary":"A short message after an action. `UiToasts.show(message)` puts a confirmation sentence in the single\nslot of the `<ui-toaster />` the app shell renders once; `UiToasts.showError(message)` puts an error there. A new\nmessage replaces the current one in place, whichever kind. It is an inverted surface (`--primary-foreground` on\n`--primary`, no contour) with a check icon. A confirmation fades out after `--toast-duration` (5 s) and the\ntimer stops while the pointer is over it or focus is inside it. An error has an alert icon, no timer, and stays until\nits cross (labeled by the second argument, \"Close\" by default) is clicked.\n\nBelow `64rem` it is centered 8px above `ui-tab-bar` (its `--tab-bar-height`, 52px plus the safe area), or 8px above\n`ui-action-bar` when one is on the page (the bar publishes its height as `--action-bar-height`); from `64rem` it is\ncentered 24px from the bottom between `--sidebar-width` (0 when the app does not set it) and the right edge. It enters\nwith opacity and an 8px rise over `--duration-base`, and leaves with opacity over `--duration-exit`. Under\n`prefers-reduced-motion: reduce` it only fades.\n\nThe toaster is a `popover=\"manual\"` shown in the top layer, and shown again every time a message arrives, so a\nmessage stays above an open `ui-drawer`. A modal that opens while a message is visible does not raise it: show the\ntoast once the dialog has closed, so it never covers the dialog's main button. Elements behind a modal are inert, so\nthe pause and the cross do not work over an open modal.","whenToUse":"* To confirm that a save, a deletion or an import went through.","whenNotToUse":"* For anything that needs an answer: use `ui-alert` or a dialog.\n* For an error a field can show: put it on the field. The error variant is for a failure with no place of its own.\n* To offer an action. A toast carries a sentence only, never a button.","accessibility":"* The `role=\"status\"` region is in the DOM while empty, so a screen reader announces the first message,\n error included.\n* Keep the sentence short: a confirmation disappears on its own. The error starts with the problem.","args":[]}]}],"tokens":[{"name":"--background","group":"color","light":"#f7f8f8","dark":"#0a0b0b","overrides":[],"role":"Page background"},{"name":"--foreground","group":"color","light":"#111414","dark":"#edefee","overrides":[],"role":"Default text on background"},{"name":"--card","group":"color","light":"#ffffff","dark":"#151717","overrides":[],"role":"Card and panel surface"},{"name":"--card-foreground","group":"color","light":"#111414","dark":"#edefee","overrides":[],"role":"Text on card"},{"name":"--elevated","group":"color","light":"#f1f3f2","dark":"#101111","overrides":[],"role":"Raised surface: menu, chart tooltip, elevated card"},{"name":"--border","group":"color","light":"#e2e5e4","dark":"#242726","overrides":[],"role":"Borders and dividers"},{"name":"--hairline","group":"color","light":"#edefee","dark":"#1e2020","overrides":[],"role":"Row separators inside a card"},{"name":"--muted-foreground","group":"color","light":"#5c6462","dark":"#989d9b","overrides":[],"role":"Secondary text"},{"name":"--subtle-foreground","group":"color","light":"#626a68","dark":"#8f9593","overrides":[],"role":"Tertiary text and unknown values"},{"name":"--primary","group":"color","light":"#161918","dark":"#f2f4f3","overrides":[],"role":"Primary actions and active states"},{"name":"--primary-to","group":"color","light":"#333937","dark":"#c9cecc","overrides":[],"role":"Far end of the primary gradient, and the primary button on hover"},{"name":"--primary-foreground","group":"color","light":"#fafbfa","dark":"#0a0b0b","overrides":[],"role":"Text on primary"},{"name":"--soft","group":"color","light":"rgb(22 25 24 / 0.07)","dark":"rgb(242 244 243 / 0.1)","overrides":[],"role":"Primary at low opacity: selected row, active nav item, pressed state, delta pill"},{"name":"--glow","group":"color","light":"rgb(22 25 24 / 0.05)","dark":"rgb(242 244 243 / 0.07)","overrides":[],"role":"Fainter primary tint: hover on rows, menu items and nav items"},{"name":"--ring","group":"color","light":"#161918","dark":"#f2f4f3","overrides":[],"role":"Focus outline"},{"name":"--positive","group":"color","light":"#047757","dark":"#3fbf8f","overrides":[],"role":"Gain"},{"name":"--negative","group":"color","light":"#b5413a","dark":"#e96e63","overrides":[],"role":"Loss and destructive actions"},{"name":"--stale","group":"color","light":"#86620d","dark":"#c4922a","overrides":[],"role":"Quote older than the freshness rule allows"},{"name":"--ramp-1","group":"color","light":"#161918","dark":"#f2f4f3","overrides":[],"role":"Allocation bar, largest share"},{"name":"--ramp-2","group":"color","light":"#3d4341","dark":"#c9cecc","overrides":[]},{"name":"--ramp-3","group":"color","light":"#646b69","dark":"#a2a9a6","overrides":[]},{"name":"--ramp-4","group":"color","light":"#8b9290","dark":"#7c8380","overrides":[]},{"name":"--ramp-5","group":"color","light":"#b0b6b4","dark":"#5a605e","overrides":[]},{"name":"--ramp-6","group":"color","light":"#ced3d1","dark":"#3b403e","overrides":[],"role":"Allocation bar, smallest share"},{"name":"--muted","group":"color","light":"#f1f3f2","dark":"#1e2020","overrides":[],"role":"Neutral fill: avatar, badge, group band, meter track, switch track, skeleton, segmented track"},{"name":"--destructive","group":"color","light":"#b8443c","dark":"#a33a33","overrides":[]},{"name":"--destructive-foreground","group":"color","light":"#fafbfa","dark":"#fafbfa","overrides":[]},{"name":"--font-sans","group":"typography","value":"'Rubik', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif","overrides":[],"utility":"font-sans"},{"name":"--numeric","group":"typography","value":"tabular-nums","overrides":[]},{"name":"--text-caption","group":"typography","value":"0.75rem","overrides":[],"utility":"text-caption","role":"Badges, price dates, table headers, the \"when to use\" table you are reading"},{"name":"--text-caption--line-height","group":"typography","value":"1.0625rem","overrides":[]},{"name":"--text-label","group":"typography","value":"0.875rem","overrides":[],"utility":"text-label","role":"Labels, secondary text, table cells, segmented options, buttons"},{"name":"--text-label--line-height","group":"typography","value":"1.25rem","overrides":[]},{"name":"--text-body","group":"typography","value":"1rem","overrides":[],"utility":"text-body","role":"Rows, body copy, inputs: 16px is the floor iOS does not zoom on focus"},{"name":"--text-body--line-height","group":"typography","value":"1.5rem","overrides":[]},{"name":"--text-title","group":"typography","value":"1.25rem","overrides":[],"utility":"text-title","role":"Card and section titles"},{"name":"--text-title--line-height","group":"typography","value":"1.75rem","overrides":[]},{"name":"--text-heading","group":"typography","value":"1.75rem","overrides":[],"utility":"text-heading","role":"The page title in the header"},{"name":"--text-heading--line-height","group":"typography","value":"2.125rem","overrides":[]},{"name":"--text-display","group":"typography","value":"2.5rem","overrides":[],"utility":"text-display","role":"The one dominant figure on a screen, such as net worth"},{"name":"--text-display--line-height","group":"typography","value":"2.75rem","overrides":[]},{"name":"--tracking-display","group":"typography","value":"-0.02em","overrides":[]},{"name":"--tracking-caption","group":"typography","value":"0.01em","overrides":[]},{"name":"--tracking-label","group":"typography","value":"normal","overrides":[]},{"name":"--gutter","group":"layout","value":"1rem","overrides":[{"media":"(min-width: 64rem)","value":"2rem"}]},{"name":"--inset-card","group":"layout","value":"1rem","overrides":[{"media":"(min-width: 64rem)","value":"1.5rem"}]},{"name":"--stack-group","group":"layout","value":"1rem","overrides":[]},{"name":"--stack-section","group":"layout","value":"1rem","overrides":[{"media":"(min-width: 64rem)","value":"1.5rem"}]},{"name":"--row-min","group":"layout","value":"2.75rem","overrides":[{"media":"(pointer: fine)","value":"2.5rem"}]},{"name":"--header-height","group":"layout","value":"3.5rem","overrides":[{"media":"(min-width: 64rem)","value":"4.5rem"}]},{"name":"--radius-control","group":"radius","value":"0.5rem","overrides":[],"utility":"rounded-control"},{"name":"--radius-container","group":"radius","value":"0.75rem","overrides":[],"utility":"rounded-container"},{"name":"--radius-pill","group":"radius","value":"9999px","overrides":[],"utility":"rounded-pill"},{"name":"--duration-press","group":"motion","value":"120ms","overrides":[]},{"name":"--duration-fast","group":"motion","value":"180ms","overrides":[]},{"name":"--duration-base","group":"motion","value":"260ms","overrides":[{"media":"(prefers-reduced-motion: reduce)","value":"150ms"}]},{"name":"--duration-exit","group":"motion","value":"180ms","overrides":[]},{"name":"--ease-out","group":"motion","value":"cubic-bezier(0.23, 1, 0.32, 1)","overrides":[],"utility":"ease-out"},{"name":"--ease-sheet","group":"motion","value":"cubic-bezier(0.32, 0.72, 0, 1)","overrides":[],"utility":"ease-sheet"},{"name":"--press-scale","group":"motion","value":"0.97","overrides":[{"media":"(prefers-reduced-motion: reduce)","value":"1"}]},{"name":"--enter-scale","group":"motion","value":"0.95","overrides":[{"media":"(prefers-reduced-motion: reduce)","value":"1"}]},{"name":"--pulse-duration","group":"motion","value":"1600ms","overrides":[{"media":"(prefers-reduced-motion: reduce)","value":"0ms"}]},{"name":"--duration-spin","group":"motion","value":"800ms","overrides":[{"media":"(prefers-reduced-motion: reduce)","value":"1600ms"}]},{"name":"--duration-highlight","group":"motion","value":"1200ms","overrides":[{"media":"(prefers-reduced-motion: reduce)","value":"600ms"}]},{"name":"--toast-duration","group":"motion","value":"5000ms","overrides":[]},{"name":"--chevron-down","group":"asset","value":"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%235c6462' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E\")","overrides":[{"media":"(prefers-color-scheme: dark)","value":"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='%23989d9b' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E\")"}]}],"foundations":[{"slug":"overview","title":"Overview","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/foundations-overview--docs","content":"# Cairn UI\n\nCairn UI is the component library behind Cairn, a personal wealth tracker. It ships as a publishable\nAngular package (`projects/ui`) and is developed entirely through this Storybook: there is no demo\napplication, so what you browse here is exactly what a consuming application gets.\n\nThe library is deliberately small. It covers the controls and surfaces Cairn actually uses, and each\none wraps a native element rather than replacing it, so keyboard behavior, forms integration and\nassistive technology support come from the platform instead of being rebuilt.\n\n## Get started\n\n- **Browse the components** in the sidebar. Each one opens on its Docs page: what it is for, when to\n reach for it, its accessibility contract, and a table of every input it accepts.\n- **Try them out.** Every story is live. Change any input under **Controls** in the side panel and\n the story rerenders.\n- **Read the code.** The **Code** panel under a story shows the exact template that produced it.\n- **Switch schemes.** The paintbrush in the toolbar renders any story in light or dark, or leaves it\n on the operating system preference. Themes are explained below.\n\n## Installation\n\n```bash\npnpm add @joanroucoux/cairn-ui\n```\n\nTwo steps are needed beyond installing the package, because it ships CSS that a JavaScript import\ncannot pull in on its own.\n\nImport the token sheet from the application's global stylesheet:\n\n```css\n@import '@joanroucoux/cairn-ui/styles/tokens.css';\n```\n\nComponents are imported one subpath per folder, the package root exports nothing:\n\n```ts\nimport { UiButton } from '@joanroucoux/cairn-ui/button';\n```\n\nThen register the package as a Tailwind source, since Tailwind does not scan `node_modules`:\n\n```css\n@source '../node_modules/@joanroucoux/cairn-ui';\n```\n\n## Themes\n\nEvery token declares both of its values at once with\n[`light-dark()`](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark), so the\nactive scheme is decided by the `color-scheme` in effect rather than by a second set of variables.\n\nAn application that does nothing inherits the operating system preference and is already correct in\nboth schemes. An application that offers its own `system | light | dark` setting forces the two\nexplicit cases by stamping `data-theme` on `<html>`:\n\n| Preference | Attribute on <html> | Scheme in effect |\n| --- | --- | --- |\n| `system` | none | whichever one the operating system asks for |\n| `light` | `data-theme=\"light\"` | light |\n| `dark` | `data-theme=\"dark\"` | dark |\n\nLeaving the attribute off is what makes `system` work, so a preference store must remove it rather\nthan write an empty value or a third theme name. That is the whole contract: one attribute, no\nJavaScript theme object, no class list to keep in sync.\n\nSee [Colors](https://joanroucoux.github.io/cairn-ui/?path=/docs/foundations-colors--docs) for every token this resolves and its measured\ncontrast at both ends.\n\n## Contributing a component\n\nComponents live one per folder in `projects/ui/`, each with its implementation, its spec and\nits stories side by side, and each one an entry point of its own (`ng-package.json` and `index.ts`),\nimported as `@joanroucoux/cairn-ui/<folder>`. `badge`, `button`\nand `input` are the reference implementations to copy the shape from: standalone, zoneless, signal\n`input()`s, and Tailwind utilities that read the tokens instead of hardcoding a color."},{"slug":"colors","title":"Colors","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/foundations-colors--docs","content":"# Colors\n\nThe library ships a single token sheet, `styles/tokens.css` (published as `dist/ui/styles/tokens.css`).\n\nCairn is **monochrome**. The brand carries no hue, which leaves semantic color (a gain, a loss, a stale quote) as the\nonly color in the interface. That is the point of the direction rather than a limitation of it: a green figure on a\nscreen where nothing else is colored is impossible to miss.\n\nEvery token declares both of its values with `light-dark()`. The active scheme follows the OS preference, unless the\nhost application forces one by setting `data-theme=\"light\"` or `data-theme=\"dark\"` on `<html>`.\n\n## Measured contrast\n\nThese ratios were computed, not eyeballed. Every text token clears the WCAG AA threshold of 4.5:1 against every ground it sits on:\n`--background`, `--card`, `--elevated`, `--muted`, and the `--soft` highlight over `--background` or `--card` (selected\nrow, active legend row, delta pill). Each row below shows the worst of them, and `tokens.spec.ts` fails below 4.5:1, so a change to a value shows up here rather than only in a number someone has to trust.\n\n| Token | Role | Light | Dark |\n| --- | --- | --- | --- |\n| `--foreground` | Default text on background | 16.62:1 | 15.58:1 |\n| `--muted-foreground` | Secondary text | 4.97:1 | 5.03:1 |\n| `--subtle-foreground` | Tertiary text and unknown values | 4.54:1 | 4.54:1 |\n| `--positive` | Gain | 4.54:1 | 5.97:1 |\n| `--negative` | Loss and destructive actions | 4.54:1 | 4.53:1 |\n| `--stale` | Quote older than the freshness rule allows | 4.56:1 | 4.94:1 |\n| `--primary-foreground` | Text on primary | 11.37:1 | 12.37:1 |\n\n`--positive`, `--negative` and `--stale` are **darker in light mode than in dark mode**. This is not an\ninconsistency: a mid-tone green that reads at 8.5:1 on a near-black ground only reaches 4.07:1 on white, which\nfails. The role is what stays fixed across schemes, never the hex.\n\n## The neutral ramp\n\n`--ramp-1` through `--ramp-6` paint allocation bars. A categorical palette would contradict the monochrome premise,\nso luminance is the only variable left; adjacent steps separate by at least 1.44:1 in dark and 1.36:1 in light.\nBecause each category gets its own bar rather than a segment in a stacked strip, that separation only has to make\nthe steps distinguishable, never identifiable on its own, since the label sits next to every bar.\n\n| Token | Role |\n| --- | --- |\n| `--background` | Page background |\n| `--foreground` | Default text on background |\n| `--card` | Card and panel surface |\n| `--card-foreground` | Text on card |\n| `--elevated` | Raised surface: menu, chart tooltip, elevated card |\n| `--border` | Borders and dividers |\n| `--hairline` | Row separators inside a card |\n| `--muted` | Neutral fill: avatar, badge, group band, meter track, switch track, skeleton, segmented track |\n| `--muted-foreground` | Secondary text |\n| `--subtle-foreground` | Tertiary text and unknown values |\n| `--primary` | Primary actions and active states |\n| `--primary-to` | Far end of the primary gradient, and the primary button on hover |\n| `--primary-foreground` | Text on primary |\n| `--soft` | Primary at low opacity: selected row, active nav item, pressed state, delta pill |\n| `--glow` | Fainter primary tint: hover on rows, menu items and nav items |\n| `--ring` | Focus outline |\n| `--positive` | Gain |\n| `--negative` | Loss and destructive actions |\n| `--stale` | Quote older than the freshness rule allows |\n| `--ramp-1` | Allocation bar, largest share |\n| `--ramp-6` | Allocation bar, smallest share |\n\n> Both columns render from the same custom properties: `light-dark()` resolves against each swatch's own\n> `color-scheme`, which is how the whole system switches themes with a single attribute."},{"slug":"motion","title":"Motion","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/foundations-motion--docs","content":"# Motion\n\nOnly `transform` and `opacity` animate anywhere in Cairn. Never `ease-in`: something that appears or moves always\neases out, so it settles rather than lunges. Exits are faster than entries. Nothing animates on page entry: the\ndashboard opens several times a day, and a screen that replays its own arrival every time reads as slow.\n\n## Durations\n\n| Token | Value | For |\n| ---------------------- | ------ | ------------------------------------------------------------------- |\n| `--duration-press` | 120ms | Press feedback, on pointer down |\n| `--duration-fast` | 180ms | Menus, the segmented control's thumb |\n| `--duration-base` | 260ms | Dialogs, sheets, the drawer, list-to-detail, a chart's range change |\n| `--duration-exit` | 180ms | Every exit |\n| `--duration-spin` | 800ms | One turn of the button spinner (1600ms under reduced motion) |\n| `--duration-highlight` | 1200ms | A freshly edited row fading back (600ms under reduced motion) |\n| `--toast-duration` | 5000ms | How long a confirmation stays (an error stays until closed) |\n\n## Curves\n\n| Token | Value | For |\n| -------------- | -------------------------------- | ---------------------------------------------------------- |\n| `--ease-out` | `cubic-bezier(0.23, 1, 0.32, 1)` | Anything that appears or moves |\n| `--ease-sheet` | `cubic-bezier(0.32, 0.72, 0, 1)` | Sheets rising from the bottom of the screen, and back down |\n\n## Scales\n\n| Token | Value | For |\n| ------------------ | ------ | ------------------------------------------------------------------------------------ |\n| `--press-scale` | 0.97 | A pressed control, on `:active`: `scale(var(--press-scale))` over `--duration-press` |\n| `--enter-scale` | 0.95 | Dialogs and menus, which start at this scale and opacity 0, never at `scale(0)` |\n| `--pulse-duration` | 1600ms | The skeleton's opacity pulse, from 1 to 0.45 and back, alternating |\n\n## Reduced motion\n\n`tokens.css` neutralizes all three under `prefers-reduced-motion: reduce`, with no component-side branching:\n\n```css\n@media (prefers-reduced-motion: reduce) {\n :root {\n --press-scale: 1; /* a press no longer scales */\n --enter-scale: 1; /* a dialog or menu fades in instead of scaling up */\n --duration-base: 150ms; /* slides and scales become short fades */\n --pulse-duration: 0ms; /* the skeleton stops pulsing */\n --duration-highlight: 600ms;\n --duration-spin: 1600ms;\n }\n}\n```\n\nBecause every animated component reads its scale, duration and curve from these custom properties instead of a\nhardcoded value, asking for reduced motion turns every scale into a no-op and every pulse into a static fill without\na single `@media` query inside a component."},{"slug":"typography","title":"Typography","url":"https://joanroucoux.github.io/cairn-ui/?path=/docs/foundations-typography--docs","content":"# Typography\n\nThe library self-hosts **Rubik**, the only family used for both the logotype and the interface. Import it once\nalongside the tokens:\n\n```css\n@import 'tailwindcss';\n@import '@joanroucoux/cairn-ui/styles/tokens.css';\n@import '@joanroucoux/cairn-ui/styles/theme.css';\n@import '@joanroucoux/cairn-ui/styles/fonts.css';\n```\n\n`theme.css` registers `font-sans`, so `var(--font-sans)` and the Tailwind default font stack both resolve to Rubik\nwith the same system fallbacks.\n\n## Sizes\n\nExactly six sizes, each with its own line height baked in: `text-{size}` sets both. `theme.css` also removes\nTailwind's default scale, so `text-sm`, `text-lg`, `text-xl`... no longer exist. A size a component does not use is a\nsize that cannot be reached.\n\n| Class | Size / line height | Use |\n| --- | --- | --- |\n| `text-caption` | 12 / 17 | Badges, price dates, table headers, the \"when to use\" table you are reading |\n| `text-label` | 14 / 20 | Labels, secondary text, table cells, segmented options, buttons |\n| `text-body` | 16 / 24 | Rows, body copy, inputs: 16px is the floor iOS does not zoom on focus |\n| `text-title` | 20 / 28 | Card and section titles |\n| `text-heading` | 28 / 34 | The page title in the header |\n| `text-display` | 40 / 44 | The one dominant figure on a screen, such as net worth |\n\n`text-heading` and `text-display` also carry a tighter tracking (`--tracking-display`, -0.02em); `text-caption`\nopens up slightly (`--tracking-caption`, 0.01em) to stay legible at 12px; `text-label` sets a normal tracking\n(`--tracking-label`), so `text-caption lg:text-label` needs no `tracking-normal` to drop the caption's.\n\n## Weights\n\nFour weights, each with a fixed role, never chosen for its own sake:\n\n| Weight | Role |\n| ------ | ---------------------------------- |\n| 400 | Body text |\n| 500 | Labels and buttons |\n| 600 | Titles and the one dominant figure |\n| 700 | The logotype, and nothing else |\n\n## Tabular figures\n\n`--numeric` sets `font-variant-numeric: tabular-nums`. Apply it wherever digits sit in a column and have to line up:\ntables, lists, a figure next to its own delta. The one dominant figure on a screen (a `text-display` net worth, a\n`ui-stat` hero) stays proportional instead: it reads better on its own than a column of tabular digits does."}]}
|