Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,18 @@ _Avoid_: input, widget, picker
To show the user what a postcode points to before the app stores it.
_Avoid_: verify, validate (for this step)

**Check status**:
What the WooCommerce plugin learned about the postcode of an order: `valid`, `invalid`, `unchecked` or `error`. The order meta `_gatepost_postcode_check` holds it.
_Avoid_: verification status, validation result

**Form value**:
The text that a field gives its form when the form is sent: the canonical form of a postcode, the 6 digits of a legacy postcode, or the text without white space at the start and the end.
_Avoid_: submitted value, output

**Message catalogue**:
A data file that holds the text of a UI in one language, one message for each key.
_Avoid_: strings file, translations, locale file

**Tell**:
A habit that makes code look machine-written, such as very long lines.
_Avoid_: smell (smells are design problems), anti-pattern
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ This repo holds the source of the site at <https://gatepost-dev.github.io/docs/>
## What the site holds

- A [playground](https://gatepost-dev.github.io/docs/playground/) where you check a postcode in your browser.
- Guides for the TypeScript core, the TypeScript client and the PHP package.
- Guides for the TypeScript core, the TypeScript client, the PHP package, the HTML field and the React field.
- An API reference that the build writes from the TSDoc comments and the PHP doc comments.
- The spec pages: the grammar, the client contract and the glossary.
- The spec pages: the grammar, the client contract, the field and the glossary.
- A [readiness tracker](https://gatepost-dev.github.io/docs/readiness/) of common tools that reject the new postcode.

## Run it on your computer
Expand Down
11 changes: 10 additions & 1 deletion astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const base = '/docs';

const [coreTypeDoc, coreTypeDocGroup] = createStarlightTypeDocPlugin();
const [clientTypeDoc, clientTypeDocGroup] = createStarlightTypeDocPlugin();
const [fieldTypeDoc, fieldTypeDocGroup] = createStarlightTypeDocPlugin();
const [reactTypeDoc, reactTypeDocGroup] = createStarlightTypeDocPlugin();

// The API pages of a package, generated from the TSDoc comments of its source.
function typeDocOf(name) {
Expand Down Expand Up @@ -95,7 +97,12 @@ export default defineConfig({
// A code block that scrolls sideways needs a focus stop for keyboard users. Wrapped lines
// need none, and they fit a phone screen.
expressiveCode: { defaultProps: { wrap: true } },
plugins: [coreTypeDoc(typeDocOf('core')), clientTypeDoc(typeDocOf('client'))],
plugins: [
coreTypeDoc(typeDocOf('core')),
clientTypeDoc(typeDocOf('client')),
fieldTypeDoc(typeDocOf('field')),
reactTypeDoc(typeDocOf('react')),
],
sidebar: [
{ label: 'Start', items: ['start', 'playground'] },
{ label: 'Guides', items: [{ autogenerate: { directory: 'guides' } }] },
Expand All @@ -105,6 +112,8 @@ export default defineConfig({
items: [
coreTypeDocGroup,
clientTypeDocGroup,
fieldTypeDocGroup,
reactTypeDocGroup,
{
label: 'gatepost/postcode',
collapsed: true,
Expand Down
2 changes: 1 addition & 1 deletion js
Submodule js updated 86 files
+5 −0 .changeset/client-spec-0-3-0.md
+5 −0 .changeset/core-spec-0-3-0.md
+5 −0 .changeset/first-field-release.md
+5 −0 .changeset/first-react-release.md
+6 −0 .changeset/shorter-readmes.md
+36 −0 .github/workflows/ci.yml
+3 −0 .gitignore
+1 −0 .prettierignore
+2 −2 AGENTS.md
+12 −0 CONTEXT.md
+50 −40 README.md
+39 −0 docs/develop.md
+3 −0 eslint.config.js
+11 −3 package.json
+58 −128 packages/client/README.md
+1 −1 packages/client/etc/client.api.md
+2 −2 packages/client/src/version.ts
+2 −4 packages/client/test/readme.test.ts
+67 −148 packages/core/README.md
+1 −1 packages/core/etc/core.api.md
+2 −2 packages/core/src/version.ts
+2 −4 packages/core/test/readme.test.ts
+202 −0 packages/field/LICENSE
+5 −0 packages/field/NOTICE
+179 −0 packages/field/README.md
+25 −0 packages/field/api-extractor.json
+72 −0 packages/field/etc/field.api.md
+74 −0 packages/field/package.json
+29 −0 packages/field/playwright.config.ts
+16 −0 packages/field/scripts/smoke.mjs
+664 −0 packages/field/src/element.ts
+31 −0 packages/field/src/index.ts
+132 −0 packages/field/src/location.ts
+63 −0 packages/field/src/messages.ts
+67 −0 packages/field/src/reading.ts
+123 −0 packages/field/src/requests.ts
+74 −0 packages/field/src/spec-messages.ts
+129 −0 packages/field/src/state.ts
+136 −0 packages/field/src/styles.ts
+48 −0 packages/field/src/types.ts
+14 −0 packages/field/src/version.ts
+153 −0 packages/field/src/view.ts
+490 −0 packages/field/test/element.test.ts
+72 −0 packages/field/test/field.ts
+50 −0 packages/field/test/gateway.ts
+42 −0 packages/field/test/journeys/fixtures.ts
+296 −0 packages/field/test/journeys/form.test.ts
+105 −0 packages/field/test/journeys/readme.test.ts
+75 −0 packages/field/test/journeys/serve.mjs
+639 −0 packages/field/test/location.test.ts
+423 −0 packages/field/test/lookup.test.ts
+53 −0 packages/field/test/messages.test.ts
+64 −0 packages/field/test/reading.test.ts
+33 −0 packages/field/test/requests.test.ts
+23 −0 packages/field/test/setup.ts
+129 −0 packages/field/test/styles.test.ts
+34 −0 packages/field/test/tsdoc.test.ts
+12 −0 packages/field/test/version.test.ts
+13 −0 packages/field/tsconfig.json
+39 −0 packages/field/tsdown.config.ts
+37 −0 packages/field/vitest.config.ts
+42 −30 packages/mock-server/README.md
+202 −0 packages/react/LICENSE
+5 −0 packages/react/NOTICE
+116 −0 packages/react/README.md
+25 −0 packages/react/api-extractor.json
+39 −0 packages/react/etc/react.api.md
+73 −0 packages/react/package.json
+18 −0 packages/react/scripts/smoke.mjs
+13 −0 packages/react/src/index.ts
+215 −0 packages/react/src/postcode-field.ts
+14 −0 packages/react/src/version.ts
+326 −0 packages/react/test/browser.test.ts
+22 −0 packages/react/test/readme-example.tsx
+41 −0 packages/react/test/readme.test.ts
+40 −0 packages/react/test/server.test.ts
+16 −0 packages/react/tsconfig.json
+28 −0 packages/react/tsdown.config.ts
+58 −0 packages/react/vitest.config.ts
+310 −6 pnpm-lock.yaml
+7 −0 scripts/check-licences.mjs
+5 −3 scripts/check-licences.test.mjs
+27 −0 scripts/eslint-config.test.mjs
+115 −0 scripts/generate-messages.mjs
+149 −0 scripts/generate-messages.test.mjs
+1 −1 spec
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"scripts": {
"sdk": "run-s sdk:js sdk:js:build sdk:php",
"sdk:js": "pnpm --dir js install --frozen-lockfile",
"sdk:js:build": "pnpm --dir js --filter @gatepost/core --filter @gatepost/client run build",
"sdk:js:build": "pnpm --dir js --filter './packages/{core,client,field,react}' run build",
"sdk:php": "composer --working-dir=php install --no-interaction --no-progress",
"dev": "ASTRO_TELEMETRY_DISABLED=1 astro dev",
"build": "ASTRO_TELEMETRY_DISABLED=1 astro build",
Expand Down
8 changes: 7 additions & 1 deletion scripts/spec-pages.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2026 The Gatepost authors
// SPDX-License-Identifier: Apache-2.0
// Writes the spec pages of the site from the spec submodule, so that the site shows the grammar,
// the client contract and the glossary that the SDKs implement, word for word.
// the client contract, the field and the glossary that the SDKs implement, word for word.
import { execFileSync } from 'node:child_process';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
Expand All @@ -25,6 +25,12 @@ const SOURCES = [
title: 'Client contract',
description: "How every Gatepost client calls NIPOST's gateway: results, errors and retries.",
},
{
file: 'field.md',
slug: 'field',
title: 'Field',
description: 'How every Gatepost postcode field behaves: settings, events, states and privacy.',
},
{
file: 'CONTEXT.md',
slug: 'glossary',
Expand Down
152 changes: 152 additions & 0 deletions src/content/docs/guides/html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
---
title: Add a postcode field to a web form
description: Use the gatepost-postcode-field element in any HTML form, with or without a framework.
sidebar:
label: HTML field
order: 4
---

`@gatepost/field` adds `<gatepost-postcode-field>`, a form field for Nigeria's digital postcode. It works in a plain HTML form. It checks the form of the postcode while the user types, and it can ask NIPOST's gateway about the postcode.

## Install

The first alpha is not published yet. This page will say when it is.

```sh
pnpm add @gatepost/field
```

Import `@gatepost/field` once in your app, and the page can use the element. A page with no build step can load the one-file build from a CDN instead, as the examples below do. Pin a version in the CDN address of a production page.

## Add the field

```html
<script type="module" src="https://cdn.jsdelivr.net/npm/@gatepost/field/dist/element.js"></script>
<form action="/address">
<gatepost-postcode-field name="postcode" required></gatepost-postcode-field>
<button>Continue</button>
</form>
```

The element is empty until the script runs, so the layout moves when it loads. A page with no JavaScript sends no postcode.

A `<label for>` outside the element cannot name the input inside it. Give the text with the `label` attribute.

A user who types `fc 01 z99 zz 01` sends `postcode=FC-01-Z99-ZZ-01`. With no key, the field checks the form offline and sends no request.

## Check postcodes with NIPOST

```html
<script type="module" src="https://cdn.jsdelivr.net/npm/@gatepost/field/dist/element.js"></script>
<form action="/address">
<gatepost-postcode-field name="postcode" api-key="nipost_pk_live_your_key" confirm="level2" gps>
</gatepost-postcode-field>
<button>Continue</button>
</form>
```

Use a publishable key, which starts with `nipost_pk_`. Add your site to its allowed origins in NIPOST's dashboard. Anyone can read a key in a page, so the field refuses a secret key. It sends no request, and it writes one error to the console.

A key holds a lookup level, and a call above it fails. The field then says that it could not check the postcode. A postcode that the gateway does not know, or a failed request, never stops the form. The gateway's data is new, so check the postcode again on your server.

<div class="table-scroll" role="region" tabindex="0" aria-label="Table: Attributes of the field">

| Attribute | Values | Default |
| --------------------------------------- | ---------------------------- | ----------------------------------- |
| `name`, `value`, `required`, `disabled` | as for a native input | none |
| `label` | the visible label | `Postcode` |
| `api-key` | a publishable key | none, so the field sends no request |
| `base-url` | the gateway's address | `https://api.postcode.gov.ng` |
| `confirm` | `none`, `level1` or `level2` | `level1` |
| `gps` | present or absent | absent |
| `legacy` | `accept` or `reject` | `accept` |

</div>

With `gps` and a key that the field accepts, a button puts the postcode of the user's location in the input. The field asks for the location only when the user presses the button. A location that is not precise enough gives a partial postcode, and the user types the rest. With `legacy="accept"`, the field also accepts an old 6-digit postcode.

## Listen for changes

```html
<script type="module" src="https://cdn.jsdelivr.net/npm/@gatepost/field/dist/element.js"></script>
<gatepost-postcode-field name="postcode"></gatepost-postcode-field>
<output id="chosen"></output>
<script type="module">
const field = document.querySelector('gatepost-postcode-field');
field.addEventListener('gatepost-change', (event) => {
document.querySelector('#chosen').value = event.detail.value;
});
</script>
```

The events bubble, so a page can listen on the form. At level 2, `gatepost-confirm` gives the recent house address of the postcode. The field never shows it, and your page should not show it to the person who typed the postcode. That person can have typed any postcode.

<div class="table-scroll" role="region" tabindex="0" aria-label="Table: Events of the field">

| Event | `detail` |
| ------------------ | ----------------------------------------------------------------------------------------------- |
| `gatepost-change` | `value`, `postcode`, `source` (`typed`, `pasted`, `suggestion` or `gps`) and `accuracyM` |
| `gatepost-confirm` | `lookup`, the result of the lookup |
| `gatepost-error` | `code`, an error code of `@gatepost/client`, or `secret_key`, `gps_denied` or `gps_unavailable` |

</div>

## Change the words and the look

```html
<script type="module" src="https://cdn.jsdelivr.net/npm/@gatepost/field/dist/element.js"></script>
<style>
gatepost-postcode-field {
--gatepost-accent: #5b2a86;
}
</style>
<gatepost-postcode-field name="postcode" required></gatepost-postcode-field>
<script type="module">
const field = document.querySelector('gatepost-postcode-field');
field.messages = { label: 'Delivery postcode', empty: 'Enter the postcode of the delivery.' };
</script>
```

The `messages` property replaces messages by key. The field keeps the English text for each key that you leave out. The [field spec](../../spec/field/) lists each key.

The tokens are `--gatepost-text`, `--gatepost-muted`, `--gatepost-background`, `--gatepost-border`, `--gatepost-accent`, `--gatepost-error`, `--gatepost-warning`, `--gatepost-radius` and `--gatepost-font`. The parts `field`, `label`, `hint`, `input`, `message` and `button` take `::part()` rules.

The default colours are for a light page. They meet a contrast of 4.5:1 on white. On a dark page, set the seven colour tokens, and check your own colours on the background of your page.

```css
gatepost-postcode-field {
--gatepost-text: #f2f5f4;
--gatepost-muted: #b9c2be;
--gatepost-background: #1c2321;
--gatepost-border: #8f9b96;
--gatepost-accent: #5ad1bf;
--gatepost-error: #ff9d94;
--gatepost-warning: #ffb95c;
}
```

## What the field does not do

- It stores nothing, sets no cookie and sends no telemetry.
- It sends a request only for a whole postcode, and only when it has a publishable key. It sends a location request only after the user presses the location button.
- It never shows a house address.

## Requirements

<div class="table-scroll" role="region" tabindex="0" aria-label="Table: Requirements of the field">

| Requirement | Version |
| ------------- | ---------------------------------------------------------- |
| Browser | a current Chrome, Edge or Firefox, or Safari 16.4 or later |
| Module format | ESM only |
| Gatepost spec | 0.3.0 |

</div>

A server can import the package. It defines the element only in a browser.

## Next steps

- [Use the field in React](../react/).
- [Read the field spec](../../spec/field/), which defines the settings, the events and the states.
- [Read the API reference of `@gatepost/field`](../../reference/js/field/).
84 changes: 84 additions & 0 deletions src/content/docs/guides/react.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
---
title: Add a postcode field to a React form
description: Use the PostcodeField component of @gatepost/react in React 18 or 19, and in Next.js.
sidebar:
label: React
order: 5
---

`@gatepost/react` wraps the field of `@gatepost/field` in a React component. It works with React 18 and React 19. It renders on the server, and a plain form sends the postcode in its canonical form.

## Install

The first alpha is not published yet. This page will say when it is.

```sh
pnpm add @gatepost/react
```

The package installs `@gatepost/field`, the custom element that does the work.

## Add the field

```tsx
'use client';
import { useState, type ReactElement } from 'react';
import { PostcodeField } from '@gatepost/react';

export function AddressForm(): ReactElement {
const [postcode, setPostcode] = useState('');
return (
<form action="/address">
<PostcodeField
name="postcode"
required
onChange={(detail) => {
setPostcode(detail.value);
}}
/>
<p>{postcode}</p>
<button>Continue</button>
</form>
);
}
```

A user who types `fc 01 z99 zz 01` sees `FC-01-Z99-ZZ-01`, and the form sends `postcode=FC-01-Z99-ZZ-01`.

The module starts with `'use client'`, so the component works in the App Router of Next.js. The HTML of a server render already holds the settings of the field.

## Choose the props

<div class="table-scroll" role="region" tabindex="0" aria-label="Table: Props of PostcodeField">

| Prop | Meaning |
| ---------------------------------------------- | --------------------------------------------------------------------- |
| `name`, `defaultValue`, `required`, `disabled` | as for a native input |
| `label` | the visible label. The default is `Postcode` |
| `apiKey` | a publishable key. Without one, the field sends no request |
| `baseUrl` | the gateway's address |
| `confirm` | `none`, `level1` or `level2`. The default is `level1` |
| `gps` | true shows the location button, when the field has a key |
| `legacy` | `accept` or `reject` an old 6-digit postcode. The default is `accept` |
| `messages` | text that replaces the English messages, by key |

</div>

`onChange`, `onConfirm` and `onError` receive the detail of each event of the field. The `ref` gives the element, for `checkValidity()` and the other members of a form control.

## Read the value

The field keeps its own text, as an uncontrolled input does. `defaultValue` sets the text at first, and a form reset returns to it. A new `defaultValue` later changes that first text, and never the text that the user typed.

Read each new value in `onChange`, or read the form when it submits. On React 19, a field that the browser rendered, not hydrated, keeps its first `defaultValue` after a later change of the prop. Set a `key` on the component if you need a new first text.

The [HTML field guide](../html/) explains the key, the events, the messages and the theme tokens.

## Requirements

React 18 or 19. The browser needs are those of `@gatepost/field`. The package is ESM only, and it implements Gatepost spec 0.3.0.

## Next steps

- [Use the field without React](../html/).
- [Read the API reference of `@gatepost/react`](../../reference/js/react/).
2 changes: 2 additions & 0 deletions src/content/docs/start.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Each Gatepost tool follows the same spec, so each one reads a postcode in the sa
| You want to | Use | Needs a key |
| -------------------------------------------------------- | -------------------------------------------------- | ---------------- |
| Check the form of a postcode in TypeScript or JavaScript | [`@gatepost/core`](../guides/typescript/) | no |
| Add a postcode field to an HTML form | [`@gatepost/field`](../guides/html/) | only to confirm |
| Add a postcode field to a React form | [`@gatepost/react`](../guides/react/) | only to confirm |
| Look up a postcode from a TypeScript server | [`@gatepost/client`](../guides/typescript-client/) | yes |
| Check or look up a postcode in PHP | [`gatepost/postcode`](../guides/php/) | only for lookups |

Expand Down
Loading