From ef90b15109ad10dbcadd58d0ff7e64439ae3f8c9 Mon Sep 17 00:00:00 2001 From: Ignat Date: Sun, 10 May 2026 23:34:23 +0900 Subject: [PATCH 1/3] feat(core): add optional baseline stylesheet at /style.css --- package.json | 2 +- packages/core/package.json | 10 +- packages/core/style.css | 217 +++++++++++++++++++++++++++++++++++++ 3 files changed, 225 insertions(+), 4 deletions(-) create mode 100644 packages/core/style.css diff --git a/package.json b/package.json index b5c2500..27e3a67 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "test": "pnpm -r run test", "clean": "pnpm -r run clean", "lint:packages": "pnpm --filter @formhaus/core --filter @formhaus/react --filter @formhaus/vue exec publint", - "check:types": "pnpm --filter @formhaus/core --filter @formhaus/react --filter @formhaus/vue exec attw --pack --profile esm-only", + "check:types": "pnpm --filter @formhaus/core --filter @formhaus/react --filter @formhaus/vue exec attw --pack --profile esm-only --exclude-entrypoints ./style.css", "changeset": "changeset", "version": "changeset version", "release": "pnpm build && changeset publish" diff --git a/packages/core/package.json b/packages/core/package.json index 09fd692..8447f26 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -3,7 +3,9 @@ "version": "0.3.1", "description": "Framework-agnostic form engine with validation, conditional visibility, and multi-step support. Define forms as plain JSON, render anywhere.", "type": "module", - "sideEffects": false, + "sideEffects": [ + "**/*.css" + ], "engines": { "node": ">=18" }, @@ -11,10 +13,12 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" - } + }, + "./style.css": "./style.css" }, "files": [ - "dist" + "dist", + "style.css" ], "scripts": { "build": "tsup", diff --git a/packages/core/style.css b/packages/core/style.css new file mode 100644 index 0000000..7ed05d5 --- /dev/null +++ b/packages/core/style.css @@ -0,0 +1,217 @@ +/** + * @formhaus/core baseline styles. + * Optional. Import from React or Vue apps: + * + * import '@formhaus/core/style.css'; + * + * Override any custom property below to retheme without forking the CSS. + */ + +:where(.fh-form) { + --fh-color-text: #111827; + --fh-color-text-muted: #6b7280; + --fh-color-border: #d1d5db; + --fh-color-border-focus: #2563eb; + --fh-color-error: #dc2626; + --fh-color-bg: #ffffff; + --fh-color-bg-disabled: #f3f4f6; + --fh-color-primary: #2563eb; + --fh-color-primary-hover: #1d4ed8; + --fh-color-primary-text: #ffffff; + --fh-color-secondary-border: #d1d5db; + --fh-radius: 6px; + --fh-gap: 16px; + --fh-input-padding: 8px 12px; + --fh-font-size: 14px; + --fh-font-size-small: 13px; +} + +.fh-form { + display: flex; + flex-direction: column; + gap: var(--fh-gap); + color: var(--fh-color-text); + font-size: var(--fh-font-size); +} + +.fh-form__fields { + display: flex; + flex-direction: column; + gap: var(--fh-gap); +} + +.fh-field { + display: flex; + flex-direction: column; + gap: 4px; +} + +.fh-field__label { + font-weight: 500; +} + +.fh-field__required { + color: var(--fh-color-error); +} + +.fh-field__input { + padding: var(--fh-input-padding); + border: 1px solid var(--fh-color-border); + border-radius: var(--fh-radius); + background: var(--fh-color-bg); + color: inherit; + font: inherit; + font-size: var(--fh-font-size); + width: 100%; + box-sizing: border-box; +} + +.fh-field__input:focus { + outline: none; + border-color: var(--fh-color-border-focus); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15); +} + +.fh-field__input:disabled { + background: var(--fh-color-bg-disabled); + cursor: not-allowed; +} + +.fh-field__input[aria-invalid="true"] { + border-color: var(--fh-color-error); +} + +.fh-field__input--textarea { + resize: vertical; + min-height: 80px; +} + +.fh-field__error, +.fh-form__top-error { + color: var(--fh-color-error); + font-size: var(--fh-font-size-small); + margin: 0; +} + +.fh-field__helper { + color: var(--fh-color-text-muted); + font-size: var(--fh-font-size-small); + margin: 0; +} + +.fh-form__top-errors { + display: flex; + flex-direction: column; + gap: 4px; +} + +.fh-field--checkbox .fh-field__checkbox-wrapper, +.fh-field--switch .fh-field__switch-wrapper { + display: flex; + align-items: center; + gap: 8px; +} + +.fh-field__radio-group, +.fh-field__multiselect-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.fh-field__radio-option, +.fh-field__multiselect-option { + display: flex; + align-items: center; + gap: 8px; +} + +.fh-form-actions { + display: flex; + gap: 8px; + align-items: center; +} + +.fh-form-actions__secondary { + display: flex; + gap: 8px; + margin-left: auto; +} + +.fh-form-actions__button { + padding: 8px 16px; + border-radius: var(--fh-radius); + border: 1px solid transparent; + background: transparent; + color: inherit; + font: inherit; + font-size: var(--fh-font-size); + cursor: pointer; +} + +.fh-form-actions__button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.fh-form-actions__button--primary { + background: var(--fh-color-primary); + color: var(--fh-color-primary-text); +} + +.fh-form-actions__button--primary:hover:not(:disabled) { + background: var(--fh-color-primary-hover); +} + +.fh-form-actions__button--secondary { + border-color: var(--fh-color-secondary-border); +} + +.fh-form-actions__button--secondary:hover:not(:disabled) { + background: var(--fh-color-bg-disabled); +} + +.fh-form-actions__button--text { + text-decoration: underline; + padding-left: 8px; + padding-right: 8px; +} + +.fh-step-progress { + display: flex; + flex-direction: column; + gap: 8px; +} + +.fh-step-progress__info { + display: flex; + flex-direction: column; + gap: 2px; +} + +.fh-step-progress__counter { + color: var(--fh-color-text-muted); + font-size: var(--fh-font-size-small); +} + +.fh-step-progress__title { + font-weight: 600; +} + +.fh-step-progress__description { + color: var(--fh-color-text-muted); + font-size: var(--fh-font-size-small); +} + +.fh-step-progress__bar { + height: 4px; + background: var(--fh-color-bg-disabled); + border-radius: 999px; + overflow: hidden; +} + +.fh-step-progress__fill { + height: 100%; + background: var(--fh-color-primary); + transition: width 200ms ease-out; +} From 8f249f2c118a5c3ce55c17a820f691438a94f80c Mon Sep 17 00:00:00 2001 From: Ignat Date: Sun, 10 May 2026 23:34:23 +0900 Subject: [PATCH 2/3] docs: explain how to opt into the baseline stylesheet --- docs/guide/index.md | 10 ++++++++++ packages/core/README.md | 10 ++++++++++ packages/react/README.md | 10 ++++++++++ packages/vue/README.md | 11 +++++++++++ 4 files changed, 41 insertions(+) diff --git a/docs/guide/index.md b/docs/guide/index.md index 053fc4c..074e70e 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -18,6 +18,16 @@ npm install @formhaus/vue # Vue Or use `@formhaus/core` directly with any framework. See the [Svelte example in the playground](/playground#svelte). +### Optional baseline styles + +The default React and Vue components render unstyled HTML. For a starting look (padding, focus, error colour) import the shared stylesheet: + +```ts +import '@formhaus/core/style.css'; +``` + +Theme via CSS custom properties (`--fh-color-primary`, `--fh-radius`, `--fh-gap`, etc.). When you bring your own components via the `components` prop, the stylesheet doesn't apply to them. + ## Quick start ```ts diff --git a/packages/core/README.md b/packages/core/README.md index f1be6ef..b58c9f6 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -45,6 +45,16 @@ engine.getSubmitValues(); // { name: 'Jane', email: 'jane@example.com' } ``` +## Optional baseline styles + +The default React and Vue field components render unstyled HTML with `fh-*` class names. For a sensible starting look (padding, focus states, error colour, button styles), import the optional stylesheet: + +```ts +import '@formhaus/core/style.css'; +``` + +Theme via CSS custom properties (`--fh-color-primary`, `--fh-radius`, `--fh-gap`, etc.) — no need to fork the file. If you're using a UI kit through the `components` prop, the stylesheet doesn't affect those. + ## What it covers - Form state with reactive `subscribe()` / `getSnapshot()` for adapters diff --git a/packages/react/README.md b/packages/react/README.md index ed9e238..af51e3f 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -64,6 +64,16 @@ Unmapped field types fall back to native HTML. `FormRenderer` is SSR-safe. It works with Next.js static and dynamic prerender, plus React's `renderToString`, with no `next/dynamic` wrapper required. +## Optional baseline styles + +By default the React adapter renders unstyled HTML. For a sensible starting look (padding, focus states, error colour, button styles) import the shared stylesheet: + +```ts +import '@formhaus/core/style.css'; +``` + +Theme via CSS custom properties (`--fh-color-primary`, `--fh-radius`, `--fh-gap`, etc.). If you bring your own components via the `components` prop (MUI, Tailwind, shadcn), the stylesheet doesn't apply to those. + ## Docs - Full guide and API reference: https://formhaus.dev diff --git a/packages/vue/README.md b/packages/vue/README.md index f7ef759..30957d1 100644 --- a/packages/vue/README.md +++ b/packages/vue/README.md @@ -69,6 +69,17 @@ defineEmits<{ (e: 'update:value', value: unknown): void }>(); Unmapped field types fall back to native HTML. +## Optional baseline styles + +By default the Vue adapter renders unstyled HTML. For a sensible starting look (padding, focus states, error colour, button styles) import the shared stylesheet from `@formhaus/core`: + +```ts +// main.ts +import '@formhaus/core/style.css'; +``` + +Theme via CSS custom properties (`--fh-color-primary`, `--fh-radius`, `--fh-gap`, etc.). If you bring your own components via the `components` prop (Vuetify, Element Plus, Naive UI), the stylesheet doesn't apply to those. + ## Docs - Full guide and API reference: https://formhaus.dev From 0ba59be1270b71a900860cc0b87f2a04432a2e87 Mon Sep 17 00:00:00 2001 From: Ignat Date: Sun, 10 May 2026 23:34:23 +0900 Subject: [PATCH 3/3] chore: changeset for optional baseline stylesheet --- .changeset/optional-baseline-css.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .changeset/optional-baseline-css.md diff --git a/.changeset/optional-baseline-css.md b/.changeset/optional-baseline-css.md new file mode 100644 index 0000000..1d2afb6 --- /dev/null +++ b/.changeset/optional-baseline-css.md @@ -0,0 +1,13 @@ +--- +"@formhaus/core": minor +--- + +Added optional baseline stylesheet at `@formhaus/core/style.css`. Import it for sensible default styling on the React and Vue native field components: padding, focus state, error colour, button styles, step progress bar. + +```ts +import '@formhaus/core/style.css'; +``` + +Theme via CSS custom properties (`--fh-color-primary`, `--fh-radius`, `--fh-gap`, etc.) — no fork required. The stylesheet only targets the default `fh-*` classes; if you swap in your own components via the `components` prop, it doesn't touch them. + +The package's `sideEffects` field is now `["**/*.css"]` (was `false`) so bundlers preserve the import.