@yelison/forma-ui 0.0.0-stage → 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.
Files changed (77) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +243 -2
  4. package/dist/base.css +25 -0
  5. package/dist/components/Badge/Badge.d.ts +14 -0
  6. package/dist/components/Badge/Badge.js +12 -0
  7. package/dist/components/Badge/Badge.module.css.js +11 -0
  8. package/dist/components/Badge/index.d.ts +1 -0
  9. package/dist/components/Button/Button.d.ts +23 -0
  10. package/dist/components/Button/Button.js +34 -0
  11. package/dist/components/Button/Button.module.css.js +13 -0
  12. package/dist/components/Button/IconButton.d.ts +17 -0
  13. package/dist/components/Button/IconButton.js +20 -0
  14. package/dist/components/Button/IconButton.module.css.js +8 -0
  15. package/dist/components/Button/buttonClassName.d.ts +17 -0
  16. package/dist/components/Button/buttonClassName.js +8 -0
  17. package/dist/components/Button/index.d.ts +3 -0
  18. package/dist/components/Dialog/Dialog.d.ts +51 -0
  19. package/dist/components/Dialog/Dialog.js +38 -0
  20. package/dist/components/Dialog/Dialog.module.css.js +12 -0
  21. package/dist/components/Dialog/index.d.ts +1 -0
  22. package/dist/components/Field/Field.d.ts +42 -0
  23. package/dist/components/Field/Field.js +40 -0
  24. package/dist/components/Field/Field.module.css.js +9 -0
  25. package/dist/components/Field/control.module.css.js +4 -0
  26. package/dist/components/Field/index.d.ts +1 -0
  27. package/dist/components/Icon/Icon.d.ts +19 -0
  28. package/dist/components/Icon/Icon.js +27 -0
  29. package/dist/components/Icon/index.d.ts +1 -0
  30. package/dist/components/Icon/paths.d.ts +26 -0
  31. package/dist/components/Icon/paths.js +32 -0
  32. package/dist/components/Input/Input.d.ts +24 -0
  33. package/dist/components/Input/Input.js +23 -0
  34. package/dist/components/Input/index.d.ts +1 -0
  35. package/dist/components/Tooltip/Tooltip.d.ts +65 -0
  36. package/dist/components/Tooltip/Tooltip.js +51 -0
  37. package/dist/components/Tooltip/Tooltip.module.css.js +4 -0
  38. package/dist/components/Tooltip/activeTooltip.d.ts +4 -0
  39. package/dist/components/Tooltip/activeTooltip.js +10 -0
  40. package/dist/components/Tooltip/index.d.ts +1 -0
  41. package/dist/contrast-pairs.json +368 -0
  42. package/dist/index.d.ts +13 -0
  43. package/dist/index.js +19 -0
  44. package/dist/lib/cx.d.ts +4 -0
  45. package/dist/lib/cx.js +6 -0
  46. package/dist/lib/position.d.ts +22 -0
  47. package/dist/lib/position.js +21 -0
  48. package/dist/lib/scrollLock.d.ts +13 -0
  49. package/dist/lib/scrollLock.js +22 -0
  50. package/dist/lib/useFloating.d.ts +25 -0
  51. package/dist/lib/useFloating.js +48 -0
  52. package/dist/lib/useModalDialog.d.ts +15 -0
  53. package/dist/lib/useModalDialog.js +42 -0
  54. package/dist/provider/FormaProvider.d.ts +17 -0
  55. package/dist/provider/FormaProvider.js +16 -0
  56. package/dist/provider/index.d.ts +3 -0
  57. package/dist/provider/strings.d.ts +14 -0
  58. package/dist/provider/strings.js +7 -0
  59. package/dist/provider/stringsContext.d.ts +6 -0
  60. package/dist/provider/stringsContext.js +6 -0
  61. package/dist/provider/useFormaStrings.d.ts +3 -0
  62. package/dist/provider/useFormaStrings.js +8 -0
  63. package/dist/styles.css +2 -0
  64. package/dist/theme/index.d.ts +5 -0
  65. package/dist/theme/themeScript.d.ts +21 -0
  66. package/dist/theme/themeScript.js +7 -0
  67. package/dist/theme/themeStore.d.ts +42 -0
  68. package/dist/theme/themeStore.js +55 -0
  69. package/dist/theme/useTheme.d.ts +21 -0
  70. package/dist/theme/useTheme.js +16 -0
  71. package/dist/tokens/contrast.d.ts +12 -0
  72. package/dist/tokens/contrast.js +25 -0
  73. package/dist/tokens/tokens.d.ts +2 -0
  74. package/dist/tokens/tokens.js +4 -0
  75. package/dist/tokens.css +156 -0
  76. package/dist/tokens.json +154 -0
  77. package/package.json +95 -4
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 CHANGED
@@ -1,3 +1,244 @@
1
- # Temporary Holding Version
1
+ # @yelison/forma-ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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,3 @@
1
+ export { Button, type ButtonProps } from './Button.js';
2
+ export { IconButton, type IconButtonProps } from './IconButton.js';
3
+ export { buttonClassName, type ButtonStyleOptions, type ButtonVariant } from './buttonClassName.js';
@@ -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';