mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(site): remove style from urls (#378)
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
---
|
||||
|
||||
import type { HTMLAttributes } from 'astro/types';
|
||||
import { isValidFramework, isValidStyleForFramework } from '@/types/docs';
|
||||
import { isValidFramework } from '@/types/docs';
|
||||
import { resolveDocsLinkUrl } from '@/utils/docs/routing';
|
||||
import A from '../typography/A.astro';
|
||||
|
||||
@@ -10,18 +9,14 @@ interface Props extends Omit<HTMLAttributes<'a'>, 'href'> {
|
||||
}
|
||||
const { slug } = Astro.props;
|
||||
|
||||
const { framework: paramFramework, style: paramStyle } = Astro.params;
|
||||
const { framework: paramFramework } = Astro.params;
|
||||
if (!paramFramework || !isValidFramework(paramFramework)) {
|
||||
throw new Error(`Invalid or missing framework param "${paramFramework ?? 'undefined'}".`);
|
||||
}
|
||||
if (!paramStyle || !isValidStyleForFramework(paramFramework, paramStyle)) {
|
||||
throw new Error(`Invalid style param "${paramStyle}" for framework "${paramFramework}".`);
|
||||
}
|
||||
|
||||
const { url: href } = resolveDocsLinkUrl({
|
||||
targetSlug: slug,
|
||||
contextFramework: paramFramework,
|
||||
contextStyle: paramStyle,
|
||||
});
|
||||
---
|
||||
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
---
|
||||
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import type { Guide, SupportedFramework, SupportedStyle } from '@/types/docs';
|
||||
import type { Guide, SupportedFramework } from '@/types/docs';
|
||||
|
||||
type Props<F extends SupportedFramework = SupportedFramework> = {
|
||||
type Props = {
|
||||
prev: Guide | null;
|
||||
next: Guide | null;
|
||||
framework: F;
|
||||
style: SupportedStyle<F>;
|
||||
framework: SupportedFramework;
|
||||
docTitles: Map<string, string>;
|
||||
};
|
||||
|
||||
const { prev, next, framework, style, docTitles } = Astro.props;
|
||||
const { prev, next, framework, docTitles } = Astro.props;
|
||||
|
||||
function getGuideUrl<F extends SupportedFramework>(framework: F, style: SupportedStyle<F>, slug: string): string {
|
||||
return `/docs/framework/${framework}/style/${style}/${slug}`;
|
||||
function getGuideUrl(framework: SupportedFramework, slug: string): string {
|
||||
return `/docs/framework/${framework}/${slug}`;
|
||||
}
|
||||
---
|
||||
|
||||
@@ -22,7 +20,7 @@ function getGuideUrl<F extends SupportedFramework>(framework: F, style: Supporte
|
||||
{
|
||||
prev ? (
|
||||
<a
|
||||
href={getGuideUrl(framework, style, prev.slug)}
|
||||
href={getGuideUrl(framework, prev.slug)}
|
||||
class="grid items-center gap-x-2 p-4 border border-light-40 dark:border-dark-80 rounded-lg intent:border-dark-40 dark:intent:border-dark-40"
|
||||
style="grid-template-areas: 'empty direction' 'icon title'; grid-template-columns: auto minmax(0,1fr); grid-template-rows: auto minmax(0,1fr);"
|
||||
>
|
||||
@@ -43,7 +41,7 @@ function getGuideUrl<F extends SupportedFramework>(framework: F, style: Supporte
|
||||
{
|
||||
next ? (
|
||||
<a
|
||||
href={getGuideUrl(framework, style, next.slug)}
|
||||
href={getGuideUrl(framework, next.slug)}
|
||||
class="grid items-center gap-x-2 p-4 border border-light-40 dark:border-dark-80 rounded-lg intent:border-dark-40 text-right dark:intent:border-dark-40"
|
||||
style="grid-template-areas: 'direction empty' 'title icon';grid-template-columns: minmax(0,1fr) auto; grid-template-rows: auto minmax(0,1fr);"
|
||||
>
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
---
|
||||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { PreferenceUpdater } from '@/components/docs/PreferenceUpdater';
|
||||
import SidebarItem from '@/components/docs/SidebarItem.astro';
|
||||
import type { SupportedFramework, SupportedStyle } from '@/types/docs';
|
||||
import type { SupportedFramework } from '@/types/docs';
|
||||
import { filterSidebar } from '@/utils/docs/sidebar';
|
||||
|
||||
type Props<F extends SupportedFramework = SupportedFramework> = {
|
||||
framework: F;
|
||||
style: SupportedStyle<F>;
|
||||
type Props = {
|
||||
framework: SupportedFramework;
|
||||
docTitles: Map<string, string>;
|
||||
class?: string;
|
||||
};
|
||||
|
||||
const { framework, style, docTitles, class: className } = Astro.props;
|
||||
const filteredSidebar = filterSidebar(framework, style);
|
||||
const { framework, docTitles, class: className } = Astro.props;
|
||||
const filteredSidebar = filterSidebar(framework);
|
||||
---
|
||||
|
||||
<div class={clsx('bg-light-80 dark:bg-dark-100', className)}>
|
||||
<slot />
|
||||
<PreferenceUpdater client:idle currentFramework={framework} currentStyle={style} />
|
||||
<PreferenceUpdater client:idle currentFramework={framework} />
|
||||
<nav class="p-6">
|
||||
{
|
||||
filteredSidebar.map((item) => (
|
||||
<SidebarItem item={item} framework={framework} style={style} docTitles={docTitles} />
|
||||
))
|
||||
}
|
||||
{filteredSidebar.map((item) => <SidebarItem item={item} framework={framework} docTitles={docTitles} />)}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -1,39 +1,37 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useEffect } from 'react';
|
||||
import { currentFramework, currentStyle } from '@/stores/preferences';
|
||||
import type { SupportedFramework, SupportedStyle } from '@/types/docs';
|
||||
import { getPreferenceClient, setPreferenceClient } from '@/utils/docs/preferences';
|
||||
import { currentFramework } from '@/stores/preferences';
|
||||
import type { SupportedFramework } from '@/types/docs';
|
||||
import { getFrameworkPreferenceClient, setFrameworkPreferenceClient } from '@/utils/docs/preferences';
|
||||
|
||||
/**
|
||||
* PreferenceSync keeps the nanostore in sync with cookies.
|
||||
* PreferenceSync keeps the framework nanostore in sync with cookies.
|
||||
*
|
||||
* On mount: Reads cookies → initializes store
|
||||
* On store change: Writes to cookies
|
||||
*
|
||||
* This component should be loaded with client:load in the base layout
|
||||
* Style preferences are handled via localStorage by StyleInit and PreferenceUpdater.
|
||||
*
|
||||
* This component should be loaded with client:idle in the base layout
|
||||
* to ensure preferences are available immediately.
|
||||
*/
|
||||
export function PreferenceSync() {
|
||||
const framework = useStore(currentFramework);
|
||||
const style = useStore(currentStyle);
|
||||
|
||||
// Initialize store from cookies on mount
|
||||
useEffect(() => {
|
||||
const prefs = getPreferenceClient();
|
||||
if (prefs.framework) {
|
||||
currentFramework.set(prefs.framework);
|
||||
}
|
||||
if (prefs.style) {
|
||||
currentStyle.set(prefs.style);
|
||||
const prefs = getFrameworkPreferenceClient();
|
||||
if (prefs) {
|
||||
currentFramework.set(prefs);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Sync store changes to cookies
|
||||
useEffect(() => {
|
||||
if (framework && style) {
|
||||
setPreferenceClient(framework as SupportedFramework, style as SupportedStyle<typeof framework>);
|
||||
if (framework) {
|
||||
setFrameworkPreferenceClient(framework as SupportedFramework);
|
||||
}
|
||||
}, [framework, style]);
|
||||
}, [framework]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import { useEffect } from 'react';
|
||||
import { currentFramework as frameworkStore, currentStyle as styleStore } from '@/stores/preferences';
|
||||
import type { SupportedFramework, SupportedStyle } from '@/types/docs';
|
||||
import { getDefaultStyle, type SupportedFramework } from '@/types/docs';
|
||||
import { getStylePreferenceClient } from '@/utils/docs/preferences';
|
||||
|
||||
interface PreferenceUpdaterProps<F extends SupportedFramework = SupportedFramework> {
|
||||
currentFramework: F;
|
||||
currentStyle: SupportedStyle<F>;
|
||||
interface PreferenceUpdaterProps {
|
||||
currentFramework: SupportedFramework;
|
||||
}
|
||||
|
||||
/**
|
||||
* PreferenceUpdater component updates the preference nanostore based on URL params.
|
||||
* PreferenceUpdater component updates the preference nanostore based on URL params and localStorage.
|
||||
* This component is loaded with client:idle directive on docs pages, making it non-blocking.
|
||||
* It updates the nanostore whenever framework or style from URL changes.
|
||||
* PreferenceSync handles persisting to cookies.
|
||||
* It updates the framework store from URL params and style store from localStorage.
|
||||
* PreferenceSync handles persisting framework to cookies.
|
||||
*/
|
||||
export function PreferenceUpdater<F extends SupportedFramework = SupportedFramework>({
|
||||
currentFramework,
|
||||
currentStyle,
|
||||
}: PreferenceUpdaterProps<F>) {
|
||||
export function PreferenceUpdater({ currentFramework }: PreferenceUpdaterProps) {
|
||||
useEffect(() => {
|
||||
// Update framework store from URL
|
||||
frameworkStore.set(currentFramework);
|
||||
styleStore.set(currentStyle);
|
||||
}, [currentFramework, currentStyle]);
|
||||
|
||||
// Read style from localStorage (StyleInit guarantees a valid value exists)
|
||||
// Fallback to default if React hydrates before StyleInit completes
|
||||
const style = getStylePreferenceClient(currentFramework) ?? getDefaultStyle(currentFramework);
|
||||
styleStore.set(style);
|
||||
}, [currentFramework]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Select } from '@/components/Select';
|
||||
import type { AnySupportedStyle, SupportedFramework, SupportedStyle } from '@/types/docs';
|
||||
import { currentStyle as styleStore } from '@/stores/preferences';
|
||||
import type { AnySupportedStyle, SupportedFramework } from '@/types/docs';
|
||||
import { FRAMEWORK_STYLES, isValidFramework, isValidStyleForFramework, SUPPORTED_FRAMEWORKS } from '@/types/docs';
|
||||
import { resolveFrameworkChange, resolveStyleChange } from '@/utils/docs/routing';
|
||||
import { setStylePreferenceClient, updateStyleAttribute } from '@/utils/docs/preferences';
|
||||
import { resolveFrameworkChange } from '@/utils/docs/routing';
|
||||
|
||||
interface SelectorProps<T extends SupportedFramework> {
|
||||
currentFramework: T;
|
||||
currentStyle: SupportedStyle<T>;
|
||||
interface SelectorProps {
|
||||
currentFramework: SupportedFramework;
|
||||
currentSlug: string;
|
||||
}
|
||||
|
||||
export function Selectors({ currentFramework, currentStyle, currentSlug }: SelectorProps<SupportedFramework>) {
|
||||
// TODO: use astro view transitions to preserve scroll position when switching from the same slug to the same slug
|
||||
export function Selectors({ currentFramework, currentSlug }: SelectorProps) {
|
||||
// Read style from nanostore (StyleInit + PreferenceUpdater guarantee a valid value)
|
||||
// Guard against React hydrating before style is initialized
|
||||
const currentStyle = useStore(styleStore);
|
||||
if (!currentStyle) return null;
|
||||
|
||||
const handleFrameworkChange = (newFramework: SupportedFramework | null) => {
|
||||
if (newFramework === null) return;
|
||||
if (!isValidFramework(newFramework)) return;
|
||||
|
||||
const { url, shouldReplace } = resolveFrameworkChange({
|
||||
currentFramework,
|
||||
currentStyle,
|
||||
currentSlug,
|
||||
newFramework,
|
||||
});
|
||||
@@ -35,20 +40,12 @@ export function Selectors({ currentFramework, currentStyle, currentSlug }: Selec
|
||||
if (newStyle === null) return;
|
||||
if (!isValidStyleForFramework(currentFramework, newStyle)) return;
|
||||
|
||||
const { url, shouldReplace } = resolveStyleChange({
|
||||
currentFramework,
|
||||
currentStyle,
|
||||
currentSlug,
|
||||
newStyle,
|
||||
});
|
||||
|
||||
if (shouldReplace) {
|
||||
// Maintaining the current slug, navigate without pushing onto the history stack
|
||||
window.location.replace(url);
|
||||
} else {
|
||||
// Changing slug, use normal navigation
|
||||
window.location.href = url;
|
||||
}
|
||||
// Update localStorage for this framework
|
||||
setStylePreferenceClient(currentFramework, newStyle);
|
||||
// Update DOM attribute
|
||||
updateStyleAttribute(newStyle);
|
||||
// Update nanostore for React components
|
||||
styleStore.set(newStyle);
|
||||
};
|
||||
|
||||
const availableStyles = FRAMEWORK_STYLES[currentFramework];
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
---
|
||||
|
||||
import GithubSlugger from 'github-slugger';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import type { Guide, Section, SupportedFramework, SupportedStyle } from '@/types/docs';
|
||||
import type { Guide, Section, SupportedFramework } from '@/types/docs';
|
||||
import { isSection } from '@/types/docs';
|
||||
|
||||
type Props<F extends SupportedFramework = SupportedFramework> = {
|
||||
type Props = {
|
||||
item: Guide | Section;
|
||||
framework: F;
|
||||
style: SupportedStyle<F>;
|
||||
framework: SupportedFramework;
|
||||
docTitles: Map<string, string>;
|
||||
depth?: number;
|
||||
};
|
||||
|
||||
const { item, framework, style, docTitles, depth = 0 } = Astro.props;
|
||||
const { item, framework, docTitles, depth = 0 } = Astro.props;
|
||||
const currentPath = Astro.url.pathname;
|
||||
const slugger = new GithubSlugger();
|
||||
|
||||
@@ -22,7 +20,7 @@ function containsActivePath(item: Guide | Section): boolean {
|
||||
if (isSection(item)) {
|
||||
return item.contents.some((child) => containsActivePath(child));
|
||||
} else {
|
||||
const itemPath = `/docs/framework/${framework}/style/${style}/${item.slug}`;
|
||||
const itemPath = `/docs/framework/${framework}/${item.slug}`;
|
||||
return itemPath === currentPath;
|
||||
}
|
||||
}
|
||||
@@ -52,7 +50,7 @@ const isActive = containsActivePath(item);
|
||||
</summary>
|
||||
<div>
|
||||
{item.contents.map((contentItem) => (
|
||||
<Astro.self item={contentItem} framework={framework} style={style} docTitles={docTitles} depth={depth + 1} />
|
||||
<Astro.self item={contentItem} framework={framework} docTitles={docTitles} depth={depth + 1} />
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
@@ -67,7 +65,7 @@ const isActive = containsActivePath(item);
|
||||
isActive ? 'text-dark-100 dark:text-light-100 border-current ' : 'border-light-40 dark:border-dark-40',
|
||||
]}
|
||||
style={`padding-left: calc(var(--spacing) * ${depth * 4})`}
|
||||
href={`/docs/framework/${framework}/style/${style}/${item.slug}`}
|
||||
href={`/docs/framework/${framework}/${item.slug}`}
|
||||
>
|
||||
{item.devOnly ? '[Dev only] ' : ''}
|
||||
{item.sidebarLabel || docTitles.get(item.slug) || item.slug}
|
||||
|
||||
@@ -1,37 +1,38 @@
|
||||
---
|
||||
|
||||
import type { AnySupportedStyle } from '@/types/docs';
|
||||
/**
|
||||
* Conditionally shows content based on the user's selected style preference.
|
||||
*
|
||||
* When `styles` is provided: Content is wrapped in a div with `data-for-style`
|
||||
* attribute. CSS rules in StyleInit.astro control visibility based on `html[data-style]`.
|
||||
*
|
||||
* When `styles` is omitted: Content is rendered directly without any wrapper,
|
||||
* making it visible regardless of style preference.
|
||||
*
|
||||
* @example
|
||||
* <!-- Show only for CSS style -->
|
||||
* <StyleCase styles={['css']}>CSS-specific content</StyleCase>
|
||||
*
|
||||
* <!-- Show for multiple styles -->
|
||||
* <StyleCase styles={['css', 'tailwind']}>Shared content</StyleCase>
|
||||
*
|
||||
* <!-- Always visible (no styles prop) -->
|
||||
* <StyleCase>Universal content</StyleCase>
|
||||
*/
|
||||
import type { AnySupportedStyle } from "@/types/docs";
|
||||
|
||||
interface Props {
|
||||
styles?: AnySupportedStyle[];
|
||||
styles?: AnySupportedStyle[];
|
||||
}
|
||||
|
||||
const { styles } = Astro.props;
|
||||
const { style } = Astro.params;
|
||||
|
||||
// Only render if current style matches, or if no styles specified (all styles)
|
||||
const shouldRender = !styles || styles.includes(style as AnySupportedStyle);
|
||||
---
|
||||
|
||||
{
|
||||
/*
|
||||
What I WANT to do is
|
||||
```
|
||||
{shouldRender && <slot />}
|
||||
```
|
||||
|
||||
But I'm running into some crazy problems with hydration.
|
||||
Putting client:load in one of these conditionals causes Astro to just give up hydrating the whole app for some reason.
|
||||
TODO: fix this
|
||||
|
||||
So, while I debug those... let's do this
|
||||
*/
|
||||
!styles ? (
|
||||
<slot />
|
||||
) : (
|
||||
<div class="contents" data-for-style={styles.join(" ")}>
|
||||
<slot />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div
|
||||
class="contents"
|
||||
hidden={!shouldRender}
|
||||
data-pagefind-ignore={shouldRender ? undefined : 'all'}
|
||||
data-llms-ignore={shouldRender ? undefined : 'all'}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
import { FRAMEWORK_STYLES, getDefaultStyle, SUPPORTED_FRAMEWORKS } from '@/types/docs';
|
||||
|
||||
const STYLE_KEY_PREFIX = 'vjs_docs_style_';
|
||||
|
||||
// Derive values from the single source of truth (FRAMEWORK_STYLES)
|
||||
// Pass as plain objects since define:vars serializes to JSON
|
||||
const frameworkStylesConfig = Object.fromEntries(SUPPORTED_FRAMEWORKS.map((fw) => [fw, [...FRAMEWORK_STYLES[fw]]]));
|
||||
const defaultStyleConfig = getDefaultStyle(SUPPORTED_FRAMEWORKS[0]);
|
||||
|
||||
// Collect all unique styles across all frameworks for CSS generation
|
||||
const allStyles = [...new Set(Object.values(FRAMEWORK_STYLES).flat())];
|
||||
---
|
||||
|
||||
{/* Style visibility CSS rules - generated from FRAMEWORK_STYLES to stay in sync */}
|
||||
<style set:html={`
|
||||
[data-for-style] { display: none; }
|
||||
${allStyles.map(style => `html[data-style='${style}'] [data-for-style~='${style}'] { display: contents; }`).join('\n ')}
|
||||
`}></style>
|
||||
|
||||
{/*
|
||||
Style initialization script - runs before paint to prevent FOUC.
|
||||
|
||||
Supports ?style= query param for deep-linking (e.g., ?style=tailwind).
|
||||
This allows external links to specify a style preference. The style is
|
||||
validated against valid styles for the current framework and persisted
|
||||
to localStorage.
|
||||
*/}
|
||||
<script
|
||||
is:inline
|
||||
define:vars={{ keyPrefix: STYLE_KEY_PREFIX, frameworkStyles: frameworkStylesConfig, defaultStyle: defaultStyleConfig }}
|
||||
>
|
||||
if (typeof localStorage !== 'undefined' && typeof document !== 'undefined') {
|
||||
// Extract framework from URL: /docs/framework/{framework}/...
|
||||
const pathMatch = window.location.pathname.match(/\/docs\/framework\/([^/]+)/);
|
||||
const framework = pathMatch?.[1];
|
||||
|
||||
const validStyles = frameworkStyles[framework];
|
||||
if (framework && validStyles) {
|
||||
const storageKey = keyPrefix + framework;
|
||||
|
||||
// Check for ?style= query param (enables deep-linking)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const queryStyle = urlParams.get('style');
|
||||
|
||||
let style;
|
||||
if (queryStyle && validStyles.includes(queryStyle)) {
|
||||
// Query param overrides and persists to localStorage for this framework
|
||||
style = queryStyle;
|
||||
localStorage.setItem(storageKey, style);
|
||||
} else {
|
||||
// Fall back to localStorage or default
|
||||
style = localStorage.getItem(storageKey);
|
||||
if (!style || !validStyles.includes(style)) {
|
||||
style = defaultStyle;
|
||||
localStorage.setItem(storageKey, style);
|
||||
}
|
||||
}
|
||||
|
||||
document.documentElement.dataset.style = style;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user