diff --git a/.agent/rules/comments.md b/.agent/rules/comments.md
index 419f730c..437bc0e5 100644
--- a/.agent/rules/comments.md
+++ b/.agent/rules/comments.md
@@ -1,13 +1,12 @@
# Comment Rules
-Add comments when they make the code easier to understand or maintain. Omit them when the code speaks for itself.
+Add comments when they make the code easier to understand or maintain. Omit them when the code speaks for itself. For prose style within comments, see `writing.md`.
## Enforce
- Add regular comments to explain **why**, a constraint, domain behavior, or to orient the reader at the start of a non-obvious block
- Place the comment directly above the code it explains
- Omit the trailing period in a short single-line regular comment
-- Use plain punctuation only
- Add doc comments to public and exported functions, methods, and types only when the name and signature alone do not make the behavior or contract clear; describe what it does first, then note constraints, side effects, or non-obvious return or error behavior when needed
- Start doc comments with a single sentence in active voice and end it with a period
- Use `// MARK: -` sparingly as a file-level or large-structure navigation aid, mainly in long files
@@ -16,7 +15,6 @@ Add comments when they make the code easier to understand or maintain. Omit them
## Avoid
-- Do not use em dashes (—) in comments
- Do not restate what the code already says in regular comments
- Do not use comments as a substitute for good naming
- Do not add comments above a block when the intent is already obvious from the code
@@ -87,7 +85,7 @@ const sortedItems = items.slice().sort(compareItems);
```ts
/** Returns whether the user is authenticated. */
-export function isAuthenticated(): boolean
+export function isAuthenticated(): boolean;
```
```ts
diff --git a/.agent/rules/feature-fetch.md b/.agent/rules/feature-fetch.md
index 3ab97a83..c2142f62 100644
--- a/.agent/rules/feature-fetch.md
+++ b/.agent/rules/feature-fetch.md
@@ -9,7 +9,7 @@ Use `feature-fetch` as the typed API layer. Prefer consistent client setup and e
- Compose only the features you need, in a clear order
- Handle `tuple-result` values explicitly; prefer the tuple pattern from `.agent/rules/tuple-result.md`
- Distinguish network failures from request failures when behavior differs
-- Keep request configuration explicit: base URL, headers, timeout, auth
+- Keep request configuration explicit: base URL, headers, and auth; use `signal` for cancellation
- Keep GraphQL operations in dedicated files and use `gql`
## Avoid
@@ -22,12 +22,10 @@ Use `feature-fetch` as the typed API layer. Prefer consistent client setup and e
## Example
```ts
-const itemId = 'item-123';
-const itemResult = await apiClient.get('/items/{itemId}', {
+const [isItemOk, itemErr, item] = await api.get('/items/{itemId}', {
pathParams: { itemId }
});
-const [isItemOk, itemErr, item] = itemResult;
if (!isItemOk) {
if (itemErr instanceof NetworkError) {
throw new AppError('Failed to connect', { cause: itemErr });
@@ -36,5 +34,5 @@ if (!isItemOk) {
throw itemErr;
}
-return item.data;
+return item;
```
diff --git a/.agent/rules/feature-state.md b/.agent/rules/feature-state.md
index ea224036..770eaa26 100644
--- a/.agent/rules/feature-state.md
+++ b/.agent/rules/feature-state.md
@@ -8,7 +8,7 @@ Use `feature-state` and `feature-react/state` as the default state pattern in pr
- Name states with a `$` prefix like `$count`, `$status`, or `$selectedId`
- Use `useFeatureState(state)` when the component needs the raw value and should re-render on changes
- Use `useCompute(state, compute)` when the component needs a derived value from one state
-- Use `useCombinedCompute(states, compute)` when the view depends on multiple states together
+- Use `useCompute(states, compute)` when the view depends on multiple states together
- Use `useListener(state, callback)` for side effects driven by state changes
- Keep state creation and state mutation helpers outside components unless the state is truly local to one component
@@ -16,7 +16,7 @@ Use `feature-state` and `feature-react/state` as the default state pattern in pr
- Do not put many unrelated fields into one state object when separate atoms would stay clearer
- Do not read `state._v` directly in app code when a hook or public API fits
-- Do not use `useFeatureState` and then derive large computed values inline on every render when `useCompute` or `useCombinedCompute` would express the intent better
+- Do not use `useFeatureState` and then derive large computed values inline on every render when `useCompute` would express the intent better
- Do not use `useListener` for simple rendering; keep it for effects and subscriptions
- Do not create ad-hoc React state when the feature already has a matching `feature-state` source of truth
@@ -25,10 +25,10 @@ Use `feature-state` and `feature-react/state` as the default state pattern in pr
### Good
```tsx
-const label = useCompute($seconds, ({ value = 0 }) => formatSeconds(value));
-const isDisabled = useCombinedCompute(
+const label = useCompute($seconds, (seconds = 0) => formatSeconds(seconds));
+const isDisabled = useCompute(
[$status, $selectedId] as const,
- ([{ value: status = 'idle' }, { value: selectedId = null }]) => {
+ ([status = 'idle', selectedId = null]) => {
return status === 'loading' || selectedId == null;
}
);
diff --git a/.agent/rules/tuple-result.md b/.agent/rules/tuple-result.md
index 0113ed78..dab13b3d 100644
--- a/.agent/rules/tuple-result.md
+++ b/.agent/rules/tuple-result.md
@@ -11,7 +11,7 @@ Use `tuple-result` as the default Result pattern when working with repo code tha
- Treat the first slot as the primary branch condition
- Handle the error branch explicitly before using the value
- Use `Ok(...)` and `Err(...)` to construct results
-- Use helpers like `mapOk`, `mapErr`, `match`, `unwrapOr`, and `tAsync` when they make the code simpler
+- Use helpers like `t`, `tAsync`, `mapOk`, `mapErr`, `match`, and `unwrapOr` when they make the code simpler
## Avoid
diff --git a/.agent/rules/vitest.md b/.agent/rules/vitest.md
index 9312a259..8164ff6a 100644
--- a/.agent/rules/vitest.md
+++ b/.agent/rules/vitest.md
@@ -5,10 +5,15 @@ Write tests so they read like small specifications.
## Enforce
- Place tests next to the source file they cover
-- Use `.test.ts` or `.test.tsx`
+- Use `.test.ts`, `.test.tsx`, or `.test-d.ts` for type tests
- Group tests with `describe`
+- Use one top-level `describe` for the unit under test
+- Add nested `describe` blocks only for public members or meaningful behavior categories
+- Include the subject kind in concrete `describe` names, such as `createForm function`, `FlatQueue class`, `listen method`, or `value property`
+- Omit the subject kind from category `describe` names, such as `types`, `validation`, or `dirty tracking`
- Start `it(...)` descriptions with `should`
- Keep each test focused on one behavior
+- Prefer 80/20 coverage by default: cover the core contract, important edge cases, and regressions without exhaustive case matrices unless explicitly requested
- Use Prepare / Act / Assert structure when the test has more than a couple of steps
- Use `async` / `await` for async tests
- Cover both success and error paths where behavior matters
@@ -19,20 +24,53 @@ Write tests so they read like small specifications.
- Do not rely on implicit promises or `.then(...)` chains in tests
- Do not hide setup inside vague helper names
- Do not use unclear test names like `works` or `test x`
+- Do not add a kind suffix to category describes that are not testing a concrete package, function, class, method, or property
-## Example
+## Examples
+
+### Good
```ts
-describe('parseInput', () => {
- it('should parse valid JSON', () => {
- // Prepare
- const input = '{"key":"value"}';
+describe('createStore function', () => {
+ describe('types', () => {
+ it('should infer value types', () => {
+ const store = createStore('value');
+
+ expectTypeOf(store.get()).toEqualTypeOf();
+ });
+ });
+
+ describe('set method', () => {
+ it('should notify listeners when the value changes', () => {
+ // Prepare
+ const store = createStore(0);
+ const listener = vi.fn();
+ store.listen(listener);
+
+ // Act
+ store.set(1);
- // Act
- const result = parseInput(input);
+ // Assert
+ expect(listener).toHaveBeenCalledWith({ value: 1 });
+ });
+ });
+});
+```
- // Assert
- expect(result).toEqual({ key: 'value' });
+### Avoid
+
+```ts
+describe('store', () => {
+ describe('set method edge cases and notification behavior', () => {
+ it('works', () => {
+ const store = createStore(0);
+ const listener = vi.fn();
+ store.listen(listener);
+ store.set(1);
+ store.set(1);
+ expect(store.get()).toBe(1);
+ expect(listener).toHaveBeenCalledTimes(1);
+ });
});
});
```
diff --git a/.agent/rules/writing.md b/.agent/rules/writing.md
new file mode 100644
index 00000000..b91b1eb9
--- /dev/null
+++ b/.agent/rules/writing.md
@@ -0,0 +1,52 @@
+# Writing Rules
+
+Apply these rules to all prose: comments, READMEs, doc strings, PR descriptions, and commit messages.
+
+## Enforce
+
+- Use a colon to introduce an explanation, elaboration, or list that follows naturally from the preceding clause
+- Use a comma or split into two sentences when an em dash would otherwise bridge two independent thoughts
+- Use parentheses for brief asides that do not need to interrupt the sentence rhythm
+- Write in active voice
+- Keep sentences short and direct; one idea per sentence is the default
+- End full sentences with a period; omit the period on short fragments used as labels or list items
+
+## Avoid
+
+- Do not use em dashes (—); replace with a colon, comma, parentheses, or a restructured sentence depending on context
+- Do not use filler phrases ("it is worth noting that", "in order to", "as mentioned above")
+- Do not pad sentences with hedges that add no information ("basically", "essentially", "simply")
+
+## Examples
+
+### Good
+
+```
+This function has two phases: validation and persistence.
+```
+
+```
+Mutable values (arrays, objects) are copied before storing.
+```
+
+```
+Eager validation can feel intrusive before the user has finished filling in the form.
+```
+
+```
+The library works directly without an adapter.
+```
+
+### Avoid
+
+```
+This function has two phases — validation and persistence.
+```
+
+```
+Mutable values — arrays, objects — are copied before storing.
+```
+
+```
+Eager validation can feel intrusive — the user hasn't finished yet.
+```
diff --git a/.github/assets/banner.svg b/.github/assets/banner.svg
index c5ad628d..685010bd 100644
--- a/.github/assets/banner.svg
+++ b/.github/assets/banner.svg
@@ -1,6 +1,6 @@
diff --git a/.github/assets/logo.png b/.github/assets/logo.png
deleted file mode 100644
index fc9f904a..00000000
Binary files a/.github/assets/logo.png and /dev/null differ
diff --git a/.github/assets/logo_v1.png b/.github/assets/logo_v1.png
deleted file mode 100644
index 86eda625..00000000
Binary files a/.github/assets/logo_v1.png and /dev/null differ
diff --git a/.gitignore b/.gitignore
index 0b7e1ff8..fe49fb08 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,4 @@ yarn-debug.log*
yarn-error.log*
.todo
.DS_Store
+.local
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index f000bbc5..69ab3d71 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -35,7 +35,7 @@
// File nesting in explorer
"explorer.fileNesting.enabled": true,
"explorer.fileNesting.patterns": {
- "*.ts": "${capture}.js, ${capture}.test.ts, ${capture}.md",
+ "*.ts": "${capture}.js, ${capture}.test.ts, ${capture}.test-d.ts, ${capture}.md",
"*.js": "${capture}.js.map, ${capture}.min.js, ${capture}.d.ts, ${capture}.test.js, ${capture}.md",
"*.jsx": "${capture}.js, ${capture}.md",
"*.tsx": "${capture}.ts, ${capture}.stories.tsx, ${capture}.md",
diff --git a/AGENTS.md b/AGENTS.md
index 6947714b..dd79bee9 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -19,6 +19,7 @@ This is the shared source of truth for agent guidance in this repository.
- `*Cx.ts` feature context pattern: `.agent/rules/cx-pattern.md`
- General code style: `.agent/rules/style-guide.md`
- Comments: `.agent/rules/comments.md`
+- Writing style (prose, READMEs, commit messages): `.agent/rules/writing.md`
- `tuple-result`: `.agent/rules/tuple-result.md`
- Vitest tests: `.agent/rules/vitest.md`
- `feature-fetch`: `.agent/rules/feature-fetch.md`
diff --git a/README.md b/README.md
index 7d17cb98..94891151 100644
--- a/README.md
+++ b/README.md
@@ -11,50 +11,61 @@
-A collection of open source libraries maintained by [builder.group](https://builder.group). Let's build together.
-
-## 📦 Packages
-
-| Package | Description | NPM Package |
-| ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- |
-| [config](https://github.com/builder-group/community/blob/develop/packages/config) | Collection of ESLint, Vite, and Typescript configurations | [`@blgc/config`](https://www.npmjs.com/package/@blgc/config) |
-| [ecsify](https://github.com/builder-group/community/blob/develop/packages/ecsify) | A flexible, typesafe, and performance-focused Entity Component System (ECS) library for TypeScript | [`ecsify`](https://www.npmjs.com/package/ecsify) |
-| [eprel-client](https://github.com/builder-group/community/blob/develop/packages/eprel-client) | Typesafe and straightforward fetch client for interacting with the European Product Registry for Energy Labelling (EPREL) API using feature-fetch | [`eprel-client`](https://www.npmjs.com/package/eprel-client) |
-| [feature-fetch](https://github.com/builder-group/community/blob/develop/packages/feature-fetch) | Straightforward, typesafe, and feature-based fetch wrapper supporting OpenAPI types | [`feature-fetch`](https://www.npmjs.com/package/feature-fetch) |
-| [feature-form](https://github.com/builder-group/community/blob/develop/packages/feature-form) | Straightforward, typesafe, and feature-based form library | [`feature-form`](https://www.npmjs.com/package/feature-form) |
-| [feature-logger](https://github.com/builder-group/community/blob/develop/packages/feature-logger) | Straightforward, typesafe, and feature-based logging library | [`feature-logger`](https://www.npmjs.com/package/feature-logger) |
-| [feature-react](https://github.com/builder-group/community/blob/develop/packages/feature-react) | ReactJs extension for the feature-state and feature-form library, providing hooks and features for ReactJs | [`feature-react`](https://www.npmjs.com/package/feature-react) |
-| [feature-state](https://github.com/builder-group/community/blob/develop/packages/feature-state) | Straightforward, typesafe, and feature-based state management library for ReactJs | [`feature-state`](https://www.npmjs.com/package/feature-state) |
-| [head-metadata](https://github.com/builder-group/community/blob/develop/packages/head-metadata) | Typesafe and straightforward utility for extracting structured metadata (like ``, ``, and ``) from the `` of an HTML document. | [`head-metadata`](https://www.npmjs.com/package/head-metadata) |
-| [openapi-ts-router](https://github.com/builder-group/community/blob/develop/packages/openapi-ts-router) | Thin wrapper around the router of web frameworks like Express and Hono, offering OpenAPI typesafety and seamless integration with validation libraries such as Valibot and Zod | [`openapi-ts-router`](https://www.npmjs.com/package/openapi-ts-router) |
-| [rollup-presets](https://github.com/builder-group/community/blob/develop/packages/rollup-presets) | A collection of opinionated, production-ready Rollup presets | [`rollup-presets`](https://www.npmjs.com/package/rollup-presets) |
-| [split-flap-board](https://github.com/builder-group/community/blob/develop/packages/split-flap-board) | Web component that simulates a split-flap display, the mechanical boards found in airports and train stations | [`split-flap-board`](https://www.npmjs.com/package/split-flap-board) |
-| [tuple-result](https://github.com/builder-group/community/blob/develop/packages/tuple-result) | A minimal, functional, and tree-shakable Result library for TypeScript that prioritizes simplicity and serialization | [`tuple-result`](https://www.npmjs.com/package/tuple-result) |
-| [types](https://github.com/builder-group/community/blob/develop/packages/types) | Shared TypeScript type definitions used across builder.group community packages | [`@blgc/types`](https://www.npmjs.com/package/@blgc/types) |
-| [utils](https://github.com/builder-group/community/blob/develop/packages/utils) | Straightforward, typesafe, and tree-shakable collection of utility functions | [`@blgc/utils`](https://www.npmjs.com/package/@blgc/utils) |
-| [validatenv](https://github.com/builder-group/community/blob/develop/packages/validatenv) | Type-safe, straightforward, and lightweight library for validating environment variables using existing validation libraries like Zod, Valibot, and Yup. | [`validatenv`](https://www.npmjs.com/package/validatenv) |
-| [validation-adapter](https://github.com/builder-group/community/blob/develop/packages/validation-adapter) | Universal validation adapter that integrates various validation libraries like Zod, Valibot, and Yup | [`validation-adapter`](https://www.npmjs.com/package/validation-adapter) |
-| [validation-adapters](https://github.com/builder-group/community/blob/develop/packages/validation-adapters) | Pre-made validation adapters for the validation-adapter library, including adapters for Zod and Valibot | [`validation-adapters`](https://www.npmjs.com/package/validation-adapters) |
-| [xml-tokenizer](https://github.com/builder-group/community/blob/develop/packages/xml-tokenizer) | Straightforward and typesafe XML tokenizer that streams tokens through a callback mechanism | [`xml-tokenizer`](https://www.npmjs.com/package/xml-tokenizer) |
-
-### 🚧 Deprecated
-
-> **Note:** These packages are deprecated. I'm no longer using them, but if you need them maintained or have questions, please [open an issue](https://github.com/builder-group/community/issues). I'm open to keep maintaining them if there's community interest.
-
-| Package | Description | NPM Package | Deprecated Since |
-| ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------------- |
-| [elevenlabs-client](https://github.com/builder-group/community/blob/develop/packages/_deprecated/elevenlabs-client) | Typesafe and straightforward fetch client for interacting with the ElevenLabs API using feature-fetch | [`elevenlabs-client`](https://www.npmjs.com/package/elevenlabs-client) | November 6, 2025 |
-| [figma-connect](https://github.com/builder-group/community/blob/develop/packages/_deprecated/figma-connect) | Straightforward and typesafe wrapper around the communication between the app/ui (iframe) and plugin (sandbox) part of a Figma Plugin | [`figma-connect`](https://www.npmjs.com/package/figma-connect) | November 6, 2025 |
-| [google-webfonts-client](https://github.com/builder-group/community/blob/develop/packages/_deprecated/google-webfonts-client) | Typesafe and straightforward fetch client for interacting with the Google Web Fonts API using feature-fetch | [`google-webfonts-client`](https://www.npmjs.com/package/google-webfonts-client) | November 6, 2025 |
-| [kleinanzeigen-client](https://github.com/builder-group/community/blob/develop/packages/_deprecated/kleinanzeigen-client) | Typesafe and straightforward fetch client for interacting with the Kleinanzeigen API using feature-fetch | [`kleinanzeigen-client`](https://www.npmjs.com/package/kleinanzeigen-client) | November 6, 2025 |
-
-### 📚 Examples
+Open source packages from [builder.group](https://builder.group) projects, shared in case they're useful.
+
+## Packages
+
+| Package | Description | NPM Package |
+| ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
+| [config](https://github.com/builder-group/community/blob/develop/packages/config) | Shared ESLint, Prettier, Vite, and TypeScript configuration presets | [`@blgc/config`](https://www.npmjs.com/package/@blgc/config) |
+| [ecsify](https://github.com/builder-group/community/blob/develop/packages/ecsify) | Typesafe ECS library for building entity-component apps in TypeScript | [`ecsify`](https://www.npmjs.com/package/ecsify) |
+| [feature-core](https://github.com/builder-group/community/blob/develop/packages/feature-core) | Typesafe `.with()` composition primitives for building extensible TypeScript libraries | [`feature-core`](https://www.npmjs.com/package/feature-core) |
+| [feature-fetch](https://github.com/builder-group/community/blob/develop/packages/feature-fetch) | Typesafe fetch wrapper with composable middleware and OpenAPI type support | [`feature-fetch`](https://www.npmjs.com/package/feature-fetch) |
+| [feature-form](https://github.com/builder-group/community/blob/develop/packages/feature-form) | Lightweight typesafe form library with reactive fields and Standard Schema validation | [`feature-form`](https://www.npmjs.com/package/feature-form) |
+| [feature-logger](https://github.com/builder-group/community/blob/develop/packages/feature-logger) | Typesafe logger with composable prefixes, labels, timestamps, and output styles | [`feature-logger`](https://www.npmjs.com/package/feature-logger) |
+| [feature-react](https://github.com/builder-group/community/blob/develop/packages/feature-react) | React hooks for reactive state and form subscriptions | [`feature-react`](https://www.npmjs.com/package/feature-react) |
+| [feature-state](https://github.com/builder-group/community/blob/develop/packages/feature-state) | Lightweight reactive state container with composable feature extensions | [`feature-state`](https://www.npmjs.com/package/feature-state) |
+| [head-metadata](https://github.com/builder-group/community/blob/develop/packages/head-metadata) | HTML head metadata extractor for title, meta, and link tags | [`head-metadata`](https://www.npmjs.com/package/head-metadata) |
+| [openapi-ts-router](https://github.com/builder-group/community/blob/develop/packages/openapi-ts-router) | OpenAPI-typed router helpers for Express and Hono with runtime validation | [`openapi-ts-router`](https://www.npmjs.com/package/openapi-ts-router) |
+| [rollup-presets](https://github.com/builder-group/community/blob/develop/packages/rollup-presets) | Rollup presets and plugins for building TypeScript libraries | [`rollup-presets`](https://www.npmjs.com/package/rollup-presets) |
+| [tuple-result](https://github.com/builder-group/community/blob/develop/packages/tuple-result) | Minimal, tree-shakable Result type using plain arrays for easy serialization | [`tuple-result`](https://www.npmjs.com/package/tuple-result) |
+| [types](https://github.com/builder-group/community/blob/develop/packages/types) | Shared utility, API, and OpenAPI TypeScript types for builder.group packages | [`@blgc/types`](https://www.npmjs.com/package/@blgc/types) |
+| [utils](https://github.com/builder-group/community/blob/develop/packages/utils) | Tree-shakable TypeScript utilities for colors, IDs, objects, URLs, and math | [`@blgc/utils`](https://www.npmjs.com/package/@blgc/utils) |
+| [validatenv](https://github.com/builder-group/community/blob/develop/packages/validatenv) | Environment variable validation with Zod, Valibot, or Yup | [`validatenv`](https://www.npmjs.com/package/validatenv) |
+| [xml-tokenizer](https://github.com/builder-group/community/blob/develop/packages/xml-tokenizer) | Streaming XML tokenizer with callback-based parsing and object helpers | [`xml-tokenizer`](https://www.npmjs.com/package/xml-tokenizer) |
+
+### Discontinued
+
+> **Note:** These packages are no longer maintained. If you need support or have questions, please [open an issue](https://github.com/builder-group/community/issues). Happy to revisit if there's community interest.
+
+| Package | Description | NPM Package | Discontinued Since |
+| ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------ |
+| [@blgc/cli](https://github.com/builder-group/community/blob/develop/packages/_deprecated/cli) | Rollup and Esbuild-powered CLI for bundling TypeScript libraries | [`@blgc/cli`](https://www.npmjs.com/package/@blgc/cli) | March 9, 2026 |
+| [elevenlabs-client](https://github.com/builder-group/community/blob/develop/packages/_deprecated/elevenlabs-client) | Typesafe API client for the ElevenLabs text-to-speech API | [`elevenlabs-client`](https://www.npmjs.com/package/elevenlabs-client) | November 6, 2025 |
+| [eprel-client](https://github.com/builder-group/community/blob/develop/packages/eprel-client) | Typesafe API client for the EU EPREL energy label registry | [`eprel-client`](https://www.npmjs.com/package/eprel-client) | May 21, 2026 |
+| [figma-connect](https://github.com/builder-group/community/blob/develop/packages/_deprecated/figma-connect) | Typed message bridge between Figma plugin UI iframes and sandbox code | [`figma-connect`](https://www.npmjs.com/package/figma-connect) | November 6, 2025 |
+| [google-webfonts-client](https://github.com/builder-group/community/blob/develop/packages/_deprecated/google-webfonts-client) | Typesafe API client for Google Web Fonts metadata and font downloads | [`google-webfonts-client`](https://www.npmjs.com/package/google-webfonts-client) | November 6, 2025 |
+| [kleinanzeigen-client](https://github.com/builder-group/community/blob/develop/packages/_deprecated/kleinanzeigen-client) | Typesafe API client for scraping and extracting Kleinanzeigen listings | [`kleinanzeigen-client`](https://www.npmjs.com/package/kleinanzeigen-client) | November 6, 2025 |
+| [openapi-express](https://github.com/builder-group/community/blob/develop/packages/_deprecated/openapi-express) | OpenAPI-typed Express router wrapper with Zod request validation | [`openapi-express`](https://www.npmjs.com/package/openapi-express) | January 1, 2025 |
+| [split-flap-board](https://github.com/builder-group/community/blob/develop/packages/split-flap-board) | Web Components for animated split-flap boards with configurable spools and grids | [`split-flap-board`](https://www.npmjs.com/package/split-flap-board) | May 21, 2026 |
+| [validation-adapter](https://github.com/builder-group/community/blob/develop/packages/validation-adapter) | Universal validation abstraction for validators like Zod, Valibot, and Yup | [`validation-adapter`](https://www.npmjs.com/package/validation-adapter) | Soon |
+| [validation-adapters](https://github.com/builder-group/community/blob/develop/packages/validation-adapters) | Ready-made validation-adapter implementations for Zod, Valibot, Yup, and Standard Schema | [`validation-adapters`](https://www.npmjs.com/package/validation-adapters) | Soon |
+| [webito](https://github.com/builder-group/community/blob/develop/packages/_deprecated/webito) | ECS-powered web editor experiment for Tailwind-style component editing | [`webito`](https://www.npmjs.com/package/webito) | May 17, 2026 |
+
+## Templates
+
+| Template | Description |
+| ------------------------------------------------------------------------------------------------ | --------------------------------------------------- |
+| [desktop-tauri](https://github.com/builder-group/community/tree/develop/templates/desktop-tauri) | Tauri desktop app template with React and Specta |
+| [web-tanstack](https://github.com/builder-group/community/tree/develop/templates/web-tanstack) | TanStack Start web app template with React and Vite |
+
+## Examples
> See [`/examples`](https://github.com/builder-group/community/tree/develop/examples)
### `feature-fetch`
-- [`feature-fetch/vanilla/open-meteo`](https://github.com/builder-group/community/tree/develop/examples/feature-fetch/vanilla/open-meteo)
+- [`feature-fetch/vanilla/basic`](https://github.com/builder-group/community/tree/develop/examples/feature-fetch/vanilla/basic)
### `feature-form`
@@ -62,7 +73,7 @@ A collection of open source libraries maintained by [builder.group](https://buil
### `feature-state`
-- [`feature-state/react/counter`](https://github.com/builder-group/community/tree/develop/examples/feature-state/react/counter)
+- [`feature-state/react/basic`](https://github.com/builder-group/community/tree/develop/examples/feature-state/react/basic)
### `openapi-ts-router`
@@ -73,14 +84,7 @@ A collection of open source libraries maintained by [builder.group](https://buil
- [`xml-tokenizer/vanilla/playground`](https://github.com/builder-group/community/tree/develop/examples/xml-tokenizer/vanilla/playground)
-## 🧩 Templates
-
-| Template | Description |
-| ------------------------------------------------------------------------------------------------ | --------------------------------------------------- |
-| [desktop-tauri](https://github.com/builder-group/community/tree/develop/templates/desktop-tauri) | Tauri desktop app template with React and Specta |
-| [web-tanstack](https://github.com/builder-group/community/tree/develop/templates/web-tanstack) | TanStack Start web app template with React and Vite |
-
-## ❓ FAQ
+## FAQ
### What does `blgc` stand for?
@@ -88,54 +92,39 @@ A collection of open source libraries maintained by [builder.group](https://buil
### Why a Monorepo?
-Maintaining all libraries in a single repository keeps things simple and efficient. A monorepo allows for shared tooling, consistent versioning, and streamlined CI/CD workflows, while making cross-library changes easier. This approach simplifies collaboration and reduces overhead, ensuring all libraries remain in sync.
+Keeping all packages in one repository means shared tooling, consistent versioning, and streamlined CI/CD with no extra overhead. Cross-package changes are easier to make and stay in sync automatically.
-The only disadvantage is that it's harder to discover individual libraries via SEO since they're all part of one repo. However, the benefits far outweigh this limitation.
+The only trade-off is that individual packages are harder to discover via SEO since they all live under one repo, but that's an acceptable cost.
### Why two package build modes (`build` vs `build:prod`)?
-Development builds (`pnpm build`):
-
-- Includes TypeScript declaration maps (IDE navigation goes directly to source files instead of compiled definitions)
-- Easier debugging (no code minification and optimizations)
+`pnpm build` (development): includes TypeScript declaration maps so IDE navigation goes to source files instead of compiled definitions, and skips minification for easier debugging.
-Production builds (`pnpm build:prod`):
+`pnpm build:prod` (production): smaller output, minification enabled, declaration maps excluded. Declaration maps cause npm publish errors (e.g. `EINVALIDTAGNAME` in GitHub CLI) so they must be stripped from published packages.
-- Smaller package size
-- No development artifacts in published packages
-- Code minification and optimizations enabled
-- Prevents npm errors with declaration maps (e.g. `EINVALIDTAGNAME` in GitHub CLI)
-
-To switch between modes:
+### Why is `@blgc/types` listed as a dependency instead of a devDependency?
-- Development: `pnpm build` (includes declaration maps)
-- Production: `pnpm build:prod` (excludes declaration maps)
+The `@blgc/types` package provides TypeScript type definitions shared across packages in this repo. When listed as a `devDependency`, these types are excluded from the final npm package, causing broken type checks and missing autocompletions for consumers. Listing it as a `dependency` ensures the types are accessible downstream.
-### Why is `@blgc/types` listed as a dependency instead of a devDependency?
+### What are features?
-The `@blgc/types` package provides crucial TypeScript type definitions to ensure full type safety for feature-based libraries. When listed as a `devDependency`, these types are excluded from the final NPM package, resulting in broken type checks and missing autocompletions in projects consuming these libraries. By adding it as a `dependency`, we ensure that the type definitions are bundled and accessible to downstream projects, maintaining a seamless developer experience.
+A feature is a self-contained extension that adds typed methods or behaviour to a host object via `.with()`. The host starts with a base API, and each `.with(feature())` call extends it by adding methods, validating dependencies, and narrowing the TypeScript type. See [`feature-core`](https://www.npmjs.com/package/feature-core) for the underlying primitives.
-### Why do we use the "wrapper pattern" (`withLogger(withStorage(withUndo(createState(0))))`) instead of a declarative API?
+### Why do libraries use a `.with()` chain instead of a declarative feature array?
-While declarative APIs like the following offer [better developer experience (DX)](https://www.reddit.com/r/reactjs/comments/1huxvci/i_built_a_bloated_state_manager_then_i_fixed_it/):
+The `.with()` chain (powered by [`feature-core`](https://www.npmjs.com/package/feature-core)) gives full TypeScript inference per step. Each call narrows the type based on what was installed before it, including dependency validation:
```ts
-createState({
- defaultValue: 0,
- features: [withUndo(), withStorage(), withLogger()]
-});
-```
+const $count = createState(0).with(undoFeature()).with(storageFeature());
-We currently use the "wrapper pattern" because it ensures better TypeScript type inference. Each wrapper function modifies the state's type in a specific sequence, which is harder to achieve reliably with a feature array.
+$count.undo(); // typed
+$count.missing(); // type error
+```
-We're [actively exploring solutions](https://github.com/builder-group/community/blob/develop/packages/feature-state/src/_experimental) to support both patterns, combining the type safety of the wrapper pattern with the simplicity of declarative APIs. Contributions and ideas are always welcome :)
+A declarative feature array like `features: [undoFeature(), storageFeature()]` can't validate dependencies or accumulate types left-to-right reliably without losing inference. The chain solves both.
-### Why do feature-based libraries use Objects instead of Classes?
+### Why objects instead of classes?
This [Medium post](https://medium.com/@markmiro/thoughts-on-choosing-between-plain-js-objects-and-classes-6422af8aaad5) explains the key differences well.
-In short, we use objects because they are more flexible and allow for the kind of extensibility we need. Achieving this level of extensibility with classes isn't feasible for our use case, so using objects was the better choice.
-
-### What Features?
-
-Think of features like components in an Entity Component System (ECS). Every feature based object (e.g., feature-state) has base functionality, and additional components (features) can be added to extend it. Unlike traditional ECS, we only adopt the concept of Components, without Systems or Entities. Each component contains the necessary functions to interact with the feature based object.
+Objects are more flexible and allow for the kind of extensibility the `.with()` model needs. Achieving the same with classes isn't feasible, so objects were the better choice.
diff --git a/apps/web/package.json b/apps/web/package.json
index 984a8330..5c7a6025 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -26,28 +26,28 @@
"update:latest": "pnpm update --latest"
},
"dependencies": {
- "@tanstack/react-router": "^1.169.1",
- "@tanstack/react-router-devtools": "^1.166.13",
- "@tanstack/react-start": "^1.167.62",
+ "@tanstack/react-router": "^1.170.7",
+ "@tanstack/react-router-devtools": "^1.167.0",
+ "@tanstack/react-start": "^1.168.10",
"@vercel/analytics": "^2.0.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"feature-fetch": "workspace:*",
- "react": "^19.2.5",
- "react-dom": "^19.2.5",
- "tailwind-merge": "^3.5.0",
- "zod": "^4.4.2"
+ "react": "^19.2.6",
+ "react-dom": "^19.2.6",
+ "tailwind-merge": "^3.6.0",
+ "zod": "^4.4.3"
},
"devDependencies": {
"@mdx-js/rollup": "^3.1.1",
"@tailwindcss/typography": "^0.5.19",
- "@tailwindcss/vite": "^4.2.4",
+ "@tailwindcss/vite": "^4.3.0",
"@types/mdx": "^2.0.13",
- "@types/node": "^25.6.0",
- "@types/react": "^19.2.14",
+ "@types/node": "^25.9.1",
+ "@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3",
- "@vitejs/plugin-react": "^6.0.1",
+ "@vitejs/plugin-react": "^6.0.2",
"nitro": "3.0.260429-beta",
- "tailwindcss": "^4.2.4"
+ "tailwindcss": "^4.3.0"
}
}
diff --git a/apps/web/src/components/display/CanvasButton.tsx b/apps/web/src/components/display/CanvasButton.tsx
index 43311269..812da244 100644
--- a/apps/web/src/components/display/CanvasButton.tsx
+++ b/apps/web/src/components/display/CanvasButton.tsx
@@ -18,11 +18,11 @@ export interface TCanvasButtonProps
VariantProps {}
export const canvasButtonVariants = cva(
- 'rounded-lg px-5 py-2.5 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-base-500 focus-visible:ring-offset-2',
+ 'focus-visible:ring-base-500 rounded-lg px-5 py-2.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none',
{
variants: {
variant: {
- outline: 'text-base-700 ring-1 ring-base-300 hover:bg-base-200 hover:ring-base-400',
+ outline: 'text-base-700 ring-base-300 hover:bg-base-200 hover:ring-base-400 ring-1',
primary: 'bg-base-950 text-base-0 hover:bg-base-800'
}
},
diff --git a/apps/web/src/components/display/StatusBadge.tsx b/apps/web/src/components/display/StatusBadge.tsx
index 099aa5e1..cfbfa0fc 100644
--- a/apps/web/src/components/display/StatusBadge.tsx
+++ b/apps/web/src/components/display/StatusBadge.tsx
@@ -32,7 +32,7 @@ export interface TStatusBadgeProps extends VariantProps {
}
export const tagLinkVariants = cva(
- 'rounded px-2 py-0.5 text-xs transition-colors hover:text-base-800 ring-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-base-500 focus-visible:ring-offset-1',
+ 'hover:text-base-800 focus-visible:ring-base-500 rounded px-2 py-0.5 text-xs ring-1 transition-colors focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:outline-none',
{
variants: {
variant: {
diff --git a/apps/web/src/environment/configs/projects.config.ts b/apps/web/src/environment/configs/projects.config.ts
index a17e5772..ad923f28 100644
--- a/apps/web/src/environment/configs/projects.config.ts
+++ b/apps/web/src/environment/configs/projects.config.ts
@@ -8,7 +8,8 @@ export const projectsConfig: {
{
id: 'abstand',
name: 'Abstand',
- description: 'Desktop app for building intentional distance from distractions',
+ description:
+ 'macOS app to schedule focus sessions with automatic app blocking and forced breaks',
logo: { type: 'image', src: '/illustrations/logos/abstand.png' },
startedAt: { year: 2026, month: 4 },
status: 'in-progress',
@@ -22,7 +23,7 @@ export const projectsConfig: {
{
id: 'kairos',
name: 'Kairos',
- description: 'iOS timer app that picks a random finish within a set range',
+ description: 'iOS timer app that picks a random end time within a set range',
logo: { type: 'image', src: '/illustrations/logos/kairos.png' },
startedAt: { year: 2026, month: 3 },
status: 'maintenance',
@@ -59,7 +60,7 @@ export const projectsConfig: {
{
id: 'derive',
name: 'Derive',
- description: 'iPhone app: pick a color, find 9 things, fill a 3×3 photo grid',
+ description: 'iPhone app to pick a color, find 9 matching things, and fill a 3×3 photo grid',
logo: { type: 'image', src: '/illustrations/logos/derive.png' },
startedAt: { year: 2026, month: 1 },
status: 'maintenance',
@@ -92,7 +93,7 @@ export const projectsConfig: {
{
id: 'learnlinesfaster',
name: 'LearnLinesFaster',
- description: 'Line-learning tool using the first-letter method',
+ description: 'Web app for learning scripts using the first-letter mnemonic method',
logo: { type: 'emoji', value: '✨' },
startedAt: { year: 2025, month: 10 },
status: 'maintenance',
@@ -147,7 +148,8 @@ export const projectsConfig: {
{
id: 'gazegames',
name: 'GazeGames',
- description: 'Generates image sets where person gazes at a moving target',
+ description:
+ 'Web tool that generates a set of images where eyes and objects gaze at a moving target',
logo: { type: 'emoji', value: '👀' },
startedAt: { year: 2025, month: 11 },
endedAt: { year: 2025, month: 11 },
@@ -192,7 +194,7 @@ export const projectsConfig: {
{
id: 'tasu',
name: 'Tasu',
- description: 'Video-first customer support tool',
+ description: 'Open-source, video-first customer support tool',
logo: { type: 'image', src: '/illustrations/logos/tasu.png' },
startedAt: { year: 2025, month: 8 },
endedAt: { year: 2025, month: 10 },
@@ -203,7 +205,7 @@ export const projectsConfig: {
{
id: 'actorpal',
name: 'ActorPal',
- description: 'Tools for actor line learning, scene practice, and auditions',
+ description: 'Suite of tools for actor line learning, scene practice, and mock auditions',
logo: { type: 'emoji', value: '✨' },
startedAt: { year: 2025, month: 10 },
endedAt: { year: 2025, month: 10 },
@@ -214,7 +216,7 @@ export const projectsConfig: {
{
id: 'chatsnap',
name: 'ChatSnap',
- description: 'Automated channel generating chat-conversation videos',
+ description: 'Automated YouTube channel posting AI-generated chat conversation videos',
logo: { type: 'image', src: '/illustrations/logos/chatsnap.png' },
startedAt: { year: 2024, month: 9 },
endedAt: { year: 2024, month: 12 },
@@ -225,7 +227,7 @@ export const projectsConfig: {
{
id: 'midimarble',
name: 'MidiMarble',
- description: 'Automated channel generating marble music videos',
+ description: 'Automated YouTube channel posting AI-generated marble music videos',
logo: { type: 'image', src: '/illustrations/logos/midimarble.png' },
startedAt: { year: 2024, month: 9 },
endedAt: { year: 2024, month: 12 },
@@ -236,7 +238,7 @@ export const projectsConfig: {
{
id: 'eu-blocks',
name: 'EU Blocks',
- description: 'Shopify app with GDPR and EU energy label compliant UI blocks',
+ description: 'Shopify app with pre-built GDPR and EU energy label compliant UI blocks',
logo: { type: 'image', src: '/illustrations/logos/eu-blocks.png' },
startedAt: { year: 2024, month: 7 },
endedAt: { year: 2024, month: 9 },
@@ -270,7 +272,7 @@ export const projectsConfig: {
{
id: 'ecsify',
name: 'ecsify',
- description: 'Typesafe Entity Component System (ECS) library for TypeScript',
+ description: 'Typesafe ECS library for building entity-component apps in TypeScript',
startedAt: { year: 2024 },
status: 'maintenance',
category: 'package',
@@ -279,10 +281,23 @@ export const projectsConfig: {
{ type: 'github', url: `${appConfig.social.githubPackages}/ecsify` }
]
},
+ {
+ id: 'feature-core',
+ name: 'feature-core',
+ description:
+ 'Typesafe .with() composition primitives for building extensible TypeScript libraries',
+ startedAt: { year: 2026 },
+ status: 'maintenance',
+ category: 'package',
+ tags: [
+ { type: 'npm', url: 'https://www.npmjs.com/package/feature-core' },
+ { type: 'github', url: `${appConfig.social.githubPackages}/feature-core` }
+ ]
+ },
{
id: 'feature-fetch',
name: 'feature-fetch',
- description: 'Typesafe fetch wrapper with OpenAPI type support',
+ description: 'Typesafe fetch wrapper with composable middleware and OpenAPI type support',
startedAt: { year: 2024 },
status: 'maintenance',
category: 'package',
@@ -294,7 +309,8 @@ export const projectsConfig: {
{
id: 'feature-form',
name: 'feature-form',
- description: 'Straightforward, typesafe, and feature-based form library',
+ description:
+ 'Lightweight typesafe form library with reactive fields and Standard Schema validation',
startedAt: { year: 2024 },
status: 'maintenance',
category: 'package',
@@ -306,7 +322,8 @@ export const projectsConfig: {
{
id: 'feature-logger',
name: 'feature-logger',
- description: 'Straightforward, typesafe, and feature-based logging library',
+ description:
+ 'Typesafe logger with composable prefixes, labels, timestamps, and output styles',
startedAt: { year: 2024 },
status: 'maintenance',
category: 'package',
@@ -318,7 +335,7 @@ export const projectsConfig: {
{
id: 'feature-react',
name: 'feature-react',
- description: 'React hooks and components for feature-state and feature-form',
+ description: 'React hooks for reactive state and form subscriptions',
startedAt: { year: 2024 },
status: 'maintenance',
category: 'package',
@@ -330,7 +347,7 @@ export const projectsConfig: {
{
id: 'feature-state',
name: 'feature-state',
- description: 'Typesafe, feature-based state management library for React',
+ description: 'Lightweight reactive state container with composable feature extensions',
startedAt: { year: 2024 },
status: 'maintenance',
category: 'package',
@@ -342,7 +359,7 @@ export const projectsConfig: {
{
id: 'openapi-ts-router',
name: 'openapi-ts-router',
- description: 'Typesafe OpenAPI router wrapper for Express and Hono',
+ description: 'OpenAPI-typed router helpers for Express and Hono with runtime validation',
startedAt: { year: 2024 },
status: 'maintenance',
category: 'package',
@@ -354,7 +371,7 @@ export const projectsConfig: {
{
id: 'xml-tokenizer',
name: 'xml-tokenizer',
- description: 'Typesafe XML tokenizer with callback-based token streaming',
+ description: 'Streaming XML tokenizer with callback-based parsing and object helpers',
startedAt: { year: 2024 },
status: 'maintenance',
category: 'package',
@@ -366,7 +383,7 @@ export const projectsConfig: {
{
id: 'tuple-result',
name: 'tuple-result',
- description: 'Minimal, tree-shakable Result type library for TypeScript',
+ description: 'Minimal, tree-shakable Result type using plain arrays for easy serialization',
startedAt: { year: 2024 },
status: 'maintenance',
category: 'package',
@@ -378,7 +395,7 @@ export const projectsConfig: {
{
id: 'validatenv',
name: 'validatenv',
- description: 'Typesafe env variable validation with Zod, Valibot, and Yup',
+ description: 'Environment variable validation using schemas such as Zod, Valibot, and Yup',
startedAt: { year: 2024 },
status: 'maintenance',
category: 'package',
@@ -390,9 +407,9 @@ export const projectsConfig: {
{
id: 'validation-adapter',
name: 'validation-adapter',
- description: 'Universal adapter interface for Zod, Valibot, and Yup',
+ description: 'Universal validation abstraction for validators like Zod, Valibot, and Yup',
startedAt: { year: 2024 },
- status: 'maintenance',
+ status: 'discontinued',
category: 'package',
tags: [
{ type: 'npm', url: 'https://www.npmjs.com/package/validation-adapter' },
@@ -402,9 +419,10 @@ export const projectsConfig: {
{
id: 'validation-adapters',
name: 'validation-adapters',
- description: 'Pre-built adapters for validation-adapter: Zod and Valibot',
+ description:
+ 'Ready-made validation-adapter implementations for Zod, Valibot, Yup, and Standard Schema',
startedAt: { year: 2024 },
- status: 'maintenance',
+ status: 'discontinued',
category: 'package',
tags: [
{ type: 'npm', url: 'https://www.npmjs.com/package/validation-adapters' },
@@ -414,7 +432,7 @@ export const projectsConfig: {
{
id: 'head-metadata',
name: 'head-metadata',
- description: 'Typesafe utility to extract metadata from an HTML ',
+ description: 'HTML head metadata extractor for title, meta, and link tags',
startedAt: { year: 2024 },
status: 'maintenance',
category: 'package',
@@ -426,7 +444,7 @@ export const projectsConfig: {
{
id: 'mado',
name: 'mado',
- description: 'A macOS-focused Rust library for active window monitoring and app metadata',
+ description: 'Rust library for macOS active window monitoring and app metadata',
startedAt: { year: 2026, month: 1 },
status: 'maintenance',
category: 'package',
@@ -441,7 +459,7 @@ export const projectsConfig: {
{
id: 'utils',
name: '@blgc/utils',
- description: 'Typesafe, tree-shakable collection of utility functions',
+ description: 'Tree-shakable TypeScript utilities for colors, IDs, objects, URLs, and math',
startedAt: { year: 2024 },
status: 'maintenance',
category: 'package',
@@ -453,7 +471,7 @@ export const projectsConfig: {
{
id: 'types',
name: '@blgc/types',
- description: 'Shared TypeScript types across builder.group packages',
+ description: 'Shared utility, API, and OpenAPI TypeScript types for builder.group packages',
startedAt: { year: 2024 },
status: 'maintenance',
category: 'package',
@@ -465,9 +483,9 @@ export const projectsConfig: {
{
id: 'eprel-client',
name: 'eprel-client',
- description: 'Typesafe fetch client for the EU EPREL energy label API',
+ description: 'Typesafe API client for the EU EPREL energy label registry',
startedAt: { year: 2024 },
- status: 'maintenance',
+ status: 'discontinued',
category: 'package',
tags: [
{ type: 'npm', url: 'https://www.npmjs.com/package/eprel-client' },
@@ -477,7 +495,7 @@ export const projectsConfig: {
{
id: 'config',
name: '@blgc/config',
- description: 'Collection of ESLint, Vite, and TypeScript configurations',
+ description: 'Shared ESLint, Prettier, Vite, and TypeScript configuration presets',
startedAt: { year: 2024 },
status: 'maintenance',
category: 'package',
@@ -489,7 +507,7 @@ export const projectsConfig: {
{
id: 'rollup-presets',
name: 'rollup-presets',
- description: 'Opinionated, production-ready Rollup configuration presets',
+ description: 'Rollup presets and plugins for building TypeScript libraries',
startedAt: { year: 2024 },
status: 'maintenance',
category: 'package',
@@ -502,14 +520,113 @@ export const projectsConfig: {
id: 'split-flap-board',
name: 'split-flap-board',
description:
- 'Web component that simulates a split-flap display inspired by airport and train station boards',
+ 'Web Components for animated split-flap boards with configurable spools and grids',
startedAt: { year: 2026 },
- status: 'maintenance',
+ status: 'discontinued',
category: 'package',
tags: [
{ type: 'npm', url: 'https://www.npmjs.com/package/split-flap-board' },
{ type: 'github', url: `${appConfig.social.githubPackages}/split-flap-board` }
]
+ },
+ {
+ id: 'figma-connect',
+ name: 'figma-connect',
+ description: 'Typed message bridge between Figma plugin UI iframes and sandbox code',
+ startedAt: { year: 2024 },
+ status: 'discontinued',
+ category: 'package',
+ tags: [
+ { type: 'npm', url: 'https://www.npmjs.com/package/figma-connect' },
+ { type: 'github', url: `${appConfig.social.githubPackages}/_deprecated/figma-connect` }
+ ]
+ },
+ {
+ id: 'google-webfonts-client',
+ name: 'google-webfonts-client',
+ description: 'Typesafe API client for Google Web Fonts metadata and font downloads',
+ startedAt: { year: 2024 },
+ status: 'discontinued',
+ category: 'package',
+ tags: [
+ { type: 'npm', url: 'https://www.npmjs.com/package/google-webfonts-client' },
+ {
+ type: 'github',
+ url: `${appConfig.social.githubPackages}/_deprecated/google-webfonts-client`
+ }
+ ]
+ },
+ {
+ id: 'elevenlabs-client',
+ name: 'elevenlabs-client',
+ description: 'Typesafe API client for the ElevenLabs text-to-speech API',
+ startedAt: { year: 2024 },
+ status: 'discontinued',
+ category: 'package',
+ tags: [
+ { type: 'npm', url: 'https://www.npmjs.com/package/elevenlabs-client' },
+ { type: 'github', url: `${appConfig.social.githubPackages}/_deprecated/elevenlabs-client` }
+ ]
+ },
+ {
+ id: 'kleinanzeigen-client',
+ name: 'kleinanzeigen-client',
+ description: 'Typesafe API client for scraping and extracting Kleinanzeigen listings',
+ startedAt: { year: 2025 },
+ status: 'discontinued',
+ category: 'package',
+ tags: [
+ { type: 'npm', url: 'https://www.npmjs.com/package/kleinanzeigen-client' },
+ {
+ type: 'github',
+ url: `${appConfig.social.githubPackages}/_deprecated/kleinanzeigen-client`
+ }
+ ]
+ },
+ {
+ id: 'cli',
+ name: '@blgc/cli',
+ description: 'Rollup and Esbuild-powered CLI for bundling TypeScript libraries',
+ startedAt: { year: 2024 },
+ status: 'discontinued',
+ category: 'package',
+ tags: [
+ { type: 'npm', url: 'https://www.npmjs.com/package/@blgc/cli' },
+ { type: 'github', url: `${appConfig.social.githubPackages}/_deprecated/cli` }
+ ]
+ },
+ {
+ id: 'widget-grid',
+ name: 'widget-grid',
+ description: 'Framework-agnostic widget grid model with cell-based layouts and widget data',
+ startedAt: { year: 2024 },
+ status: 'discontinued',
+ category: 'package',
+ tags: [{ type: 'npm', url: 'https://www.npmjs.com/package/widget-grid' }]
+ },
+ {
+ id: 'openapi-express',
+ name: 'openapi-express',
+ description: 'OpenAPI-typed Express router wrapper with Zod request validation',
+ startedAt: { year: 2024 },
+ status: 'discontinued',
+ category: 'package',
+ tags: [
+ { type: 'npm', url: 'https://www.npmjs.com/package/openapi-express' },
+ { type: 'github', url: `${appConfig.social.githubPackages}/_deprecated/openapi-express` }
+ ]
+ },
+ {
+ id: 'webito',
+ name: 'webito',
+ description: 'ECS-powered web editor experiment for Tailwind-style component editing',
+ startedAt: { year: 2024 },
+ status: 'discontinued',
+ category: 'package',
+ tags: [
+ { type: 'npm', url: 'https://www.npmjs.com/package/webito' },
+ { type: 'github', url: `${appConfig.social.githubPackages}/_deprecated/webito` }
+ ]
}
]
};
diff --git a/apps/web/src/lib/npm-downloads.ts b/apps/web/src/lib/npm-downloads.ts
index b44ee9a0..05d4bfbc 100644
--- a/apps/web/src/lib/npm-downloads.ts
+++ b/apps/web/src/lib/npm-downloads.ts
@@ -19,12 +19,12 @@ export async function getNpmTotalDownloads(
const results = await Promise.allSettled(
packageNames.map(async (name) => {
- const [ok, , value] = await fetchClient.get(
+ const [ok, , data] = await fetchClient.get(
'https://api.npmjs.org/downloads/point/{range}/{name}',
{ pathParams: { range, name } }
);
if (ok) {
- return { name, data: value.data };
+ return { name, data };
}
return { name, data: null };
})
diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx
index ceeec938..8c2f5c13 100644
--- a/apps/web/src/routes/index.tsx
+++ b/apps/web/src/routes/index.tsx
@@ -57,6 +57,8 @@ function RouteComponent() {
const packages = projectsConfig.projects
.filter((p) => p.category === 'package')
.sort((a, b) => (npmDownloads[b.name] ?? 0) - (npmDownloads[a.name] ?? 0));
+ const activePackages = packages.filter((p) => p.status !== 'discontinued');
+ const discontinuedPackages = packages.filter((p) => p.status === 'discontinued');
return (
@@ -137,9 +139,21 @@ function RouteComponent() {
- {packages.map((pkg) => (
+ {activePackages.map((pkg) => (
))}
+ {discontinuedPackages.length > 0 && (
+ <>
+
+
+ Discontinued libraries, kept available for existing users
+
+
+ {discontinuedPackages.map((pkg) => (
+
+ ))}
+ >
+ )}
diff --git a/crates/mado/README.md b/crates/mado/README.md
index 9578db85..06ea3746 100644
--- a/crates/mado/README.md
+++ b/crates/mado/README.md
@@ -117,13 +117,13 @@ monitor.run()?;
**Config options:**
-| Option | Default | Description |
-| ---------------------- | ------- | -------------------------------------------------------------------- |
-| `include_app_icon` | `false` | Extract app icon as base64 PNG |
+| Option | Default | Description |
+| ---------------------- | ------- | --------------------------------------------------------------------- |
+| `include_app_icon` | `false` | Extract app icon as base64 PNG |
| `include_app_color` | `false` | Also derive the dominant app color when `include_app_icon` is enabled |
-| `include_browser_info` | `false` | Extract browser URL and private mode |
-| `include_website_info` | `false` | Extract domain, fetch favicon, and extract color (~50-500ms, cached) |
-| `track_window_changes` | `true` | Track window focus/title changes (requires Accessibility permission) |
+| `include_browser_info` | `false` | Extract browser URL and private mode |
+| `include_website_info` | `false` | Extract domain, fetch favicon, and extract color (~50-500ms, cached) |
+| `track_window_changes` | `true` | Track window focus/title changes (requires Accessibility permission) |
### Stop monitoring
@@ -175,11 +175,11 @@ if let Some(color) = mado::get_app_color("com.apple.finder") {
**Config options:**
-| Option | Default | Description |
-| -------------- | ------- | ---------------------------------------------- |
-| `include_icon` | `false` | Extract icons as base64 PNG |
+| Option | Default | Description |
+| ------------------- | ------- | ----------------------------------------------------------------- |
+| `include_icon` | `false` | Extract icons as base64 PNG |
| `include_app_color` | `false` | Also derive the dominant app color when `include_icon` is enabled |
-| `icon_size` | `32` | Icon size in pixels |
+| `icon_size` | `32` | Icon size in pixels |
## 📐 Architecture
diff --git a/examples/feature-fetch/vanilla/open-meteo/.gitignore b/examples/feature-fetch/vanilla/basic/.gitignore
similarity index 100%
rename from examples/feature-fetch/vanilla/open-meteo/.gitignore
rename to examples/feature-fetch/vanilla/basic/.gitignore
diff --git a/examples/feature-fetch/vanilla/basic/README.md b/examples/feature-fetch/vanilla/basic/README.md
new file mode 100644
index 00000000..f4efbeeb
--- /dev/null
+++ b/examples/feature-fetch/vanilla/basic/README.md
@@ -0,0 +1,32 @@
+# feature-fetch Vanilla Basic
+
+Vanilla TypeScript example for `feature-fetch`. It shows REST helpers, OpenAPI-generated types, and GraphQL documents using the same tuple-result client model.
+
+- REST calls with `createApiFetchClient`
+- OpenAPI-typed calls with `createOpenApiFetchClient`
+- GraphQL calls with `createGraphQLFetchClient`
+- tuple-result success and error branches
+- generated OpenAPI and GraphQL types
+- real requests logged to the browser console
+
+## Run
+
+```sh
+pnpm dev
+```
+
+## Refresh Generated Types
+
+Generated OpenAPI and GraphQL types are committed. Refresh them when upstream schemas change:
+
+```sh
+pnpm openapi:generate
+pnpm graphql:generate
+```
+
+Use `pnpm graphql:schema` first to refresh the committed Countries GraphQL schema.
+
+## API Sources
+
+- Open-Meteo OpenAPI schema: [openapi.yml](https://github.com/open-meteo/open-meteo/blob/main/openapi.yml)
+- Countries GraphQL API: [countries.trevorblades.com/graphql](https://countries.trevorblades.com/graphql)
diff --git a/examples/feature-fetch/vanilla/basic/index.html b/examples/feature-fetch/vanilla/basic/index.html
new file mode 100644
index 00000000..ce07746f
--- /dev/null
+++ b/examples/feature-fetch/vanilla/basic/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ feature-fetch basic
+
+
+
+
feature-fetch Vanilla basic
+
Open the browser console to see the REST, OpenAPI, and GraphQL requests.
+
+
+
+
diff --git a/examples/feature-fetch/vanilla/basic/package.json b/examples/feature-fetch/vanilla/basic/package.json
new file mode 100644
index 00000000..ac770605
--- /dev/null
+++ b/examples/feature-fetch/vanilla/basic/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "feature-fetch-vanilla-basic",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "tsc && vite build",
+ "dev": "vite",
+ "graphql:generate": "gql.tada generate-output -c ./tsconfig.json -o ./src/graphql/gen/countries-env.d.ts",
+ "graphql:schema": "gql.tada generate-schema https://countries.trevorblades.com/graphql -o ./resources/graphql/countries.graphql",
+ "openapi:generate": "openapi-typescript ./resources/openapi/open-meteo-v1.yaml -o ./src/openapi/gen/open-meteo-v1.ts",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "feature-fetch": "workspace:*",
+ "gql.tada": "^1.9.2"
+ },
+ "devDependencies": {
+ "openapi-typescript": "^7.10.1",
+ "typescript": "~6.0.2",
+ "vite": "^8.0.12"
+ }
+}
diff --git a/examples/feature-fetch/vanilla/basic/public/favicon.svg b/examples/feature-fetch/vanilla/basic/public/favicon.svg
new file mode 100644
index 00000000..6893eb13
--- /dev/null
+++ b/examples/feature-fetch/vanilla/basic/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/feature-fetch/vanilla/basic/resources/graphql/countries.graphql b/examples/feature-fetch/vanilla/basic/resources/graphql/countries.graphql
new file mode 100644
index 00000000..6f64a3fb
--- /dev/null
+++ b/examples/feature-fetch/vanilla/basic/resources/graphql/countries.graphql
@@ -0,0 +1,75 @@
+type Continent {
+ code: ID!
+ countries: [Country!]!
+ name: String!
+}
+
+input ContinentFilterInput {
+ code: StringQueryOperatorInput
+}
+
+type Country {
+ awsRegion: String!
+ capital: String
+ code: ID!
+ continent: Continent!
+ currencies: [String!]!
+ currency: String
+ emoji: String!
+ emojiU: String!
+ languages: [Language!]!
+ name(lang: String): String!
+ native: String!
+ phone: String!
+ phones: [String!]!
+ states: [State!]!
+ subdivisions: [Subdivision!]!
+}
+
+input CountryFilterInput {
+ code: StringQueryOperatorInput
+ continent: StringQueryOperatorInput
+ currency: StringQueryOperatorInput
+ name: StringQueryOperatorInput
+}
+
+type Language {
+ code: ID!
+ countries: [Country!]!
+ name: String!
+ native: String!
+ rtl: Boolean!
+}
+
+input LanguageFilterInput {
+ code: StringQueryOperatorInput
+}
+
+type Query {
+ continent(code: ID!): Continent
+ continents(filter: ContinentFilterInput = {}): [Continent!]!
+ countries(filter: CountryFilterInput = {}): [Country!]!
+ country(code: ID!): Country
+ language(code: ID!): Language
+ languages(filter: LanguageFilterInput = {}): [Language!]!
+}
+
+type State {
+ code: String
+ country: Country!
+ name: String!
+}
+
+input StringQueryOperatorInput {
+ eq: String
+ in: [String!]
+ ne: String
+ nin: [String!]
+ regex: String
+}
+
+type Subdivision {
+ code: ID!
+ emoji: String
+ name: String!
+}
\ No newline at end of file
diff --git a/examples/feature-fetch/vanilla/open-meteo/resources/openapi-v1.yaml b/examples/feature-fetch/vanilla/basic/resources/openapi/open-meteo-v1.yaml
similarity index 82%
rename from examples/feature-fetch/vanilla/open-meteo/resources/openapi-v1.yaml
rename to examples/feature-fetch/vanilla/basic/resources/openapi/open-meteo-v1.yaml
index 95f14be4..cb826bd5 100644
--- a/examples/feature-fetch/vanilla/open-meteo/resources/openapi-v1.yaml
+++ b/examples/feature-fetch/vanilla/basic/resources/openapi/open-meteo-v1.yaml
@@ -95,14 +95,14 @@ paths:
description: "WGS84 coordinate"
schema:
type: number
- format: float
+ format: double
- name: longitude
in: query
required: true
description: "WGS84 coordinate"
schema:
type: number
- format: float
+ format: double
- name: current_weather
in: query
schema:
@@ -148,7 +148,7 @@ paths:
- 1
- 2
responses:
- 200:
+ "200":
description: OK
content:
application/json:
@@ -170,31 +170,28 @@ paths:
generationtime_ms:
type: number
example: 2.2119
- description: Generation time of the weather forecast in milli seconds. This is mainly used for performance monitoring and improvements.
+ description: Generation time of the weather forecast in milliseconds. This is mainly used for performance monitoring and improvements.
utc_offset_seconds:
type: integer
example: 3600
description: Applied timezone offset from the &timezone= parameter.
hourly:
- type: HourlyResponse
- description: For each selected weather variable, data will be returned as a floating point array. Additionally a `time` array will be returned with ISO8601 timestamps.
+ $ref: "#/components/schemas/HourlyResponse"
hourly_units:
type: object
additionalProperties:
type: string
description: For each selected weather variable, the unit will be listed here.
daily:
- type: DailyResponse
- description: For each selected daily weather variable, data will be returned as a floating point array. Additionally a `time` array will be returned with ISO8601 timestamps.
+ $ref: "#/components/schemas/DailyResponse"
daily_units:
type: object
additionalProperties:
type: string
description: For each selected daily weather variable, the unit will be listed here.
current_weather:
- type: CurrentWeather
- description: "Current weather conditions with the attributes: time, temperature, wind_speed, wind_direction and weather_code"
- 400:
+ $ref: "#/components/schemas/CurrentWeather"
+ "400":
description: Bad Request
content:
application/json:
@@ -212,6 +209,7 @@ components:
schemas:
HourlyResponse:
type: object
+ description: For each selected weather variable, data will be returned as a floating point array. Additionally a `time` array will be returned with ISO8601 timestamps.
required:
- time
properties:
@@ -222,153 +220,154 @@ components:
temperature_2m:
type: array
items:
- type: float
+ type: number
relative_humidity_2m:
type: array
items:
- type: float
+ type: number
dew_point_2m:
type: array
items:
- type: float
+ type: number
apparent_temperature:
type: array
items:
- type: float
+ type: number
pressure_msl:
type: array
items:
- type: float
+ type: number
cloud_cover:
type: array
items:
- type: float
+ type: number
cloud_cover_low:
type: array
items:
- type: float
+ type: number
cloud_cover_mid:
type: array
items:
- type: float
+ type: number
cloud_cover_high:
type: array
items:
- type: float
+ type: number
wind_speed_10m:
type: array
items:
- type: float
+ type: number
wind_speed_80m:
type: array
items:
- type: float
+ type: number
wind_speed_120m:
type: array
items:
- type: float
+ type: number
wind_speed_180m:
type: array
items:
- type: float
+ type: number
wind_direction_10m:
type: array
items:
- type: float
+ type: number
wind_direction_80m:
type: array
items:
- type: float
+ type: number
wind_direction_120m:
type: array
items:
- type: float
+ type: number
wind_direction_180m:
type: array
items:
- type: float
+ type: number
wind_gusts_10m:
type: array
items:
- type: float
+ type: number
shortwave_radiation:
type: array
items:
- type: float
+ type: number
direct_radiation:
type: array
items:
- type: float
+ type: number
direct_normal_irradiance:
type: array
items:
- type: float
+ type: number
diffuse_radiation:
type: array
items:
- type: float
+ type: number
vapour_pressure_deficit:
type: array
items:
- type: float
+ type: number
evapotranspiration:
type: array
items:
- type: float
+ type: number
precipitation:
type: array
items:
- type: float
+ type: number
weather_code:
type: array
items:
- type: float
+ type: number
snow_height:
type: array
items:
- type: float
+ type: number
freezing_level_height:
type: array
items:
- type: float
+ type: number
soil_temperature_0cm:
type: array
items:
- type: float
+ type: number
soil_temperature_6cm:
type: array
items:
- type: float
+ type: number
soil_temperature_18cm:
type: array
items:
- type: float
+ type: number
soil_temperature_54cm:
type: array
items:
- type: float
+ type: number
soil_moisture_0_1cm:
type: array
items:
- type: float
+ type: number
soil_moisture_1_3cm:
type: array
items:
- type: float
+ type: number
soil_moisture_3_9cm:
type: array
items:
- type: float
+ type: number
soil_moisture_9_27cm:
type: array
items:
- type: float
+ type: number
soil_moisture_27_81cm:
type: array
items:
- type: float
+ type: number
DailyResponse:
type: object
+ description: For each selected daily weather variable, data will be returned as a floating point array. Additionally a `time` array will be returned with ISO8601 timestamps.
properties:
time:
type: array
@@ -377,82 +376,83 @@ components:
temperature_2m_max:
type: array
items:
- type: float
+ type: number
temperature_2m_min:
type: array
items:
- type: float
+ type: number
apparent_temperature_max:
type: array
items:
- type: float
+ type: number
apparent_temperature_min:
type: array
items:
- type: float
+ type: number
precipitation_sum:
type: array
items:
- type: float
+ type: number
precipitation_hours:
type: array
items:
- type: float
+ type: number
weather_code:
type: array
items:
- type: float
+ type: number
sunrise:
type: array
items:
- type: float
+ type: number
sunset:
type: array
items:
- type: float
+ type: number
wind_speed_10m_max:
type: array
items:
- type: float
+ type: number
wind_gusts_10m_max:
type: array
items:
- type: float
+ type: number
wind_direction_10m_dominant:
type: array
items:
- type: float
+ type: number
shortwave_radiation_sum:
type: array
items:
- type: float
+ type: number
uv_index_max:
type: array
items:
- type: float
+ type: number
uv_index_clear_sky_max:
type: array
items:
- type: float
+ type: number
et0_fao_evapotranspiration:
type: array
items:
- type: float
+ type: number
required:
- time
CurrentWeather:
type: object
+ description: "Current weather conditions with the attributes: time, temperature, wind_speed, wind_direction and weather_code"
properties:
time:
type: string
temperature:
- type: float
- wind_speed:
- type: float
- wind_direction:
- type: float
+ type: number
+ windspeed:
+ type: number
+ winddirection:
+ type: number
weather_code:
- type: int
+ type: integer
required:
- time
- temperature
diff --git a/examples/feature-fetch/vanilla/basic/src/api/index.ts b/examples/feature-fetch/vanilla/basic/src/api/index.ts
new file mode 100644
index 00000000..a1d903b1
--- /dev/null
+++ b/examples/feature-fetch/vanilla/basic/src/api/index.ts
@@ -0,0 +1 @@
+export * from './open-meteo';
diff --git a/examples/feature-fetch/vanilla/basic/src/api/open-meteo.ts b/examples/feature-fetch/vanilla/basic/src/api/open-meteo.ts
new file mode 100644
index 00000000..aa6aba9a
--- /dev/null
+++ b/examples/feature-fetch/vanilla/basic/src/api/open-meteo.ts
@@ -0,0 +1,43 @@
+import { createApiFetchClient } from 'feature-fetch';
+
+const apiFetchClient = createApiFetchClient({
+ baseUrl: 'https://api.open-meteo.com'
+});
+
+export async function fetchWeatherWithApiFetchClient(
+ latitude: number,
+ longitude: number
+): Promise {
+ const [isOk, error, forecast] = await apiFetchClient.get(
+ '/v1/forecast',
+ {
+ queryParams: {
+ latitude,
+ longitude,
+ current_weather: true
+ }
+ }
+ );
+
+ if (!isOk) {
+ console.error('[api] Error Result', { error });
+ return;
+ }
+
+ console.log('[api] Ok Result', { forecast });
+}
+
+export interface TForecastResponse {
+ current_weather?: {
+ time: string;
+ temperature: number;
+ windspeed: number;
+ winddirection: number;
+ weathercode: number;
+ };
+}
+
+export interface TForecastError {
+ error?: boolean;
+ reason?: string;
+}
diff --git a/examples/feature-fetch/vanilla/basic/src/graphql/countries.ts b/examples/feature-fetch/vanilla/basic/src/graphql/countries.ts
new file mode 100644
index 00000000..5ed34b8f
--- /dev/null
+++ b/examples/feature-fetch/vanilla/basic/src/graphql/countries.ts
@@ -0,0 +1,33 @@
+import { createGraphQLFetchClient } from 'feature-fetch';
+import { gql } from './gql';
+
+const countriesFetchClient = createGraphQLFetchClient({
+ baseUrl: 'https://countries.trevorblades.com/graphql'
+});
+
+const COUNTRY_QUERY = gql(`
+ query Country($code: ID!) {
+ country(code: $code) {
+ code
+ name
+ emoji
+ capital
+ currency
+ }
+ }
+`);
+
+export async function fetchCountryWithGraphQLFetchClient(code: string): Promise {
+ const [isOk, error, countryData] = await countriesFetchClient.query(COUNTRY_QUERY, {
+ variables: {
+ code
+ }
+ });
+
+ if (!isOk) {
+ console.error('[graphql] Error Result', { error });
+ return;
+ }
+
+ console.log('[graphql] Ok Result', { country: countryData.country });
+}
diff --git a/examples/feature-fetch/vanilla/basic/src/graphql/gen/countries-env.d.ts b/examples/feature-fetch/vanilla/basic/src/graphql/gen/countries-env.d.ts
new file mode 100644
index 00000000..6d0a64b0
--- /dev/null
+++ b/examples/feature-fetch/vanilla/basic/src/graphql/gen/countries-env.d.ts
@@ -0,0 +1,42 @@
+/* eslint-disable */
+/* prettier-ignore */
+
+export type introspection_types = {
+ 'Boolean': unknown;
+ 'Continent': { kind: 'OBJECT'; name: 'Continent'; fields: { 'code': { name: 'code'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'countries': { name: 'countries'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Country'; ofType: null; }; }; }; } }; 'name': { name: 'name'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; }; };
+ 'ContinentFilterInput': { kind: 'INPUT_OBJECT'; name: 'ContinentFilterInput'; isOneOf: false; inputFields: [{ name: 'code'; type: { kind: 'INPUT_OBJECT'; name: 'StringQueryOperatorInput'; ofType: null; }; defaultValue: null }]; };
+ 'Country': { kind: 'OBJECT'; name: 'Country'; fields: { 'awsRegion': { name: 'awsRegion'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'capital': { name: 'capital'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'code': { name: 'code'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'continent': { name: 'continent'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Continent'; ofType: null; }; } }; 'currencies': { name: 'currencies'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; }; } }; 'currency': { name: 'currency'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'emoji': { name: 'emoji'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'emojiU': { name: 'emojiU'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'languages': { name: 'languages'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Language'; ofType: null; }; }; }; } }; 'name': { name: 'name'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'native': { name: 'native'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'phone': { name: 'phone'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'phones': { name: 'phones'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; }; } }; 'states': { name: 'states'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'State'; ofType: null; }; }; }; } }; 'subdivisions': { name: 'subdivisions'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Subdivision'; ofType: null; }; }; }; } }; }; };
+ 'CountryFilterInput': { kind: 'INPUT_OBJECT'; name: 'CountryFilterInput'; isOneOf: false; inputFields: [{ name: 'code'; type: { kind: 'INPUT_OBJECT'; name: 'StringQueryOperatorInput'; ofType: null; }; defaultValue: null }, { name: 'continent'; type: { kind: 'INPUT_OBJECT'; name: 'StringQueryOperatorInput'; ofType: null; }; defaultValue: null }, { name: 'currency'; type: { kind: 'INPUT_OBJECT'; name: 'StringQueryOperatorInput'; ofType: null; }; defaultValue: null }, { name: 'name'; type: { kind: 'INPUT_OBJECT'; name: 'StringQueryOperatorInput'; ofType: null; }; defaultValue: null }]; };
+ 'ID': unknown;
+ 'Language': { kind: 'OBJECT'; name: 'Language'; fields: { 'code': { name: 'code'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'countries': { name: 'countries'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Country'; ofType: null; }; }; }; } }; 'name': { name: 'name'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'native': { name: 'native'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; 'rtl': { name: 'rtl'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'Boolean'; ofType: null; }; } }; }; };
+ 'LanguageFilterInput': { kind: 'INPUT_OBJECT'; name: 'LanguageFilterInput'; isOneOf: false; inputFields: [{ name: 'code'; type: { kind: 'INPUT_OBJECT'; name: 'StringQueryOperatorInput'; ofType: null; }; defaultValue: null }]; };
+ 'Query': { kind: 'OBJECT'; name: 'Query'; fields: { 'continent': { name: 'continent'; type: { kind: 'OBJECT'; name: 'Continent'; ofType: null; } }; 'continents': { name: 'continents'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Continent'; ofType: null; }; }; }; } }; 'countries': { name: 'countries'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Country'; ofType: null; }; }; }; } }; 'country': { name: 'country'; type: { kind: 'OBJECT'; name: 'Country'; ofType: null; } }; 'language': { name: 'language'; type: { kind: 'OBJECT'; name: 'Language'; ofType: null; } }; 'languages': { name: 'languages'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Language'; ofType: null; }; }; }; } }; }; };
+ 'State': { kind: 'OBJECT'; name: 'State'; fields: { 'code': { name: 'code'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'country': { name: 'country'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'OBJECT'; name: 'Country'; ofType: null; }; } }; 'name': { name: 'name'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; }; };
+ 'String': unknown;
+ 'StringQueryOperatorInput': { kind: 'INPUT_OBJECT'; name: 'StringQueryOperatorInput'; isOneOf: false; inputFields: [{ name: 'eq'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }, { name: 'in'; type: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; }; defaultValue: null }, { name: 'ne'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }, { name: 'nin'; type: { kind: 'LIST'; name: never; ofType: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; }; }; defaultValue: null }, { name: 'regex'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; }; defaultValue: null }]; };
+ 'Subdivision': { kind: 'OBJECT'; name: 'Subdivision'; fields: { 'code': { name: 'code'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'ID'; ofType: null; }; } }; 'emoji': { name: 'emoji'; type: { kind: 'SCALAR'; name: 'String'; ofType: null; } }; 'name': { name: 'name'; type: { kind: 'NON_NULL'; name: never; ofType: { kind: 'SCALAR'; name: 'String'; ofType: null; }; } }; }; };
+};
+
+/** An IntrospectionQuery representation of your schema.
+ *
+ * @remarks
+ * This is an introspection of your schema saved as a file by GraphQLSP.
+ * It will automatically be used by `gql.tada` to infer the types of your GraphQL documents.
+ * If you need to reuse this data or update your `scalars`, update `tadaOutputLocation` to
+ * instead save to a .ts instead of a .d.ts file.
+ */
+export type introspection = {
+ name: never;
+ query: 'Query';
+ mutation: never;
+ subscription: never;
+ types: introspection_types;
+};
+
+import * as gqlTada from 'gql.tada';
+
+declare module 'gql.tada' {
+ interface setupSchema {
+ introspection: introspection
+ }
+}
\ No newline at end of file
diff --git a/examples/feature-fetch/vanilla/basic/src/graphql/gql.ts b/examples/feature-fetch/vanilla/basic/src/graphql/gql.ts
new file mode 100644
index 00000000..213e66d7
--- /dev/null
+++ b/examples/feature-fetch/vanilla/basic/src/graphql/gql.ts
@@ -0,0 +1,9 @@
+import { initGraphQLTada } from 'gql.tada';
+import type { introspection } from './gen/countries-env';
+
+export const gql = initGraphQLTada<{
+ introspection: introspection;
+ scalars: {
+ ID: string;
+ };
+}>();
diff --git a/examples/feature-fetch/vanilla/basic/src/graphql/index.ts b/examples/feature-fetch/vanilla/basic/src/graphql/index.ts
new file mode 100644
index 00000000..8ab54348
--- /dev/null
+++ b/examples/feature-fetch/vanilla/basic/src/graphql/index.ts
@@ -0,0 +1 @@
+export * from './countries';
diff --git a/examples/feature-fetch/vanilla/open-meteo/src/main.ts b/examples/feature-fetch/vanilla/basic/src/main.ts
similarity index 63%
rename from examples/feature-fetch/vanilla/open-meteo/src/main.ts
rename to examples/feature-fetch/vanilla/basic/src/main.ts
index df6b7e3c..4348e64f 100644
--- a/examples/feature-fetch/vanilla/open-meteo/src/main.ts
+++ b/examples/feature-fetch/vanilla/basic/src/main.ts
@@ -1,5 +1,8 @@
import { fetchWeatherWithApiFetchClient } from './api';
+import { fetchCountryWithGraphQLFetchClient } from './graphql';
import { fetchWeatherWithOpenApiFetchClient } from './openapi';
+import './style.css';
await fetchWeatherWithApiFetchClient(52.52, 13.41);
await fetchWeatherWithOpenApiFetchClient(52.52, 13.41);
+await fetchCountryWithGraphQLFetchClient('DE');
diff --git a/examples/feature-fetch/vanilla/basic/src/openapi/gen/open-meteo-v1.ts b/examples/feature-fetch/vanilla/basic/src/openapi/gen/open-meteo-v1.ts
new file mode 100644
index 00000000..0fc825d5
--- /dev/null
+++ b/examples/feature-fetch/vanilla/basic/src/openapi/gen/open-meteo-v1.ts
@@ -0,0 +1,197 @@
+/**
+ * This file was auto-generated by openapi-typescript.
+ * Do not make direct changes to the file.
+ */
+
+export interface paths {
+ "/v1/forecast": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /**
+ * 7 day weather forecast for coordinates
+ * @description 7 day weather variables in hourly and daily resolution for given WGS84 latitude and longitude coordinates. Available worldwide.
+ */
+ get: {
+ parameters: {
+ query: {
+ hourly?: ("temperature_2m" | "relative_humidity_2m" | "dew_point_2m" | "apparent_temperature" | "pressure_msl" | "cloud_cover" | "cloud_cover_low" | "cloud_cover_mid" | "cloud_cover_high" | "wind_speed_10m" | "wind_speed_80m" | "wind_speed_120m" | "wind_speed_180m" | "wind_direction_10m" | "wind_direction_80m" | "wind_direction_120m" | "wind_direction_180m" | "wind_gusts_10m" | "shortwave_radiation" | "direct_radiation" | "direct_normal_irradiance" | "diffuse_radiation" | "vapour_pressure_deficit" | "evapotranspiration" | "precipitation" | "weather_code" | "snow_height" | "freezing_level_height" | "soil_temperature_0cm" | "soil_temperature_6cm" | "soil_temperature_18cm" | "soil_temperature_54cm" | "soil_moisture_0_1cm" | "soil_moisture_1_3cm" | "soil_moisture_3_9cm" | "soil_moisture_9_27cm" | "soil_moisture_27_81cm")[];
+ daily?: ("temperature_2m_max" | "temperature_2m_min" | "apparent_temperature_max" | "apparent_temperature_min" | "precipitation_sum" | "precipitation_hours" | "weather_code" | "sunrise" | "sunset" | "wind_speed_10m_max" | "wind_gusts_10m_max" | "wind_direction_10m_dominant" | "shortwave_radiation_sum" | "uv_index_max" | "uv_index_clear_sky_max" | "et0_fao_evapotranspiration")[];
+ /** @description WGS84 coordinate */
+ latitude: number;
+ /** @description WGS84 coordinate */
+ longitude: number;
+ current_weather?: boolean;
+ temperature_unit?: "celsius" | "fahrenheit";
+ wind_speed_unit?: "kmh" | "ms" | "mph" | "kn";
+ /** @description If format `unixtime` is selected, all time values are returned in UNIX epoch time in seconds. Please not that all time is then in GMT+0! For daily values with unix timestamp, please apply `utc_offset_seconds` again to get the correct date. */
+ timeformat?: "iso8601" | "unixtime";
+ /** @description If `timezone` is set, all timestamps are returned as local-time and data is returned starting at 0:00 local-time. Any time zone name from the [time zone database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) is supported. */
+ timezone?: string;
+ /** @description If `past_days` is set, yesterdays or the day before yesterdays data are also returned. */
+ past_days?: 1 | 2;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description OK */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /**
+ * @description WGS84 of the center of the weather grid-cell which was used to generate this forecast. This coordinate might be up to 5 km away.
+ * @example 52.52
+ */
+ latitude?: number;
+ /**
+ * @description WGS84 of the center of the weather grid-cell which was used to generate this forecast. This coordinate might be up to 5 km away.
+ * @example 13.419.52
+ */
+ longitude?: number;
+ /**
+ * @description The elevation in meters of the selected weather grid-cell. In mountain terrain it might differ from the location you would expect.
+ * @example 44.812
+ */
+ elevation?: number;
+ /**
+ * @description Generation time of the weather forecast in milliseconds. This is mainly used for performance monitoring and improvements.
+ * @example 2.2119
+ */
+ generationtime_ms?: number;
+ /**
+ * @description Applied timezone offset from the &timezone= parameter.
+ * @example 3600
+ */
+ utc_offset_seconds?: number;
+ hourly?: components["schemas"]["HourlyResponse"];
+ /** @description For each selected weather variable, the unit will be listed here. */
+ hourly_units?: {
+ [key: string]: string;
+ };
+ daily?: components["schemas"]["DailyResponse"];
+ /** @description For each selected daily weather variable, the unit will be listed here. */
+ daily_units?: {
+ [key: string]: string;
+ };
+ current_weather?: components["schemas"]["CurrentWeather"];
+ };
+ };
+ };
+ /** @description Bad Request */
+ 400: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @description Always set true for errors */
+ error?: boolean;
+ /**
+ * @description Description of the error
+ * @example Latitude must be in range of -90 to 90°. Given: 300
+ */
+ reason?: string;
+ };
+ };
+ };
+ };
+ };
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+}
+export type webhooks = Record;
+export interface components {
+ schemas: {
+ /** @description For each selected weather variable, data will be returned as a floating point array. Additionally a `time` array will be returned with ISO8601 timestamps. */
+ HourlyResponse: {
+ time: string[];
+ temperature_2m?: number[];
+ relative_humidity_2m?: number[];
+ dew_point_2m?: number[];
+ apparent_temperature?: number[];
+ pressure_msl?: number[];
+ cloud_cover?: number[];
+ cloud_cover_low?: number[];
+ cloud_cover_mid?: number[];
+ cloud_cover_high?: number[];
+ wind_speed_10m?: number[];
+ wind_speed_80m?: number[];
+ wind_speed_120m?: number[];
+ wind_speed_180m?: number[];
+ wind_direction_10m?: number[];
+ wind_direction_80m?: number[];
+ wind_direction_120m?: number[];
+ wind_direction_180m?: number[];
+ wind_gusts_10m?: number[];
+ shortwave_radiation?: number[];
+ direct_radiation?: number[];
+ direct_normal_irradiance?: number[];
+ diffuse_radiation?: number[];
+ vapour_pressure_deficit?: number[];
+ evapotranspiration?: number[];
+ precipitation?: number[];
+ weather_code?: number[];
+ snow_height?: number[];
+ freezing_level_height?: number[];
+ soil_temperature_0cm?: number[];
+ soil_temperature_6cm?: number[];
+ soil_temperature_18cm?: number[];
+ soil_temperature_54cm?: number[];
+ soil_moisture_0_1cm?: number[];
+ soil_moisture_1_3cm?: number[];
+ soil_moisture_3_9cm?: number[];
+ soil_moisture_9_27cm?: number[];
+ soil_moisture_27_81cm?: number[];
+ };
+ /** @description For each selected daily weather variable, data will be returned as a floating point array. Additionally a `time` array will be returned with ISO8601 timestamps. */
+ DailyResponse: {
+ time: string[];
+ temperature_2m_max?: number[];
+ temperature_2m_min?: number[];
+ apparent_temperature_max?: number[];
+ apparent_temperature_min?: number[];
+ precipitation_sum?: number[];
+ precipitation_hours?: number[];
+ weather_code?: number[];
+ sunrise?: number[];
+ sunset?: number[];
+ wind_speed_10m_max?: number[];
+ wind_gusts_10m_max?: number[];
+ wind_direction_10m_dominant?: number[];
+ shortwave_radiation_sum?: number[];
+ uv_index_max?: number[];
+ uv_index_clear_sky_max?: number[];
+ et0_fao_evapotranspiration?: number[];
+ };
+ /** @description Current weather conditions with the attributes: time, temperature, wind_speed, wind_direction and weather_code */
+ CurrentWeather: {
+ time: string;
+ temperature: number;
+ windspeed?: number;
+ winddirection?: number;
+ weather_code: number;
+ };
+ };
+ responses: never;
+ parameters: never;
+ requestBodies: never;
+ headers: never;
+ pathItems: never;
+}
+export type $defs = Record;
+export type operations = Record;
diff --git a/examples/feature-fetch/vanilla/basic/src/openapi/index.ts b/examples/feature-fetch/vanilla/basic/src/openapi/index.ts
new file mode 100644
index 00000000..a1d903b1
--- /dev/null
+++ b/examples/feature-fetch/vanilla/basic/src/openapi/index.ts
@@ -0,0 +1 @@
+export * from './open-meteo';
diff --git a/examples/feature-fetch/vanilla/basic/src/openapi/open-meteo.ts b/examples/feature-fetch/vanilla/basic/src/openapi/open-meteo.ts
new file mode 100644
index 00000000..f3e1b55d
--- /dev/null
+++ b/examples/feature-fetch/vanilla/basic/src/openapi/open-meteo.ts
@@ -0,0 +1,26 @@
+import { createOpenApiFetchClient } from 'feature-fetch';
+import type { paths } from './gen/open-meteo-v1';
+
+const openApiFetchClient = createOpenApiFetchClient({
+ baseUrl: 'https://api.open-meteo.com'
+});
+
+export async function fetchWeatherWithOpenApiFetchClient(
+ latitude: number,
+ longitude: number
+): Promise {
+ const [isOk, error, forecast] = await openApiFetchClient.get('/v1/forecast', {
+ queryParams: {
+ latitude,
+ longitude,
+ current_weather: true
+ }
+ });
+
+ if (!isOk) {
+ console.error('[openapi] Error Result', { error });
+ return;
+ }
+
+ console.log('[openapi] Ok Result', { forecast });
+}
diff --git a/examples/feature-fetch/vanilla/basic/src/style.css b/examples/feature-fetch/vanilla/basic/src/style.css
new file mode 100644
index 00000000..031ce139
--- /dev/null
+++ b/examples/feature-fetch/vanilla/basic/src/style.css
@@ -0,0 +1,33 @@
+:root {
+ font-family:
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ color: #172026;
+ background: #f6f8fb;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+body {
+ margin: 0;
+}
+
+main {
+ box-sizing: border-box;
+ width: min(720px, 100%);
+ margin: 0 auto;
+ padding: 48px 24px;
+}
+
+h1 {
+ margin: 0 0 12px;
+ font-size: 32px;
+ line-height: 1.1;
+ letter-spacing: 0;
+}
+
+p {
+ margin: 0;
+ color: #52616b;
+}
diff --git a/examples/feature-fetch/vanilla/basic/tsconfig.json b/examples/feature-fetch/vanilla/basic/tsconfig.json
new file mode 100644
index 00000000..f52b4fe7
--- /dev/null
+++ b/examples/feature-fetch/vanilla/basic/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "es2023",
+ "module": "esnext",
+ "lib": ["ES2023", "DOM"],
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "plugins": [
+ {
+ "name": "gql.tada/ts-plugin",
+ "schema": "./resources/graphql/countries.graphql",
+ "tadaOutputLocation": "./src/graphql/gen/countries-env.d.ts"
+ }
+ ]
+ },
+ "include": ["src"]
+}
diff --git a/examples/feature-fetch/vanilla/open-meteo/ README.md b/examples/feature-fetch/vanilla/open-meteo/ README.md
deleted file mode 100644
index 3fb68e23..00000000
--- a/examples/feature-fetch/vanilla/open-meteo/ README.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# Vanilla Open Meteo Example
-
-This example demonstrates how to use the [Open Meteo API](https://open-meteo.com/) in a vanilla TypeScript setup, utilizing the Open Mateo OpenApi specification and using [feature-fetch](https://www.npmjs.com/package/feature-fetch).
-
-## Setup Instructions
-
-1. **Build the Monorepo** (only required once):
- - At the root of the monorepo, run:
- ```bash
- pnpm build
- ```
-
-2. **Install Dependencies**:
- - In this example directory, run:
- ```bash
- pnpm install
- ```
-
-3. **Start the Development Server**:
- - In this example directory, run:
- ```bash
- pnpm dev
- ```
-
-## About
-
-This project was bootstrapped using Vite with the Vanilla TypeScript template:
-
-```bash
-pnpm create vite example-name --template vanilla-ts
-```
-
-## Open Meteo API
-
-For detailed information about the Open Meteo API, refer to the [OpenAPI v1 YAML source](https://github.com/open-meteo/open-meteo/blob/main/openapi.yml).
diff --git a/examples/feature-fetch/vanilla/open-meteo/index.html b/examples/feature-fetch/vanilla/open-meteo/index.html
deleted file mode 100644
index 44a93350..00000000
--- a/examples/feature-fetch/vanilla/open-meteo/index.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
- Vite + TS
-
-
-
-
-
-
diff --git a/examples/feature-fetch/vanilla/open-meteo/package.json b/examples/feature-fetch/vanilla/open-meteo/package.json
deleted file mode 100644
index a1728ade..00000000
--- a/examples/feature-fetch/vanilla/open-meteo/package.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "name": "feature-fetch-vanilla-open-meteo",
- "version": "0.0.1",
- "private": true,
- "type": "module",
- "scripts": {
- "dev": "vite",
- "openapi:generate": "npx openapi-typescript ./resources/openapi-v1.yaml -o ./src/gen/v1.ts"
- },
- "dependencies": {
- "feature-fetch": "workspace:*"
- },
- "devDependencies": {
- "openapi-typescript": "^7.10.1",
- "typescript": "^5.9.3",
- "vite": "^7.3.1"
- }
-}
diff --git a/examples/feature-fetch/vanilla/open-meteo/public/vite.svg b/examples/feature-fetch/vanilla/open-meteo/public/vite.svg
deleted file mode 100644
index e7b8dfb1..00000000
--- a/examples/feature-fetch/vanilla/open-meteo/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/examples/feature-fetch/vanilla/open-meteo/src/api.ts b/examples/feature-fetch/vanilla/open-meteo/src/api.ts
deleted file mode 100644
index da6bba6d..00000000
--- a/examples/feature-fetch/vanilla/open-meteo/src/api.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { createApiFetchClient } from 'feature-fetch';
-import { paths } from './gen/v1';
-
-const apiFetchClient = createApiFetchClient({ prefixUrl: 'https://api.open-meteo.com/' });
-
-export async function fetchWeatherWithApiFetchClient(latitude: number, longitude: number) {
- const result = await apiFetchClient.get<
- paths['/v1/forecast']['get']['responses']['200']['content']['application/json']
- >('/v1/forecast', {
- queryParams: {
- latitude,
- longitude
- }
- });
-
- // Handle response
- if (result.isOk()) {
- console.log('[api] Ok Result', { data: result.value.data });
- } else {
- console.error('[api] Error Result', { error: result.error });
- }
-
- // Or unwrap the response, throwing an exception on error
- try {
- const data = result.unwrap().data;
- console.log('[api/unwrap] Ok Result', { data });
- } catch (error) {
- console.error('[api/unwrap] Error Result', { error });
- }
-}
diff --git a/examples/feature-fetch/vanilla/open-meteo/src/gen/v1.ts b/examples/feature-fetch/vanilla/open-meteo/src/gen/v1.ts
deleted file mode 100644
index a900f658..00000000
--- a/examples/feature-fetch/vanilla/open-meteo/src/gen/v1.ts
+++ /dev/null
@@ -1,252 +0,0 @@
-/**
- * This file was auto-generated by openapi-typescript.
- * Do not make direct changes to the file.
- */
-
-export interface paths {
- '/v1/forecast': {
- parameters: {
- query?: never;
- header?: never;
- path?: never;
- cookie?: never;
- };
- /**
- * 7 day weather forecast for coordinates
- * @description 7 day weather variables in hourly and daily resolution for given WGS84 latitude and longitude coordinates. Available worldwide.
- */
- get: {
- parameters: {
- query: {
- hourly?: (
- | 'temperature_2m'
- | 'relative_humidity_2m'
- | 'dew_point_2m'
- | 'apparent_temperature'
- | 'pressure_msl'
- | 'cloud_cover'
- | 'cloud_cover_low'
- | 'cloud_cover_mid'
- | 'cloud_cover_high'
- | 'wind_speed_10m'
- | 'wind_speed_80m'
- | 'wind_speed_120m'
- | 'wind_speed_180m'
- | 'wind_direction_10m'
- | 'wind_direction_80m'
- | 'wind_direction_120m'
- | 'wind_direction_180m'
- | 'wind_gusts_10m'
- | 'shortwave_radiation'
- | 'direct_radiation'
- | 'direct_normal_irradiance'
- | 'diffuse_radiation'
- | 'vapour_pressure_deficit'
- | 'evapotranspiration'
- | 'precipitation'
- | 'weather_code'
- | 'snow_height'
- | 'freezing_level_height'
- | 'soil_temperature_0cm'
- | 'soil_temperature_6cm'
- | 'soil_temperature_18cm'
- | 'soil_temperature_54cm'
- | 'soil_moisture_0_1cm'
- | 'soil_moisture_1_3cm'
- | 'soil_moisture_3_9cm'
- | 'soil_moisture_9_27cm'
- | 'soil_moisture_27_81cm'
- )[];
- daily?: (
- | 'temperature_2m_max'
- | 'temperature_2m_min'
- | 'apparent_temperature_max'
- | 'apparent_temperature_min'
- | 'precipitation_sum'
- | 'precipitation_hours'
- | 'weather_code'
- | 'sunrise'
- | 'sunset'
- | 'wind_speed_10m_max'
- | 'wind_gusts_10m_max'
- | 'wind_direction_10m_dominant'
- | 'shortwave_radiation_sum'
- | 'uv_index_max'
- | 'uv_index_clear_sky_max'
- | 'et0_fao_evapotranspiration'
- )[];
- /** @description WGS84 coordinate */
- latitude: number;
- /** @description WGS84 coordinate */
- longitude: number;
- current_weather?: boolean;
- temperature_unit?: 'celsius' | 'fahrenheit';
- wind_speed_unit?: 'kmh' | 'ms' | 'mph' | 'kn';
- /** @description If format `unixtime` is selected, all time values are returned in UNIX epoch time in seconds. Please not that all time is then in GMT+0! For daily values with unix timestamp, please apply `utc_offset_seconds` again to get the correct date. */
- timeformat?: 'iso8601' | 'unixtime';
- /** @description If `timezone` is set, all timestamps are returned as local-time and data is returned starting at 0:00 local-time. Any time zone name from the [time zone database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) is supported. */
- timezone?: string;
- /** @description If `past_days` is set, yesterdays or the day before yesterdays data are also returned. */
- past_days?: 1 | 2;
- };
- header?: never;
- path?: never;
- cookie?: never;
- };
- requestBody?: never;
- responses: {
- /** @description OK */
- 200: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- 'application/json': {
- /**
- * @description WGS84 of the center of the weather grid-cell which was used to generate this forecast. This coordinate might be up to 5 km away.
- * @example 52.52
- */
- latitude?: number;
- /**
- * @description WGS84 of the center of the weather grid-cell which was used to generate this forecast. This coordinate might be up to 5 km away.
- * @example 13.419.52
- */
- longitude?: number;
- /**
- * @description The elevation in meters of the selected weather grid-cell. In mountain terrain it might differ from the location you would expect.
- * @example 44.812
- */
- elevation?: number;
- /**
- * @description Generation time of the weather forecast in milli seconds. This is mainly used for performance monitoring and improvements.
- * @example 2.2119
- */
- generationtime_ms?: number;
- /**
- * @description Applied timezone offset from the &timezone= parameter.
- * @example 3600
- */
- utc_offset_seconds?: number;
- /** @description For each selected weather variable, data will be returned as a floating point array. Additionally a `time` array will be returned with ISO8601 timestamps. */
- hourly?: Record;
- /** @description For each selected weather variable, the unit will be listed here. */
- hourly_units?: {
- [key: string]: string;
- };
- /** @description For each selected daily weather variable, data will be returned as a floating point array. Additionally a `time` array will be returned with ISO8601 timestamps. */
- daily?: Record;
- /** @description For each selected daily weather variable, the unit will be listed here. */
- daily_units?: {
- [key: string]: string;
- };
- /** @description Current weather conditions with the attributes: time, temperature, wind_speed, wind_direction and weather_code */
- current_weather?: Record;
- };
- };
- };
- /** @description Bad Request */
- 400: {
- headers: {
- [name: string]: unknown;
- };
- content: {
- 'application/json': {
- /** @description Always set true for errors */
- error?: boolean;
- /**
- * @description Description of the error
- * @example Latitude must be in range of -90 to 90°. Given: 300
- */
- reason?: string;
- };
- };
- };
- };
- };
- put?: never;
- post?: never;
- delete?: never;
- options?: never;
- head?: never;
- patch?: never;
- trace?: never;
- };
-}
-export type webhooks = Record;
-export interface components {
- schemas: {
- HourlyResponse: {
- time: string[];
- temperature_2m?: Record[];
- relative_humidity_2m?: Record[];
- dew_point_2m?: Record[];
- apparent_temperature?: Record[];
- pressure_msl?: Record[];
- cloud_cover?: Record[];
- cloud_cover_low?: Record[];
- cloud_cover_mid?: Record[];
- cloud_cover_high?: Record[];
- wind_speed_10m?: Record[];
- wind_speed_80m?: Record[];
- wind_speed_120m?: Record[];
- wind_speed_180m?: Record[];
- wind_direction_10m?: Record[];
- wind_direction_80m?: Record[];
- wind_direction_120m?: Record[];
- wind_direction_180m?: Record[];
- wind_gusts_10m?: Record[];
- shortwave_radiation?: Record[];
- direct_radiation?: Record[];
- direct_normal_irradiance?: Record[];
- diffuse_radiation?: Record[];
- vapour_pressure_deficit?: Record[];
- evapotranspiration?: Record[];
- precipitation?: Record[];
- weather_code?: Record[];
- snow_height?: Record[];
- freezing_level_height?: Record[];
- soil_temperature_0cm?: Record[];
- soil_temperature_6cm?: Record[];
- soil_temperature_18cm?: Record[];
- soil_temperature_54cm?: Record[];
- soil_moisture_0_1cm?: Record[];
- soil_moisture_1_3cm?: Record[];
- soil_moisture_3_9cm?: Record[];
- soil_moisture_9_27cm?: Record[];
- soil_moisture_27_81cm?: Record[];
- };
- DailyResponse: {
- time: string[];
- temperature_2m_max?: Record[];
- temperature_2m_min?: Record[];
- apparent_temperature_max?: Record[];
- apparent_temperature_min?: Record[];
- precipitation_sum?: Record[];
- precipitation_hours?: Record[];
- weather_code?: Record[];
- sunrise?: Record[];
- sunset?: Record[];
- wind_speed_10m_max?: Record[];
- wind_gusts_10m_max?: Record[];
- wind_direction_10m_dominant?: Record[];
- shortwave_radiation_sum?: Record[];
- uv_index_max?: Record[];
- uv_index_clear_sky_max?: Record[];
- et0_fao_evapotranspiration?: Record[];
- };
- CurrentWeather: {
- time: string;
- temperature: Record;
- wind_speed: Record;
- wind_direction: Record;
- weather_code: Record;
- };
- };
- responses: never;
- parameters: never;
- requestBodies: never;
- headers: never;
- pathItems: never;
-}
-export type $defs = Record;
-export type operations = Record;
diff --git a/examples/feature-fetch/vanilla/open-meteo/src/openapi.ts b/examples/feature-fetch/vanilla/open-meteo/src/openapi.ts
deleted file mode 100644
index 84b5d234..00000000
--- a/examples/feature-fetch/vanilla/open-meteo/src/openapi.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import { createOpenApiFetchClient } from 'feature-fetch';
-import { type paths } from './gen/v1';
-
-const openApiFetchClient = createOpenApiFetchClient({
- prefixUrl: 'https://api.open-meteo.com/'
-});
-
-export async function fetchWeatherWithOpenApiFetchClient(latitude: number, longitude: number) {
- const result = await openApiFetchClient.get('/v1/forecast', {
- queryParams: {
- latitude,
- longitude
- }
- });
-
- // Handle response
- if (result.isOk()) {
- console.log('[openapi] Ok Result', { data: result.value.data });
- } else {
- console.error('[openapi] Error Result', { error: result.error });
- }
-
- // Or unwrap the response, throwing an exception on error
- try {
- const data = result.unwrap().data;
- console.log('[openapi/unwrap] Ok Result', { data });
- } catch (error) {
- console.error('[openapi/unwrap] Error Result', { error });
- }
-}
diff --git a/examples/feature-fetch/vanilla/open-meteo/src/vite-env.d.ts b/examples/feature-fetch/vanilla/open-meteo/src/vite-env.d.ts
deleted file mode 100644
index 11f02fe2..00000000
--- a/examples/feature-fetch/vanilla/open-meteo/src/vite-env.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-///
diff --git a/examples/feature-fetch/vanilla/open-meteo/vite.config.ts b/examples/feature-fetch/vanilla/open-meteo/vite.config.ts
deleted file mode 100644
index c049f46e..00000000
--- a/examples/feature-fetch/vanilla/open-meteo/vite.config.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { defineConfig } from 'vite';
-
-export default defineConfig({});
diff --git a/examples/feature-form/react/basic/README.md b/examples/feature-form/react/basic/README.md
new file mode 100644
index 00000000..cf2e6426
--- /dev/null
+++ b/examples/feature-form/react/basic/README.md
@@ -0,0 +1,16 @@
+# feature-form React Basic
+
+React example for `feature-form` and `feature-react/form`. It shows a typed form where fields are reactive state, validation uses Zod through Standard Schema, and components subscribe to the field or form state they render.
+
+- `createForm` with typed submit data
+- field bindings with `useFormField`
+- Zod validation through Standard Schema
+- form-level validation for the admin age rule
+- dirty tracking with `dirtyFeature()`
+- render counters for per-field subscriptions
+
+## Run
+
+```sh
+pnpm dev
+```
diff --git a/examples/feature-form/react/basic/eslint.config.cjs b/examples/feature-form/react/basic/eslint.config.cjs
deleted file mode 100644
index 3436c292..00000000
--- a/examples/feature-form/react/basic/eslint.config.cjs
+++ /dev/null
@@ -1,17 +0,0 @@
-const reactRefresh = require('eslint-plugin-react-refresh');
-
-/**
- * @see https://eslint.org/docs/latest/use/configure/configuration-files
- * @type {import("eslint").Linter.Config}
- */
-module.exports = [
- ...require('@blgc/config/eslint/react-internal'),
- {
- plugins: {
- 'react-refresh': reactRefresh
- },
- rules: {
- 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }]
- }
- }
-];
diff --git a/examples/feature-form/react/basic/eslint.config.js b/examples/feature-form/react/basic/eslint.config.js
new file mode 100644
index 00000000..ba573c21
--- /dev/null
+++ b/examples/feature-form/react/basic/eslint.config.js
@@ -0,0 +1,22 @@
+import js from '@eslint/js';
+import reactHooks from 'eslint-plugin-react-hooks';
+import reactRefresh from 'eslint-plugin-react-refresh';
+import { defineConfig, globalIgnores } from 'eslint/config';
+import globals from 'globals';
+import tseslint from 'typescript-eslint';
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite
+ ],
+ languageOptions: {
+ globals: globals.browser
+ }
+ }
+]);
diff --git a/examples/feature-form/react/basic/index.html b/examples/feature-form/react/basic/index.html
index e4b78eae..14b41a07 100644
--- a/examples/feature-form/react/basic/index.html
+++ b/examples/feature-form/react/basic/index.html
@@ -2,9 +2,9 @@
-
+
- Vite + React + TS
+ feature-form React basic
diff --git a/examples/feature-form/react/basic/package.json b/examples/feature-form/react/basic/package.json
index f9bae056..846e7740 100644
--- a/examples/feature-form/react/basic/package.json
+++ b/examples/feature-form/react/basic/package.json
@@ -1,31 +1,33 @@
{
"name": "feature-form-react-basic",
- "version": "0.0.1",
+ "version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
- "dev": "vite"
+ "build": "tsc -b && vite build",
+ "dev": "vite",
+ "lint": "eslint .",
+ "preview": "vite preview"
},
"dependencies": {
- "@blgc/utils": "workspace:*",
"feature-form": "workspace:*",
"feature-react": "workspace:*",
- "react": "^19.2.3",
- "react-dom": "^19.2.3",
- "valibot": "1.2.0",
- "validation-adapters": "workspace:*",
- "zod": "^4.3.5"
+ "react": "^19.2.6",
+ "react-dom": "^19.2.6",
+ "zod": "^4.4.3"
},
"devDependencies": {
- "@types/react": "^19.2.8",
+ "@eslint/js": "^10.0.1",
+ "@types/node": "^24.12.3",
+ "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
- "@typescript-eslint/eslint-plugin": "^8.53.0",
- "@typescript-eslint/parser": "^8.53.0",
- "@vitejs/plugin-react": "^5.1.2",
- "eslint": "^9.39.2",
- "eslint-plugin-react-hooks": "^7.0.1",
- "eslint-plugin-react-refresh": "^0.4.26",
- "typescript": "^5.9.3",
- "vite": "^7.3.1"
+ "@vitejs/plugin-react": "^6.0.1",
+ "eslint": "^10.3.0",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.6.0",
+ "typescript": "~6.0.2",
+ "typescript-eslint": "^8.59.2",
+ "vite": "^8.0.12"
}
}
diff --git a/examples/feature-form/react/basic/public/favicon.svg b/examples/feature-form/react/basic/public/favicon.svg
new file mode 100644
index 00000000..6893eb13
--- /dev/null
+++ b/examples/feature-form/react/basic/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/examples/feature-form/react/basic/public/vite.svg b/examples/feature-form/react/basic/public/vite.svg
deleted file mode 100644
index e7b8dfb1..00000000
--- a/examples/feature-form/react/basic/public/vite.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/examples/feature-form/react/basic/src/App.css b/examples/feature-form/react/basic/src/App.css
index 9fca8d70..543e98ee 100644
--- a/examples/feature-form/react/basic/src/App.css
+++ b/examples/feature-form/react/basic/src/App.css
@@ -1,76 +1,52 @@
-body {
- background: #0e101c;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
- 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-}
-
-form {
- max-width: 500px;
+.app {
+ display: grid;
+ gap: 24px;
margin: 0 auto;
+ max-width: 960px;
+ padding: 40px 24px;
}
-h1 {
- font-weight: 100;
- color: white;
- text-align: center;
- padding-bottom: 10px;
- border-bottom: 1px solid rgb(79, 98, 148);
-}
-
-p {
- color: white;
+form,
+.section {
+ display: grid;
+ gap: 16px;
}
-.form {
- background: #0e101c;
- max-width: 400px;
- margin: 0 auto;
+.section {
+ border: 1px solid #d8d8d8;
+ padding: 16px;
}
-.error {
- color: #bf1650;
+.field-grid {
+ display: grid;
+ gap: 12px;
}
-.error::before {
- display: inline;
- content: '⚠ ';
+.stack {
+ display: grid;
+ gap: 10px;
}
-textarea,
-input {
- display: block;
- box-sizing: border-box;
- width: 100%;
- border-radius: 4px;
- border: 1px solid white;
- padding: 10px 15px;
- margin-bottom: 10px;
- font-size: 14px;
+.field-row {
+ align-items: start;
+ display: grid;
+ gap: 8px;
+ grid-template-columns: 140px minmax(160px, 1fr) minmax(180px, 1fr) 110px 180px;
}
-label {
- line-height: 2;
- text-align: left;
- display: block;
- margin-bottom: 13px;
- margin-top: 20px;
- color: white;
- font-size: 14px;
- font-weight: 200;
+.actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
}
-.App {
- max-width: 600px;
- margin: 0 auto;
+.render-count {
+ color: #666;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
-button {
- display: block;
- appearance: none;
- margin-top: 40px;
- border: 1px solid #333;
- margin-bottom: 20px;
- text-transform: uppercase;
- padding: 10px 20px;
- border-radius: 4px;
+@media (max-width: 760px) {
+ .field-row {
+ grid-template-columns: 1fr;
+ }
}
diff --git a/examples/feature-form/react/basic/src/App.tsx b/examples/feature-form/react/basic/src/App.tsx
index dc63091d..e7c2d002 100644
--- a/examples/feature-form/react/basic/src/App.tsx
+++ b/examples/feature-form/react/basic/src/App.tsx
@@ -1,220 +1,271 @@
-import { randomHex, shortId } from '@blgc/utils';
import {
- bitwiseFlag,
createForm,
- FormFieldReValidateMode,
- FormFieldValidateMode,
- TFormFieldValidator
+ dirtyFeature,
+ type TFormErrors,
+ type TFormFieldKey,
+ type TValidationStatusValue
} from 'feature-form';
-import { useForm } from 'feature-react/form';
-import { useFeatureState, withGlobalBind } from 'feature-react/state';
+import { useFormField } from 'feature-react/form';
+import { useFeatureState } from 'feature-react/state';
import React from 'react';
-import * as v from 'valibot';
-import { createValidator } from 'validation-adapters';
-import { vValidator } from 'validation-adapters/valibot';
-import { zValidator } from 'validation-adapters/zod';
import * as z from 'zod';
import './App.css';
-import { StatusMessage } from './components';
-import { isLightColor } from './utils';
-type TGender = 'male' | 'female' | 'diverse';
+const $form = createForm({
+ fields: {
+ firstName: {
+ defaultValue: '',
+ validator: z.string().min(2, 'Enter at least two characters')
+ },
+ email: {
+ defaultValue: '',
+ validator: z.email('Enter a valid email')
+ },
+ age: {
+ defaultValue: 18,
+ validator: z.number().min(18, 'You must be at least 18')
+ },
+ role: {
+ defaultValue: 'reader',
+ validator: z.enum(['reader', 'admin'])
+ }
+ },
+ validator: z.custom(
+ (value) => {
+ const data = value as TFormData;
+ return data.role !== 'admin' || data.age >= 21;
+ },
+ {
+ error: 'Admins must be at least 21',
+ path: ['age']
+ }
+ ),
+ validateOn: ['submit'],
+ revalidateOn: ['change', 'blur', 'submit']
+}).with(dirtyFeature());
-type TFormData = {
- // [key: string]: string; // https://stackoverflow.com/questions/65799316/why-cant-an-interface-be-assigned-to-recordstring-unknown
+interface TFormData {
firstName: string;
- lastName: string;
- gender: TGender;
email: string;
- image: {
- id: string;
- color: string;
- };
-};
+ age: number;
+ role: TRole;
+}
-const valibotNameValidator = vValidator(
- v.pipe(v.string(), v.minLength(2), v.maxLength(10), v.regex(/^([^0-9]*)$/))
-);
-
-const $form = withGlobalBind(
- '__form',
- createForm({
- fields: {
- firstName: {
- validator: valibotNameValidator.clone().append(
- createValidator([
- {
- key: 'jeff',
- validate: (cx) => {
- if (cx.value !== 'Jeff') {
- cx.registerError({
- code: 'jeff',
- message: 'Only the name Jeff is allowed.'
- });
- }
- }
- }
- ])
- ),
- defaultValue: ''
- },
- lastName: {
- validator: valibotNameValidator,
- defaultValue: ''
- },
- gender: {
- validator: createValidator([
- {
- key: 'gender',
- validate: (cx) => {
- if (cx.value !== 'female' && cx.value !== 'male' && cx.value !== 'diverse') {
- cx.registerError({
- code: 'invalid-gender',
- message: 'Unknown gender.'
- });
- }
- }
- }
- ]) as TFormFieldValidator,
- defaultValue: 'female'
- },
- email: {
- validator: zValidator(z.email().max(30).min(1)),
- defaultValue: ''
+type TRole = 'reader' | 'admin';
+
+export const App: React.FC = () => {
+ const [submitResult, setSubmitResult] = React.useState(null);
+
+ const handleSubmit = React.useCallback((event: React.FormEvent) => {
+ event.preventDefault();
+ void $form.submit({
+ onValidSubmit(data) {
+ setSubmitResult({ type: 'valid', data });
},
- image: {
- validator: createValidator([
- {
- key: 'color',
- validate: (cx) => {
- const value = cx.value as TFormData['image'] | undefined;
- const color = value?.color;
- if (color != null && !isLightColor(color)) {
- cx.registerError({
- code: 'too-dark',
- message: 'The image is too dark.'
- });
- }
- }
- }
- ]),
- defaultValue: {
- id: shortId(),
- color: randomHex()
- }
+ onInvalidSubmit(errors) {
+ setSubmitResult({ type: 'invalid', errors });
}
- },
- onValidSubmit: (data, listenerContext) => {
- console.log('ValidSubmit', { data, listenerContext });
- return { valid: true };
- },
- onInvalidSubmit: (errors, listenerContext) => {
- console.log('Invalid Submit', { errors, listenerContext });
- return { valid: false };
- },
- notifyOnStatusChange: false,
- validateMode: bitwiseFlag(FormFieldValidateMode.OnSubmit),
- reValidateMode: bitwiseFlag(FormFieldReValidateMode.OnBlur, FormFieldReValidateMode.OnChange),
- collectErrorMode: 'firstError'
- })
-);
+ });
+ }, []);
+
+ const handleReset = React.useCallback(() => {
+ $form.reset();
+ setSubmitResult(null);
+ }, []);
+
+ return (
+
+
+
feature-form React basic
+
+ Per-field subscriptions with Standard Schema validation, dirty tracking, and visible
+ render counts.
+