Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: latest
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ starlight({ plugins: [nebari({ logoHref: 'https://packs.nebari.dev/' })] })
- **Brand colors** - nebari-design's OKLCH tokens mapped onto Starlight's theme
variables in both light and dark mode. Starlight's WCAG-tuned gray scale is
kept for accessible body and muted text.
- **Typography** - Poppins for headings, Atkinson Hyperlegible for body, Fira
Code for code, all self-hosted (no external font requests at runtime).
- **Typography** - Geist for body and headings, IBM Plex Mono for code, both
self-hosted (no external font requests at runtime).
- **Logo, favicon, and footer** - the Nebari mark in the header, an inlined
symbol favicon, and a branded footer on every page.
- **Search** - Starlight's built-in Pagefind, ready to merge additional pack
Expand Down
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@ export default defineConfig({
integrations: [
starlight({
title: 'Nebari Starlight',
plugins: [nebari()],
description: 'Shared Starlight theme for Nebari documentation sites.',
plugins: [
nebari({
nav: [
{ label: 'Docs', href: '/' },
{ label: 'Guides', href: '/guides/authoring-content/' },
{ label: 'Reference', href: '/reference/configuration/' },
],
}),
],
sidebar: [
{
label: 'Getting Started',
Expand Down Expand Up @@ -56,6 +65,7 @@ export default defineConfig({
link: '/reference/configuration/',
},
{ label: 'Components', link: '/reference/components/' },
{ label: 'Kitchen Sink', link: '/reference/kitchen-sink/' },
],
},
],
Expand Down
90 changes: 79 additions & 11 deletions docs/e2e/theme-search-a11y.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,23 +79,91 @@ test('search returns the seeded token', async ({ page }) => {
});
});

test('the mobile drawer exposes nav tabs and keeps accessible names', async ({
page,
}) => {
await page.setViewportSize({ width: 375, height: 800 });
await page.goto('/guides/deployment/build/');

await expect(page.locator('.nbr-nav-tabs--header')).toBeHidden();
const menu = page.locator('starlight-menu-button');
await expect(page.locator('.nbr-nav-tabs--drawer')).toBeHidden();
await menu.locator('button').click();
await expect(menu).toHaveAttribute('aria-expanded', 'true');

const drawerTabs = page.locator('.nbr-nav-tabs--drawer');
await expect(drawerTabs).toBeVisible();
await expect(drawerTabs.locator('a[aria-current="page"]')).toHaveCount(1);

const summaries = page.locator('#starlight__sidebar summary');
for (let i = 0; i < (await summaries.count()); i++) {
expect((await summaries.nth(i).innerText()).trim().length).toBeGreaterThan(
0,
);
}
});

test('home and content pages have no serious/critical a11y violations', async ({
page,
}) => {
// Cover the splash home plus a component-heavy guide and a table-heavy
// reference page, so the a11y sweep exercises the full docs layout.
for (const path of [
'/',
'/guides/authoring-content/',
'/reference/components/',
for (const colorScheme of ['light', 'dark'] as const) {
await page.emulateMedia({ colorScheme });
for (const [width, height] of [
[1440, 900],
[375, 800],
]) {
await page.setViewportSize({ width, height });
for (const path of [
'/',
'/guides/authoring-content/',
'/reference/components/',
'/reference/kitchen-sink/',
]) {
await page.goto(path);
await expect(page.locator('html')).toHaveAttribute(
'data-theme',
colorScheme,
);
await page.waitForFunction(() =>
[...document.querySelectorAll('pre')].every(
(el) =>
el.scrollWidth <= el.clientWidth || el.hasAttribute('tabindex'),
),
);
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
const serious = results.violations.filter(
(v) => v.impact === 'serious' || v.impact === 'critical',
);
expect(
serious,
`${colorScheme} ${width}px ${path}: ${JSON.stringify(
serious.map((v) => v.id),
)}`,
).toEqual([]);
}
}
}
});

test('exactly one "Site" nav landmark is exposed at each width', async ({
page,
}) => {
for (const [width, height] of [
[1440, 900],
[375, 800],
]) {
await page.goto(path);
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa'])
.analyze();
const serious = results.violations.filter(
(v) => v.impact === 'serious' || v.impact === 'critical',
await page.setViewportSize({ width, height });
await page.goto('/guides/authoring-content/');
const exposed = await page.evaluate(
() =>
[...document.querySelectorAll('nav[aria-label="Site"]')].filter(
(n) => (n as HTMLElement).offsetParent !== null,
).length,
);
expect(serious, JSON.stringify(serious.map((v) => v.id))).toEqual([]);
expect(exposed, `${width}px exposed ${exposed} "Site" navs`).toBe(1);
}
});
7 changes: 4 additions & 3 deletions docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"build:base": "astro build --base /demo-pack",
"theme": "bun run --filter '@nebari/starlight' build",
"dev": "bun run theme && astro dev",
"build": "bun run theme && astro build",
"build:base": "bun run theme && astro build --base /demo-pack",
"preview": "astro preview",
"e2e": "playwright test"
},
Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/getting-started/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ title: Hello, Nebari
description: My first themed page.
---

Welcome to my pack. This paragraph already uses Inter, and code blocks use Fira
Code:
Welcome to my pack. This paragraph already uses Geist, and code blocks use IBM
Plex Mono:

```py
print("themed code, no extra setup")
Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/guides/authoring-content.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ This is a tip.

## Code blocks

Code blocks are styled with Fira Code and support titles, line highlighting, and
diff markers via Expressive Code (bundled with Starlight):
Code blocks are styled with IBM Plex Mono and support titles, line highlighting,
and diff markers via Expressive Code (bundled with Starlight):

```js title="astro.config.mjs" {3} del={4} ins={5}
starlight({
Expand Down
6 changes: 3 additions & 3 deletions docs/src/content/docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ accessible, on-brand site out of the box. New here? Jump to the
</Card>
<Card title="On-brand by default" icon="approve-check">
OKLCH design tokens from nebari-design map onto Starlight's variables: a
magenta-violet accent with slate-tinted surfaces, tuned for light and dark.
magenta accent with slate-tinted surfaces, tuned for light and dark.
</Card>
<Card title="Typography that ships with it" icon="seti:font">
Inter for body, Space Grotesk for headings, Lora for page titles, and Fira
Code for code — all self-hosted, no CDN calls, no layout shift.
Geist for body and headings, IBM Plex Mono for code — all self-hosted, no
CDN calls, no layout shift.
</Card>
<Card title="Search included" icon="magnifier">
Starlight's built-in Pagefind search is enabled and styled to match, ready
Expand Down
159 changes: 159 additions & 0 deletions docs/src/content/docs/reference/kitchen-sink.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
---
title: Kitchen Sink
description: Every content block the docs theme styles, on one page, for visual regression checking against the design.
---

import { Aside, Badge, Steps, Tabs, TabItem } from '@astrojs/starlight/components';

This page contains every content block the docs theme supports. Use this to verify
typography, spacing, and color tokens across components.

## Overview

Authentication in Nebari is handled by Keycloak, the identity service bundled with
every deployment. When a user visits the hub, unauthenticated requests are
redirected to the configured provider, and the returned identity is mapped onto
Nebari roles and groups.

:::note
Provider changes take effect after you run `nebari deploy`. Existing user sessions
are preserved, but new logins use the updated configuration.
:::

## Configure a provider

Set the provider under `security.authentication.type` and supply its credentials in
your `nebari-config.yaml` file.

A tabbed frame with an `OUTPUT` frame joined directly beneath it — authored as two
adjacent blocks with no blank-line-separated content between them:

<Tabs syncKey="lang">
<TabItem label="lang1">
```py
print("Hello World!")
```
</TabItem>
<TabItem label="lang2">
```js
console.log('Hello World!');
```
</TabItem>
</Tabs>
```text title="OUTPUT" frame="terminal"
Hello World!
```

A titled frame with terminal chrome — `frame="terminal"` plus a `title`:

```sh title="nebari-config.yaml" frame="terminal"
nebari init aws --project nebari-demo --domain nebari.quansight.dev
nebari deploy --config nebari-config.yaml
nebari destroy --config nebari-config.yaml
```

A plain frame, no title:

```yaml
# GitHub OAuth provider
security:
authentication:
type: GitHub
config:
client_id: "Iv1.a1b2c3d4e5f6"
client_secret: "${GITHUB_CLIENT_SECRET}"
# callback URL is generated by Nebari
```

## Supported providers

Nebari ships with adapters for the most common identity providers. Password auth is
available for local testing but not recommended in production.

| Provider | Protocol | Status |
| -------- | --------- | ---------------------------------------------- |
| GitHub | OAuth 2.0 | <Badge text="Available" variant="success" /> |
| Auth0 | OIDC | <Badge text="Available" variant="success" /> |
| Google | OAuth 2.0 | <Badge text="Available" variant="success" /> |
| Password | Local | <Badge text="Testing only" variant="caution" /> |

## Callouts & tabs

:::tip
Use `nebari validate` before deploying — it catches most configuration mistakes
locally.
:::

:::caution
Changing the authentication type logs out all users. Schedule provider migrations
outside working hours.
:::

:::danger
Deleting the Keycloak realm removes all users, groups, and client secrets. This
cannot be undone.
:::

<Tabs syncKey="cloud">
<TabItem label="AWS">
Deploying to AWS requires credentials with EKS, VPC, and S3 permissions. Set
your region before running `nebari init`.
</TabItem>
<TabItem label="GCP">
Deploying to GCP requires a service account with GKE and Cloud Storage roles.
</TabItem>
<TabItem label="Azure">
Deploying to Azure requires a subscription with AKS and Blob Storage enabled.
</TabItem>
<TabItem label="Local">
Local deployments run on `kind` and need no cloud credentials.
</TabItem>
</Tabs>

## Media & lists

Markdown images do not emit a `<figure>`, so captioned figures are authored as
literal HTML in `.mdx`:

<figure>
<svg viewBox="0 0 816 300" role="img" aria-label="Keycloak brokers identity for JupyterHub">
<rect x="205" y="115" width="160" height="70" rx="8" fill="none" stroke="currentColor" stroke-width="1" />
<text x="285" y="155" text-anchor="middle" dominant-baseline="middle" font-size="16" fill="currentColor">Keycloak</text>
<line x1="365" y1="150" x2="451" y2="150" stroke="currentColor" stroke-width="2" />
<rect x="451" y="115" width="160" height="70" rx="8" fill="none" stroke="currentColor" stroke-width="1" />
<text x="531" y="155" text-anchor="middle" dominant-baseline="middle" font-size="16" fill="currentColor">JupyterHub</text>
</svg>
<figcaption>Figure 1 — Keycloak brokers identity for JupyterHub and every Nebari service.</figcaption>
</figure>

- Cloud providers
- AWS — full support, including GPU node groups
- GCP — full support
- Azure — full support
- Existing Kubernetes clusters
- Local (kind) — testing and development only

1. Install the CLI: `pip install nebari`
2. Generate a config: `nebari init`
3. Deploy: `nebari deploy -c nebari-config.yaml`

## Steps

<Steps>

1. Import the component into your MDX file:

```js
import { Steps } from '@astrojs/starlight/components';
```

2. Wrap `<Steps>` around your ordered list items.

</Steps>

## Aside with a component title

<Aside type="note" title="Heads up">
Asides accept a custom title. The title bar keeps the variant tint; only the
label changes.
</Aside>
10 changes: 10 additions & 0 deletions docs/test/base-path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,13 @@ test('assets and links are prefixed with the base path', () => {
// No bare-root asset references that would 404 behind the Worker subpath.
expect(html).not.toMatch(/href="\/_astro\//);
});

test('nav tab and footer hrefs carry the base exactly once', () => {
const html = readFileSync(HOME, 'utf8');
const tabs = html.match(
/<nav class="nbr-nav-tabs[^"]*"[^>]*>[\s\S]*?<\/nav>/,
);
expect(tabs, 'no nav tabs rendered').not.toBeNull();
expect(tabs?.[0]).toMatch(/href="\/demo-pack\/guides\//);
expect(html).not.toMatch(/\/demo-pack\/demo-pack\//);
});
2 changes: 2 additions & 0 deletions packages/starlight/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"src/fonts",
"src/assets",
"src/components",
"src/utils",
"src/virtual.d.ts"
],
"exports": {
Expand All @@ -39,6 +40,7 @@
},
"scripts": {
"build": "tsup",
"prepare": "bun run build",
"prepack": "bun run build",
"prepublishOnly": "bun run build"
},
Expand Down
Loading