Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
25 changes: 24 additions & 1 deletion frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,30 @@ palette variables. Page-specific layout and decorative styles use `styleBuilder`
and `themes.forEach`, with framework breakpoint tokens. They are emitted into
`site.css` and `site.dark.css`; there is no separate handwritten CSS asset.

To change the identity, edit the hues in `theme/studioSkinShared.ts`. To add a
The palette icon before the theme toggle offers Cobalt, Violet, Teal, Emerald,
Amber, and Rose. Edit `theme/studioColorPresets.ts` to adjust each complete color
family: neutrals, accents, companion colors, and semantic roles. Each preset
coordinates page backgrounds, reading surfaces, text, borders, controls, shadows,
and highlights in both modes. Status colors retain their familiar meanings.
`theme/studioSurfaceProfiles.ts` defines each preset's lightness steps for the
page, cards, inset surfaces, and controls, plus border strength, saturation, and
shadow density. Dark presets range from crisp slate to a brighter warm studio.
Documentation derives its colors from these same roles, so its code panels and
copy controls keep their own layers instead of sharing one flat reading surface.
`theme/studioLandingColors.ts` keeps the landing page's original light-paper and
deep-slate brightness while applying the selected color family's hues. Its
palette overrides stay within the studio template; documentation retains each
preset's distinct layer contrast. The initial background style uses the same
template-specific palette before the full stylesheets load.
`studioColorStyles.ts` emits the changed palette variables and documentation
color variables. A tiny inline startup script before the
stylesheet links restores `purestack-site-color` from local storage. The picker
uses the normal `PageScript` asset pipeline for its separately bundled runtime,
supports native radio keyboard controls, and follows changes from other tabs.
If storage is unavailable, the choice still applies for the current visit.

To change a selectable identity, edit its preset; `theme/studioSkinShared.ts`
defines the skin's baseline hues and shared typography. To add a
reusable section,
compose existing primitives in its own file under `components/`, register its
definition in `studioComponents.ts`, and register only the extra layout styles
Expand Down
28 changes: 28 additions & 0 deletions frontend/components/studioColorPicker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { defineComponent, html } from 'regor'
import { studioColorPresets } from '../theme/studioColorPresets'

const template = html`<details class="studio-color-picker">
<summary class="studio-color-picker__trigger" aria-label="Site color" title="Choose site color">
<Icon name="tabler:palette" aria-hidden="true"/>
</summary>
<fieldset class="studio-color-picker__panel">
<legend>Site color</legend>
<p>Choose your site’s color palette.</p>
<div class="studio-color-picker__options">
${studioColorPresets.map(renderColorOption).join('')}
</div>
<span class="studio-color-picker__note">Saved on this device.</span>
</fieldset>
</details><PageScript src="/color-picker.ts"/>`

export function defineStudioColorPickerComponent() {
return defineComponent(template)
}

function renderColorOption(preset: (typeof studioColorPresets)[number]) {
return html`<label class="studio-color-picker__option" data-site-color-option="${preset.id}">
<input type="radio" name="studio-site-color" value="${preset.id}"/>
<span class="studio-color-picker__swatch" aria-hidden="true"><Icon name="tabler:check"/></span>
<span>${preset.label}</span>
</label>`
}
2 changes: 2 additions & 0 deletions frontend/components/studioComponents.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { defineStudioActivityChartComponent } from './studioActivityChart'
import { defineStudioBrandComponent } from './studioBrand'
import { defineStudioCommandComponent } from './studioCommand'
import { defineStudioColorPickerComponent } from './studioColorPicker'
import { defineStudioCopyComponent } from './studioCopy'
import { defineStudioFaqComponent } from './studioFaq'
import { defineStudioFeatureComponent } from './studioFeature'
Expand All @@ -11,6 +12,7 @@ import { defineStudioWorkbenchComponent } from './studioWorkbench'
export function defineStudioComponents() {
return {
studioBrand: defineStudioBrandComponent(),
studioColorPicker: defineStudioColorPickerComponent(),
studioFeature: defineStudioFeatureComponent(),
studioPackage: defineStudioPackageComponent(),
studioRegorConnection: defineStudioRegorConnectionComponent(),
Expand Down
20 changes: 20 additions & 0 deletions frontend/docs/documentationLinks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/** Add native section permalinks without changing headings in live examples. */
export function addDocumentationLinks(document: Document) {
if (!document.body.classList.contains('template-doc')) return
if (document.body.classList.contains('preview')) return
const article = document.querySelector('.doc-main > .doc-content')
if (!article) return

for (const heading of article.children) {
if (!['H2', 'H3'].includes(heading.tagName) || !heading.id) continue
if (heading.querySelector('a')) continue
const link = document.createElement('a')
link.className = 'doc-heading-link'
link.setAttribute('href', `#${encodeURIComponent(heading.id)}`)
link.setAttribute('aria-label', `Link to ${heading.textContent?.trim()}`)
link.setAttribute('title', 'Link to this section')
link.setAttribute('data-pagefind-ignore', '')
link.textContent = '#'
heading.appendChild(link)
}
}
4 changes: 2 additions & 2 deletions frontend/docs/previewTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ const frameScript = `
document.querySelectorAll('.site-guide-sample details.sign-in').forEach(menu=>menu.open=true);
document.querySelectorAll('.site-guide-sample .consent__banner').forEach(banner=>{banner.hidden=false;banner.removeAttribute('aria-hidden')});
if(window.parent!==window){
const sync=()=>{document.documentElement.dataset.theme=parent.document.documentElement.dataset.theme||'light';document.documentElement.setAttribute('data-theme-ready','true')};
const sync=()=>{document.documentElement.dataset.theme=parent.document.documentElement.dataset.theme||'light';document.documentElement.dataset.siteColor=parent.document.documentElement.dataset.siteColor||'cobalt';document.documentElement.setAttribute('data-theme-ready','true')};
sync();
new MutationObserver(sync).observe(parent.document.documentElement,{attributes:true,attributeFilter:['data-theme']});
new MutationObserver(sync).observe(parent.document.documentElement,{attributes:true,attributeFilter:['data-theme','data-site-color']});
const minimumHeight=frameElement ? parseFloat(frameElement.style.height)||0 : 0;
const resize=()=>{if(frameElement)frameElement.style.height=Math.max(minimumHeight,Math.ceil(document.body.getBoundingClientRect().height))+'px'};
new ResizeObserver(resize).observe(document.body);
Expand Down
73 changes: 73 additions & 0 deletions frontend/purestack.studio/color-picker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
DEFAULT_SITE_COLOR,
isSiteColor,
SITE_COLOR_STORAGE_KEY,
studioColorPresets,
type SiteColor,
} from '../theme/studioColorPresets'

const root = document.documentElement
const pickerSelector = '.studio-color-picker'

function applyColor(color: SiteColor) {
root.dataset.siteColor = color
const label = studioColorPresets.find((preset) => preset.id === color)?.label
for (const picker of document.querySelectorAll<HTMLDetailsElement>(
pickerSelector,
)) {
picker
.querySelector('summary')
?.setAttribute('aria-label', `Site color: ${label}`)
for (const input of picker.querySelectorAll<HTMLInputElement>(
'input[type="radio"]',
)) {
input.checked = input.value === color
}
}
}

function saveColor(color: SiteColor) {
applyColor(color)
let message = 'Saved on this device.'
try {
localStorage.setItem(SITE_COLOR_STORAGE_KEY, color)
} catch {
message = 'Applied for this visit.'
}
for (const note of document.querySelectorAll('.studio-color-picker__note')) {
note.textContent = message
}
}

function closePickers(returnFocus = false) {
for (const picker of document.querySelectorAll<HTMLDetailsElement>(
`${pickerSelector}[open]`,
)) {
picker.open = false
if (returnFocus) picker.querySelector('summary')?.focus()
}
}

document.addEventListener('change', (event) => {
const input = event.target
if (!(input instanceof HTMLInputElement) || !input.closest(pickerSelector))
return
if (isSiteColor(input.value)) saveColor(input.value)
})

document.addEventListener('click', (event) => {
if (event.target instanceof Element && !event.target.closest(pickerSelector))
closePickers()
})

document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') closePickers(true)
})

window.addEventListener('storage', (event) => {
if (event.key !== SITE_COLOR_STORAGE_KEY && event.key !== null) return
applyColor(isSiteColor(event.newValue) ? event.newValue : DEFAULT_SITE_COLOR)
})

const initial = root.dataset.siteColor
applyColor(isSiteColor(initial) ? initial : DEFAULT_SITE_COLOR)
27 changes: 23 additions & 4 deletions frontend/purestack.studio/components/header.mdx
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
<TopBar tone="neutral" variant="glass" class="spotlight-from-top-right">
<TopBar tone="neutral" variant="surface">
<template #before-actions><StudioColorPicker/></template>
<template #actions>
<a class="topbar__icon" href="https://github.com/PureStackStudio/PureStack" target="_blank" rel="noopener" aria-label="PureStack GitHub repository">
<a
class="topbar__icon"
href="https://github.com/PureStackStudio/PureStack"
target="_blank"
rel="noopener"
aria-label="PureStack GitHub repository"
>
<Icon name="tabler:brand-github"/>
</a>
<a class="topbar__icon" href="https://regor.purestack.studio" target="_blank" rel="noopener" aria-label="Regor website">
<a
class="topbar__icon"
href="https://regor.purestack.studio"
target="_blank"
rel="noopener"
aria-label="Regor website"
>
<Icon name="lucide:orbit"/>
</a>
<a class="topbar__icon" href="https://x.com/purestackstudio" target="_blank" rel="noopener" aria-label="PureStack Studio on X">
<a
class="topbar__icon"
href="https://x.com/purestackstudio"
target="_blank"
rel="noopener"
aria-label="PureStack Studio on X"
>
<Icon name="tabler:brand-x"/>
</a>
</template>
Expand Down
2 changes: 1 addition & 1 deletion frontend/purestack.studio/components/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ nav:

Explore all 67 public components and browser entry points. Each guide brings together working examples, the source behind them, behavior guidance and a focused API reference.

<Panel tone="accent" variant="surface" bodyClass="p-4">
<Panel tone="accent" variant="surface" bodyClass="p-4" class="mb-5">
<SectionHeader
title="Find the right piece"
titleTag="h2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,8 @@ TopBar renders a fixed doc-nav-toggle ID and should appear once per document. It

<div class="api-contract">
<p>
Use the actions slot to add links or controls beside the theme toggle.
Use the before-actions slot to add controls before the theme toggle, and
the actions slot to add links or controls after it.
Register defineTopBarComponents and the selected logo component,
SearchBox, ThemeToggle, SignIn, Flex and Icon families. Standard SSG
registration provides these dependencies.
Expand Down
27 changes: 23 additions & 4 deletions frontend/purestack.studio/guides/header.mdx
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
<TopBar tone="neutral" variant="glass" class="spotlight-from-top-right">
<TopBar tone="neutral" variant="surface">
<template #before-actions><StudioColorPicker/></template>
<template #actions>
<a class="topbar__icon" href="https://github.com/PureStackStudio/PureStack" target="_blank" rel="noopener" aria-label="PureStack GitHub repository">
<a
class="topbar__icon"
href="https://github.com/PureStackStudio/PureStack"
target="_blank"
rel="noopener"
aria-label="PureStack GitHub repository"
>
<Icon name="tabler:brand-github"/>
</a>
<a class="topbar__icon" href="https://regor.purestack.studio" target="_blank" rel="noopener" aria-label="Regor website">
<a
class="topbar__icon"
href="https://regor.purestack.studio"
target="_blank"
rel="noopener"
aria-label="Regor website"
>
<Icon name="lucide:orbit"/>
</a>
<a class="topbar__icon" href="https://x.com/purestackstudio" target="_blank" rel="noopener" aria-label="PureStack Studio on X">
<a
class="topbar__icon"
href="https://x.com/purestackstudio"
target="_blank"
rel="noopener"
aria-label="PureStack Studio on X"
>
<Icon name="tabler:brand-x"/>
</a>
</template>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ nav:

PureStack generates a small vocabulary of reusable CSS classes alongside its component styles. Use them in MDX markup, in a component's `class` or `bodyClass` prop, or in a TypeScript `html` template. They are generated for each configured theme, so classes that use palette colors or font sizes follow the active skin.

<Panel tone="info" bodyClass="p-3">
<Panel tone="info" bodyClass="p-3" class="mb-5">
<p class="text-eyebrow mt-0">Release note</p>
<h2 class="mt-0 mb-2">A clearer way to build</h2>
<p class="mb-0">Spacing and type follow the site's theme.</p>
</Panel>

```mdx
<Panel tone="info" bodyClass="p-3">
<Panel tone="info" bodyClass="p-3" class="mb-5">
<p class="text-eyebrow mt-0">Release note</p>
<h2 class="mt-0 mb-2">A clearer way to build</h2>
<p class="mb-0">Spacing and type follow the site's theme.</p>
Expand Down
1 change: 1 addition & 0 deletions frontend/purestack.studio/header.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
</nav>
<div class="header-actions">
<div class="topbar__actions">
<StudioColorPicker/>
<ThemeToggle/>
<a
class="topbar__icon"
Expand Down
3 changes: 1 addition & 2 deletions frontend/purestack.studio/siteConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@
"includeIndex": true,
"sortBy": "order",
"roots": ["guides", "components"],
"variant": "spotlight",
"class": "spotlight-from-bottom-right"
"variant": "flat"
},
"sitemap": {
"enabled": true,
Expand Down
12 changes: 12 additions & 0 deletions frontend/studioPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@ import { card } from './demoStyle'
import { registerApiReferenceStyles } from './docs/apiReferenceStyles'
import { registerComponentGuideStyles } from './docs/componentGuide'
import { defineDocumentationComponents } from './docs/docsComponents'
import { addDocumentationLinks } from './docs/documentationLinks'
import { previewTemplate } from './docs/previewTemplate'
import { sitePreviewPages } from './docs/sitePreviews'
import { ensureLegalContent } from './ensureLegalContent'
import { registerDocumentationStyles } from './theme/documentationStyles'
import { studioSkin } from './theme/studioSkin'
import { registerStudioStyles } from './theme/studioStyles'
import { renderStudioColorHead } from './theme/studioColorHead'
import { registerStudioColorStyles } from './theme/studioColorStyles'
import { registerStudioColorPickerStyles } from './theme/studioColorPickerStyles'

/** Everything purestack.studio adds to PureStack: skin, components, layout, previews. */
export const studioPlugin = definePlugin({
name: 'purestack-studio',
skins: { studio: studioSkin },
head: renderStudioColorHead,
components: (config) => ({
...defineStudioComponents(),
...defineDocumentationComponents(config),
Expand Down Expand Up @@ -44,11 +50,17 @@ export const studioPlugin = definePlugin({
},
},
hooks: {
onPageDocument(_context, { document }) {
addDocumentationLinks(document)
},
async onConfigResolved({ config }) {
await ensureLegalContent(config.contentDir)
registerStudioStyles()
registerApiReferenceStyles()
registerComponentGuideStyles()
registerDocumentationStyles()
registerStudioColorStyles()
registerStudioColorPickerStyles()
},
},
})
47 changes: 47 additions & 0 deletions frontend/theme/documentationColors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Style } from '@purestack/ts-css'
import type { ThemePalette } from '@purestack/ts-style'

export type DocumentationColors = ReturnType<typeof createDocumentationColors>

export const documentationColorBindings: DocumentationColors = {
canvas: 'var(--ps-studio-doc-canvas)',
surface: 'var(--ps-studio-doc-surface)',
code: 'var(--ps-studio-doc-code)',
control: 'var(--ps-studio-doc-control)',
ink: 'var(--ps-studio-doc-ink)',
heading: 'var(--ps-studio-doc-heading)',
muted: 'var(--ps-studio-doc-muted)',
border: 'var(--ps-studio-doc-border)',
shadow: 'var(--ps-studio-doc-shadow)',
tint: 'var(--ps-studio-doc-tint)',
elevation: 'var(--ps-studio-doc-elevation)',
}

/** Documentation uses the preset's actual hierarchy, including inset and control layers. */
export function createDocumentationColors(palette: ThemePalette) {
const neutral = palette.semanticTone.neutral
const shadow = `color-mix(in srgb, ${neutral.overlay} 22%, transparent)`

return {
canvas: neutral.canvas,
surface: neutral.surface.rest.background,
code: neutral.surfaceAlt.rest.background,
control: neutral.button.rest.background,
ink: neutral.surfaceAlt.rest.text,
heading: neutral.text.default,
muted: neutral.text.subtle,
border: neutral.border.subtle,
shadow,
tint: palette.semanticTone.accent.surfaceAlt.rest.background,
elevation: palette.effect.softShadow,
}
}

export function setDocumentationColorVariables(
root: Style,
colors: DocumentationColors,
) {
for (const [name, value] of Object.entries(colors)) {
root.set(`--ps-studio-doc-${name}`, value)
}
}
Loading
Loading