@estiva-app/ui 0.41.0 → 0.42.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/dist/CommandPalette.d.ts.map +1 -1
- package/dist/Field.d.ts.map +1 -1
- package/dist/Form.d.ts +18 -1
- package/dist/Form.d.ts.map +1 -1
- package/dist/Select.d.ts.map +1 -1
- package/dist/eslint/index.js +15 -1
- package/dist/eslint/index.js.map +3 -3
- package/dist/eslint/no-restyled-part.d.ts +4 -0
- package/dist/eslint/no-restyled-part.d.ts.map +1 -1
- package/dist/index.js +26 -17
- package/dist/index.js.map +2 -2
- package/dist/looks.d.ts +6 -4
- package/dist/looks.d.ts.map +1 -1
- package/package.json +1 -1
- package/registry.json +27 -14
- package/src/ChipInput.tsx +1 -1
- package/src/CommandPalette.tsx +6 -4
- package/src/DialogShell.stories.tsx +4 -3
- package/src/Field.mdx +5 -0
- package/src/Field.tsx +4 -1
- package/src/Form.mdx +9 -3
- package/src/Form.stories.tsx +18 -1
- package/src/Form.test.tsx +17 -3
- package/src/Form.tsx +20 -2
- package/src/ListColumn.stories.tsx +1 -1
- package/src/Select.tsx +3 -0
- package/src/eslint/no-restyled-part.test.ts +17 -1
- package/src/eslint/no-restyled-part.ts +26 -1
- package/src/looks.ts +6 -4
package/dist/looks.d.ts
CHANGED
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
* Only looks — colour, text, border, corner, shadow. Where a part sits and how
|
|
11
11
|
* much room it takes stay in the part.
|
|
12
12
|
*/
|
|
13
|
-
/**
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* A field's box: TextInput and Textarea. The hairline strengthens on hover — of the box, or of anywhere in the `Field` around it, label included, so the field is one thing under the pointer (Katerina, 28 September) — and not while switched off: a label passes its hover to its box, switched off or not (FIELD_DISABLED_CLASSES holds it back). Focus outranks it, so a focused field keeps its colour under the pointer.
|
|
15
|
+
*/
|
|
16
|
+
export declare const FIELD_BOX_CLASSES = "bg-bg-field border border-border-field shadow-field hover:border-border-strong group-hover/field:border-border-strong focus:border-border-focus group-hover/field:focus:border-border-focus rounded-lg";
|
|
15
17
|
/** What a field's words look like, typed or waiting: TextInput, Textarea and Select. */
|
|
16
18
|
export declare const FIELD_TEXT_CLASSES = "text-text-primary placeholder:text-text-muted";
|
|
17
19
|
/** A field that cannot be used: TextInput, Textarea and Select. */
|
|
18
|
-
export declare const FIELD_DISABLED_CLASSES = "disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled";
|
|
20
|
+
export declare const FIELD_DISABLED_CLASSES = "disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled disabled:border-border-field group-hover/field:disabled:border-border-field";
|
|
19
21
|
/** The ring a focused field wears in Signal: TextInput and Textarea. */
|
|
20
22
|
export declare const FIELD_FOCUS_RING_CLASSES = "signal:transition-shadow signal:focus:shadow-focus-ring";
|
|
21
23
|
/** A field's two sizes, its padding and its words: TextInput, Textarea (default only) and Select. The small one is the small Select's trigger. */
|
|
@@ -24,7 +26,7 @@ export declare const FIELD_SIZE_CLASSES: {
|
|
|
24
26
|
readonly small: "h-6 min-h-6 px-2 text-caption";
|
|
25
27
|
};
|
|
26
28
|
/** The box around an input that sits inside it, before its focus look: SearchInput and ChipInput. */
|
|
27
|
-
export declare const FIELD_SHELL_CLASSES = "bg-bg-field border border-border-field shadow-field hover:border-border-strong rounded-lg transition-colors";
|
|
29
|
+
export declare const FIELD_SHELL_CLASSES = "bg-bg-field border border-border-field shadow-field hover:border-border-strong group-hover/field:border-border-strong rounded-lg transition-colors";
|
|
28
30
|
/** A panel that floats over the page: Menu's panel and Tooltip. */
|
|
29
31
|
export declare const FLOATING_SURFACE_CLASSES = "rounded-lg border border-border-default bg-bg-elevated shadow-lg";
|
|
30
32
|
/** A chip's words: Chip's label and Reaction's count. */
|
package/dist/looks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"looks.d.ts","sourceRoot":"","sources":["../src/looks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH
|
|
1
|
+
{"version":3,"file":"looks.d.ts","sourceRoot":"","sources":["../src/looks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB,2MAA2M,CAAA;AAEzO,wFAAwF;AACxF,eAAO,MAAM,kBAAkB,kDAAkD,CAAA;AAEjF,mEAAmE;AACnE,eAAO,MAAM,sBAAsB,iKAAiK,CAAA;AAEpM,wEAAwE;AACxE,eAAO,MAAM,wBAAwB,4DAA4D,CAAA;AAEjG,kJAAkJ;AAClJ,eAAO,MAAM,kBAAkB;;;CAGrB,CAAA;AAEV,qGAAqG;AACrG,eAAO,MAAM,mBAAmB,uJAAuJ,CAAA;AAEvL,mEAAmE;AACnE,eAAO,MAAM,wBAAwB,qEAAqE,CAAA;AAE1G,yDAAyD;AACzD,eAAO,MAAM,iBAAiB,0FAA0F,CAAA;AAExH,4FAA4F;AAC5F,eAAO,MAAM,kBAAkB,8EAA8E,CAAA"}
|
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"kind": "package",
|
|
5
5
|
"repo": "estiva-ui",
|
|
6
6
|
"package": "@estiva-app/ui",
|
|
7
|
-
"packageVersion": "0.
|
|
7
|
+
"packageVersion": "0.42.0",
|
|
8
8
|
"exports": 90,
|
|
9
9
|
"typeExports": 101,
|
|
10
10
|
"files": null
|
|
@@ -966,7 +966,6 @@
|
|
|
966
966
|
"looks": [
|
|
967
967
|
"bg-bg-elevated border border-border-subtle rounded-md",
|
|
968
968
|
"bg-bg-field border border-border-field shadow-field hover:border-border-strong rounded-lg transition-colors focus-within:border-border-focus focus-within:hover:border-border-focus cursor-text signal:transition-shadow signal:focus-within:shadow-focus-ring",
|
|
969
|
-
"focus-within:border-border-focus focus-within:hover:border-border-focus cursor-text signal:transition-shadow signal:focus-within:shadow-focus-ring",
|
|
970
969
|
"text-body-2 border-none bg-transparent text-text-primary placeholder:text-text-muted outline-none"
|
|
971
970
|
]
|
|
972
971
|
},
|
|
@@ -2109,6 +2108,12 @@
|
|
|
2109
2108
|
"required": false,
|
|
2110
2109
|
"note": "Whether Enter in a one-line field sends."
|
|
2111
2110
|
},
|
|
2111
|
+
{
|
|
2112
|
+
"name": "layout",
|
|
2113
|
+
"takes": "fields | free",
|
|
2114
|
+
"required": false,
|
|
2115
|
+
"note": "`fields`, the default: everything inside stacks 24px apart."
|
|
2116
|
+
},
|
|
2112
2117
|
{
|
|
2113
2118
|
"name": "children",
|
|
2114
2119
|
"takes": "anything",
|
|
@@ -2116,7 +2121,15 @@
|
|
|
2116
2121
|
"note": null
|
|
2117
2122
|
}
|
|
2118
2123
|
],
|
|
2119
|
-
"variants": [
|
|
2124
|
+
"variants": [
|
|
2125
|
+
{
|
|
2126
|
+
"prop": "layout",
|
|
2127
|
+
"values": [
|
|
2128
|
+
"fields",
|
|
2129
|
+
"free"
|
|
2130
|
+
]
|
|
2131
|
+
}
|
|
2132
|
+
],
|
|
2120
2133
|
"ownsBehaviours": [
|
|
2121
2134
|
{
|
|
2122
2135
|
"id": "base-ui",
|
|
@@ -4596,14 +4609,14 @@
|
|
|
4596
4609
|
"docPage": "src/Select.mdx",
|
|
4597
4610
|
"app": null,
|
|
4598
4611
|
"looks": [
|
|
4599
|
-
"rounded-lg border bg-bg-field shadow-field text-left border-border-field outline-none transition-colors text-text-primary placeholder:text-text-muted disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled focus-visible:border-border-focus aria-expanded:border-border-focus signal:transition-shadow signal:focus-visible:shadow-focus-ring text-input-value",
|
|
4600
|
-
"rounded-lg border bg-bg-field shadow-field text-left border-border-field outline-none transition-colors text-text-primary placeholder:text-text-muted disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled focus-visible:border-border-focus aria-expanded:border-border-focus signal:transition-shadow signal:focus-visible:shadow-focus-ring text-caption",
|
|
4601
|
-
"rounded-lg border bg-bg-field shadow-field text-left border-border-field outline-none transition-colors text-text-primary placeholder:text-text-muted disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled cursor-not-allowed bg-bg-disabled text-text-disabled focus-visible:border-border-focus aria-expanded:border-border-focus signal:transition-shadow signal:focus-visible:shadow-focus-ring text-input-value",
|
|
4602
|
-
"rounded-lg border bg-bg-field shadow-field text-left border-border-field outline-none transition-colors text-text-primary placeholder:text-text-muted disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled cursor-not-allowed bg-bg-disabled text-text-disabled focus-visible:border-border-focus aria-expanded:border-border-focus signal:transition-shadow signal:focus-visible:shadow-focus-ring text-caption",
|
|
4603
|
-
"rounded-lg border bg-bg-field shadow-field text-left border-border-field outline-none transition-colors text-text-primary placeholder:text-text-muted hover:border-border-strong focus-visible:hover:border-border-focus aria-expanded:hover:border-border-focus disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled focus-visible:border-border-focus aria-expanded:border-border-focus signal:transition-shadow signal:focus-visible:shadow-focus-ring text-input-value",
|
|
4604
|
-
"rounded-lg border bg-bg-field shadow-field text-left border-border-field outline-none transition-colors text-text-primary placeholder:text-text-muted hover:border-border-strong focus-visible:hover:border-border-focus aria-expanded:hover:border-border-focus disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled focus-visible:border-border-focus aria-expanded:border-border-focus signal:transition-shadow signal:focus-visible:shadow-focus-ring text-caption",
|
|
4605
|
-
"rounded-lg border bg-bg-field shadow-field text-left border-border-field outline-none transition-colors text-text-primary placeholder:text-text-muted hover:border-border-strong focus-visible:hover:border-border-focus aria-expanded:hover:border-border-focus disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled cursor-not-allowed bg-bg-disabled text-text-disabled focus-visible:border-border-focus aria-expanded:border-border-focus signal:transition-shadow signal:focus-visible:shadow-focus-ring text-input-value",
|
|
4606
|
-
"rounded-lg border bg-bg-field shadow-field text-left border-border-field outline-none transition-colors text-text-primary placeholder:text-text-muted hover:border-border-strong focus-visible:hover:border-border-focus aria-expanded:hover:border-border-focus disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled cursor-not-allowed bg-bg-disabled text-text-disabled focus-visible:border-border-focus aria-expanded:border-border-focus signal:transition-shadow signal:focus-visible:shadow-focus-ring text-caption",
|
|
4612
|
+
"rounded-lg border bg-bg-field shadow-field text-left border-border-field outline-none transition-colors text-text-primary placeholder:text-text-muted disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled disabled:border-border-field focus-visible:border-border-focus aria-expanded:border-border-focus signal:transition-shadow signal:focus-visible:shadow-focus-ring text-input-value",
|
|
4613
|
+
"rounded-lg border bg-bg-field shadow-field text-left border-border-field outline-none transition-colors text-text-primary placeholder:text-text-muted disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled disabled:border-border-field focus-visible:border-border-focus aria-expanded:border-border-focus signal:transition-shadow signal:focus-visible:shadow-focus-ring text-caption",
|
|
4614
|
+
"rounded-lg border bg-bg-field shadow-field text-left border-border-field outline-none transition-colors text-text-primary placeholder:text-text-muted disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled disabled:border-border-field cursor-not-allowed bg-bg-disabled text-text-disabled focus-visible:border-border-focus aria-expanded:border-border-focus signal:transition-shadow signal:focus-visible:shadow-focus-ring text-input-value",
|
|
4615
|
+
"rounded-lg border bg-bg-field shadow-field text-left border-border-field outline-none transition-colors text-text-primary placeholder:text-text-muted disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled disabled:border-border-field cursor-not-allowed bg-bg-disabled text-text-disabled focus-visible:border-border-focus aria-expanded:border-border-focus signal:transition-shadow signal:focus-visible:shadow-focus-ring text-caption",
|
|
4616
|
+
"rounded-lg border bg-bg-field shadow-field text-left border-border-field outline-none transition-colors text-text-primary placeholder:text-text-muted hover:border-border-strong focus-visible:hover:border-border-focus aria-expanded:hover:border-border-focus disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled disabled:border-border-field focus-visible:border-border-focus aria-expanded:border-border-focus signal:transition-shadow signal:focus-visible:shadow-focus-ring text-input-value",
|
|
4617
|
+
"rounded-lg border bg-bg-field shadow-field text-left border-border-field outline-none transition-colors text-text-primary placeholder:text-text-muted hover:border-border-strong focus-visible:hover:border-border-focus aria-expanded:hover:border-border-focus disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled disabled:border-border-field focus-visible:border-border-focus aria-expanded:border-border-focus signal:transition-shadow signal:focus-visible:shadow-focus-ring text-caption",
|
|
4618
|
+
"rounded-lg border bg-bg-field shadow-field text-left border-border-field outline-none transition-colors text-text-primary placeholder:text-text-muted hover:border-border-strong focus-visible:hover:border-border-focus aria-expanded:hover:border-border-focus disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled disabled:border-border-field cursor-not-allowed bg-bg-disabled text-text-disabled focus-visible:border-border-focus aria-expanded:border-border-focus signal:transition-shadow signal:focus-visible:shadow-focus-ring text-input-value",
|
|
4619
|
+
"rounded-lg border bg-bg-field shadow-field text-left border-border-field outline-none transition-colors text-text-primary placeholder:text-text-muted hover:border-border-strong focus-visible:hover:border-border-focus aria-expanded:hover:border-border-focus disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled disabled:border-border-field cursor-not-allowed bg-bg-disabled text-text-disabled focus-visible:border-border-focus aria-expanded:border-border-focus signal:transition-shadow signal:focus-visible:shadow-focus-ring text-caption",
|
|
4607
4620
|
"rounded-lg border bg-bg-field shadow-field text-left"
|
|
4608
4621
|
]
|
|
4609
4622
|
},
|
|
@@ -4851,7 +4864,7 @@
|
|
|
4851
4864
|
"docPage": "src/Textarea.mdx",
|
|
4852
4865
|
"app": null,
|
|
4853
4866
|
"looks": [
|
|
4854
|
-
"bg-bg-field border border-border-field shadow-field hover:border-border-strong focus:border-border-focus rounded-lg text-input-value text-text-primary placeholder:text-text-muted resize-none outline-none transition-colors disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled signal:transition-shadow signal:focus:shadow-focus-ring"
|
|
4867
|
+
"bg-bg-field border border-border-field shadow-field hover:border-border-strong focus:border-border-focus rounded-lg text-input-value text-text-primary placeholder:text-text-muted resize-none outline-none transition-colors disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled disabled:border-border-field signal:transition-shadow signal:focus:shadow-focus-ring"
|
|
4855
4868
|
]
|
|
4856
4869
|
},
|
|
4857
4870
|
{
|
|
@@ -4892,8 +4905,8 @@
|
|
|
4892
4905
|
"docPage": "src/TextInput.mdx",
|
|
4893
4906
|
"app": null,
|
|
4894
4907
|
"looks": [
|
|
4895
|
-
"bg-bg-field border border-border-field shadow-field hover:border-border-strong focus:border-border-focus rounded-lg text-input-value text-text-primary placeholder:text-text-muted outline-none transition-colors disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled signal:transition-shadow signal:focus:shadow-focus-ring",
|
|
4896
|
-
"bg-bg-field border border-border-field shadow-field hover:border-border-strong focus:border-border-focus rounded-lg text-caption text-text-primary placeholder:text-text-muted outline-none transition-colors disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled signal:transition-shadow signal:focus:shadow-focus-ring"
|
|
4908
|
+
"bg-bg-field border border-border-field shadow-field hover:border-border-strong focus:border-border-focus rounded-lg text-input-value text-text-primary placeholder:text-text-muted outline-none transition-colors disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled disabled:border-border-field signal:transition-shadow signal:focus:shadow-focus-ring",
|
|
4909
|
+
"bg-bg-field border border-border-field shadow-field hover:border-border-strong focus:border-border-focus rounded-lg text-caption text-text-primary placeholder:text-text-muted outline-none transition-colors disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled disabled:border-border-field signal:transition-shadow signal:focus:shadow-focus-ring"
|
|
4897
4910
|
]
|
|
4898
4911
|
},
|
|
4899
4912
|
{
|
package/src/ChipInput.tsx
CHANGED
|
@@ -246,7 +246,7 @@ export function ChipInput<T extends ChipInputOption = ChipInputOption>({
|
|
|
246
246
|
>
|
|
247
247
|
{/* The box the chips and the input share. `Combobox.Chips` is what makes
|
|
248
248
|
the two one control for the keyboard; the look is what it always was. */}
|
|
249
|
-
<Combobox.Chips ref={setBox} className={cn(FIELD_SHELL_CLASSES, 'focus-within:border-border-focus focus-within:hover:border-border-focus px-3 py-1.5 flex flex-wrap items-center gap-1.5 min-h-[38px] cursor-text signal:transition-shadow signal:focus-within:shadow-focus-ring')}>
|
|
249
|
+
<Combobox.Chips ref={setBox} className={cn(FIELD_SHELL_CLASSES, 'focus-within:border-border-focus focus-within:hover:border-border-focus group-hover/field:focus-within:border-border-focus px-3 py-1.5 flex flex-wrap items-center gap-1.5 min-h-[38px] cursor-text signal:transition-shadow signal:focus-within:shadow-focus-ring')}>
|
|
250
250
|
{value.map((option) => (
|
|
251
251
|
<Combobox.Chip key={option.id} className={cn(CHIP_BOX, chipPadding(!!chipLeading, true))}>
|
|
252
252
|
{chipLeading && <span className="flex shrink-0 items-center">{chipLeading(option)}</span>}
|
package/src/CommandPalette.tsx
CHANGED
|
@@ -19,7 +19,7 @@ import { Button } from './Button'
|
|
|
19
19
|
import { InputChip } from './ChipInput'
|
|
20
20
|
import { EmptyState } from './EmptyState'
|
|
21
21
|
import { FieldLine } from './Field'
|
|
22
|
-
import { Form } from './Form'
|
|
22
|
+
import { FIELDS_CLASSES, Form } from './Form'
|
|
23
23
|
import { Kbd } from './Kbd'
|
|
24
24
|
import { EnterHint, MenuItemBody, menuItemClassName } from './Menu'
|
|
25
25
|
import { ScrollArea } from './ScrollArea'
|
|
@@ -563,10 +563,12 @@ export function CommandPaletteForm({ chip, icon, submitLabel, onSubmit, submitWa
|
|
|
563
563
|
{/* The package Form, from the fields to the button (UIG-7). The chip row
|
|
564
564
|
stays outside it, so its ✕ still goes back while the form works; the
|
|
565
565
|
button row is inside it, so focus that was on the button is held.
|
|
566
|
-
The fields are found through data-command-palette-fields.
|
|
567
|
-
|
|
566
|
+
The fields are found through data-command-palette-fields. The form is
|
|
567
|
+
`free`, a scrolling body over a button row; the fields inside stack as
|
|
568
|
+
every form's do, 24px apart (Katerina, 28 September: they were 16). */}
|
|
569
|
+
<Form data-command-palette-fields="" busy={busy} enterSends={false} onSubmit={submit} layout="free" className="flex min-h-0 flex-col">
|
|
568
570
|
<ScrollArea viewportClassName="max-h-[420px]" contentClassName="flex flex-col px-5 py-4">
|
|
569
|
-
<div className=
|
|
571
|
+
<div className={FIELDS_CLASSES}>{children}</div>
|
|
570
572
|
</ScrollArea>
|
|
571
573
|
<div className="flex shrink-0 items-center gap-3 px-5 pb-4">
|
|
572
574
|
<div className="min-w-0 flex-1">
|
|
@@ -6,6 +6,7 @@ import { Chip } from './Chip'
|
|
|
6
6
|
import { DialogShell } from './DialogShell'
|
|
7
7
|
import { IconButton } from './IconButton'
|
|
8
8
|
import { Field } from './Field'
|
|
9
|
+
import { Form } from './Form'
|
|
9
10
|
import { TextInput } from './TextInput'
|
|
10
11
|
import { Textarea } from './Textarea'
|
|
11
12
|
|
|
@@ -32,22 +33,22 @@ type Story = StoryObj<typeof meta>
|
|
|
32
33
|
*/
|
|
33
34
|
export const Default: Story = {
|
|
34
35
|
args: {
|
|
35
|
-
bodyClassName: 'flex flex-col gap-6',
|
|
36
36
|
footer: (
|
|
37
37
|
<>
|
|
38
38
|
<Button variant="muted">Cancel</Button>
|
|
39
39
|
<Button variant="primary">Confirm</Button>
|
|
40
40
|
</>
|
|
41
41
|
),
|
|
42
|
+
// In a Form, as a dialog's fields always are: it places them 24px apart.
|
|
42
43
|
children: (
|
|
43
|
-
|
|
44
|
+
<Form onSubmit={() => {}} aria-label="Example">
|
|
44
45
|
<Field label="Title" required>
|
|
45
46
|
<TextInput placeholder="What is this about?" />
|
|
46
47
|
</Field>
|
|
47
48
|
<Field label="Notes">
|
|
48
49
|
<Textarea placeholder="Optional notes…" className="h-[80px]" />
|
|
49
50
|
</Field>
|
|
50
|
-
|
|
51
|
+
</Form>
|
|
51
52
|
),
|
|
52
53
|
},
|
|
53
54
|
}
|
package/src/Field.mdx
CHANGED
|
@@ -53,6 +53,10 @@ import { Field, TextInput } from '@estiva-app/ui'
|
|
|
53
53
|
`Field` has no `required` of its own, so the Field puts it there. A control
|
|
54
54
|
that says so already is left alone, and a `children` of several elements
|
|
55
55
|
keeps the asterisk and owes its own.
|
|
56
|
+
- **The whole field is one thing under the pointer** (Katerina, 28
|
|
57
|
+
September): the label, the space under it, the box and the line under the
|
|
58
|
+
box. Anywhere on it strengthens the box's border, as hovering the box does;
|
|
59
|
+
a focused box keeps its focus colour, and a switched-off one stays as it is.
|
|
56
60
|
|
|
57
61
|
## What it owns
|
|
58
62
|
|
|
@@ -62,6 +66,7 @@ It does these by itself. Write none of them around it.
|
|
|
62
66
|
|---|---|---|
|
|
63
67
|
| Ties the label, the description and the error to the control inside it | `aria-label`, an `id` / `htmlFor` pair, or `aria-describedby` | |
|
|
64
68
|
| Marks the control invalid when it has an `error`, and announces it | `aria-invalid` | |
|
|
69
|
+
| Strengthens the box's border while the pointer is anywhere on the field | a hover of your own on the label or around the box | |
|
|
65
70
|
|
|
66
71
|
## Props
|
|
67
72
|
|
package/src/Field.tsx
CHANGED
|
@@ -83,7 +83,10 @@ export function Field({ label, required = false, helper, error, children }: Fiel
|
|
|
83
83
|
})
|
|
84
84
|
: children
|
|
85
85
|
return (
|
|
86
|
-
|
|
86
|
+
// `group/field`: the label, the box and the line under it are one thing under the pointer —
|
|
87
|
+
// anywhere in it strengthens the box's border, as hovering the box does (Katerina, 28 September).
|
|
88
|
+
// Before, the 8px between the label and the box was a gap where the border went out again.
|
|
89
|
+
<BaseField.Root invalid={!!error} className="group/field flex flex-col gap-2">
|
|
87
90
|
{/* `cn`, like everywhere else. It was a template literal, with a comment
|
|
88
91
|
saying the type token must never be merged — which stopped being true
|
|
89
92
|
when `cn()` was taught the ramp: `input-label` is in it, and
|
package/src/Form.mdx
CHANGED
|
@@ -27,7 +27,7 @@ is switched off at once.
|
|
|
27
27
|
```tsx
|
|
28
28
|
import { Button, Field, Form, TextInput } from '@estiva-app/ui'
|
|
29
29
|
|
|
30
|
-
<Form onSubmit={send} busy={sending}
|
|
30
|
+
<Form onSubmit={send} busy={sending}>
|
|
31
31
|
<Field label="Title" required error={titleError}>
|
|
32
32
|
<TextInput value={title} onChange={(e) => { setTitle(e.target.value); setTitleError(undefined) }} />
|
|
33
33
|
</Field>
|
|
@@ -55,8 +55,13 @@ import { Button, Field, Form, TextInput } from '@estiva-app/ui'
|
|
|
55
55
|
standing keeps the form from ever sending.
|
|
56
56
|
- The browser's own validation bubbles are off (`noValidate`). `Field`'s
|
|
57
57
|
`required` marks a field; it does not block sending by itself.
|
|
58
|
-
-
|
|
59
|
-
|
|
58
|
+
- **The form places its fields: one column, 24px apart** (Katerina, 28
|
|
59
|
+
September), so every form reads the same. Pass no layout of your own: the
|
|
60
|
+
gate lint refuses a gap, a direction or a grid on a `Form`. `className`
|
|
61
|
+
still places the form itself: its space around, its size.
|
|
62
|
+
- **`layout="free"`** is for a form that is not a list of fields: a field and
|
|
63
|
+
its button, a composer. It places nothing, and `className` lays it out.
|
|
64
|
+
- The form draws no box, and neither does the `<fieldset>` inside it.
|
|
60
65
|
|
|
61
66
|
## What it owns
|
|
62
67
|
|
|
@@ -67,6 +72,7 @@ It does these by itself. Write none of them around it.
|
|
|
67
72
|
| Sends on Enter in a one-line field, and on Ctrl+Enter anywhere inside | a key handler that submits | |
|
|
68
73
|
| Leaves Enter alone where it means something else: a Textarea, a list, a people picker | exceptions of your own | |
|
|
69
74
|
| Does nothing while it is sending: no second send, and nothing inside takes focus | a busy flag on every control | |
|
|
75
|
+
| Stacks its fields in one column, 24px apart | a gap, a direction or a grid on the form | `no-restyled-part` |
|
|
70
76
|
|
|
71
77
|
## Keys
|
|
72
78
|
|
package/src/Form.stories.tsx
CHANGED
|
@@ -11,7 +11,7 @@ const meta = {
|
|
|
11
11
|
title: 'Inputs/Form',
|
|
12
12
|
component: Form,
|
|
13
13
|
parameters: { layout: 'padded' },
|
|
14
|
-
args: { onSubmit: fn(), busy: false,
|
|
14
|
+
args: { onSubmit: fn(), busy: false, 'aria-label': 'Example', children: null },
|
|
15
15
|
argTypes: { children: { control: false }, onSubmit: { control: false } },
|
|
16
16
|
decorators: [(Story) => <div className="w-96"><Story /></div>],
|
|
17
17
|
} satisfies Meta<typeof Form>
|
|
@@ -35,6 +35,7 @@ const fields = (error?: string) => (
|
|
|
35
35
|
</>
|
|
36
36
|
)
|
|
37
37
|
|
|
38
|
+
/** A list of fields: the form stacks them 24px apart itself. */
|
|
38
39
|
export const Default: Story = { render: (args) => <Form {...args}>{fields()}</Form> }
|
|
39
40
|
|
|
40
41
|
/** Sending: every field and button inside is switched off at once. */
|
|
@@ -43,6 +44,22 @@ export const Busy: Story = { args: { busy: true }, render: (args) => <Form {...a
|
|
|
43
44
|
/** A field showing its error. The form does not send while it shows. */
|
|
44
45
|
export const WithError: Story = { render: (args) => <Form {...args}>{fields('That is not a valid value.')}</Form> }
|
|
45
46
|
|
|
47
|
+
/**
|
|
48
|
+
* **`layout="free"`**: a form that is not a list of fields, here a field and
|
|
49
|
+
* its button, places nothing, and lays itself out through `className`.
|
|
50
|
+
*/
|
|
51
|
+
export const Free: Story = {
|
|
52
|
+
args: { layout: 'free', className: 'flex items-center gap-2' },
|
|
53
|
+
render: (args) => (
|
|
54
|
+
<Form {...args}>
|
|
55
|
+
<TextInput placeholder="Add a name" aria-label="Name" className="min-w-0 flex-1" />
|
|
56
|
+
<Button variant="primary" type="submit">
|
|
57
|
+
Add
|
|
58
|
+
</Button>
|
|
59
|
+
</Form>
|
|
60
|
+
),
|
|
61
|
+
}
|
|
62
|
+
|
|
46
63
|
/**
|
|
47
64
|
* Live. Press Enter in the first field, or Send: the form is busy for a
|
|
48
65
|
* second and a half, and focus comes back to where it was.
|
package/src/Form.test.tsx
CHANGED
|
@@ -26,7 +26,7 @@ afterEach(cleanup)
|
|
|
26
26
|
|
|
27
27
|
function Harness({ onSubmit = vi.fn(), error, busy = false }: { onSubmit?: () => void; error?: string; busy?: boolean }) {
|
|
28
28
|
return (
|
|
29
|
-
<Form onSubmit={onSubmit} busy={busy} aria-label="Probe"
|
|
29
|
+
<Form onSubmit={onSubmit} busy={busy} aria-label="Probe" id="probe">
|
|
30
30
|
<Field label="First">
|
|
31
31
|
<TextInput />
|
|
32
32
|
</Field>
|
|
@@ -213,13 +213,27 @@ describe('Form', () => {
|
|
|
213
213
|
expect(fireEvent.submit(form)).toBe(false)
|
|
214
214
|
})
|
|
215
215
|
|
|
216
|
-
it('is a form with no browser validation bubbles, and passes id
|
|
216
|
+
it('is a form with no browser validation bubbles, and passes id on', () => {
|
|
217
217
|
render(<Harness />)
|
|
218
218
|
const form = screen.getByRole('form', { name: 'Probe' }) as HTMLFormElement
|
|
219
219
|
expect(form.tagName).toBe('FORM')
|
|
220
220
|
expect(form.noValidate).toBe(true)
|
|
221
221
|
expect(form.id).toBe('probe')
|
|
222
|
-
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('stacks its fields 24px apart itself, and a free form places nothing', () => {
|
|
225
|
+
render(
|
|
226
|
+
<>
|
|
227
|
+
<Form onSubmit={() => {}} aria-label="Fields" className="px-5">x</Form>
|
|
228
|
+
<Form onSubmit={() => {}} aria-label="Free" layout="free" className="flex items-center gap-2">x</Form>
|
|
229
|
+
</>,
|
|
230
|
+
)
|
|
231
|
+
const fields = screen.getByRole('form', { name: 'Fields' }).className.split(' ')
|
|
232
|
+
expect(fields).toEqual(expect.arrayContaining(['flex', 'flex-col', 'gap-6', 'px-5']))
|
|
233
|
+
const free = screen.getByRole('form', { name: 'Free' }).className.split(' ')
|
|
234
|
+
expect(free).not.toContain('flex-col')
|
|
235
|
+
expect(free).not.toContain('gap-6')
|
|
236
|
+
expect(free).toEqual(expect.arrayContaining(['flex', 'items-center', 'gap-2']))
|
|
223
237
|
})
|
|
224
238
|
|
|
225
239
|
it('draws no box of its own around the fields', () => {
|
package/src/Form.tsx
CHANGED
|
@@ -37,6 +37,15 @@ import { FormBusyContext, useFormBusy } from './formBusy'
|
|
|
37
37
|
* from sending, for a form where only Ctrl+Enter may send (`CommandPalette`).
|
|
38
38
|
* Every way of sending goes through the form's submit, so Base UI's field
|
|
39
39
|
* check runs for each.
|
|
40
|
+
*
|
|
41
|
+
* **The form places its fields** (Katerina, 28 September). It drew no box and
|
|
42
|
+
* left the space between fields to every caller, and they chose three: Peek's
|
|
43
|
+
* dialogs 8px — the label's own distance from its box, so a label sat as close
|
|
44
|
+
* to the field above it as to its own — Ship's 24px, and the package's own
|
|
45
|
+
* launcher 16px. Everything inside now stacks 24px apart, and the gate lint
|
|
46
|
+
* refuses a layout passed in (`no-restyled-part`). A form that is not a list
|
|
47
|
+
* of fields — a field and its button, a composer — says `layout="free"` and
|
|
48
|
+
* lays itself out.
|
|
40
49
|
*/
|
|
41
50
|
export interface FormProps extends Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit' | 'children' | 'noValidate'> {
|
|
42
51
|
/** Enter in a field, or a submit button. The page's own submit is already prevented. */
|
|
@@ -48,9 +57,18 @@ export interface FormProps extends Omit<FormHTMLAttributes<HTMLFormElement>, 'on
|
|
|
48
57
|
* Ctrl+Enter may send — a form inside `CommandPalette`. Ctrl+Enter sends either way.
|
|
49
58
|
*/
|
|
50
59
|
enterSends?: boolean
|
|
60
|
+
/**
|
|
61
|
+
* `fields`, the default: everything inside stacks 24px apart. `free`: the
|
|
62
|
+
* form places nothing, for a form that is not a list of fields — a field and
|
|
63
|
+
* its button, a composer — which lays itself out through `className`.
|
|
64
|
+
*/
|
|
65
|
+
layout?: 'fields' | 'free'
|
|
51
66
|
children: ReactNode
|
|
52
67
|
}
|
|
53
68
|
|
|
69
|
+
/** A list of fields: one column, 24px apart. `CommandPalette` stacks its fields with it too. */
|
|
70
|
+
export const FIELDS_CLASSES = 'flex flex-col gap-6'
|
|
71
|
+
|
|
54
72
|
const CONTROL = 'input:not([type="hidden"]), textarea, select, button, [role="checkbox"], [role="combobox"], [tabindex]:not([tabindex="-1"])'
|
|
55
73
|
|
|
56
74
|
/** The inputs a person types one line into; a picker's input (`role="combobox"`) is not one. */
|
|
@@ -70,7 +88,7 @@ function firstInvalid(form: HTMLElement): HTMLElement | undefined {
|
|
|
70
88
|
return undefined
|
|
71
89
|
}
|
|
72
90
|
|
|
73
|
-
export function Form({ onSubmit, busy: ownBusy = false, enterSends = true, className, children, ...props }: FormProps) {
|
|
91
|
+
export function Form({ onSubmit, busy: ownBusy = false, enterSends = true, layout = 'fields', className, children, ...props }: FormProps) {
|
|
74
92
|
// A form inside a busy form is busy too.
|
|
75
93
|
const outerBusy = useFormBusy()
|
|
76
94
|
const busy = ownBusy || outerBusy
|
|
@@ -148,7 +166,7 @@ export function Form({ onSubmit, busy: ownBusy = false, enterSends = true, class
|
|
|
148
166
|
// Focus waits here only while busy; the rest of the time a click between two fields must not land on the form.
|
|
149
167
|
tabIndex={busy ? -1 : props.tabIndex}
|
|
150
168
|
// A form that holds focus for a moment must not draw a focus ring.
|
|
151
|
-
className={cn('outline-none', className)}
|
|
169
|
+
className={cn('outline-none', layout === 'fields' && FIELDS_CLASSES, className)}
|
|
152
170
|
onFocus={(event) => {
|
|
153
171
|
if (event.target !== form.current) lastInside.current = event.target
|
|
154
172
|
props.onFocus?.(event)
|
|
@@ -97,7 +97,7 @@ export const WithARowAbove: Story = {
|
|
|
97
97
|
<ListColumn
|
|
98
98
|
title="Items"
|
|
99
99
|
above={
|
|
100
|
-
<Form onSubmit={() => setName('')} className="flex items-center gap-2 px-3 pt-3 pb-1">
|
|
100
|
+
<Form onSubmit={() => setName('')} layout="free" className="flex items-center gap-2 px-3 pt-3 pb-1">
|
|
101
101
|
<TextInput className="min-w-0 flex-1" value={name} onChange={(e) => setName(e.target.value)} placeholder="New item name" aria-label="New item name" />
|
|
102
102
|
<Button className="shrink-0" type="submit" disabled={!name.trim()}>
|
|
103
103
|
Create
|
package/src/Select.tsx
CHANGED
|
@@ -94,7 +94,10 @@ export function Select({ value, onChange, options, size = 'default', ariaLabel,
|
|
|
94
94
|
// The focused border survives a hover: hover alone strengthens the
|
|
95
95
|
// hairline, but hover while focused must not grey the focus colour —
|
|
96
96
|
// the stacked variant outranks plain hover by specificity.
|
|
97
|
+
// Anywhere in the `Field` around it counts as over it (Katerina, 28 September).
|
|
97
98
|
!held && 'hover:border-border-strong focus-visible:hover:border-border-focus aria-expanded:hover:border-border-focus',
|
|
99
|
+
// Left out while focused or open, so their colour stays under the pointer.
|
|
100
|
+
!held && 'group-hover/field:border-border-strong group-hover/field:focus-visible:border-border-focus group-hover/field:aria-expanded:border-border-focus',
|
|
98
101
|
FIELD_DISABLED_CLASSES,
|
|
99
102
|
held && 'cursor-not-allowed bg-bg-disabled text-text-disabled',
|
|
100
103
|
'focus-visible:border-border-focus aria-expanded:border-border-focus',
|
|
@@ -74,6 +74,9 @@ tester.run('no-restyled-part', noRestyledPart, {
|
|
|
74
74
|
{ name: 'style the code works out while it runs passes', filename: app, code: ui('Card', ' <Card style={props.style} />') },
|
|
75
75
|
{ name: 'style on a component that hands on only its className is not read', filename: app, code: local('ConversationCard', './components/ConversationCard', ' <ConversationCard title="x" style={{ opacity: 0.5 }} />') },
|
|
76
76
|
{ name: 'EmptyState placed', filename: app, code: ui('EmptyState', ' <EmptyState className="mt-2 flex-1" message="Nothing yet" />') },
|
|
77
|
+
{ name: 'Form placed, its fields its own', filename: app, code: ui('Form', ' <Form onSubmit={send} className="min-h-0 flex-1 px-5">x</Form>') },
|
|
78
|
+
{ name: 'a free Form lays itself out', filename: app, code: ui('Form', ' <Form onSubmit={send} layout="free" className="flex items-center gap-2">x</Form>') },
|
|
79
|
+
{ name: 'a Form whose layout the rule cannot read passes', filename: app, code: ui('Form', ' <Form onSubmit={send} layout={how} className="flex gap-2">x</Form>') },
|
|
77
80
|
// escapes
|
|
78
81
|
{ name: 'escaped, with its reason', filename: app, code: ui('Link', ' // @estiva-escape(no-restyled-part): the whole row is the link, and no part does that yet\n <Link href="/x" className="after:absolute after:inset-0">x</Link>') },
|
|
79
82
|
// the package itself
|
|
@@ -186,6 +189,18 @@ tester.run('no-restyled-part', noRestyledPart, {
|
|
|
186
189
|
code: "import { Link, type LinkProps } from '@estiva-app/ui'\nfunction RouterLink({ href, ...props }: LinkProps) {\n return <Link href={href} {...props} />\n}\nexport function Probe() {\n return <RouterLink href=\"/x\" className=\"text-h5\">x</RouterLink>\n}\n",
|
|
187
190
|
errors: [{ message: restyled('`text-h5`', '`RouterLink` (it hands its props to `Link`)', ' Use its `variant`, `truncate` or `cover`.') }],
|
|
188
191
|
},
|
|
192
|
+
{
|
|
193
|
+
name: 'a Form laid out by hand',
|
|
194
|
+
filename: app,
|
|
195
|
+
code: ui('Form', ' <Form onSubmit={send} className="flex flex-col gap-2 sm:gap-4 mt-2">x</Form>'),
|
|
196
|
+
errors: [{ message: '`flex` `flex-col` `gap-2` `sm:gap-4` lays out the fields of `Form`. A Form places its own fields, 24px apart (Katerina, 28 September), so every form reads the same: remove it. A form that is not a list of fields (a field and its button, a composer) says `layout="free"` and lays itself out.' }],
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'a Form that says fields is laid out by the Form',
|
|
200
|
+
filename: app,
|
|
201
|
+
code: ui('Form', ' <Form onSubmit={send} layout="fields" className="space-y-3">x</Form>'),
|
|
202
|
+
errors: [{ message: '`space-y-3` lays out the fields of `Form`. A Form places its own fields, 24px apart (Katerina, 28 September), so every form reads the same: remove it. A form that is not a list of fields (a field and its button, a composer) says `layout="free"` and lays itself out.' }],
|
|
203
|
+
},
|
|
189
204
|
{
|
|
190
205
|
name: 'EmptyState padded directly',
|
|
191
206
|
filename: app,
|
|
@@ -307,7 +322,8 @@ describe('the look props the message names are the package’s own', () => {
|
|
|
307
322
|
})
|
|
308
323
|
|
|
309
324
|
/** Named like a look, and not one: AttachmentCard's `size` is the file's, in bytes. */
|
|
310
|
-
|
|
325
|
+
// Form's `layout` says whether it places its fields at all, not how anything looks.
|
|
326
|
+
const NOT_A_LOOK: Record<string, string[]> = { AttachmentCard: ['size'], Form: ['layout'] }
|
|
311
327
|
|
|
312
328
|
it('every look prop a part has is named', () => {
|
|
313
329
|
for (const part of PARTS) {
|
|
@@ -36,6 +36,10 @@ import { ESCAPE_MESSAGES, isEscaped } from './escape'
|
|
|
36
36
|
*
|
|
37
37
|
* `EmptyState` takes no padding either (Katerina's ruling 6 of 14 September):
|
|
38
38
|
* its room comes from the box its rows live in.
|
|
39
|
+
*
|
|
40
|
+
* `Form` takes no layout for its fields (Katerina, 28 September): it stacks
|
|
41
|
+
* them 24px apart itself, so every form reads the same. A form that is not a
|
|
42
|
+
* list of fields says `layout="free"`, and then lays itself out.
|
|
39
43
|
*/
|
|
40
44
|
|
|
41
45
|
// ---------------------------------------------------------------- the classes
|
|
@@ -93,6 +97,14 @@ export function isPlacement(token: string): boolean {
|
|
|
93
97
|
return PLACEMENT.some(({ pattern }) => pattern.test(utility))
|
|
94
98
|
}
|
|
95
99
|
|
|
100
|
+
/** How a form's fields are laid out: its display, direction, gaps and alignment. */
|
|
101
|
+
const FIELDS_LAYOUT = /^(?:block|flex|grid|inline-flex|inline-grid|contents|flow-root)$|^(?:gap|gap-x|gap-y|space-x|space-y|grid-cols|grid-rows|grid-flow|auto-cols|auto-rows|items|justify|justify-items|place-content|place-items)-|^flex-(?:row|col|wrap|nowrap)|^content-(?!\[)/
|
|
102
|
+
|
|
103
|
+
const isFieldsLayout = (token: string) => {
|
|
104
|
+
const { variants, utility } = splitClass(token)
|
|
105
|
+
return !variants.some((variant) => REACHING_VARIANT.test(variant)) && FIELDS_LAYOUT.test(utility)
|
|
106
|
+
}
|
|
107
|
+
|
|
96
108
|
const isPadding = (token: string) => {
|
|
97
109
|
const { variants, utility } = splitClass(token)
|
|
98
110
|
return !variants.some((variant) => REACHING_VARIANT.test(variant)) && /^p[xytrblse]?-/.test(utility)
|
|
@@ -608,6 +620,8 @@ export const noRestyledPart: Rule.RuleModule = {
|
|
|
608
620
|
'{{keys}} in the `style` of {{where}} changes how it looks. A part of @estiva-app/ui is placed from outside, never restyled, by class or by style: only space, size, flex and grid, and position pass in.{{use}} A look for what is around it goes on your own element around it.',
|
|
609
621
|
emptyStatePadding:
|
|
610
622
|
'{{classes}} pads {{where}}. EmptyState takes no padding (Katerina, 14 September): its room comes from the box its rows live in, so put the padding on that box.',
|
|
623
|
+
formLayout:
|
|
624
|
+
'{{classes}} lays out the fields of {{where}}. A Form places its own fields, 24px apart (Katerina, 28 September), so every form reads the same: remove it. A form that is not a list of fields (a field and its button, a composer) says `layout="free"` and lays itself out.',
|
|
611
625
|
...ESCAPE_MESSAGES,
|
|
612
626
|
},
|
|
613
627
|
},
|
|
@@ -623,6 +637,15 @@ export const noRestyledPart: Rule.RuleModule = {
|
|
|
623
637
|
return escapes.get(anchor) === true
|
|
624
638
|
}
|
|
625
639
|
|
|
640
|
+
/** A `Form` lays out its own fields unless it says `layout="free"`; a layout the rule cannot read passes. */
|
|
641
|
+
const placesItsFields = (element: Node) =>
|
|
642
|
+
!children(element, 'attributes').some((attribute) => {
|
|
643
|
+
if (attribute?.type !== 'JSXAttribute' || nameOf(child(attribute, 'name')) !== 'layout') return false
|
|
644
|
+
const value = child(attribute, 'value')
|
|
645
|
+
const literal = value?.type === 'JSXExpressionContainer' ? child(value, 'expression') : value
|
|
646
|
+
return !(literal?.type === 'Literal' && literal.value === 'fields')
|
|
647
|
+
})
|
|
648
|
+
|
|
626
649
|
const check = (element: Node, binding: PartBinding, prop: string, value: Node | null | undefined, at: Node) => {
|
|
627
650
|
const partProp = binding.props === 'all' ? prop : binding.props[prop]
|
|
628
651
|
if (!partProp || !value) return
|
|
@@ -632,7 +655,8 @@ export const noRestyledPart: Rule.RuleModule = {
|
|
|
632
655
|
const where = `${via}${partProp === 'className' ? '' : `'s \`${partProp}\``}`
|
|
633
656
|
const restyled = [...new Set(classes.filter((token) => !isPlacement(token)))]
|
|
634
657
|
const padding = binding.part === 'EmptyState' && binding.props === 'all' ? [...new Set(classes.filter(isPadding))] : []
|
|
635
|
-
|
|
658
|
+
const layout = binding.part === 'Form' && partProp === 'className' && placesItsFields(element) ? [...new Set(classes.filter(isFieldsLayout))] : []
|
|
659
|
+
if (!restyled.length && !padding.length && !layout.length) return
|
|
636
660
|
if (escaped(element)) return
|
|
637
661
|
if (restyled.length) {
|
|
638
662
|
const props = PART_LOOK_PROPS[binding.part]
|
|
@@ -640,6 +664,7 @@ export const noRestyledPart: Rule.RuleModule = {
|
|
|
640
664
|
context.report({ loc: at.loc as NonNullable<Rule.Node['loc']>, messageId: 'restyled', data: { classes: code(restyled), where, use } })
|
|
641
665
|
}
|
|
642
666
|
if (padding.length) context.report({ loc: at.loc as NonNullable<Rule.Node['loc']>, messageId: 'emptyStatePadding', data: { classes: code(padding), where } })
|
|
667
|
+
if (layout.length) context.report({ loc: at.loc as NonNullable<Rule.Node['loc']>, messageId: 'formLayout', data: { classes: code(layout), where } })
|
|
643
668
|
}
|
|
644
669
|
|
|
645
670
|
/** `style` on a part: every key it can read must be placement (B6). */
|
package/src/looks.ts
CHANGED
|
@@ -11,14 +11,16 @@
|
|
|
11
11
|
* much room it takes stay in the part.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* A field's box: TextInput and Textarea. The hairline strengthens on hover — of the box, or of anywhere in the `Field` around it, label included, so the field is one thing under the pointer (Katerina, 28 September) — and not while switched off: a label passes its hover to its box, switched off or not (FIELD_DISABLED_CLASSES holds it back). Focus outranks it, so a focused field keeps its colour under the pointer.
|
|
16
|
+
*/
|
|
17
|
+
export const FIELD_BOX_CLASSES = 'bg-bg-field border border-border-field shadow-field hover:border-border-strong group-hover/field:border-border-strong focus:border-border-focus group-hover/field:focus:border-border-focus rounded-lg'
|
|
16
18
|
|
|
17
19
|
/** What a field's words look like, typed or waiting: TextInput, Textarea and Select. */
|
|
18
20
|
export const FIELD_TEXT_CLASSES = 'text-text-primary placeholder:text-text-muted'
|
|
19
21
|
|
|
20
22
|
/** A field that cannot be used: TextInput, Textarea and Select. */
|
|
21
|
-
export const FIELD_DISABLED_CLASSES = 'disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled'
|
|
23
|
+
export const FIELD_DISABLED_CLASSES = 'disabled:pointer-events-none disabled:bg-bg-disabled disabled:text-text-disabled disabled:border-border-field group-hover/field:disabled:border-border-field'
|
|
22
24
|
|
|
23
25
|
/** The ring a focused field wears in Signal: TextInput and Textarea. */
|
|
24
26
|
export const FIELD_FOCUS_RING_CLASSES = 'signal:transition-shadow signal:focus:shadow-focus-ring'
|
|
@@ -30,7 +32,7 @@ export const FIELD_SIZE_CLASSES = {
|
|
|
30
32
|
} as const
|
|
31
33
|
|
|
32
34
|
/** The box around an input that sits inside it, before its focus look: SearchInput and ChipInput. */
|
|
33
|
-
export const FIELD_SHELL_CLASSES = 'bg-bg-field border border-border-field shadow-field hover:border-border-strong rounded-lg transition-colors'
|
|
35
|
+
export const FIELD_SHELL_CLASSES = 'bg-bg-field border border-border-field shadow-field hover:border-border-strong group-hover/field:border-border-strong rounded-lg transition-colors'
|
|
34
36
|
|
|
35
37
|
/** A panel that floats over the page: Menu's panel and Tooltip. */
|
|
36
38
|
export const FLOATING_SURFACE_CLASSES = 'rounded-lg border border-border-default bg-bg-elevated shadow-lg'
|