For the full contribution process — what we accept, how to propose new components, and how API decisions are made — read the Contributing wiki.
Key pages:
- API Conventions — naming, prop patterns, composition rules (read before submitting an RFC)
- Design Conventions — the design-side bar: tokens, spacing, radius, elevation, type, color, motion, and state representations
- Specification Protocol — the 9-phase process for new components
- Component Lifecycle — how components move from lab → core and templates from hidden → visible
- API Arbitration — how we resolve API design questions
- Contributing Templates — building templates/blocks and the template grading rubric
- Blog Review Rubric — how docsite blog posts are reviewed
- Contributing with AI — safe zones, spec protocol, and working with AI tools
This file covers local development setup.
The Node version lives in .nvmrc (currently the 24.x line). CI reads the same
file via node-version-file, so local and CI never drift apart. Don't declare
the version anywhere else.
Via nvm (recommended):
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.zshrc
nvm install # no argument — reads .nvmrcfnm and mise read .nvmrc too. asdf does not; its .tool-versions is
git-ignored precisely so it cannot become a competing source of truth.
Via nodejs.org: Download and install from https://nodejs.org
Astryx uses pnpm as its package manager (declared in
the packageManager and devEngines.packageManager fields of
package.json). You can install pnpm directly:
# Via npm
npm install -g pnpm@11
# Via Homebrew (macOS)
brew install pnpm
# Via standalone installer (no npm or Node.js required)
curl -fsSL https://get.pnpm.io/install.sh | env PNPM_VERSION=11.10.0 sh -
# Via GitHub releases (single binary, no dependencies)
# https://github.com/pnpm/pnpm/releases/latestOr use Corepack to install the exact pnpm version Astryx pins:
corepack enableCorepack ships with Node.js 22 and 24, but current Node.js 25+ releases no
longer bundle it. If corepack is missing and you want the auto-pinning path,
install Corepack manually first:
npm install -g corepack
corepack enableVerify installation:
node --version # v22.x.x or v24.x.x
pnpm --version # 11.x.x# Clone the repo
git clone https://github.com/facebook/astryx.git
cd astryx
# Install dependencies
pnpm install
# Build core package first (required for Storybook)
pnpm -F @astryxdesign/core build
# Start Storybook for component development
cd apps/storybook
pnpm devStorybook loads pre-built packages from dist/ folders, so you need to build packages before running Storybook.
First time setup:
# Build all packages
pnpm build
# Or build just core
pnpm -F @astryxdesign/core buildStart Storybook:
cd apps/storybook
pnpm devStorybook will open at http://localhost:6006 with:
- Theme switcher - Toggle between the base tokens and the Neutral, Stone, and Y2K themes
- Mode switcher - Toggle between Light and Dark modes
- Component stories - Interactive component examples
If you make changes to @astryxdesign/core:
# Rebuild core package
pnpm -F @astryxdesign/core build
# Restart Storybook to see changes
cd apps/storybook
pnpm devThe doc site (apps/docsite/) is a Next.js app that renders the component
documentation at https://astryx.dev. To run it locally:
# First time only — build the workspace packages it depends on
pnpm build
# Start the doc site (Next dev server, defaults to localhost:3000)
pnpm docsitepnpm docsite is a thin alias for pnpm -F @astryxdesign/docsite dev,
which runs the doc site's generate step (theme CSS, registries,
playground scope) before booting Next.
Note:
pnpm docscollides with thenpm docsbuiltin, which tries to open the package's npm page in a browser. Usepnpm docsiteinstead.
astryx/
├── apps/
│ ├── storybook/ # Component playground (localhost:6006)
│ ├── docsite/ # Doc site (localhost:3000)
│ └── sandbox/ # Development testing
│
├── packages/
│ ├── core/ # Core components (Button, Input, etc.)
│ ├── cli/ # CLI tooling (astryx)
│ ├── lab/ # Experimental components (not yet stable)
│ └── themes/ # Theme presets (neutral, stone, y2k, and more)
│
└── internal/ # Internal tooling (not published)
└── test-utils/ # Shared test helpers
| Command | Description |
|---|---|
pnpm install |
Install all dependencies |
pnpm dev |
Start Storybook (alias for pnpm storybook) |
pnpm build |
Build all packages |
pnpm test |
Run all tests |
pnpm test:watch |
Run tests in watch mode |
pnpm storybook |
Start Storybook at localhost:6006 |
pnpm lint |
Lint all packages |
Components use colocated tests — test files live alongside the component.
mkdir -p packages/core/src/MyComponentpackages/core/src/MyComponent/
├── MyComponent.tsx # Component implementation
├── MyComponent.test.tsx # Unit tests (colocated)
├── MyComponent.doc.mjs # Component doc (props, features, examples)
└── index.ts # Public exports
Stories are not colocated — they live in the Storybook app:
apps/storybook/stories/MyComponent.stories.tsx
// MyComponent.tsx
import type {HTMLAttributes, ReactNode, Ref} from 'react';
export interface MyComponentProps extends HTMLAttributes<HTMLDivElement> {
/** Ref forwarded to the root element */
ref?: Ref<HTMLDivElement>;
/** Description for AI-assisted development */
children: ReactNode;
}
/**
* Brief description of the component.
*
* @example
* ```
* <MyComponent>Hello</MyComponent>
* ```
*/
export function MyComponent({children, ref, ...props}: MyComponentProps) {
return (
<div ref={ref} {...props}>
{children}
</div>
);
}
MyComponent.displayName = 'MyComponent';// MyComponent.test.tsx
import {describe, it, expect} from 'vitest';
import {render, screen} from '@testing-library/react';
import {MyComponent} from './MyComponent';
describe('MyComponent', () => {
it('renders children', () => {
render(<MyComponent>Hello</MyComponent>);
expect(screen.getByText('Hello')).toBeInTheDocument();
});
});Stories live in the Storybook app, not next to the component —
apps/storybook/.storybook/main.ts discovers them with
'../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)', so a story anywhere under
packages/ is never picked up. Import the component through its published
entry point, and title it under Core/ (or Lab/ for @astryxdesign/lab).
// apps/storybook/stories/MyComponent.stories.tsx
import type {Meta, StoryObj} from '@storybook/react';
import {MyComponent} from '@astryxdesign/core/MyComponent';
const meta = {
title: 'Core/MyComponent',
component: MyComponent,
tags: ['autodocs'],
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
children: 'Hello World',
},
};// packages/core/src/index.ts
export * from './MyComponent';Note: Do not manually edit the
"exports"field inpackages/core/package.json. It is auto-generated from thesrc/directory byscripts/sync-exports.jsand committed automatically when changes land onmain. If you need to verify your component will be included, runpnpm sync:exports:check.
Every new component — and any change to an interactive one — must clear the
Accessibility Checklist
(wiki) before review. The checklist lives on the wiki so accessibility
experts can refine it without a code PR; reviewers block on it (it is A1–A16
on the Component Audit Rubric),
and it is a hard requirement for a lab → core promotion (see
packages/lab/README.md).
Two repo-side rules worth restating here:
- Compose the shared primitives —
VisuallyHidden,useAnnounce,useFocusTrap, and the focus hooks (useListFocus,useGridFocus,useTreeFocus) — rather than hand-rolling equivalents. They implement the WAI-ARIA APG patterns and are tested once; a bespoke reimplementation of one is a review reject. - CI is the enforcement layer, not a replacement for the checklist: the
pr-a11yjob inci.ymlruns an axe audit on every PR that touches components, a weekly workflow scans the full component surface, and theuseAnnouncelint rule rejects hand-wiredaria-liveregions. axe only catches static, DOM-level issues — keyboard behavior, focus management, and announcement timing are exactly what the checklist and the component's unit tests cover.
The CLI (packages/cli/) is layered so behavior, presentation, and contracts stay separable:
clients/cli/— the Commander program and per-command handlers. A handler is a thin wrapper: parse flags → call the matchingapi/function → render (JSON viajsonOut, or text via the formatter kit inclients/cli/formatters/).api/— the programmatic API (@astryxdesign/cli/api). Each command maps toapi/<name>/, whose functions return a typed{ type, data }envelope. This is the behavior source of truth, soastryx --jsonand the imported function return identical data.authoring/— the pure data contracts (@astryxdesign/cli/authoring): the TypeScript types you author objects against (config, integration, codemod, and the doc-types) plus the sealed zod parsers the CLI runs at the load boundary.foundation/— the bottom layer: cross-cutting infra that everything above builds on — the{ type, data }JSON contract, the stableERROR_CODES, discovery (components, templates), integration contribution validators, and path-safety. It never importsapi/orclients/; if foundation needs something, that something belongs in foundation.
Every CLI surface has a colocated, typed .doc.mjs next to what it describes, annotated with a @type from @astryxdesign/cli/authoring:
| Surface | Doc-type | Lives next to |
|---|---|---|
An API function (a hook or an api/ export) |
FunctionDoc |
api/<name>/<fn>.doc.mjs |
| A CLI command | CommandDoc (references its FunctionDoc via fn) |
clients/cli/commands/<name>.doc.mjs |
| An authored object (config, integration, codemod, the doc-types, the response envelope) | SchemaDoc |
beside the schema (authoring/**, foundation/response/) |
| A closed vocabulary (error codes, response types) | EnumDoc |
foundation/response/ |
| A long-form topic (tokens, principles, theming, …) | ReferenceDoc |
assets/docs/<topic>.doc.mjs |
These are not free-form. parseDoc validates each at load, and a drift harness (packages/cli/test/drift/) enforces that they mirror their source of truth: every CommandDoc's fn/args/options match the live CLI, and the EnumDocs equal ERROR_CODES / the manifest's response-type set exactly. A doc that drifts fails CI.
Most of the conventions above are mechanical, so they're checked rather than reviewed:
| Rule | Enforced by |
|---|---|
the layer directions hold: authoring/ imports no other layer, foundation/ never imports api/ or clients/, api/ never imports clients/ |
ESLint (no-restricted-imports) |
zod stays sealed behind the authoring/ parsers |
ESLint (no-restricted-imports) |
commands register via defineCommand, never straight onto Commander |
ESLint (no-restricted-syntax) |
each doc-type ships type.ts + parse.mjs + <kind>.doc.mjs, re-exports its parser, and appears in parseDoc's @returns |
pnpm check:cli-structure |
each api/<name>/ ships its typedefs, a FunctionDoc, and a test |
pnpm check:cli-structure |
every CommandDoc/EnumDoc matches the live CLI |
the drift harness |
You never hand-write the .d.mts declarations. packages/cli/scripts/sync-api-types.mjs emits them for both api/ and authoring/ from the .mjs JSDoc — gitignored, regenerated at prepack, and stamped @generated. Edit the JSDoc and run pnpm -F @astryxdesign/cli sync:api-types.
That matters because a hand-written declaration shadows the JSDoc in its .mjs, and both ways it can lie shipped once: a missing declaration made a strict consumer resolve the parser as any (surfacing only at pack time as TS7016, since local typechecks run with checkJs and never exercise the packed surface), and a stale parseDoc union silently dropped three doc kinds from the published type while still compiling. Generation removes both. The one declaration still written by hand is authoring/index.d.ts, the curated public barrel.
Author the docs before the handler: defineCommand builds the Commander command from the CommandDoc, so the handler needs it to exist.
- Add the behavior under
api/<name>/, with a colocated<name>.type.mjs(theOptions+{ type, data }response typedefs — the shape source of truth) and a test. - Author the docs — a
FunctionDocatapi/<name>/<fn>.doc.mjsand aCommandDocatclients/cli/commands/<name>.doc.mjs. Copy thesearchpair as a template. - Write the thin handler in
clients/cli/commands/<name>.mjs, registering it withdefineCommand(program, <name>Command, {fn: <name>Fn, action})so--helpand the manifest come from the doc. Call itsregister<Name>fromclients/cli/index.mjs. - Run the checks below. The drift harness catches a doc that disagrees with the live command, and
check:cli-structurecatches a missing typedef, doc, or test.
# Run the CLI locally (no build needed)
node packages/cli/clients/cli/bin/astryx.mjs --help
# Validate every colocated doc parses + mirrors its source of truth
pnpm -F @astryxdesign/cli test # includes the drift suite
pnpm -F @astryxdesign/cli typecheck:authoring
# Structural conventions (doc-type quartets, api/ leaf contents). Also runs as
# part of `pnpm lint` via check:repo, and in the pre-commit hook.
pnpm check:cli-structure
# Keep the generated CLI README tables (commands, error codes, response types)
# in sync with the manifest + EnumDocs. After an intended change, refresh + review:
pnpm -F @astryxdesign/cli readme # regenerate the tables
pnpm -F @astryxdesign/cli readme:check # CI gate: fails on any un-refreshed drift# All tests
pnpm test
# Watch mode
pnpm test:watch
# Specific package
pnpm -F @astryxdesign/core test
# With coverage
pnpm test:coverage
# Accessibility and RTL audits over the built Storybook (see below)
pnpm a11y:audit
pnpm rtl:auditTests are colocated with components:
src/Button/
├── Button.tsx
└── Button.test.tsx # Tests live here
PRs that touch components run an axe-core audit (the pr-a11y CI job) over
the Storybook stories of the changed components. The job fails when it
finds a violation that is not listed in the checked-in baseline,
.github/a11y-baseline.json. Violations are keyed
Component::Story::rule-id, so unrelated markup churn does not invalidate
baseline entries.
To reproduce and fix a failure locally:
# One-time setup
pnpm storybook:build
npx playwright install chromium
# Audit specific components against the baseline (what CI does)
pnpm a11y:audit -- --components Button,DialogFix the violation whenever possible. If it is a known, intentional exception, add it to the baseline (scoped to the affected components, and expect reviewers to ask why):
pnpm a11y:baseline -- --components Button,DialogWhen the audit reports baseline entries as "resolved", delete them from
.github/a11y-baseline.json — the baseline should only shrink over time.
Scope caveat: axe-core automates only a subset of WCAG (roughly a third of the success criteria). A green
pr-a11yjob does not mean a component is accessible — keyboard flows, focus order, screen-reader semantics, and contrast in context still need manual checks.
PRs that touch components also run an RTL audit (pr-rtl), scoped to the
changed components like pr-a11y. It is soft-gated — findings show in the job
summary but don't block. Repro locally with pnpm rtl:audit -- --filter Avatar
(the -- matters: pnpm -F is itself --filter). See
apps/storybook/rtl-audit/README.md.
The same pr-a11y job runs one more Chromium probe
(.github/scripts/modal-close-visibility.js, or pnpm guard:modal-close
against a built Storybook). It opens each modal <dialog> surface in the
list at the top of that script, closes it, and fails if the dialog's computed
display was none at the moment close() ran.
A dialog hidden while still :modal swallows every click on the page, and a
browser is not obliged to release that when close() finally runs — Safari
26.1 did not (#4290). The ordering comes from a CSS transition, so jsdom
cannot see it and the unit suites pass either way. Add a target here when a
component closes a <dialog> on a delay.
A disabled element must never paint a hover state: :hover keeps matching a
disabled control in every engine, so a hover treatment written for the enabled
element is still painted under the pointer. Guard every self-:hover with
:hover:where(:not(:disabled,[aria-disabled="true"])) — :where() adds no
specificity, so the rule weighs the same as before. The @astryx/no-hover-on-disabled
lint rule (autofixable) enforces it at author time; pnpm guard:disabled-hover --storybook-dir apps/storybook/dist sweeps a built Storybook in Chromium and
fails on any disabled element whose paint changes under a forced :hover.
A disabled control must not answer the pointer with an interactive cursor: the
cursor is the only affordance a pointer user gets before they commit to a
click. Write every cursor so the disabled state takes it back —
cursor: {default: 'pointer', ':is(:disabled,[aria-disabled="true"])': 'default'}
— including a flat cursor inside a disabled style, since StyleX merges one
property at a time and a later declaration replaces the earlier one's
conditions along with its value. default rather than not-allowed: a
disabled control sealed behind pointer-events: none shows whatever its
ancestor shows, so one cursor everywhere beats a stronger one we can only
paint on some of them. The @astryx/disabled-cursor lint rule (autofixable)
enforces it at author time; pnpm guard:disabled-cursor --storybook-dir apps/storybook/dist hit-tests every disabled element in a built Storybook in
Chromium and fails on any other cursor.
We use Changesets for versioning, with a thin Astryx layer on top so changelogs stay categorized, contributor-attributed, and aligned with our pre-1.0 conventions.
When you make a change that should be released:
pnpm changeset:newThis wrapper:
- Detects which packages you changed from your git diff and pre-selects them — no hand-enumerating the frontmatter.
- Asks for a category (
breaking,component,feat,fix,perf,docs,chore) — this drives changelog grouping, not the semver bump. - Captures the contributor(s) — defaults to your
gh/git identity, so credit is recorded at authoring time (not reconstructed from the release bot's commit). - Derives the semver bump from the category — a
[breaking]change bumps the minor; everything else bumps the patch (see below).
It writes a normal .changeset/<id>.md — commit it with your PR. The body looks like:
---
'@astryxdesign/core': patch
---
[fix] Spinner inherits the variant foreground on themed buttons (#2717)
@yourhandleYou can also pass everything as flags for non-interactive use:
pnpm changeset:new --category fix --summary "…" --pr 2717 --contributor yourhandleThe bare
pnpm changesetCLI still works, but you must follow the body convention by hand ([category]first line +@handleline). CI (pnpm check:changesets) rejects changesets missing a category or contributor, or whose bump doesn't match the category ([breaking]must beminor, everything elsepatch), or declaring amajorbump while 0.x.
- 0.x (current): bump follows the category. We track standard semver for the
0.x.yrange, where a minor bump is the breaking tier (under a caret range like^0.1.8, npm resolves<0.2.0, so0.1.x → 0.2.0is what signals "may break you"). A[breaking]change bumps the minor (0.x.y → 0.(x+1).0); every other category (feat,fix,component,perf,docs,chore) bumps the patch.majoris never used while 0.x — it would jump to1.0.0.pnpm changeset:newwrites the right bump from the category you pick;pnpm check:changesetsis the CI backstop that enforces the coupling both ways. - All publishable packages are a
fixedgroup, so a single change co-bumps them to the same version. Only genuinely-affected packages get a changelog entry — the rest get a clean version-only bump.
pnpm version-packages # changeset version + scripts/format-changelogs.mjsformat-changelogs.mjs rewrites each just-bumped package CHANGELOG into the doc-site format (h1 version, #### <Category> sections in canonical order, and a #### Contributors section aggregated from the changeset @handles). It's idempotent and has a --check mode for CI drift detection.
Labels signal what's open for contribution:
good first issue/help wanted— ready to be picked up; start here.discussion— still being shaped and not ready for contribution. The problem is recorded but the solution isn't decided. Please don't start work on a fix until it's triaged out ofdiscussion. Comments and ideas are welcome.
For pull requests, use GitHub's native Draft state to signal "not ready to review/merge yet" — open the PR as a draft and mark it ready for review when it's done.
The bar — what a change has to carry, and what blocks — lives on the
Component Audit Rubric.
It is stated there once, so it cannot drift between this file, the reviewer
instructions, and the wiki. Read it before you open a PR: it is the same page
the reviewer applies to your change, and every check carries an id
(A8, T1, P2…) so a finding always points back to the rule behind it.
What you will find there:
- The bright lines that block —
hardcoded colors (
T1,T3) · removing a themeable surface (T2) · raw CSS where StyleX suffices (T8) or raw HTML where a primitive exists (T29) · a broken accessible path (A8) and the accessibility bright lines (A1,A3,A14) · hardcoded user-facing strings (I1,I2,A16) · public API-convention violations (P1–P10) · dropped passthroughs and breaking changes (P2,P11,P12) · a public-repo leak (L15) · a missing changeset (X20). Severity is set by what breaks if it ships, not by how likely the trigger is — the rubric states each rule, its exceptions, and how it is judged. - The bar for your kind of change —
a bug fix owes evidence it was broken before and is fixed now; a new feature
runs the automatable checks plus whatever the diff touches; a new component in
coregets a full audit; a new component inlabis deliberately lax, with the audit as the promotion gate rather than an entry fee. - Which checks your diff earns — a trigger table from what you touched to the checks that fire, so a two-line fix is not reviewed like a new component.
- Recorded component grades —
audited components have a score and an open-blocker count in the wiki's
component-scores.jsonledger, which is useful context on what shape a component is in before you change it. Most components are unaudited, which means "no evidence", not "fine". No PR is gated on a score, and you are never asked to fix problems you inherited by touching a file.
These are this repo's mechanical gates. A reviewer stops at a red one rather than spending judgment on a PR that doesn't build.
pnpm lint:strict # CI severity, not the local warn tier — a warn-tier-green PR is not lint-clean
pnpm test # the full suite, locally; CI is not your test runner
pnpm buildpnpm lint:strict runs pnpm check:repo first, which covers check:sync,
check:package-boundaries, check:changesets, check:demo-media,
check:executable-bits, check:cli-structure, check:use-client, and
check:i18n-catalog — so a green lint:strict also clears the changeset and
'use client' gates.
Also attach before/after screenshots for any visual change, and update the Storybook story for anything you added or altered.
- Create a feature branch from
main - Make your changes with tests
- Clear Before you push:
pnpm lint:strict,pnpm test,pnpm build - Add a changeset if needed:
pnpm changeset:new - Open a PR with a clear description
- Leave "Allow edits by maintainers" enabled (it's checked by default when
you open the PR). This lets us rebase your branch onto the latest
mainto clear merge conflicts and keep CI passing against currentmain, so a PR that's ready doesn't get stuck behind staleness while you're away.
Why this helps.
mainmoves quickly, and a branch that was green a few days ago can go stale — CI last ran against an oldermain, or a merge conflict appears. With maintainer edits enabled we can rebase and re-run CI for you instead of round-tripping. (One exception: PRs that modify.github/workflows/**can't be pushed on your behalf — GitHub requires the author to update those; we'll ping you if so.)
The design-system rules — StyleX usage, semantic tokens, theming, API conventions, accessibility — are on the wiki and indexed from the Component Audit Rubric. What this repo enforces mechanically:
- TypeScript strict mode
- Functional components that declare
refas a prop (React 19 — noforwardRef;@eslint-react/no-forward-refrejects it, and@astryx/require-ref-proprequiresref?: React.Ref<T>on a publicly exported props interface) 'use client';as the first statement of any file importing a React client API — only comments and blank lines may precede it (pnpm check:use-client, part ofpnpm check:repo)- JSDoc comments for AI-assisted development, with
@examplefences left untagged (plain```) or Storybook autodocs won't render them - Export types alongside components
pnpm: command not found
Install pnpm directly:
npm install -g pnpm@11Or enable Corepack if you want to use the repository's pinned pnpm version:
corepack enablecorepack: command not found
Install Corepack manually, then enable it:
npm install -g corepack
corepack enableNode 25+ does not include Corepack. You can either install Corepack manually or install pnpm directly.
Unexpected Node.js version
Check the active version before installing dependencies:
node --versionUse an active LTS line such as 22 or 24 if your shell selected a different
version, such as a non-LTS stable release.
CLI path issues
If astryx is not found in a consuming app, add the package script shown in the
root README.md and run it through your package manager:
pnpm astryx -- component --listIf corepack enable succeeds but pnpm fails to download its binary
(e.g. ECONNRESET, fetch failed, or 503 from registry.npmjs.org),
your environment likely blocks outbound network access.
Alternative install methods (no registry.npmjs.org needed):
brew install pnpm # Homebrew (macOS)
curl -fsSL https://get.pnpm.io/install.sh | sh - # Standalone installer
npm install -g pnpm@11 # Via npmYou can also download the binary directly from GitHub Releases.
Sandboxed IDE terminals: if your IDE blocks all network, run
corepack enable && pnpm install from a regular terminal first, then
open the project in your IDE — node_modules is on the local filesystem
and doesn't need network to use.
"Failed to fetch dynamically imported module"
- Cause: Core package not built or out of date
- Fix:
pnpm -F @astryxdesign/core buildthen restart Storybook
"React is not defined"
- Cause: Missing React import in preview.tsx
- Fix: Ensure
import * as React from 'react';at top of preview.tsx
"Unexpected 'stylex.defineVars' call at runtime"
- Cause: StyleX code trying to run without compilation
- Fix: Storybook should load from
dist/notsrc/. Check vite.config.ts aliases.
Changes not appearing in Storybook
- Rebuild the package:
pnpm -F @astryxdesign/core build - Hard refresh browser: Cmd+Shift+R (Mac) or Ctrl+Shift+R (Windows)
- Clear Storybook cache: Remove
apps/storybook/node_modules/.cache
Astryx accepts community translations via Crowdin. To help translate astryx into your language, visit https://crowdin.com/project/astryx. New locales are picked up automatically after a maintainer reviews the auto-generated translations PR.
Calendar’s compact weekday labels, such as Su and Mo, are generated from
Unicode CLDR data because browsers do not provide that format.
In order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Meta's open source projects.
Complete your CLA here: https://code.facebook.com/cla
We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue.
Meta has a bounty program for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue.