Skip to content
Open
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
679 changes: 630 additions & 49 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ packages:
- 'website'

allowBuilds:
'@astryxdesign/cli': true
'@astryxdesign/core': true
canvas: true
esbuild: true
unrs-resolver: true
33 changes: 33 additions & 0 deletions website/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# AGENTS

Project-specific guidance for AI coding agents.

<!-- ASTRYX:START -->
Astryx v0.4.5 · 158 components
CLI: run every command as `pnpm exec astryx <cmd>` (shown below as `astryx ...`).

SETUP (once, in your app entry e.g. main.tsx) — without these, components render unstyled:
import "@astryxdesign/core/reset.css";
import "@astryxdesign/core/astryx.css";

WORKFLOW — discover, don't guess. Before writing UI:
1. `astryx build "<idea>"` — START HERE: returns a kit (closest [page] + [block]s + [component]s). No args = full playbook.
2. `astryx template <name> [--skeleton]` — scaffold the [page]/[block]s it named, or study their layout. Templates are reference code.
3. `astryx component <Name>` — props + examples for every component you use.

RULES:
- No <div> — components do all layout/spacing, page frame included.
- Frame first: read `astryx docs layout` before writing any page or screen — page frame, region widths, breakpoint behavior.
- Dense data = rows (Table, List/Item), never Card-wrapped list items; Card is for standalone widgets. Status = StatusDot/Token; Badge = counts only.
- Custom styling: component props first; else style/className with tokens — var(--color-*|--spacing-*|--radius-*). No raw hex/px. (No StyleX/Tailwind compiler here — don't use xstyle/utility classes.)
- Tokens for every value (`astryx docs tokens`). Brand/accent belongs in the theme (`astryx theme list` / `theme add <slug>`, or `astryx theme template` for a custom one) — never override --color-* in :root.
- SELF-CHECK before you finish: re-read the file and replace any raw <div>/<span> layout, imported .css/@apply, or hardcoded value (#hex, 16px) with the component or a token (var(--color-*|--spacing-*|…)). If unsure a component/prop exists, run `astryx component <Name>` / `astryx search "<thing>"`; don't hand-roll CSS.

MORE CLI:
search "<query>" find any component / hook / doc / template / block
component --list 158 components by category
template --list page + block recipes
docs <topic> color, elevation, icons, illustrations, internationalization, layout, migration, motion, principles, shape, spacing, styling, theme, tokens, typography
swizzle <Name> eject component source for deep customization
upgrade --apply run after any @astryxdesign/core bump
<!-- ASTRYX:END -->
96 changes: 96 additions & 0 deletions website/demo-nav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type {ComponentType, MouseEvent as ReactMouseEvent} from 'react';
import React, {useCallback, useEffect, useState} from 'react';
import {useAppShellMobile} from '@astryxdesign/core/AppShell';
import {TopNav, TopNavHeading, TopNavItem} from '@astryxdesign/core/TopNav';

export type DemoEntry = {
readonly label: string;
readonly description: string;
readonly component: ComponentType;
readonly file: `website/${string}.tsx`;
};

export type DemoRegistry = Record<string, DemoEntry> & {
readonly full: DemoEntry;
};
export type DemoKey<T extends DemoRegistry = DemoRegistry> = keyof T & string;

export function parseDemoKey<T extends DemoRegistry>(
search: string,
demos: T
): DemoKey<T> {
const demo = new URLSearchParams(search).get('demo');
if (demo != null && demo in demos) {
return demo as DemoKey<T>;
}
return 'full';
}

export function useDemoNavigation<T extends DemoRegistry>(
demos: T
): readonly [DemoKey<T>, (next: DemoKey<T>) => void] {
const [selectedKey, setSelectedKey] = useState(() =>
parseDemoKey(window.location.search, demos)
);

useEffect(() => {
function handlePopState() {
setSelectedKey(parseDemoKey(window.location.search, demos));
}
window.addEventListener('popstate', handlePopState);
return () => {
window.removeEventListener('popstate', handlePopState);
};
}, [demos]);

const navigate = useCallback((next: DemoKey<T>) => {
setSelectedKey((current) => {
if (current === next) {
return current;
}
history.pushState(null, '', `?demo=${next}`);
return next;
});
}, []);

useEffect(() => {
document.title = `QRCode.react Demo - ${demos[selectedKey].label}`;
}, [demos, selectedKey]);

return [selectedKey, navigate];
}

export function DemoTopNav<T extends DemoRegistry>(props: {
readonly demos: T;
readonly selectedKey: DemoKey<T>;
readonly onSelect: (next: DemoKey<T>) => void;
}) {
const {closeMobileNav} = useAppShellMobile();
const keys = Object.keys(props.demos) as Array<DemoKey<T>>;

return (
<TopNav
label="Demos"
heading={<TopNavHeading heading="QRCode.react" />}
endContent={
<TopNavItem
label={`v${__APP_VERSION__}`}
href={`https://www.npmjs.com/package/qrcode.react/v/${__APP_VERSION__}`}
/>
}>
{keys.map((key) => (
<TopNavItem
key={key}
label={props.demos[key].label}
href={`?demo=${key}`}
isSelected={key === props.selectedKey}
onClick={(e: ReactMouseEvent) => {
e.preventDefault();
props.onSelect(key);
closeMobileNav();
}}
/>
))}
</TopNav>
);
}
116 changes: 116 additions & 0 deletions website/demo-tool.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type {ReactElement, ReactNode} from 'react';
import React from 'react';
import {useAppShellMobile} from '@astryxdesign/core/AppShell';
import {Card} from '@astryxdesign/core/Card';
import {CodeBlock} from '@astryxdesign/core/CodeBlock';
import {Heading} from '@astryxdesign/core/Heading';
import {Layout, LayoutContent, LayoutPanel} from '@astryxdesign/core/Layout';
import {Text} from '@astryxdesign/core/Text';
import {Theme} from '@astryxdesign/core/theme';
import {VStack} from '@astryxdesign/core/VStack';
import {neutralTheme} from './theme/neutralTheme';

type DemoToolProps = {
readonly title: string;
readonly description: string;
readonly content: ReactNode;
readonly panel: ReactNode;
};

type QrPreviewProps = {
readonly title: string;
readonly children: ReactNode;
readonly code?: string;
};

function DemoHeading(props: {
readonly title: string;
readonly description: string;
}) {
return (
<VStack gap={1}>
<Heading level={1}>{props.title}</Heading>
<Text display="block" color="secondary">
{props.description}
</Text>
</VStack>
);
}

function DemoPage(props: {
readonly title: string;
readonly description: string;
readonly children: ReactNode;
}): ReactElement {
return (
<VStack gap={6} padding={6}>
<DemoHeading title={props.title} description={props.description} />
{props.children}
</VStack>
);
}

function DemoTool(props: DemoToolProps): ReactElement {
const {isMobile} = useAppShellMobile();

if (isMobile) {
return (
<VStack gap={4} padding={4}>
<DemoHeading title={props.title} description={props.description} />
{props.panel}
{props.content}
</VStack>
);
}

return (
<Layout
height="fill"
start={
<LayoutPanel
label="Properties"
hasDivider
isScrollable
width={380}
padding={4}>
{props.panel}
</LayoutPanel>
}
content={
<LayoutContent padding={4}>
<VStack gap={4}>
<DemoHeading title={props.title} description={props.description} />
{props.content}
</VStack>
</LayoutContent>
}
/>
);
}

function QrPreview(props: QrPreviewProps): ReactElement {
return (
<Card>
<VStack gap={3}>
<Heading level={2}>{props.title}</Heading>
<Theme theme={neutralTheme} mode="light">
<VStack
className="qr-preview-plate"
padding={3}
style={{
background: 'var(--color-background-surface)',
borderRadius: 'var(--radius-inner)',
width: 'fit-content',
}}>
{props.children}
</VStack>
</Theme>
{props.code != null ? (
<CodeBlock code={props.code} language="tsx" />
) : null}
</VStack>
</Card>
);
}

export {DemoTool, DemoPage, QrPreview};
108 changes: 29 additions & 79 deletions website/demo.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import React, {
useState,
StrictMode,
useCallback,
useEffect,
version as reactVersion,
} from 'react';
import '@astryxdesign/core/reset.css';
import '@astryxdesign/core/astryx.css';

import React, {StrictMode} from 'react';
import {createRoot} from 'react-dom/client';
import {version as reactDOMVersion} from 'react-dom';
import {FullDemo} from './full';
import {AppShell} from '@astryxdesign/core/AppShell';
import {Theme} from '@astryxdesign/core/theme';
import {DemoTopNav, useDemoNavigation} from './demo-nav';
import type {DemoEntry} from './demo-nav';
import {DownloadDemo} from './download';
import {FullDemo} from './full';
import {ImageDemo} from './image';
import {PayloadsDemo} from './payloads';
import {neutralTheme} from './theme/neutralTheme';

const DEMOS = {
full: {
Expand Down Expand Up @@ -40,86 +41,35 @@ const DEMOS = {
component: PayloadsDemo,
file: 'website/payloads.tsx',
},
};

type DemoComponentKeys = keyof typeof DEMOS;

function getInitialDemo(): DemoComponentKeys {
const urlParams = new URLSearchParams(window.location.search);
const demo = urlParams.get('demo');
return demo && demo in DEMOS ? (demo as DemoComponentKeys) : 'full';
}
} satisfies Record<string, DemoEntry> & {readonly full: DemoEntry};

function Demo() {
const [demo, setDemo] = useState<DemoComponentKeys>(getInitialDemo());

const handleDemoChange = useCallback((nextDemo: DemoComponentKeys) => {
setDemo(nextDemo);
history.pushState({demo: nextDemo}, '', `?demo=${nextDemo}`);
}, []);

useEffect(() => {
function handlePopState(e: PopStateEvent) {
setDemo(e.state?.demo || 'full');
}
window.addEventListener('popstate', handlePopState);
return () => {
window.removeEventListener('popstate', handlePopState);
};
}, []);

useEffect(() => {
document.title = `QRCode.react Demo - ${DEMOS[demo].label}`;
}, [demo]);

const demoData = DEMOS[demo];
const [demo, navigateToDemo] = useDemoNavigation(DEMOS);
const ActiveDemo = DEMOS[demo].component;

const Component = demoData.component;
return (
<>
<div className="container">
<h1>
QRCode.react Demo -{' '}
<a
href={`https://www.npmjs.com/package/qrcode.react/v/${__APP_VERSION__}`}>
v{__APP_VERSION__}
</a>
</h1>
</div>
<div className="container">
<p>
Using <code>react@{reactVersion}</code> &{' '}
<code>react-dom@{reactDOMVersion}</code>
</p>
</div>
<div className="container">
<label>
<select
onChange={(e) =>
handleDemoChange(e.target.value as DemoComponentKeys)
}
value={demo}>
{Object.keys(DEMOS).map((key) => {
const data = DEMOS[key as DemoComponentKeys];
return (
<option key={key} value={key} title={data.description}>
{data.label} - {data.description}
</option>
);
})}
</select>
</label>
</div>
<hr />
<Component />
</>
<AppShell
height={demo === 'full' ? 'fill' : 'auto'}
contentPadding={0}
variant="section"
topNav={
<DemoTopNav
demos={DEMOS}
selectedKey={demo}
onSelect={navigateToDemo}
/>
}>
<ActiveDemo />
</AppShell>
);
}

const container = document.getElementById('demo');
const root = createRoot(container!);
root.render(
<StrictMode>
<Demo />
<Theme theme={neutralTheme} mode="system">
<Demo />
</Theme>
</StrictMode>
);
Loading