@estiva-app/ui 0.41.0 → 0.43.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 +16 -0
- 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.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -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 +67 -16
- package/src/ChipInput.tsx +1 -1
- package/src/CommandPalette.mdx +6 -0
- package/src/CommandPalette.stories.tsx +26 -1
- package/src/CommandPalette.test.tsx +14 -0
- package/src/CommandPalette.tsx +35 -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/index.ts +2 -0
- package/src/looks.ts +6 -4
- package/src/registry/registry.test.ts +3 -3
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,9 +4,9 @@
|
|
|
4
4
|
"kind": "package",
|
|
5
5
|
"repo": "estiva-ui",
|
|
6
6
|
"package": "@estiva-app/ui",
|
|
7
|
-
"packageVersion": "0.
|
|
8
|
-
"exports":
|
|
9
|
-
"typeExports":
|
|
7
|
+
"packageVersion": "0.43.0",
|
|
8
|
+
"exports": 91,
|
|
9
|
+
"typeExports": 102,
|
|
10
10
|
"files": null
|
|
11
11
|
},
|
|
12
12
|
"storybook": {
|
|
@@ -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
|
},
|
|
@@ -1281,6 +1280,44 @@
|
|
|
1281
1280
|
"app": null,
|
|
1282
1281
|
"looks": []
|
|
1283
1282
|
},
|
|
1283
|
+
{
|
|
1284
|
+
"name": "CommandPaletteProposal",
|
|
1285
|
+
"repo": "estiva-ui",
|
|
1286
|
+
"kind": "component",
|
|
1287
|
+
"importPath": "@estiva-app/ui",
|
|
1288
|
+
"sourceFile": "src/CommandPalette.tsx",
|
|
1289
|
+
"purpose": "Something the palette offers to make, before anything is made: a card with where it would be made on top, then what it would be.",
|
|
1290
|
+
"purposeFrom": "comment",
|
|
1291
|
+
"props": [
|
|
1292
|
+
{
|
|
1293
|
+
"name": "source",
|
|
1294
|
+
"takes": "text",
|
|
1295
|
+
"required": true,
|
|
1296
|
+
"note": "Where it would be made: the app and the action — \"App › New item\"."
|
|
1297
|
+
},
|
|
1298
|
+
{
|
|
1299
|
+
"name": "mark",
|
|
1300
|
+
"takes": "anything",
|
|
1301
|
+
"required": false,
|
|
1302
|
+
"note": "The app's mark before the source, as small as the line (about 10px)."
|
|
1303
|
+
},
|
|
1304
|
+
{
|
|
1305
|
+
"name": "children",
|
|
1306
|
+
"takes": "text",
|
|
1307
|
+
"required": true,
|
|
1308
|
+
"note": "What would be made, in a sentence."
|
|
1309
|
+
}
|
|
1310
|
+
],
|
|
1311
|
+
"variants": [],
|
|
1312
|
+
"ownsBehaviours": [],
|
|
1313
|
+
"status": "stable",
|
|
1314
|
+
"migrationStage": null,
|
|
1315
|
+
"docsId": "overlays-commandpalette--docs",
|
|
1316
|
+
"storyId": "overlays-commandpalette--first-level",
|
|
1317
|
+
"docPage": null,
|
|
1318
|
+
"app": null,
|
|
1319
|
+
"looks": []
|
|
1320
|
+
},
|
|
1284
1321
|
{
|
|
1285
1322
|
"name": "CommandPaletteQuote",
|
|
1286
1323
|
"repo": "estiva-ui",
|
|
@@ -2109,6 +2146,12 @@
|
|
|
2109
2146
|
"required": false,
|
|
2110
2147
|
"note": "Whether Enter in a one-line field sends."
|
|
2111
2148
|
},
|
|
2149
|
+
{
|
|
2150
|
+
"name": "layout",
|
|
2151
|
+
"takes": "fields | free",
|
|
2152
|
+
"required": false,
|
|
2153
|
+
"note": "`fields`, the default: everything inside stacks 24px apart."
|
|
2154
|
+
},
|
|
2112
2155
|
{
|
|
2113
2156
|
"name": "children",
|
|
2114
2157
|
"takes": "anything",
|
|
@@ -2116,7 +2159,15 @@
|
|
|
2116
2159
|
"note": null
|
|
2117
2160
|
}
|
|
2118
2161
|
],
|
|
2119
|
-
"variants": [
|
|
2162
|
+
"variants": [
|
|
2163
|
+
{
|
|
2164
|
+
"prop": "layout",
|
|
2165
|
+
"values": [
|
|
2166
|
+
"fields",
|
|
2167
|
+
"free"
|
|
2168
|
+
]
|
|
2169
|
+
}
|
|
2170
|
+
],
|
|
2120
2171
|
"ownsBehaviours": [
|
|
2121
2172
|
{
|
|
2122
2173
|
"id": "base-ui",
|
|
@@ -4596,14 +4647,14 @@
|
|
|
4596
4647
|
"docPage": "src/Select.mdx",
|
|
4597
4648
|
"app": null,
|
|
4598
4649
|
"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",
|
|
4650
|
+
"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",
|
|
4651
|
+
"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",
|
|
4652
|
+
"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",
|
|
4653
|
+
"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",
|
|
4654
|
+
"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",
|
|
4655
|
+
"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",
|
|
4656
|
+
"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",
|
|
4657
|
+
"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
4658
|
"rounded-lg border bg-bg-field shadow-field text-left"
|
|
4608
4659
|
]
|
|
4609
4660
|
},
|
|
@@ -4851,7 +4902,7 @@
|
|
|
4851
4902
|
"docPage": "src/Textarea.mdx",
|
|
4852
4903
|
"app": null,
|
|
4853
4904
|
"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"
|
|
4905
|
+
"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
4906
|
]
|
|
4856
4907
|
},
|
|
4857
4908
|
{
|
|
@@ -4892,8 +4943,8 @@
|
|
|
4892
4943
|
"docPage": "src/TextInput.mdx",
|
|
4893
4944
|
"app": null,
|
|
4894
4945
|
"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"
|
|
4946
|
+
"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",
|
|
4947
|
+
"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
4948
|
]
|
|
4898
4949
|
},
|
|
4899
4950
|
{
|
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.mdx
CHANGED
|
@@ -23,6 +23,12 @@ form — and the app keeps the levels.
|
|
|
23
23
|
above the rows, for a result that is written rather than listed: a line while
|
|
24
24
|
it is on its way, an answer with marks pointing at the rows it came from, and
|
|
25
25
|
text written to be used somewhere else.
|
|
26
|
+
- **`CommandPaletteProposal`** — above the rows, for something the palette
|
|
27
|
+
offers to make before anything is made: a card with where it would be made
|
|
28
|
+
(`source`, with the app's `mark`), then what it would be. The rows under it
|
|
29
|
+
say what happens next.
|
|
30
|
+
|
|
31
|
+
<Canvas of={CommandPaletteStories.Proposal} />
|
|
26
32
|
|
|
27
33
|
<Canvas of={CommandPaletteStories.Answer} />
|
|
28
34
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
CommandPalette,
|
|
8
8
|
CommandPaletteAnswer,
|
|
9
9
|
CommandPaletteForm,
|
|
10
|
+
CommandPaletteProposal,
|
|
10
11
|
CommandPaletteQuote,
|
|
11
12
|
CommandPaletteSearch,
|
|
12
13
|
CommandPaletteWorking,
|
|
@@ -54,6 +55,7 @@ type Level =
|
|
|
54
55
|
| { kind: 'place'; name: string }
|
|
55
56
|
| { kind: 'ask' }
|
|
56
57
|
| { kind: 'draft' }
|
|
58
|
+
| { kind: 'make' }
|
|
57
59
|
| { kind: 'form' }
|
|
58
60
|
| { kind: 'one-list' }
|
|
59
61
|
|
|
@@ -62,7 +64,7 @@ interface Frame {
|
|
|
62
64
|
query: string
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
type Start = 'first' | 'arriving' | 'nothing' | 'place' | 'working' | 'answer' | 'draft' | 'form' | 'refused' | 'one-list'
|
|
67
|
+
type Start = 'first' | 'arriving' | 'nothing' | 'place' | 'working' | 'answer' | 'draft' | 'make' | 'form' | 'refused' | 'one-list'
|
|
66
68
|
|
|
67
69
|
const PLACES = ['Place one', 'Place two']
|
|
68
70
|
const ACTIONS = [
|
|
@@ -81,6 +83,7 @@ const GROUPS = [
|
|
|
81
83
|
]
|
|
82
84
|
const ANSWER = 'Item one moved on Monday [1], and item two is waiting on a reply [2].\n\nNothing else changed this week.'
|
|
83
85
|
const DRAFT = 'Thanks, both. I will look at item one today\nand reply on item two tomorrow.'
|
|
86
|
+
const PROPOSAL = 'An item for the reply on item two, due tomorrow.'
|
|
84
87
|
|
|
85
88
|
function startStack(start: Start): Frame[] {
|
|
86
89
|
const first: Frame = { level: { kind: 'first' }, query: '' }
|
|
@@ -96,6 +99,8 @@ function startStack(start: Start): Frame[] {
|
|
|
96
99
|
return [first, { level: { kind: 'ask' }, query: 'What changed this week?' }]
|
|
97
100
|
case 'draft':
|
|
98
101
|
return [first, { level: { kind: 'draft' }, query: 'Say I will look today' }]
|
|
102
|
+
case 'make':
|
|
103
|
+
return [first, { level: { kind: 'make' }, query: '' }]
|
|
99
104
|
case 'form':
|
|
100
105
|
case 'refused':
|
|
101
106
|
return [first, { level: { kind: 'place', name: 'Place one' }, query: '' }, { level: { kind: 'form' }, query: '' }]
|
|
@@ -180,6 +185,17 @@ function Demo({ start, where, modKey, label }: { start: Start; where?: string; m
|
|
|
180
185
|
},
|
|
181
186
|
]
|
|
182
187
|
}
|
|
188
|
+
if (level.kind === 'make') {
|
|
189
|
+
return [
|
|
190
|
+
{
|
|
191
|
+
label: 'Next',
|
|
192
|
+
rows: [
|
|
193
|
+
{ id: 'make-open', label: 'Open the form', description: 'Place one › Action one · nothing is made until you submit', icon, onSelect: () => push({ kind: 'form' }) },
|
|
194
|
+
{ id: 'make-again', label: 'Suggest something else', icon, onSelect: () => close('Suggest something else') },
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
]
|
|
198
|
+
}
|
|
183
199
|
if (level.kind === 'ask' || level.kind === 'draft') {
|
|
184
200
|
if (!asked?.done) return []
|
|
185
201
|
const changed = frame.query.trim() !== asked.question.trim()
|
|
@@ -236,6 +252,7 @@ function Demo({ start, where, modKey, label }: { start: Start; where?: string; m
|
|
|
236
252
|
rows: [
|
|
237
253
|
{ id: 'ask', label: 'Ask about this page', icon, onSelect: () => push({ kind: 'ask' }) },
|
|
238
254
|
{ id: 'draft', label: 'Write a draft', icon, onSelect: () => push({ kind: 'draft' }) },
|
|
255
|
+
{ id: 'make', label: 'Make something from this page', icon, onSelect: () => push({ kind: 'make' }) },
|
|
239
256
|
{ id: 'one-list', label: 'Change kind', description: 'Item one', icon, onSelect: () => push({ kind: 'one-list' }) },
|
|
240
257
|
],
|
|
241
258
|
},
|
|
@@ -289,6 +306,7 @@ function Demo({ start, where, modKey, label }: { start: Start; where?: string; m
|
|
|
289
306
|
}
|
|
290
307
|
|
|
291
308
|
const body = (() => {
|
|
309
|
+
if (level.kind === 'make') return <CommandPaletteProposal source="Place one › Action one" mark={<IconSquareRounded size={10} stroke={1.5} />}>{PROPOSAL}</CommandPaletteProposal>
|
|
292
310
|
if (level.kind !== 'ask' && level.kind !== 'draft') return undefined
|
|
293
311
|
if (!asked || !asked.done) return <CommandPaletteWorking>Reading 12 notes…</CommandPaletteWorking>
|
|
294
312
|
const changed = frame.query.trim() !== asked.question.trim()
|
|
@@ -355,6 +373,8 @@ function Demo({ start, where, modKey, label }: { start: Start; where?: string; m
|
|
|
355
373
|
? 'Ask anything about this page'
|
|
356
374
|
: level.kind === 'draft'
|
|
357
375
|
? 'What should it say?'
|
|
376
|
+
: level.kind === 'make'
|
|
377
|
+
? 'Looking at this page'
|
|
358
378
|
: 'Search, or choose a place'
|
|
359
379
|
}
|
|
360
380
|
groups={groups}
|
|
@@ -365,6 +385,8 @@ function Demo({ start, where, modKey, label }: { start: Start; where?: string; m
|
|
|
365
385
|
? { label: 'Ask', onBack: back }
|
|
366
386
|
: level.kind === 'draft'
|
|
367
387
|
? { label: 'Draft', onBack: back }
|
|
388
|
+
: level.kind === 'make'
|
|
389
|
+
? { label: 'Make', onBack: back }
|
|
368
390
|
: undefined
|
|
369
391
|
}
|
|
370
392
|
pending={level.kind === 'first' && searching ? 'Searching…' : undefined}
|
|
@@ -412,6 +434,9 @@ export const Answer: Story = story('answer')
|
|
|
412
434
|
export const Quote: Story = story('draft')
|
|
413
435
|
|
|
414
436
|
/** A form. Ctrl+Enter creates; a missing field says so and takes focus; Backspace in an empty field goes back and keeps what was typed. */
|
|
437
|
+
/** Something it offers to make: where, then what. Nothing is made until the form is sent. */
|
|
438
|
+
export const Proposal: Story = story('make')
|
|
439
|
+
|
|
415
440
|
export const Form: Story = story('form')
|
|
416
441
|
|
|
417
442
|
/** Creating is refused: the fields unlock, the reason sits beside the button, and focus is back where it was. */
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
CommandPalette,
|
|
18
18
|
CommandPaletteAnswer,
|
|
19
19
|
CommandPaletteForm,
|
|
20
|
+
CommandPaletteProposal,
|
|
20
21
|
CommandPaletteQuote,
|
|
21
22
|
CommandPaletteSearch,
|
|
22
23
|
CommandPaletteWorking,
|
|
@@ -421,6 +422,19 @@ describe('what sits above the rows', () => {
|
|
|
421
422
|
expect(screen.getByText(/Line one/).textContent).toBe('Line one\nLine two')
|
|
422
423
|
})
|
|
423
424
|
|
|
425
|
+
it('draws a proposal as a card: where it would be made, with its mark, then what', () => {
|
|
426
|
+
const { container } = render(
|
|
427
|
+
<CommandPaletteProposal source="Place one › Action one" mark={<svg data-testid="mark" />}>
|
|
428
|
+
An item for tomorrow.
|
|
429
|
+
</CommandPaletteProposal>,
|
|
430
|
+
)
|
|
431
|
+
const lines = container.firstElementChild?.children
|
|
432
|
+
expect(lines).toHaveLength(2)
|
|
433
|
+
expect(lines?.[0].textContent).toBe('Place one › Action one')
|
|
434
|
+
expect(lines?.[0].contains(screen.getByTestId('mark'))).toBe(true)
|
|
435
|
+
expect(lines?.[1].textContent).toBe('An item for tomorrow.')
|
|
436
|
+
})
|
|
437
|
+
|
|
424
438
|
it('announces what is being worked on, with its grey bars', () => {
|
|
425
439
|
const { container } = render(<CommandPaletteWorking bars={3}>Reading…</CommandPaletteWorking>)
|
|
426
440
|
expect(screen.getByRole('status').textContent).toContain('Reading…')
|
package/src/CommandPalette.tsx
CHANGED
|
@@ -16,10 +16,11 @@ import type { BaseUIEvent } from '@base-ui/react/types'
|
|
|
16
16
|
import { IconLoader2, IconSearch } from '@tabler/icons-react'
|
|
17
17
|
import { cn } from './cn'
|
|
18
18
|
import { Button } from './Button'
|
|
19
|
+
import { Card } from './Card'
|
|
19
20
|
import { InputChip } from './ChipInput'
|
|
20
21
|
import { EmptyState } from './EmptyState'
|
|
21
22
|
import { FieldLine } from './Field'
|
|
22
|
-
import { Form } from './Form'
|
|
23
|
+
import { FIELDS_CLASSES, Form } from './Form'
|
|
23
24
|
import { Kbd } from './Kbd'
|
|
24
25
|
import { EnterHint, MenuItemBody, menuItemClassName } from './Menu'
|
|
25
26
|
import { ScrollArea } from './ScrollArea'
|
|
@@ -563,10 +564,12 @@ export function CommandPaletteForm({ chip, icon, submitLabel, onSubmit, submitWa
|
|
|
563
564
|
{/* The package Form, from the fields to the button (UIG-7). The chip row
|
|
564
565
|
stays outside it, so its ✕ still goes back while the form works; the
|
|
565
566
|
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
|
-
|
|
567
|
+
The fields are found through data-command-palette-fields. The form is
|
|
568
|
+
`free`, a scrolling body over a button row; the fields inside stack as
|
|
569
|
+
every form's do, 24px apart (Katerina, 28 September: they were 16). */}
|
|
570
|
+
<Form data-command-palette-fields="" busy={busy} enterSends={false} onSubmit={submit} layout="free" className="flex min-h-0 flex-col">
|
|
568
571
|
<ScrollArea viewportClassName="max-h-[420px]" contentClassName="flex flex-col px-5 py-4">
|
|
569
|
-
<div className=
|
|
572
|
+
<div className={FIELDS_CLASSES}>{children}</div>
|
|
570
573
|
</ScrollArea>
|
|
571
574
|
<div className="flex shrink-0 items-center gap-3 px-5 pb-4">
|
|
572
575
|
<div className="min-w-0 flex-1">
|
|
@@ -659,3 +662,31 @@ export interface CommandPaletteQuoteProps {
|
|
|
659
662
|
export function CommandPaletteQuote({ children }: CommandPaletteQuoteProps) {
|
|
660
663
|
return <p className="whitespace-pre-wrap border-l-2 border-border-default pl-3 text-body-2 text-text-primary">{children}</p>
|
|
661
664
|
}
|
|
665
|
+
|
|
666
|
+
export interface CommandPaletteProposalProps {
|
|
667
|
+
/** Where it would be made: the app and the action — "App › New item". */
|
|
668
|
+
source: string
|
|
669
|
+
/** The app's mark before the source, as small as the line (about 10px). */
|
|
670
|
+
mark?: ReactNode
|
|
671
|
+
/** What would be made, in a sentence. */
|
|
672
|
+
children: string
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Something the palette offers to make, before anything is made: a card with
|
|
677
|
+
* where it would be made on top, then what it would be. A card because it is a
|
|
678
|
+
* thing about to exist, not text to read or use; the source first because two
|
|
679
|
+
* apps may offer something alike (UIG-40, Katerina 28 September: the look Peek's
|
|
680
|
+
* launcher already had, now drawn by the palette, ruling P4).
|
|
681
|
+
*/
|
|
682
|
+
export function CommandPaletteProposal({ source, mark, children }: CommandPaletteProposalProps) {
|
|
683
|
+
return (
|
|
684
|
+
<Card fill="surface" className="flex flex-col gap-1 p-3">
|
|
685
|
+
<span className="flex items-center gap-2 text-caption text-text-secondary">
|
|
686
|
+
{mark}
|
|
687
|
+
{source}
|
|
688
|
+
</span>
|
|
689
|
+
<span className="text-body-2 text-text-primary">{children}</span>
|
|
690
|
+
</Card>
|
|
691
|
+
)
|
|
692
|
+
}
|
|
@@ -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)
|