AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
翻译准备中
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证Accessible Angular component library: standalone, zoneless, signal forms, SSR, design tokens
翻译准备中
取自仓库 README
正在加载完整 README。 以下为已保存的摘录。
Angular component library — built as a modern, accessible alternative to suites like PrimeNG.
Nx monorepo.
Status: early, and the API still moves. Form controls are built as a pct-field wrapper
with a control inside it. What is already decided, and what is still missing, is in
docs/ — starting with the gate registry, which lists
every promise next to the machine that proves it.查看完整 README ↗Angular component library — built as a modern, accessible alternative to suites like PrimeNG. Nx monorepo.
Status: early, and the API still moves. Form controls are built as a
pct-fieldwrapper with a control inside it. What is already decided, and what is still missing, is in docs/ — starting with the gate registry, which lists every promise next to the machine that proves it.
Angular 22 · TypeScript 6 · Nx 23 · Vitest · Playwright · SSR
@angular/cdk alone (CDK Overlay only in the entrypoints whose
row of libs/components/size.snapshot.md names it).zone.js is not a dependency of this project), SSR.var() references kept
intact, so an override cascades without a rebuild.prefers-color-scheme), reduced motion
(prefers-reduced-motion) and high contrast (forced-colors).Each of those is a promise with a gate behind it. Which gate, and what its reference control is, is in the registry — generated from the requirements, so it cannot quietly disagree with them.
apps/
sandbox/ demo application / playground
sandbox-e2e/ e2e tests (Playwright)
libs/
components/ the @pacit/components package — one entrypoint per component, 21 in all
(./accordion … ./tooltip, plus ./core and ./icon; the full table is in
libs/components/README.md#entrypoints)
tokens/ DTCG source + build -> CSS/SCSS/TS + contrast gate
docs/ requirements, decisions, lessons and the generated registry (start: docs/README.md)
tools/ the gates (check-*) and the release script
Node 24 (the repo uses nvm). In a non-interactive shell, prefix commands with the wrapper script:
scripts/with-node npm ci
scripts/with-node npx nx serve sandbox --port 4200 # demo on http://localhost:4200
(Or manually: export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh")
Tokens build themselves: tokens is a graph dependency of components and sandbox, so
nx serve and nx build generate them before their consumers. On their own: npx nx build tokens.
npx nx test components # unit tests of the library (Vitest)
npx nx vite:test sandbox # unit tests of the demo app (a different target than `test`)
npx nx e2e sandbox-e2e # e2e on three engines + axe-core audit
npx nx check-package components # one gate; the registry lists the rest
Thirty check-* gates run in CI next to lint, unit tests, build, typecheck and a mutation run.
Each answers for one named promise and each has a reference control — prepared inputs it must
reject — because a gate that quietly stopped measuring anything is the failure this repository
keeps meeting (docs/lessons.md).
E2E covers an axe-core audit of every view, an SSR hydration-error gate, system preferences
(dark mode / reduced motion / forced colors) and visual tests, whose baselines live in
apps/sandbox-e2e/src/__screenshots__/<platform>/ and are versioned with the code. After
a deliberate visual change, refresh them and read the diff in the commit:
npx nx e2e sandbox-e2e -- --update-snapshots=changed visual.spec.ts
ng add @pacit/components
The schematic adds two stylesheets to the build configuration: the skin
(@pacit/components/themes/pct.css) and the CDK overlay styles
(@angular/cdk/overlay-prebuilt.css). Without the first, components render with no appearance at
all, and silently — an unresolved var() is not an error (lesson-36).
Without the second, the PctSelect panel appears somewhere random on the page.
The skin goes first in the list, so an application's own override beats the default. In
a workspace without angular.json (Nx, for instance) the schematic guesses nothing — it prints
both entries to add by hand.
Source: libs/tokens/src/*.json (DTCG). libs/tokens/build.mjs generates dist/pct.css (custom
properties: light theme on :root, [data-theme="light"] and [data-theme="dark"], plus
@media blocks for system preferences) and typed dist/tokens.ts.
Three tiers — primitive → semantic → component — with references kept as var(), so
overriding one variable in any scope cascades without recompiling. Dark mode binds to
:root:not([data-theme]), which makes the system preference a default rather than an order;
<html data-theme="light"> is the switch, and nested data-theme keeps working. Reduced motion
swaps the motion axis, so a component that takes its duration from a token gets that preference
for free.
Including the skin by hand, where ng add did not do it:
// angular.json / project.json — styles
"node_modules/@pacit/components/themes/pct.css"
// or from a stylesheet
@use '@pacit/components/themes/pct.css';
The contrast policy (libs/tokens/src/contrast.policy.json) validates text/background pairs
against WCAG thresholds per theme — error fails the build, warn informs. Token names, tiers
and pairs have gates of their own; the promises they answer for are in
docs/requirements/tokens.md.
The contract, keyboard map and known limits of each are in docs/components/.
PctButton (@pacit/components/button) — an attribute selector on a native <button> or on a
native <a>, variants solid|outline|ghost|soft|hero, sizes sm|md|lg, states
disabled/loading. State is exposed as data-pct-*, inner elements as data-pct-part.
<button pctButton variant="outline" size="lg">Save</button> <a pctButton href="/start">Get started</a>
PctField (@pacit/components/field) — the field wrapper: label, hint, error message,
required marker and [pctPrefix] / [pctSuffix] slots inside the frame. The form contract is
implemented by the control inside, not by the wrapper, so typing stays with the kind of field.
Any control can sit in there — a text input, a select, a checkbox, a radio group.
<!-- text input: a component on a native <input> -->
<pct-field label="E-mail" hint="Work address">
<input pctText type="email" [formField]="userForm.email" />
</pct-field>
<!-- same wrapper, different control -->
<pct-field label="Country">
<pct-select [options]="countries" [formField]="userForm.country" />
</pct-field>
<!-- decorations inside the frame; `inset` (the default) sits on the field surface,
`fill` takes the whole slot and is a surface of its own -->
<pct-field label="Price">
<span pctPrefix aria-hidden="true">$</span>
<input pctText inputmode="numeric" [(value)]="price" />
<button pctSuffix pctButton size="sm" aria-label="Clear">×</button>
</pct-field>
<pct-field label="Search">
<input pctText [(value)]="query" />
<button pctSuffix="fill" pctButton>Search</button>
</pct-field>
Controls also work without the wrapper (with no label and no messages then); a checkbox and a radio group draw their own label in that case.
PctNumber (@pacit/components/field) — a numeric field (input[pctNumber]) whose value is
number | null. Built on <input type="text"> with role="spinbutton" and formatting through
Intl.NumberFormat per LOCALE_ID, deliberately not on <input type="number"> — what that
input costs is in decision 0009.
<!-- integer by default; bounds come from the schema's min()/max() validators -->
<pct-field label="Seats">
<input pctNumber [formField]="form.seats" />
</pct-field>
<!-- an amount: two decimals, half-unit step -->
<pct-field label="Price">
<span pctPrefix aria-hidden="true">$</span>
<input pctNumber [minFractionDigits]="2" [maxFractionDigits]="2" [step]="0.5" [(value)]="price" />
</pct-field>
An empty field is null, not 0; a comma and a dot are both accepted while typing whatever the
locale; rounding and clamping to min/max happen on blur, not mid-keystroke. The keyboard map
is in docs/components/number.md.
PctCheckbox (@pacit/components/checkbox) — a native signal forms control
(FormCheckboxControl). The required field is checked, not value, so it binds with brackets.
<pct-checkbox label="I accept the terms" [formField]="userForm.terms" />
<!-- inside the wrapper: pct-field renders the label, the checkbox does not repeat it -->
<pct-checkbox label="Remember me" [(checked)]="remember" />
<pct-checkbox label="Partially selected" [indeterminate]="true" />
PctRadioGroup + PctRadio (@pacit/components/radio) — a composite: the form control is the
group, not the individual options. Arrow-key navigation comes from the browser (native radios
sharing a name), with no roving tabindex of our own.
<pct-radio-group label="Plan" [formField]="userForm.plan">
<pct-radio value="free">Free</pct-radio>
<pct-radio value="pro">Pro</pct-radio>
</pct-radio-group>
The value is of any type (PctRadioGroup<T>, string by default) — an option can be a union
member or an entity. Equality is the application's call through compareWith, and no selection
is null.
PctSelect (@pacit/components/select) — a listbox with its own panel (not a native <select>),
following the ARIA "select-only combobox" pattern: focus stays on the trigger, the active option
is pointed at by aria-activedescendant. Panel placement and the keyboard map are in
docs/components/select.md.
<pct-field label="Country">
<pct-select [options]="countries" [formField]="userForm.country" />
</pct-field>
The value is of any type (PctSelect<T> / PctSelectOption<T>, string by default) and follows
from the option list. Entities are compared by key — an instance from the server is not the same
reference as the option in the list:
<!-- protected byId = (a: City, b: City) => a.id === b.id; -->
<pct-select [options]="cities" [compareWith]="byId" [(value)]="city" />
No selection is null. An application with a non-nullable field supplies its own empty value
([emptyValue]="''"), so the cross that clears it (clearable) does not write null against
the model's type. A form's reset writes nothing into a control: form().reset() keeps the
values, as Angular documents, and form().reset(value) shows the one it is given.
More than one answer is a different tag, pct-multi-select — same panel, same options, a
value that is a list:
<!-- protected chosen = signal<string[]>([]); -->
<pct-multi-select [options]="countries" [(value)]="chosen" />
Picking leaves the panel open, the chosen rows carry a mark, and the value comes back in the
order of the list. It is a tag rather than a multiple flag because an input cannot decide what
value is — the measurement behind that is in
decision 0034.
Requires the CDK overlay styles:
node_modules/@angular/cdk/overlay-prebuilt.css.
Strings a component prints on its own (the select placeholder, the empty-list message) are English and travel through a DI token. Fields you pass override the defaults, the rest stay:
bootstrapApplication(App, {
providers: [providePctTexts({ selectPlaceholder: 'Sélectionner…', selectEmpty: 'Aucune option' })],
});
providePctTexts also works locally (a component's own providers), so one section of an
application can speak a different language than the rest. Development warnings are not part of
that channel: they are English and silent outside development mode.
Releases go through the manual Release workflow (.github/workflows/release.yml), a dry run
by default, in two dispatches, because main takes only a commit whose CI went green, and the
release commit gets there as a pull request
(0094):
step: prepare bumps the version, writes the CHANGELOG and commits, then pushes the
commit to release/<version>, dispatches CI on it and opens the pull request.step: publish, once CI is green on main, stages the merged commit on npm, then tags
it and creates the GitHub Release.The same rehearsal locally:
node tools/release.mjs --dry-run
A script drives the release rather than nx release alone, because the order matters: version →
stamp the PCT_VERSION constant → build → package gate → CHANGELOG and commit; then, from the
merged commit, build → package gate → stage on npm → tag and GitHub Release. A build before the
bump ships the old constant (lesson-41). The version follows from
conventional commits; before 1.0 a breaking change bumps the minor.
The publish is a staged one. The trusted publisher the workflow runs as may only
npm stage publish: the tarball goes up with provenance, and the version reaches consumers once
a maintainer approves it with 2FA — on npmjs.com, or with npm stage list @pacit/components and
npm stage approve <id>. No token is stored anywhere; the workflow trades its OIDC identity for
a short-lived one at publish time
(0079).
Publishing needs the repository field in libs/components/package.json — without it npm
refuses to attach provenance. check-package.mjs --release blocks on that; day to day it only warns.
What a consumer gets after the release — which Angular majors a version supports, how long an
old line lives, how much notice a deprecation gets and why a breaking change arrives with an
ng update migration — is in docs/support.md. The three numbers there are
not prose: check-support.mjs reads them out of the table and requires the Angular window to
equal the one the peer ranges admit, so the document and the manifest cannot drift apart.
A question about using the library goes to
Discussions. A defect — a promise broken —
and a proposal — a promise to add — are issues,
each with a form that asks for the promise. A vulnerability is reported privately, as
SECURITY.md says.
MIT — Copyright (c) 2026 PacIT - Marek Pac. No CLA: contributions arrive under the repository's license (CONTRIBUTING.md), and what that choice costs us is in decision 0015.
The license covers the code, not the name and not the mark: pacit, the npm scope @pacit,
the domain pacit.pl and the PacIT logo — the drawing inlined in the documentation site's
topbar and shipped as its favicon — belong to the entity in the Copyright line. Releasing
under MIT cannot be undone, and every component goes out under it:
decision 0095.
What that means in practice, because a trademark notice that only says "all rights reserved" tells a fork nothing it can act on: the code is yours to fork, the mark is not yours to wear. Copy, modify and redistribute the library under MIT, with or without attribution beyond the license text. A fork that keeps the logo, the name or the scope in its own published identity — an npm package, a site, an application store listing — is claiming to be this project, and that is the one thing MIT does not grant. Replace the mark with your own, and say what you forked from in words instead. Referring to the project by name to say what your work is built on, or to review or criticise it, needs no permission from anybody.
The LICENSE file ships in the package and two gates watch it: check-package reads the dist
directory, check-consumer the archive after npm pack — because the files field stands
between the two.
The site: components.pacit.pl — every component with its live demos, API, parts, tokens and keyboard map, and the trust page, which renders the registry below. Behind it, the sources:
Requirements and architectural decisions (req-* identifiers): docs/ —
the project axis, requirements,
decisions, lessons and the generated
registry.