@rxova/astro-ui 0.0.0 → 0.1.0

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.
@@ -0,0 +1,34 @@
1
+ ---
2
+ /**
3
+ * Starlight `SiteTitle` override: the mark links (absolutely) to rxova.org, the wordmark to this
4
+ * project's docs root via `siteTitleHref`. The image comes from @rxova/brand.
5
+ */
6
+ import mark from '@rxova/brand/assets/rxova-logo-256.png'
7
+ import { RXOVA_ORIGIN } from '@rxova/brand'
8
+
9
+ // Narrowed structurally, not via an `App.Locals` augmentation, which would collide with
10
+ // Starlight's own in the consumer site's build.
11
+ const { siteTitle, siteTitleHref } = (
12
+ Astro.locals as unknown as {
13
+ starlightRoute: { siteTitle: string; siteTitleHref: string }
14
+ }
15
+ ).starlightRoute
16
+ ---
17
+
18
+ <div class="rx-site-title">
19
+ <a href={RXOVA_ORIGIN} class="rx-site-title__mark" aria-label="Rxova home">
20
+ <img src={mark.src} alt="" width="28" height="28" />
21
+ </a>
22
+ <span class="rx-site-title__sep" aria-hidden="true"></span>
23
+ <a href={siteTitleHref} class="rx-site-title__project" translate="no">
24
+ {siteTitle}
25
+ </a>
26
+ </div>
27
+
28
+ <style>
29
+ /* Visual styling lives in ../styles/starlight.css so the Astro landing can
30
+ reuse the same classnames without importing this component. */
31
+ .rx-site-title__mark {
32
+ display: inline-flex;
33
+ }
34
+ </style>
@@ -0,0 +1,18 @@
1
+ ---
2
+ /**
3
+ * Starlight `SocialIcons` override: the stock icons plus the project switcher, which lands just
4
+ * before search and the theme toggle.
5
+ */
6
+ import Default from '@astrojs/starlight/components/SocialIcons.astro'
7
+ import ProjectSwitcher from '../components/ProjectSwitcher.astro'
8
+ import { projectFromBase } from '@rxova/brand'
9
+
10
+ // Overrides receive no props, so the current project is inferred from the base
11
+ // path the aggregator mounts this site at.
12
+ const current = projectFromBase(import.meta.env.BASE_URL)
13
+ ---
14
+
15
+ <ProjectSwitcher current={current} />
16
+ <Default>
17
+ <slot />
18
+ </Default>
@@ -0,0 +1,42 @@
1
+ ---
2
+ /**
3
+ * Wraps Starlight's theme picker and resyncs the theme on a bfcache restore, where Starlight's
4
+ * parse-time theme code does not run again and the page would keep a stale `data-theme`.
5
+ */
6
+ import Default from '@astrojs/starlight/components/ThemeSelect.astro'
7
+ ---
8
+
9
+ <Default>
10
+ <slot />
11
+ </Default>
12
+
13
+ <script>
14
+ type Theme = 'auto' | 'dark' | 'light'
15
+
16
+ /** Same key and coercion Starlight uses; unknown values mean "follow the OS". */
17
+ const STORAGE_KEY = 'starlight-theme'
18
+ const parseTheme = (theme: unknown): Theme =>
19
+ theme === 'auto' || theme === 'dark' || theme === 'light' ? theme : 'auto'
20
+ const preferredColorScheme = (): Theme =>
21
+ matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'
22
+
23
+ // `pageshow` is the only notification of a bfcache restore; a normal load (`persisted: false`)
24
+ // is left to Starlight.
25
+ window.addEventListener('pageshow', (event) => {
26
+ if (!event.persisted) return
27
+
28
+ let stored: string | null
29
+ try {
30
+ stored = localStorage.getItem(STORAGE_KEY)
31
+ } catch {
32
+ // Storage can throw outright (Safari private mode, blocked cookies).
33
+ // Leaving the restored theme alone is the right fallback.
34
+ return
35
+ }
36
+
37
+ const theme = parseTheme(stored)
38
+ document.documentElement.dataset.theme = theme === 'auto' ? preferredColorScheme() : theme
39
+ // Keeps the picker from disagreeing with the page it controls.
40
+ StarlightThemeProvider.updatePickers(theme)
41
+ })
42
+ </script>
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Shared Starlight configuration for the rxova docs sites; each site adds only its sidebar and
3
+ * extras. Imported under Node (no CSS or components) and typed structurally, not as Starlight's.
4
+ */
5
+
6
+ import { RXOVA_ORIGIN, getProject, type ProjectId } from '@rxova/brand'
7
+
8
+ export interface SharedStarlightOptions {
9
+ /** Which project's docs this site is. */
10
+ project: ProjectId
11
+ /** Starlight sidebar config — the one thing every site defines itself. */
12
+ sidebar: unknown[]
13
+ /** Extra stylesheets, appended after the shared ones so they win. */
14
+ customCss?: string[]
15
+ /** Extra Starlight component overrides, merged over the shared ones. */
16
+ components?: Record<string, string>
17
+ /** Path under the repo root that holds the docs site, for the edit link. */
18
+ editLinkBase?: string
19
+ /** Build body-only docs for rxova-website's shell: Starlight's page UI stays, the footer goes. */
20
+ pageComponent?: boolean
21
+ }
22
+
23
+ export function sharedStarlightConfig({
24
+ project,
25
+ sidebar,
26
+ customCss = [],
27
+ components = {},
28
+ editLinkBase = 'apps/docs',
29
+ pageComponent = false,
30
+ }: SharedStarlightOptions) {
31
+ const self = getProject(project)
32
+
33
+ return {
34
+ title: self.label,
35
+ description: self.tagline,
36
+ // One origin, one tab icon. Each site must have this file in `public/`: Starlight resolves
37
+ // `favicon` against the site's own static directory.
38
+ favicon: '/favicon.svg',
39
+
40
+ // No `logo` on purpose: the SiteTitle override renders the mark from @rxova/brand's assets.
41
+
42
+ social: [
43
+ { icon: 'github' as const, label: 'GitHub', href: self.repo },
44
+ { icon: 'npm' as const, label: 'npm', href: self.npm },
45
+ ],
46
+
47
+ editLink: {
48
+ baseUrl: `${self.repo}/edit/main/${editLinkBase}/`,
49
+ },
50
+
51
+ // Order matters: fonts, then tokens+mapping, then per-site overrides.
52
+ customCss: ['@rxova/brand/fonts.css', '@rxova/astro-ui/styles/starlight.css', ...customCss],
53
+
54
+ components: {
55
+ // The rxova mark (linking back to the umbrella site) plus the project wordmark.
56
+ SiteTitle: '@rxova/astro-ui/starlight/SiteTitle.astro',
57
+ // Appends the cross-project switcher to the social icons. Without it the
58
+ // three docs sites are three islands under one domain.
59
+ SocialIcons: '@rxova/astro-ui/starlight/SocialIcons.astro',
60
+ // Starlight's default footer (pagination, edit link, last updated) plus
61
+ // the shared four-column site footer beneath it.
62
+ ...(!pageComponent ? { Footer: '@rxova/astro-ui/starlight/Footer.astro' } : {}),
63
+ // Starlight's own picker, plus a resync when a page is restored from the bfcache.
64
+ ThemeSelect: '@rxova/astro-ui/starlight/ThemeSelect.astro',
65
+ ...components,
66
+ },
67
+
68
+ head: [
69
+ {
70
+ tag: 'meta' as const,
71
+ attrs: { property: 'og:image', content: `${RXOVA_ORIGIN}/og/${project}.png` },
72
+ },
73
+ {
74
+ tag: 'meta' as const,
75
+ attrs: { name: 'twitter:card', content: 'summary_large_image' },
76
+ },
77
+ // SoftwareSourceCode JSON-LD built from PROJECTS, on every docs page: Starlight has no
78
+ // "site index only" hook.
79
+ {
80
+ tag: 'script' as const,
81
+ attrs: { type: 'application/ld+json' },
82
+ content: JSON.stringify({
83
+ '@context': 'https://schema.org',
84
+ '@type': 'SoftwareSourceCode',
85
+ name: self.label,
86
+ description: self.tagline,
87
+ url: `${RXOVA_ORIGIN}${self.mount}`,
88
+ codeRepository: self.repo,
89
+ programmingLanguage: 'TypeScript',
90
+ runtimePlatform: 'Node.js',
91
+ license: 'https://opensource.org/licenses/MIT',
92
+ author: { '@type': 'Person', name: 'Jonatan Kruszewski' },
93
+ }).replace(/</g, '\\u003c'),
94
+ },
95
+ ],
96
+
97
+ // Pagefind ships with Starlight and replaces the third-party search plugin
98
+ // journey was carrying.
99
+ pagefind: true,
100
+
101
+ sidebar,
102
+ }
103
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * What the shared header and footer need, and nothing document-level: the tokens and the footer.
3
+ * No reset, which is document.css: unlayered, it would flatten a Starlight page this is composed into.
4
+ */
5
+
6
+ @import '@rxova/brand/tokens.css';
7
+ /* The footer every surface renders, Starlight or not. */
8
+ @import './footer.css';
@@ -0,0 +1,72 @@
1
+ /**
2
+ * For sites that own their document (the landing, /blog, /updates): chrome.css plus a reset and base
3
+ * element styles. Never inside a Starlight document, where the unlayered reset would flatten the page.
4
+ */
5
+
6
+ @import './chrome.css';
7
+
8
+ /* --- Reset ---------------------------------------------------------------- */
9
+
10
+ *,
11
+ *::before,
12
+ *::after {
13
+ box-sizing: border-box;
14
+ margin: 0;
15
+ padding: 0;
16
+ }
17
+
18
+ html {
19
+ -webkit-text-size-adjust: 100%;
20
+ scroll-behavior: smooth;
21
+ }
22
+
23
+ @media (prefers-reduced-motion: reduce) {
24
+ html {
25
+ scroll-behavior: auto;
26
+ }
27
+ }
28
+
29
+ body {
30
+ position: relative;
31
+ background: var(--rx-bg);
32
+ color: var(--rx-fg);
33
+ font-family: var(--rx-font-sans);
34
+ font-size: 1.0625rem;
35
+ line-height: 1.65;
36
+ -webkit-font-smoothing: antialiased;
37
+ text-rendering: optimizeLegibility;
38
+ transition:
39
+ background-color 0.2s ease,
40
+ color 0.2s ease;
41
+
42
+ /* At least a screen tall, with `main` taking the slack, so a short page keeps its footer at the
43
+ bottom. Flex rather than grid because the number of children varies. */
44
+ display: flex;
45
+ flex-direction: column;
46
+ min-height: 100dvh;
47
+ }
48
+
49
+ /* Flex items with `margin: 0 auto` shrink to their content; full width restores block behaviour.
50
+ `:where()` keeps both rules at zero specificity. */
51
+ :where(body > *) {
52
+ width: 100%;
53
+ }
54
+
55
+ :where(body > main) {
56
+ flex-grow: 1;
57
+ }
58
+
59
+ a {
60
+ color: inherit;
61
+ }
62
+
63
+ a:focus-visible {
64
+ outline: 2px solid var(--rx-fg);
65
+ outline-offset: 3px;
66
+ border-radius: 2px;
67
+ }
68
+
69
+ img {
70
+ max-width: 100%;
71
+ height: auto;
72
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Styles for `components/SiteFooter.astro`, shared by Starlight and plain Astro surfaces.
3
+ * Imported by `starlight.css` and `chrome.css`; uses `--rx-*` tokens only.
4
+ */
5
+
6
+ @import '@rxova/brand/tokens.css';
7
+
8
+ .rx-footer {
9
+ margin-top: 4rem;
10
+ padding: 3rem 0 2rem;
11
+ border-top: 1px solid var(--rx-rule);
12
+ color: var(--rx-muted);
13
+ font-size: 0.875rem;
14
+ }
15
+
16
+ /* Brand block and link columns side by side where the container (not viewport) has room.
17
+ `min(100%, 20rem)` lets the track floor collapse so a narrow footer never overflows the page. */
18
+ .rx-footer__top {
19
+ display: grid;
20
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
21
+ gap: 2.5rem 3rem;
22
+ }
23
+
24
+ .rx-footer__brand {
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: 0.75rem;
28
+ align-items: flex-start;
29
+ }
30
+
31
+ .rx-footer__mark {
32
+ display: inline-flex;
33
+ align-items: center;
34
+ gap: 0.55rem;
35
+ color: var(--rx-fg);
36
+ font-size: 1rem;
37
+ font-weight: 640;
38
+ letter-spacing: -0.01em;
39
+ text-decoration: none;
40
+ }
41
+
42
+ .rx-footer__mark img {
43
+ display: block;
44
+ border-radius: 6px;
45
+ }
46
+
47
+ .rx-footer__blurb {
48
+ margin: 0;
49
+ max-width: 30ch;
50
+ color: var(--rx-faint);
51
+ line-height: 1.6;
52
+ }
53
+
54
+ /* 8rem: the widest track that still fits two columns on a 360px phone. */
55
+ .rx-footer__columns {
56
+ display: grid;
57
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 8rem), 1fr));
58
+ gap: 2rem 1.5rem;
59
+ }
60
+
61
+ .rx-footer__title {
62
+ margin-bottom: 0.75rem;
63
+ color: var(--rx-fg);
64
+ font-size: 0.75rem;
65
+ font-weight: 700;
66
+ letter-spacing: 0.06em;
67
+ text-transform: uppercase;
68
+ }
69
+
70
+ .rx-footer__list {
71
+ display: flex;
72
+ flex-direction: column;
73
+ gap: 0.45rem;
74
+ margin: 0;
75
+ padding: 0;
76
+ list-style: none;
77
+ }
78
+
79
+ .rx-footer__list a {
80
+ color: var(--rx-muted);
81
+ text-decoration: none;
82
+ }
83
+
84
+ .rx-footer__list a:hover {
85
+ color: var(--rx-fg);
86
+ text-decoration: underline;
87
+ text-underline-offset: 2px;
88
+ }
89
+
90
+ /* The bottom bar: copyright on the left, the legal links on the right, and one
91
+ column on a phone. */
92
+ .rx-footer__legal {
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: space-between;
96
+ flex-wrap: wrap;
97
+ gap: 0.5rem 1.25rem;
98
+ margin-top: 2.5rem;
99
+ padding-top: 1.25rem;
100
+ border-top: 1px solid var(--rx-rule);
101
+ color: var(--rx-faint);
102
+ font-size: 0.8125rem;
103
+ }
104
+
105
+ .rx-footer__legal p {
106
+ margin: 0;
107
+ }
108
+
109
+ .rx-footer__legal-links {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 1.25rem;
113
+ }
114
+
115
+ .rx-footer__legal a {
116
+ color: var(--rx-faint);
117
+ text-decoration: none;
118
+ }
119
+
120
+ .rx-footer__legal a:hover {
121
+ color: var(--rx-fg);
122
+ text-decoration: underline;
123
+ text-underline-offset: 2px;
124
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Maps rxova tokens onto Starlight's theme variables; load via `customCss` so it wins without
3
+ * !important. One block covers both modes because every --rx-* neutral already flips.
4
+ */
5
+
6
+ @import '@rxova/brand/tokens.css';
7
+ @import './footer.css';
8
+
9
+ :root {
10
+ /* Accent. */
11
+ --sl-color-accent: var(--rx-primary);
12
+ --sl-color-accent-low: color-mix(in srgb, var(--rx-primary) 20%, var(--rx-bg));
13
+ --sl-color-accent-high: var(--rx-primary-lightest);
14
+ --sl-color-text-accent: var(--rx-primary);
15
+
16
+ /* Neutral ladder. "white" and "black" are Starlight's names for the two
17
+ contrast extremes, not literal colours — they invert with the theme. */
18
+ --sl-color-white: var(--rx-fg);
19
+ --sl-color-gray-1: var(--rx-fg);
20
+ --sl-color-gray-2: var(--rx-muted);
21
+ --sl-color-gray-3: var(--rx-faint);
22
+ --sl-color-gray-4: var(--rx-rule-strong);
23
+ --sl-color-gray-5: var(--rx-rule);
24
+ --sl-color-gray-6: var(--rx-tag-bg);
25
+ --sl-color-gray-7: var(--rx-card);
26
+ --sl-color-black: var(--rx-bg);
27
+
28
+ /* Surfaces. Nav and sidebar sit flush with the page — the hairline does the
29
+ separating, not a fill change. */
30
+ --sl-color-bg: var(--rx-bg);
31
+ --sl-color-bg-nav: var(--rx-bg);
32
+ --sl-color-bg-sidebar: var(--rx-bg);
33
+ --sl-color-bg-inline-code: var(--rx-tag-bg);
34
+ --sl-color-bg-badge: var(--rx-tag-bg);
35
+
36
+ --sl-color-hairline: var(--rx-rule);
37
+ --sl-color-hairline-light: var(--rx-rule);
38
+ --sl-color-hairline-shade: var(--rx-rule-strong);
39
+ --sl-color-backdrop-overlay: color-mix(in srgb, var(--rx-bg) 80%, transparent);
40
+
41
+ /* Type. */
42
+ --sl-font: var(--rx-font-sans);
43
+ --sl-font-mono: var(--rx-font-mono);
44
+
45
+ --sl-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.06);
46
+ --sl-shadow-md: 0 4px 12px -4px rgb(0 0 0 / 0.12);
47
+ --sl-shadow-lg: var(--rx-shadow-soft);
48
+ }
49
+
50
+ /* Starlight ships a slightly cooler focus ring; align it with the accent. */
51
+ :root {
52
+ --sl-color-focus: var(--rx-primary);
53
+ }
54
+
55
+ /* --- Shared chrome ------------------------------------------------------- */
56
+
57
+ /* The gradient, the brand's only chroma, gets exactly two placements: the header hairline and
58
+ the switcher's current-project dot. */
59
+ .site-title::after {
60
+ content: none;
61
+ }
62
+
63
+ header.header {
64
+ border-bottom: 1px solid var(--sl-color-hairline);
65
+ background: var(--sl-color-bg-nav);
66
+ }
67
+
68
+ /* In a schema-2 page bundle rxova-website renders the umbrella header outside `.page`; keep
69
+ Starlight's sticky docs bar below it. */
70
+ html[data-rxova-shell] {
71
+ --rx-shell-header-height: 3.5rem;
72
+ }
73
+
74
+ html[data-rxova-shell] .page > header.header {
75
+ inset-block-start: var(--rx-shell-header-height);
76
+ }
77
+
78
+ html[data-rxova-shell] .sidebar-pane {
79
+ inset-block-start: calc(var(--rx-shell-header-height) + var(--sl-nav-height));
80
+ }
81
+
82
+ html[data-rxova-shell] mobile-starlight-toc nav {
83
+ top: calc(var(--rx-shell-header-height) + var(--sl-nav-height) - 1px);
84
+ }
85
+
86
+ /* Starlight 0.42 dropped the <starlight-menu-button> wrapper for a bare
87
+ button.sl-menu-button; the peer range still covers the older markup. */
88
+ html[data-rxova-shell] starlight-menu-button button,
89
+ html[data-rxova-shell] .sl-menu-button {
90
+ top: calc(
91
+ var(--rx-shell-header-height) + (var(--sl-nav-height) - var(--sl-menu-button-size)) / 2
92
+ );
93
+ }
94
+
95
+ @media (min-width: 72rem) {
96
+ html[data-rxova-shell] .right-sidebar {
97
+ top: var(--rx-shell-header-height);
98
+ height: calc(100vh - var(--rx-shell-header-height));
99
+ }
100
+ }
101
+
102
+ @media (max-width: 34rem) {
103
+ html[data-rxova-shell] {
104
+ --rx-shell-header-height: 3.25rem;
105
+ }
106
+ }
107
+
108
+ /* Site title: the rxova mark sits beside the project wordmark. */
109
+ .rx-site-title {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 0.625rem;
113
+ color: var(--rx-fg);
114
+ font-weight: 600;
115
+ letter-spacing: -0.01em;
116
+ text-decoration: none;
117
+ }
118
+
119
+ .rx-site-title a {
120
+ color: inherit;
121
+ text-decoration: none;
122
+ }
123
+
124
+ .rx-site-title a:hover {
125
+ color: var(--rx-fg);
126
+ }
127
+
128
+ .rx-site-title img {
129
+ width: 28px;
130
+ height: 28px;
131
+ border-radius: var(--rx-radius-sm);
132
+ }
133
+
134
+ .rx-site-title__sep {
135
+ width: 1px;
136
+ height: 1.1rem;
137
+ background: var(--rx-rule-strong);
138
+ }
139
+
140
+ .rx-site-title__project {
141
+ color: var(--rx-muted);
142
+ font-weight: 500;
143
+ }
144
+
145
+ /* Project switcher. */
146
+ .rx-switcher {
147
+ position: relative;
148
+ }
149
+
150
+ .rx-switcher__current::before {
151
+ content: '';
152
+ display: inline-block;
153
+ width: 0.5rem;
154
+ height: 0.5rem;
155
+ margin-inline-end: 0.5rem;
156
+ border-radius: var(--rx-radius-pill);
157
+ background: var(--rx-gradient);
158
+ }
159
+
160
+ /* --- Footer -------------------------------------------------------------- */
161
+
162
+ /* Footer styles live in footer.css (imported above) because /blog and /updates need them too. */