@yelison/forma-ui 0.0.1-bootstrap.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/dist/base.css +25 -0
- package/dist/components/Badge/Badge.d.ts +14 -0
- package/dist/components/Badge/Badge.js +12 -0
- package/dist/components/Badge/Badge.module.css.js +11 -0
- package/dist/components/Badge/index.d.ts +1 -0
- package/dist/components/Button/Button.d.ts +23 -0
- package/dist/components/Button/Button.js +34 -0
- package/dist/components/Button/Button.module.css.js +13 -0
- package/dist/components/Button/IconButton.d.ts +17 -0
- package/dist/components/Button/IconButton.js +20 -0
- package/dist/components/Button/IconButton.module.css.js +8 -0
- package/dist/components/Button/buttonClassName.d.ts +17 -0
- package/dist/components/Button/buttonClassName.js +8 -0
- package/dist/components/Button/index.d.ts +3 -0
- package/dist/components/Dialog/Dialog.d.ts +51 -0
- package/dist/components/Dialog/Dialog.js +38 -0
- package/dist/components/Dialog/Dialog.module.css.js +12 -0
- package/dist/components/Dialog/index.d.ts +1 -0
- package/dist/components/Field/Field.d.ts +42 -0
- package/dist/components/Field/Field.js +40 -0
- package/dist/components/Field/Field.module.css.js +9 -0
- package/dist/components/Field/control.module.css.js +4 -0
- package/dist/components/Field/index.d.ts +1 -0
- package/dist/components/Icon/Icon.d.ts +19 -0
- package/dist/components/Icon/Icon.js +27 -0
- package/dist/components/Icon/index.d.ts +1 -0
- package/dist/components/Icon/paths.d.ts +26 -0
- package/dist/components/Icon/paths.js +32 -0
- package/dist/components/Input/Input.d.ts +24 -0
- package/dist/components/Input/Input.js +23 -0
- package/dist/components/Input/index.d.ts +1 -0
- package/dist/components/Tooltip/Tooltip.d.ts +65 -0
- package/dist/components/Tooltip/Tooltip.js +51 -0
- package/dist/components/Tooltip/Tooltip.module.css.js +4 -0
- package/dist/components/Tooltip/activeTooltip.d.ts +4 -0
- package/dist/components/Tooltip/activeTooltip.js +10 -0
- package/dist/components/Tooltip/index.d.ts +1 -0
- package/dist/contrast-pairs.json +368 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +19 -0
- package/dist/lib/cx.d.ts +4 -0
- package/dist/lib/cx.js +6 -0
- package/dist/lib/position.d.ts +22 -0
- package/dist/lib/position.js +21 -0
- package/dist/lib/scrollLock.d.ts +13 -0
- package/dist/lib/scrollLock.js +22 -0
- package/dist/lib/useFloating.d.ts +25 -0
- package/dist/lib/useFloating.js +48 -0
- package/dist/lib/useModalDialog.d.ts +15 -0
- package/dist/lib/useModalDialog.js +42 -0
- package/dist/provider/FormaProvider.d.ts +17 -0
- package/dist/provider/FormaProvider.js +16 -0
- package/dist/provider/index.d.ts +3 -0
- package/dist/provider/strings.d.ts +14 -0
- package/dist/provider/strings.js +7 -0
- package/dist/provider/stringsContext.d.ts +6 -0
- package/dist/provider/stringsContext.js +6 -0
- package/dist/provider/useFormaStrings.d.ts +3 -0
- package/dist/provider/useFormaStrings.js +8 -0
- package/dist/styles.css +2 -0
- package/dist/theme/index.d.ts +5 -0
- package/dist/theme/themeScript.d.ts +21 -0
- package/dist/theme/themeScript.js +7 -0
- package/dist/theme/themeStore.d.ts +42 -0
- package/dist/theme/themeStore.js +55 -0
- package/dist/theme/useTheme.d.ts +21 -0
- package/dist/theme/useTheme.js +16 -0
- package/dist/tokens/contrast.d.ts +12 -0
- package/dist/tokens/contrast.js +25 -0
- package/dist/tokens/tokens.d.ts +2 -0
- package/dist/tokens/tokens.js +4 -0
- package/dist/tokens.css +156 -0
- package/dist/tokens.json +154 -0
- package/package.json +93 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @yelison/forma-ui
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d76f066: Publish the color pairs the package keeps readable, with their WCAG thresholds, as `@yelison/forma-ui/contrast-pairs.json`, so that a consumer can measure them against `tokens.json`.
|
|
8
|
+
- 35bef50: `Field` and `Input` take an `announce` prop: `'off'` shows the error without announcing it (for specimens and documentation, never for a form people fill in), and the default `'assertive'` keeps the `role="alert"` it always had.
|
|
9
|
+
- e06c795: First release of Forma UI: accessible React components and semantic design tokens, extracted from Resolve.
|
|
10
|
+
|
|
11
|
+
- **Components:** `Button`, `IconButton`, `Badge`, `Field`, `Input`, `Icon`, `Tooltip` and `Dialog` (also exported as `Modal`), with their props types.
|
|
12
|
+
- **Tokens:** the design tokens as CSS custom properties for the light, dark and system themes (`@yelison/forma-ui/tokens.css`), the resolved values (`tokens.json`), the `tokenNames` list and the `contrastRatio` helper.
|
|
13
|
+
- **Theme:** `createThemeStore`, `useTheme` and `themeScript` to own the light, dark or system preference and avoid a flash on first paint.
|
|
14
|
+
- **`FormaProvider`:** replaces the few strings the components show on their own, which default to English.
|
|
15
|
+
- **Distribution:** ES modules with declarations, three stylesheets (`tokens.css`, `styles.css`, `base.css`) and `react` and `react-dom` (`^19.2`) as peer dependencies.
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- ce8e511: Publish the library as one module per source file, so that a bundler keeps in each chunk only the components it uses. The public API, the stylesheets and their import paths do not change.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yelison Ortiz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# @yelison/forma-ui
|
|
2
|
+
|
|
3
|
+
Accessible React components and semantic design tokens, extracted from [Resolve](https://github.com/Yelison/resolve).
|
|
4
|
+
Light, dark and system themes, English text by default that you can replace, and native HTML semantics first.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm install @yelison/forma-ui react react-dom
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
React and React DOM are peer dependencies (`^19.2`): the package never bundles its own copy.
|
|
13
|
+
|
|
14
|
+
## Quick start
|
|
15
|
+
|
|
16
|
+
Import the three stylesheets once, in the entry point of your application, then use the components:
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import '@yelison/forma-ui/tokens.css'
|
|
20
|
+
import '@yelison/forma-ui/styles.css'
|
|
21
|
+
import '@yelison/forma-ui/base.css'
|
|
22
|
+
// then your own CSS
|
|
23
|
+
|
|
24
|
+
import { Button, FormaProvider } from '@yelison/forma-ui'
|
|
25
|
+
|
|
26
|
+
export function App() {
|
|
27
|
+
return (
|
|
28
|
+
<FormaProvider>
|
|
29
|
+
<Button variant="primary">Save</Button>
|
|
30
|
+
</FormaProvider>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## The CSS you import
|
|
36
|
+
|
|
37
|
+
The package ships its CSS as three files, in this order:
|
|
38
|
+
|
|
39
|
+
| Import | What it holds |
|
|
40
|
+
| ------------------------------ | ------------------------------------------------------------------------------------------- |
|
|
41
|
+
| `@yelison/forma-ui/tokens.css` | The design tokens as CSS custom properties, for the light, dark and system themes. |
|
|
42
|
+
| `@yelison/forma-ui/styles.css` | The rules of every component, in one file. |
|
|
43
|
+
| `@yelison/forma-ui/base.css` | Two utility classes, `.forma-visually-hidden` and `.forma-scroll-locked`, and nothing else. |
|
|
44
|
+
|
|
45
|
+
- The package's files go before your own CSS. A `className` you pass to a component has the same specificity as the
|
|
46
|
+
component's own rule (for example `.forma-badge__blue`), so the later stylesheet wins: yours has to come after
|
|
47
|
+
`styles.css`.
|
|
48
|
+
- `tokens.css` is the foundation: the component rules read its custom properties, and without it they render unstyled.
|
|
49
|
+
- Importing `@yelison/forma-ui` pulls in no CSS and has no side effects: the components are tree-shakeable (see
|
|
50
|
+
[Bundle size](#bundle-size)), and the stylesheet is yours to place. `styles.css` is one file for all the components, whether you use them all or not.
|
|
51
|
+
- `base.css` holds only plain `.forma-*` class selectors, so nothing in it can match your own markup. A modal dialog
|
|
52
|
+
applies `.forma-scroll-locked` to `<html>` while it is open: import `base.css` unless you use no such component.
|
|
53
|
+
- A consumer without React, such as an identity provider's login theme, imports `tokens.css` alone.
|
|
54
|
+
- `tokens.json` (`@yelison/forma-ui/tokens.json`) holds the resolved token values, and `tokenNames` lists the custom
|
|
55
|
+
properties.
|
|
56
|
+
- `contrast-pairs.json` (`@yelison/forma-ui/contrast-pairs.json`) lists the color pairs the package keeps readable: for
|
|
57
|
+
each one, the foreground and background tokens (without the `--color-` prefix), the kind of use (`text`, held to 4.5:1,
|
|
58
|
+
or `nonText`, held to 3:1) and the themes it is painted in, with the two thresholds. To check your own theme
|
|
59
|
+
overrides, measure each pair with `contrastRatio` over your values, taking from `tokens.json` the tokens you do not
|
|
60
|
+
override.
|
|
61
|
+
|
|
62
|
+
The component classes are named `forma-<module>__<class>` and are not a styling API: restyle through the tokens, and
|
|
63
|
+
pass your own `className` to a component.
|
|
64
|
+
|
|
65
|
+
## `FormaProvider` and the library's text
|
|
66
|
+
|
|
67
|
+
The few strings the components show on their own have English defaults. A `FormaProvider` replaces them for the part of
|
|
68
|
+
the application it wraps; entries you leave out keep the value of the closest provider above, or the default.
|
|
69
|
+
|
|
70
|
+
| Key | Default | Where it appears |
|
|
71
|
+
| --------------- | ---------- | --------------------------------------------------------------------------------- |
|
|
72
|
+
| `buttonLoading` | `Loading…` | The accessible name of a `Button` while it is `loading`. |
|
|
73
|
+
| `dialogClose` | `Close` | Returned by `useFormaStrings()`, for a close button you put in a `Dialog` footer. |
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
<FormaProvider strings={{ buttonLoading: 'Enviando…', dialogClose: 'Cerrar' }}>
|
|
77
|
+
<App />
|
|
78
|
+
</FormaProvider>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
A prop on a component wins over the provider (`<Button loading loadingLabel="Saving…">`), and the provider wins over the
|
|
82
|
+
default. The provider is optional for an application in English. Read the current strings in your own components with
|
|
83
|
+
`useFormaStrings()`.
|
|
84
|
+
|
|
85
|
+
## Theme
|
|
86
|
+
|
|
87
|
+
`tokens.css` paints the light theme, follows `prefers-color-scheme` until a theme is chosen, and switches by the
|
|
88
|
+
`data-theme` attribute on `<html>`: `light` or `dark` set it, and `system` removes it. The package gives you the pieces
|
|
89
|
+
to own that choice:
|
|
90
|
+
|
|
91
|
+
- `createThemeStore({ storageKey })` creates the store of one application: the preference (`light`, `dark` or
|
|
92
|
+
`system`), kept in `localStorage` under your key and in memory when storage is unavailable. Creating it does not touch
|
|
93
|
+
the page.
|
|
94
|
+
- `useTheme(store)` reads it in a component and returns `preference`, `resolved`, `setPreference` and `toggle`.
|
|
95
|
+
- `themeScript({ storageKey })` returns a string for an inline `<script>` in `<head>`, so that a reload does not flash
|
|
96
|
+
the theme the operating system prefers before React renders. It must use the same key as the store, and it must be in
|
|
97
|
+
the HTML that is served: a script that React renders does not run.
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
import { createThemeStore, themeScript, useTheme } from '@yelison/forma-ui'
|
|
101
|
+
|
|
102
|
+
const themeStore = createThemeStore({ storageKey: 'my-app-theme' })
|
|
103
|
+
|
|
104
|
+
// In the HTML your server or build sends, in <head>:
|
|
105
|
+
const firstPaint = `<script>${themeScript({ storageKey: 'my-app-theme' })}</script>`
|
|
106
|
+
|
|
107
|
+
function ThemeToggle() {
|
|
108
|
+
const { resolved, toggle } = useTheme(themeStore)
|
|
109
|
+
return <button onClick={toggle}>{resolved === 'dark' ? 'Light theme' : 'Dark theme'}</button>
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Components
|
|
114
|
+
|
|
115
|
+
`Badge`, `Button`, `IconButton`, `Icon` and `Input` forward the attributes of their native element. `Field`, `Tooltip` and `Dialog` take only the props they document.
|
|
116
|
+
|
|
117
|
+
| Component | Use it for | Accessibility |
|
|
118
|
+
| ----------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
119
|
+
| `Button` | An action; `primary`, `secondary`, `ghost` or `danger`. | A native `button` whose `type` is `button` unless set. While `loading` it stays focusable with `aria-busy` and `aria-disabled`; `disabled` is for an unavailable action. |
|
|
120
|
+
| `IconButton` | An action with an icon and no visible text. | `label` is required and becomes `aria-label`; show it in a `Tooltip` too. |
|
|
121
|
+
| `Badge` | A short status or category tag, in five tones. | The tone only colors it: the text has to say the same thing. |
|
|
122
|
+
| `Field` | A label, hint and error around a control you provide. | Links them with `id` and `aria-describedby`, sets `aria-invalid` and announces the error with `role="alert"`; `announce="off"` shows it without announcing it. |
|
|
123
|
+
| `Input` | A native text input with its `Field`. | Same linking as `Field`, and it forwards `announce` to it; `disabled` and `readOnly` stay different states. |
|
|
124
|
+
| `Icon` | One of the library's icons, as inline SVG. | Decorative and hidden from screen readers unless you give it a `label`. |
|
|
125
|
+
| `Tooltip` | A short description for a control. | Opens on hover and on keyboard focus, stays open while the pointer travels onto it, `Escape` closes it, and it describes the trigger with `aria-describedby`. |
|
|
126
|
+
| `Dialog` (also `Modal`) | A modal on the native `<dialog>`. | Opened with `showModal()`: focus stays inside and the page behind is inert. `Escape` and a backdrop click call `onClose`, and focus returns to the opener. |
|
|
127
|
+
|
|
128
|
+
Also exported: `buttonClassName` (the classes of a button, for a link that must look like one), `useScrollLock` (the
|
|
129
|
+
scroll lock of the dialog, for an overlay of your own), `useFormaStrings` and `defaultStrings` (the text the components show
|
|
130
|
+
on their own), `contrastRatio` and `relativeLuminance` (WCAG contrast, over `#rgb` and `#rrggbb` colors) and every props
|
|
131
|
+
type next to its component.
|
|
132
|
+
|
|
133
|
+
## Bundle size
|
|
134
|
+
|
|
135
|
+
The package is published as one ES module per source file (`dist/components/Badge/Badge.js`), and `dist/index.js` only
|
|
136
|
+
re-exports them. Your bundler keeps what you import, chunk by chunk: a dialog that loads on demand brings its own code
|
|
137
|
+
with it, and the entry chunk of your application does not carry the code of components it never shows on the first
|
|
138
|
+
screen.
|
|
139
|
+
|
|
140
|
+
- **`sideEffects`:** `package.json` declares `"sideEffects": ["*.css"]`. The JavaScript has no effect at import time, so
|
|
141
|
+
a bundler may drop any module you do not use; the three stylesheets are the only files it must keep, because importing
|
|
142
|
+
one is all they are for.
|
|
143
|
+
- **Styles:** unchanged. Importing the package still pulls in no CSS: `styles.css` is built beside the modules as one
|
|
144
|
+
file for every component, and you import it yourself, once, as shown in [The CSS you import](#the-css-you-import).
|
|
145
|
+
- **Imports:** from the package, `import { Dialog } from '@yelison/forma-ui'`. `exports` is the only way in, so the
|
|
146
|
+
layout of `dist/` can change without breaking you, and there are no per-component subpaths: a bundler that tree-shakes
|
|
147
|
+
already gets the same result from the package root, and each subpath would be a public name to keep.
|
|
148
|
+
|
|
149
|
+
`npm run pack:check` prints the current sizes, minified and gzipped, and fails when one grows past its budget; its output
|
|
150
|
+
is the source of truth. At the time of writing they are:
|
|
151
|
+
|
|
152
|
+
| What you import | Size |
|
|
153
|
+
| --------------- | ------- |
|
|
154
|
+
| Everything | 6.76 kB |
|
|
155
|
+
| `{ Button }` | 2.91 kB |
|
|
156
|
+
| `{ Badge }` | 0.23 kB |
|
|
157
|
+
| `styles.css` | 1.47 kB |
|
|
158
|
+
|
|
159
|
+
`Button` is not small because `Icon` looks its path up in one object that holds every icon, so the whole table travels
|
|
160
|
+
with any component that draws an icon.
|
|
161
|
+
|
|
162
|
+
## Compatibility
|
|
163
|
+
|
|
164
|
+
- **React:** 19.2 or a later 19.x release (`^19.2`), with `react-dom`.
|
|
165
|
+
- **Module format:** ES modules only. `import` it; `require()` of the entry point works too, on the Node versions the
|
|
166
|
+
package supports (22.12 or later), which load an ES module from CommonJS.
|
|
167
|
+
- **TypeScript:** declarations are included. Use `moduleResolution` `bundler`, `node16` or `nodenext`. `node10`
|
|
168
|
+
resolves the entry point and its types through `main` and `types`, but it ignores `exports`, so it cannot resolve the
|
|
169
|
+
CSS and JSON subpaths: import those through your bundler.
|
|
170
|
+
- **Node:** 22.12 or later (`engines`), for server rendering and tooling: it is what the package is tested on.
|
|
171
|
+
- **Browsers:** `Dialog` uses the native `<dialog>` element and `showModal()`.
|
|
172
|
+
|
|
173
|
+
## How the package is verified
|
|
174
|
+
|
|
175
|
+
The repository's CI checks the packed tarball, as a consumer receives it, not the sources.
|
|
176
|
+
|
|
177
|
+
`npm run check:consumer` packs the package, installs it into a throwaway project, compiles that project with
|
|
178
|
+
`moduleResolution: nodenext` and runs it. It fails if a CSS export does not resolve, if a module of `dist/` imports CSS, if a
|
|
179
|
+
class in `styles.css` lacks the `forma-` prefix, if a rendered component carries a class with no rule or if a CSS
|
|
180
|
+
module has no class rendered at all. The build lists the classes of every module in `dist/css-modules.json`, which is
|
|
181
|
+
not packed. A component that ships CSS is added to `scripts/consumer/main.tsx`.
|
|
182
|
+
|
|
183
|
+
`npm run pack:check` fails with a message that names the file or the rule:
|
|
184
|
+
|
|
185
|
+
- **Contents:** only `dist/**` (without `dist/css-modules.json`), `package.json`, `README.md`, `LICENSE` and, once it
|
|
186
|
+
exists, `CHANGELOG.md`; everything `main`, `types` and `exports` point at is in it; `react` and `react-dom` are peer
|
|
187
|
+
dependencies, never dependencies; `LICENSE` is the repository's. The list of `dist/` is closed by a rule, not by
|
|
188
|
+
hand: every module is imported from the entry point, every import resolves to a packed file, and the only other files
|
|
189
|
+
are the stylesheets, `tokens.json` and `contrast-pairs.json`. An orphan module, a test, a source map or a missing
|
|
190
|
+
import fails.
|
|
191
|
+
- **Types and exports:** [publint](https://publint.dev) in strict mode and
|
|
192
|
+
[Are The Types Wrong?](https://arethetypeswrong.github.io) read the packed `package.json` and resolve the typed
|
|
193
|
+
entry point in `node10`, `node16` (CommonJS and ESM) and `bundler`; the consumer is compiled under `bundler`; and
|
|
194
|
+
`require()` of the entry point must load on Node 22.12+.
|
|
195
|
+
- **One copy of React:** no packed module imports anything but `react`, `react-dom` and its own modules or holds React code, and
|
|
196
|
+
the production bundle of a Vite consumer (`scripts/consumer/client.tsx`) resolves `react`, `react-dom` and
|
|
197
|
+
`scheduler` to one folder each. React inlined into the library cannot be seen from the module graph alone, so the
|
|
198
|
+
first half reads every file.
|
|
199
|
+
- **Side effects:** `sideEffects` is a list of patterns that covers every stylesheet in `exports` and no JavaScript.
|
|
200
|
+
The promise behind it is checked too: with the field taken out of a copy of the packed `package.json`, so that the
|
|
201
|
+
bundler has to read every module to know whether it does anything, a bare `import '@yelison/forma-ui'` in a Vite
|
|
202
|
+
consumer (React left external, as in an application) renders no code of the package. A module that sets an attribute
|
|
203
|
+
or a global when it is imported fails there, naming the file. The three stylesheets each emit their CSS when imported.
|
|
204
|
+
- **Lazy chunks:** an application that uses `Button` and loads `Dialog` on demand has no `Dialog` code in its entry chunk,
|
|
205
|
+
and the chunk loaded on demand has it. A package that reached the bundler as one module fails this, because the entry
|
|
206
|
+
chunk would carry everything the other chunks use. The entry chunk has a weight budget too
|
|
207
|
+
(`scripts/pack-check/lazy-chunk.ts`).
|
|
208
|
+
- **Size budget:** what a Vite consumer's bundler makes of the tarball, bundled as an application, minified and
|
|
209
|
+
gzipped (level 9), with React left out: every export, `import { Button }` alone, `import { Badge }` alone (the
|
|
210
|
+
smallest component, with no icon, so that what `Button` carries cannot hide a regression in it) and
|
|
211
|
+
`dist/styles.css`. Each budget is the size measured plus about 20% and lives in
|
|
212
|
+
`scripts/pack-check/size-budget.ts`; the message names the budget, the size and the excess.
|
|
213
|
+
|
|
214
|
+
## Versions and publishing
|
|
215
|
+
|
|
216
|
+
The package follows [Semantic Versioning](https://semver.org), from `0.1.0`. While the version is below 1.0, a minor
|
|
217
|
+
release may include breaking changes, and each one is marked **Breaking** in the
|
|
218
|
+
[changelog](https://github.com/Yelison/forma-ui/blob/main/packages/forma-ui/CHANGELOG.md): pin an exact version, and
|
|
219
|
+
read the changelog before you update.
|
|
220
|
+
|
|
221
|
+
How a release is made, in short; [CONTRIBUTING.md](https://github.com/Yelison/forma-ui/blob/main/CONTRIBUTING.md) has
|
|
222
|
+
the steps, the recovery and the first publication:
|
|
223
|
+
|
|
224
|
+
- **Versioning:** each pull request that changes what the package ships adds a [Changesets](https://github.com/changesets/changesets)
|
|
225
|
+
file, and the `Changeset` job of CI fails one that does not. A regular pull request runs `npm run version-packages`,
|
|
226
|
+
which bumps the version, writes the changelog and consumes the changesets.
|
|
227
|
+
- **Who publishes:** only the `Release` workflow, when that pull request is merged into `main` and npm does not have
|
|
228
|
+
the version yet. It authenticates with npm trusted publishing (OIDC) and no token is stored. Nobody publishes from a
|
|
229
|
+
workstation, and `publishConfig.provenance` makes `npm publish` fail anywhere else.
|
|
230
|
+
- **Provenance:** every release is published with [provenance](https://docs.npmjs.com/generating-provenance-statements),
|
|
231
|
+
a signed statement that the tarball was built from this repository by that workflow run. The package page on npm shows
|
|
232
|
+
it, with a link to the run.
|
|
233
|
+
- **Verify it:** in a project that installs the package, `npm audit signatures` checks the registry signature and the
|
|
234
|
+
provenance attestation of every installed package that has one.
|
|
235
|
+
- **Rebuild it:** `npm run pack:reproducible` builds a commit twice from clean checkouts and fails unless both tarballs
|
|
236
|
+
have the same sha256. To compare a release with its source, check out the tag `@yelison/forma-ui@<version>`, use the
|
|
237
|
+
Node version in `.nvmrc` (other npm versions may pack the same files into different bytes), run `npm ci`,
|
|
238
|
+
`npm run build -w @yelison/forma-ui` and `npm pack -w @yelison/forma-ui`, and compare the tarball's SHA-512
|
|
239
|
+
(`openssl dgst -sha512 -binary <file>.tgz | base64`) with `npm view @yelison/forma-ui@<version> dist.integrity`, which
|
|
240
|
+
holds it after `sha512-`.
|
|
241
|
+
|
|
242
|
+
## License
|
|
243
|
+
|
|
244
|
+
[MIT](https://github.com/Yelison/forma-ui/blob/main/LICENSE)
|
package/dist/base.css
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The utility classes that the library and its consumers apply by name.
|
|
3
|
+
*
|
|
4
|
+
* The application imports this file, so it holds plain `.forma-*` class selectors and nothing else: an element,
|
|
5
|
+
* universal or pseudo-class selector here would reach into the consumer's own markup (base.css.test.ts fails on any
|
|
6
|
+
* other selector). Resets, focus rings and motion rules live in each component's module, or stay in the consumer.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/* Hides content visually and keeps it in the accessibility tree, for labels that a screen reader announces. */
|
|
10
|
+
.forma-visually-hidden {
|
|
11
|
+
position: absolute;
|
|
12
|
+
width: 1px;
|
|
13
|
+
height: 1px;
|
|
14
|
+
margin: -1px;
|
|
15
|
+
padding: 0;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
clip-path: inset(50%);
|
|
18
|
+
white-space: nowrap;
|
|
19
|
+
border: 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Set on <html> by the scroll lock (src/lib/scrollLock.ts) while a modal overlay is open. */
|
|
23
|
+
.forma-scroll-locked {
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
/** The color roles a badge can take. */
|
|
3
|
+
export type BadgeTone = 'blue' | 'green' | 'amber' | 'red' | 'neutral';
|
|
4
|
+
/** Props of {@link Badge}: the native `span` attributes plus the tone. */
|
|
5
|
+
export interface BadgeProps extends ComponentProps<'span'> {
|
|
6
|
+
/** Color of the badge. Defaults to `neutral`. */
|
|
7
|
+
tone?: BadgeTone;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A short label that tags an item with a status or category.
|
|
11
|
+
*
|
|
12
|
+
* The tone only colors the badge: its text must say the same thing, because color alone does not convey meaning.
|
|
13
|
+
*/
|
|
14
|
+
export declare function Badge({ tone, className, ...props }: BadgeProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { cx as e } from "../../lib/cx.js";
|
|
2
|
+
import t from "./Badge.module.css.js";
|
|
3
|
+
import { jsx as n } from "react/jsx-runtime";
|
|
4
|
+
//#region src/components/Badge/Badge.tsx
|
|
5
|
+
function r({ tone: r = "neutral", className: i, ...a }) {
|
|
6
|
+
return /* @__PURE__ */ n("span", {
|
|
7
|
+
className: e(t.badge, t[r], i),
|
|
8
|
+
...a
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { r as Badge };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/components/Badge/Badge.module.css
|
|
2
|
+
var e = "forma-badge", t = "forma-badge__blue", n = "forma-badge__green", r = "forma-badge__amber", i = "forma-badge__red", a = "forma-badge__neutral", o = {
|
|
3
|
+
badge: e,
|
|
4
|
+
blue: t,
|
|
5
|
+
green: n,
|
|
6
|
+
amber: r,
|
|
7
|
+
red: i,
|
|
8
|
+
neutral: a
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
|
11
|
+
export { r as amber, e as badge, t as blue, o as default, n as green, a as neutral, i as red };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Badge, type BadgeProps, type BadgeTone } from './Badge.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
2
|
+
import { type IconName } from '../Icon/index.js';
|
|
3
|
+
import { type ButtonStyleOptions } from './buttonClassName.js';
|
|
4
|
+
/** Props of {@link Button}: the native `button` attributes plus the style options and the loading state. */
|
|
5
|
+
export interface ButtonProps extends ComponentProps<'button'>, ButtonStyleOptions {
|
|
6
|
+
/** Icon shown before the content. A spinner takes its place while `loading`. */
|
|
7
|
+
icon?: IconName;
|
|
8
|
+
/** Ignores clicks and shows `loadingLabel` in place of the content while an action runs. Defaults to `false`. */
|
|
9
|
+
loading?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Content that replaces the button's own while `loading`, and so its accessible name. Without it the button shows
|
|
12
|
+
* the `buttonLoading` string of the closest `FormaProvider`, or `Loading…` when there is none.
|
|
13
|
+
*/
|
|
14
|
+
loadingLabel?: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* A button for an action. `type` is `button` unless set, so a button inside a form does not submit it by accident.
|
|
18
|
+
*
|
|
19
|
+
* While `loading` the button stays focusable: it is `aria-disabled` and `aria-busy`, not `disabled`, because a
|
|
20
|
+
* disabled button drops out of the tab order and a keyboard user would lose their place. Use `disabled` for an action
|
|
21
|
+
* that is not available at all.
|
|
22
|
+
*/
|
|
23
|
+
export declare function Button({ variant, block, className, icon, loading, loadingLabel, type, onClick, children, ...props }: ButtonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useFormaStrings as e } from "../../provider/useFormaStrings.js";
|
|
2
|
+
import { Icon as t } from "../Icon/Icon.js";
|
|
3
|
+
import n from "./Button.module.css.js";
|
|
4
|
+
import { buttonClassName as r } from "./buttonClassName.js";
|
|
5
|
+
import { jsx as i, jsxs as a } from "react/jsx-runtime";
|
|
6
|
+
//#region src/components/Button/Button.tsx
|
|
7
|
+
function o({ variant: o, block: s, className: c, icon: l, loading: u = !1, loadingLabel: d, type: f = "button", onClick: p, children: m, ...h }) {
|
|
8
|
+
let { buttonLoading: g } = e();
|
|
9
|
+
function _(e) {
|
|
10
|
+
if (u) {
|
|
11
|
+
e.preventDefault();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
p?.(e);
|
|
15
|
+
}
|
|
16
|
+
return /* @__PURE__ */ a("button", {
|
|
17
|
+
...h,
|
|
18
|
+
type: f,
|
|
19
|
+
className: r({
|
|
20
|
+
variant: o,
|
|
21
|
+
block: s,
|
|
22
|
+
className: c
|
|
23
|
+
}),
|
|
24
|
+
"aria-busy": u || h["aria-busy"],
|
|
25
|
+
"aria-disabled": u || h["aria-disabled"],
|
|
26
|
+
onClick: _,
|
|
27
|
+
children: [u ? /* @__PURE__ */ i("span", {
|
|
28
|
+
className: n.spinner,
|
|
29
|
+
"aria-hidden": "true"
|
|
30
|
+
}) : l && /* @__PURE__ */ i(t, { name: l }), u ? d ?? g : m]
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { o as Button };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/components/Button/Button.module.css
|
|
2
|
+
var e = "forma-button", t = "forma-button__block", n = "forma-button__primary", r = "forma-button__secondary", i = "forma-button__ghost", a = "forma-button__danger", o = "forma-button__spinner", s = "forma-button__spin", c = {
|
|
3
|
+
button: e,
|
|
4
|
+
block: t,
|
|
5
|
+
primary: n,
|
|
6
|
+
secondary: r,
|
|
7
|
+
ghost: i,
|
|
8
|
+
danger: a,
|
|
9
|
+
spinner: o,
|
|
10
|
+
spin: s
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { t as block, e as button, a as danger, c as default, i as ghost, n as primary, r as secondary, s as spin, o as spinner };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ComponentProps } from 'react';
|
|
2
|
+
import { type IconName } from '../Icon/index.js';
|
|
3
|
+
/** Props of {@link IconButton}: the native `button` attributes, without `children`, plus the icon and its name. */
|
|
4
|
+
export interface IconButtonProps extends Omit<ComponentProps<'button'>, 'children'> {
|
|
5
|
+
/** The icon to draw, or several to draw side by side, overlapping, as one glyph, such as a double arrow. */
|
|
6
|
+
icon: IconName | readonly IconName[];
|
|
7
|
+
/** Mirrors the icon horizontally, so one drawing serves two directions. Defaults to `false`. */
|
|
8
|
+
flip?: boolean;
|
|
9
|
+
/** The accessible name. Required: the button has no visible text, so without it a screen reader has nothing to say. */
|
|
10
|
+
label: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A square button that holds only an icon. `type` is `button` unless set.
|
|
14
|
+
*
|
|
15
|
+
* `label` names the button for screen readers; show it as a tooltip too, so that sighted users can read it.
|
|
16
|
+
*/
|
|
17
|
+
export declare function IconButton({ icon, flip, label, className, type, ...props }: IconButtonProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cx as e } from "../../lib/cx.js";
|
|
2
|
+
import { Icon as t } from "../Icon/Icon.js";
|
|
3
|
+
import n from "./IconButton.module.css.js";
|
|
4
|
+
import { jsx as r } from "react/jsx-runtime";
|
|
5
|
+
//#region src/components/Button/IconButton.tsx
|
|
6
|
+
function i({ icon: i, flip: a = !1, label: o, className: s, type: c = "button", ...l }) {
|
|
7
|
+
let u = typeof i == "string" ? [i] : i;
|
|
8
|
+
return /* @__PURE__ */ r("button", {
|
|
9
|
+
...l,
|
|
10
|
+
type: c,
|
|
11
|
+
"aria-label": o,
|
|
12
|
+
className: e(n.iconButton, s),
|
|
13
|
+
children: /* @__PURE__ */ r("span", {
|
|
14
|
+
className: e(n.icons, a && n.flip),
|
|
15
|
+
children: u.map((e, n) => /* @__PURE__ */ r(t, { name: e }, n))
|
|
16
|
+
})
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { i as IconButton };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/components/Button/IconButton.module.css
|
|
2
|
+
var e = "forma-button-icon-button", t = "forma-button-icon-button__icons", n = "forma-button-icon-button__flip", r = {
|
|
3
|
+
iconButton: e,
|
|
4
|
+
icons: t,
|
|
5
|
+
flip: n
|
|
6
|
+
};
|
|
7
|
+
//#endregion
|
|
8
|
+
export { r as default, n as flip, e as iconButton, t as icons };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** The visual weights a button can take. */
|
|
2
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
|
|
3
|
+
/** The options that decide how a button looks, shared by `Button` and by `buttonClassName`. */
|
|
4
|
+
export interface ButtonStyleOptions {
|
|
5
|
+
/** Visual weight of the button. Defaults to `primary`. */
|
|
6
|
+
variant?: ButtonVariant;
|
|
7
|
+
/** Makes the button fill the width of its container. Defaults to `false`. */
|
|
8
|
+
block?: boolean;
|
|
9
|
+
/** Extra class names, added after the library's own. */
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Returns the class names of a button, for an element that is not a `Button` but must look like one, such as a link.
|
|
14
|
+
*
|
|
15
|
+
* The result only styles the element: a link keeps its own semantics and its own keyboard behavior.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buttonClassName({ variant, block, className }?: ButtonStyleOptions): string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { cx as e } from "../../lib/cx.js";
|
|
2
|
+
import t from "./Button.module.css.js";
|
|
3
|
+
//#region src/components/Button/buttonClassName.ts
|
|
4
|
+
function n({ variant: n = "primary", block: r = !1, className: i } = {}) {
|
|
5
|
+
return e(t.button, t[n], r && t.block, i);
|
|
6
|
+
}
|
|
7
|
+
//#endregion
|
|
8
|
+
export { n as buttonClassName };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/** Props of {@link Dialog}. */
|
|
3
|
+
export interface DialogProps {
|
|
4
|
+
/** Whether the dialog is open. */
|
|
5
|
+
open: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Called when the user asks to close the dialog: `Escape`, a click on the backdrop or a native close such as a
|
|
8
|
+
* `method="dialog"` form. Set `open` to `false` in it: the dialog stays open until the parent decides.
|
|
9
|
+
* It is not called when the parent closes the dialog by setting `open` itself.
|
|
10
|
+
*/
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
/** The dialog's title and its accessible name. */
|
|
13
|
+
title: ReactNode;
|
|
14
|
+
/** Text under the title. It becomes the dialog's accessible description. */
|
|
15
|
+
description?: ReactNode;
|
|
16
|
+
/**
|
|
17
|
+
* The actions at the bottom, usually a secondary and a primary `Button`. Put the way to dismiss the dialog here: a
|
|
18
|
+
* `Button` labeled with `useFormaStrings().dialogClose` follows the language of the closest `FormaProvider`.
|
|
19
|
+
*/
|
|
20
|
+
footer?: ReactNode;
|
|
21
|
+
/** Width of the dialog: `--dialog-width` for `default` and `--dialog-width-wide` for `wide`. Defaults to `default`. */
|
|
22
|
+
size?: 'default' | 'wide';
|
|
23
|
+
/** Extra class names for the `<dialog>`, added after the library's own. */
|
|
24
|
+
className?: string;
|
|
25
|
+
/** The content between the description and the footer. It is rendered only while the dialog is open. */
|
|
26
|
+
children?: ReactNode;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A modal dialog on the native `<dialog>`: it opens with `showModal()`, so the focus stays inside it and the page
|
|
30
|
+
* behind is inert. `Escape` and a click on the backdrop call `onClose`, and the focus goes back to the element that
|
|
31
|
+
* had it when the dialog opened. While it is open the page scroll is locked, which needs `@yelison/forma-ui/base.css`.
|
|
32
|
+
*
|
|
33
|
+
* A click on the backdrop closes the dialog only if the pointer was pressed and released on the backdrop, so a drag
|
|
34
|
+
* that ends there, such as the end of a text selection, does not. If the element that had the focus is no longer in the
|
|
35
|
+
* document when the dialog closes, such as the item of a menu that unmounted, nothing takes the focus back and it
|
|
36
|
+
* falls to the page. Place it from an effect of the parent that runs when `open` becomes false: it runs after the
|
|
37
|
+
* dialog has closed, when the page is no longer inert. Do it in `onClose` and the page is still inert.
|
|
38
|
+
*
|
|
39
|
+
* The entrance moves the dialog for 200 ms, and a floating element is placed against where its anchor is when it opens.
|
|
40
|
+
* A `Tooltip` on the control that takes the initial focus opens in that time and ends a few pixels off its trigger:
|
|
41
|
+
* put another control first, or give the dialog `animation: none` through `className`. With `prefers-reduced-motion`
|
|
42
|
+
* the dialog does not move.
|
|
43
|
+
*
|
|
44
|
+
* The dialog is always in the document and its content only while `open`, so a closed dialog renders no children and
|
|
45
|
+
* a form inside starts empty each time.
|
|
46
|
+
*/
|
|
47
|
+
export declare function Dialog({ open, onClose, title, description, footer, size, className, children, }: DialogProps): import("react").JSX.Element;
|
|
48
|
+
/** The name Resolve gave the dialog. The same component, so that Resolve adopts the package without renaming. */
|
|
49
|
+
export declare const Modal: typeof Dialog;
|
|
50
|
+
/** Props of {@link Modal}: the same type as {@link DialogProps}. */
|
|
51
|
+
export type ModalProps = DialogProps;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { cx as e } from "../../lib/cx.js";
|
|
2
|
+
import { useModalDialog as t } from "../../lib/useModalDialog.js";
|
|
3
|
+
import n from "./Dialog.module.css.js";
|
|
4
|
+
import { useId as r } from "react";
|
|
5
|
+
import { jsx as i, jsxs as a } from "react/jsx-runtime";
|
|
6
|
+
//#region src/components/Dialog/Dialog.tsx
|
|
7
|
+
function o({ open: o, onClose: s, title: c, description: l, footer: u, size: d = "default", className: f, children: p }) {
|
|
8
|
+
let m = r(), h = r(), g = t(o, s);
|
|
9
|
+
return /* @__PURE__ */ i("dialog", {
|
|
10
|
+
...g,
|
|
11
|
+
className: e(n.dialog, d === "wide" && n.wide, f),
|
|
12
|
+
"aria-labelledby": m,
|
|
13
|
+
"aria-describedby": l ? h : void 0,
|
|
14
|
+
children: o && /* @__PURE__ */ a("div", {
|
|
15
|
+
className: n.content,
|
|
16
|
+
children: [
|
|
17
|
+
/* @__PURE__ */ i("h2", {
|
|
18
|
+
id: m,
|
|
19
|
+
className: n.title,
|
|
20
|
+
children: c
|
|
21
|
+
}),
|
|
22
|
+
l && /* @__PURE__ */ i("p", {
|
|
23
|
+
id: h,
|
|
24
|
+
className: n.description,
|
|
25
|
+
children: l
|
|
26
|
+
}),
|
|
27
|
+
p,
|
|
28
|
+
u && /* @__PURE__ */ i("div", {
|
|
29
|
+
className: n.footer,
|
|
30
|
+
children: u
|
|
31
|
+
})
|
|
32
|
+
]
|
|
33
|
+
})
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
var s = o;
|
|
37
|
+
//#endregion
|
|
38
|
+
export { o as Dialog, s as Modal };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/components/Dialog/Dialog.module.css
|
|
2
|
+
var e = "forma-dialog", t = "forma-dialog__enter", n = "forma-dialog__wide", r = "forma-dialog__content", i = "forma-dialog__title", a = "forma-dialog__description", o = "forma-dialog__footer", s = {
|
|
3
|
+
dialog: e,
|
|
4
|
+
enter: t,
|
|
5
|
+
wide: n,
|
|
6
|
+
content: r,
|
|
7
|
+
title: i,
|
|
8
|
+
description: a,
|
|
9
|
+
footer: o
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
export { r as content, s as default, a as description, e as dialog, t as enter, o as footer, i as title, n as wide };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Dialog, Modal, type DialogProps, type ModalProps } from './Dialog.js';
|