Skip to content

Commit ec2a203

Browse files
committed
Improve forms and input docs.
1 parent eb4d28b commit ec2a203

31 files changed

Lines changed: 1249 additions & 195 deletions

File tree

‎frontend/docs/componentGuide.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,22 @@ export function registerComponentGuideStyles() {
239239
border: 'none',
240240
padding: '0',
241241
})
242+
root.select('.forms-appearance-grid--composer').css({
243+
gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 24rem), 1fr))',
244+
})
245+
root.select('.forms-copy-sample').css({
246+
border: 'none',
247+
padding: '0',
248+
margin: '0',
249+
minWidth: '0',
250+
})
251+
root.select('.forms-composer-card').css({
252+
width: '100%',
253+
padding: '1rem',
254+
border: `1px solid ${neutral.border.subtle}`,
255+
borderRadius: '0.75rem',
256+
background: neutral.surface.rest.background,
257+
})
242258
root
243259
.select('.component-appearance-cell > .btn-group__dropdown')
244260
.css({ alignSelf: 'start' })

‎frontend/docs/docsComponents.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import { defineSignInPreviewComponent } from '../purestack.studio/components/sit
66
import { defineTopBarPreviewComponent } from '../purestack.studio/components/site/top-bar/preview'
77
import { defineApiPropertyComponent } from './apiProperty'
88
import { defineComponentGuideComponents } from './componentGuide'
9+
import { defineFormAppearanceGallery } from './formAppearance'
910
import { defineModalExampleComponents } from './modal'
1011

1112
export function defineDocumentationComponents(site: SiteConfig) {
1213
return {
1314
...defineComponentGuideComponents(),
15+
FormAppearanceGallery: defineFormAppearanceGallery(),
1416
ApiProperty: defineApiPropertyComponent(),
1517
ConsentPreview: defineConsentPreviewComponent(site),
1618
NavMenuPreview: defineNavMenuPreviewComponent(site),

‎frontend/docs/formAppearance.ts‎

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import type { ComponentVariant } from '@purestack/ts-components'
2+
import { SEMANTIC_TONES } from '@purestack/ts-style'
3+
import { defineComponent, html, type RefOrValue, ref, sref, unref } from 'regor'
4+
5+
export interface FormAppearanceGallery {
6+
component: RefOrValue<string>
7+
axis: RefOrValue<'tone' | 'variant' | 'type'>
8+
prefix: RefOrValue<string>
9+
}
10+
11+
const variants: ComponentVariant[] = [
12+
'solid',
13+
'surface',
14+
'surfaceAlt',
15+
'spotlight',
16+
'glass',
17+
'flat',
18+
'flatAlt',
19+
'flatSolid',
20+
'outlineFill',
21+
'outline',
22+
'subtle',
23+
'subtleBtn',
24+
'link',
25+
'sheen',
26+
'underline',
27+
'rail',
28+
'bracket',
29+
'none',
30+
]
31+
const inputTypes = [
32+
'color',
33+
'text',
34+
'email',
35+
'password',
36+
'number',
37+
'tel',
38+
'url',
39+
'search',
40+
'date',
41+
'time',
42+
'datetime-local',
43+
'month',
44+
'week',
45+
]
46+
47+
const template = html`<div class="component-appearance-grid forms-appearance-grid" :class="{ 'forms-appearance-grid--composer': component === 'Composer' }">
48+
<div class="component-appearance-cell" r-for="sample in samples">
49+
<code>{{ sample.label }}</code>
50+
<AppForm r-if="component === 'AppForm'" class="p-3" :tone="sample.tone" :variant="sample.variant" @submit.prevent="sample.submit($event)">
51+
<FormInputField :id="sample.id" label="Project name" name="project" :model="sample.model" :required="true"/>
52+
<FormSubmit label="Preview save" :tone="sample.tone"/>
53+
<FormStatus :hidden="!sample.receipt">{{ sample.receipt }}</FormStatus>
54+
</AppForm>
55+
<FormInputField r-if="component === 'FormInputField'" :id="sample.id" :label="axis === 'type' ? sample.label : 'Project name'" :type="axis === 'type' ? sample.label : 'text'" :model="sample.model" :tone="sample.tone" :variant="sample.variant" placeholder="Your next idea"/>
56+
<FormSelectField r-if="component === 'FormSelectField'" :id="sample.id" label="Region" :model="sample.model" :options="options" :tone="sample.tone" :variant="sample.variant"/>
57+
<FormCheck r-if="component === 'FormCheck'" :id="sample.id" label="Release updates" :checked="sample.checked" :tone="sample.tone"/>
58+
<form r-if="component === 'FormSubmit'" @submit.prevent="sample.submit($event)">
59+
<FormSubmit :label="sample.label" :tone="sample.tone" :variant="sample.variant"/>
60+
<FormStatus :hidden="!sample.receipt">{{ sample.receipt }}</FormStatus>
61+
</form>
62+
<FormStatus r-if="component === 'FormStatus'" :tone="sample.tone" :variant="sample.variant">Your draft is ready for review.</FormStatus>
63+
<AutoCompleteInput r-if="component === 'AutoCompleteInput'" :id="sample.id" label="Region" :model="sample.query" :selectedValue="sample.selected" :options="options" :tone="sample.tone" :variant="sample.variant" placeholder="Search a region"/>
64+
<MultiAutoCompleteInput r-if="component === 'MultiAutoCompleteInput'" :id="sample.id" label="Release tags" :items="sample.items" :model="sample.query" :options="tags" :tone="sample.tone" :variant="sample.variant" placeholder="Add a tag" :allowCustomValues="true"/>
65+
<Composer r-if="component === 'Composer'" :html="sample.content" :label="sample.label + ' editor'" :tone="sample.tone" :variant="sample.variant" minHeight="8rem"/>
66+
<DropFiles r-if="component === 'DropFiles'" :files="sample.files" label="Attachments" hint="Choose files, drop, or paste." :tone="sample.tone" :variant="sample.variant"/>
67+
</div>
68+
</div>`
69+
70+
export function defineFormAppearanceGallery() {
71+
return defineComponent<FormAppearanceGallery>(template, {
72+
props: ['component', 'axis', 'prefix'],
73+
context: (head) => {
74+
const axis = unref(head.props.axis)
75+
const component = unref(head.props.component)
76+
const values =
77+
axis === 'tone'
78+
? SEMANTIC_TONES
79+
: axis === 'type'
80+
? inputTypes
81+
: variants
82+
return {
83+
...head.props,
84+
samples: values.map((value) => {
85+
const receipt = ref('')
86+
return {
87+
id: `${unref(head.props.prefix)}-${value}`,
88+
label: value,
89+
tone: axis === 'tone' ? value : 'accent',
90+
variant:
91+
axis === 'variant'
92+
? value
93+
: component === 'FormSubmit'
94+
? 'solid'
95+
: 'surface',
96+
model: ref(
97+
axis === 'type'
98+
? inputValue(value)
99+
: component === 'FormSelectField'
100+
? 'eu'
101+
: 'Studio docs',
102+
),
103+
query: ref(''),
104+
selected: ref<string | number | null>(null),
105+
checked: ref(true),
106+
items: sref([{ label: 'Documentation', value: 'docs' }]),
107+
files: sref([]),
108+
content: ref('<p>A place for your <strong>next idea</strong>.</p>'),
109+
receipt,
110+
submit: (event: Event) => {
111+
const form = event.target as HTMLFormElement
112+
if (form.reportValidity())
113+
receipt('Preview complete. Nothing was sent.')
114+
},
115+
}
116+
}),
117+
options: [
118+
{ label: 'Europe Central', value: 'eu', keywords: ['Frankfurt'] },
119+
{ label: 'US East', value: 'us', keywords: ['Virginia'] },
120+
{
121+
label: 'Private region · unavailable',
122+
value: 'private',
123+
disabled: true,
124+
},
125+
],
126+
tags: [
127+
{ label: 'Documentation', value: 'docs' },
128+
{ label: 'Accessibility', value: 'a11y' },
129+
{ label: 'Performance', value: 'perf' },
130+
],
131+
}
132+
},
133+
})
134+
}
135+
136+
function inputValue(type: string): string {
137+
const values: Record<string, string> = {
138+
color: '#2563eb',
139+
text: 'Studio docs',
140+
email: 'you@example.com',
141+
password: 'example-password',
142+
number: '3',
143+
tel: '+49 30 123456',
144+
url: 'https://purestack.studio',
145+
search: 'components',
146+
date: '2026-10-03',
147+
time: '09:30',
148+
'datetime-local': '2026-10-03T09:30',
149+
month: '2026-10',
150+
week: '2026-W40',
151+
}
152+
return values[type] ?? ''
153+
}

‎frontend/purestack.studio/components/forms/app-form/app-form.mdx‎

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,19 @@ nav:
1313

1414
# App<wbr/>Form
1515

16-
A themed native form that keeps field composition, validation and submission in your control.
17-
18-
<Badge tone="accent" variant="surface">AppForm</Badge>
19-
20-
[View source](https://github.com/PureStackStudio/PureStack/blob/main/packages/ts-components/src/standard/form/form.ts) · [API reference](#api-reference)
21-
22-
16+
<ComponentHeader name="AppForm" kind="Form composition" summary="Compose a clear path from input to outcome. A themed native form with validation and submission in your control." source="https://github.com/PureStackStudio/PureStack/blob/main/packages/ts-components/src/standard/form/form.ts" section="Forms &amp; input" sectionHref="/components/forms/"/>
2317

2418
## Save a project
2519

2620
Edit the project name, save it locally, then reset. The handler explicitly checks native constraints because AppForm renders novalidate.
2721

2822
<Tabs group="app-form-sample-1" selectedTab="app-form-sample-1-preview" ariaLabel="Save a project example" tone="neutral" variant="surface" tabVariant="underline">
2923
<TabPane id="app-form-sample-1-preview" label="Preview">
24+
<ComponentExample label="AppForm in context" caption="Edit the project name, save a local preview, and reset the form. No request leaves this page.">
3025
<RegorApp id="app-form-demo" src="./playground.ts"/>
31-
</TabPane>
26+
27+
</ComponentExample>
28+
</TabPane>
3229
<TabPane id="app-form-sample-1-source-0" label="playground.mdx">
3330
```html
3431
<RegorApp id="app-form-demo" src="./playground.ts"/>
@@ -43,8 +40,8 @@ import {
4340
defineFormInputField,
4441
definePanelComponents,
4542
} from '@purestack/ts-components'
46-
4743
import { createApp, defineComponent, html, type Ref, ref } from 'regor'
44+
import { mountFormAppearanceGalleries } from '../appearance'
4845

4946
export interface AppFormExample {
5047
projectName: Ref<string>
@@ -109,6 +106,8 @@ createApp(
109106
},
110107
{ selector: 'app#app-form-demo', template: html`<AppFormExample />` },
111108
)
109+
110+
mountFormAppearanceGalleries()
112111
```
113112
</TabPane>
114113
</Tabs>
@@ -119,6 +118,7 @@ Choose the surrounding treatment independently of the fields. This disabled exam
119118

120119
<Tabs group="app-form-sample-2" selectedTab="app-form-sample-2-preview" ariaLabel="Compose a form surface example" tone="neutral" variant="surface" tabVariant="underline">
121120
<TabPane id="app-form-sample-2-preview" label="Preview">
121+
<div class="component-preview-stage">
122122
<AppForm method="post" tone="neutral" variant="outline" class="p-4">
123123
<FormInputField
124124
id="form-structure-name"
@@ -129,7 +129,9 @@ Choose the surrounding treatment independently of the fields. This disabled exam
129129
/>
130130
<FormStatus>Connect the form to your application’s submit handler.</FormStatus>
131131
</AppForm>
132-
</TabPane>
132+
133+
</div>
134+
</TabPane>
133135
<TabPane id="app-form-sample-2-source-0" label="form-structure.mdx">
134136
```html
135137
<AppForm method="post" tone="neutral" variant="outline" class="p-4">
@@ -146,6 +148,45 @@ Choose the surrounding treatment independently of the fields. This disabled exam
146148
</TabPane>
147149
</Tabs>
148150

151+
## All semantic tones
152+
153+
All ten tones are visible at once. The control’s label and content explain the intent independently of color.
154+
155+
<ComponentExample label="All ten semantic tones" caption="Each sample is interactive and keeps its own local state. Switch the site theme to compare both palettes.">
156+
<div id="forms-app-form-tone-gallery" data-form-gallery data-component="AppForm" data-axis="tone">
157+
<FormAppearanceGallery component="AppForm" axis="tone" prefix="forms-app-form-tone-gallery"/>
158+
</div>
159+
</ComponentExample>
160+
161+
## All visual variants
162+
163+
All eighteen supported treatments, shown in accent. Tone and variant are independent, so any treatment can be combined with a semantic tone.
164+
165+
<ComponentExample label="All eighteen visual variants" caption="Each sample is interactive and keeps its own local state. Switch the site theme to compare both palettes.">
166+
<div id="forms-app-form-variant-gallery" data-form-gallery data-component="AppForm" data-axis="variant">
167+
<FormAppearanceGallery component="AppForm" axis="variant" prefix="forms-app-form-variant-gallery"/>
168+
</div>
169+
</ComponentExample>
170+
171+
## Composition, validation, and submission
172+
173+
| Feature | Visible example |
174+
| --- | --- |
175+
| action and method | Native endpoint attributes; the local example intercepts submit. |
176+
| Default slot | Inputs, metadata, help links, divider, and feedback can be composed freely. |
177+
| Validation | AppForm uses novalidate. Call reportValidity in your submit handler when you want native validation. |
178+
| Form data | Give each contributing field a name, then read new FormData(form). |
179+
| Reset | The live example resets its refs explicitly; resetting a native form alone does not define application state. |
180+
| Appearance | Every tone and variant is visible above. |
181+
182+
```html
183+
<AppForm action="/api/projects" method="post" @submit.prevent="save">
184+
<FormInputField id="project-name" name="project" label="Project name" :model="projectName" :required="true"/>
185+
<FormMeta><FormSubmit label="Save project" tone="accent"/><FormAssistLink href="/guides/" label="Need help?"/></FormMeta>
186+
<FormStatus :hidden="!message">{{ message }}</FormStatus>
187+
</AppForm>
188+
```
189+
149190
## Behavior and accessibility
150191

151192
AppForm always renders `novalidate`. Use reportValidity or your own validation in a submit handler. Named controls participate in FormData; disabled controls do not. Reset reactive refs explicitly when handling reset in a Regor app.
@@ -186,8 +227,6 @@ AppForm always renders `novalidate`. Use reportValidity or your own validation i
186227
<p><strong>Default slot:</strong> native fields and form components. Native submit and reset events bubble normally. Register defineFormComponents; register each field family separately.</p>
187228
</div>
188229

189-
190-
191230
## Related components
192231

193232
- [FormInputField](/components/forms/form-input-field/)

‎frontend/purestack.studio/components/forms/app-form/playground.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import {
55
defineFormInputField,
66
definePanelComponents,
77
} from '@purestack/ts-components'
8-
98
import { createApp, defineComponent, html, type Ref, ref } from 'regor'
9+
import { mountFormAppearanceGalleries } from '../appearance'
1010

1111
export interface AppFormExample {
1212
projectName: Ref<string>
@@ -71,3 +71,5 @@ createApp(
7171
},
7272
{ selector: 'app#app-form-demo', template: html`<AppFormExample />` },
7373
)
74+
75+
mountFormAppearanceGalleries()

0 commit comments

Comments
 (0)