chore: sync codebase remediation, gameplay systems, and docs
Security & infrastructure: - Remove unused services/ (auth, spacetimedb) and auth.db - Add .env.example template, expand .gitignore for env/db files - Add GitHub Actions CI + commitlint config and workflows - Add manual vendor chunking and source maps to docs/site vite configs Shared UI & docs app: - Add ARIA props and focus-visible rings to Button/Panel - Add ButtonAsLink primitive; use shared Button in NotFound - Wire @void-nav/ui into docs app; refresh content pages - Replace Todo page with Kanban board Gameplay (Bevy): - Add ai module (behavior, faction, navigation, perception, spawning, states) - Add narrative module (events, history, synthesis, ui) - Refine galaxy contents and in-system flight/scene systems
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
export { Button } from "./primitives/Button";
|
||||
export type { ButtonProps } from "./primitives/Button";
|
||||
export { ButtonAsLink } from "./primitives/ButtonAsLink";
|
||||
export { Panel } from "./primitives/Panel";
|
||||
export type { PanelProps } from "./primitives/Panel";
|
||||
|
||||
@@ -6,49 +6,95 @@ type BaseProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
tone?: ButtonTone;
|
||||
/** Accessible label for screen readers (overrides children text) */
|
||||
"aria-label"?: string;
|
||||
/** Indicates if this button is part of a larger grouping */
|
||||
"aria-haspopup"?: boolean | "menu" | "listbox" | "tree" | "grid" | "dialog";
|
||||
/** Identifies the element controlled by this button */
|
||||
"aria-controls"?: string;
|
||||
/** Identifies the expanded state (for toggle buttons) */
|
||||
"aria-expanded"?: boolean;
|
||||
/** Identifies the pressed state (for toggle buttons) */
|
||||
"aria-pressed"?: boolean;
|
||||
/** Keyboard navigation: explicit tab index */
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
type NativeButtonProps = BaseProps &
|
||||
ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
href?: never;
|
||||
to?: never;
|
||||
};
|
||||
|
||||
type AnchorButtonProps = BaseProps &
|
||||
AnchorHTMLAttributes<HTMLAnchorElement> & {
|
||||
href: string;
|
||||
to?: never;
|
||||
};
|
||||
|
||||
/**
|
||||
* Button component with accessibility support and multiple visual tones.
|
||||
*
|
||||
* Can be rendered as:
|
||||
* - `<button>` (default)
|
||||
* - `<a>` (when `href` is provided)
|
||||
*
|
||||
* Note: For React Router links, use the ButtonAsLink component instead.
|
||||
*
|
||||
* @example
|
||||
* // Native button
|
||||
* <Button tone="primary" onClick={handleClick}>Click me</Button>
|
||||
*
|
||||
* @example
|
||||
* // External link
|
||||
* <Button href="https://example.com" tone="secondary">External</Button>
|
||||
*/
|
||||
export type ButtonProps = NativeButtonProps | AnchorButtonProps;
|
||||
|
||||
const tones: Record<ButtonTone, string> = {
|
||||
primary: "border-accent bg-accent text-bg hover:bg-accent-hover",
|
||||
secondary: "border-border bg-surface-raised text-fg hover:border-border-light hover:bg-surface-hover",
|
||||
ghost: "border-transparent bg-transparent text-cyan hover:border-border-light hover:bg-surface",
|
||||
primary: "border-accent bg-accent text-bg hover:bg-accent-hover focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg",
|
||||
secondary: "border-border bg-surface-raised text-fg hover:border-border-light hover:bg-surface-hover focus-visible:ring-2 focus-visible:ring-border focus-visible:ring-offset-2 focus-visible:ring-offset-bg",
|
||||
ghost: "border-transparent bg-transparent text-cyan hover:border-border-light hover:bg-surface focus-visible:ring-2 focus-visible:ring-cyan focus-visible:ring-offset-2 focus-visible:ring-offset-bg",
|
||||
};
|
||||
|
||||
export function Button({ children, className = "", tone = "secondary", ...props }: ButtonProps) {
|
||||
const classes = [
|
||||
/**
|
||||
* Shared button styles and accessibility support.
|
||||
* Used by both Button and ButtonAsLink components.
|
||||
*/
|
||||
export function useButtonProps(tone: ButtonTone = "secondary", className: string = "") {
|
||||
return [
|
||||
"inline-flex min-h-10 cursor-pointer items-center justify-center gap-2 rounded-md border px-4 py-2 text-sm font-semibold transition-colors duration-150 disabled:cursor-not-allowed disabled:opacity-55",
|
||||
"focus-visible:outline-none",
|
||||
tones[tone],
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
if ("href" in props) {
|
||||
const anchorProps = props as Omit<AnchorButtonProps, keyof BaseProps>;
|
||||
export function Button({ children, className = "", tone = "secondary", ...props }: ButtonProps) {
|
||||
const classes = useButtonProps(tone, className);
|
||||
|
||||
// Extract accessibility props to spread on the rendered element
|
||||
const { "aria-label": ariaLabel, "aria-haspopup": ariaHaspopup, "aria-controls": ariaControls, "aria-expanded": ariaExpanded, "aria-pressed": ariaPressed, tabIndex, ...restProps } = props;
|
||||
const ariaProps = { "aria-label": ariaLabel, "aria-haspopup": ariaHaspopup, "aria-controls": ariaControls, "aria-expanded": ariaExpanded, "aria-pressed": ariaPressed, tabIndex };
|
||||
|
||||
// Handle native anchor (href prop)
|
||||
if ("href" in restProps && restProps.href) {
|
||||
const anchorProps = restProps as Omit<AnchorButtonProps, keyof BaseProps>;
|
||||
|
||||
return (
|
||||
<a className={classes} {...anchorProps}>
|
||||
<a className={classes} {...anchorProps} {...ariaProps}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const buttonProps = props as Omit<NativeButtonProps, keyof BaseProps>;
|
||||
// Handle native button
|
||||
const buttonProps = restProps as Omit<NativeButtonProps, keyof BaseProps>;
|
||||
|
||||
return (
|
||||
<button className={classes} {...buttonProps}>
|
||||
<button className={classes} type="button" {...buttonProps} {...ariaProps}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
49
packages/ui/src/primitives/ButtonAsLink.tsx
Normal file
49
packages/ui/src/primitives/ButtonAsLink.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { LinkProps } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useButtonProps } from "./Button";
|
||||
|
||||
type ButtonTone = "primary" | "secondary" | "ghost";
|
||||
|
||||
type ButtonAsLinkProps = LinkProps & {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
tone?: ButtonTone;
|
||||
/** Accessible label for screen readers (overrides children text) */
|
||||
"aria-label"?: string;
|
||||
/** Indicates if this button is part of a larger grouping */
|
||||
"aria-haspopup"?: boolean | "menu" | "listbox" | "tree" | "grid" | "dialog";
|
||||
/** Identifies the element controlled by this button */
|
||||
"aria-controls"?: string;
|
||||
/** Identifies the expanded state (for toggle buttons) */
|
||||
"aria-expanded"?: boolean;
|
||||
/** Identifies the pressed state (for toggle buttons) */
|
||||
"aria-pressed"?: boolean;
|
||||
/** Keyboard navigation: explicit tab index */
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Button component rendered as a React Router Link.
|
||||
*
|
||||
* Use this when you need button styling with React Router navigation.
|
||||
* For external links, use the regular Button component with an `href` prop.
|
||||
*
|
||||
* @example
|
||||
* import { ButtonAsLink } from "@void-nav/ui";
|
||||
*
|
||||
* <ButtonAsLink to="/docs" tone="primary">Go to Docs</ButtonAsLink>
|
||||
*/
|
||||
export function ButtonAsLink({ children, className = "", tone = "secondary", ...props }: ButtonAsLinkProps) {
|
||||
const classes = useButtonProps(tone, className);
|
||||
|
||||
// Extract accessibility props to spread on the rendered element
|
||||
const { "aria-label": ariaLabel, "aria-haspopup": ariaHaspopup, "aria-controls": ariaControls, "aria-expanded": ariaExpanded, "aria-pressed": ariaPressed, tabIndex, to, ...restProps } = props;
|
||||
const ariaProps = { "aria-label": ariaLabel, "aria-haspopup": ariaHaspopup, "aria-controls": ariaControls, "aria-expanded": ariaExpanded, "aria-pressed": ariaPressed, tabIndex };
|
||||
|
||||
return (
|
||||
<Link className={classes} to={to} {...restProps} {...ariaProps}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +1,52 @@
|
||||
import type { HTMLAttributes, ReactNode } from "react";
|
||||
|
||||
export type PanelProps = HTMLAttributes<HTMLElement> & {
|
||||
as?: "article" | "section" | "div";
|
||||
/** HTML tag to render (defaults to section) */
|
||||
as?: "article" | "section" | "div" | "aside" | "main";
|
||||
children: ReactNode;
|
||||
/** Accessible label for screen readers */
|
||||
"aria-label"?: string;
|
||||
/** Reference to another element that describes this panel */
|
||||
"aria-describedby"?: string;
|
||||
/** Identifies this panel as a landmark region */
|
||||
role?: "region" | "article" | "complementary" | "main";
|
||||
};
|
||||
|
||||
export function Panel({ as: Tag = "section", children, className = "", ...props }: PanelProps) {
|
||||
/**
|
||||
* Panel component for grouping related content with proper semantics.
|
||||
*
|
||||
* Renders a styled container with semantic HTML and accessibility support.
|
||||
* Use this to create visually distinct sections of your interface.
|
||||
*
|
||||
* @example
|
||||
* // Default section
|
||||
* <Panel>
|
||||
* <h2>Section Title</h2>
|
||||
* <p>Section content...</p>
|
||||
* </Panel>
|
||||
*
|
||||
* @example
|
||||
* // With accessibility
|
||||
* <Panel aria-label="Search results" role="region">
|
||||
* {results}
|
||||
* </Panel>
|
||||
*/
|
||||
export function Panel({ as: Tag = "section", children, className = "", role, ...props }: PanelProps) {
|
||||
const { "aria-label": ariaLabel, "aria-describedby": ariaDescribedby, ...restProps } = props;
|
||||
|
||||
return (
|
||||
<Tag
|
||||
role={role}
|
||||
aria-label={ariaLabel}
|
||||
aria-describedby={ariaDescribedby}
|
||||
className={[
|
||||
"rounded-lg border border-border bg-surface/92 p-5 shadow-[0_16px_60px_rgba(0,0,0,0.22)]",
|
||||
"focus-within:ring-2 focus-within:ring-border focus-within:ring-offset-2 focus-within:ring-offset-bg",
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ")}
|
||||
{...props}
|
||||
{...restProps}
|
||||
>
|
||||
{children}
|
||||
</Tag>
|
||||
|
||||
Reference in New Issue
Block a user