diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..563b11d --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +# No environment variables are required to run or build this site. diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 32478db..fadf2f2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -8,7 +8,7 @@ jobs: name: Build Docusaurus runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 with: fetch-depth: 0 @@ -17,9 +17,9 @@ jobs: npm install -g corepack corepack enable - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v7 with: - node-version: 20 + node-version: 22 cache: yarn - name: Install dependencies @@ -27,8 +27,12 @@ jobs: - name: Build website run: yarn build + # From v4 this action excludes dotfiles, so Docusaurus's `.nojekyll` is not in the artifact. + # Harmless here: artifact deployments are served as-is and never run Jekyll, and the build + # output has no underscore-prefixed paths for Jekyll to strip. If a dotfile ever has to ship, + # build the artifact by hand instead of using this action. - name: Upload Build Artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v5 with: path: build @@ -50,4 +54,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 \ No newline at end of file + uses: actions/deploy-pages@v5 \ No newline at end of file diff --git a/.gitignore b/.gitignore index b2d6de3..0dcc78c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ # Dependencies /node_modules +# Yarn writes this on every install; it is a local cache, not a lockfile +.yarn/install-state.gz + # Production /build @@ -10,6 +13,7 @@ # Misc .DS_Store +.env .env.local .env.development.local .env.test.local @@ -18,3 +22,5 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +TODO \ No newline at end of file diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz deleted file mode 100644 index b7de631..0000000 Binary files a/.yarn/install-state.gz and /dev/null differ diff --git a/README.md b/README.md index 677c23b..37e9ca5 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,22 @@ # @bedrock-core/docs -Documentation for @bedrock-core, built using [Docusaurus](https://docusaurus.io/). +Documentation for @bedrock-core, built with [Docusaurus](https://docusaurus.io/). -Available in +Available at -## Installation +## Install ```bash yarn ``` -## Local Development +## Develop ```bash yarn start ``` -This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. +Starts a local dev server and opens a browser window. Most changes are reflected live without a restart. ## Build @@ -24,20 +24,12 @@ This command starts a local development server and opens up a browser window. Mo yarn build ``` -This command generates static content into the `build` directory and can be served using any static contents hosting service. +Generates static content into the `build` directory, servable by any static hosting service. -## Deployment +## Writing a page -Using SSH: +Every page under `docs/` follows [`STYLE.md`](./STYLE.md). -```bash -USE_SSH=true yarn deploy -``` - -Not using SSH: - -```bash -GIT_USER= yarn deploy -``` +## Sections -If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. +`src/data/sections.ts` is the registry of sections: id, category, status, description, icon and source repository. `docusaurus.config.ts` creates one docs-plugin instance per section that has a matching `docs/` folder, and the navbar, sidebar switcher and home page all read the same registry. diff --git a/STYLE.md b/STYLE.md new file mode 100644 index 0000000..2ae3e69 --- /dev/null +++ b/STYLE.md @@ -0,0 +1,132 @@ +# Writing a docs page + +House rules for every page under `docs/`. They extend the "Content fundamentals" in `design/DESIGN-GUIDE.md`; where the two differ, this file wins. + +## Sections + +One section = one folder under `docs/` = one route = one sidebar. `src/data/sections.ts` is the registry: id, category, status, description. Add a section there first; `docusaurus.config.ts` creates the docs-plugin instance from it. + +```text +
/ + index.md slug: / Overview + installation.md only when install differs from `npm install` + guides/ concepts and how-tos, sentence-case titles + components/ hooks/ api/ reference, one page per export, identifier titles +``` + +Sections with more than six pages use groups; smaller sections stay flat. + +Sidebar labels: the section is the lowercase package name (`ore-styled`). Group labels are sentence case (`Get started`, `Guides`, `Components`, `Hooks`, `API`, `Deprecated`). Leaf labels are the identifier as typed (`Form.Toggle`, `useState`, `core.registry`). + +Links: relative `.md` inside a section, absolute `/docs/
/...` across sections. Another package's mechanics get one sentence and a link, never a paragraph. + +## Page types + +**Overview** (`index.md`, `slug: /`, one per section) + +```md +# ore-styled ← package name, lowercase +One sentence. + +:::caution Beta … ::: ← until this package ships 1.0 + +## What is @bedrock-core/ore-styled? +## Install +## What you get ← **Noun** — mechanism, then what it protects you from +## Next steps ← - [`page`](./page.md) — description +``` + +**Guide** (`guides/*.md`): sentence-case H1, one-sentence intro, sections, `## Next steps`. + +**Component** (`components/*.md`) + +```md +# Button +One sentence. +![Button](/img/ore-styled/Button.png) ← optional + +## Import +## Usage +## Props +| Prop | Type | Default | Description | +Inherits [control props](../control-props.md). ← one line, never repeated +## Examples ← ### sentence-case titles, one idea each, ≤ 25 lines +## Notes ← optional: engine facts only +``` + +**Function or hook** (`hooks/*.md`, `api/*.md`) + +```md +# useState +One sentence. + +## Import +## Signature ← one ```ts line +## Parameters +| Parameter | Type | Default | Description | +## Returns +## Usage +## Examples +## Notes +``` + +**Filter** (`filters/*.md`) + +```md +# i18n +One sentence: what goes in, what comes out. + +## Install ← config.json snippet, place in the stack +## Authoring +## What it generates +| Output | Where | Commit it? | +## Settings +| Setting | Type | Default | Description | +## Checks +| Check | What fails | +``` + +Reference pages end when the content ends: no `Next steps`, no `Best practices`. Overviews and guides always end with `## Next steps`. + +## Tables + +| Use | Header | +| --- | --- | +| Component props | `\| Prop \| Type \| Default \| Description \|` | +| Function parameters | `\| Parameter \| Type \| Default \| Description \|` | +| Options objects | `\| Option \| Type \| Default \| Description \|` | +| Filter settings | `\| Setting \| Type \| Default \| Description \|` | +| Export index | `\| Export \| Kind \| Description \|` | +| Section index | `\| Page \| Description \|` | + +Cell rules: types in backticks, `\|` inside unions; Default is `—` when none; a required prop carries `` after its name (a red asterisk) and `—` as its default; descriptions are one sentence with no trailing period; a compound component gets one table per part under `### RadioGroup`, `### Radio`. + +## Voice and typography + +- Emoji: `✅` and `❌` only, and only where a yes/no scan helps — a yes/no column in a table, or a paired right/wrong code sample. Never in headings, prose or bullet lists. No other emoji or glyphs (`✓ ○ → ⇆ ✕`) outside literal program output. +- Sentence case for every heading. Package names lowercase in backticks. Component, hook and member names exactly as typed. +- Link lists: `` - [`name`](path) — description `` with an em dash. +- American spelling; always `behavior pack`. +- No `---` rules. No "we", "our", "us". +- `## Notes` for behavior facts, `## Limits` for what is not supported. Not `Limitations`, `Caveats`, `Things to know`, `Rules & Restrictions`. +- Front matter on every page: `sidebar_position` (integer) and `description` (one line; it feeds `llms.txt` and the meta tags). + +## Never in a page + +- Version history, migration guides, before/after blocks, "used to", "no longer", "what changed in". Changelogs live in each package's `CHANGELOG.md`. +- Dated measurements, session logs, spike names, "measured while building". +- Roadmap phrased as a promise ("not yet", "coming"). State the limit as a fact: "Horizontal scrolling is not exposed." +- Links into monorepo source files. +- Generic React tutorial content. The ui overview links react.dev once. + +## Examples + +- Import from the public package name. +- `console.warn`, never `console.log`. +- Examples take a `Player` parameter. The `world.afterEvents.buttonPush` + `isPlayer` boilerplate appears once, on the ui overview. +- JSX attribute values in braces: `flexDirection={'row'}`. +- One idea per example, at most 25 lines. + +## Checks + +`grep` over `docs/**/*.md` must return nothing for: `## Best Practices`, `^---$`, `^- Type:`, `## Next Steps`, `## In This Section`, `behaviour`, `colour`, `labelled`, `centred`, `recognise`, `serialised`, `parameterised`, `github.com/bedrock-core/ui/blob`, `used to`, `no longer`. `✅` and `❌` may appear only inside a table row or a code fence; every other emoji is an error. diff --git a/design/COMPONENT-REFERENCE.md b/design/COMPONENT-REFERENCE.md new file mode 100644 index 0000000..ce12faf --- /dev/null +++ b/design/COMPONENT-REFERENCE.md @@ -0,0 +1,750 @@ +# Component reference — @bedrock-core design system + +Every component, its contract and its usage note, in one file. The runnable +source is NOT part of this drop-in (it is React; the docs site is Docusaurus +and needs no React kit to be themed). This file exists so you can match the +markup and class-free inline styling the design system expects. + + +## components/core/ + +### Badge + +Mono-type status pill for release state and stability markers. + +```jsx +beta +v0.9.2 +``` + +
Props contract + +```ts +export interface BadgeProps { + children?: React.ReactNode; + tone?: 'neutral' | 'accent' | 'info' | 'success' | 'warning' | 'danger'; + uppercase?: boolean; + style?: React.CSSProperties; +} +export declare function Badge(props: BadgeProps): JSX.Element; +``` + +
+ +### Button + +The standard action control — use `primary` once per view, `secondary` for the paired action, `ghost` inside dense toolbars. + +```jsx + + +``` + +Sizes sm 28 / md 34 / lg 44px. Press state nudges down 1px; there is no scale or bounce anywhere in this brand. + +
Props contract + +```ts +/** + * @startingPoint section="Core" subtitle="Buttons, tags, badges and other primitives" viewport="700x220" + */ +export interface ButtonProps { + children?: React.ReactNode; + /** primary = emerald solid; secondary = raised stone; ghost = bare; accentSoft = tinted; danger = redstone. */ + variant?: 'primary' | 'secondary' | 'ghost' | 'accentSoft' | 'danger'; + size?: 'sm' | 'md' | 'lg'; + /** Lucide icon name rendered before the label. */ + iconLeft?: string; + /** Lucide icon name rendered after the label. */ + iconRight?: string; + disabled?: boolean; + fullWidth?: boolean; + as?: 'button' | 'a'; + href?: string; + onClick?: (e: React.MouseEvent) => void; + style?: React.CSSProperties; +} +export declare function Button(props: ButtonProps): JSX.Element; +``` + +
+ +### Card + +Flat stone panel used for every boxed surface in the system. + +```jsx +

ore-styled

+``` + +The accent rule sits on the TOP edge — never a coloured left border. + +
Props contract + +```ts +export interface CardProps { + children?: React.ReactNode; + /** Lifts onto --bg-raised with a small shadow. */ + raised?: boolean; + /** Adds pointer + border-strong hover. */ + interactive?: boolean; + /** Colour of the 2px top rule — pass a --pkg-* token. Top edge only, never a left border. */ + accent?: string; + padding?: string; + onClick?: (e: React.MouseEvent) => void; + style?: React.CSSProperties; +} +export declare function Card(props: CardProps): JSX.Element; +``` + +
+ +### Divider + +Hairline separator, optionally with an uppercase mono label. + +```jsx + +``` + +
Props contract + +```ts +export interface DividerProps { + /** Centred uppercase mono label. */ + label?: string; + vertical?: boolean; + style?: React.CSSProperties; +} +export declare function Divider(props: DividerProps): JSX.Element; +``` + +
+ +### Icon + +Monochrome glyph wrapper around the Lucide icon set — use it anywhere an icon is needed instead of inlining SVG. + +```jsx + + +``` + +Icons are fetched from the Lucide CDN and inlined as real SVG, so they always take `currentColor` from their parent. Sizes: xs 12 / sm 14 / md 16 / lg 20 / xl 24, or a raw number. + +
Props contract + +```ts +export interface IconProps { + /** Lucide icon name in kebab-case, e.g. "terminal", "box", "arrow-right". */ + name: string; + size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | number; + /** Any CSS color. Defaults to currentColor. */ + color?: string; + /** Lucide's own default is 2. */ + strokeWidth?: number; + style?: React.CSSProperties; +} +export declare function Icon(props: IconProps): JSX.Element; +``` + +
+ +### IconButton + +Icon-only square control — theme toggle, copy button, sidebar collapse. + +```jsx + + +``` + +
Props contract + +```ts +export interface IconButtonProps { + /** Lucide icon name. */ + icon: string; + /** Accessible label — also used as the tooltip. */ + label: string; + size?: 'sm' | 'md' | 'lg'; + variant?: 'ghost' | 'outline'; + active?: boolean; + onClick?: (e: React.MouseEvent) => void; + style?: React.CSSProperties; +} +export declare function IconButton(props: IconButtonProps): JSX.Element; +``` + +
+ +### Kbd + +Key cap for shortcut hints. Bottom border is 2px to fake the cap edge. + +```jsx +⌘K +``` + +
Props contract + +```ts +export interface KbdProps { + children?: React.ReactNode; + style?: React.CSSProperties; +} +export declare function Kbd(props: KbdProps): JSX.Element; +``` + +
+ +### Tag + +Chip for package names and topics. The dot carries the package accent. + +```jsx +@bedrock-core/server +``` + +
Props contract + +```ts +export interface TagProps { + children?: React.ReactNode; + /** Colour of the leading dot — pass a --pkg-* token to carry package identity. */ + accent?: string; + mono?: boolean; + dot?: boolean; + style?: React.CSSProperties; +} +export declare function Tag(props: TagProps): JSX.Element; +``` + +
+ +## components/navigation/ + +### Breadcrumbs + +Trail above the docs H1. Separator is a literal slash — it matches the package-scope typography. + +```jsx + +``` + +
Props contract + +```ts +export interface Crumb { label: string; href?: string } +export interface BreadcrumbsProps { + items: Crumb[]; + style?: React.CSSProperties; +} +export declare function Breadcrumbs(props: BreadcrumbsProps): JSX.Element; +``` + +
+ +### DocsMenu + +Header mega-menu that replaces per-package nav tabs once the docs outgrow two sections — categories across the top, a row per package underneath. + +```jsx + +``` + +Use up to four categories per row. `status: "planned"` is how unreleased surfaces stay visible without being clickable. + +
Props contract + +```ts +export interface DocsMenuItem { + id: string; + label: string; + description?: string; + /** "planned" dims the row and disables selection; any other string renders as a badge. */ + status?: string; +} +export interface DocsMenuCategory { + id: string; + label: string; + /** Category identity colour — pass a --cat-* token. */ + accent: string; + items: DocsMenuItem[]; +} +export interface DocsMenuProps { + label?: string; + categories: DocsMenuCategory[]; + activeId?: string; + onSelect?: (id: string) => void; + style?: React.CSSProperties; +} +export declare function DocsMenu(props: DocsMenuProps): JSX.Element; +``` + +
+ +### NavBar + +Site header for both the docs and marketing surfaces. Translucent + blurred over the canvas. + +```jsx +} /> +``` + +Section labels are lowercase mono — they are package names, not sentence-case nav items. + +
Props contract + +```ts +export interface NavSection { id: string; label: string; accent?: string } +export interface NavBarProps { + /** Path to the @bedrock-core mark, usually assets/logo-mark.png. */ + logoSrc?: string; + /** Left-hand nav slot — put a here once the docs outgrow a tab strip. Rendered before `sections`. */ + menu?: React.ReactNode; + sections?: NavSection[]; + activeSection?: string; + onSelectSection?: (id: string) => void; + /** Right-hand slot: search, GitHub/Discord buttons, theme toggle. */ + right?: React.ReactNode; + sticky?: boolean; + style?: React.CSSProperties; +} +export declare function NavBar(props: NavBarProps): JSX.Element; +``` + +
+ +### PaginationNav + +Prev/next footer that closes a docs article. + +```jsx + +``` + +
Props contract + +```ts +export interface PageLink { label: string; href?: string; onClick?: (e: React.MouseEvent) => void } +export interface PaginationNavProps { + prev?: PageLink; + next?: PageLink; + style?: React.CSSProperties; +} +export declare function PaginationNav(props: PaginationNavProps): JSX.Element; +``` + +
+ +### SectionSwitcher + +Sidebar header that names the current docs section and drops down to switch. Pair it with `SidebarNav`'s `header` slot — it is the flat, searchable counterpart to `DocsMenu`. + +```jsx +} … /> +``` + +
Props contract + +```ts +export interface SwitcherSection { + id: string; + label: string; + /** Category identity colour — pass a --cat-* token. */ + accent: string; + /** Category name shown right-aligned in the list. */ + category?: string; + /** "planned" dims the row and disables selection. */ + status?: string; +} +export interface SectionSwitcherProps { + sections: SwitcherSection[]; + value?: string; + onChange?: (id: string) => void; + scope?: string; + /** Release badge shown on the closed trigger, e.g. "beta". */ + status?: string; + style?: React.CSSProperties; +} +export declare function SectionSwitcher(props: SectionSwitcherProps): JSX.Element; +``` + +
+ +### SidebarNav + +Docs left sidebar. Top-level entries are sans; nested pages are mono (they are file/API names). + +```jsx + +``` + +
Props contract + +```ts +export interface SidebarNode { + id: string; + label: string; + items?: SidebarNode[]; + /** Collapsed on first render when false. */ + defaultOpen?: boolean; +} +export interface SidebarNavProps { + items: SidebarNode[]; + activeId?: string; + onSelect?: (id: string) => void; + /** Slot above the tree — version switcher, package pill. */ + header?: React.ReactNode; + style?: React.CSSProperties; +} +export declare function SidebarNav(props: SidebarNavProps): JSX.Element; +``` + +
+ +### SiteFooter + +Footer shared by the docs and marketing surfaces. + +```jsx + +``` + +
Props contract + +```ts +export interface FooterColumn { title: string; links: { label: string; href?: string }[] } +export interface SiteFooterProps { + /** Path to the wordmark PNG, usually assets/logo-wordmark.png. */ + wordmarkSrc?: string; + columns?: FooterColumn[]; + note?: string; + style?: React.CSSProperties; +} +export declare function SiteFooter(props: SiteFooterProps): JSX.Element; +``` + +
+ +### TableOfContents + +Right-hand page outline. Active item gets an emerald left rule, not a background. + +```jsx + +``` + +
Props contract + +```ts +export interface TocItem { id: string; label: string; depth?: 0 | 1 | 2 } +export interface TableOfContentsProps { + items: TocItem[]; + activeId?: string; + onSelect?: (id: string) => void; + title?: string; + style?: React.CSSProperties; +} +export declare function TableOfContents(props: TableOfContentsProps): JSX.Element; +``` + +
+ +## components/docs/ + +### Callout + +Admonition block for the docs body — the "Beta" banner on every package overview is `kind="warning"`. + +```jsx +The API can change between releases. +``` + +
Props contract + +```ts +export interface CalloutProps { + kind?: 'note' | 'tip' | 'info' | 'warning' | 'danger'; + /** Overrides the default uppercase title. */ + title?: string; + children?: React.ReactNode; + style?: React.CSSProperties; +} +export declare function Callout(props: CalloutProps): JSX.Element; +``` + +
+ +### CodeBlock + +Code fence for the docs body and marketing hero. Always dark, in both themes. + +```jsx + + +``` + +
Props contract + +```ts +export interface CodeBlockProps { + code: string; + /** Shown as an uppercase mono badge in the title bar. */ + language?: string; + /** Filename or command context, e.g. "main.ts". */ + title?: string; + showLineNumbers?: boolean; + copyable?: boolean; + style?: React.CSSProperties; +} +export declare function CodeBlock(props: CodeBlockProps): JSX.Element; +``` + +
+ +### DocTabs + +Underlined tab strip for alternative instructions (npm / pnpm / yarn, TS / JS). + +```jsx + + {(id) => } + +``` + +
Props contract + +```ts +export interface DocTab { id: string; label: string } +export interface DocTabsProps { + tabs: DocTab[]; + /** Controlled active tab id. */ + value?: string; + defaultValue?: string; + onChange?: (id: string) => void; + /** Node, or a render function receiving the active tab id. */ + children?: React.ReactNode | ((activeId: string) => React.ReactNode); + style?: React.CSSProperties; +} +export declare function DocTabs(props: DocTabsProps): JSX.Element; +``` + +
+ +### FeatureCard + +Feature block for the marketing page's "what you get" grid. + +```jsx +Addons find each other at runtime. +``` + +
Props contract + +```ts +export interface FeatureCardProps { + /** Lucide icon name. */ + icon?: string; + title?: string; + children?: React.ReactNode; + accent?: string; + style?: React.CSSProperties; +} +export declare function FeatureCard(props: FeatureCardProps): JSX.Element; +``` + +
+ +### NextStepsList + +Closing link list for a docs page — mono link title, sentence-case description on the same line. + +```jsx + +``` + +
Props contract + +```ts +export interface NextStepItem { title: string; description?: string; href?: string } +export interface NextStepsListProps { + items: NextStepItem[]; + style?: React.CSSProperties; +} +export declare function NextStepsList(props: NextStepsListProps): JSX.Element; +``` + +
+ +### PackageCard + +Tile for one package in the homepage grid or a docs index. + +```jsx + +``` + +
Props contract + +```ts +/** + * @startingPoint section="Docs" subtitle="Package tiles, callouts, code fences and API tables" viewport="700x320" + */ +export interface PackageCardProps { + /** Package name after the scope, e.g. "server". */ + name: string; + scope?: string; + description?: string; + /** Lucide icon name. */ + icon?: string; + /** Package identity colour — pass a --pkg-* token. */ + accent?: string; + /** Short release-state label, e.g. "beta". */ + status?: string; + href?: string; + style?: React.CSSProperties; +} +export declare function PackageCard(props: PackageCardProps): JSX.Element; +``` + +
+ +### PropsTable + +API reference table used under every component/API heading in the docs. + +```jsx + +``` + +
Props contract + +```ts +export interface PropsTableRow { + name: string; + type: string; + default?: string; + required?: boolean; + description?: React.ReactNode; +} +export interface PropsTableProps { + rows: PropsTableRow[]; + style?: React.CSSProperties; +} +export declare function PropsTable(props: PropsTableProps): JSX.Element; +``` + +
+ +## components/forms/ + +### SearchInput + +Docs search field for the header. + +```jsx + +``` + +
Props contract + +```ts +export interface SearchInputProps { + value?: string; + onChange?: (value: string) => void; + placeholder?: string; + /** Second key in the hint; pass null to hide the hint entirely. */ + shortcut?: string | null; + width?: number | string; + style?: React.CSSProperties; +} +export declare function SearchInput(props: SearchInputProps): JSX.Element; +``` + +
+ +### Select + +Compact select for version and locale switching. Mono type — the values are identifiers. + +```jsx + + {'Save'} + +``` + +It is [`Input`](/docs/ui/components/Input) with the [theme](./theme.md)'s field textures and font applied, plus a caption above the box. The widget itself is the engine's — it is owned by the client while the form is open, and every field's value comes back at once on submit. + +## Props + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `name` | `string` | — | Result key — the value appears at `values[name]` in the form's `onSubmit` | +| `label` | `string` | — | Caption rendered above the field | +| `placeholder` | `string` | — | Text shown inside the field when it is empty | +| `defaultValue` | `string` | `''` | Initial text | + +Inherits every prop of [`Input`](/docs/ui/components/Input) — the font, the scale, the text offsets and the per-state box textures — with the theme's values as defaults rather than a lock: pass one and yours wins. Through it, [control props](/docs/ui/components/control-props) as well. + +## Examples + +### Two fields in one form + +```tsx +
save(values.nickname, values.motto)}> + + + {'Save'} +
+``` + +### Disabled + +```tsx + +``` + +## Notes + +Give the field a `width`, or let it stretch in a column, so the framed box reads as a field even when empty. A `placeholder` keeps an empty one reading as editable. + +There is no `onChange`: a native modal is atomic, so nothing reaches script while the form is open. For a value the screen reacts to immediately, use a press — [`Checkbox`](./Checkbox.md) and [`Toggle`](./Toggle.md) do that on the hosts where a press reaches script. + +## Limits + +Modal-only. The engine draws no text field on an action form or a container screen, so [`useMechanism('Input')`](/docs/ui/hooks/useMechanism) refuses it there at build, naming the fix. diff --git a/docs/ore-styled/MenuRow.md b/docs/ore-styled/MenuRow.md new file mode 100644 index 0000000..167174e --- /dev/null +++ b/docs/ore-styled/MenuRow.md @@ -0,0 +1,164 @@ +--- +description: "The browse-screen row: leading thumbnail, title, one-line subtitle, and a trailing chevron, drawn on the dropdown-option face." +--- +# MenuRow + +The browse-screen row: leading thumbnail, title, one-line subtitle, and a trailing chevron, drawn on the dropdown-option face. Every list in the shared UI — addons, guide index, config scopes, entity rosters — is built from it, so lists read as one system. + +![MenuRow](/img/ore-styled/MenuRow.png) + +## Import + +```tsx +import { MenuRow } from '@bedrock-core/ore-styled'; +``` + +## Usage + +```tsx +function Row(): JSX.Element { + const { navigate } = useNavigation(); + + return ( + navigate('shop:details', { params: { id: 'diamond' } })} + /> + ); +} +``` + +## Props + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `title` | [`DisplayText`](/docs/i18n/api#displaytext) | — | First line — the row's name | +| `subtitle` | `DisplayText` | — | Second line, rendered muted. Omit for a single-line row | +| `icon` | `string` | — | Leading thumbnail texture path. Omit for a text-only row | +| `iconSize` | `number` | the theme's row icon size | Thumbnail edge in px | +| `chevron` | `boolean` | `true` | Trailing `>` affordance. Set `false` for rows that select rather than navigate | +| `selected` | `boolean` | `false` | Whether this row is the list's current selection. For a selecting list, where one row stands after the press | +| `depth` | `number` | `0` | Indent level for nested index rows. Each step insets the row's whole box, not its contents, so a child row is visibly narrower than its section header | +| `onPress` | `(event: PressEvent) => unknown \| Promise` | — | Press handler. Use `to` instead when the press opens another screen | +| `to` | `ScreenKey` | — | The screen this row opens, `:`. A row with one is a [``](/docs/ui/components/Link) | +| `replace` | `boolean` | `false` | With `to`: take the place of the screen this row is on rather than stacking over it | +| `titleMaxLength` | `number` | — | Characters the title reserves, for a title only known when the screen is shown | +| `subtitleMaxLength` | `number` | — | Characters the subtitle reserves. Setting it also keeps the subtitle line when the subtitle is empty, so the row has one shape | +| `enabled` | `boolean` | `true` | A disabled row keeps its face and greys its text | + +Inherits [control props](/docs/ui/components/control-props). It sets `alignSelf: 'stretch'` rather than an explicit width, so do not hard-code a `width` alongside `depth`. + +## An index other addons can show + +A row with `to` is a link, so where it leads is data rather than a handler — which is what lets an index of rows be shown by an addon running none of this one's script. A row with `onPress` cannot be described that way, and does nothing in a foreign realm. + +```tsx + +``` + +## Reserving room for live text + +A compiled screen bakes a row's text unless told how long a live one may be. Give `titleMaxLength` — and `subtitleMaxLength` where there is a second line — for a row whose text comes from data: + +```tsx + +``` + +## Localized labels + +`title` and `subtitle` are `DisplayText`, so a row may carry a literal, a `.lang` key or a `RawMessage`: + +```tsx +const { key, raw } = useTranslation(i18n); + + $.shop.title)} + subtitle={raw($ => $.shop.stock, { count })} + onPress={() => navigate('shop:home')} +/> +``` + +:::caution Color prefixes only apply to literals +MenuRow colors its lines with a `§` prefix, and applies it **only** to literal strings — a `RawMessage`, or a string the active resolver recognizes as a key, passes through untouched in the label's own color. + +If you need a specific color on localized text, bake the `§` code into the authored translation value instead of the call site. +::: + +## Examples + +### Text-only list + +```tsx + + open('general')} /> + open('economy')} /> + open('permissions')} /> + +``` + +### Nested index + +```tsx + + {}} /> + open('installation')} /> + open('first-screen')} /> + {}} /> + open('components')} /> + +``` + +### Selection rows + +Drop the chevron for rows that pick a value rather than navigating deeper. + +```tsx +{themes.map(name => ( + setTheme(name)} + /> +))} +``` + +### With an icon and a disabled state + +```tsx + navigate('shop:server_config')} +/> +``` + +## Theme tokens + +Read from `theme.components.menuRow`: + +| Token | Default | +| --- | --- | +| `padding` | `4` | +| `gap` | `4` | +| `iconSize` | `16` | +| `textStyle.font` | `'mojangles'` | +| `textStyle.scale` | `1` | +| `textStyle.color` | `'§f'` | +| `textStyle.disabledColor` | `'§8'` | +| `textStyle.muted` | `'§7'` | +| `textStyle.mutedDisabled` | `'§8'` | +| `textures.background` | the dropdown option face | +| `textures.backgroundSelected` | the dropdown's selected option face | + +## Notes + +- Use `MenuRow` for every list in a screen rather than hand-rolling rows. +- Keep subtitles to one short line; both lines are clipped with an ellipsis at one line each. +- Use `depth` for hierarchy instead of nesting `Panel`s with padding — the inset box is what communicates the level. +- Set `chevron={false}` whenever pressing the row does not open another screen. +- Pair with [`Header`](./Header.md) above and a [`Scroll`](/docs/ui/components/Scroll) around the rows for a standard browse screen. diff --git a/docs/ore-styled/Radio.md b/docs/ore-styled/Radio.md new file mode 100644 index 0000000..4184fee --- /dev/null +++ b/docs/ore-styled/Radio.md @@ -0,0 +1,104 @@ +--- +description: "One choice out of several with every option visible: a bullet and a label per row." +--- +# Radio + +One choice out of several, every option visible: a bullet and a label per row. + +![Radio](/img/ore-styled/Radio.png) + +## Import + +```tsx +import { Radio } from '@bedrock-core/ore-styled'; +``` + +## Usage + +```tsx + +``` + +The options are an array, not children — this layer owns them and maps each entry to a `Option`, so a caller-supplied child could only fight the array. + +## It depends on the screen + +`Radio` asks [`useMechanism('Select')`](/docs/ui/hooks/useMechanism) what a single choice becomes on the screen it is being drawn on. + +| Screen | What it becomes | What reaches script | +| --- | --- | --- | +| [`
`](/docs/ui/components/Form) | the engine's own [inline select](/docs/ui/components/Select) | nothing until submit — the chosen option's **index** arrives at `values[name]` | +| [``](/docs/ui/components/Screen) | a button per row | `onChange`, with the chosen **value** | +| [``](/docs/ui/components/Container) | a row whose press is an item taken and put back | `onChange`, with the chosen **value** | + +So `name` is the modal's prop, and `value` / `onChange` only do anything where a press reaches script. Note the asymmetry: the modal answers with an index, a press answers with the value. + +The rows are laid out by the library's own flex system either way, so the geometry belongs to this layer — change `rowHeight` or `gap` and the in-game layout follows with no JSON UI edit. + +## Props + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `options` | `RadioOption[]` | — | The options, top to bottom | +| `name` | `string` | — | Result key on a modal, where it is required; ignored where the press is the answer | +| `label` | `string` | — | Caption rendered above the group | +| `defaultValue` | `string` | the first option | Initial selection, matched on `value` | +| `rowHeight` | `number` | `17` | Height of each option row | +| `gap` | `Spacing` | `2` | Space between rows | +| `value` | `string` | — | Held by the caller instead of by the group. Only where a press reaches script | +| `onChange` | `(value: string) => void` | — | Called with the chosen value, on the hosts where a press reaches script | +| `enabled` | `boolean` | `true` | `false` draws the disabled bullets and ignores presses | + +```ts +interface RadioOption { + value: string; + label: string; +} +``` + +Inherits every prop of [`Select`](/docs/ui/components/Select) except `children` — the bullet glyphs per state, their size, the option row faces and the label style — with the theme's values as defaults. Row surfaces default to nothing: the bullet carries the look. Through it, [control props](/docs/ui/components/control-props) as well. + +## Examples + +### In a modal, reading the index back + +```tsx +const TEAMS = [{ value: 'red', label: 'Red' }, { value: 'blue', label: 'Blue' }]; + + join(TEAMS[Number(values.team)].value)}> + + {'Join'} + +``` + +### On a screen of buttons, reading the value + +```tsx +function TeamPicker(): JSX.Element { + const [team, setTeam] = useState('red'); + + return ; +} +``` + +### Tighter rows + +```tsx + +``` + +## Notes + +Use `Radio` when the options read as a list and each needs its own line. [`ToggleButtons`](./ToggleButtons.md) is the same choice drawn as side-by-side segments, for when a button-sized hit target suits better. + +## Limits + +A screen that can draw neither an inline select nor a press refuses it at build, in that host's own words. diff --git a/docs/ore-styled/Slider.md b/docs/ore-styled/Slider.md new file mode 100644 index 0000000..e653795 --- /dev/null +++ b/docs/ore-styled/Slider.md @@ -0,0 +1,68 @@ +--- +description: "A themed numeric slider for a modal form." +--- +# Slider + +A themed numeric slider. + +![Slider](/img/ore-styled/Slider.png) + +## Import + +```tsx +import { Slider } from '@bedrock-core/ore-styled'; +``` + +## Usage + +Render it inside a [`
`](/docs/ui/components/Form). The value arrives in the form's `onSubmit`, keyed by `name`. + +```tsx + console.warn(values.volume)}> + + {'Save'} + +``` + +It is [`Slider`](/docs/ui/components/Slider) with the [theme](./theme.md)'s track, progress fill and thumb textures applied, plus a caption above it. The widget is the engine's: the player drags it while the form is open, and the value comes back with every other field on submit. + +## Props + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `name` | `string` | — | Result key — the value appears at `values[name]` in the form's `onSubmit` | +| `label` | `string` | — | Caption rendered above the slider | +| `min` | `number` | — | Minimum selectable value; the left end of the track | +| `max` | `number` | — | Maximum selectable value; the right end of the track | +| `step` | `number` | `1` | Increment between selectable values | +| `defaultValue` | `number` | `min` | Initial value | + +Inherits every prop of [`Slider`](/docs/ui/components/Slider) — the track, progress and thumb textures, `trackHeight`, `thumbWidth` / `thumbHeight` — with the theme's values as defaults rather than a lock. Through it, [control props](/docs/ui/components/control-props) as well. + +## Examples + +### A settings form + +```tsx +
apply(values)}> + + + {'Apply'} + +``` + +### Disabled + +```tsx + +``` + +## Notes + +The interactive hitbox of the thumb is a fixed 16 × 16, so keep the visual thumb at its default size unless a mismatch is acceptable — a larger one looks draggable in places it is not. + +There is no `onChange`: a native modal is atomic, so nothing reaches script while the form is open. + +## Limits + +Modal-only. The engine draws no slider on an action form or a container screen, so [`useMechanism('Slider')`](/docs/ui/hooks/useMechanism) refuses it there at build, naming the fix. diff --git a/docs/ore-styled/Tabs.md b/docs/ore-styled/Tabs.md new file mode 100644 index 0000000..da82ed0 --- /dev/null +++ b/docs/ore-styled/Tabs.md @@ -0,0 +1,69 @@ +--- +description: "Panes switched on the client, each header a label on the theme's faces." +--- +# Tabs + +Panes switched on the client, each header a label on the theme's faces. + +![Tabs](/img/ore-styled/Tabs.png) + +## Import + +```tsx +import { Tabs } from '@bedrock-core/ore-styled'; +``` + +## Usage + +```tsx + + + + + + + + +``` + +It is [`Tabs`](/docs/ui/components/Tabs) with the [theme](./theme.md)'s `tabs` faces applied and a label in place of a drawn header. A chosen tab wears the pressed face and its label drops a pixel; labels are white whichever tab is chosen. + +Switching tabs reaches no script and presents nothing again. A tab whose content depends on the switch is a screen change, not a tab. + +## Props + +### Tabs + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `tabHeight` | `number` | the theme's tab height | Height of the header row; the panes take what is left | +| `tabBackground` | `string` | the theme's normal face | The face a header is drawn on while its tab is not chosen | +| `tabHover` | `string` | the theme's hover face | The face while the pointer is over a tab that is not chosen | +| `tabSelected` | `string` | the theme's pressed face | The face while its tab is the chosen one | +| `gap` | `Spacing` | `-1` | Space between headers; `-1` is the overlap that fuses adjacent borders | + +Inherits [control props](/docs/ui/components/control-props). + +### Tabs.Tab + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `label` | [`DisplayText`](/docs/i18n/api#displaytext) | — | The tab's name on its header | +| `children` | `JSX.Node` | — | The pane shown while the tab is chosen | + +## Examples + +### A face of your own for the chosen tab + +```tsx + + {first} + {second} + +``` + +## Notes + +The headers share the row's width evenly, so keep each label short enough to fit its share. + +The theme's `tabs` section and its textures started as copies of the toggle buttons'. diff --git a/docs/ore-styled/Toggle.md b/docs/ore-styled/Toggle.md new file mode 100644 index 0000000..005fe52 --- /dev/null +++ b/docs/ore-styled/Toggle.md @@ -0,0 +1,77 @@ +--- +description: "A themed on/off switch that draws as a native field on a modal and a press everywhere else." +--- +# Toggle + +An on/off switch: the caption on the left, the switch pinned to the right — the settings-row reading order. + +![Toggle](/img/ore-styled/Toggle.png) + +## Import + +```tsx +import { Toggle } from '@bedrock-core/ore-styled'; +``` + +## Usage + +```tsx + +``` + +## It depends on the screen + +`Toggle` is the same control as [`Checkbox`](./Checkbox.md) with the theme's switch faces: it asks [`useMechanism('Toggle')`](/docs/ui/hooks/useMechanism) what a boolean becomes on the screen it is being drawn on, and draws that. + +| Screen | What it becomes | What reaches script | +| --- | --- | --- | +| [`
`](/docs/ui/components/Form) | a native [`Toggle`](/docs/ui/components/Toggle) the engine owns | nothing until submit — the value arrives at `values[name]` | +| [``](/docs/ui/components/Screen) | a press that holds its own state | `onChange`, on every press | +| [``](/docs/ui/components/Container) | an item taken and put straight back | `onChange`, on every press | + +`name` is the modal's prop and required there; `on` and `onChange` only do anything where a press reaches script. + +## Props + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `name` | `string` | — | Result key on a modal, where it is required; ignored where the press itself is the answer | +| `label` | `string` | — | The caption beside it. Without one, the switch is drawn bare | +| `defaultValue` | `boolean` | `false` | Which way it starts | +| `on` | `boolean` | — | Held by the caller instead of by the control. Only where a press reaches script | +| `onChange` | `(on: boolean) => void` | — | Called with the new state, on the hosts where a press reaches script | +| `enabled` | `boolean` | `true` | `false` draws the disabled texture and ignores presses | + +The theme's switch textures are defaults, not a lock: `background`, `backgroundHover`, `backgroundPressed`, `backgroundLocked`, `checkedBackground`, `checkedHover` and `checkedLocked` are all accepted and yours wins. Inherits [control props](/docs/ui/components/control-props). + +## Examples + +### A settings form + +```tsx + apply(values)}> + + + {'Save'} + +``` + +### On a screen of buttons + +```tsx +function SoundRow(): JSX.Element { + const [on, setOn] = useState(true); + + return ; +} +``` + +### Disabled + +```tsx + +``` + +## Notes + +Reach for `Toggle` in a settings list, where the caption leads and the switches line up on the right, and for [`Checkbox`](./Checkbox.md) where the box should lead. diff --git a/docs/ore-styled/ToggleButtons.md b/docs/ore-styled/ToggleButtons.md new file mode 100644 index 0000000..b43084d --- /dev/null +++ b/docs/ore-styled/ToggleButtons.md @@ -0,0 +1,123 @@ +--- +description: "Choices drawn as side-by-side segments with fused borders: one, or any number with multiple." +--- +# ToggleButtons + +Choices drawn as side-by-side segments: one, or any number with `multiple`. + +![ToggleButtons](/img/ore-styled/FormToggleButton.png) + +## Import + +```tsx +import { ToggleButtons } from '@bedrock-core/ore-styled'; +``` + +## Usage + +```tsx + +``` + +The same choice a [`Radio`](./Radio.md) makes, in the shape a segmented control wears. Use it when the options benefit from a button-sized hit target and should read as one connected control. + +With `multiple`, every segment is on or off by itself and pressing one leaves the others alone. Nothing else changes: the segments, their textures and the way a chosen one reads are the same in both modes. + +## It depends on the screen + +`ToggleButtons` asks [`useMechanism('Select')`](/docs/ui/hooks/useMechanism) what a choice becomes on the screen it is being drawn on. + +| Screen | What it becomes | What reaches script | +| --- | --- | --- | +| [`
`](/docs/ui/components/Form) | the engine's own [inline select](/docs/ui/components/Select), or a native toggle per segment with `multiple` | nothing until submit — the chosen **index** arrives at `values[name]`, or with `multiple` the **indices** that are on | +| [``](/docs/ui/components/Screen) | a button per segment | `onChange`, with the chosen **value**, or every chosen **value** with `multiple` | +| [``](/docs/ui/components/Container) | a segment whose press is an item taken and put back | `onChange`, as on a screen | + +The segments are laid out by the library's own flex system either way — equal widths, and a one-pixel overlap so adjacent borders fuse — so the geometry belongs to this layer and no JSON UI edit follows a change here. + +## Props + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `options` | `ToggleButtonsOption[]` | — | The segments, left to right | +| `multiple` | `boolean` | `false` | Any number of segments may be on at once | +| `name` | `string` | — | Result key on a modal, where it is required; ignored where the press is the answer | +| `label` | `string` | — | Caption rendered above the segments | +| `defaultValue` | `string`, or `string[]` with `multiple` | the first option, or none with `multiple` | Initial selection, matched on `value` | +| `value` | `string`, or `string[]` with `multiple` | — | Held by the caller instead of by the control. Only where a press reaches script | +| `onChange` | `(value: string) => void`, or `(values: string[]) => void` with `multiple` | — | Called with the chosen value, or every chosen value in segment order, on the hosts where a press reaches script | +| `segmentHeight` | `number` | the theme's toggle-button height | Height of each segment | +| `gap` | `Spacing` | `-1` | Space between segments; `-1` is the overlap that fuses adjacent borders | +| `enabled` | `boolean` | `true` | `false` draws the disabled faces and ignores presses | + +```ts +interface ToggleButtonsOption { + value: string; + label: string; +} +``` + +Inherits every prop of [`Select`](/docs/ui/components/Select) except `children`, with the theme's segment faces, label colours and drop as defaults. Segments are glyph-less by default; pass a `bullet` to opt into one and the primitive's per-state fallbacks apply. Through it, [control props](/docs/ui/components/control-props) as well. + +## Examples + +### In a modal + +```tsx +const MODES = [ + { value: 'easy', label: 'Easy' }, + { value: 'normal', label: 'Normal' }, + { value: 'hard', label: 'Hard' }, +]; + + setMode(MODES[Number(values.difficulty)].value)}> + + {'Start'} + +``` + +### Several at once in a modal + +```tsx +const NOTICES = [ + { value: 'join', label: 'Join' }, + { value: 'leave', label: 'Leave' }, + { value: 'buy', label: 'Buy' }, +]; + +
setNotices((values.notices as number[]).map(index => NOTICES[index].value))}> + + {'Save'} + +``` + +### On a screen of buttons + +```tsx +function ModePicker(): JSX.Element { + const [mode, setMode] = useState('normal'); + + return ; +} +``` + +### Separated segments + +```tsx + +``` + +## Notes + +A chosen segment wears the pressed face, a white label and a one-pixel drop, which is what reads as pressed. On a modal the label is drawn inside each state of the engine's own control, so it changes with the segment rather than staying one colour. + +Segments share the width evenly, so a long label in one of them widens every segment. Keep them short, or reach for [`Radio`](./Radio.md) where each option gets its own line. diff --git a/docs/ore-styled/Trail.md b/docs/ore-styled/Trail.md new file mode 100644 index 0000000..614a65d --- /dev/null +++ b/docs/ore-styled/Trail.md @@ -0,0 +1,77 @@ +--- +description: "A breadcrumb trail as a row of its own: title > scope > entity, travelling as one value that collapses from the middle when it does not fit." +--- +# Trail + +The breadcrumb trail every [`Header`](./Header.md) wears, as a row of its own so any screen can show one. + +![Trail](/img/ore-styled/Trail.png) + +## Import + +```tsx +import { Trail, trailText } from '@bedrock-core/ore-styled'; +``` + +## Usage + +```tsx + +``` + +Renders as `Economy > Server > Pricing`, the separators in the trail's lighter color. + +## Props + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `text` | [`DisplayText`](/docs/i18n/api#displaytext) | — | The whole trail as one value, drawn by one label. `trailText` composes one | +| `maxLength` | `number` | — | Characters `text` reserves, which is what makes the label live | +| `segments` | `readonly DisplayText[]` | — | A trail known at build time, in place of `text`: composed by the build in every language, collapsed to the room the row is laid out with | + +Inherits [control props](/docs/ui/components/control-props). + +## A live trail is one value + +A trail whose segments are only known when the screen opens — an addon's name, the entity being edited — is composed server-side and travels as a single `RawMessage`: + +```tsx +const trail = trailText( + [addonName, scopeLabel, entityName], + core.translations.forPlayer(player), + { back: 'icon' }, +); + + +``` + +A form entry's text is resolved by the **client** before the binding sees it, so every segment reaches the player in their own language off one entry. `maxLength` is what makes the label live: the compiled screen keeps one entry for the trail and shows whatever it is sent. + +## Collapsing from the middle + +`trailText` resolves every segment through the resolver it is given and drops what does not fit in the room the header's controls leave. The **last** segment always stays — it is where the player is — the **first** is kept while it fits, and the rest come back in from the end: + +```text +Economy > Server > Economy > Balances +Economy > ... > Economy > Balances +Economy > ... > Balances +... > Balances +``` + +Everything dropped becomes one `...` in the trail's own color. `{ back: 'cancel' }` says the header wears a modal's labeled dismiss, which leaves the trail less room than the icon back. + +## Baked segments + +`segments` is the build-time trail. The build composes it once for every language the pack ships: each segment resolved in that language, then collapsed from the middle to the width the row was laid out at, by the same rule `trailText` applies to a live trail. One label draws it, through a key the build writes into every language, so the player reads their own. + +Every segment is already resolved, so the trail's colors ride along as `§` codes. + +## Why it hugs + +The row is a [``](/docs/ui/components/Panel#stack) of hugging labels. A compiled screen solves a box as wide as the longest string it may ever hold, so a shorter one would leave the rest of that box as air and the trail would drift off-center. + +The label instead draws at the width of its own glyphs, the engine packs the row, and the stack hangs from the middle — so the trail stays centered on what it actually says. + +## Notes + +[`Header`](./Header.md) builds a baked trail from its `title` and `breadcrumbs`, or takes a composed one through its `trail` prop. Use `Trail` directly for a trail somewhere other than a header bar. diff --git a/docs/ore-styled/fieldLabel.md b/docs/ore-styled/fieldLabel.md new file mode 100644 index 0000000..58d8d7d --- /dev/null +++ b/docs/ore-styled/fieldLabel.md @@ -0,0 +1,58 @@ +--- +description: "A form field's caption, colored by enabled state, for a control of your own." +--- +# fieldLabel + +A form field's caption, colored by enabled state. + +## Import + +```tsx +import { fieldLabel } from '@bedrock-core/ore-styled'; +``` + +## Signature + +```ts +function fieldLabel(label: string, enabled: boolean): JSX.Element +``` + +## Parameters + +| Parameter | Type | Default | Description | +| --- | --- | --- | --- | +| `label` | `string` | — | The caption text | +| `enabled` | `boolean` | — | Whether the control it labels accepts input | + +## Returns + +`JSX.Element` — a `Text` in the theme's field-label style, colored for the enabled state. + +## Usage + +```tsx +fieldLabel('Volume', true); +``` + +The runtime's own fields — `Toggle`, `Slider`, `Dropdown`, `Input` — are deliberately label-free. Every themed field in this package composes its caption with `fieldLabel` when given a `label` prop; reach for it directly only when styling a control of your own. + +## Examples + +### A custom labeled field + +```tsx +function RatingField({ label, enabled, stars }: { label: string; enabled: boolean; stars: number }): JSX.Element { + return ( + + {fieldLabel(label, enabled)} + + + ); +} +``` + +## Notes + +A literal `label` carries the theme's enabled or disabled color as a `§` prefix. A string the active [translation resolver](/docs/ui/hooks/useTranslationResolver) recognizes as a `.lang` key passes through untouched instead — a `§` prefix in front of a key would stop it resolving, the same rule [`MenuRow`](./MenuRow.md) follows for its own text. Bake the color codes into the translation itself when a localized caption needs them. + +Reads `theme.components.form.labelStyle` for its font, scale, color and boldness — see [theme](./theme.md). diff --git a/docs/ore-styled/index.md b/docs/ore-styled/index.md new file mode 100644 index 0000000..6853a43 --- /dev/null +++ b/docs/ore-styled/index.md @@ -0,0 +1,85 @@ +--- +slug: / +sidebar_position: 1 +sidebar_label: Overview +description: "@bedrock-core/ore-styled is a themed component layer over the @bedrock-core/ui primitives." +--- +# ore-styled + +`@bedrock-core/ore-styled` is a themed component layer over the [`@bedrock-core/ui`](/docs/ui/components) primitives. + +:::caution Beta +`@bedrock-core/ore-styled` is in beta: the API can change between releases. Pin exact versions and read the changelog before upgrading. +::: + +## What is @bedrock-core/ore-styled? + +Every component renders with authentic Minecraft textures shipped in the [render pack](/docs/ui/guides/render-pack), so a screen matches the vanilla look with no texture work. It is entirely optional: the primitives underneath are fine to style yourself. + +The textures are **defaults, not a lock**. Every surface prop a primitive accepts is accepted here too, with the theme's value as the fallback — pass one and yours wins. + +## Install + + + +No extra resource pack: the theme's textures live in the same [render pack](/docs/ui/guides/render-pack) the framework already requires. + +## Import + +```tsx +import { Button, Card, Checkbox, Divider, Dropdown, Form, Header, Input, MenuRow, Radio, Slider, Tabs, Toggle, ToggleButtons, Trail } from '@bedrock-core/ore-styled'; +``` + +## Layout and chrome + +- [**` + +
+ ); +} +``` + +The build runs the component once to decide the **shape**; the runtime runs it again per viewer to decide the **values**, and the two walks line up position for position because a compiled screen's shape is fixed. Nothing about a screen is declared twice. + +A container screen is the same file with `` at its root and a host — an entity or a block — to open from: + +```tsx title="packs/BP/scripts/screens/crafting_table.screen.tsx" +/** @jsxImportSource @bedrock-core/ui */ +import { Container, Panel, Slot, Text, useState, type JSX } from '@bedrock-core/ui'; + +export default function CraftingTable(): JSX.Element { + const [planks, setPlanks] = useState(0); + + return ( + + {`planks ${planks}`} + + setPlanks(value => value + stack.amount)} + onRemove={({ stack }) => setPlanks(value => value - stack.amount)} + /> + + + ); +} +``` + +A block-hosted screen is the same shape with `block` in place of `entity` — see [Host definitions](./output.md#host-definitions) for what each stamps onto its definition. + +## Names and keys + +A screen's **name** is its file name without the suffix: `counter.screen.tsx` is `counter`. With the addon's namespace it becomes the JSON UI namespace `_counter`, the output file, and the key `:counter` that `` and `navigate()` take. The name has to be unique across the addon whatever directory the screen sits in. + +## Live values are reserved, not discovered + +A compiled screen is baked, so a string that changes has to say how much room to reserve for it: + +```tsx +{`count ${count}`} +``` + +The build renders the component with each state slot perturbed and fails on anything that moved without a reservation, naming the strings it saw and the `maxLength` each needs. The same probe fails a screen whose **shape** moved — a cell added, dropped or reordered — because the cells are numbered once, at build time. + +## Looks are discovered + +Everything else an element draws — a texture, a colour, an alignment, its size and its place — needs no marker. The probe perturbs each state slot and presses each button, and every element whose look moved is drawn once per look it can take, with the one worn chosen at runtime: by an entry on a form, by an item's durability on a container screen. Props moved by different causes are paired, so a word coloured by one control and moved by another has every combination drawn. A button draws its looks inside its face; any other element draws each version in place, with its children drawn once over them. + +An element the host draws itself — a press, a slot, a live string, a live image or a list — cannot be copied per look, so a prop that changes on one fails the build, naming the prop and the values it saw. Keep it the same for every state and put what changes on a plain element beside it. + +## Conditionals become carried visibility + +`{cond && }` has already collapsed to `false` by the time any renderer sees it, and nothing can tell which element went missing. So the build rewrites the source text of every `.screen.tsx` before executing it, and ships the same rewrite to the runtime: + +| Written | Compiled as | +| --- | --- | +| `{cond && }` | `` | +| `{cond ? : }` | `` | +| `{cond ? : null}` | `` | + +An element that already carries `visible` keeps it, joined with `&&`. Only branches that are single elements are rewritten; a string, fragment or call in a branch is left alone — wrap it in an element to make it compilable. A form carries visibility on an entry; a container screen bakes it, so a container screen whose `visible` moves is a build error. [Conditional rendering](../components/control-props.md#conditional-rendering) shows the same rewrite from the component side. + +## Static screens + +A screen is **static** when every string it shows is baked and every press is a `` or the way out. Nothing about it can differ between one present and the next, so the build already knows everything showing it takes — the title, the value each entry carries, the key and params each press leads to — and the addon ships that table instead of the component that would recompute it. + +`` is the assertion, not the mechanism: a qualifying screen is detected either way, and declaring it makes the build fail the moment the screen stops qualifying. + +```tsx title="packs/BP/scripts/screens/menu.screen.tsx" +/** @jsxImportSource @bedrock-core/ui */ +import { Link, Panel, Screen, Text, type JSX } from '@bedrock-core/ui'; + +export default function Menu(): JSX.Element { + return ( + + + {'Menu'} + {'Shop'} + {'Balance'} + + + ); +} +``` + +A key with no `:` in front of it is one of this bundle's own, named as its file is; a key that names another addon resolves through the reference that addon published. + +A modal never qualifies — its fields are built per present — and neither does anything with a live value, a handler of its own, or a link whose params are not plain data. [Navigation](../guides/navigation.md#static-screens) covers publishing the table so other realms can show the screen. + +## Import time + +:::caution A screen module, and everything it imports, must not touch the world at import time +The filter evaluates the module once, on the build machine, with `@minecraft/server` and `@minecraft/server-ui` replaced by stubs that answer every name the packages declare and do nothing. Hooks are fine: the compiler renders the component with its initial state. What breaks is module-scope code that reaches for the game — `world.afterEvents.*.subscribe(...)`, `system.run(...)`, a dynamic property read next to an `import`. Keep that in the module that opens the screen; a screen's handlers and effects only ever run in game. +::: + +## Library screens + +A screen can also come from a module whose default export is a record of components, named by its keys. The filter's `screens` setting lists such modules: + +```jsonc title="config.json" +{ "filter": "ui-compiler", "settings": { "screens": ["./BP/scripts/screens/shared.ts"] } } +``` + +## App screens + +The bedrock-core apps need no entry in `screens`: declaring is what asks for their screens. The filter reads the register call in `BP/scripts/main.ts` or `BP/scripts/index.ts` and, for each app it finds there, bakes the module the app's declaration names and asks that module for the screens that follow from the declaration. The filter knows no app by name; an app describes itself. + +| On the declaration | What it is | +| --- | --- | +| `app` | The app's name, `'catalog'`, `'config'`, `'guide'` | +| `compiled` | The module to bake. Its default export is the app's own screens, like any `screens` module. Absent for an app whose screens another filter writes, as the guides filter does per page | + +A `compiled` module may also export `shape(declared, manifest)`: given the declaration as data, its installer dropped, and the manifest fields, it returns the screens the declaration implies, named. The [catalog](/docs/catalog/page)'s returns the page the manifest becomes; [config](/docs/config)'s returns one screen per section of the declared schema. The generated module calls it at build time to bake those screens and again at runtime, so an app registers there whatever it needs to find them later. + +## Next steps + +- [What the build writes](./output.md) — where a screen is mounted, how the client picks it, and what is stamped onto its host +- [Build checks](./checks.md) — everything that stops a build, and what fails it +- [Hosts](../guides/hosts.md) — which root to write, and what each screen can carry +- [Container screens](../guides/container-screens.md) — serving a screen an entity or a block owns diff --git a/docs/ui/ui-runtime/components/Background.md b/docs/ui/components/Background.md similarity index 81% rename from docs/ui/ui-runtime/components/Background.md rename to docs/ui/components/Background.md index 9a7f473..51e4c17 100644 --- a/docs/ui/ui-runtime/components/Background.md +++ b/docs/ui/components/Background.md @@ -1,5 +1,5 @@ --- -sidebar_position: 6 +description: "Draw a full-screen texture behind everything in a form." --- # Background @@ -22,7 +22,7 @@ import { Background } from '@bedrock-core/ui'; `Background` occupies no layout space — it takes no width/height, participates in no flexbox flow, and is invisible to its siblings. It simply renders the given texture across the whole screen, behind all other form content. -Place one anywhere in the tree (conventionally first, at the root). It works on both backends — a plain `ActionForm` tree and inside a [``](./Form/Form.md) modal. +Place one anywhere in the tree (conventionally first, at the root). It works on both backends — a plain `ActionForm` tree and inside a [``](./Form.md) modal. :::note Only the first `` wins If a tree contains more than one ``, only the first is rendered; the rest are ignored. @@ -30,12 +30,9 @@ If a tree contains more than one ``, only the first is rendered; the ## Props -### Component-Specific Props - -#### `texture` -- Type: `string` -- Required: yes -- Description: Resource-pack texture path drawn as the full-screen backdrop, e.g. `'textures/ui/my_background'`. +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `texture` | `string` | — | Resource-pack texture path drawn as the full-screen backdrop, e.g. `'textures/ui/my_background'` | - Constraints: The path must fit within 80 UTF-8 bytes and may not contain a `;` character. ## Examples @@ -52,9 +49,9 @@ function Settings({ onSubmit }) { {'§lSettings'} {'Music'} - - - + + + {'Save'} ); } @@ -81,7 +78,7 @@ function Menu() { } ``` -## Limitations +## Limits - The texture path is capped at 80 UTF-8 bytes and cannot contain `;`. - Only the first `` in a tree is drawn. diff --git a/docs/ui/components/Button.md b/docs/ui/components/Button.md new file mode 100644 index 0000000..6dfe720 --- /dev/null +++ b/docs/ui/components/Button.md @@ -0,0 +1,129 @@ +--- +description: "A press: a handler on a screen or a container screen. A form's own submit and exit are Form.Button." +--- +# Button + +A press. + +## Import + +```tsx +import { Button } from '@bedrock-core/ui'; +``` + +## Usage + +```tsx + +``` + +Buttons are sized intrinsically from their content plus the button's built-in padding. Drop them inside a `Panel` and use flex props to lay them out. + +## Props + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `onPress` | `(event: PressEvent) => unknown \| Promise` | — | Runs on the press, with the player who pressed; see below | +| `children` | `JSX.Node` | — | What the button shows — a `Text`, an `Image`, or a row of both | +| `backgroundHover` | `string` | `background` | Texture drawn while the player hovers | +| `backgroundPressed` | `string` | `background` | Texture drawn while the button is being pressed | +| `backgroundLocked` | `string` | `background` | Texture drawn while the button is disabled | +| `hug` | `boolean` | `false` | Draw the press around what its children draw rather than in the box the layout solved, so it covers exactly the glyphs the client shows. For a press in a line of hugging text, which is what [``](./Trans.md) draws. Action forms only | + +Inherits [control props](./control-props.md). Every state texture falls back to `background`, and `background` falls back to the blank-canvas placeholder, so one texture styles all four states. + +## What a press does + +A press is one of three things, and each is its own component. + +| Component | What happens | Where | +| --- | --- | --- | +| ` + + +``` + +### An icon + +```tsx + +``` + +### Disabled + +```tsx + +``` + +### Fully themed + +```tsx + +``` + +## Notes + +Let buttons size to their content rather than hardcoding `width` and `height`, unless the layout needs a specific footprint. Inside a row, `flex={1}` on each distributes the space evenly. + +Disable a button when its action is unavailable rather than hiding it: a hidden control leaves its box behind on a compiled screen unless the row is a [``](./Panel.md#stack). + +A handler that returns a promise keeps the press's transaction open until it settles, which is what makes an async handoff flash-free. See [State](../guides/state.md#one-ui-slot-per-player). + +This is the primitive [`@bedrock-core/ore-styled`'s `Button`](/docs/ore-styled/Button) is built on. diff --git a/docs/ui/components/Container.md b/docs/ui/components/Container.md new file mode 100644 index 0000000..40530b6 --- /dev/null +++ b/docs/ui/components/Container.md @@ -0,0 +1,103 @@ +--- +description: "The root of a container screen: names the entity or block the screen opens from, and is the screen's own panel." +--- +# Container + +The root of a [container screen](../guides/container-screens.md): names the entity or the block the screen opens from, and is the screen's own panel. + +## Import + +```tsx +import { Container } from '@bedrock-core/ui'; +``` + +## Usage + +```tsx +export default function Furnace() { + return ( + + {'§fFurnace'} + + + + + ); +} +``` + +Its presence decides the backend, the way [`
`](./Form.md) makes a screen a native modal: a tree rooted in a `Container` is laid out once at build time by the [ui-compiler filter](/docs/filters/ui-compiler), baked into JSON UI, and served by `createContainerScreen` to every player who opens the host it names. `render()` rejects it — a container screen is compiled ahead of time, not serialized per player. + +:::caution Experimental: block containers +A block-hosted screen uses its block's `minecraft:block_entity` container, and block containers are an experimental game feature. Expect rough edges, and expect this to change when the game's feature does. Entity-hosted screens are not affected. +::: + +A screen names **exactly one** of `entity` and `block`. Naming both, or neither, fails the build: a screen opens from one host, and which one decides what the build stamps and which vanilla screen the layout is routed onto. Nothing else about the screen changes — the same components, the same cells, the same handlers. + +## Props + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `entity` | `string` | — | Type of the entity the screen opens from, e.g. `'core:furnace'`. The build sizes that entity's `minecraft:inventory` and stamps it with the screen's layout key; the runtime serves the screen when a player interacts with it. The entity must exist in the behavior pack, or the build fails. Exactly one of this and `block` | +| `block` | `string` | — | **Experimental.** Type of the block the screen opens from, e.g. `'core:workbench'`. The build gives that block a `minecraft:block_entity` container sized to the layout, turns its dynamic properties on and stamps the layout key as a block state; a player opens the screen by interacting with a placed one. The block must exist in the behavior pack, or the build fails | +| `onOpen` | `(event: ContainerEvent) => void` | `undefined` | Ran when a player opens the screen, and again for every further viewer. `event.player` is who opened it, `event.host` the entity or block it opened and `event.container` that host's own cells. One layout serves everyone looking, so this is where a screen learns who is there — keep what it needs in state | +| `onClose` | `(event: ContainerEvent) => void` | `undefined` | Ran when a player closes the screen, or leaves the world with it open. `event.player` is who left, `event.host` the entity or block it belonged to, `event.container` its own cells | +| `children` | `JSX.Node` | — | The screen. Anything a form can hold except the [form-only components](../guides/container-screens.md#not-supported), plus [`Slot`](./Slot.md), [`PlayerInventory`](./PlayerInventory.md) and [`Hotbar`](./Hotbar.md) | + +Inherits [control props](./control-props.md). It is the screen's root panel, so `background` draws the frame and `padding` / `gap` / `flexDirection` lay the children out — the same way they would on a `Panel`. + +`event.container` is the screen's own cells as the engine's own [`Container`](https://learn.microsoft.com/minecraft/creator/scriptapi/minecraft/server/container) — the drawn [``](./Slot.md)s in document order, by index or by the `name` each declared, with the routing key, the buttons and the live-value bank invisible. The same cells are reachable from outside the component through `screen.container(host)`; see [Reaching the cells](../guides/container-screens.md#reaching-the-cells). + +## Rules + +The build enforces these, with a message naming the fix: + +- **Exactly one, at the root.** Providers and fragments above it are looked through; anything else beside it is rejected, and a screen with no root at all is refused with the list of roots. +- **Exactly one host.** `entity` or `block`, never both and never neither. +- **No nesting.** One host opens one screen, and no root sits inside another — compose the inner part as a component instead. +- **The content fits the canvas.** A container screen is laid out on the same 320 × 210 canvas as a form, but it cannot scroll: content past the canvas fails the build rather than clipping. +- **A block holds 54 slots.** That is the whole allocation — the routing key, every cell and the live-value bank — so a block-hosted screen that needs more fails the build. An entity's inventory has no such cap. + +## Examples + +### A framed screen + +```tsx + + {'§lVault'} + + + + + + + + + +``` + +`PlayerInventory` and `Hotbar` are not free: a container screen owns the whole vanilla screen, so leave them out and the player's own grids are gone. + +### The same screen on either host + +Nothing above the root differs, so a screen written once serves both: + +```tsx +// crafting.tsx — the screen, named by neither host +export function Crafting({ entity, block }: { entity?: string; block?: string }) { + return ( + + {'§fCrafting'} + + + ); +} + +// crafting_table.screen.tsx +export default () => ; + +// crafting_block.screen.tsx +export default () => ; +``` + +Each screen file is compiled separately and gets its own layout key, so the two hosts open their own copies of the same layout. diff --git a/docs/ui/components/Disclosure.md b/docs/ui/components/Disclosure.md new file mode 100644 index 0000000..a7fc641 --- /dev/null +++ b/docs/ui/components/Disclosure.md @@ -0,0 +1,52 @@ +--- +description: "A header that folds the rows under it entirely on the client, reflowing what is below." +--- +# Disclosure + +A header that folds the rows under it, entirely on the client. + +## Import + +```tsx +import { Disclosure } from '@bedrock-core/ui'; +``` + +## Usage + +```tsx +{'Getting started'}} + headerClosed={{'Getting started...'}} + gap={2} +> + {'Intro'} + {'First steps'} + +``` + +## Props + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `header` | `JSX.Element` | — | What the header draws while the rows show | +| `headerClosed` | `JSX.Element` | `header` | What it draws while they hide | +| `headerHeight` | `number` | `20` | Height of the header box in texels | +| `defaultOpen` | `boolean` | `true` | Whether the rows show when the screen opens | + +Inherits [control props](./control-props.md). The flow props — `flexDirection`, `gap`, `justifyContent`, `alignItems`, `alignContent`, `wrap` — apply to the **rows**, since that is what an author writing `gap` on a disclosure means; everything else sizes and places the fold itself. + +## What folding costs + +The header is a client-side toggle and the rows are a panel that *follows* it, which is vanilla's own idiom for a section that opens and closes. So a fold costs no press, no re-present and no payload. + +The rows sit in a stack, and a stack gives a hidden child no space, so everything below moves up when they fold. That is the one native reflow a frozen screen has — the same one [``](./List.md) is built on. + +## Notes + +Two headers, because a fold is the only thing that can tell them apart. Each is drawn inside its own state, so either may hold anything. + +Rows *follow* the header rather than nesting inside it because content inside a state has no say over its siblings, and rows that have to push what is under them need exactly that. + +## Limits + +Compiled-only. On a serialized screen a fold would be a re-render, which `useState` already does for nothing. diff --git a/docs/ui/components/Dropdown.md b/docs/ui/components/Dropdown.md new file mode 100644 index 0000000..c37fb6c --- /dev/null +++ b/docs/ui/components/Dropdown.md @@ -0,0 +1,98 @@ +--- +description: "Select field with a popup, for use inside a Form." +--- +# Dropdown + +Select field with a popup, for use inside a [`Form`](./Form.md). Pressing it opens a scrollable list of [`Option`](./Option.md) children to choose from. + +## Import + +```tsx +import { Dropdown, Form, Option } from '@bedrock-core/ui'; +``` + +## Usage + +```tsx + console.warn(v.mode)}> + + + {'Save'} + +``` + +:::caution Result is an index, not a value +`Dropdown` reports the selected option's **index** (a `number`) at `values[name]`, not its `value` string — this is the native modal dropdown's behavior. If you need the string back, map the index into your own options array yourself. +::: + +## Props + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `name` | `string` | — | Result key — the selected index appears at `values[name]` in the form's `onSubmit` | +| `defaultValue` | `string` | the first option | Initial selection, matched against a [`Option`](./Option.md)'s `value` | +| `children` | `JSX.Node` | — | The selectable options, authored as [`Option`](./Option.md) elements. Popup rows flow at a fixed row height, so an option's own layout props are ignored here — only its `value`/`label`/style are read | +| `popupBackground` | `string` | the unstyled placeholder texture | Background texture for the popup surface behind the option list | +| `optionBackground` / `optionHover` / `optionSelected` | `string` | — | Group-level default row textures for idle/hover/selected states. Any `Option` can override its own | +| `optionFont` / `optionScale` / `optionAlign` | `TextFont` / `number` / `'left' \| 'center' \| 'right'` | `'mojangles'` / `1.0` / `'left'` | Group-level default label styling for option rows. Any `Option` can override its own | +| `currentColor` | `string` | `''` | Color code prefix (e.g. `'§0'`) applied to the closed-box current-value text | +| `currentFont` / `currentScale` | `TextFont` / `number` | `'mojangles'` / `1.0` | Font and scale for the closed-box current-value label | +| `currentInsetX` / `currentInsetY` | `number` | `8` / vertically centered | Position offset (px) of the current-value label from the closed box's left-middle frame | + +The closed box uses `background`/`backgroundHover`/`backgroundPressed`/`backgroundLocked` for per-state texturing — the same shape as [`Button`](./Button.md). + +Inherits [control props](./control-props.md). + +## Examples + +### Basic dropdown + +```tsx + + +``` + +### Per-option alignment override + +The group sets a default alignment for all options; any option can override its own. + +```tsx + + +``` + +### Reading the result + +```tsx +const options = ['Easy', 'Normal', 'Hard']; + +
{ + const selected = options[v.mode as number]; + console.warn(selected); +}}> + + {options.map(o => + {'Save'} +
+``` + +## Notes + +- Keep `Option` children order stable across renders — the result is an index, so reordering shifts what a saved index means. +- Read `values[name]` as an index and map it back to your own array if you need the string; don't assume it's the `value` you passed in. +- Prefer [`Select`](./Select.md) instead when you want every option visible without an extra tap (e.g. a short radio-style choice). +- For themed screens, prefer [`@bedrock-core/ore-styled`](/docs/ore-styled/Dropdown)'s `Dropdown` over styling this primitive by hand. + +## Limits + +Modal-only. The engine draws no popup on an action form or a container screen, so writing one there is a build error naming the fix rather than a control drawn inert. [``](./Toggle.md) and [` + + +); +``` + +## Props + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `host` | `'form-action' \| 'form-modal' \| 'chest'` | — | The screen this fragment is written for | + +Draws nothing and takes no layout space: its children are laid out as though it were not there. + +## When to reach for it + +A root names the host, so anything under a ``, `
` or `` already knows what it becomes. This is for the other case: a component library that renders **into** a screen it does not own — a set of fields meant for a modal, exported as a fragment for an addon to place. + +Written at the top of that fragment, it says what the library assumed. An addon that drops it on the wrong screen is told where, once, instead of being told about each field in turn. + +## Notes + +The host ids are the same three [Hosts](../guides/hosts.md) names a root resolves to. diff --git a/docs/ui/components/Form.md b/docs/ui/components/Form.md new file mode 100644 index 0000000..8827f12 --- /dev/null +++ b/docs/ui/components/Form.md @@ -0,0 +1,85 @@ +--- +description: "The root that makes a screen a native modal form: one atomic ModalFormData, every value arriving together on submit." +--- +# Form + +The root that makes a screen a native modal form. + +## Import + +```tsx +import { Form } from '@bedrock-core/ui'; +``` + +## Usage + +```tsx title="packs/BP/scripts/settings.screen.tsx" +export default function Settings(): JSX.Element { + return ( + apply(values)}> + {'Settings'} + + + + {'Save'} + + ); +} +``` + +`Form` is a **host**, the way [``](./Screen.md) and [``](./Container.md) are. Its presence makes the renderer build one atomic `ModalFormData` instead of a screen of buttons. + +## One member: Form.Button + +`Form.Button`, with `type` `submit` or `exit`, is the modal's own submit and dismiss button, and the only member `Form` has. Every field is a top-level component imported by its own name, and **which hosts it works on is its own capability**, not something a namespace decides: + +```tsx +import { Button, Dropdown, Form, Input, Option, Select, Slider, Text, Toggle } from '@bedrock-core/ui'; +``` + +[``](./Toggle.md), [``](./Input.md), [``](./Slider.md) and [``](./Dropdown.md) only work here, because the engine draws no text field, slider or popup anywhere else. See [Hosts](../guides/hosts.md) for the whole table. + +## Props + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `onSubmit` | `(event: SubmitEvent) => void` | — | Called once when the player submits. `event.values` holds every control's value keyed by its `name` | +| `onCancel` | `(event: UiEvent) => void` | — | Called when the player dismisses the modal — the X, Esc, or an exit button | +| `children` | `JSX.Node` | — | The controls, the decoration and the action buttons, in any order | + +`Form` takes no [control props](./control-props.md): it is a marker, not a panel. Put a `Panel` inside it for a background, padding or a direction. + +## Values arrive once + +The native modal is atomic: nothing reaches script while it is open, and every value comes back together on submit. + +```tsx +
{ + values.music; // boolean + values.volume; // number + values.nickname; // string +}}> +``` + +`FormValues` is `Record`, where `ModalValue` is `string | number | boolean | undefined`. That is why a control here has a `name` and no `onChange` — there is nothing to call one from. + +A heading is a ``: the modal has no `title` or `body` prop of its own. + +## Rules + +The build enforces these, with a message naming the fix. + +- **Exactly one submit.** A modal has no built-in submit control, so the screen declares one: ``. At most one `` beside it. +- **No nested root.** A `` inside a ``, or a `` or `` inside one, is refused. Mix the two form kinds across separate screens, never nested. +- **Only what this host can draw.** A [``](./Slot.md) or a plain press with a handler is refused here by name — a modal draws its typed controls plus its own two actions and nothing else. + +## Notes + +A form cannot be mutated while it is open, so a state change never repaints it. The player sees a new snapshot when they press. [State](../guides/state.md) covers what follows from that. + +## Next steps + +- [Hosts](../guides/hosts.md) — what each of the three screens can carry +- [``](./Toggle.md) — the clearest example of one component, three mechanisms +- [Modal fields](./Input.md) — the three that live only here +- [`@bedrock-core/ore-styled`](/docs/ore-styled) — the same controls, themed diff --git a/docs/ui/components/Fragment.md b/docs/ui/components/Fragment.md new file mode 100644 index 0000000..f854de2 --- /dev/null +++ b/docs/ui/components/Fragment.md @@ -0,0 +1,80 @@ +--- +description: "A logical grouping component that doesn't render any visual container." +--- +# Fragment + +A logical grouping component that doesn't render any visual container. + +## Import + +```tsx +import { Fragment } from '@bedrock-core/ui'; +// Or use the shorthand syntax: <>... — no import needed +``` + +## Usage + +### Using fragment component + +```tsx + + {'First element'} + {'Second element'} + +``` + +### Using JSX shorthand + +```tsx +<> + {'First element'} + {'Second element'} + +``` + +## Props + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `children` | `JSX.Node` | — | What the fragment groups | + +## Examples + +### Dynamic content groups + +```tsx +function StatusDisplay({ isOnline }: { isOnline: boolean }) { + return ( + <> + {'Status:'} + {isOnline ? ( + <> + {'§2Online'} + {'Connected users: 5'} + + ) : ( + <> + {'§cOffline'} + {'Reconnecting...'} + + )} + + ); +} +``` + +## Notes + +- **Prefer the shorthand syntax** (`<>...`) for readability. +- Use Fragment when you need to return multiple elements from a component. +- Don't use Fragment when you need a visual container or its own flex layout (use `Panel` instead). +- Fragment participates in the parent's flex flow — its children are lifted into the parent's layout. + +## Fragment vs panel + +| Aspect | Fragment | Panel | +|--------|----------|-------| +| Visual rendering | None | Visible container | +| Layout props | Not supported | Supported | +| Flex container | No (children lifted into parent flow) | Yes | +| Use case | Logical grouping | Visual container with its own layout | diff --git a/docs/ui/components/Hotbar.md b/docs/ui/components/Hotbar.md new file mode 100644 index 0000000..cd2e6f0 --- /dev/null +++ b/docs/ui/components/Hotbar.md @@ -0,0 +1,33 @@ +--- +description: "The player's hotbar — the 9 × 1 grid — inside a container screen." +--- +# Hotbar + +The player's hotbar — the 9 × 1 grid — inside a [container screen](../guides/container-screens.md). + +## Import + +```tsx +import { Hotbar } from '@bedrock-core/ui'; +``` + +## Usage + +```tsx + + {'§fFurnace'} + + + + +``` + +The same redrawn grid as [`PlayerInventory`](./PlayerInventory.md), one row tall: 162 × 18 — a thin [`SlotGrid`](./SlotGrid.md) wrapper, ``. Placed by the layout engine at its natural size, and — like the inventory — not drawn unless asked for, because a container screen owns the whole chest screen. + +## Props + +Inherits [control props](./control-props.md). Its size is fixed by the engine's cell; use `alignSelf`, margins and the surrounding panel to place it. + +## Notes + +- Vanilla stacks the hotbar four texels under the inventory; `marginTop={4}` reproduces that. diff --git a/docs/ui/components/Image.md b/docs/ui/components/Image.md new file mode 100644 index 0000000..a89e7fd --- /dev/null +++ b/docs/ui/components/Image.md @@ -0,0 +1,95 @@ +--- +description: "A texture from a resource pack, baked into the screen or carried at runtime." +--- +# Image + +A texture from a resource pack. + +## Import + +```tsx +import { Image } from '@bedrock-core/ui'; +``` + +## Usage + +```tsx + +``` + +Unlike `Text` and `Button`, `Image` is **not** intrinsically sized — give it `width` and `height`, or flex props, or it has no footprint. + +## Props + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `texture` | `string` | `'textures/ui/unstyled'` | Path to the texture in the resource pack, without the file extension | +| `live` | `boolean` | `false` | Carry the texture path at runtime rather than baking it | + +Inherits [control props](./control-props.md). The default is the blank-canvas placeholder, so an `Image` with no `texture` draws a plain box. + +## live + +A compiled image is **baked**: the path is written into the pack, and a later render showing another texture is silently wrong. `live` is how an image whose texture changes says so. + +```tsx + +``` + +It costs one entry on a form. Leave it off for a texture that never changes, which is every decorative image. + +## Texture paths + +A path is relative to the resource pack root, omits the file extension, and uses forward slashes. + +```txt +packs/RP/ +└── textures/ + └── ui/ + ├── icons/ + │ ├── health.png + │ └── mana.png + └── backgrounds/ + └── panel_bg.png +``` + +```tsx + + +``` + +Paths are any length: the texture rides the payload's variable-length tail, so it is never padded, truncated or capped. + +## Examples + +### A row of icons + +```tsx +function Icons(): JSX.Element { + const items = ['diamond', 'gold_ingot', 'iron_ingot', 'emerald']; + + return ( + + {items.map(item => ( + + ))} + + ); +} +``` + +### An icon that presses + +```tsx + +``` + +## Notes + +Prefer nine-sliced textures for backgrounds that scale, keep file sizes small, and match texture dimensions to the UI footprint for crisp rendering. + +## Limits + +Animated (flipbook) textures are not supported. diff --git a/docs/ui/components/Input.md b/docs/ui/components/Input.md new file mode 100644 index 0000000..5ed1675 --- /dev/null +++ b/docs/ui/components/Input.md @@ -0,0 +1,68 @@ +--- +description: "Single-line text field, for use inside a Form." +--- +# Input + +Single-line text field, for use inside a [`Form`](./Form.md). + +## Import + +```tsx +import { Form, Input } from '@bedrock-core/ui'; +``` + +## Usage + +```tsx + console.warn(v.nickname)}> + + {'Save'} + +``` + +## How it works + +`Input` is a pure field declaration — no `onChange` / controlled value. It renders to the native `ModalFormData.textField` control; the result (`string`) arrives at `values[name]` in the form's `onSubmit`, once, on submit. + +## Props + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `name` | `string` | — | Result key — the value appears at `values[name]` in the form's `onSubmit` | +| `placeholder` | `string` | — | Text shown inside the native field when empty | +| `defaultValue` | `string` | `''` | Initial text | +| `font` | `TextFont` | `'mojangles'` | Font family for the typed value and placeholder | +| `scale` | `number` | `1.0` | Scale multiplier relative to the standard glyph size | +| `textOffsetX` / `textOffsetY` | `number` | `8` / vertically centered | Typed-value position offset (px) from the box's left-middle frame | +| `placeholderOffsetX` / `placeholderOffsetY` | `number` | `8` / vertically centered | Placeholder position offset (px), same frame as the typed value | + +The box uses `background`/`backgroundHover`/`backgroundPressed`/`backgroundLocked` for per-state texturing — the same shape as [`Button`](./Button.md); `backgroundPressed` doubles as the focused-field state. + +Inherits [control props](./control-props.md). + +## Examples + +### Basic input + +```tsx + +``` + +### Two inputs side by side + +```tsx + + + + +``` + +## Notes + +- Always provide a `placeholder` — it's the only hint the player gets about what to type. +- Keep `name` stable across renders; it's the only key you get back on submit. +- For themed screens, prefer [`@bedrock-core/ore-styled`](/docs/ore-styled/Input)'s `Input` over styling this primitive by hand. + +## Limits + +Modal-only. The engine draws no text field on an action form or a container screen, so writing one there is a build error naming the fix rather than a control drawn inert. [``](./Toggle.md) and [``](./Select.md) or [``](./Dropdown.md). + +It follows its parent onto whichever host that parent can be drawn on — a native field's row on a modal, a press on a screen, a cell on a container. + +## Import + +```tsx +import { Form, Option, Select } from '@bedrock-core/ui'; +``` + +## Usage + +```tsx + + +``` + +## How it works + +`Option` is layout-only — it is not itself a native control, and the whole group submits as its parent's value: one index, or under a `multiple` `Select` the indices that are on. Under `Select` each option is genuinely flex-laid-out like any other component; under `Dropdown` the popup rows flow at a fixed height, so an option's own layout props are ignored there. + +## Props + +| Prop | Type | Default | Description | +| --- | --- | --- | --- | +| `value` | `string` | — | The option's stable identifier — what a parent's `defaultValue` is matched against. The parent reports the SELECTED option's index on submit, not this value | +| `label` | `string` | — | Option text rendered in the row | +| `font` / `scale` / `align` | `TextFont` / `number` / `'left' \| 'center' \| 'right'` | falls back to the parent's `optionFont` / `optionScale` / `optionAlign` | Per-option label style override | +| `color` / `colorSelected` | `[number, number, number]` | falls back to the parent's `optionColor` / `optionColorSelected`; `colorSelected` then to `color` | Per-option label colour at rest and while selected, RGB in 0..1 (only meaningful under `Select`) | +| `dropSelected` | `number` | falls back to the parent's `optionDropSelected` | How far the label sits lower while selected, in px (only meaningful under `Select`) | +| `background` / `backgroundHover` / `backgroundSelected` | `string` | falls back to the parent's `optionBackground` / `optionHover` / `optionSelected` | Per-option row background override | +| `bullet` / `bulletSelected` / `bulletHover` / `bulletSelectedHover` | `string` | falls back to the parent's matching group prop | Per-option bullet glyph override (only meaningful under `Select`) | +| `bulletWidth` / `bulletHeight` | `number` | falls back to the parent's `bulletWidth` / `bulletHeight` | Per-option bullet size override | + +Inherits [control props](./control-props.md), which lay out real flex rows under `Select`. Under `Dropdown`, layout props are accepted but ignored — popup rows flow at a fixed height. + +## Examples + +### Per-option override + +```tsx + + +``` + +## Notes + +- Keep `value` unique within a group — it's what `defaultValue` matches against. +- A per-option override always wins over the group-level style prop of the same name. +- Generate options from a data array (`options.map(o =>