@nadicodeai/ui 12.1.146 → 12.1.148
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -1
- package/dist/components/chart-bar-horizontal.d.ts +1 -1
- package/dist/components/chart-bar-horizontal.d.ts.map +1 -1
- package/dist/components/chart-bar-horizontal.js +8 -6
- package/dist/components/record-time.d.ts +13 -3
- package/dist/components/record-time.d.ts.map +1 -1
- package/dist/components/record-time.js +38 -6
- package/dist/showcase/usage.generated.js +2 -2
- package/package.json +5 -2
package/AGENTS.md
CHANGED
|
@@ -45,7 +45,9 @@ and `lint` checks. A component change a person can see goes through
|
|
|
45
45
|
an operating system, a messaging channel or a sign-in provider is its icon in
|
|
46
46
|
Iconify's `logos` set (`platform-mark.tsx`).
|
|
47
47
|
- **Charts** are stock shadcn registry items; the deliberate deviations are held
|
|
48
|
-
by `tests/guards/chart-kit.test.ts
|
|
48
|
+
by `tests/guards/chart-kit.test.ts`, and what only a browser lays out, such as
|
|
49
|
+
a category axis's labels, by the browser suite in `tests/visual/`, which
|
|
50
|
+
post-merge's `test (website, packages)` job runs. A chart the kit lacks is pulled from
|
|
49
51
|
`https://ui.shadcn.com/r/styles/new-york-v4/<name>.json`.
|
|
50
52
|
- **Releases.** No change names a version; the publish picks it after merge:
|
|
51
53
|
`../../docs/agents/publish.md`.
|
|
@@ -11,7 +11,7 @@ declare function signedAxis(data: readonly Record<string, number | string>[], va
|
|
|
11
11
|
* @instead Never encode a loss as a share. An unsigned ranking is the compact `Progress` ranking or a `DataTable`.
|
|
12
12
|
* @behaviour Horizontal bars from a 1.5 px zero line: favourable values to the right in success with right corners rounded, unfavourable to the left in destructive with left corners rounded.
|
|
13
13
|
* @words The money unit in the period or the axis. Losses stay negative, written with a true minus: −€360.
|
|
14
|
-
* @access Each bar's sign is in its position and its tooltip, never only its colour. Category labels are text, readable without hue: the axis is as wide as the longest label up to 128 px, a longer one wraps to a second line
|
|
14
|
+
* @access Each bar's sign is in its position and its tooltip, never only its colour. Category labels are text, readable without hue: the axis is as wide as the longest label up to 128 px, a longer one wraps to a second line where its row holds two lines and is cut with an ellipsis past what its lines hold, whole in the tooltip, so no label runs into its neighbour.
|
|
15
15
|
*/
|
|
16
16
|
declare function ChartBarHorizontal({ title, period, controls, link, summary, aside, footer, headingLevel, data, xKey, valueKey, displayKey, categoryDisplayKey, positiveLabel, negativeLabel, valueFormatter, axisFormatter, emptyState, size, className, }: Omit<ChartCardProps, "series" | "entityColorMap" | "otherLabel"> & {
|
|
17
17
|
valueKey: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chart-bar-horizontal.d.ts","sourceRoot":"","sources":["../../src/components/chart-bar-horizontal.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"chart-bar-horizontal.d.ts","sourceRoot":"","sources":["../../src/components/chart-bar-horizontal.tsx"],"names":[],"mappings":"AAsBA,OAAO,EAKL,KAAK,cAAc,EACpB,MAAM,yBAAyB,CAAC;AAoCjC,iBAAS,UAAU,CAAC,IAAI,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,MAAM;;;EAqCrF;AAED;;;;;;;;;GASG;AACH,iBAAS,kBAAkB,CAAC,EAC1B,KAAK,EACL,MAAM,EACN,QAAQ,EACR,IAAI,EACJ,OAAO,EACP,KAAK,EACL,MAAM,EACN,YAAY,EACZ,IAAI,EACJ,IAAI,EACJ,QAAQ,EACR,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,aAAa,EACb,cAAsC,EACtC,aAAa,EACb,UAAU,EACV,IAAgB,EAChB,SAAS,GACV,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,gBAAgB,GAAG,YAAY,CAAC,GAAG;IACpE,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB,+BA+GA;AAED,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,CAAC"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Pulled from the shadcn registry item `chart-bar-horizontal`.
|
|
2
2
|
"use client";
|
|
3
3
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
4
|
-
import { Bar, BarChart, CartesianGrid, ReferenceLine, Text, XAxis, YAxis } from "recharts";
|
|
4
|
+
import { Bar, BarChart, CartesianGrid, ReferenceLine, Text, XAxis, YAxis, } from "recharts";
|
|
5
5
|
import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, } from "./chart.js";
|
|
6
6
|
import { niceAxisMax, niceAxisTicks, niceStep } from "./chart-axis.js";
|
|
7
7
|
import { CHART_LEGEND_CLASS, ChartCard, chartPlotClassName, defaultValueFormatter, } from "./chart-card.js";
|
|
@@ -9,18 +9,20 @@ const POSITIVE_KEY = "__positive";
|
|
|
9
9
|
const NEGATIVE_KEY = "__negative";
|
|
10
10
|
const POSITIVE_BAR_COLOR = "var(--success)";
|
|
11
11
|
const NEGATIVE_BAR_COLOR = "var(--destructive)";
|
|
12
|
+
const TWO_LINE_ROW_HEIGHT = 28;
|
|
12
13
|
/**
|
|
13
14
|
* Recharts keeps only SVG attributes from a `tick` object, so `maxLines` reaches
|
|
14
|
-
* its `Text` only through a tick it renders
|
|
15
|
+
* its `Text` only through a tick it renders.
|
|
15
16
|
*/
|
|
16
17
|
function CategoryTick({ payload, ...props }) {
|
|
17
|
-
|
|
18
|
+
const rowHeight = Number(props.height) / props.visibleTicksCount;
|
|
19
|
+
return (_jsx(Text, { ...props, maxLines: rowHeight >= TWO_LINE_ROW_HEIGHT ? 2 : 1, children: payload.value }));
|
|
18
20
|
}
|
|
19
|
-
/** About one 12 px character, and the bounds that keep a name
|
|
21
|
+
/** About one 12 px character, and the bounds that keep a long name readable without starving the bars. */
|
|
20
22
|
const CATEGORY_CHAR_WIDTH = 6.6;
|
|
21
23
|
const CATEGORY_AXIS_MIN = 64;
|
|
22
24
|
const CATEGORY_AXIS_MAX = 128;
|
|
23
|
-
/** The category axis as wide as its longest name up to the bound; a longer name wraps
|
|
25
|
+
/** The category axis as wide as its longest name up to the bound; a longer name wraps or is cut, as `CategoryTick` decides. */
|
|
24
26
|
function categoryAxisWidth(data, xKey) {
|
|
25
27
|
const longest = data.reduce((most, datum) => Math.max(most, String(datum[xKey] ?? "").length), 0);
|
|
26
28
|
return Math.min(CATEGORY_AXIS_MAX, Math.max(CATEGORY_AXIS_MIN, Math.ceil(longest * CATEGORY_CHAR_WIDTH) + 16));
|
|
@@ -62,7 +64,7 @@ function signedAxis(data, valueKey) {
|
|
|
62
64
|
* @instead Never encode a loss as a share. An unsigned ranking is the compact `Progress` ranking or a `DataTable`.
|
|
63
65
|
* @behaviour Horizontal bars from a 1.5 px zero line: favourable values to the right in success with right corners rounded, unfavourable to the left in destructive with left corners rounded.
|
|
64
66
|
* @words The money unit in the period or the axis. Losses stay negative, written with a true minus: −€360.
|
|
65
|
-
* @access Each bar's sign is in its position and its tooltip, never only its colour. Category labels are text, readable without hue: the axis is as wide as the longest label up to 128 px, a longer one wraps to a second line
|
|
67
|
+
* @access Each bar's sign is in its position and its tooltip, never only its colour. Category labels are text, readable without hue: the axis is as wide as the longest label up to 128 px, a longer one wraps to a second line where its row holds two lines and is cut with an ellipsis past what its lines hold, whole in the tooltip, so no label runs into its neighbour.
|
|
66
68
|
*/
|
|
67
69
|
function ChartBarHorizontal({ title, period, controls, link, summary, aside, footer, headingLevel, data, xKey, valueKey, displayKey, categoryDisplayKey, positiveLabel, negativeLabel, valueFormatter = defaultValueFormatter, axisFormatter, emptyState, size = "default", className, }) {
|
|
68
70
|
const axis = signedAxis(data, valueKey);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
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,
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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(
|
|
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:
|
|
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
|
}
|
|
@@ -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
|
},
|
|
@@ -1574,7 +1574,7 @@ export const USAGE = [
|
|
|
1574
1574
|
instead: "Never encode a loss as a share. An unsigned ranking is the compact `Progress` ranking or a `DataTable`.",
|
|
1575
1575
|
behaviour: "Horizontal bars from a 1.5 px zero line: favourable values to the right in success with right corners rounded, unfavourable to the left in destructive with left corners rounded.",
|
|
1576
1576
|
words: "The money unit in the period or the axis. Losses stay negative, written with a true minus: −€360.",
|
|
1577
|
-
access: "Each bar's sign is in its position and its tooltip, never only its colour. Category labels are text, readable without hue: the axis is as wide as the longest label up to 128 px, a longer one wraps to a second line
|
|
1577
|
+
access: "Each bar's sign is in its position and its tooltip, never only its colour. Category labels are text, readable without hue: the axis is as wide as the longest label up to 128 px, a longer one wraps to a second line where its row holds two lines and is cut with an ellipsis past what its lines hold, whole in the tooltip, so no label runs into its neighbour.",
|
|
1578
1578
|
},
|
|
1579
1579
|
{
|
|
1580
1580
|
name: "ChartBarMultiple",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nadicodeai/ui",
|
|
3
|
-
"version": "12.1.
|
|
3
|
+
"version": "12.1.148",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"check:static": "npm run check",
|
|
75
75
|
"pretest": "npm run build",
|
|
76
76
|
"test": "node ../../tools/workspace/heavy-slots.mjs --skip-if-filtered -- vitest run",
|
|
77
|
+
"test:visual": "node ../../tools/workspace/heavy-slots.mjs -- playwright test --config playwright.visual.config.ts",
|
|
77
78
|
"check:export-targets": "node ../../tools/release/check-export-targets.mjs",
|
|
78
79
|
"test:distribution": "node ../../tools/release/package-smoke-test.mjs"
|
|
79
80
|
},
|
|
@@ -83,7 +84,7 @@
|
|
|
83
84
|
"@base-ui/react": "^1.9.0",
|
|
84
85
|
"@iconify-icons/logos": "^2.0.3",
|
|
85
86
|
"@iconify/react": "^6.0.2",
|
|
86
|
-
"@nadicodeai/design-system": "12.1.
|
|
87
|
+
"@nadicodeai/design-system": "12.1.148",
|
|
87
88
|
"@tanstack/react-table": "^9.3.1",
|
|
88
89
|
"@vgpu/wgsl": "0.6.0",
|
|
89
90
|
"class-variance-authority": "^0.7.1",
|
|
@@ -118,7 +119,9 @@
|
|
|
118
119
|
"@nadicodeai/eslint-config": "*",
|
|
119
120
|
"@nadicodeai/eslint-plugin": "*",
|
|
120
121
|
"@nadicodeai/oxlint-config": "*",
|
|
122
|
+
"@nadicodeai/static-design-server": "*",
|
|
121
123
|
"@nadicodeai/test-support": "*",
|
|
124
|
+
"@playwright/test": "^1.64.1",
|
|
122
125
|
"@stryker-mutator/core": "10.0.0",
|
|
123
126
|
"@stryker-mutator/vitest-runner": "10.0.0",
|
|
124
127
|
"@tailwindcss/postcss": "^4.3.3",
|