Skip to content

Commit 2c5fe70

Browse files
committed
feat(i18n,theming): a form wrote English over every field, and a dark select opened white on white
Two visitor-facing defects and the guards that would have caught them. FORM LABELS. Every label came from a wire name through humanize(), so a Spanish page rendered `Birth date` over a translated city box, and no catalogue keyed on English source text could reach it: that text is itself computed here, so there is no constant to key on. The API now publishes the map at /languages/field-labels, keyed by wire name with enums as {field}.{value}. It is pulled at BUILD time by scripts/sync-field-labels.ts, which `bun run generate` calls right after it refreshes the spec, because they are one fact from one source. Build time, not run time, and the first answer was the other way round. A runtime fetch was argued as decoupling language from the release chain, but this form already consumes version-pinned schema slices baked at build, so there was no decoupling to protect, and on an embed it would paint English and then re-render translated a moment later. The labels ride the locales/{lang}.js payload a translated page already downloads: no extra request, English still ships nothing, works with no network at render time. humanize() stays as the FALLBACK, which is what makes partial locales shippable: an untranslated language, a key a translator declined to guess, a blocked request and a page with no payload are one code path, not four. DARK MODE. `color-scheme` was never declared anywhere in the library, so the browser painted every native control light. A <select> looked right closed and opened a light popup whose <option> rows inherited our near-white --roxy-fg: white on white. Same omission dimmed the date and time pickers, the number spinner, the autofill highlight and the scrollbar. Declared once per theme trigger beside the tokens, because a per-control fix cannot reach the popup at all: the OS renders it outside the DOM. DATES. formatDateGrain passed a bare date straight to new Date(), which the platform reads as UTC midnight and Intl renders in the VIEWER's zone, so everyone west of Greenwich read the previous day. Normalisation moved into resolveDisplayDate and three inline copies went with it. GUARDS. THEMING.md is now asserted against tokens.css (every token documented, none invented, every printed hex correct) and caught three real defects: --roxy-ring and --roxy-accent-ink were documented as hardcoded hex when both DERIVE from the accent, so the doc told a themer to override what already follows their brand. The layout e2e now asserts document-level scroll, and the untranslated-literal ratchet holds every file not in its budget at zero, so a new component cannot start out leaking. RELEASE. scripts/purge-cdn.ts purges both jsDelivr aliases and then POLLS x-jsd-version until every asset serves the published version, as a separate verify-cdn job so a CDN hiccup cannot strand the packages on npm with no tag or release. Publishing is not shipping: 0.27.2 and 0.27.3 sat half-live for twelve hours with npm correct and nothing erroring.
1 parent 832b012 commit 2c5fe70

37 files changed

Lines changed: 3062 additions & 264 deletions

.github/workflows/release.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,32 @@ jobs:
250250
steps:
251251
- id: deployment
252252
uses: actions/deploy-pages@v5
253+
254+
# Publishing to npm is NOT shipping. `@latest` resolves per FILE, so a release can go
255+
# half-live and stay that way silently: on 2026-08-12, twelve hours after 0.27.2 and
256+
# 0.27.3 published, the CDN still served `roxy-ui.js` at 0.27.1 while the locale
257+
# catalogues had already moved, so every hosted embed ran a bundle older than the strings
258+
# it was loading and rendered untranslated in all seven languages. npm looked correct
259+
# throughout and nothing errored. It was a hand-typed runbook step, and it was missed on
260+
# two consecutive releases, so it is automated.
261+
#
262+
# It is a SEPARATE JOB, and deliberately not a step at the end of `release`. The purge can
263+
# only run after `npm publish`, but a CDN hiccup must never strand the release: as a step
264+
# it would fail before "Commit, tag, push", leaving the packages published with no git
265+
# tag, no GitHub Release and no Pages deploy, which is strictly worse than the stale CDN
266+
# it exists to prevent. Behind `needs: release` every one of those has already happened,
267+
# so a failure here means exactly one thing, says so loudly, and is fixed by re-running
268+
# this job alone. The script is idempotent.
269+
verify-cdn:
270+
needs: release
271+
if: needs.release.outputs.version != ''
272+
runs-on: ubuntu-latest
273+
steps:
274+
- uses: actions/checkout@v7
275+
- uses: oven-sh/setup-bun@v2
276+
# No `bun install` and no build. The script talks to the CDN over plain fetch and
277+
# derives its asset list from the built manifest plus the theme filenames, both of
278+
# which are produced by the release job. It reads them from the checkout, so the
279+
# locale and theme lists are whatever that version actually shipped.
280+
- name: Purge jsDelivr and verify the edge actually flipped
281+
run: bun run purge:cdn ${{ needs.release.outputs.version }}

AGENTS.md

Lines changed: 33 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -215,51 +215,6 @@ Several components select a view, mode, or chart layout in addition to `data`. T
215215

216216
The full set: `RoxyNatalChart` `houseSystem`, `RoxyHoroscopeCard` `period`, `RoxyMoonPhase` `mode`, `RoxyCompatibilityCard` `mode`, `RoxyVedicKundli` and `RoxyDivisionalChart` `chartStyle`, `RoxyPanchangTable` `detail`, `RoxyDashaTimeline` `period`, `RoxyDoshaCard` `type`, `RoxyNumerologyCard` `type`, `RoxyTarotSpread` `spread`, `RoxyBiorhythmChart` `mode`, `RoxyHexagram` `mode`. Outside React and Vue, set the same value as a kebab-case attribute or a JS property on the element (for example `chart-style="south"` or `el.chartStyle = 'south'`).
217217

218-
### 6d. Chart without the written report
219-
220-
Every component takes `hide-readings` (`hideReadings` in React and Vue). It renders the chart and the data and leaves the interpretation out of the page: wheels, maps, tables, grids, legends, badges and every number stay, the interpretive prose goes. Off by default, so a component that does not set it is unchanged.
221-
222-
The line is drawn on the CONTENT, never on the block, so a block that reads like analysis still stays when it is made of measurements. `<roxy-natal-chart>` and `<roxy-aspects-table>` both render the `patterns` payload and both make the same cut: the figure name, element, modality, tightness and planet chips are geometry and survive, the paragraph under them is the reading and goes. To remove such a block entirely, use `hide-sections` below.
223-
224-
```html
225-
<roxy-natal-chart hide-readings></roxy-natal-chart>
226-
```
227-
228-
```tsx
229-
<RoxyNatalChart data={chart} hideReadings />
230-
```
231-
232-
Reach for it when the page supplies its own words, which is the usual case for a practitioner site that writes its own commentary under the chart. The prose is left out of the markup rather than hidden with CSS, so the page never ships text it is not showing.
233-
234-
**Which components act on it.** Every component that renders a written interpretation, so there is no tag you have to test to find out. A pure chart or table has no interpretation to take away, so the attribute is a no-op on those by definition.
235-
236-
One component is a documented no-op despite being mostly prose:
237-
238-
| Component | Behaviour |
239-
|---|---|
240-
| `<roxy-dream-card>` | Ignores `hide-readings`. The dream symbol response is the symbol, its dictionary letter and the interpretation, so removing the interpretation would leave a heading over nothing. Style it with `::part(card)` or leave the card out of the page instead. |
241-
242-
What survives, by family: charts keep the drawing, legend, glyphs, degrees and tab strip; tables keep every row and every calculated column (kaksha bindus, koota scores, significance bars, orbs, strengths); cards keep the header, badges, meters and fact grids, so a dosha keeps its verdict, phase and severity, a crystal keeps its Mohs hardness and attribute grid, and a horoscope keeps its energy meter, Moon placement, active transits and key dates; every Vedic response keeps its sidereal frame caption. What goes: interpretation paragraphs, reading accordions, keyword chips attached to a reading, remedies, action steps and strengths lists, and any section whose only content was one of those, heading included.
243-
244-
### 6e. Removing a whole block
245-
246-
Every component also takes `hide-sections` (`hideSections` in React and Vue): a comma-separated list of `part` names, and each one named is taken off that component.
247-
248-
```html
249-
<roxy-natal-chart hide-sections="patterns"></roxy-natal-chart>
250-
<roxy-natal-chart hide-sections="patterns, legend"></roxy-natal-chart>
251-
```
252-
253-
```tsx
254-
<RoxyNatalChart data={chart} hideSections="patterns" />
255-
```
256-
257-
**It is per element, which is the difference from a stylesheet.** A `::part()` rule in your CSS reaches every matching component on the site at once; this reaches the one you put it on, so the same chart can keep its patterns on one page and drop them on another with no CSS at all.
258-
259-
**Use `hide-readings` when the words must not ship, and `hide-sections` when a block should not show.** They are different tools rather than two spellings of one. `hide-readings` drops interpretive prose out of the markup entirely, so the page never ships text it is not displaying. `hide-sections` hides the block with CSS and leaves it in the DOM, because a block is a rendering concern and its content may be measurements you have no reason to strip.
260-
261-
Any name a component publishes works, and the names are in `components-catalog.json` under `parts`. A name the component does not carry hides nothing and is not an error, so one list can be applied across a page of mixed components. Names are case-insensitive and spaces around the commas are ignored.
262-
263218
### 6c. Vue and Nuxt
264219

265220
`@roxyapi/ui-vue` exposes the same components with the same prop names. Bind `data` and the config props normally; the package sets them as DOM properties for you, so an object payload never gets stringified into an attribute. Listen to widget events with the usual `@` syntax.
@@ -607,20 +562,37 @@ Per-element scope is supported:
607562

608563
Every visible aspect of the chart is driven by `--roxy-*` CSS custom properties on `:host`. Override any token on `:root`, on `:host`, or per element. Do not write Tailwind utility classes inside the components; the Shadow DOM boundary stops them at the door.
609564

610-
## Restyling a block from outside the component
565+
## Choosing what a component renders
611566

612-
Tokens cover colour, spacing and type. When you need to reach a specific block, every chart and reading component names its structural pieces with `part`, so a stylesheet on the page can target them. The names are identical in every component, so one rule covers the library.
567+
Three tools, one vocabulary. Every structural block carries a `part` name; `::part()` targets it from a stylesheet, `hide-sections` drops it on one element, and `hide-readings` removes interpretive prose from the markup entirely.
613568

614-
```css
615-
roxy-natal-chart::part(readings) {
616-
display: none;
617-
}
618-
roxy-natal-chart::part(card) {
619-
border: 0;
620-
box-shadow: none;
621-
}
569+
**Pick by what you need:**
570+
571+
| Need | Use |
572+
|---|---|
573+
| Restyle a block | `::part(name)` in your CSS |
574+
| Hide a block everywhere on the site | `::part(name) { display: none }` |
575+
| Hide a block on ONE element, keep it on another | `hide-sections="name"` |
576+
| Keep the chart, drop the written interpretation | `hide-readings` |
577+
578+
`hide-readings` and `hide-sections` are different tools, not two spellings of one. `hide-readings` leaves the prose out of the markup, so the page never ships text it is not displaying. `hide-sections` hides the block whatever it holds, because a block may be measurements you have no reason to strip.
579+
580+
```html
581+
<roxy-natal-chart hide-readings></roxy-natal-chart>
582+
<roxy-natal-chart hide-sections="patterns, legend"></roxy-natal-chart>
583+
```
584+
585+
```tsx
586+
<RoxyNatalChart data={chart} hideReadings />
587+
<RoxyNatalChart data={chart} hideSections="patterns" />
622588
```
623589

590+
Both are off by default. `hide-sections` takes a comma-separated list, is case-insensitive, ignores spaces around commas, and a name the component does not carry hides nothing and is not an error, so one list can be applied across a page of mixed components.
591+
592+
**The line `hide-readings` draws is on the CONTENT, never on the block**, so a block that reads like analysis stays when it is made of measurements. `<roxy-natal-chart>` and `<roxy-aspects-table>` both render the `patterns` payload and both make the same cut: figure name, element, modality, tightness and planet chips are geometry and survive; the paragraph under them is the reading and goes. To remove the block itself, that is what `hide-sections` is for. What survives elsewhere: charts keep the drawing, legend, glyphs, degrees and tab strip; tables keep every calculated column (kaksha bindus, koota scores, orbs, strengths); cards keep header, badges, meters and fact grids; every Vedic response keeps its sidereal frame caption. One documented no-op: `<roxy-dream-card>` ignores `hide-readings`, because the response IS the interpretation and removing it would leave a heading over nothing.
593+
594+
### The part vocabulary
595+
624596
| part | What it names |
625597
|---|---|
626598
| `card` | The component's own card or wrapper root |
@@ -635,9 +607,7 @@ roxy-natal-chart::part(card) {
635607
| `reading` | One disclosure card inside it |
636608
| `form`, `loading`, `error`, `edit-bar`, `attribution` | The built-in states, on every component |
637609

638-
The table is the shared vocabulary, not the whole list. **`components-catalog.json` carries a `parts` array for every component, so you can read the exact names a component answers to instead of guessing or inspecting the DOM.** A name means the same block wherever it appears, which is what makes one rule enough: `::part(aspects)` reaches the aspect grid on a natal chart, the aspect list on an aspects table and the transit aspects on a transits table alike.
639-
640-
Two notes. Parts reach exactly one shadow root deep, and a component that draws another one re-exports its parts, so `roxy-relocation-wheel::part(readings)` reaches the wheel it nests. And to remove a reading rather than hide it, use `hide-readings` (above): a `display: none` rule still ships the words in the page.
610+
That table is the shared vocabulary, not the whole list. **`components-catalog.json` carries a `parts` array for every component, so read the exact names a component answers to instead of guessing or inspecting the DOM.** A name means the same block wherever it appears, which is what makes one rule enough: `::part(aspects)` reaches the aspect grid on a natal chart, the aspect list on an aspects table and the transit aspects on a transits table alike. Parts reach exactly one shadow root deep, and a component that draws another re-exports its parts, so `roxy-relocation-wheel::part(readings)` reaches the wheel it nests.
641611

642612
## Rules every agent must follow
643613

@@ -651,7 +621,7 @@ Two notes. Parts reach exactly one shadow root deep, and a component that draws
651621

652622
## Domain ordering
653623

654-
When listing domains in user-visible copy, use the canonical order: Western astrology, Vedic astrology, numerology, tarot, human design, forecast, biorhythm, I Ching, crystals, dreams, angel numbers. Location is utility, not a selling domain.
624+
When listing domains in user-visible copy, use the canonical order: Western astrology, Vedic astrology, forecast, human design, numerology, tarot, biorhythm, I Ching, crystals, dreams, angel numbers. Location is utility, not a selling domain. The `domain` field in `components-catalog.json` carries the same names, so sort by this list rather than by whatever order you discovered components in.
655625

656626
## What not to ship
657627

@@ -666,9 +636,8 @@ When listing domains in user-visible copy, use the canonical order: Western astr
666636

667637
## Where to look next
668638

669-
- Component source: `packages/ui/src/components/`
670-
- Sample data for every component: `apps/docs/sample-data.js`
671-
- Token reference: `packages/ui/THEMING.md`
672-
- Live preview: `bun run preview` then open `http://localhost:3001`
639+
- **Machine-readable catalog, and the one to reach for first**: <https://cdn.jsdelivr.net/npm/@roxyapi/ui@latest/components-catalog.json>. Every component with its `tag`, `pascal` name, `domain`, what it renders, the `endpoints` it consumes, its `parts`, and copy-paste `snippets`. Fetch and query it instead of scraping the tables in this file.
640+
- Token reference: `THEMING.md`, beside this file in the package
673641
- Endpoint reference: <https://roxyapi.com/api-reference>
674-
- Machine-readable component catalog (every component, its domain, what it renders, and the endpoint(s) it consumes): <https://cdn.jsdelivr.net/npm/@roxyapi/ui@latest/components-catalog.json>. Fetch it to discover or map components programmatically instead of scraping this table.
642+
- Integration guide: <https://roxyapi.com/docs/ui>
643+
- Live preview of every component: <https://roxyapi.github.io/ui/>

README.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -736,7 +736,11 @@ roxy-natal-chart::part(card) {
736736
}
737737
```
738738

739-
The parts: `card`, `header`, `chart`, `legend`, `details`, `table`, `tablist`, `tab`, `panel`, `section`, `readings`, `reading`, plus `form`, `loading`, `error` and `attribution` on the built-in states. A `section` also carries its own name, which is what `::part(patterns)` above targets, so any single block can be dropped or restyled on its own.
739+
The parts: `card`, `header`, `chart`, `legend`, `details`, `table`, `tablist`, `tab`, `panel`, `section`, `readings`, `reading`, plus `form`, `loading`, `error`, `edit-bar` and `attribution` on the built-in states. A `section` also carries its own name, which is what `::part(patterns)` above targets, so any single block can be dropped or restyled on its own.
740+
741+
That list is the shared vocabulary, not the whole set. **[`components-catalog.json`](https://cdn.jsdelivr.net/npm/@roxyapi/ui@latest/components-catalog.json) carries a `parts` array for every component**, so you can read the exact names a component answers to instead of guessing or inspecting the DOM. A name means the same block wherever it appears, which is what makes one rule enough: `::part(aspects)` reaches the aspect grid on a natal chart, the aspect list on an aspects table and the transit aspects on a transits table alike.
742+
743+
One nesting rule worth knowing: parts reach exactly one shadow root deep, and a component that draws another one re-exports its parts, so `roxy-relocation-wheel::part(readings)` reaches the wheel nested inside it.
740744

741745
### Chart without the written report
742746

@@ -752,7 +756,7 @@ For the same outcome in markup rather than CSS, set `hide-readings`. The compone
752756

753757
It is off by default, so nothing changes until you ask for it. Wheels, maps, tables, grids, legends, badges and every number stay; the interpretive prose is what goes.
754758

755-
That line is drawn on the content, not on the block, so a block made of measurements stays even when it reads like analysis. The clearest case is the natal chart patterns block: a T-Square or a Stellium is a geometric fact about where the bodies sit, so the figure, its element and modality, its tightness and its planets all survive, and only the paragraph under them goes. To drop the block itself, hide its part: `roxy-natal-chart::part(patterns) { display: none }`.
759+
That line is drawn on the content, not on the block, so a block made of measurements stays even when it reads like analysis. The clearest case is the natal chart patterns block: a T-Square or a Stellium is a geometric fact about where the bodies sit, so the figure, its element and modality, its tightness and its planets all survive, and only the paragraph under them goes. To drop the block itself, use `hide-sections` below.
756760

757761
#### Which components it applies to
758762

@@ -773,6 +777,25 @@ What each family keeps when you set it:
773777

774778
What goes: interpretation paragraphs, the reading accordions, keyword chips attached to a reading, remedies and action steps and strengths lists (sentences laid out as bullets), and any section whose only content was one of those, heading included.
775779

780+
### Remove a whole block
781+
782+
Every component also takes `hide-sections` (`hideSections` in React and Vue): a comma-separated list of `part` names, and each one named is taken off that component.
783+
784+
```html
785+
<roxy-natal-chart hide-sections="patterns"></roxy-natal-chart>
786+
<roxy-natal-chart hide-sections="patterns, legend"></roxy-natal-chart>
787+
```
788+
789+
```tsx
790+
<RoxyNatalChart data={chart} hideSections="patterns" />
791+
```
792+
793+
The names are the same `part` names listed above, so anything you can target with `::part()` you can also drop with an attribute. The rule is generated from the name rather than from a list of components that opted in, so it works on every component, and a name no block carries hides nothing rather than erroring.
794+
795+
**Use `hide-readings` when the words must not ship, and `hide-sections` when a block should not show.** They are different tools rather than two spellings of one. `hide-readings` drops interpretive prose out of the markup entirely, so the page never ships text it is not displaying. `hide-sections` hides the block and leaves it in the DOM, because a block is a rendering concern and its content may be measurements you have no reason to strip.
796+
797+
**Reach for `::part()` instead when one rule should cover a whole site**, and for `hide-sections` when two components on the same page have to differ, which a stylesheet cannot express.
798+
776799
## Languages
777800

778801
A component reads the page language and renders its own labels in it. Set `lang` on the element, on any ancestor of it, or on `<html>`, and add the catalogue for that language.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
"build:registry": "bun run scripts/build-registry.ts",
1919
"build:widgets": "bun run scripts/build-widgets.ts",
2020
"build:schemas": "bun run scripts/build-schemas.ts",
21+
"purge:cdn": "bun run scripts/purge-cdn.ts",
2122
"size:check": "bun run scripts/check-sizes.ts",
2223
"docs:sync": "bun run scripts/sync-docs.ts",
2324
"version:sync": "bun run scripts/sync-version.ts",
2425
"tokens:sync": "bun run scripts/sync-tokens.ts",
26+
"labels:sync": "bun run scripts/sync-field-labels.ts",
2527
"themes:sync": "bun run scripts/sync-themes.ts",
2628
"manifest:sync": "bun run scripts/sync-manifest.ts",
2729
"bindings:sync": "bun run scripts/sync-bindings.ts",

0 commit comments

Comments
 (0)