rei-kit 0.4.4 → 0.5.1

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/README.md CHANGED
@@ -1,22 +1,47 @@
1
1
  # rei-kit
2
2
 
3
- Vue 3 and Tailwind 4 design system and shared runtime. Extracted from
4
- [Hibi](https://github.com/ramazandogna/hibi).
3
+ Vue 3 and Tailwind 4 design system and shared runtime.
5
4
 
6
5
  零 — the layer everything else starts from.
7
6
 
8
7
  ## Why it exists
9
8
 
10
- Hibi's shared layer turned out to be genuinely portable: no file under
11
- `shared/` imported from `features/`, so the components, the date and theme
12
- helpers, and the i18n runtime could leave without being rewritten. This is that
13
- layer, packaged so the next app installs it instead of copying it.
9
+ **The kit distributes decisions, not a look.** Three apps run on it and no two
10
+ resemble each other: a phone journal, a phone ledger, and a wide Japanese
11
+ course site. None of them forked it, because there is not a single hex value
12
+ inside a component. Colours are named for the role they play, and an app
13
+ rebrands by redefining eleven values.
14
+
15
+ That is the whole trick, and everything else follows from it.
16
+
17
+ ### What gets to be in here
18
+
19
+ Three things have to be true at once:
20
+
21
+ 1. **Two apps genuinely need it** — measured, not predicted. A component one
22
+ app needs stays in that app however general it looks.
23
+ 2. **It adds no required peer.** Anything that needs a new library goes behind
24
+ its own entry point, or stays in the app.
25
+ 3. **It contains no product decision** — no colour, no copy, no icon. Those
26
+ arrive as props and slots.
27
+
28
+ The corollary is the useful one: **a file that is 90% identical in two apps is
29
+ a kit candidate.** The measure is `diff`, not taste.
14
30
 
15
31
  ## Status
16
32
 
17
- **v0.0.0extraction complete, no consumer yet.** Thirteen components,
18
- eight utilities, six composables, a generic i18n runtime and an optional
19
- Supabase entry. Hibi moves onto it next.
33
+ **v0.4.4three consumers.**
34
+
35
+ | | |
36
+ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
37
+ | Components | 22 |
38
+ | Composables | 11 |
39
+ | Utilities | 20 |
40
+ | Also | a generic i18n runtime, an optional Supabase entry |
41
+ | Consumers | [Hibi](https://github.com/ramazandogna/hibi) · [Kakei](https://github.com/ramazandogna/kakei) · [Kakehashi](https://github.com/ramazandogna/kakehashi) |
42
+
43
+ Every export is listed by name in `src/__tests__/public-api.spec.ts`, which is
44
+ the package's promise written down.
20
45
 
21
46
  ## Install
22
47
 
@@ -37,7 +62,7 @@ is the only copy:
37
62
  | `@supabase/supabase-js` | the `rei-kit/supabase` entry only |
38
63
 
39
64
  This is not a formality. A second copy of a library that works through
40
- provide/inject is not a spare copy -- it is a different injection key, so the
65
+ provide/inject is not a spare copy it is a different injection key, so the
41
66
  app's own provider becomes invisible and the component throws on mount.
42
67
 
43
68
  ## Use
@@ -59,12 +84,12 @@ import { createSupabaseClient } from 'rei-kit/supabase'
59
84
 
60
85
  ### Wiring the styles
61
86
 
62
- Three lines, and all three are load-bearing:
87
+ Four lines, and all four are load-bearing:
63
88
 
64
89
  ```css
65
90
  /* your app's main.css */
66
91
  @import 'tailwindcss';
67
- @import 'rei-kit/tokens.css'; /* colour roles, the dark variant, utilities */
92
+ @import 'rei-kit/tokens.css'; /* colour roles, the dark variant, measures, utilities */
68
93
  @import 'rei-kit/styles.css'; /* compiled component styles */
69
94
 
70
95
  /* Tailwind generates a utility only where it has seen the class, and it does
@@ -76,27 +101,19 @@ Three lines, and all three are load-bearing:
76
101
  The path is relative to the CSS file, so adjust the `../` depth to where your
77
102
  `main.css` sits.
78
103
 
79
- Leaving any of the three out fails quietly: the build succeeds, the components
80
- mount, and they come out unstyled. Nothing type-checks this, so it is worth a
81
- test -- see _Not breaking the apps that use it_.
82
-
83
- ### Prerendering
104
+ **Leaving any of the four out fails quietly:** the build succeeds, the
105
+ components mount, and they come out unstyled. Nothing type-checks this, so it
106
+ is worth a test Hibi's `kit-styling.spec.ts` reads its own stylesheet and
107
+ asserts all four, at unit-test speed. Copy it.
84
108
 
85
- The kit imports and renders on a server, so an app can prerender with
86
- `vite-ssg` or any other SSR build. Two things stay the app's job, because only
87
- the app knows the answer:
88
-
89
- - **The theme.** `applyTheme` does nothing without a document, so prerendered
90
- HTML carries no `.dark`. Set it before hydration with a small synchronous
91
- script in `index.html`, or the first paint flashes light.
92
- - **Today's date.** `useToday()` on a server is the *server's* today, a
93
- different day from the visitor's either side of midnight. Render anything
94
- derived from it on the client.
109
+ That test exists because the failure is real: Hibi shipped with the tab bar
110
+ invisible once, and separately spent three versions restating `tokens.css`
111
+ locally instead of importing it, which nothing noticed.
95
112
 
96
113
  ### Colours
97
114
 
98
- `tokens.css` defines all eleven roles, a `.dark` block for each surface, and the
99
- `dark` variant. A new app rebrands by overriding values, never by renaming:
115
+ `tokens.css` defines all eleven roles, a `.dark` block for each surface, and
116
+ the `dark` variant. A new app rebrands by overriding values, never by renaming:
100
117
 
101
118
  ```css
102
119
  @theme {
@@ -125,8 +142,52 @@ the app's own dark-mode overrides carry into the kit's components:
125
142
  }
126
143
  ```
127
144
 
128
- Tokens are named for what they do, not for the app they came from, so a new
129
- product changes values rather than renaming anything.
145
+ Put your own `.dark` block _after_ the import. Both blocks match at the same
146
+ specificity, so the later one wins — which is what makes the override work.
147
+
148
+ ### Measures
149
+
150
+ Widths are roles too. `PageContainer` shipped with `75rem` baked in, and the
151
+ first app that wanted it had deliberately measured its page at 1120px, so the
152
+ component written to remove that app's hand-rolled container could not replace
153
+ it — the same mistake as a hex inside a component, one axis over.
154
+
155
+ ```css
156
+ @theme {
157
+ --measure-page: 1120px; /* a page */
158
+ --measure-reading: 68ch; /* a column of prose */
159
+ }
160
+ ```
161
+
162
+ ### The phone shell
163
+
164
+ `tokens.css` also carries the geometry of a phone shell — `shell-frame`,
165
+ `page-slide`, `page-auth` and the slide transitions between screens. All of it
166
+ is opt-in: nothing applies unless you put the class on an element, so a wide
167
+ app can ignore it. Two of the three consumers use it.
168
+
169
+ ### Prerendering
170
+
171
+ The kit imports and renders on a server, so an app can prerender with
172
+ `vite-ssg` or any other SSR build. Two things stay the app's job, because only
173
+ the app knows the answer:
174
+
175
+ - **The theme.** `applyTheme` does nothing without a document, so prerendered
176
+ HTML carries no `.dark`. Set it before hydration with a small synchronous
177
+ script in `index.html`, or the first paint flashes light.
178
+ - **Today's date.** `useToday()` on a server is the _server's_ today, a
179
+ different day from the visitor's either side of midnight. Render anything
180
+ derived from it on the client.
181
+
182
+ ## Seeing what is in it
183
+
184
+ ```sh
185
+ pnpm showcase
186
+ ```
187
+
188
+ A page that wires the kit exactly the way this README says to, so a broken
189
+ install shows up there before it ships. A component nobody can see is a
190
+ component nobody uses.
130
191
 
131
192
  ## Commands
132
193
 
@@ -135,26 +196,34 @@ product changes values rather than renaming anything.
135
196
  | `pnpm dev` | Rebuild on change, for use with a linked app |
136
197
  | `pnpm build` | Type-check, then build |
137
198
  | `pnpm check` | Everything CI runs: format, lint, types, tests, build |
199
+ | `pnpm showcase` | The showcase, in dev mode |
138
200
  | `pnpm test:unit` | Vitest, watch mode |
139
201
  | `pnpm lint` | oxlint + ESLint, with `--fix` |
140
202
 
141
203
  ## Not breaking the apps that use it
142
204
 
143
- Three layers, cheapest first.
205
+ Four layers, cheapest first.
144
206
 
145
- **Pinned ranges.** A consumer depends on `^0.1.0`, which at 0.x means
146
- `>=0.1.0 <0.2.0` — publishing 0.2.0 upgrades nobody. Apps move on their own
147
- schedule, and a release can never reach an app that has not asked for it.
207
+ **Pinned ranges.** A consumer depends on `^0.4.0`, which at 0.x means
208
+ `>=0.4.0 <0.5.0` — publishing 0.5.0 upgrades nobody. Apps move on their own
209
+ schedule, and a release can never reach an app that has not asked for it. The
210
+ cost is the mirror image: an app that never asks never moves. Two of these
211
+ three sat two minors behind, so `PATCHNOTES.md` exists to make taking one a
212
+ short read.
148
213
 
149
214
  **The public API test.** `src/__tests__/public-api.spec.ts` lists every export
150
215
  by name. The kit compiles perfectly well without an export nothing here calls,
151
216
  so removing one is invisible to every other test; this one fails loudly and
152
217
  asks whether the version should be a major.
153
218
 
219
+ **The SSR test.** `src/__tests__/ssr.spec.ts` renders in the **node**
220
+ environment, not jsdom — jsdom supplies the very `document` a server lacks, and
221
+ passed all four of the SSR bugs 0.2.2 fixed.
222
+
154
223
  **The consumer check.** `.github/workflows/consumer.yml` packs the tarball npm
155
- would serve, installs it into Hibi, and runs Hibi's full gate — format, lint,
156
- types, 37 tests, production build. A renamed prop shows up as a red pull
157
- request here rather than as a broken app after release.
224
+ would serve and installs it into **all three** apps, running each one's full
225
+ gate — format, lint, types, tests, production build. Kakehashi's build is
226
+ `vite-ssg build`, so that job is also the real prerender.
158
227
 
159
228
  That last one is the important one: the kit's own tests never import it the way
160
229
  an app does.
@@ -169,6 +238,9 @@ pnpm version minor
169
238
  git push --follow-tags
170
239
  ```
171
240
 
241
+ Then write the release into `PATCHNOTES.md` — what a consumer gains, and what
242
+ they have to do to take it.
243
+
172
244
  ## License
173
245
 
174
246
  MIT
@@ -1,13 +1,45 @@
1
+ /**
2
+ * The kit's button, and — when asked — its link.
3
+ *
4
+ * `as` exists because a button and a link are the same shape and a different
5
+ * element, and the app was resolving that by nesting them: a consumer had
6
+ * `<RouterLink><BaseButton>` in every call to action, which is an `<a>` around
7
+ * a `<button>`. That is invalid HTML, two stops in the tab order and two
8
+ * controls to a screen reader, for one thing on the screen. Whether something
9
+ * navigates is the app's decision; carrying it is this component's job.
10
+ *
11
+ * `router-link` is resolved by name rather than imported, so `vue-router` stays
12
+ * the optional peer it is. Only an app that passes `as="router-link"` needs it,
13
+ * and an app that passes it has it.
14
+ */
1
15
  type __VLS_Props = {
16
+ /** What to render. `button` unless this navigates. */
17
+ as?: 'button' | 'a' | 'router-link' | undefined;
2
18
  variant?: 'primary' | 'secondary' | 'ghost' | 'danger' | undefined;
3
- size?: 'sm' | 'md' | undefined;
19
+ size?: 'sm' | 'md' | 'lg' | undefined;
4
20
  loading?: boolean | undefined;
5
21
  disabled?: boolean | undefined;
22
+ /** Ignored unless `as` is `button`. */
6
23
  type?: 'button' | 'submit' | undefined;
24
+ /**
25
+ * Square, sized to its icon, with no label beside it.
26
+ *
27
+ * **Pass `aria-label`.** An icon on its own has no accessible name, and a
28
+ * control a screen reader announces as "button" is not usable. Attributes
29
+ * fall through, so `aria-label` lands where it should — nothing here can
30
+ * check that you passed one, which is why it is said this loudly.
31
+ */
32
+ icon?: boolean | undefined;
33
+ /** Fills its container. The ordinary case under a form. */
34
+ block?: boolean | undefined;
35
+ /** For `as="router-link"`. */
36
+ to?: string | Record<string, unknown> | undefined;
37
+ /** For `as="a"`. */
38
+ href?: string | undefined;
7
39
  };
8
- declare var __VLS_1: {};
40
+ declare var __VLS_8: {};
9
41
  type __VLS_Slots = {} & {
10
- default?: (props: typeof __VLS_1) => any;
42
+ default?: (props: typeof __VLS_8) => any;
11
43
  };
12
44
  declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
13
45
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * A single checkbox, with its label beside it.
3
+ *
4
+ * Deliberately not built on `FormField`. That component stacks a label above
5
+ * its control, which is right for every field where the control is a box you
6
+ * type into and wrong here: a checkbox is read as one sentence with a mark in
7
+ * front of it, and putting the words above the box breaks the association a
8
+ * sighted reader makes before they get to the accessible name.
9
+ *
10
+ * The whole row is the label, so the words are part of the hit target. On a
11
+ * phone that is the difference between a control and a coin toss.
12
+ */
13
+ type __VLS_Props = {
14
+ label: string;
15
+ error?: string | undefined;
16
+ hint?: string | undefined;
17
+ disabled?: boolean | undefined;
18
+ /**
19
+ * `md` is a setting: a line the reader came here to change, in ink.
20
+ * `sm` is an aside — "remember me" under a sign-in form, "show the ones I
21
+ * have learned" above a list — quieter and tighter.
22
+ *
23
+ * The two are not a guess. Of the five hand-written checkboxes across the
24
+ * three consuming apps, four were the aside and one was the setting, and
25
+ * they differed in exactly these two ways.
26
+ */
27
+ size?: 'sm' | 'md' | undefined;
28
+ };
29
+ type __VLS_ModelProps = {
30
+ modelValue?: boolean;
31
+ };
32
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
33
+ declare const __VLS_export: import('vue').DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
34
+ "update:modelValue": (value: boolean) => any;
35
+ }, string, import('vue').PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
36
+ "onUpdate:modelValue"?: (value: boolean) => any;
37
+ }>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
38
+ declare const _default: typeof __VLS_export;
39
+ export default _default;
@@ -9,6 +9,8 @@ type __VLS_Props = {
9
9
  */
10
10
  labelHidden?: boolean | undefined;
11
11
  type?: 'text' | 'email' | 'password' | 'number' | undefined;
12
+ /** `sm` for a field inside a row rather than in a form of its own. */
13
+ size?: 'sm' | 'md' | undefined;
12
14
  };
13
15
  type __VLS_ModelProps = {
14
16
  modelValue?: string | undefined;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * A set of radios, and the reason there is no `BaseRadio`.
3
+ *
4
+ * One radio on its own is not a control — it is half of a choice that cannot
5
+ * be unmade, and every real use is a group. So the group is the component.
6
+ *
7
+ * `fieldset` and `legend` rather than a label: a label points at one element,
8
+ * and the thing being named here is the question, not any single answer. Left
9
+ * as a plain label, a screen reader reads the options with no idea what they
10
+ * are options for.
11
+ */
12
+ type __VLS_Props = {
13
+ legend: string;
14
+ options: readonly {
15
+ value: string;
16
+ label: string;
17
+ disabled?: boolean | undefined;
18
+ }[];
19
+ error?: string | undefined;
20
+ hint?: string | undefined;
21
+ legendHidden?: boolean | undefined;
22
+ };
23
+ type __VLS_ModelProps = {
24
+ modelValue?: string | undefined;
25
+ };
26
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
27
+ declare const __VLS_export: import('vue').DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
28
+ "update:modelValue": (value: string | undefined) => any;
29
+ }, string, import('vue').PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
30
+ "onUpdate:modelValue"?: (value: string | undefined) => any;
31
+ }>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
32
+ declare const _default: typeof __VLS_export;
33
+ export default _default;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * A native `<select>`, wearing the kit's field.
3
+ *
4
+ * Native on purpose. A custom listbox has to reimplement typeahead, the
5
+ * keyboard, and the way a phone lifts the options into its own picker — and it
6
+ * gets one of them wrong. What is worth replacing is the chrome, so the arrow
7
+ * is drawn and the browser's own is removed.
8
+ *
9
+ * Options are passed rather than slotted so the value can be anything and the
10
+ * label can be a translated string the kit never sees.
11
+ */
12
+ type __VLS_Props = {
13
+ label: string;
14
+ options: readonly {
15
+ value: string;
16
+ label: string;
17
+ disabled?: boolean | undefined;
18
+ }[];
19
+ error?: string | undefined;
20
+ hint?: string | undefined;
21
+ labelHidden?: boolean | undefined;
22
+ /**
23
+ * An unselectable first row, for a field with no sensible default.
24
+ *
25
+ * Disabled rather than merely empty: an empty option that can be chosen lets
26
+ * someone go back to having answered nothing, which no form wants.
27
+ */
28
+ placeholder?: string | undefined;
29
+ /**
30
+ * `sm` for a select that filters or sorts rather than answers a form.
31
+ *
32
+ * Every hand-written select across the three consuming apps was this one.
33
+ */
34
+ size?: 'sm' | 'md' | undefined;
35
+ };
36
+ type __VLS_ModelProps = {
37
+ modelValue?: string | undefined;
38
+ };
39
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
40
+ declare const __VLS_export: import('vue').DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
41
+ "update:modelValue": (value: string | undefined) => any;
42
+ }, string, import('vue').PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
43
+ "onUpdate:modelValue"?: (value: string | undefined) => any;
44
+ }>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
45
+ declare const _default: typeof __VLS_export;
46
+ export default _default;
@@ -0,0 +1,20 @@
1
+ type __VLS_Props = {
2
+ label: string;
3
+ error?: string | undefined;
4
+ hint?: string | undefined;
5
+ labelHidden?: boolean | undefined;
6
+ rows?: number | undefined;
7
+ /** `sm` for editing in place, inside a row that is already small. */
8
+ size?: 'sm' | 'md' | undefined;
9
+ };
10
+ type __VLS_ModelProps = {
11
+ modelValue?: string | undefined;
12
+ };
13
+ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
14
+ declare const __VLS_export: import('vue').DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
15
+ "update:modelValue": (value: string | undefined) => any;
16
+ }, string, import('vue').PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
17
+ "onUpdate:modelValue"?: (value: string | undefined) => any;
18
+ }>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
19
+ declare const _default: typeof __VLS_export;
20
+ export default _default;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * A label, a hint, an error, and the wiring between them.
3
+ *
4
+ * This was inside `BaseInput`, which is why the kit had one form control
5
+ * instead of five. The hard part of a field is not the `<input>` — it is
6
+ * generating an id, pointing the label at it, deciding whether the description
7
+ * is the hint or the error, and telling assistive tech which one to read. That
8
+ * is identical for a select, a textarea and an input, and every app that
9
+ * needed one of the other two wrote the whole thing again.
10
+ *
11
+ * The control comes in through the slot and is handed what it needs to be
12
+ * described. It is a slot rather than a prop so the field never has to know
13
+ * what it is wrapping.
14
+ *
15
+ * @example
16
+ * ```vue
17
+ * <FormField :label="t('profile.name')" :error="errors.name">
18
+ * <template #default="{ id, describedBy, invalid }">
19
+ * <input :id="id" :aria-describedby="describedBy" :aria-invalid="invalid" />
20
+ * </template>
21
+ * </FormField>
22
+ * ```
23
+ */
24
+ type __VLS_Props = {
25
+ label: string;
26
+ error?: string | undefined;
27
+ hint?: string | undefined;
28
+ /**
29
+ * `sm` for a control that sits inside something else — a toolbar, a filter
30
+ * row, a settings line — rather than in a form of its own.
31
+ *
32
+ * It exists because every hand-written select in all three apps was the
33
+ * small one, and the kit only had the large one. A part is not reusable if
34
+ * reaching for it costs a size somebody chose on purpose.
35
+ */
36
+ size?: 'sm' | 'md' | undefined;
37
+ /**
38
+ * Hides the label visually but keeps it for assistive tech. For fields whose
39
+ * surrounding row already names them — dropping the label entirely would
40
+ * leave the control with no accessible name at all.
41
+ */
42
+ labelHidden?: boolean | undefined;
43
+ };
44
+ declare var __VLS_1: {
45
+ id: string;
46
+ describedBy: string | undefined;
47
+ invalid: boolean;
48
+ size: "sm" | "md";
49
+ };
50
+ type __VLS_Slots = {} & {
51
+ default?: (props: typeof __VLS_1) => any;
52
+ };
53
+ declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
54
+ declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
55
+ declare const _default: typeof __VLS_export;
56
+ export default _default;
57
+ type __VLS_WithSlots<T, S> = T & {
58
+ new (): {
59
+ $slots: S;
60
+ };
61
+ };
package/dist/index.d.ts CHANGED
@@ -47,7 +47,12 @@ export { default as BaseButton } from './components/BaseButton.vue';
47
47
  export { default as BaseInput } from './components/BaseInput.vue';
48
48
  export { default as BaseSheet } from './components/BaseSheet.vue';
49
49
  export { default as BaseCard } from './components/BaseCard.vue';
50
+ export { default as BaseCheckbox } from './components/BaseCheckbox.vue';
51
+ export { default as BaseRadioGroup } from './components/BaseRadioGroup.vue';
52
+ export { default as BaseSelect } from './components/BaseSelect.vue';
53
+ export { default as BaseTextarea } from './components/BaseTextarea.vue';
50
54
  export { default as EmptyState } from './components/EmptyState.vue';
55
+ export { default as FormField } from './components/FormField.vue';
51
56
  export { default as ErrorBoundary } from './components/ErrorBoundary.vue';
52
57
  export { default as PageContainer } from './components/PageContainer.vue';
53
58
  export { default as PageHeader } from './components/PageHeader.vue';