@aglyn/aglyn 1.0.0-beta.153 → 1.0.0-beta.155
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/package.json +10 -10
- package/src/lib/app-utils/collection-entries.d.ts +18 -6
- package/src/lib/app-utils/collection-entries.js +14 -14
- package/src/lib/app-utils/collection-entries.js.map +1 -1
- package/src/lib/app-utils/collection-entry-date.d.ts +49 -4
- package/src/lib/app-utils/collection-entry-date.js +80 -11
- package/src/lib/app-utils/collection-entry-date.js.map +1 -1
- package/src/lib/foundation/definitions/org-billing.types.d.ts +18 -0
- package/src/lib/foundation/definitions/org-billing.types.js.map +1 -1
|
@@ -98,14 +98,59 @@ export declare function normalizeCollectionEntryDateFormat(value: unknown): Coll
|
|
|
98
98
|
*/
|
|
99
99
|
export declare const COLLECTION_ENTRY_DATE_LOCALE = "en-US";
|
|
100
100
|
/**
|
|
101
|
-
* The zone
|
|
102
|
-
*
|
|
103
|
-
*
|
|
101
|
+
* The zone a calendar day is read in when a site has not named one.
|
|
102
|
+
*
|
|
103
|
+
* Entry timestamps are absolute instants; the day they are ATTRIBUTED to is
|
|
104
|
+
* an editorial fact about the PUBLISHER, and `timeZone` below is how a site
|
|
105
|
+
* states it. UTC remains the default because it is the answer every existing
|
|
106
|
+
* site is already rendering — a site that never sets a zone must not have its
|
|
107
|
+
* archive move by a day on deploy.
|
|
108
|
+
*
|
|
109
|
+
* ⚑ What AGL-1926 needed was not UTC, it was a zone BOTH SIDES AGREE ON. A
|
|
110
|
+
* per-site zone is still that: it is resolved once, server-side, and travels
|
|
111
|
+
* in the node props, so the client re-render reads the same string. What
|
|
112
|
+
* would reopen the React #418 crash is reading the VISITOR's zone, because
|
|
113
|
+
* only one side of the render knows it.
|
|
104
114
|
*/
|
|
105
115
|
export declare const COLLECTION_ENTRY_DATE_TIME_ZONE = "UTC";
|
|
116
|
+
/**
|
|
117
|
+
* Is this a zone the runtime can actually format in (AGL-3237)?
|
|
118
|
+
*
|
|
119
|
+
* `Intl` is the only authority worth asking — an allowlist of IANA names goes
|
|
120
|
+
* stale, and `toLocaleDateString` throws a RangeError on a name it does not
|
|
121
|
+
* know, which would take down a composing page rather than mis-date one post.
|
|
122
|
+
*/
|
|
123
|
+
export declare function isSupportedTimeZone(value: unknown): boolean;
|
|
124
|
+
/**
|
|
125
|
+
* The zone a site's dates are read in (AGL-3237).
|
|
126
|
+
*
|
|
127
|
+
* The ORGANIZATION names it and every site it owns inherits it: a customer
|
|
128
|
+
* sets it once for the workspace rather than per site. Unset is UTC — the
|
|
129
|
+
* value every existing site already renders, so nothing moves by a day when
|
|
130
|
+
* this ships.
|
|
131
|
+
*
|
|
132
|
+
* Validated on the way out rather than trusted: a stored zone can predate an
|
|
133
|
+
* IANA rename, or be whatever a hand-edited document holds, and a bad one has
|
|
134
|
+
* to fall back rather than throw inside a page render.
|
|
135
|
+
*
|
|
136
|
+
* ⚑ Org only, deliberately. A per-SITE override is a reasonable thing to want
|
|
137
|
+
* — an agency running sites in several regions — but it would make `timeZone`
|
|
138
|
+
* a field of the host document, and the host write-deny guard (AGL-1361)
|
|
139
|
+
* would then require it classified in the host rules too. That is a second
|
|
140
|
+
* rules change, and rules ship by hand rather than with a promotion, so the
|
|
141
|
+
* override is worth its own issue rather than a free ride on this one.
|
|
142
|
+
*/
|
|
143
|
+
export declare function resolveSiteTimeZone(org?: {
|
|
144
|
+
timeZone?: string;
|
|
145
|
+
} | null): string;
|
|
106
146
|
export declare function formatCollectionEntryDate(publishedAt: {
|
|
107
147
|
seconds: number;
|
|
108
|
-
} | null | undefined, format?: CollectionEntryDateFormat, locale?: string
|
|
148
|
+
} | null | undefined, format?: CollectionEntryDateFormat, locale?: string,
|
|
149
|
+
/**
|
|
150
|
+
* The site's zone (AGL-3237) — resolved once per render and passed down, so
|
|
151
|
+
* server and client format the same instant into the same string.
|
|
152
|
+
*/
|
|
153
|
+
timeZoneName?: string): string;
|
|
109
154
|
/**
|
|
110
155
|
* One entry's publish instant for a machine to read: an ISO 8601 date-time in
|
|
111
156
|
* UTC, or `''` for an entry with no date (AGL-2956).
|
|
@@ -113,15 +113,71 @@
|
|
|
113
113
|
* "whatever this runtime happens to be".
|
|
114
114
|
*/ export const COLLECTION_ENTRY_DATE_LOCALE = 'en-US';
|
|
115
115
|
/**
|
|
116
|
-
* The zone
|
|
117
|
-
*
|
|
118
|
-
*
|
|
116
|
+
* The zone a calendar day is read in when a site has not named one.
|
|
117
|
+
*
|
|
118
|
+
* Entry timestamps are absolute instants; the day they are ATTRIBUTED to is
|
|
119
|
+
* an editorial fact about the PUBLISHER, and `timeZone` below is how a site
|
|
120
|
+
* states it. UTC remains the default because it is the answer every existing
|
|
121
|
+
* site is already rendering — a site that never sets a zone must not have its
|
|
122
|
+
* archive move by a day on deploy.
|
|
123
|
+
*
|
|
124
|
+
* ⚑ What AGL-1926 needed was not UTC, it was a zone BOTH SIDES AGREE ON. A
|
|
125
|
+
* per-site zone is still that: it is resolved once, server-side, and travels
|
|
126
|
+
* in the node props, so the client re-render reads the same string. What
|
|
127
|
+
* would reopen the React #418 crash is reading the VISITOR's zone, because
|
|
128
|
+
* only one side of the render knows it.
|
|
119
129
|
*/ export const COLLECTION_ENTRY_DATE_TIME_ZONE = 'UTC';
|
|
120
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Is this a zone the runtime can actually format in (AGL-3237)?
|
|
132
|
+
*
|
|
133
|
+
* `Intl` is the only authority worth asking — an allowlist of IANA names goes
|
|
134
|
+
* stale, and `toLocaleDateString` throws a RangeError on a name it does not
|
|
135
|
+
* know, which would take down a composing page rather than mis-date one post.
|
|
136
|
+
*/ export function isSupportedTimeZone(value) {
|
|
137
|
+
const name = String(value != null ? value : '').trim();
|
|
138
|
+
if (!name) return false;
|
|
139
|
+
try {
|
|
140
|
+
new Intl.DateTimeFormat('en-US', {
|
|
141
|
+
timeZone: name
|
|
142
|
+
});
|
|
143
|
+
return true;
|
|
144
|
+
} catch (unused) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The zone a site's dates are read in (AGL-3237).
|
|
150
|
+
*
|
|
151
|
+
* The ORGANIZATION names it and every site it owns inherits it: a customer
|
|
152
|
+
* sets it once for the workspace rather than per site. Unset is UTC — the
|
|
153
|
+
* value every existing site already renders, so nothing moves by a day when
|
|
154
|
+
* this ships.
|
|
155
|
+
*
|
|
156
|
+
* Validated on the way out rather than trusted: a stored zone can predate an
|
|
157
|
+
* IANA rename, or be whatever a hand-edited document holds, and a bad one has
|
|
158
|
+
* to fall back rather than throw inside a page render.
|
|
159
|
+
*
|
|
160
|
+
* ⚑ Org only, deliberately. A per-SITE override is a reasonable thing to want
|
|
161
|
+
* — an agency running sites in several regions — but it would make `timeZone`
|
|
162
|
+
* a field of the host document, and the host write-deny guard (AGL-1361)
|
|
163
|
+
* would then require it classified in the host rules too. That is a second
|
|
164
|
+
* rules change, and rules ship by hand rather than with a promotion, so the
|
|
165
|
+
* override is worth its own issue rather than a free ride on this one.
|
|
166
|
+
*/ export function resolveSiteTimeZone(org) {
|
|
167
|
+
var _ref;
|
|
168
|
+
const orgZone = String((_ref = org == null ? void 0 : org.timeZone) != null ? _ref : '').trim();
|
|
169
|
+
return isSupportedTimeZone(orgZone) ? orgZone : COLLECTION_ENTRY_DATE_TIME_ZONE;
|
|
170
|
+
}
|
|
171
|
+
export function formatCollectionEntryDate(publishedAt, format, locale = COLLECTION_ENTRY_DATE_LOCALE, /**
|
|
172
|
+
* The site's zone (AGL-3237) — resolved once per render and passed down, so
|
|
173
|
+
* server and client format the same instant into the same string.
|
|
174
|
+
*/ timeZoneName = COLLECTION_ENTRY_DATE_TIME_ZONE) {
|
|
121
175
|
const seconds = publishedAt == null ? void 0 : publishedAt.seconds;
|
|
122
176
|
if (!seconds) return '';
|
|
123
177
|
const date = new Date(seconds * 1000);
|
|
124
|
-
|
|
178
|
+
// An unknown zone formats in UTC rather than throwing: this runs while a
|
|
179
|
+
// page composes, and a RangeError here is a site that does not render.
|
|
180
|
+
const timeZone = isSupportedTimeZone(timeZoneName) ? timeZoneName : COLLECTION_ENTRY_DATE_TIME_ZONE;
|
|
125
181
|
switch(normalizeCollectionEntryDateFormat(format)){
|
|
126
182
|
case 'monthYear':
|
|
127
183
|
return date.toLocaleDateString(locale, {
|
|
@@ -144,12 +200,25 @@ export function formatCollectionEntryDate(publishedAt, format, locale = COLLECTI
|
|
|
144
200
|
timeZone
|
|
145
201
|
});
|
|
146
202
|
case 'iso':
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
203
|
+
{
|
|
204
|
+
// The calendar day IN THE SITE'S ZONE, not `toISOString()`'s slice by
|
|
205
|
+
// luck and not the RUNTIME's local day: `getFullYear`/`getMonth`/
|
|
206
|
+
// `getDate` read the host's zone, so this branch moved an entry by a
|
|
207
|
+
// day depending on who rendered it. Read through the same formatter the
|
|
208
|
+
// branches above use, so all four shapes name one day.
|
|
209
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
210
|
+
timeZone,
|
|
211
|
+
year: 'numeric',
|
|
212
|
+
month: '2-digit',
|
|
213
|
+
day: '2-digit'
|
|
214
|
+
}).formatToParts(date);
|
|
215
|
+
const part = (type)=>{
|
|
216
|
+
var _ref;
|
|
217
|
+
var _parts_find;
|
|
218
|
+
return (_ref = (_parts_find = parts.find((entry)=>entry.type === type)) == null ? void 0 : _parts_find.value) != null ? _ref : '';
|
|
219
|
+
};
|
|
220
|
+
return `${part('year')}-${part('month')}-${part('day')}`;
|
|
221
|
+
}
|
|
153
222
|
default:
|
|
154
223
|
return date.toLocaleDateString(locale, {
|
|
155
224
|
timeZone
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/collection-entry-date.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * How an entry's published date is spelled, and the one function that spells\n * it.\n *\n * Its own module because a published page's client bundle reads\n * `formatCollectionEntryDate` and nothing else here: `collection-entries.ts`\n * carries the whole collection runtime — routing, pagination, category links,\n * the search index and the four block expanders — and a bundler cannot drop\n * the rest of it around a single named import.\n */\n\n/**\n * How an entry's published date reads (AGL-1459).\n *\n * Named shapes rather than a format string, for the same reason the styles\n * panel offers a unit picker rather than a CSS box: an editor choosing a\n * byline is not choosing a grammar, and a free-typed pattern is a field where\n * every typo renders as itself on a published page.\n */\nexport type CollectionEntryDateFormat =\n 'default' | 'monthYear' | 'mediumDate' | 'longDate' | 'iso'\n\n/**\n * \"However the site already renders it\" — a REAL value, never absence\n * (AGL-1459).\n *\n * `''` is the shape AGL-1451/AGL-1453 closed repo-wide: an emptied field\n * cannot survive a save, so an author who tried a format and wanted the\n * original back would have no option to pick. So the do-nothing choice is a\n * word, exactly like `COLLECTION_ALL_PILL_NONE` in `collection-entries.ts`.\n */\nexport const COLLECTION_ENTRY_DATE_FORMAT_DEFAULT: CollectionEntryDateFormat =\n 'default'\n\n/**\n * The offered formats, with the labels an author reads (AGL-1459). Lives here\n * rather than in the block's schema so the list and the formatter cannot\n * drift into offering a shape nothing knows how to produce.\n *\n * Example dates in the labels are written the way the default runtime renders\n * them, which is what makes the choice legible without opening a preview.\n */\nexport const COLLECTION_ENTRY_DATE_FORMAT_OPTIONS: readonly {\n value: CollectionEntryDateFormat\n label: string\n}[] = [\n { value: 'default', label: 'Site default — 8/9/2026' },\n { value: 'monthYear', label: 'Month and year — Aug 2026' },\n { value: 'mediumDate', label: 'Short date — Aug 9, 2026' },\n { value: 'longDate', label: 'Long date — August 9, 2026' },\n { value: 'iso', label: 'ISO — 2026-08-09' },\n]\n\n/** Any stored value read back as a format this module knows (AGL-1459). */\nexport function normalizeCollectionEntryDateFormat(\n value: unknown,\n): CollectionEntryDateFormat {\n const wanted = String(value ?? '').trim()\n const match = COLLECTION_ENTRY_DATE_FORMAT_OPTIONS.find(\n (option) => option.value === wanted,\n )\n return match?.value ?? COLLECTION_ENTRY_DATE_FORMAT_DEFAULT\n}\n\n/**\n * One entry's published date, in the shape the author asked for (AGL-1459).\n *\n * **The formatting lives HERE, beside the timestamp, and deliberately not in\n * the block.** By the time a date reaches the component it is already a\n * formatted string, and re-parsing one is ambiguous by construction: the same\n * `8/9/2026` reads as 9 August under an `en-US` runtime and 8 September under\n * `en-GB`. A byline that silently moves an article by three weeks is far worse\n * than one that is the wrong shape.\n *\n * `default` returns exactly `toLocaleDateString()` — the string this function\n * replaced, character for character — because the block is live on published\n * entries and opening a dropdown must not restyle them.\n *\n * ## Why the runtime gets no say (AGL-1926)\n *\n * Every branch pins BOTH the locale and the time zone, and the answer is a\n * pure function of the timestamp. It used to pass `locale` straight through\n * (normally `undefined`) with no `timeZone` at all, so the output was a\n * function of the RUNTIME: `en-US` + UTC on a Vercel server, the visitor's\n * own locale and zone in their browser. That is fine while only the server\n * ever calls it — the string is stamped into node props at compose time and\n * both sides then agree on it — but it makes the function a hydration\n * mismatch waiting for its first client-side caller, and `catch-all-client`\n * was exactly that caller. A post published at 02:30 UTC is dated the 10th by\n * the server and the 9th by every visitor west of Greenwich; React reports\n * the difference as a text mismatch (the live React #418 on tenant-web,\n * AGL-1926) and then reconciles against a DOM it no longer describes, which\n * is where the `removeChild`/`insertBefore` pair comes from.\n *\n * The pinned values are the ones production already emits, so no published\n * page changes: Vercel runs UTC with an `en-US` ICU default, which is why\n * every entry date served from aglyn.com today reads `8/9/2026`. Pinning\n * makes that byte-for-byte guaranteed instead of a property of the host.\n *\n * `locale` still exists for a caller that must render in a different one; it\n * now defaults to the value the server was picking implicitly rather than to\n * \"whatever this runtime happens to be\".\n */\nexport const COLLECTION_ENTRY_DATE_LOCALE = 'en-US'\n\n/**\n * The zone the calendar day is read in. Entry timestamps are absolute\n * instants; the day they are ATTRIBUTED to has to be one both sides agree on,\n * and UTC is the only zone a server and an unknown visitor share.\n */\nexport const COLLECTION_ENTRY_DATE_TIME_ZONE = 'UTC'\n\nexport function formatCollectionEntryDate(\n publishedAt: { seconds: number } | null | undefined,\n format?: CollectionEntryDateFormat,\n locale: string = COLLECTION_ENTRY_DATE_LOCALE,\n): string {\n const seconds = publishedAt?.seconds\n if (!seconds) return ''\n const date = new Date(seconds * 1000)\n const timeZone = COLLECTION_ENTRY_DATE_TIME_ZONE\n switch (normalizeCollectionEntryDateFormat(format)) {\n case 'monthYear':\n return date.toLocaleDateString(locale, {\n month: 'short',\n year: 'numeric',\n timeZone,\n })\n case 'mediumDate':\n return date.toLocaleDateString(locale, {\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n timeZone,\n })\n case 'longDate':\n return date.toLocaleDateString(locale, {\n month: 'long',\n day: 'numeric',\n year: 'numeric',\n timeZone,\n })\n case 'iso':\n // The calendar day in the pinned zone, not `toISOString()`'s slice by\n // luck and not the RUNTIME's local day: `getFullYear`/`getMonth`/\n // `getDate` read the host's zone, so this branch moved an entry by a\n // day depending on who rendered it. The UTC accessors are the same\n // reading the three `toLocaleDateString` branches above now take.\n return (\n `${date.getUTCFullYear()}-` +\n `${String(date.getUTCMonth() + 1).padStart(2, '0')}-` +\n `${String(date.getUTCDate()).padStart(2, '0')}`\n )\n default:\n return date.toLocaleDateString(locale, { timeZone })\n }\n}\n\n/**\n * One entry's publish instant for a machine to read: an ISO 8601 date-time in\n * UTC, or `''` for an entry with no date (AGL-2956).\n *\n * {@link formatCollectionEntryDate} writes the date a READER sees, and every\n * one of its shapes is ambiguous or incomplete to a parser: `8/9/2026` is a\n * different day in `en-GB`, and even its `iso` shape is a calendar day with no\n * time or zone. A field that has to be parsed — a Video element's publication\n * date, which the page publishes as `VideoObject.uploadDate` — needs the\n * instant itself, spelled by `toISOString()` as `Article.datePublished` is.\n *\n * It reads the same `publishedAt` and treats a missing or zero `seconds` the\n * same way, so an entry is dated here exactly when it is dated there.\n *\n * A `seconds` no `Date` can hold answers `''` rather than throwing:\n * `toISOString` raises a RangeError on an invalid date, and this runs while a\n * page composes.\n */\nexport function collectionEntryPublishedAtIso(\n publishedAt: { seconds: number } | null | undefined,\n): string {\n const seconds = publishedAt?.seconds\n if (!seconds) return ''\n const date = new Date(seconds * 1000)\n return Number.isNaN(date.getTime()) ? '' : date.toISOString()\n}\n"],"names":["COLLECTION_ENTRY_DATE_FORMAT_DEFAULT","COLLECTION_ENTRY_DATE_FORMAT_OPTIONS","value","label","normalizeCollectionEntryDateFormat","wanted","String","trim","match","find","option","COLLECTION_ENTRY_DATE_LOCALE","COLLECTION_ENTRY_DATE_TIME_ZONE","formatCollectionEntryDate","publishedAt","format","locale","seconds","date","Date","timeZone","toLocaleDateString","month","year","day","getUTCFullYear","getUTCMonth","padStart","getUTCDate","collectionEntryPublishedAtIso","Number","isNaN","getTime","toISOString"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;CASC,GAED;;;;;;;CAOC,GAID;;;;;;;;CAQC,GACD,OAAO,MAAMA,uCACX,UAAS;AAEX;;;;;;;CAOC,GACD,OAAO,MAAMC,uCAGP;IACJ;QAAEC,OAAO;QAAWC,OAAO;IAA0B;IACrD;QAAED,OAAO;QAAaC,OAAO;IAA4B;IACzD;QAAED,OAAO;QAAcC,OAAO;IAA2B;IACzD;QAAED,OAAO;QAAYC,OAAO;IAA6B;IACzD;QAAED,OAAO;QAAOC,OAAO;IAAmB;CAC3C,CAAA;AAED,yEAAyE,GACzE,OAAO,SAASC,mCACdF,KAAc;;IAEd,MAAMG,SAASC,OAAOJ,gBAAAA,QAAS,IAAIK,IAAI;IACvC,MAAMC,QAAQP,qCAAqCQ,IAAI,CACrD,CAACC,SAAWA,OAAOR,KAAK,KAAKG;IAE/B,eAAOG,yBAAAA,MAAON,KAAK,mBAAIF;AACzB;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsCC,GACD,OAAO,MAAMW,+BAA+B,QAAO;AAEnD;;;;CAIC,GACD,OAAO,MAAMC,kCAAkC,MAAK;AAEpD,OAAO,SAASC,0BACdC,WAAmD,EACnDC,MAAkC,EAClCC,SAAiBL,4BAA4B;IAE7C,MAAMM,UAAUH,+BAAAA,YAAaG,OAAO;IACpC,IAAI,CAACA,SAAS,OAAO;IACrB,MAAMC,OAAO,IAAIC,KAAKF,UAAU;IAChC,MAAMG,WAAWR;IACjB,OAAQR,mCAAmCW;QACzC,KAAK;YACH,OAAOG,KAAKG,kBAAkB,CAACL,QAAQ;gBACrCM,OAAO;gBACPC,MAAM;gBACNH;YACF;QACF,KAAK;YACH,OAAOF,KAAKG,kBAAkB,CAACL,QAAQ;gBACrCM,OAAO;gBACPE,KAAK;gBACLD,MAAM;gBACNH;YACF;QACF,KAAK;YACH,OAAOF,KAAKG,kBAAkB,CAACL,QAAQ;gBACrCM,OAAO;gBACPE,KAAK;gBACLD,MAAM;gBACNH;YACF;QACF,KAAK;YACH,sEAAsE;YACtE,kEAAkE;YAClE,qEAAqE;YACrE,mEAAmE;YACnE,kEAAkE;YAClE,OACE,GAAGF,KAAKO,cAAc,GAAG,CAAC,CAAC,GAC3B,GAAGnB,OAAOY,KAAKQ,WAAW,KAAK,GAAGC,QAAQ,CAAC,GAAG,KAAK,CAAC,CAAC,GACrD,GAAGrB,OAAOY,KAAKU,UAAU,IAAID,QAAQ,CAAC,GAAG,MAAM;QAEnD;YACE,OAAOT,KAAKG,kBAAkB,CAACL,QAAQ;gBAAEI;YAAS;IACtD;AACF;AAEA;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,SAASS,8BACdf,WAAmD;IAEnD,MAAMG,UAAUH,+BAAAA,YAAaG,OAAO;IACpC,IAAI,CAACA,SAAS,OAAO;IACrB,MAAMC,OAAO,IAAIC,KAAKF,UAAU;IAChC,OAAOa,OAAOC,KAAK,CAACb,KAAKc,OAAO,MAAM,KAAKd,KAAKe,WAAW;AAC7D"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/collection-entry-date.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * How an entry's published date is spelled, and the one function that spells\n * it.\n *\n * Its own module because a published page's client bundle reads\n * `formatCollectionEntryDate` and nothing else here: `collection-entries.ts`\n * carries the whole collection runtime — routing, pagination, category links,\n * the search index and the four block expanders — and a bundler cannot drop\n * the rest of it around a single named import.\n */\n\n/**\n * How an entry's published date reads (AGL-1459).\n *\n * Named shapes rather than a format string, for the same reason the styles\n * panel offers a unit picker rather than a CSS box: an editor choosing a\n * byline is not choosing a grammar, and a free-typed pattern is a field where\n * every typo renders as itself on a published page.\n */\nexport type CollectionEntryDateFormat =\n 'default' | 'monthYear' | 'mediumDate' | 'longDate' | 'iso'\n\n/**\n * \"However the site already renders it\" — a REAL value, never absence\n * (AGL-1459).\n *\n * `''` is the shape AGL-1451/AGL-1453 closed repo-wide: an emptied field\n * cannot survive a save, so an author who tried a format and wanted the\n * original back would have no option to pick. So the do-nothing choice is a\n * word, exactly like `COLLECTION_ALL_PILL_NONE` in `collection-entries.ts`.\n */\nexport const COLLECTION_ENTRY_DATE_FORMAT_DEFAULT: CollectionEntryDateFormat =\n 'default'\n\n/**\n * The offered formats, with the labels an author reads (AGL-1459). Lives here\n * rather than in the block's schema so the list and the formatter cannot\n * drift into offering a shape nothing knows how to produce.\n *\n * Example dates in the labels are written the way the default runtime renders\n * them, which is what makes the choice legible without opening a preview.\n */\nexport const COLLECTION_ENTRY_DATE_FORMAT_OPTIONS: readonly {\n value: CollectionEntryDateFormat\n label: string\n}[] = [\n { value: 'default', label: 'Site default — 8/9/2026' },\n { value: 'monthYear', label: 'Month and year — Aug 2026' },\n { value: 'mediumDate', label: 'Short date — Aug 9, 2026' },\n { value: 'longDate', label: 'Long date — August 9, 2026' },\n { value: 'iso', label: 'ISO — 2026-08-09' },\n]\n\n/** Any stored value read back as a format this module knows (AGL-1459). */\nexport function normalizeCollectionEntryDateFormat(\n value: unknown,\n): CollectionEntryDateFormat {\n const wanted = String(value ?? '').trim()\n const match = COLLECTION_ENTRY_DATE_FORMAT_OPTIONS.find(\n (option) => option.value === wanted,\n )\n return match?.value ?? COLLECTION_ENTRY_DATE_FORMAT_DEFAULT\n}\n\n/**\n * One entry's published date, in the shape the author asked for (AGL-1459).\n *\n * **The formatting lives HERE, beside the timestamp, and deliberately not in\n * the block.** By the time a date reaches the component it is already a\n * formatted string, and re-parsing one is ambiguous by construction: the same\n * `8/9/2026` reads as 9 August under an `en-US` runtime and 8 September under\n * `en-GB`. A byline that silently moves an article by three weeks is far worse\n * than one that is the wrong shape.\n *\n * `default` returns exactly `toLocaleDateString()` — the string this function\n * replaced, character for character — because the block is live on published\n * entries and opening a dropdown must not restyle them.\n *\n * ## Why the runtime gets no say (AGL-1926)\n *\n * Every branch pins BOTH the locale and the time zone, and the answer is a\n * pure function of the timestamp. It used to pass `locale` straight through\n * (normally `undefined`) with no `timeZone` at all, so the output was a\n * function of the RUNTIME: `en-US` + UTC on a Vercel server, the visitor's\n * own locale and zone in their browser. That is fine while only the server\n * ever calls it — the string is stamped into node props at compose time and\n * both sides then agree on it — but it makes the function a hydration\n * mismatch waiting for its first client-side caller, and `catch-all-client`\n * was exactly that caller. A post published at 02:30 UTC is dated the 10th by\n * the server and the 9th by every visitor west of Greenwich; React reports\n * the difference as a text mismatch (the live React #418 on tenant-web,\n * AGL-1926) and then reconciles against a DOM it no longer describes, which\n * is where the `removeChild`/`insertBefore` pair comes from.\n *\n * The pinned values are the ones production already emits, so no published\n * page changes: Vercel runs UTC with an `en-US` ICU default, which is why\n * every entry date served from aglyn.com today reads `8/9/2026`. Pinning\n * makes that byte-for-byte guaranteed instead of a property of the host.\n *\n * `locale` still exists for a caller that must render in a different one; it\n * now defaults to the value the server was picking implicitly rather than to\n * \"whatever this runtime happens to be\".\n */\nexport const COLLECTION_ENTRY_DATE_LOCALE = 'en-US'\n\n/**\n * The zone a calendar day is read in when a site has not named one.\n *\n * Entry timestamps are absolute instants; the day they are ATTRIBUTED to is\n * an editorial fact about the PUBLISHER, and `timeZone` below is how a site\n * states it. UTC remains the default because it is the answer every existing\n * site is already rendering — a site that never sets a zone must not have its\n * archive move by a day on deploy.\n *\n * ⚑ What AGL-1926 needed was not UTC, it was a zone BOTH SIDES AGREE ON. A\n * per-site zone is still that: it is resolved once, server-side, and travels\n * in the node props, so the client re-render reads the same string. What\n * would reopen the React #418 crash is reading the VISITOR's zone, because\n * only one side of the render knows it.\n */\nexport const COLLECTION_ENTRY_DATE_TIME_ZONE = 'UTC'\n\n/**\n * Is this a zone the runtime can actually format in (AGL-3237)?\n *\n * `Intl` is the only authority worth asking — an allowlist of IANA names goes\n * stale, and `toLocaleDateString` throws a RangeError on a name it does not\n * know, which would take down a composing page rather than mis-date one post.\n */\nexport function isSupportedTimeZone(value: unknown): boolean {\n const name = String(value ?? '').trim()\n if (!name) return false\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: name })\n return true\n } catch {\n return false\n }\n}\n\n/**\n * The zone a site's dates are read in (AGL-3237).\n *\n * The ORGANIZATION names it and every site it owns inherits it: a customer\n * sets it once for the workspace rather than per site. Unset is UTC — the\n * value every existing site already renders, so nothing moves by a day when\n * this ships.\n *\n * Validated on the way out rather than trusted: a stored zone can predate an\n * IANA rename, or be whatever a hand-edited document holds, and a bad one has\n * to fall back rather than throw inside a page render.\n *\n * ⚑ Org only, deliberately. A per-SITE override is a reasonable thing to want\n * — an agency running sites in several regions — but it would make `timeZone`\n * a field of the host document, and the host write-deny guard (AGL-1361)\n * would then require it classified in the host rules too. That is a second\n * rules change, and rules ship by hand rather than with a promotion, so the\n * override is worth its own issue rather than a free ride on this one.\n */\nexport function resolveSiteTimeZone(\n org?: { timeZone?: string } | null,\n): string {\n const orgZone = String(org?.timeZone ?? '').trim()\n return isSupportedTimeZone(orgZone) ? orgZone : COLLECTION_ENTRY_DATE_TIME_ZONE\n}\n\nexport function formatCollectionEntryDate(\n publishedAt: { seconds: number } | null | undefined,\n format?: CollectionEntryDateFormat,\n locale: string = COLLECTION_ENTRY_DATE_LOCALE,\n /**\n * The site's zone (AGL-3237) — resolved once per render and passed down, so\n * server and client format the same instant into the same string.\n */\n timeZoneName: string = COLLECTION_ENTRY_DATE_TIME_ZONE,\n): string {\n const seconds = publishedAt?.seconds\n if (!seconds) return ''\n const date = new Date(seconds * 1000)\n // An unknown zone formats in UTC rather than throwing: this runs while a\n // page composes, and a RangeError here is a site that does not render.\n const timeZone = isSupportedTimeZone(timeZoneName)\n ? timeZoneName\n : COLLECTION_ENTRY_DATE_TIME_ZONE\n switch (normalizeCollectionEntryDateFormat(format)) {\n case 'monthYear':\n return date.toLocaleDateString(locale, {\n month: 'short',\n year: 'numeric',\n timeZone,\n })\n case 'mediumDate':\n return date.toLocaleDateString(locale, {\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n timeZone,\n })\n case 'longDate':\n return date.toLocaleDateString(locale, {\n month: 'long',\n day: 'numeric',\n year: 'numeric',\n timeZone,\n })\n case 'iso': {\n // The calendar day IN THE SITE'S ZONE, not `toISOString()`'s slice by\n // luck and not the RUNTIME's local day: `getFullYear`/`getMonth`/\n // `getDate` read the host's zone, so this branch moved an entry by a\n // day depending on who rendered it. Read through the same formatter the\n // branches above use, so all four shapes name one day.\n const parts = new Intl.DateTimeFormat('en-US', {\n timeZone,\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n }).formatToParts(date)\n const part = (type: string) =>\n parts.find((entry) => entry.type === type)?.value ?? ''\n return `${part('year')}-${part('month')}-${part('day')}`\n }\n default:\n return date.toLocaleDateString(locale, { timeZone })\n }\n}\n\n/**\n * One entry's publish instant for a machine to read: an ISO 8601 date-time in\n * UTC, or `''` for an entry with no date (AGL-2956).\n *\n * {@link formatCollectionEntryDate} writes the date a READER sees, and every\n * one of its shapes is ambiguous or incomplete to a parser: `8/9/2026` is a\n * different day in `en-GB`, and even its `iso` shape is a calendar day with no\n * time or zone. A field that has to be parsed — a Video element's publication\n * date, which the page publishes as `VideoObject.uploadDate` — needs the\n * instant itself, spelled by `toISOString()` as `Article.datePublished` is.\n *\n * It reads the same `publishedAt` and treats a missing or zero `seconds` the\n * same way, so an entry is dated here exactly when it is dated there.\n *\n * A `seconds` no `Date` can hold answers `''` rather than throwing:\n * `toISOString` raises a RangeError on an invalid date, and this runs while a\n * page composes.\n */\nexport function collectionEntryPublishedAtIso(\n publishedAt: { seconds: number } | null | undefined,\n): string {\n const seconds = publishedAt?.seconds\n if (!seconds) return ''\n const date = new Date(seconds * 1000)\n return Number.isNaN(date.getTime()) ? '' : date.toISOString()\n}\n"],"names":["COLLECTION_ENTRY_DATE_FORMAT_DEFAULT","COLLECTION_ENTRY_DATE_FORMAT_OPTIONS","value","label","normalizeCollectionEntryDateFormat","wanted","String","trim","match","find","option","COLLECTION_ENTRY_DATE_LOCALE","COLLECTION_ENTRY_DATE_TIME_ZONE","isSupportedTimeZone","name","Intl","DateTimeFormat","timeZone","resolveSiteTimeZone","org","orgZone","formatCollectionEntryDate","publishedAt","format","locale","timeZoneName","seconds","date","Date","toLocaleDateString","month","year","day","parts","formatToParts","part","type","entry","collectionEntryPublishedAtIso","Number","isNaN","getTime","toISOString"],"mappings":"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"}
|
|
@@ -1091,6 +1091,24 @@ export interface AglynOrgBilling extends AglynDocument {
|
|
|
1091
1091
|
name?: string;
|
|
1092
1092
|
/** Free-text workspace description; nothing gates on it. */
|
|
1093
1093
|
description?: string;
|
|
1094
|
+
/**
|
|
1095
|
+
* The IANA zone this workspace's published dates read in (AGL-3237) —
|
|
1096
|
+
* `America/Chicago`, `Europe/Berlin`. Absent is UTC.
|
|
1097
|
+
*
|
|
1098
|
+
* What it decides is which CALENDAR DAY an instant is attributed to on a
|
|
1099
|
+
* published site, which is an editorial fact about the publisher: a post
|
|
1100
|
+
* that went out at 19:30 in Chicago is dated the 21st here and was dated
|
|
1101
|
+
* the 22nd while UTC decided it. Sites inherit it; see
|
|
1102
|
+
* `resolveSiteTimeZone`.
|
|
1103
|
+
*
|
|
1104
|
+
* SERVER-OWNED: written through /api/orgs/settings and denied to the
|
|
1105
|
+
* client SDK, because `resolveSiteTimeZone` reads it in `app-utils` and
|
|
1106
|
+
* the write-deny guard treats a resolver input as server-owned.
|
|
1107
|
+
* Nothing bills, gates or routes on it, and it is validated at every read
|
|
1108
|
+
* through `isSupportedTimeZone`, so an unusable value renders as UTC rather
|
|
1109
|
+
* than throwing inside a page render.
|
|
1110
|
+
*/
|
|
1111
|
+
timeZone?: string;
|
|
1094
1112
|
/** The workspace URL segment, reserved through `orgSlugs/{slug}`. */
|
|
1095
1113
|
slug?: string;
|
|
1096
1114
|
/**
|