Copy row
A read-only value in mono with a copy button, for keys, IDs and URLs.
Connect Okta
Paste these into a new SAML app in Okta, then come back for the metadata URL.
Entity ID
https://app.oration.ai/saml/cedarlineACS URL
https://app.oration.ai/saml/cedarline/acsSign-in link
https://app.oration.ai/sso/cedarline-oktaimport { Button } from "@oration/canon/components/button";import { CopyRow } from "@oration/canon/components/copy-row";import { toast } from "@oration/canon/components/toast";export function Hero() { return ( <div className="flex w-full max-w-md flex-col gap-4 rounded-xl bg-popover p-4 text-left shadow-lg"> <div className="flex flex-col gap-1"> <p className="text-base leading-none font-medium"> Connect Okta </p> <p className="text-sm text-pretty text-muted-foreground"> Paste these into a new SAML app in Okta, then come back for the metadata URL. </p> </div> <CopyRow label="Entity ID" value="https://app.oration.ai/saml/cedarline" /> <CopyRow label="ACS URL" value="https://app.oration.ai/saml/cedarline/acs" /> <CopyRow label="Sign-in link" value="https://app.oration.ai/sso/cedarline-okta" /> <div className="flex justify-end gap-2"> <Button type="button" variant="ghost" onClick={() => toast.add({ title: "Setup saved for later" }) } > Finish later </Button> <Button type="button" onClick={() => toast.add({ title: "Next, add the metadata URL", description: "Okta shows it on the app's Sign On tab.", }) } > Continue </Button> </div> </div> );}Usage#
Copy row shows a read-only machine value with its copy button: a 12px Slate Meta label over a Well Gray well that holds the value in Geist Mono. CopyValue is the well alone, for lists where the name lives elsewhere, such as a key's row. SSO setup and the public keys list use them for entity IDs, ACS URLs and keys. The common mistake is a read-only Input for these values; it looks editable, and people click in and try to type.
When to use
- For values people paste into another system during setup: an entity ID, an ACS URL, a sign-in link, a forwarding address.
- For API keys and other credentials listed by name, with
CopyValuebeside the name. - In a sheet, dialog or settings card, stacked with other rows or fields.
When not to use
- When the value should be editable. Use an input with a label. Use Input
- For an ID inline in a header or a table cell. A bare copy button beside the text is lighter. Use Copy button
- For a multi-line snippet or a code sample. Use Code block
- For a label and value pair that isn't copied, such as a record's details. Use Meta line
The Tint Well Rule
The Machine Mono Rule
Anatomy#
ACS URL
https://app.oration.ai/saml/cedarline/acs- Label.
CopyRowonly: 12px Slate Meta above the well, 4px gap. - Well. Well Gray at 70% with 10px corners, 10px left and 4px right padding.
- Value. 12px Geist Mono in ink, one line, truncated with an ellipsis.
- Copy button. A 28px ghost Copy button. In
CopyRowit is named Copy {label}.
Examples#
The well alone
CopyValue is the well without the label, for lists where the name sits elsewhere. Its label is the button's full name, and className styles the well, here dimmed for a key that's turned off.
- ProductionActive
pk_live_cedarline_4kT9wQ2mZx - StagingActive
pk_test_cedarline_8Rb3vN1pLc - Legacy portalTurned off
pk_live_cedarline_0Ah7sJ5yEd
import { CopyValue } from "@oration/canon/components/copy-row";import { Switch } from "@oration/canon/components/switch";import { toast } from "@oration/canon/components/toast";import { cn } from "@oration/canon/lib/utils";import * as React from "react";export function Values() { const [keys, setKeys] = React.useState([ { id: "prod", name: "Production", key: "pk_live_cedarline_4kT9wQ2mZx", on: true, }, { id: "staging", name: "Staging", key: "pk_test_cedarline_8Rb3vN1pLc", on: true, }, { id: "legacy", name: "Legacy portal", key: "pk_live_cedarline_0Ah7sJ5yEd", on: false, }, ]); return ( <ul className="flex w-full max-w-lg flex-col divide-y divide-border rounded-xl bg-card text-left shadow-border"> {keys.map((key) => ( <li key={key.id} className="flex items-center gap-3 px-4 py-3"> <div className="flex w-28 shrink-0 flex-col"> <span className="text-13 font-medium">{key.name}</span> <span className="text-xs text-muted-foreground"> {key.on ? "Active" : "Turned off"} </span> </div> <CopyValue value={key.key} label={`Copy ${key.name} key`} className={cn( "min-w-0 flex-1", !key.on && "opacity-60", )} /> <Switch aria-label={`${key.name} key active`} checked={key.on} onCheckedChange={(on) => { setKeys((list) => list.map((k) => k.id === key.id ? { ...k, on } : k, ), ); toast.add({ title: `${key.name} key turned ${on ? "on" : "off"}`, }); }} /> </li> ))} </ul> );}Long values
The value stays on one line and truncates with an ellipsis. The button still copies all of it.
Forwarding address
invoices+cedarline-northwind-freight-ap@inbound.oration.aiimport { CopyRow } from "@oration/canon/components/copy-row";export function Truncation() { return ( <div className="flex w-full max-w-[15rem] flex-col gap-3 text-left"> <CopyRow label="Forwarding address" value="invoices+cedarline-northwind-freight-ap@inbound.oration.ai" /> </div> );}In a settings card
Rows stack with 16px between them inside a card, where the well follows the Tint Well Rule.
Email intake
Suppliers email invoices here. Each one becomes a draft for review.
Forwarding address
invoices+cedarline@inbound.oration.aiIntake ID
int_3vX9q2Lmimport { CopyRow } from "@oration/canon/components/copy-row";export function InASheet() { return ( <div className="flex w-full max-w-sm flex-col gap-4 rounded-xl bg-card p-4 text-left shadow-border"> <div className="flex flex-col gap-0.5"> <p className="text-sm font-medium">Email intake</p> <p className="text-13 text-pretty text-muted-foreground"> Suppliers email invoices here. Each one becomes a draft for review. </p> </div> <CopyRow label="Forwarding address" value="invoices+cedarline@inbound.oration.ai" /> <CopyRow label="Intake ID" value="int_3vX9q2Lm" /> </div> );}States#
| State | Treatment |
|---|---|
| Rest | The well, the value and a Slate Meta copy icon. |
| Copy hover and focus | From Copy button: Well Gray fill on hover, the indigo focus ring. |
| Copied | The icon turns into a green check for 1.6 seconds. |
| Truncated | A value wider than the well ends in an ellipsis; the button copies all of it. |
| Dimmed | Pass opacity-60 to CopyValue for a turned-off or revoked credential, and say why beside it. |
Behavior#
- Both are stateless wrappers around Copy button; the full
valuegoes to the clipboard whatever is visible. CopyRow's button is named Copy pluslabel.CopyValueuses itslabelas the whole name, so pass Copy Production key.- The value is plain selectable text, so people can also select and copy part of it.
CopyValuefills its container's width. Size it withclassName, such asmin-w-0 flex-1in a row.
Do and don't#
Entity ID
https://app.oration.ai/saml/cedarlineContent#
- Labels use the other system's term exactly, so people can match fields: Entity ID, ACS URL, not Our ID.
- Values are shown in full, never masked. If the value is a secret people shouldn't see, don't show it in a copy row.
- Say where to paste it in the text around the rows: Paste these into a new SAML app in Okta.
Accessibility#
- The copy button carries the name: Copy Entity ID in
CopyRow, or yourlabelinCopyValue. - The visible label is a
<p>, not a<label>; the value isn't programmatically labelled, so screen reader users hear the value and then the named button. - Truncation is visual only; screen readers read the full value.
- Copied confirmation is a name change on the button. Add a toast through Copy button when confirmation matters.
| Keys | Action |
|---|---|
| Tab | Moves focus to the copy button. |
| Enter | Copies the value. |
| Space | Copies the value. |
Design tokens#
| Token | Used for |
|---|---|
--muted | Well at 70%; copy button hover |
--muted-foreground | Label and copy icon |
--foreground | Value |
--font-mono | Value |
--success | Copied check |
API reference#
CopyRow
A label over a CopyValue.
| Prop | Type | Default | Description |
|---|---|---|---|
labelRequired | string | No default | Shown above the well and used in the button's name. |
valueRequired | string | No default | The value shown and copied. |
CopyValue
The well with the value and the copy button.
| Prop | Type | Default | Description |
|---|---|---|---|
valueRequired | string | No default | The value shown and copied. |
labelRequired | string | No default | The copy button's full accessible name, such as Copy Production key. |
className | string | No default | Merged onto the well. |
Known gaps#
Where the implementation and the system disagree today. Follow the system, not the gap.
The label is a <p> with no relationship to the value, so the value has no programmatic label.
A truncated value can't be read in full anywhere on screen: no tooltip, no wrap option.
CopyRow takes no className, description or id, so it can't carry a hint or be described by an error.
It inherits Copy button's gaps: it confirms even when the clipboard write fails, and the button has no tooltip.