Skip to content
Open
Show file tree
Hide file tree
Changes from 12 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
185 changes: 174 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,186 @@ 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('wide viewports centre the content panel when a TOC is present', async ({
page,
}) => {
await page.setViewportSize({ width: 1896, height: 940 });
await page.goto('/getting-started/quickstart/');

const [left, right] = await page.evaluate(() => {
const panel = document.querySelector('main > .content-panel');
const container = panel?.querySelector('.sl-container');
if (!panel || !container) return [NaN, NaN];
const pr = panel.getBoundingClientRect();
const cr = container.getBoundingClientRect();
return [cr.left - pr.left, pr.right - cr.right];
});
expect(left).toBeGreaterThan(0);
expect(Math.abs(left - right)).toBeLessThan(1);
});

test('tables stay in column and scroll locally', async ({ page }) => {
for (const width of [375, 620, 900]) {
await page.setViewportSize({ width, height: 800 });

for (const path of [
'/reference/configuration/',
'/reference/kitchen-sink/',
]) {
await page.goto(path);
await page.waitForFunction(
() => document.querySelector('.nbr-table-scroll') !== null,
);
const layout = await page.evaluate(() => {
const wrappers = [
...document.querySelectorAll<HTMLElement>('.nbr-table-scroll'),
];
return {
pageWidth: document.documentElement.scrollWidth,
viewport: document.documentElement.clientWidth,
wrappers: wrappers.map((wrapper) => {
const table = wrapper.querySelector('table');
const row = table?.querySelector('tr');
return {
client: wrapper.clientWidth,
scroll: wrapper.scrollWidth,
tabIndex: wrapper.tabIndex,
tableWidth: table?.clientWidth ?? 0,
rowWidth: row?.getBoundingClientRect().width ?? 0,
};
}),
};
});
const label = `${width}px ${path}`;
expect(layout.pageWidth, label).toBe(layout.viewport);
expect(
layout.wrappers.every(
(wrapper) => wrapper.tableWidth <= wrapper.rowWidth + 2,
),
`${label} table grid must meet its border`,
).toBe(true);
expect(
layout.wrappers.every((wrapper) =>
wrapper.scroll > wrapper.client
? wrapper.tabIndex === 0
: wrapper.tabIndex <= 0,
),
`${label} wrapper must be keyboard-reachable when it scrolls`,
).toBe(true);
}
}
});

test('an unbreakable table value scrolls inside its cell', async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto('/reference/configuration/');
await page.waitForFunction(
() => document.querySelector('.nbr-table-cell-scroll') !== null,
);

const overflow = await page.evaluate(() => {
const cell = document.querySelector('.sl-markdown-content td');
const scroller = cell?.querySelector('.nbr-table-cell-scroll');
const content = scroller?.querySelector('.nbr-table-cell-content');
if (!scroller || !content) return null;
content.textContent = 'x'.repeat(200);
return {
pageWidth: document.documentElement.scrollWidth,
viewport: document.documentElement.clientWidth,
cellScroll: scroller.scrollWidth,
cellClient: scroller.clientWidth,
};
});
expect(overflow).not.toBeNull();
expect(overflow?.pageWidth).toBe(overflow?.viewport);
expect((overflow?.cellScroll ?? 0) > (overflow?.cellClient ?? 0)).toBe(true);
});

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
Loading