@vegastack/design 0.7.98 → 0.7.100
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/bin/verify-registry-item.mjs +24 -0
- package/dist/{chunk-4YF3V35A.js → chunk-R6IZOGT5.js} +24 -6
- package/dist/create-animated-icon.js +1 -1
- package/dist/icons/index.js +1 -1
- package/dist/index.cjs +24 -6
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/skills/vegastack-consume/SKILL.md +52 -50
- package/skills/vegastack-design-audit/SKILL.md +1 -1
- package/skills/vegastack-design-system/SKILL.md +14 -12
|
@@ -64,6 +64,7 @@ import {
|
|
|
64
64
|
writeFileSync,
|
|
65
65
|
} from "node:fs";
|
|
66
66
|
import { tmpdir } from "node:os";
|
|
67
|
+
import { parseEnv } from "node:util";
|
|
67
68
|
import { pathToFileURL } from "node:url";
|
|
68
69
|
import { join, resolve, basename, relative, isAbsolute, sep } from "node:path";
|
|
69
70
|
import { createHash } from "node:crypto";
|
|
@@ -71,6 +72,28 @@ import tsconfigPaths from "tsconfig-paths";
|
|
|
71
72
|
|
|
72
73
|
const { loadConfig: loadTsconfigPaths } = tsconfigPaths;
|
|
73
74
|
|
|
75
|
+
/** Load credential values only; checkout-local files never control trust anchors. */
|
|
76
|
+
export function loadRegistryCredentials(cwd, env = process.env) {
|
|
77
|
+
const loaded = {};
|
|
78
|
+
for (const name of [".env", ".env.local"]) {
|
|
79
|
+
let contents;
|
|
80
|
+
try {
|
|
81
|
+
contents = readFileSync(join(cwd, name), "utf8");
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error.code === "ENOENT") continue;
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
const values = parseEnv(contents);
|
|
87
|
+
for (const key of ["CF_ACCESS_CLIENT_ID", "CF_ACCESS_CLIENT_SECRET"]) {
|
|
88
|
+
if (values[key] !== undefined) loaded[key] = values[key];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const [key, value] of Object.entries(loaded)) {
|
|
92
|
+
if (env[key] === undefined) env[key] = value;
|
|
93
|
+
}
|
|
94
|
+
return env;
|
|
95
|
+
}
|
|
96
|
+
|
|
74
97
|
// ── canonical item hash (INLINED from tooling/registry-hash.mjs — keep IDENTICAL) ──
|
|
75
98
|
// Covers the WHOLE item (not just file content); meta.integrity is excluded so the hash
|
|
76
99
|
// is self-describing and stable. The provenance header (`// @vegastack <name>@<version>
|
|
@@ -947,6 +970,7 @@ if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
|
|
947
970
|
/\/$/,
|
|
948
971
|
"",
|
|
949
972
|
);
|
|
973
|
+
loadRegistryCredentials(process.cwd());
|
|
950
974
|
const headers = requestHeaders();
|
|
951
975
|
|
|
952
976
|
const tempDir = mkdtempSync(join(tmpdir(), "vegastack-verify-"));
|
|
@@ -134,14 +134,32 @@ var FLOATING = {
|
|
|
134
134
|
};
|
|
135
135
|
function mergeRefs(...refs) {
|
|
136
136
|
return (node) => {
|
|
137
|
-
|
|
138
|
-
if (ref == null)
|
|
137
|
+
const cleanups = refs.map((ref) => {
|
|
138
|
+
if (ref == null) return void 0;
|
|
139
139
|
if (typeof ref === "function") {
|
|
140
|
-
ref(node);
|
|
141
|
-
|
|
142
|
-
ref.current = node;
|
|
140
|
+
const cleanup = ref(node);
|
|
141
|
+
return typeof cleanup === "function" ? cleanup : () => ref(null);
|
|
143
142
|
}
|
|
144
|
-
|
|
143
|
+
ref.current = node;
|
|
144
|
+
return () => {
|
|
145
|
+
ref.current = null;
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
let cleaned = false;
|
|
149
|
+
return () => {
|
|
150
|
+
if (cleaned) return;
|
|
151
|
+
cleaned = true;
|
|
152
|
+
const errors = [];
|
|
153
|
+
for (const cleanup of cleanups) {
|
|
154
|
+
try {
|
|
155
|
+
cleanup?.();
|
|
156
|
+
} catch (error) {
|
|
157
|
+
errors.push(error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (errors.length)
|
|
161
|
+
throw new AggregateError(errors, "Merged ref cleanup failed");
|
|
162
|
+
};
|
|
145
163
|
};
|
|
146
164
|
}
|
|
147
165
|
|
package/dist/icons/index.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -163,14 +163,32 @@ var FLOATING = {
|
|
|
163
163
|
};
|
|
164
164
|
function mergeRefs(...refs) {
|
|
165
165
|
return (node) => {
|
|
166
|
-
|
|
167
|
-
if (ref == null)
|
|
166
|
+
const cleanups = refs.map((ref) => {
|
|
167
|
+
if (ref == null) return void 0;
|
|
168
168
|
if (typeof ref === "function") {
|
|
169
|
-
ref(node);
|
|
170
|
-
|
|
171
|
-
ref.current = node;
|
|
169
|
+
const cleanup = ref(node);
|
|
170
|
+
return typeof cleanup === "function" ? cleanup : () => ref(null);
|
|
172
171
|
}
|
|
173
|
-
|
|
172
|
+
ref.current = node;
|
|
173
|
+
return () => {
|
|
174
|
+
ref.current = null;
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
let cleaned = false;
|
|
178
|
+
return () => {
|
|
179
|
+
if (cleaned) return;
|
|
180
|
+
cleaned = true;
|
|
181
|
+
const errors = [];
|
|
182
|
+
for (const cleanup of cleanups) {
|
|
183
|
+
try {
|
|
184
|
+
cleanup?.();
|
|
185
|
+
} catch (error) {
|
|
186
|
+
errors.push(error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (errors.length)
|
|
190
|
+
throw new AggregateError(errors, "Merged ref cleanup failed");
|
|
191
|
+
};
|
|
174
192
|
};
|
|
175
193
|
}
|
|
176
194
|
// Annotate the CommonJS export names for ESM import in node:
|
package/dist/index.d.cts
CHANGED
|
@@ -193,7 +193,7 @@ declare const FLOATING: {
|
|
|
193
193
|
* (only ref objects the caller already holds), so it is server-safe like `cn`.
|
|
194
194
|
*
|
|
195
195
|
* **Not memoized.** Calling it produces a NEW function every time, and React detaches a
|
|
196
|
-
* changed ref callback (
|
|
196
|
+
* changed ref callback (runs its cleanup; legacy callbacks receive `null`) and reattaches it on every render. Wrap the
|
|
197
197
|
* CALL at the call site when the inputs are stable:
|
|
198
198
|
*
|
|
199
199
|
* @example
|
package/dist/index.d.ts
CHANGED
|
@@ -193,7 +193,7 @@ declare const FLOATING: {
|
|
|
193
193
|
* (only ref objects the caller already holds), so it is server-safe like `cn`.
|
|
194
194
|
*
|
|
195
195
|
* **Not memoized.** Calling it produces a NEW function every time, and React detaches a
|
|
196
|
-
* changed ref callback (
|
|
196
|
+
* changed ref callback (runs its cleanup; legacy callbacks receive `null`) and reattaches it on every render. Wrap the
|
|
197
197
|
* CALL at the call site when the inputs are stable:
|
|
198
198
|
*
|
|
199
199
|
* @example
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vegastack/design",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.100",
|
|
4
4
|
"description": "VegaStack design system — cn utility, icon runtime, Tailwind v4 preset, and the vegastack-design CLI (tokens ship separately as @vegastack/design-tokens)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"lint": "eslint . && node ../../tooling/design-lint.mjs src && pnpm run verify",
|
|
68
68
|
"verify": "node ../../tooling/verify-preset-source.mjs",
|
|
69
69
|
"typecheck": "tsc --noEmit",
|
|
70
|
-
"test": "node test/compare.test.mjs && node test/check-updates.test.mjs && node test/skills-install.test.mjs && node test/doctor.test.mjs"
|
|
70
|
+
"test": "node test/compare.test.mjs && node test/check-updates.test.mjs && node test/skills-install.test.mjs && node test/doctor.test.mjs && node --test test/merge-refs.test.mjs test/registry-credentials.test.mjs"
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
73
|
"@vegastack/design-tokens": "^0.7.80",
|
|
@@ -18,15 +18,16 @@ the icon runtime, the Tailwind v4 preset, and the `vegastack-design` CLI.
|
|
|
18
18
|
|
|
19
19
|
**Invoking the CLI.** The bin is `vegastack-design` but the package is `@vegastack/design`, so a
|
|
20
20
|
bare `npx vegastack-design …` in a project that does not already have it installed would try to
|
|
21
|
-
fetch an unrelated, unscoped package from npm. Use
|
|
22
|
-
|
|
23
|
-
standalone one-off:
|
|
21
|
+
fetch an unrelated, unscoped package from npm. Use the installed CLI so the project's lockfile
|
|
22
|
+
selects its approved version:
|
|
24
23
|
|
|
25
24
|
```bash
|
|
26
25
|
pnpm exec vegastack-design <command>
|
|
27
|
-
npx --package=@vegastack/design vegastack-design <command>
|
|
28
26
|
```
|
|
29
27
|
|
|
28
|
+
A standalone invocation must explicitly pin an approved `@vegastack/design` version; do not
|
|
29
|
+
resolve a different verifier version during the preflight/install/post-write sequence.
|
|
30
|
+
|
|
30
31
|
`@vegastack/ui` is private and is never installed downstream — components arrive by copy-in.
|
|
31
32
|
|
|
32
33
|
## 2. Tailwind v4 CSS
|
|
@@ -52,12 +53,51 @@ inside the published builds — the icon runtime, and the compiled Toaster (its
|
|
|
52
53
|
and status-tint classes live in `@vegastack/ui/dist`, and Tailwind has to be told to generate them).
|
|
53
54
|
Your own application and component source is scanned by Tailwind as usual.
|
|
54
55
|
|
|
55
|
-
## 3.
|
|
56
|
+
## 3. Configure registry access
|
|
57
|
+
|
|
58
|
+
Components come from a private registry behind Cloudflare Access service tokens.
|
|
59
|
+
|
|
60
|
+
If the project has no `components.json` yet, create one first — pick the **base** style, never
|
|
61
|
+
`radix`, because VegaStack components are Base UI:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pnpm dlx shadcn@4.21.0 init --base base
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Then add the registry block to `components.json`:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"registries": {
|
|
72
|
+
"@vegastack": {
|
|
73
|
+
"url": "https://design.vegastack.com/r/{name}.json",
|
|
74
|
+
"headers": {
|
|
75
|
+
"CF-Access-Client-Id": "${CF_ACCESS_CLIENT_ID}",
|
|
76
|
+
"CF-Access-Client-Secret": "${CF_ACCESS_CLIENT_SECRET}"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Store credentials in your secret manager or ignored `.env.local`. The installed verifier loads
|
|
84
|
+
only `CF_ACCESS_CLIENT_ID` and `CF_ACCESS_CLIENT_SECRET` from `.env` then `.env.local`; an existing
|
|
85
|
+
process value wins. Registry origin and signer overrides are accepted only from the operator's
|
|
86
|
+
process environment, never a checkout-local dotenv file. Do not source the whole dotenv file.
|
|
87
|
+
|
|
88
|
+
Configure this registry before the first provider/component add. Inspect before writing anything:
|
|
56
89
|
|
|
57
90
|
```bash
|
|
58
|
-
|
|
91
|
+
pnpm dlx shadcn@4.21.0 add @vegastack/button --dry-run
|
|
59
92
|
```
|
|
60
93
|
|
|
94
|
+
External and client projects are **tokenless** — components are copied in during development, and
|
|
95
|
+
the shipped application holds zero VegaStack credentials.
|
|
96
|
+
|
|
97
|
+
## 4. Wrap the app root in the provider
|
|
98
|
+
|
|
99
|
+
Install `provider` using the complete fail-closed procedure in step 5 before adding this wrapper.
|
|
100
|
+
|
|
61
101
|
This copies `VegaStackProvider` and `useVegaStackTheme` into your project, composing the `toast`
|
|
62
102
|
Toaster item.
|
|
63
103
|
|
|
@@ -80,8 +120,7 @@ putting `className="isolate"` on your `<body>` yourself. Without it, popups can
|
|
|
80
120
|
chrome or mis-position in stacking-context-heavy layouts.
|
|
81
121
|
|
|
82
122
|
**Stop iOS focus zoom.** iOS Safari zooms into any focused field under 16px. `base.css` renders
|
|
83
|
-
text-entry controls at 16px on touch pointers
|
|
84
|
-
stops focus zoom while pinch-zoom keeps working on iOS. In Next.js, in the root layout:
|
|
123
|
+
text-entry controls at 16px on touch pointers. Keep zoom unrestricted; do not add a maximum-scale cap. In Next.js, in the root layout:
|
|
85
124
|
|
|
86
125
|
```tsx
|
|
87
126
|
import type { Viewport } from "next";
|
|
@@ -89,46 +128,9 @@ import type { Viewport } from "next";
|
|
|
89
128
|
export const viewport: Viewport = {
|
|
90
129
|
width: "device-width",
|
|
91
130
|
initialScale: 1,
|
|
92
|
-
maximumScale: 1,
|
|
93
131
|
};
|
|
94
132
|
```
|
|
95
133
|
|
|
96
|
-
## 4. Configure registry access
|
|
97
|
-
|
|
98
|
-
Components come from a private registry behind Cloudflare Access service tokens.
|
|
99
|
-
|
|
100
|
-
If the project has no `components.json` yet, create one first — pick the **base** style, never
|
|
101
|
-
`radix`, because VegaStack components are Base UI:
|
|
102
|
-
|
|
103
|
-
```bash
|
|
104
|
-
pnpm dlx shadcn@latest init --base base
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
Then add the registry block to `components.json`:
|
|
108
|
-
|
|
109
|
-
```json
|
|
110
|
-
{
|
|
111
|
-
"registries": {
|
|
112
|
-
"@vegastack": {
|
|
113
|
-
"url": "https://design.vegastack.com/r/{name}.json",
|
|
114
|
-
"headers": {
|
|
115
|
-
"CF-Access-Client-Id": "${CF_ACCESS_CLIENT_ID}",
|
|
116
|
-
"CF-Access-Client-Secret": "${CF_ACCESS_CLIENT_SECRET}"
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
Put the credentials in `.env.local`. Inspect before writing anything:
|
|
124
|
-
|
|
125
|
-
```bash
|
|
126
|
-
pnpm dlx shadcn@latest add @vegastack/button --dry-run
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
External and client projects are **tokenless** — components are copied in during development, and
|
|
130
|
-
the shipped application holds zero VegaStack credentials.
|
|
131
|
-
|
|
132
134
|
## 5. Add a component (fail-closed, three steps)
|
|
133
135
|
|
|
134
136
|
This is the only safe way to add a component. Do not collapse it to a bare `shadcn add`: that leaves
|
|
@@ -140,16 +142,16 @@ a time-of-check/time-of-use gap, because `shadcn add` re-fetches the item after
|
|
|
140
142
|
DIR="$(mktemp -d "${TMPDIR:-/tmp}/vegastack-verify.XXXXXX")"; ITEM="$DIR/button.json"
|
|
141
143
|
|
|
142
144
|
# 1) PRE-WRITE — verify signature + hash, and save the trusted bytes for step 3.
|
|
143
|
-
|
|
145
|
+
pnpm exec vegastack-design verify --save "$ITEM" button
|
|
144
146
|
|
|
145
147
|
# Retain the verified digest in the parent shell BEFORE shadcn or dependency code runs.
|
|
146
148
|
EXPECTED="$(node -e 'process.stdout.write(JSON.parse(require("node:fs").readFileSync(process.argv[1],"utf8")).meta.integrity)' "$ITEM")"
|
|
147
149
|
|
|
148
150
|
# 2) COPY-IN — shadcn writes the files, rewriting import aliases per your components.json.
|
|
149
|
-
pnpm dlx shadcn@
|
|
151
|
+
pnpm dlx shadcn@4.21.0 add @vegastack/button
|
|
150
152
|
|
|
151
153
|
# 3) POST-WRITE — prove the copied files match the SAVED item. Exits 1 on any tampering.
|
|
152
|
-
|
|
154
|
+
pnpm exec vegastack-design verify \
|
|
153
155
|
--post-write --item "$ITEM" --expected-integrity "$EXPECTED" --target-dir .
|
|
154
156
|
```
|
|
155
157
|
|
|
@@ -176,7 +178,7 @@ Never override a `--color-*` variable — that is the build-inlined bridge, not
|
|
|
176
178
|
Copy-in means no automatic updates.
|
|
177
179
|
|
|
178
180
|
```bash
|
|
179
|
-
|
|
181
|
+
pnpm exec vegastack-design check-updates # ⬆ update · ≈ drift · ✓ up to date
|
|
180
182
|
```
|
|
181
183
|
|
|
182
184
|
Then per stale component: `shadcn add @vegastack/<name> --diff` to review, `--overwrite` to apply,
|
|
@@ -190,7 +192,7 @@ Add the Renovate preset `github>VegaStack/renovate-config` so additive token bum
|
|
|
190
192
|
If this project's agents do not already have them:
|
|
191
193
|
|
|
192
194
|
```bash
|
|
193
|
-
|
|
195
|
+
pnpm exec vegastack-design skills install
|
|
194
196
|
```
|
|
195
197
|
|
|
196
198
|
Writes the public VegaStack skills into `.claude/skills/` and `.agents/skills/`.
|
|
@@ -83,7 +83,7 @@ rg -n '(focus|focus-visible|focus-within|data-focused|popup-open)[^ "]*:border-(
|
|
|
83
83
|
- **a focus border** — a border colour under a focus or open-popup variant (`focus:border-ring/70`,
|
|
84
84
|
`focus-within:border-ring`, `data-popup-open:border-input`, `not-focus:aria-invalid:…`) in app
|
|
85
85
|
code. A border never changes colour on focus or while active (FOC-14) — the DS's own text-entry
|
|
86
|
-
components are the one exception (a subtle `border-ring/
|
|
86
|
+
components are the one exception (a subtle `border-ring/50` focus border, no fill) and carry it
|
|
87
87
|
themselves; only an invalid field's destructive border shows, in every state. **error**
|
|
88
88
|
- a status FILL used as a text ink on that family's own tint — `bg-destructive/10 text-destructive`
|
|
89
89
|
measures 3.98:1. The readable half is `text-destructive-text`. **error**
|
|
@@ -79,12 +79,10 @@ link` (upstream's set, verbatim). `destructive` is a soft tint, not a solid red
|
|
|
79
79
|
need a search field (it is the preset `country-select` and `region-select` are built from — reach
|
|
80
80
|
for it before composing `combobox` by hand); **`combobox`** directly only for free text,
|
|
81
81
|
suggestions or multi-select chips.
|
|
82
|
-
- **One view-switch rule.**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
**`tabs`**; moving between URLs is navigation — links, not `tabs` (a route-tabs recipe is
|
|
87
|
-
not shipped yet).
|
|
82
|
+
- **One view-switch rule.** Form choices use `radio-group`; toolbar scope switches use default
|
|
83
|
+
`Tabs`, and Grid | List | Board uses `ViewToggle` (which composes Tabs). `ToggleGroup` is for
|
|
84
|
+
pressed action/value choices. URL navigation uses links and the route-tabs recipe.
|
|
85
|
+
|
|
88
86
|
- **Empty is tiered** — nothing yet, no matches ("Clear filters"), couldn't load (`role="alert"`,
|
|
89
87
|
"Try again"), blocked. Pick the tier from the empty-state foundation
|
|
90
88
|
(<https://design.vegastack.com/docs/foundations/empty-states>); never leave a region blank.
|
|
@@ -243,7 +241,7 @@ on shadcn's `neutral` base. Always use the utility, never a raw value.
|
|
|
243
241
|
| Fill | `bg-primary` (solid action, every checked control) · `bg-secondary` (soft) · `bg-muted` (well, track, skeleton) · `bg-accent` (hover) |
|
|
244
242
|
| Text | `text-foreground` · `text-muted-foreground` · `text-{primary,secondary,accent,card,popover}-foreground` |
|
|
245
243
|
| Status | `bg-{destructive,success,warning,info}` · `-foreground` (ink ON the fill) · `-text` (ink on the page or on the family's own tint) |
|
|
246
|
-
| Border | `border-border` · `border-input` — there are no rings; focus is a global background tint (Tabs
|
|
244
|
+
| Border | `border-border` · `border-input` — there are no rings; focus is a global background tint (Tabs included) |
|
|
247
245
|
| Radius | `rounded-{sm,md,lg,xl,2xl}` — all derived from the single `--radius` |
|
|
248
246
|
| Type | Tailwind's own `text-{xs…7xl}`. `text-sm` is 14px, `text-base` is 16px. Line-height and letter-spacing above `text-base` come from the theme — never write `tracking-*`, an arbitrary `text-[13px]`, or `uppercase` |
|
|
249
247
|
| Font | `font-sans` `font-mono` `font-serif` `font-heading` |
|
|
@@ -290,10 +288,10 @@ heading down `n` levels (capped at h6) so the outline never skips back up.
|
|
|
290
288
|
|
|
291
289
|
**Markdown is read with `MarkdownView` and edited with `TextEdit format="markdown"`** — never a
|
|
292
290
|
textarea, a local prose class, or a hand-rolled editor. For in-place editing, swap the view for
|
|
293
|
-
`<TextEdit format="markdown" variant="
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
291
|
+
`<TextEdit format="markdown" variant="document" onCommit={save} />` for inline document editing.
|
|
292
|
+
Use `variant="boxed"` inside forms and `variant="composer"` for submission composers.
|
|
293
|
+
Commit through `onCommit`; `onRevert` observes a revert. The editor owns its contextual toolbar;
|
|
294
|
+
there are no `toolbar`, `onSave` or `onCancel` props.
|
|
297
295
|
|
|
298
296
|
`muted`, `accent` and `secondary` share one value in this base, and all three are kept: name the one
|
|
299
297
|
whose ROLE you mean, so a consumer can retune one without moving the others.
|
|
@@ -357,7 +355,11 @@ contract.
|
|
|
357
355
|
players); schedules → `formatDateRange` / `formatTimeOfDay`; feed/inbox headers → `groupByDay`.
|
|
358
356
|
Pass the viewer's `timeZone` (the `tz` cookie via `getTimeZone(cookie, orgZone)` on the server,
|
|
359
357
|
`useTimeZone()` on the client; render `TimeZoneScript` + `TimeZoneProvider` once at the root).
|
|
360
|
-
|
|
358
|
+
Pass a serialized request timestamp as `TimeZoneProvider.referenceNow`; groups, due badges and
|
|
359
|
+
picker presets read the same live epoch clock via `useDateTimeNow()` from `relative-time`.
|
|
360
|
+
`DateTime` and `DueLabel` share it too; explicit `DateTime.options.now` stays controlled.
|
|
361
|
+
It is `undefined` during SSR without that reference; render affected date content pending rather
|
|
362
|
+
than reading a separate render-time clock. API/JSON output stays ISO.
|
|
361
363
|
|
|
362
364
|
## Do / Don't
|
|
363
365
|
|