@nadicodeai/ui 12.1.147 → 12.1.149

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.
@@ -20,7 +20,7 @@ export type MessageNoticeProps = {
20
20
  *
21
21
  * @family Feedback
22
22
  * @when "Display text that requires attention or provides additional information." (https://vercel.com/geist/note) "Use a Note for inline contextual feedback next to the field, card, or section it describes: a region change warning above a region picker, a rate-limit notice next to a usage gauge." (https://vercel.com/geist/note) "Choose `variant` by meaning: `error` for a problem the user must fix, `warning` for a consequence to acknowledge, `success` for a passed check, `secondary` for neutral information." (https://vercel.com/geist/note) Ours: a message beside the field, card or section it describes, under the control whose write failed (`action`), or atop the section whose condition lasts (`section`). The `tone` is the `variant`: `destructive` is `error`, `warning` is `warning`, `info` is `success` (a passed check or a state the product manages for the reader, drawn in the accent blue) and `neutral` is `secondary`; there is no violet or cyan.
23
- * @instead "Pick `Banner` when the message is page-level or system-wide and needs a CTA, `Toast` for transient acknowledgments, `Modal` for destructive confirmations." (https://vercel.com/geist/note) Ours: `Banner` is `NoticeBanner`, `Toast` is `showMessageToast` and `Modal` is `ConfirmDialog`. For a field's wrong value use `FieldError`. For a failed read use `EmptyState failed`.
23
+ * @instead "Pick `Banner` when the message is page-level or system-wide and needs a CTA, `Toast` for transient acknowledgments, `Modal` for destructive confirmations." (https://vercel.com/geist/note) Ours: `Banner` is `NoticeBanner`, for a condition of the page's own object, and a system-wide condition is a notification in the product's inbox, never a banner; `Toast` is `showMessageToast` and `Modal` is `ConfirmDialog`. For a field's wrong value use `FieldError`. For a failed read use `EmptyState failed`.
24
24
  * @behaviour "A Note is persistent until the underlying state changes; don’t add an ad hoc dismiss control because it competes with the message." (https://vercel.com/geist/note) "One Note per concept. Stacking three Notes on a card means the page architecture, not the Note copy, is wrong." (https://vercel.com/geist/note) "The optional `NoteAction` holds a single inline CTA. Don’t pair it with a second button." (https://vercel.com/geist/note) Ours: `Alert` is its primitive. Tone `destructive` by default, `warning`, `info` or `neutral`; there is no `success` tone. Colour sits on the line, the text and the mark; the ground stays clear, and the action stays neutral. `section` is the framed note, a line 36 px high with its mark and sentence on one row and the action at its trailing edge (under 768 px the action takes its own row, full width), with or without an action; `action` is the unframed passage, its mark and sentence on one row with the action link inside the sentence, after a space. The title and the body run on as one passage, the title closed by a full stop when it has none of its own, so the two never read as one sentence; the title leads in bold (semibold in a note, medium in a passage) only when a body follows it, and stands at regular weight alone. A message worded for a notice has one action, a small default button in a section and an underlined 14 px link in the passage, and at most a second one after it (Nadia's *Ask Nadia* after *Try again*), an outline button in a section and a second link in the passage; a third is not shown. Long words wrap, nothing truncates. A successful retry removes it. Required: the catalogue `message`, so the sentence and its mark are always drawn.
25
25
  * @words "`NoteLabel` is a 1–2 word Title Case prefix that names the topic: `Region Change`, `Rate Limit`, `Plan Limit`. Cut hedges like `Heads Up`, `FYI`, and `Note`." (https://vercel.com/geist/note) "`NoteContent` is one sentence in active voice that names the impact: `Changing this region restarts all functions.`" (https://vercel.com/geist/note) "No `variant="info"` exists; omit `variant` for the default info icon or use `variant="secondary"` for neutral copy." (https://vercel.com/geist/note) "Single-fragment labels take no period; full descriptive sentences in the body do." (https://vercel.com/geist/note) Ours: words come from the catalogue only. The message's title stands in for `NoteLabel` and `NoteContent`, in sentence case, and `info` exists as Geist's blue `success`. The action is labelled with its result: *Try again*, *Sign in*. When the message carries an action, its body does not repeat it: the link is the step, so the body ends before "Try again".
26
26
  * @access `action`: `role="alert"` (`status` for info and neutral). `section`: `role="status"`, `aria-live="polite"`, atomic. The mark is hidden. Focus stays where the person was.
@@ -1 +1 @@
1
- {"version":3,"file":"message-notice.d.ts","sourceRoot":"","sources":["../../src/components/message-notice.tsx"],"names":[],"mappings":"AAKA,OAAO,EAIL,KAAK,UAAU,IAAI,iBAAiB,EACrC,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAE1E,YAAY,EAAE,iBAAiB,EAAE,CAAC;AAalC;;;;;;;GAOG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,eAAe,CAAC;IACzB,SAAS,EAAE,QAAQ,GAAG,SAAS,CAAC;IAChC,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EACP,SAAS,EACT,IAAoB,EACpB,SAAS,GACV,EAAE,kBAAkB,+BAuEpB"}
1
+ {"version":3,"file":"message-notice.d.ts","sourceRoot":"","sources":["../../src/components/message-notice.tsx"],"names":[],"mappings":"AAKA,OAAO,EAKL,KAAK,UAAU,IAAI,iBAAiB,EACrC,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EAAmB,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAE1E,YAAY,EAAE,iBAAiB,EAAE,CAAC;AAWlC;;;;;;;GAOG;AACH,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,EAAE,eAAe,CAAC;IACzB,SAAS,EAAE,QAAQ,GAAG,SAAS,CAAC;IAChC,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EACP,SAAS,EACT,IAAoB,EACpB,SAAS,GACV,EAAE,kBAAkB,+BAuEpB"}
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { OctagonAlert } from "lucide-react";
4
4
  import { Alert } from "./alert.js";
5
- import { NOTICE_EDGE, NoticeLink, NoticeMark, } from "../internal/notice.js";
5
+ import { closedSentence, NOTICE_EDGE, NoticeLink, NoticeMark, } from "../internal/notice.js";
6
6
  import { cn } from "../lib/utils.js";
7
7
  import { PageCommandButton } from "./page-actions.js";
8
8
  import { bodyBeyondTitle } from "./product-message.js";
@@ -13,13 +13,12 @@ const TONE_TEXT = {
13
13
  destructive: "text-destructive-soft-foreground",
14
14
  };
15
15
  const LEAD = { action: "font-medium", section: "font-semibold" };
16
- const closedSentence = (title) => (/[.!?:…]$/u.test(title) ? title : `${title}.`);
17
16
  /**
18
17
  * A catalogue message about one thing on the screen.
19
18
  *
20
19
  * @family Feedback
21
20
  * @when "Display text that requires attention or provides additional information." (https://vercel.com/geist/note) "Use a Note for inline contextual feedback next to the field, card, or section it describes: a region change warning above a region picker, a rate-limit notice next to a usage gauge." (https://vercel.com/geist/note) "Choose `variant` by meaning: `error` for a problem the user must fix, `warning` for a consequence to acknowledge, `success` for a passed check, `secondary` for neutral information." (https://vercel.com/geist/note) Ours: a message beside the field, card or section it describes, under the control whose write failed (`action`), or atop the section whose condition lasts (`section`). The `tone` is the `variant`: `destructive` is `error`, `warning` is `warning`, `info` is `success` (a passed check or a state the product manages for the reader, drawn in the accent blue) and `neutral` is `secondary`; there is no violet or cyan.
22
- * @instead "Pick `Banner` when the message is page-level or system-wide and needs a CTA, `Toast` for transient acknowledgments, `Modal` for destructive confirmations." (https://vercel.com/geist/note) Ours: `Banner` is `NoticeBanner`, `Toast` is `showMessageToast` and `Modal` is `ConfirmDialog`. For a field's wrong value use `FieldError`. For a failed read use `EmptyState failed`.
21
+ * @instead "Pick `Banner` when the message is page-level or system-wide and needs a CTA, `Toast` for transient acknowledgments, `Modal` for destructive confirmations." (https://vercel.com/geist/note) Ours: `Banner` is `NoticeBanner`, for a condition of the page's own object, and a system-wide condition is a notification in the product's inbox, never a banner; `Toast` is `showMessageToast` and `Modal` is `ConfirmDialog`. For a field's wrong value use `FieldError`. For a failed read use `EmptyState failed`.
23
22
  * @behaviour "A Note is persistent until the underlying state changes; don’t add an ad hoc dismiss control because it competes with the message." (https://vercel.com/geist/note) "One Note per concept. Stacking three Notes on a card means the page architecture, not the Note copy, is wrong." (https://vercel.com/geist/note) "The optional `NoteAction` holds a single inline CTA. Don’t pair it with a second button." (https://vercel.com/geist/note) Ours: `Alert` is its primitive. Tone `destructive` by default, `warning`, `info` or `neutral`; there is no `success` tone. Colour sits on the line, the text and the mark; the ground stays clear, and the action stays neutral. `section` is the framed note, a line 36 px high with its mark and sentence on one row and the action at its trailing edge (under 768 px the action takes its own row, full width), with or without an action; `action` is the unframed passage, its mark and sentence on one row with the action link inside the sentence, after a space. The title and the body run on as one passage, the title closed by a full stop when it has none of its own, so the two never read as one sentence; the title leads in bold (semibold in a note, medium in a passage) only when a body follows it, and stands at regular weight alone. A message worded for a notice has one action, a small default button in a section and an underlined 14 px link in the passage, and at most a second one after it (Nadia's *Ask Nadia* after *Try again*), an outline button in a section and a second link in the passage; a third is not shown. Long words wrap, nothing truncates. A successful retry removes it. Required: the catalogue `message`, so the sentence and its mark are always drawn.
24
23
  * @words "`NoteLabel` is a 1–2 word Title Case prefix that names the topic: `Region Change`, `Rate Limit`, `Plan Limit`. Cut hedges like `Heads Up`, `FYI`, and `Note`." (https://vercel.com/geist/note) "`NoteContent` is one sentence in active voice that names the impact: `Changing this region restarts all functions.`" (https://vercel.com/geist/note) "No `variant="info"` exists; omit `variant` for the default info icon or use `variant="secondary"` for neutral copy." (https://vercel.com/geist/note) "Single-fragment labels take no period; full descriptive sentences in the body do." (https://vercel.com/geist/note) Ours: words come from the catalogue only. The message's title stands in for `NoteLabel` and `NoteContent`, in sentence case, and `info` exists as Geist's blue `success`. The action is labelled with its result: *Try again*, *Sign in*. When the message carries an action, its body does not repeat it: the link is the step, so the body ends before "Try again".
25
24
  * @access `action`: `role="alert"` (`status` for info and neutral). `section`: `role="status"`, `aria-live="polite"`, atomic. The mark is hidden. Focus stays where the person was.
@@ -3,30 +3,33 @@ import type { LucideIcon } from "lucide-react";
3
3
  import { type NoticeTone } from "../internal/notice.js";
4
4
  import type { PageCommand } from "./page-actions.js";
5
5
  export type NoticeBannerTone = NoticeTone;
6
- export type NoticeBannerLiveRegion = "off" | "polite";
6
+ export type NoticeBannerLiveRegion = "off" | "polite" | "assertive";
7
7
  export type NoticeBannerProps = {
8
8
  /** The one fix the page offers, drawn as an underlined link. Required: a condition is shown with its fix. */
9
9
  action: PageCommand;
10
10
  className?: string;
11
- description?: ReactNode;
11
+ /** One more sentence the title leaves out, run on after it; not shown when it repeats the title. */
12
+ description?: string;
13
+ /** What runs under the passage on a row of its own: a progress bar, a failed retry's line. */
12
14
  detail?: ReactNode;
13
15
  /** Replaces the tone's mark with another lucide icon. */
14
16
  icon?: LucideIcon;
15
17
  /** One or two words naming the topic, leading the title: "Billing", "Slack". */
16
18
  label?: string;
17
19
  liveRegion: NoticeBannerLiveRegion;
18
- title: ReactNode;
20
+ /** The one sentence naming the condition, as the product's catalogue words it. */
21
+ title: string;
19
22
  tone: NoticeBannerTone;
20
23
  } & Omit<ComponentProps<"div">, "children" | "title">;
21
24
  /**
22
- * A condition of the whole page, the account or the service, with its fix.
25
+ * A condition of the page's own object, with the fix the page offers.
23
26
  *
24
27
  * @family Feedback
25
- * @when "A prominent message that spans the full width of its container to announce important information." (https://vercel.com/geist/banner) "Used for temporary, project-wide notifications that require resolution" (https://vercel.com/geist/project-banner) "Use Project Banner for project-wide states that need resolution: overdue billing, an active rollback, attack mitigation, an expiring trial blocking deploys." (https://vercel.com/geist/project-banner) "Match `variant` to severity: `error` for critical downtime or payment-blocking states, `warning` for an exceptional state with non-immediate action, `success` for a positive temporary mitigation, `gray` for routine project-wide notices." (https://vercel.com/geist/project-banner) Ours: a condition of the whole page, the account or the service that the page itself can resolve, with that resolving action on the banner. The `tone` is the `variant`: `destructive` is `error`, `warning` is `warning`, `info` is `success` and `neutral` is `gray`. One sits above the page's content, under its title.
26
- * @instead "Pick `Note` for inline contextual messages tied to a single field or card, `Toast` for transient acknowledgments, `Modal` for confirmations." (https://vercel.com/geist/project-banner) Ours: there is no announcement strip. `Note` is `MessageNotice` (`placement="section"`), `Toast` is `showMessageToast` and `Modal` is `ConfirmDialog`. For a measured level use a `KpiCard` with its tone; crossing a threshold is a notification in the inbox, not a banner.
27
- * @behaviour "Project Banner is non-dismissible by design. If the message can be dismissed without resolving the underlying state, it isn’t banner-worthy; move it to a `Note`." (https://vercel.com/geist/project-banner) "Show one Project Banner at a time. Stacking competing banners drowns the most urgent state." (https://vercel.com/geist/project-banner) "Always pass a `callToAction` that resolves the state. A banner with no route is a dead end." (https://vercel.com/geist/project-banner) Ours: a strip as wide as its container: a filled ground with a line on its top and bottom edge only, no corners, 40 px high and centred, with the mark, the sentence and the fix on one row; under 768 px the fix drops under the sentence, aligned with its text. Colour sits on the whole strip (ground, both lines, mark and text); the fix is an underlined link in the darkest step of the same hue (neutral: `text-foreground`) and returns to the strip's text on hover. Neutral and info take the Info mark, warning and destructive the TriangleAlert. `action` is the `callToAction`: one command, never two, never a ready-made button, and there is no close button. Required: `action`, `title`, `tone` and `liveRegion`; the mark is always drawn, and `icon` swaps the tone's mark for another lucide icon.
28
- * @words "`label` is one sentence in sentence case that names the impact: `Your Pro trial expires in 3 days.` Don’t open with `Heads up` or apologetic preambles." (https://vercel.com/geist/project-banner) "`callToAction.label` is Title Case `Verb + Noun` and points at the resolver: `Update Payment Method`, `Reactivate Project`, `Review Tokens`." (https://vercel.com/geist/project-banner) "Name the affected entity when the project context isn’t obvious from the surrounding chrome (`Production deployments are paused on my-project`)." (https://vercel.com/geist/project-banner) "Don’t encode severity in the copy with emoji or interjections; the variant carries that signal." (https://vercel.com/geist/project-banner) "`NoteLabel` is a 1–2 word Title Case prefix that names the topic: `Region Change`, `Rate Limit`, `Plan Limit`. Cut hedges like `Heads Up`, `FYI`, and `Note`." (https://vercel.com/geist/note) Ours: the `title` is the `label`: the one sentence, naming the condition, its impact and the object. A `description` only adds what the title leaves out, in one more sentence. Our `label` is the `NoteLabel` topic prefix. The action is sentence case, verb and noun, pointing at the resolver: "Open Slack settings", "Update card". No "Please". The words come from the product's catalogue code.
29
- * @access `liveRegion="polite"` gives `role="status"`, announced once when it appears; `"off"` gives `role="note"` for one already there at load. The mark, given or not, is hidden.
28
+ * @when "A prominent message that spans the full width of its container to announce important information." (https://vercel.com/geist/banner) "Used for temporary, project-wide notifications that require resolution" (https://vercel.com/geist/project-banner) "Use Project Banner for project-wide states that need resolution: overdue billing, an active rollback, attack mitigation, an expiring trial blocking deploys." (https://vercel.com/geist/project-banner) "Match `variant` to severity: `error` for critical downtime or payment-blocking states, `warning` for an exceptional state with non-immediate action, `success` for a positive temporary mitigation, `gray` for routine project-wide notices." (https://vercel.com/geist/project-banner) Ours: a condition of the page's own object that the page itself resolves, with that resolving action on the banner: a past-due subscription on its billing page, a failed refresh of the page's own reading. The `tone` is the `variant`: `destructive` is `error`, `warning` is `warning`, `info` is `success` and `neutral` is `gray`. One sits above the page's content, under its title.
29
+ * @instead "Pick `Note` for inline contextual messages tied to a single field or card, `Toast` for transient acknowledgments, `Modal` for confirmations." (https://vercel.com/geist/project-banner) Ours: there is no announcement strip. `Note` is `MessageNotice` (`placement="section"`), `Toast` is `showMessageToast` and `Modal` is `ConfirmDialog`. For a measured level use a `KpiCard` with its tone; crossing a threshold is a notification in the inbox, not a banner. A condition of the system, the setup or the staff (a connection, a stored secret, a failing job, anything a staff member fixes in settings) is a notification in the product's inbox, never a banner on a page: "The Vercel dashboard displays a popover, which contains all relevant notifications" (https://vercel.com/docs/notifications), and the Portal's System health raises it through Knock to the bell at the sidebar's foot.
30
+ * @behaviour "Project Banner is non-dismissible by design. If the message can be dismissed without resolving the underlying state, it isn’t banner-worthy; move it to a `Note`." (https://vercel.com/geist/project-banner) "Show one Project Banner at a time. Stacking competing banners drowns the most urgent state." (https://vercel.com/geist/project-banner) "Always pass a `callToAction` that resolves the state. A banner with no route is a dead end." (https://vercel.com/geist/project-banner) Ours: a strip as wide as its container: a filled ground with a line on its top and bottom edge only, no corners, 40 px high and centred, with the mark, the sentence and the fix on one row; under 768 px the fix drops under the sentence, aligned with its text. The title and the description run on as one passage: the title closed by a full stop when it has none of its own, leading in semibold when a description follows and at regular weight alone, the description after it on the same line, and a description that repeats the title not shown. `detail` takes its own row under the passage. Colour sits on the whole strip (ground, both lines, mark and text); the fix is an underlined link in the darkest step of the same hue (neutral: `text-foreground`) and returns to the strip's text on hover. Neutral and info take the Info mark, warning and destructive the TriangleAlert. `action` is the `callToAction`: one command, never two, never a ready-made button, and there is no close button. Required: `action`, `title`, `tone` and `liveRegion`; the mark is always drawn, and `icon` swaps the tone's mark for another lucide icon.
31
+ * @words "`label` is one sentence in sentence case that names the impact: `Your Pro trial expires in 3 days.` Don’t open with `Heads up` or apologetic preambles." (https://vercel.com/geist/project-banner) "`callToAction.label` is Title Case `Verb + Noun` and points at the resolver: `Update Payment Method`, `Reactivate Project`, `Review Tokens`." (https://vercel.com/geist/project-banner) "Name the affected entity when the project context isn’t obvious from the surrounding chrome (`Production deployments are paused on my-project`)." (https://vercel.com/geist/project-banner) "Don’t encode severity in the copy with emoji or interjections; the variant carries that signal." (https://vercel.com/geist/project-banner) "`NoteLabel` is a 1–2 word Title Case prefix that names the topic: `Region Change`, `Rate Limit`, `Plan Limit`. Cut hedges like `Heads Up`, `FYI`, and `Note`." (https://vercel.com/geist/note) Ours: the `title` is the `label`: the one sentence, naming the condition, its impact and the object. A `description` only adds what the title leaves out, in one more sentence. Our `label` is the `NoteLabel` topic prefix. The action is sentence case, verb and noun, pointing at the resolver: "Update payment method", "Try again". No "Please". The words come from the product's catalogue code.
32
+ * @access `liveRegion="assertive"` gives `role="alert"`, for a failure the person just caused; `"polite"` gives `role="status"`, announced once when it appears; `"off"` gives `role="note"` for one already there at load. The mark, given or not, is hidden.
30
33
  */
31
34
  export declare function NoticeBanner({ action, className, description, detail, icon, label, liveRegion, title, tone, ...props }: NoticeBannerProps): import("react").JSX.Element;
32
35
  //# sourceMappingURL=notice-banner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"notice-banner.d.ts","sourceRoot":"","sources":["../../src/components/notice-banner.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG/C,OAAO,EAAuC,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEzF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD,MAAM,MAAM,gBAAgB,GAAG,UAAU,CAAC;AAC1C,MAAM,MAAM,sBAAsB,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEtD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,6GAA6G;IAC7G,MAAM,EAAE,WAAW,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,yDAAyD;IACzD,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,sBAAsB,CAAC;IACnC,KAAK,EAAE,SAAS,CAAC;IACjB,IAAI,EAAE,gBAAgB,CAAC;CACxB,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,CAAC;AAGtD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EACN,SAAS,EACT,WAAW,EACX,MAAM,EACN,IAAI,EACJ,KAAK,EACL,UAAU,EACV,KAAK,EACL,IAAI,EACJ,GAAG,KAAK,EACT,EAAE,iBAAiB,+BA4CnB"}
1
+ {"version":3,"file":"notice-banner.d.ts","sourceRoot":"","sources":["../../src/components/notice-banner.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG/C,OAAO,EAKL,KAAK,UAAU,EAChB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGlD,MAAM,MAAM,gBAAgB,GAAG,UAAU,CAAC;AAC1C,MAAM,MAAM,sBAAsB,GAAG,KAAK,GAAG,QAAQ,GAAG,WAAW,CAAC;AAEpE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,6GAA6G;IAC7G,MAAM,EAAE,WAAW,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oGAAoG;IACpG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8FAA8F;IAC9F,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,yDAAyD;IACzD,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,sBAAsB,CAAC;IACnC,kFAAkF;IAClF,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,gBAAgB,CAAC;CACxB,GAAG,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,CAAC;AAKtD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,EAC3B,MAAM,EACN,SAAS,EACT,WAAW,EACX,MAAM,EACN,IAAI,EACJ,KAAK,EACL,UAAU,EACV,KAAK,EACL,IAAI,EACJ,GAAG,KAAK,EACT,EAAE,iBAAiB,+BAkDnB"}
@@ -1,19 +1,22 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Alert, AlertDescription, AlertTitle } from "./alert.js";
3
- import { NOTICE_EDGE, NoticeLink, NoticeMark } from "../internal/notice.js";
3
+ import { closedSentence, NOTICE_EDGE, NoticeLink, NoticeMark, } from "../internal/notice.js";
4
4
  import { cn } from "../lib/utils.js";
5
+ import { bodyBeyondTitle } from "./product-message.js";
6
+ const LIVE_REGION_ROLE = { assertive: "alert", polite: "status", off: "note" };
5
7
  // eslint-disable-next-line no-warning-comments -- the block quotes Geist's banner rule word for word (https://vercel.com/geist/banner); "temporary" there is prose, not a marker
6
8
  /**
7
- * A condition of the whole page, the account or the service, with its fix.
9
+ * A condition of the page's own object, with the fix the page offers.
8
10
  *
9
11
  * @family Feedback
10
- * @when "A prominent message that spans the full width of its container to announce important information." (https://vercel.com/geist/banner) "Used for temporary, project-wide notifications that require resolution" (https://vercel.com/geist/project-banner) "Use Project Banner for project-wide states that need resolution: overdue billing, an active rollback, attack mitigation, an expiring trial blocking deploys." (https://vercel.com/geist/project-banner) "Match `variant` to severity: `error` for critical downtime or payment-blocking states, `warning` for an exceptional state with non-immediate action, `success` for a positive temporary mitigation, `gray` for routine project-wide notices." (https://vercel.com/geist/project-banner) Ours: a condition of the whole page, the account or the service that the page itself can resolve, with that resolving action on the banner. The `tone` is the `variant`: `destructive` is `error`, `warning` is `warning`, `info` is `success` and `neutral` is `gray`. One sits above the page's content, under its title.
11
- * @instead "Pick `Note` for inline contextual messages tied to a single field or card, `Toast` for transient acknowledgments, `Modal` for confirmations." (https://vercel.com/geist/project-banner) Ours: there is no announcement strip. `Note` is `MessageNotice` (`placement="section"`), `Toast` is `showMessageToast` and `Modal` is `ConfirmDialog`. For a measured level use a `KpiCard` with its tone; crossing a threshold is a notification in the inbox, not a banner.
12
- * @behaviour "Project Banner is non-dismissible by design. If the message can be dismissed without resolving the underlying state, it isn’t banner-worthy; move it to a `Note`." (https://vercel.com/geist/project-banner) "Show one Project Banner at a time. Stacking competing banners drowns the most urgent state." (https://vercel.com/geist/project-banner) "Always pass a `callToAction` that resolves the state. A banner with no route is a dead end." (https://vercel.com/geist/project-banner) Ours: a strip as wide as its container: a filled ground with a line on its top and bottom edge only, no corners, 40 px high and centred, with the mark, the sentence and the fix on one row; under 768 px the fix drops under the sentence, aligned with its text. Colour sits on the whole strip (ground, both lines, mark and text); the fix is an underlined link in the darkest step of the same hue (neutral: `text-foreground`) and returns to the strip's text on hover. Neutral and info take the Info mark, warning and destructive the TriangleAlert. `action` is the `callToAction`: one command, never two, never a ready-made button, and there is no close button. Required: `action`, `title`, `tone` and `liveRegion`; the mark is always drawn, and `icon` swaps the tone's mark for another lucide icon.
13
- * @words "`label` is one sentence in sentence case that names the impact: `Your Pro trial expires in 3 days.` Don’t open with `Heads up` or apologetic preambles." (https://vercel.com/geist/project-banner) "`callToAction.label` is Title Case `Verb + Noun` and points at the resolver: `Update Payment Method`, `Reactivate Project`, `Review Tokens`." (https://vercel.com/geist/project-banner) "Name the affected entity when the project context isn’t obvious from the surrounding chrome (`Production deployments are paused on my-project`)." (https://vercel.com/geist/project-banner) "Don’t encode severity in the copy with emoji or interjections; the variant carries that signal." (https://vercel.com/geist/project-banner) "`NoteLabel` is a 1–2 word Title Case prefix that names the topic: `Region Change`, `Rate Limit`, `Plan Limit`. Cut hedges like `Heads Up`, `FYI`, and `Note`." (https://vercel.com/geist/note) Ours: the `title` is the `label`: the one sentence, naming the condition, its impact and the object. A `description` only adds what the title leaves out, in one more sentence. Our `label` is the `NoteLabel` topic prefix. The action is sentence case, verb and noun, pointing at the resolver: "Open Slack settings", "Update card". No "Please". The words come from the product's catalogue code.
14
- * @access `liveRegion="polite"` gives `role="status"`, announced once when it appears; `"off"` gives `role="note"` for one already there at load. The mark, given or not, is hidden.
12
+ * @when "A prominent message that spans the full width of its container to announce important information." (https://vercel.com/geist/banner) "Used for temporary, project-wide notifications that require resolution" (https://vercel.com/geist/project-banner) "Use Project Banner for project-wide states that need resolution: overdue billing, an active rollback, attack mitigation, an expiring trial blocking deploys." (https://vercel.com/geist/project-banner) "Match `variant` to severity: `error` for critical downtime or payment-blocking states, `warning` for an exceptional state with non-immediate action, `success` for a positive temporary mitigation, `gray` for routine project-wide notices." (https://vercel.com/geist/project-banner) Ours: a condition of the page's own object that the page itself resolves, with that resolving action on the banner: a past-due subscription on its billing page, a failed refresh of the page's own reading. The `tone` is the `variant`: `destructive` is `error`, `warning` is `warning`, `info` is `success` and `neutral` is `gray`. One sits above the page's content, under its title.
13
+ * @instead "Pick `Note` for inline contextual messages tied to a single field or card, `Toast` for transient acknowledgments, `Modal` for confirmations." (https://vercel.com/geist/project-banner) Ours: there is no announcement strip. `Note` is `MessageNotice` (`placement="section"`), `Toast` is `showMessageToast` and `Modal` is `ConfirmDialog`. For a measured level use a `KpiCard` with its tone; crossing a threshold is a notification in the inbox, not a banner. A condition of the system, the setup or the staff (a connection, a stored secret, a failing job, anything a staff member fixes in settings) is a notification in the product's inbox, never a banner on a page: "The Vercel dashboard displays a popover, which contains all relevant notifications" (https://vercel.com/docs/notifications), and the Portal's System health raises it through Knock to the bell at the sidebar's foot.
14
+ * @behaviour "Project Banner is non-dismissible by design. If the message can be dismissed without resolving the underlying state, it isn’t banner-worthy; move it to a `Note`." (https://vercel.com/geist/project-banner) "Show one Project Banner at a time. Stacking competing banners drowns the most urgent state." (https://vercel.com/geist/project-banner) "Always pass a `callToAction` that resolves the state. A banner with no route is a dead end." (https://vercel.com/geist/project-banner) Ours: a strip as wide as its container: a filled ground with a line on its top and bottom edge only, no corners, 40 px high and centred, with the mark, the sentence and the fix on one row; under 768 px the fix drops under the sentence, aligned with its text. The title and the description run on as one passage: the title closed by a full stop when it has none of its own, leading in semibold when a description follows and at regular weight alone, the description after it on the same line, and a description that repeats the title not shown. `detail` takes its own row under the passage. Colour sits on the whole strip (ground, both lines, mark and text); the fix is an underlined link in the darkest step of the same hue (neutral: `text-foreground`) and returns to the strip's text on hover. Neutral and info take the Info mark, warning and destructive the TriangleAlert. `action` is the `callToAction`: one command, never two, never a ready-made button, and there is no close button. Required: `action`, `title`, `tone` and `liveRegion`; the mark is always drawn, and `icon` swaps the tone's mark for another lucide icon.
15
+ * @words "`label` is one sentence in sentence case that names the impact: `Your Pro trial expires in 3 days.` Don’t open with `Heads up` or apologetic preambles." (https://vercel.com/geist/project-banner) "`callToAction.label` is Title Case `Verb + Noun` and points at the resolver: `Update Payment Method`, `Reactivate Project`, `Review Tokens`." (https://vercel.com/geist/project-banner) "Name the affected entity when the project context isn’t obvious from the surrounding chrome (`Production deployments are paused on my-project`)." (https://vercel.com/geist/project-banner) "Don’t encode severity in the copy with emoji or interjections; the variant carries that signal." (https://vercel.com/geist/project-banner) "`NoteLabel` is a 1–2 word Title Case prefix that names the topic: `Region Change`, `Rate Limit`, `Plan Limit`. Cut hedges like `Heads Up`, `FYI`, and `Note`." (https://vercel.com/geist/note) Ours: the `title` is the `label`: the one sentence, naming the condition, its impact and the object. A `description` only adds what the title leaves out, in one more sentence. Our `label` is the `NoteLabel` topic prefix. The action is sentence case, verb and noun, pointing at the resolver: "Update payment method", "Try again". No "Please". The words come from the product's catalogue code.
16
+ * @access `liveRegion="assertive"` gives `role="alert"`, for a failure the person just caused; `"polite"` gives `role="status"`, announced once when it appears; `"off"` gives `role="note"` for one already there at load. The mark, given or not, is hidden.
15
17
  */
16
18
  export function NoticeBanner({ action, className, description, detail, icon, label, liveRegion, title, tone, ...props }) {
17
- const isLive = liveRegion === "polite";
18
- return (_jsxs(Alert, { "aria-atomic": isLive ? "true" : undefined, "aria-live": isLive ? "polite" : undefined, className: cn("flex min-h-10 flex-col items-start gap-2 rounded-none border-x-0 px-6 py-2 md:flex-row md:items-center md:justify-center", tone === "neutral" && "bg-muted text-muted-foreground", NOTICE_EDGE[tone], className), "data-live-region": liveRegion, "data-notice-banner": true, "data-tone": tone, role: isLive ? "status" : "note", variant: tone === "neutral" ? "default" : tone, ...props, children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [_jsx("span", { className: "flex h-5 shrink-0 items-center [&_svg]:size-4", children: _jsx(NoticeMark, { icon: icon, tone: tone }) }), _jsxs("div", { className: "grid min-w-0 text-body-sm", children: [_jsxs(AlertTitle, { className: "font-normal", children: [label ? (_jsxs(_Fragment, { children: [_jsx("strong", { "data-notice-label": true, children: label }), " "] })) : null, title] }), description || detail ? (_jsxs(AlertDescription, { className: "flex min-w-0 flex-col gap-2 text-current", children: [description ? _jsx("div", { "data-notice-description": true, children: description }) : null, detail ? _jsx("div", { "data-notice-detail": true, children: detail }) : null] })) : null] })] }), _jsx(NoticeLink, { className: "pl-6 md:pl-0", command: action, place: "strip", tone: tone })] }));
19
+ const isLive = liveRegion !== "off";
20
+ const body = bodyBeyondTitle({ title, body: description ?? "" });
21
+ return (_jsxs(Alert, { "aria-atomic": isLive ? "true" : undefined, "aria-live": isLive ? liveRegion : undefined, className: cn("flex min-h-10 flex-col items-start gap-2 rounded-none border-x-0 px-6 py-2 md:flex-row md:items-center md:justify-center", tone === "neutral" && "bg-muted text-muted-foreground", NOTICE_EDGE[tone], className), "data-live-region": liveRegion, "data-notice-banner": true, "data-tone": tone, role: LIVE_REGION_ROLE[liveRegion], variant: tone === "neutral" ? "default" : tone, ...props, children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [_jsx("span", { className: "flex h-5 shrink-0 items-center [&_svg]:size-4", children: _jsx(NoticeMark, { icon: icon, tone: tone }) }), _jsxs("div", { className: "grid min-w-0 gap-2 text-body-sm", children: [_jsxs(AlertTitle, { className: "font-normal break-words", children: [label ? (_jsxs(_Fragment, { children: [_jsx("strong", { "data-notice-label": true, children: label }), " "] })) : null, body ? _jsx("span", { className: "font-semibold", children: closedSentence(title) }) : title, body ? (_jsxs(_Fragment, { children: [" ", _jsx("span", { "data-notice-description": true, children: body })] })) : null] }), detail ? (_jsx(AlertDescription, { className: "min-w-0 text-current", "data-notice-detail": true, children: detail })) : null] })] }), _jsx(NoticeLink, { className: "pl-6 md:pl-0", command: action, place: "strip", tone: tone })] }));
19
22
  }
@@ -28,7 +28,7 @@ export declare function wordMessage(parts: {
28
28
  actions?: readonly MessageAction[];
29
29
  }): ResolvedMessage;
30
30
  /** The body a surface shows: none when it would repeat the title. */
31
- export declare function bodyBeyondTitle(message: ResolvedMessage): string | null;
31
+ export declare function bodyBeyondTitle(message: Pick<ResolvedMessage, "title" | "body">): string | null;
32
32
  /** The title and the body as one line of sentences: "Couldn't pause Discord. The Gateway didn't answer." */
33
33
  export declare function messageSentences(message: ResolvedMessage): string;
34
34
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"product-message.d.ts","sourceRoot":"","sources":["../../src/components/product-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC;AAEtE,MAAM,MAAM,cAAc,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,IAAI;IACzD,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF,wDAAwD;AACxD,MAAM,MAAM,aAAa,GAAG,WAAW,CAAC;AAExC,OAAO,CAAC,MAAM,MAAM,EAAE,OAAO,MAAM,CAAC;AAEpC,uGAAuG;AACvG,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,CAAC;IAC3C,QAAQ,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC;CACzB,CAAC;AAEF,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;CACpC,GAAG,eAAe,CAGlB;AAED,qEAAqE;AACrE,wBAAgB,eAAe,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAGvE;AAID,4GAA4G;AAC5G,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAKjE"}
1
+ {"version":3,"file":"product-message.d.ts","sourceRoot":"","sources":["../../src/components/product-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC;AAEtE,MAAM,MAAM,cAAc,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM,IAAI;IACzD,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF,wDAAwD;AACxD,MAAM,MAAM,aAAa,GAAG,WAAW,CAAC;AAExC,OAAO,CAAC,MAAM,MAAM,EAAE,OAAO,MAAM,CAAC;AAEpC,uGAAuG;AACvG,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,SAAS,aAAa,EAAE,CAAC;IAC3C,QAAQ,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC;CACzB,CAAC;AAEF,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;CACpC,GAAG,eAAe,CAGlB;AAED,qEAAqE;AACrE,wBAAgB,eAAe,CAAC,OAAO,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,GAAG,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAG/F;AAID,4GAA4G;AAC5G,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAKjE"}
@@ -1,4 +1,4 @@
1
- import type { ComponentProps, ReactNode } from "react";
1
+ import { type ComponentProps, type ReactNode } from "react";
2
2
  import { HoverCardContent } from "./hover-card.js";
3
3
  export type RecordTimeProps = {
4
4
  /** When it happened; null or a time that names no moment draws `children`, or nothing. */
@@ -17,7 +17,7 @@ export type RecordTimeProps = {
17
17
  * UTC on the server and while the page hydrates, so the server's HTML never shows its own zone as the viewer's.
18
18
  */
19
19
  timeZone?: string;
20
- /** The current time, for a reading that must not drift while it renders. */
20
+ /** The current time, for a reading that must not drift while it renders; left out, the clock `RecordTimeRenderedAt` describes. */
21
21
  now?: number;
22
22
  } & ({
23
23
  mode?: "relative";
@@ -33,6 +33,16 @@ export type RecordTimeProps = {
33
33
  });
34
34
  /** "3 hours ago" within a week, then "12 Sep", with the year when it is not this year. */
35
35
  export declare function formatRecordTime(at: Date, locale?: string, now?: number, timeZone?: string): string;
36
+ /**
37
+ * The moment the server drew the page, for every `RecordTime` below: the server writes each relative
38
+ * time against it and the hydrating page reads the same one, so the two agree whatever the clock did
39
+ * in between, and the live page then reads the browser's clock. A server layout passes it unawaited,
40
+ * as a promise that settles once a request arrives, so the static shell never waits for it.
41
+ */
42
+ export declare function RecordTimeRenderedAt({ moment, children, }: {
43
+ moment: Promise<number>;
44
+ children: ReactNode;
45
+ }): import("react").JSX.Element;
36
46
  /** The moment `at` names, or null when it names none, such as an empty or malformed time. */
37
47
  export declare function recordDate(at: Date | number | string | null | undefined): Date | null;
38
48
  /**
@@ -45,7 +55,7 @@ export declare function recordDate(at: Date | number | string | null | undefined
45
55
  * @instead "For a static date in body prose past 7 days, render `Mar 14, 2026` directly." (https://vercel.com/geist/relative-time-card)
46
56
  * Ours: a date a person picks is a Calendar; a span of days a page reads is a DateRangeControl; a run's duration is a fact written by the product (*Took 4 min*).
47
57
  * @behaviour "The hover card already shows absolute UTC and local time, so don’t duplicate that copy elsewhere on the row." (https://vercel.com/geist/relative-time-card)
48
- * Ours: relative within a week, then the day and month, with the year only when it is another year; `mode="clock"` prints the time of day in the viewer's zone in tabular figures. Hovering either shows a card that opens above after 150 ms, 13 px from the time, with a stem: the relative reading first, then the exact date and time in UTC and in the viewer's zone, each behind a zone chip, the viewer's by its offset (*GMT+2*). A page rendered on a server passes the viewer's `timeZone` when it knows it, so the server and the browser write the same time; otherwise the server and the hydrating page write UTC and the page then rewrites it in the browser's zone. A time that names no moment draws `children`, a state such as *Pending*, or nothing. While `at` does name a moment, `children` (*Queued*) replaces the sentence and the card stays.
58
+ * Ours: relative within a week, then the day and month, with the year only when it is another year; `mode="clock"` prints the time of day in the viewer's zone in tabular figures. Hovering either shows a card that opens above after 150 ms, 13 px from the time, with a stem: the relative reading first, then the exact date and time in UTC and in the viewer's zone, each behind a zone chip, the viewer's by its offset (*GMT+2*). A page rendered on a server passes the viewer's `timeZone` when it knows it, so the server and the browser write the same time; otherwise the server and the hydrating page write UTC and the page then rewrites it in the browser's zone. A relative time counts from the moment the server drew the page, which a `RecordTimeRenderedAt` above gives the server and the hydrating page alike, so a minute that turns before the page is live changes nothing, and the live page then counts from the browser's clock; a server with no such moment writes the day with its year (*Signed up Oct 14, 2026*), and the live page rewrites it. The product supplies any refresh after that. A time that names no moment draws `children`, a state such as *Pending*, or nothing. While `at` does name a moment, `children` (*Queued*) replaces the sentence and the card stays.
49
59
  * @words "Pass `date` as a number (Unix ms or epoch)." "Don’t pre-format the value, and don’t replace `children` with a formatted string." "The component’s short formatter is the canonical form (`2m`, `5h`, `Yesterday`)." "Don’t append `ago` after the component." "The formatter already produces `2m ago` / `5h ago`, so `<RelativeTimeCard /> ago` reads as `2m ago ago`." "Use `children` only for non-time labels (`Just now`, `Pending`, `Queued`) where the formatter can’t describe the state." "Pair with a leading label when the row is ambiguous on its own: `Last deploy <RelativeTimeCard date={ts} />`." (https://vercel.com/geist/relative-time-card)
50
60
  * Ours: `at` takes a Date, epoch milliseconds or an ISO string, never a display string such as *3 hours ago*. The reading is the locale's long form (*3 hours ago*, *12 Sep*), not `2m`. The product writes the sentence through `phrase` (*Updated {when}*, *Ran {when}*), so *ago* is never added after it; the clock needs no words. The package formats every date with the person's `locale`.
51
61
  * @access Ours: a `<time>` element with the machine-readable moment; assistive technology reads the exact local moment after the visible text.
@@ -1 +1 @@
1
- {"version":3,"file":"record-time.d.ts","sourceRoot":"","sources":["../../src/components/record-time.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvD,OAAO,EAAa,gBAAgB,EAAoB,MAAM,yBAAyB,CAAC;AAYxF,MAAM,MAAM,eAAe,GAAG;IAC5B,0FAA0F;IAC1F,EAAE,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC9C;;;OAGG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,kEAAkE;IAClE,IAAI,CAAC,EAAE,cAAc,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;IACvD,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GAAG,CACA;IACE,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;;;OAGG;IACH,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CAClC;AACH,gEAAgE;GAC9D;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CACpB,CAAC;AAEF,0FAA0F;AAC1F,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,IAAI,EACR,MAAM,CAAC,EAAE,MAAM,EACf,GAAG,SAAa,EAChB,QAAQ,CAAC,EAAE,MAAM,GAChB,MAAM,CAcR;AAeD,6FAA6F;AAC7F,wBAAgB,UAAU,CAAC,EAAE,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,GAAG,IAAI,CAIrF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,sCAyEhD"}
1
+ {"version":3,"file":"record-time.d.ts","sourceRoot":"","sources":["../../src/components/record-time.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAsB,KAAK,cAAc,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEhF,OAAO,EAAa,gBAAgB,EAAoB,MAAM,yBAAyB,CAAC;AAYxF,MAAM,MAAM,eAAe,GAAG;IAC5B,0FAA0F;IAC1F,EAAE,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC9C;;;OAGG;IACH,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,kEAAkE;IAClE,IAAI,CAAC,EAAE,cAAc,CAAC,OAAO,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC;IACvD,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kIAAkI;IAClI,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,GAAG,CACA;IACE,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;;;OAGG;IACH,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CAClC;AACH,gEAAgE;GAC9D;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CACpB,CAAC;AAEF,0FAA0F;AAC1F,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,IAAI,EACR,MAAM,CAAC,EAAE,MAAM,EACf,GAAG,SAAa,EAChB,QAAQ,CAAC,EAAE,MAAM,GAChB,MAAM,CAWR;AAeD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,MAAM,EACN,QAAQ,GACT,EAAE;IACD,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACxB,QAAQ,EAAE,SAAS,CAAC;CACrB,+BAEA;AAgDD,6FAA6F;AAC7F,wBAAgB,UAAU,CAAC,EAAE,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,IAAI,GAAG,IAAI,CAIrF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,sCA+EhD"}
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { createContext, use } from "react";
3
4
  import { HoverCard, HoverCardContent, HoverCardTrigger } from "./hover-card.js";
4
5
  import { useHydrated } from "../hooks/use-hydrated.js";
5
6
  const DAY = 86_400_000;
@@ -21,12 +22,41 @@ export function formatRecordTime(at, locale, now = Date.now(), timeZone) {
21
22
  return relative.format(0, "minute");
22
23
  }
23
24
  const yearOf = new Intl.DateTimeFormat("en", { timeZone, year: "numeric" });
24
- const sameYear = yearOf.format(at) === yearOf.format(now);
25
+ return recordDay(at, locale, timeZone, yearOf.format(at) !== yearOf.format(now));
26
+ }
27
+ function recordDay(at, locale, timeZone, withYear) {
25
28
  const shown = { timeZone, day: "numeric", month: "short" };
26
- if (!sameYear)
29
+ if (withYear)
27
30
  shown.year = "numeric";
28
31
  return new Intl.DateTimeFormat(locale, shown).format(at);
29
32
  }
33
+ const RenderedAt = createContext(null);
34
+ /**
35
+ * The moment the server drew the page, for every `RecordTime` below: the server writes each relative
36
+ * time against it and the hydrating page reads the same one, so the two agree whatever the clock did
37
+ * in between, and the live page then reads the browser's clock. A server layout passes it unawaited,
38
+ * as a promise that settles once a request arrives, so the static shell never waits for it.
39
+ */
40
+ export function RecordTimeRenderedAt({ moment, children, }) {
41
+ return _jsx(RenderedAt, { value: moment, children: children });
42
+ }
43
+ function useReadingTime(now, counted) {
44
+ const hydrated = useHydrated();
45
+ if (now !== undefined || !counted)
46
+ return now;
47
+ if (hydrated)
48
+ return Date.now();
49
+ const renderedAt = use(RenderedAt);
50
+ return renderedAt ? use(renderedAt) : undefined;
51
+ }
52
+ function relativeWhen(at, locale, reading, timeZone) {
53
+ return reading === undefined
54
+ ? recordDay(at, locale, timeZone, true)
55
+ : formatRecordTime(at, locale, reading, timeZone);
56
+ }
57
+ function OpenedReading({ at, locale, now, timeZone, }) {
58
+ return relativeWhen(at, locale, useReadingTime(now, true), timeZone);
59
+ }
30
60
  function offsetLabel(at, locale, timeZone) {
31
61
  return new Intl.DateTimeFormat(locale, { timeZone, timeZoneName: "shortOffset" })
32
62
  .formatToParts(at)
@@ -55,7 +85,7 @@ export function recordDate(at) {
55
85
  * @instead "For a static date in body prose past 7 days, render `Mar 14, 2026` directly." (https://vercel.com/geist/relative-time-card)
56
86
  * Ours: a date a person picks is a Calendar; a span of days a page reads is a DateRangeControl; a run's duration is a fact written by the product (*Took 4 min*).
57
87
  * @behaviour "The hover card already shows absolute UTC and local time, so don’t duplicate that copy elsewhere on the row." (https://vercel.com/geist/relative-time-card)
58
- * Ours: relative within a week, then the day and month, with the year only when it is another year; `mode="clock"` prints the time of day in the viewer's zone in tabular figures. Hovering either shows a card that opens above after 150 ms, 13 px from the time, with a stem: the relative reading first, then the exact date and time in UTC and in the viewer's zone, each behind a zone chip, the viewer's by its offset (*GMT+2*). A page rendered on a server passes the viewer's `timeZone` when it knows it, so the server and the browser write the same time; otherwise the server and the hydrating page write UTC and the page then rewrites it in the browser's zone. A time that names no moment draws `children`, a state such as *Pending*, or nothing. While `at` does name a moment, `children` (*Queued*) replaces the sentence and the card stays.
88
+ * Ours: relative within a week, then the day and month, with the year only when it is another year; `mode="clock"` prints the time of day in the viewer's zone in tabular figures. Hovering either shows a card that opens above after 150 ms, 13 px from the time, with a stem: the relative reading first, then the exact date and time in UTC and in the viewer's zone, each behind a zone chip, the viewer's by its offset (*GMT+2*). A page rendered on a server passes the viewer's `timeZone` when it knows it, so the server and the browser write the same time; otherwise the server and the hydrating page write UTC and the page then rewrites it in the browser's zone. A relative time counts from the moment the server drew the page, which a `RecordTimeRenderedAt` above gives the server and the hydrating page alike, so a minute that turns before the page is live changes nothing, and the live page then counts from the browser's clock; a server with no such moment writes the day with its year (*Signed up Oct 14, 2026*), and the live page rewrites it. The product supplies any refresh after that. A time that names no moment draws `children`, a state such as *Pending*, or nothing. While `at` does name a moment, `children` (*Queued*) replaces the sentence and the card stays.
59
89
  * @words "Pass `date` as a number (Unix ms or epoch)." "Don’t pre-format the value, and don’t replace `children` with a formatted string." "The component’s short formatter is the canonical form (`2m`, `5h`, `Yesterday`)." "Don’t append `ago` after the component." "The formatter already produces `2m ago` / `5h ago`, so `<RelativeTimeCard /> ago` reads as `2m ago ago`." "Use `children` only for non-time labels (`Just now`, `Pending`, `Queued`) where the formatter can’t describe the state." "Pair with a leading label when the row is ambiguous on its own: `Last deploy <RelativeTimeCard date={ts} />`." (https://vercel.com/geist/relative-time-card)
60
90
  * Ours: `at` takes a Date, epoch milliseconds or an ISO string, never a display string such as *3 hours ago*. The reading is the locale's long form (*3 hours ago*, *12 Sep*), not `2m`. The product writes the sentence through `phrase` (*Updated {when}*, *Ran {when}*), so *ago* is never added after it; the clock needs no words. The package formats every date with the person's `locale`.
61
91
  * @access Ours: a `<time>` element with the machine-readable moment; assistive technology reads the exact local moment after the visible text.
@@ -65,6 +95,8 @@ export function RecordTime(props) {
65
95
  const hydrated = useHydrated();
66
96
  const timeZone = props.timeZone ?? (hydrated ? undefined : "UTC");
67
97
  const date = recordDate(at);
98
+ const clock = props.mode === "clock";
99
+ const reading = useReadingTime(now, date !== null && !clock);
68
100
  if (!date) {
69
101
  return props.children ? (_jsx("span", { className: "whitespace-nowrap text-muted-foreground", "data-slot": "record-time", children: props.children })) : null;
70
102
  }
@@ -81,12 +113,12 @@ export function RecordTime(props) {
81
113
  ...exactMoment(date, locale, timeZone),
82
114
  },
83
115
  ];
84
- const clock = props.mode === "clock";
116
+ const when = relativeWhen(date, locale, reading, timeZone);
85
117
  const shown = props.children ??
86
118
  (clock
87
119
  ? new Intl.DateTimeFormat(locale, { timeStyle: "short", timeZone }).format(date)
88
- : props.phrase(formatRecordTime(date, locale, now, timeZone)));
120
+ : props.phrase(when));
89
121
  return (_jsxs(HoverCard, { children: [_jsxs(HoverCardTrigger, { render: _jsx("time", { className: clock
90
122
  ? "whitespace-nowrap tabular-nums text-muted-foreground"
91
- : "whitespace-nowrap text-muted-foreground", "data-slot": "record-time", dateTime: date.toISOString() }), children: [shown, _jsxs("span", { className: "sr-only", children: [", ", local] })] }), _jsx(HoverCardContent, { side: props.side, sideOffset: 13, children: _jsxs("div", { className: "grid min-w-75 gap-3 text-body-compact", "data-slot": "record-time-exact", children: [_jsx("p", { className: "text-muted-foreground tabular-nums", children: formatRecordTime(date, locale, now, timeZone) }), _jsx("dl", { className: "grid gap-2", children: exactRows.map((row) => (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("dt", { className: "inline-flex h-4 items-center rounded-xs bg-recessed px-1.5 font-label-mono text-label-mono text-muted-foreground", children: row.zone }), _jsxs("dd", { className: "flex min-w-0 flex-1 items-center justify-between gap-3", children: [_jsx("span", { children: row.date }), _jsx("span", { className: "font-label-mono text-label-mono text-muted-foreground tabular-nums", children: row.time })] })] }, row.id))) })] }) })] }));
123
+ : "whitespace-nowrap text-muted-foreground", "data-slot": "record-time", dateTime: date.toISOString() }), children: [shown, _jsxs("span", { className: "sr-only", children: [", ", local] })] }), _jsx(HoverCardContent, { side: props.side, sideOffset: 13, children: _jsxs("div", { className: "grid min-w-75 gap-3 text-body-compact", "data-slot": "record-time-exact", children: [_jsx("p", { className: "text-muted-foreground tabular-nums", children: clock ? (_jsx(OpenedReading, { at: date, locale: locale, now: now, timeZone: timeZone })) : (when) }), _jsx("dl", { className: "grid gap-2", children: exactRows.map((row) => (_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsx("dt", { className: "inline-flex h-4 items-center rounded-xs bg-recessed px-1.5 font-label-mono text-label-mono text-muted-foreground", children: row.zone }), _jsxs("dd", { className: "flex min-w-0 flex-1 items-center justify-between gap-3", children: [_jsx("span", { children: row.date }), _jsx("span", { className: "font-label-mono text-label-mono text-muted-foreground tabular-nums", children: row.time })] })] }, row.id))) })] }) })] }));
92
124
  }
@@ -3,6 +3,8 @@ import { type PageCommand } from "../components/page-actions.js";
3
3
  export type NoticeTone = "neutral" | "info" | "warning" | "destructive";
4
4
  /** The hue of a notice's edge: a light step of its text colour, neutral for the plain tone. */
5
5
  export declare const NOTICE_EDGE: Record<NoticeTone, string>;
6
+ /** A notice's title as the first of two sentences: closed by a full stop when it has no mark of its own. */
7
+ export declare const closedSentence: (title: string) => string;
6
8
  /** The mark a tone draws: Info for neutral and info, the triangle for warning and destructive. */
7
9
  export declare function NoticeMark({ icon, tone }: {
8
10
  icon?: LucideIcon | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"notice.d.ts","sourceRoot":"","sources":["../../src/internal/notice.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAiB,MAAM,cAAc,CAAC;AAEpE,OAAO,EAAE,KAAK,WAAW,EAAqB,MAAM,2BAA2B,CAAC;AAGhF,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,aAAa,CAAC;AAExE,+FAA+F;AAC/F,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAKlD,CAAC;AAEF,kGAAkG;AAClG,wBAAgB,UAAU,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,+BAG7F;AAgBD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EACzB,SAAS,EACT,OAAO,EACP,GAAG,KAAK,EACT,EAAE;IACD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,WAAW,CAAC;CACtB,GAAG,CAAC;IAAE,KAAK,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC,+BAc/D"}
1
+ {"version":3,"file":"notice.d.ts","sourceRoot":"","sources":["../../src/internal/notice.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAiB,MAAM,cAAc,CAAC;AAEpE,OAAO,EAAE,KAAK,WAAW,EAAqB,MAAM,2BAA2B,CAAC;AAGhF,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,aAAa,CAAC;AAExE,+FAA+F;AAC/F,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAKlD,CAAC;AAEF,4GAA4G;AAC5G,eAAO,MAAM,cAAc,GAAI,OAAO,MAAM,KAAG,MACA,CAAC;AAEhD,kGAAkG;AAClG,wBAAgB,UAAU,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,+BAG7F;AAgBD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EACzB,SAAS,EACT,OAAO,EACP,GAAG,KAAK,EACT,EAAE;IACD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,WAAW,CAAC;CACtB,GAAG,CAAC;IAAE,KAAK,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC,+BAc/D"}
@@ -9,6 +9,8 @@ export const NOTICE_EDGE = {
9
9
  warning: "border-warning-soft-foreground/25",
10
10
  destructive: "border-destructive-soft-foreground/25",
11
11
  };
12
+ /** A notice's title as the first of two sentences: closed by a full stop when it has no mark of its own. */
13
+ export const closedSentence = (title) => /[.!?:…]$/u.test(title) ? title : `${title}.`;
12
14
  /** The mark a tone draws: Info for neutral and info, the triangle for warning and destructive. */
13
15
  export function NoticeMark({ icon, tone }) {
14
16
  const Mark = icon ?? (tone === "warning" || tone === "destructive" ? TriangleAlert : Info);
@@ -71,7 +71,7 @@ export const FEEDBACK = {
71
71
  ]), placement: "action" }), _jsx(MessageNotice, { message: sampleMessage("Billing is paused", "Agents stop at the end of the cycle.", [
72
72
  { label: "Update payment", onClick: nothing },
73
73
  ]), placement: "section", tone: "warning" }), _jsx(MessageNotice, { message: sampleMessage("Orders is read-only for you", "Ask an owner of Acme to change who can write to it."), placement: "section", tone: "info" })] })),
74
- NoticeBanner: () => (_jsxs("div", { className: "grid max-w-lg gap-6", children: [_jsx(Captioned, { caption: "Warning, with a label and a command", children: _jsx(NoticeBanner, { action: { label: "Open Slack settings", onClick: nothing }, description: "Agents can't post to Slack until it is connected again.", label: "Slack", liveRegion: "off", title: "Slack is disconnected", tone: "warning" }) }), _jsx(Captioned, { caption: "Destructive, with its own icon and a link", children: _jsx(NoticeBanner, { action: { label: "Update payment method", link: _jsx("a", { href: "#billing" }) }, description: "Agents stop at the end of the billing cycle.", icon: CreditCard, label: "Billing", liveRegion: "polite", title: "Your last payment failed", tone: "destructive" }) })] })),
74
+ NoticeBanner: () => (_jsxs("div", { className: "grid max-w-lg gap-6", children: [_jsx(Captioned, { caption: "Warning, with a label and a command", children: _jsx(NoticeBanner, { action: { label: "Try again", onClick: nothing }, description: "They show the reading from 10:42.", label: "Usage", liveRegion: "off", title: "The figures couldn't be refreshed", tone: "warning" }) }), _jsx(Captioned, { caption: "Destructive, with its own icon and a link", children: _jsx(NoticeBanner, { action: { label: "Update payment method", link: _jsx("a", { href: "#billing" }) }, description: "Agents stop at the end of the billing cycle.", icon: CreditCard, label: "Billing", liveRegion: "polite", title: "Your last payment failed", tone: "destructive" }) })] })),
75
75
  PendingLabel: PendingLabelSpecimen,
76
76
  Skeleton: () => (_jsxs(SkeletonRegion, { busy: true, className: "grid max-w-sm gap-3", children: [_jsx(Skeleton, { boxHeight: 20, height: 16, width: "75%" }), _jsx(Skeleton, { height: 16, width: 160 }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Skeleton, { height: 32, shape: "circle", width: 32 }), _jsx(Skeleton, { height: 48, shape: "squared", width: 48 })] }), _jsx(Skeleton, { height: 100, width: "100%", children: null }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Skeleton, { children: _jsx(Button, { children: "Save changes" }) }), _jsx(Skeleton, { show: false, children: _jsx(Button, { children: "Save changes" }) })] })] })),
77
77
  SkeletonRegion: SkeletonRegionSpecimen,
@@ -1154,7 +1154,7 @@ export const USAGE = [
1154
1154
  summary: "When something happened to a record: a relative time in the sentence that says what, or the time of day in a feed's time column.",
1155
1155
  when: '"Use `<RelativeTimeCard>` anywhere a recent timestamp appears in scannable surfaces: `Table` cells, `Entity` rows, deploy lists, activity feeds." (https://vercel.com/geist/relative-time-card) Ours: `RelativeTimeCard` is `RecordTime`: the moment a record was made, changed or last ran, in its row or its facts, and the time of each event in an ActivityFeed or a RunTimeline step (`mode="clock"`).',
1156
1156
  instead: '"For a static date in body prose past 7 days, render `Mar 14, 2026` directly." (https://vercel.com/geist/relative-time-card) Ours: a date a person picks is a Calendar; a span of days a page reads is a DateRangeControl; a run\'s duration is a fact written by the product (*Took 4 min*).',
1157
- behaviour: "\"The hover card already shows absolute UTC and local time, so don’t duplicate that copy elsewhere on the row.\" (https://vercel.com/geist/relative-time-card) Ours: relative within a week, then the day and month, with the year only when it is another year; `mode=\"clock\"` prints the time of day in the viewer's zone in tabular figures. Hovering either shows a card that opens above after 150 ms, 13 px from the time, with a stem: the relative reading first, then the exact date and time in UTC and in the viewer's zone, each behind a zone chip, the viewer's by its offset (*GMT+2*). A page rendered on a server passes the viewer's `timeZone` when it knows it, so the server and the browser write the same time; otherwise the server and the hydrating page write UTC and the page then rewrites it in the browser's zone. A time that names no moment draws `children`, a state such as *Pending*, or nothing. While `at` does name a moment, `children` (*Queued*) replaces the sentence and the card stays.",
1157
+ behaviour: "\"The hover card already shows absolute UTC and local time, so don’t duplicate that copy elsewhere on the row.\" (https://vercel.com/geist/relative-time-card) Ours: relative within a week, then the day and month, with the year only when it is another year; `mode=\"clock\"` prints the time of day in the viewer's zone in tabular figures. Hovering either shows a card that opens above after 150 ms, 13 px from the time, with a stem: the relative reading first, then the exact date and time in UTC and in the viewer's zone, each behind a zone chip, the viewer's by its offset (*GMT+2*). A page rendered on a server passes the viewer's `timeZone` when it knows it, so the server and the browser write the same time; otherwise the server and the hydrating page write UTC and the page then rewrites it in the browser's zone. A relative time counts from the moment the server drew the page, which a `RecordTimeRenderedAt` above gives the server and the hydrating page alike, so a minute that turns before the page is live changes nothing, and the live page then counts from the browser's clock; a server with no such moment writes the day with its year (*Signed up Oct 14, 2026*), and the live page rewrites it. The product supplies any refresh after that. A time that names no moment draws `children`, a state such as *Pending*, or nothing. While `at` does name a moment, `children` (*Queued*) replaces the sentence and the card stays.",
1158
1158
  words: '"Pass `date` as a number (Unix ms or epoch)." "Don’t pre-format the value, and don’t replace `children` with a formatted string." "The component’s short formatter is the canonical form (`2m`, `5h`, `Yesterday`)." "Don’t append `ago` after the component." "The formatter already produces `2m ago` / `5h ago`, so `<RelativeTimeCard /> ago` reads as `2m ago ago`." "Use `children` only for non-time labels (`Just now`, `Pending`, `Queued`) where the formatter can’t describe the state." "Pair with a leading label when the row is ambiguous on its own: `Last deploy <RelativeTimeCard date={ts} />`." (https://vercel.com/geist/relative-time-card) Ours: `at` takes a Date, epoch milliseconds or an ISO string, never a display string such as *3 hours ago*. The reading is the locale\'s long form (*3 hours ago*, *12 Sep*), not `2m`. The product writes the sentence through `phrase` (*Updated {when}*, *Ran {when}*), so *ago* is never added after it; the clock needs no words. The package formats every date with the person\'s `locale`.',
1159
1159
  access: "Ours: a `<time>` element with the machine-readable moment; assistive technology reads the exact local moment after the visible text.",
1160
1160
  },
@@ -1307,7 +1307,7 @@ export const USAGE = [
1307
1307
  family: "Feedback",
1308
1308
  summary: "A catalogue message about one thing on the screen.",
1309
1309
  when: '"Display text that requires attention or provides additional information." (https://vercel.com/geist/note) "Use a Note for inline contextual feedback next to the field, card, or section it describes: a region change warning above a region picker, a rate-limit notice next to a usage gauge." (https://vercel.com/geist/note) "Choose `variant` by meaning: `error` for a problem the user must fix, `warning` for a consequence to acknowledge, `success` for a passed check, `secondary` for neutral information." (https://vercel.com/geist/note) Ours: a message beside the field, card or section it describes, under the control whose write failed (`action`), or atop the section whose condition lasts (`section`). The `tone` is the `variant`: `destructive` is `error`, `warning` is `warning`, `info` is `success` (a passed check or a state the product manages for the reader, drawn in the accent blue) and `neutral` is `secondary`; there is no violet or cyan.',
1310
- instead: '"Pick `Banner` when the message is page-level or system-wide and needs a CTA, `Toast` for transient acknowledgments, `Modal` for destructive confirmations." (https://vercel.com/geist/note) Ours: `Banner` is `NoticeBanner`, `Toast` is `showMessageToast` and `Modal` is `ConfirmDialog`. For a field\'s wrong value use `FieldError`. For a failed read use `EmptyState failed`.',
1310
+ instead: "\"Pick `Banner` when the message is page-level or system-wide and needs a CTA, `Toast` for transient acknowledgments, `Modal` for destructive confirmations.\" (https://vercel.com/geist/note) Ours: `Banner` is `NoticeBanner`, for a condition of the page's own object, and a system-wide condition is a notification in the product's inbox, never a banner; `Toast` is `showMessageToast` and `Modal` is `ConfirmDialog`. For a field's wrong value use `FieldError`. For a failed read use `EmptyState failed`.",
1311
1311
  behaviour: '"A Note is persistent until the underlying state changes; don’t add an ad hoc dismiss control because it competes with the message." (https://vercel.com/geist/note) "One Note per concept. Stacking three Notes on a card means the page architecture, not the Note copy, is wrong." (https://vercel.com/geist/note) "The optional `NoteAction` holds a single inline CTA. Don’t pair it with a second button." (https://vercel.com/geist/note) Ours: `Alert` is its primitive. Tone `destructive` by default, `warning`, `info` or `neutral`; there is no `success` tone. Colour sits on the line, the text and the mark; the ground stays clear, and the action stays neutral. `section` is the framed note, a line 36 px high with its mark and sentence on one row and the action at its trailing edge (under 768 px the action takes its own row, full width), with or without an action; `action` is the unframed passage, its mark and sentence on one row with the action link inside the sentence, after a space. The title and the body run on as one passage, the title closed by a full stop when it has none of its own, so the two never read as one sentence; the title leads in bold (semibold in a note, medium in a passage) only when a body follows it, and stands at regular weight alone. A message worded for a notice has one action, a small default button in a section and an underlined 14 px link in the passage, and at most a second one after it (Nadia\'s *Ask Nadia* after *Try again*), an outline button in a section and a second link in the passage; a third is not shown. Long words wrap, nothing truncates. A successful retry removes it. Required: the catalogue `message`, so the sentence and its mark are always drawn.',
1312
1312
  words: '"`NoteLabel` is a 1–2 word Title Case prefix that names the topic: `Region Change`, `Rate Limit`, `Plan Limit`. Cut hedges like `Heads Up`, `FYI`, and `Note`." (https://vercel.com/geist/note) "`NoteContent` is one sentence in active voice that names the impact: `Changing this region restarts all functions.`" (https://vercel.com/geist/note) "No `variant="info"` exists; omit `variant` for the default info icon or use `variant="secondary"` for neutral copy." (https://vercel.com/geist/note) "Single-fragment labels take no period; full descriptive sentences in the body do." (https://vercel.com/geist/note) Ours: words come from the catalogue only. The message\'s title stands in for `NoteLabel` and `NoteContent`, in sentence case, and `info` exists as Geist\'s blue `success`. The action is labelled with its result: *Try again*, *Sign in*. When the message carries an action, its body does not repeat it: the link is the step, so the body ends before "Try again".',
1313
1313
  access: '`action`: `role="alert"` (`status` for info and neutral). `section`: `role="status"`, `aria-live="polite"`, atomic. The mark is hidden. Focus stays where the person was.',
@@ -1316,12 +1316,12 @@ export const USAGE = [
1316
1316
  name: "NoticeBanner",
1317
1317
  module: "notice-banner",
1318
1318
  family: "Feedback",
1319
- summary: "A condition of the whole page, the account or the service, with its fix.",
1320
- when: '"A prominent message that spans the full width of its container to announce important information." (https://vercel.com/geist/banner) "Used for temporary, project-wide notifications that require resolution" (https://vercel.com/geist/project-banner) "Use Project Banner for project-wide states that need resolution: overdue billing, an active rollback, attack mitigation, an expiring trial blocking deploys." (https://vercel.com/geist/project-banner) "Match `variant` to severity: `error` for critical downtime or payment-blocking states, `warning` for an exceptional state with non-immediate action, `success` for a positive temporary mitigation, `gray` for routine project-wide notices." (https://vercel.com/geist/project-banner) Ours: a condition of the whole page, the account or the service that the page itself can resolve, with that resolving action on the banner. The `tone` is the `variant`: `destructive` is `error`, `warning` is `warning`, `info` is `success` and `neutral` is `gray`. One sits above the page\'s content, under its title.',
1321
- instead: '"Pick `Note` for inline contextual messages tied to a single field or card, `Toast` for transient acknowledgments, `Modal` for confirmations." (https://vercel.com/geist/project-banner) Ours: there is no announcement strip. `Note` is `MessageNotice` (`placement="section"`), `Toast` is `showMessageToast` and `Modal` is `ConfirmDialog`. For a measured level use a `KpiCard` with its tone; crossing a threshold is a notification in the inbox, not a banner.',
1322
- behaviour: '"Project Banner is non-dismissible by design. If the message can be dismissed without resolving the underlying state, it isn’t banner-worthy; move it to a `Note`." (https://vercel.com/geist/project-banner) "Show one Project Banner at a time. Stacking competing banners drowns the most urgent state." (https://vercel.com/geist/project-banner) "Always pass a `callToAction` that resolves the state. A banner with no route is a dead end." (https://vercel.com/geist/project-banner) Ours: a strip as wide as its container: a filled ground with a line on its top and bottom edge only, no corners, 40 px high and centred, with the mark, the sentence and the fix on one row; under 768 px the fix drops under the sentence, aligned with its text. Colour sits on the whole strip (ground, both lines, mark and text); the fix is an underlined link in the darkest step of the same hue (neutral: `text-foreground`) and returns to the strip\'s text on hover. Neutral and info take the Info mark, warning and destructive the TriangleAlert. `action` is the `callToAction`: one command, never two, never a ready-made button, and there is no close button. Required: `action`, `title`, `tone` and `liveRegion`; the mark is always drawn, and `icon` swaps the tone\'s mark for another lucide icon.',
1323
- words: '"`label` is one sentence in sentence case that names the impact: `Your Pro trial expires in 3 days.` Don’t open with `Heads up` or apologetic preambles." (https://vercel.com/geist/project-banner) "`callToAction.label` is Title Case `Verb + Noun` and points at the resolver: `Update Payment Method`, `Reactivate Project`, `Review Tokens`." (https://vercel.com/geist/project-banner) "Name the affected entity when the project context isn’t obvious from the surrounding chrome (`Production deployments are paused on my-project`)." (https://vercel.com/geist/project-banner) "Don’t encode severity in the copy with emoji or interjections; the variant carries that signal." (https://vercel.com/geist/project-banner) "`NoteLabel` is a 1–2 word Title Case prefix that names the topic: `Region Change`, `Rate Limit`, `Plan Limit`. Cut hedges like `Heads Up`, `FYI`, and `Note`." (https://vercel.com/geist/note) Ours: the `title` is the `label`: the one sentence, naming the condition, its impact and the object. A `description` only adds what the title leaves out, in one more sentence. Our `label` is the `NoteLabel` topic prefix. The action is sentence case, verb and noun, pointing at the resolver: "Open Slack settings", "Update card". No "Please". The words come from the product\'s catalogue code.',
1324
- access: '`liveRegion="polite"` gives `role="status"`, announced once when it appears; `"off"` gives `role="note"` for one already there at load. The mark, given or not, is hidden.',
1319
+ summary: "A condition of the page's own object, with the fix the page offers.",
1320
+ when: '"A prominent message that spans the full width of its container to announce important information." (https://vercel.com/geist/banner) "Used for temporary, project-wide notifications that require resolution" (https://vercel.com/geist/project-banner) "Use Project Banner for project-wide states that need resolution: overdue billing, an active rollback, attack mitigation, an expiring trial blocking deploys." (https://vercel.com/geist/project-banner) "Match `variant` to severity: `error` for critical downtime or payment-blocking states, `warning` for an exceptional state with non-immediate action, `success` for a positive temporary mitigation, `gray` for routine project-wide notices." (https://vercel.com/geist/project-banner) Ours: a condition of the page\'s own object that the page itself resolves, with that resolving action on the banner: a past-due subscription on its billing page, a failed refresh of the page\'s own reading. The `tone` is the `variant`: `destructive` is `error`, `warning` is `warning`, `info` is `success` and `neutral` is `gray`. One sits above the page\'s content, under its title.',
1321
+ instead: '"Pick `Note` for inline contextual messages tied to a single field or card, `Toast` for transient acknowledgments, `Modal` for confirmations." (https://vercel.com/geist/project-banner) Ours: there is no announcement strip. `Note` is `MessageNotice` (`placement="section"`), `Toast` is `showMessageToast` and `Modal` is `ConfirmDialog`. For a measured level use a `KpiCard` with its tone; crossing a threshold is a notification in the inbox, not a banner. A condition of the system, the setup or the staff (a connection, a stored secret, a failing job, anything a staff member fixes in settings) is a notification in the product\'s inbox, never a banner on a page: "The Vercel dashboard displays a popover, which contains all relevant notifications" (https://vercel.com/docs/notifications), and the Portal\'s System health raises it through Knock to the bell at the sidebar\'s foot.',
1322
+ behaviour: '"Project Banner is non-dismissible by design. If the message can be dismissed without resolving the underlying state, it isn’t banner-worthy; move it to a `Note`." (https://vercel.com/geist/project-banner) "Show one Project Banner at a time. Stacking competing banners drowns the most urgent state." (https://vercel.com/geist/project-banner) "Always pass a `callToAction` that resolves the state. A banner with no route is a dead end." (https://vercel.com/geist/project-banner) Ours: a strip as wide as its container: a filled ground with a line on its top and bottom edge only, no corners, 40 px high and centred, with the mark, the sentence and the fix on one row; under 768 px the fix drops under the sentence, aligned with its text. The title and the description run on as one passage: the title closed by a full stop when it has none of its own, leading in semibold when a description follows and at regular weight alone, the description after it on the same line, and a description that repeats the title not shown. `detail` takes its own row under the passage. Colour sits on the whole strip (ground, both lines, mark and text); the fix is an underlined link in the darkest step of the same hue (neutral: `text-foreground`) and returns to the strip\'s text on hover. Neutral and info take the Info mark, warning and destructive the TriangleAlert. `action` is the `callToAction`: one command, never two, never a ready-made button, and there is no close button. Required: `action`, `title`, `tone` and `liveRegion`; the mark is always drawn, and `icon` swaps the tone\'s mark for another lucide icon.',
1323
+ words: '"`label` is one sentence in sentence case that names the impact: `Your Pro trial expires in 3 days.` Don’t open with `Heads up` or apologetic preambles." (https://vercel.com/geist/project-banner) "`callToAction.label` is Title Case `Verb + Noun` and points at the resolver: `Update Payment Method`, `Reactivate Project`, `Review Tokens`." (https://vercel.com/geist/project-banner) "Name the affected entity when the project context isn’t obvious from the surrounding chrome (`Production deployments are paused on my-project`)." (https://vercel.com/geist/project-banner) "Don’t encode severity in the copy with emoji or interjections; the variant carries that signal." (https://vercel.com/geist/project-banner) "`NoteLabel` is a 1–2 word Title Case prefix that names the topic: `Region Change`, `Rate Limit`, `Plan Limit`. Cut hedges like `Heads Up`, `FYI`, and `Note`." (https://vercel.com/geist/note) Ours: the `title` is the `label`: the one sentence, naming the condition, its impact and the object. A `description` only adds what the title leaves out, in one more sentence. Our `label` is the `NoteLabel` topic prefix. The action is sentence case, verb and noun, pointing at the resolver: "Update payment method", "Try again". No "Please". The words come from the product\'s catalogue code.',
1324
+ access: '`liveRegion="assertive"` gives `role="alert"`, for a failure the person just caused; `"polite"` gives `role="status"`, announced once when it appears; `"off"` gives `role="note"` for one already there at load. The mark, given or not, is hidden.',
1325
1325
  },
1326
1326
  {
1327
1327
  name: "PendingLabel",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nadicodeai/ui",
3
- "version": "12.1.147",
3
+ "version": "12.1.149",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -84,7 +84,7 @@
84
84
  "@base-ui/react": "^1.9.0",
85
85
  "@iconify-icons/logos": "^2.0.3",
86
86
  "@iconify/react": "^6.0.2",
87
- "@nadicodeai/design-system": "12.1.147",
87
+ "@nadicodeai/design-system": "12.1.149",
88
88
  "@tanstack/react-table": "^9.3.1",
89
89
  "@vgpu/wgsl": "0.6.0",
90
90
  "class-variance-authority": "^0.7.1",