@estiva-app/ui 0.16.1 → 0.17.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/README.md +3 -1
- package/dist/Button.d.ts.map +1 -1
- package/dist/Checkbox.d.ts +14 -1
- package/dist/Checkbox.d.ts.map +1 -1
- package/dist/FilePicker.d.ts +22 -0
- package/dist/FilePicker.d.ts.map +1 -0
- package/dist/Form.d.ts +34 -0
- package/dist/Form.d.ts.map +1 -0
- package/dist/IconButton.d.ts.map +1 -1
- package/dist/SearchInput.d.ts.map +1 -1
- package/dist/TextInput.d.ts.map +1 -1
- package/dist/Textarea.d.ts.map +1 -1
- package/dist/eslint/index.d.ts +1 -1
- package/dist/eslint/index.d.ts.map +1 -1
- package/dist/eslint/index.js +88 -8
- package/dist/eslint/index.js.map +4 -4
- package/dist/eslint/no-raw-element.d.ts +88 -0
- package/dist/eslint/no-raw-element.d.ts.map +1 -0
- package/dist/formBusy.d.ts +15 -0
- package/dist/formBusy.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +356 -239
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/src/Button.tsx +4 -1
- package/src/Checkbox.mdx +19 -1
- package/src/Checkbox.stories.tsx +12 -0
- package/src/Checkbox.test.tsx +54 -0
- package/src/Checkbox.tsx +40 -3
- package/src/FilePicker.mdx +49 -0
- package/src/FilePicker.stories.tsx +61 -0
- package/src/FilePicker.test.tsx +79 -0
- package/src/FilePicker.tsx +40 -0
- package/src/Form.mdx +68 -0
- package/src/Form.stories.tsx +68 -0
- package/src/Form.test.tsx +282 -0
- package/src/Form.tsx +113 -0
- package/src/IconButton.tsx +4 -1
- package/src/SearchInput.mdx +2 -2
- package/src/SearchInput.tsx +3 -1
- package/src/TextInput.mdx +2 -2
- package/src/TextInput.test.tsx +7 -0
- package/src/TextInput.tsx +4 -1
- package/src/Textarea.tsx +4 -1
- package/src/eslint/index.test.ts +24 -15
- package/src/eslint/index.ts +6 -5
- package/src/eslint/{no-raw-button.test.ts → no-raw-element.test.ts} +53 -8
- package/src/eslint/no-raw-element.ts +180 -0
- package/src/formBusy.ts +19 -0
- package/src/index.ts +2 -0
- package/stories/Choosing.mdx +3 -0
- package/dist/eslint/no-raw-button.d.ts +0 -11
- package/dist/eslint/no-raw-button.d.ts.map +0 -1
- package/src/eslint/no-raw-button.ts +0 -39
package/README.md
CHANGED
|
@@ -67,7 +67,9 @@ export default {
|
|
|
67
67
|
|
|
68
68
|
The UI Guardrails' rules ship with the package, as an ESLint plugin, so an app
|
|
69
69
|
gets a new rule with a version bump. Each rule names the component to use
|
|
70
|
-
instead. Today there is one: `estiva/no-raw-
|
|
70
|
+
instead. Today there is one: `estiva/no-raw-element`, which refuses a raw
|
|
71
|
+
interactive element (`<a>`, `<input>`, `<form>`, `<dialog>`…) and names the part to
|
|
72
|
+
use, or says the package has none yet.
|
|
71
73
|
|
|
72
74
|
```js
|
|
73
75
|
// eslint.config.js — alongside your own rules
|
package/dist/Button.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../src/Button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../src/Button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAM7D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,aAAa,CAAA;AAC5E,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,OAAO,CAAA;AAE5C;;;;;;;;;GASG;AACH,MAAM,WAAW,WAAY,SAAQ,qBAAqB,CAAC,QAAQ,CAAC;IAClE,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,IAAI,CAAC,EAAE,UAAU,CAAA;IACjB,2DAA2D;IAC3D,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB;6EACyE;IACzE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED,wBAAgB,MAAM,CAAC,EACrB,OAAiB,EACjB,IAAgB,EAChB,WAAW,EACX,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,cAAc,EACd,IAAe,EACf,GAAG,KAAK,EACT,EAAE,WAAW,+BAkDb"}
|
package/dist/Checkbox.d.ts
CHANGED
|
@@ -20,15 +20,28 @@
|
|
|
20
20
|
* "it should be fixed"). With the tick always there, Base UI's `<span>` and
|
|
21
21
|
* the `<button>` this used to be land on the same pixel, measured. In a flex
|
|
22
22
|
* row nothing ever moved.
|
|
23
|
+
*
|
|
24
|
+
* With `label`, the words sit beside the box and are part of the target: Base
|
|
25
|
+
* UI's `Field`, its `Field.Label` around the box and the words — the way its
|
|
26
|
+
* Checkbox page labels a checkbox inside a form. The class list is Peek's
|
|
27
|
+
* Read state panel's, where it was written by hand (UIG-7, 16 September). The
|
|
28
|
+
* words keep their colour when the box is disabled (Katerina: "no need").
|
|
29
|
+
*
|
|
30
|
+
* Inside a busy `Form` it is disabled, and looks it (`formBusy.ts`).
|
|
23
31
|
*/
|
|
24
32
|
export interface CheckboxProps {
|
|
25
33
|
checked: boolean;
|
|
26
34
|
onChange?: (checked: boolean) => void;
|
|
27
35
|
disabled?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Words beside the box. With `onChange`, clicking them toggles it too, and
|
|
38
|
+
* they name it, so no `aria-label` is needed. `className` stays on the box.
|
|
39
|
+
*/
|
|
40
|
+
label?: string;
|
|
28
41
|
'aria-label'?: string;
|
|
29
42
|
/** Set by a `Field` with `required`; a caller inside one owes nothing. */
|
|
30
43
|
'aria-required'?: boolean | 'true' | 'false';
|
|
31
44
|
className?: string;
|
|
32
45
|
}
|
|
33
|
-
export declare function Checkbox({ checked, onChange, disabled, className, ...aria }: CheckboxProps): import("react").JSX.Element;
|
|
46
|
+
export declare function Checkbox({ checked, onChange, disabled: ownDisabled, label, className, ...aria }: CheckboxProps): import("react").JSX.Element;
|
|
34
47
|
//# sourceMappingURL=Checkbox.d.ts.map
|
package/dist/Checkbox.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Checkbox.d.ts","sourceRoot":"","sources":["../src/Checkbox.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Checkbox.d.ts","sourceRoot":"","sources":["../src/Checkbox.tsx"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAA;IAChB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,0EAA0E;IAC1E,eAAe,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAA;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAiBD,wBAAgB,QAAQ,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,WAAmB,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,EAAE,aAAa,+BA6CtH"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type InputHTMLAttributes } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* The browser's file picker, with nothing drawn: a hidden `<input type="file">`
|
|
4
|
+
* that the caller's own button opens with `ref.current.click()` (UIG-7,
|
|
5
|
+
* 16 September). Base UI has no part for it.
|
|
6
|
+
*
|
|
7
|
+
* Peek's paperclip and Ship's two wrote this by hand, each with the same two
|
|
8
|
+
* details — the files handed over as they were chosen, and the value cleared
|
|
9
|
+
* so the same file can be chosen again after a failure or after it was
|
|
10
|
+
* removed. Those are here once.
|
|
11
|
+
*
|
|
12
|
+
* Hidden from the accessibility tree and from Tab: the button that opens it is
|
|
13
|
+
* the control, and two things both named "Attach" is one more than a screen
|
|
14
|
+
* reader should find. A `display: none` input still opens its picker on a
|
|
15
|
+
* scripted click.
|
|
16
|
+
*/
|
|
17
|
+
export interface FilePickerProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'onChange' | 'value' | 'defaultValue' | 'className' | 'style' | 'children'> {
|
|
18
|
+
/** The chosen files, in the order the picker gave them. Not called when the picker is closed with nothing chosen. */
|
|
19
|
+
onPick: (files: File[]) => void;
|
|
20
|
+
}
|
|
21
|
+
export declare const FilePicker: import("react").ForwardRefExoticComponent<FilePickerProps & import("react").RefAttributes<HTMLInputElement>>;
|
|
22
|
+
//# sourceMappingURL=FilePicker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FilePicker.d.ts","sourceRoot":"","sources":["../src/FilePicker.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,mBAAmB,EAAE,MAAM,OAAO,CAAA;AAE5D;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,eAAgB,SAAQ,IAAI,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,cAAc,GAAG,WAAW,GAAG,OAAO,GAAG,UAAU,CAAC;IACvK,qHAAqH;IACrH,MAAM,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAA;CAChC;AAED,eAAO,MAAM,UAAU,8GAiBrB,CAAA"}
|
package/dist/Form.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type FormHTMLAttributes, type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* A form: Enter in a field, or a submit button, sends it, and the page never
|
|
4
|
+
* reloads. On Base UI's `Form` (UIG-7, 16 September), which the apps' five
|
|
5
|
+
* forms had each written around a plain `<form>`: `preventDefault`, a busy
|
|
6
|
+
* flag, and `disabled={busy}` on every field one by one.
|
|
7
|
+
*
|
|
8
|
+
* Base UI's part, as it renders it: a `<form noValidate>` — the browser's own
|
|
9
|
+
* validation bubbles are off, which no field in either app used — that, on
|
|
10
|
+
* submit, checks every `Field` inside and moves focus to the first one that is
|
|
11
|
+
* invalid instead of sending. `Field`'s `error` is still how an error shows.
|
|
12
|
+
*
|
|
13
|
+
* `busy` is ours. While it is on, the children sit in a disabled
|
|
14
|
+
* `<fieldset>` (Base UI's `Fieldset`, with no box of its own), so every field
|
|
15
|
+
* and button inside is switched off at once — the package's Button,
|
|
16
|
+
* IconButton and Checkbox wearing their own switched-off look, which a
|
|
17
|
+
* fieldset alone does not give them (`formBusy.ts`) — and focus waits on the form
|
|
18
|
+
* rather than falling to the page. Chrome drops focus to `<body>` the moment
|
|
19
|
+
* the focused field is disabled, before any effect runs, and jsdom does not —
|
|
20
|
+
* so the form remembers the last element inside it that had focus, and takes
|
|
21
|
+
* focus back from the page on its behalf (measured in Chrome, 16 September).
|
|
22
|
+
* When `busy` ends, focus goes to the first invalid field, else to what sent
|
|
23
|
+
* the form, else to the first control. That is `CommandPalette`'s order
|
|
24
|
+
* too, so the two read the same (UIG-29).
|
|
25
|
+
*/
|
|
26
|
+
export interface FormProps extends Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit' | 'children' | 'noValidate'> {
|
|
27
|
+
/** Enter in a field, or a submit button. The page's own submit is already prevented. */
|
|
28
|
+
onSubmit: () => void | Promise<void>;
|
|
29
|
+
/** While sending: every field and button inside is switched off, and focus waits on the form. */
|
|
30
|
+
busy?: boolean;
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
}
|
|
33
|
+
export declare function Form({ onSubmit, busy: ownBusy, className, children, ...props }: FormProps): import("react").JSX.Element;
|
|
34
|
+
//# sourceMappingURL=Form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Form.d.ts","sourceRoot":"","sources":["../src/Form.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,kBAAkB,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAMxF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,SAAU,SAAQ,IAAI,CAAC,kBAAkB,CAAC,eAAe,CAAC,EAAE,UAAU,GAAG,UAAU,GAAG,YAAY,CAAC;IAClH,wFAAwF;IACxF,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACpC,iGAAiG;IACjG,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,SAAS,CAAA;CACpB;AAQD,wBAAgB,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAe,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,EAAE,SAAS,+BAoEjG"}
|
package/dist/IconButton.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IconButton.d.ts","sourceRoot":"","sources":["../src/IconButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"IconButton.d.ts","sourceRoot":"","sources":["../src/IconButton.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAM7D;;;;;;;;;GASG;AACH,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,CAAA;AAEhE,2EAA2E;AAC3E,MAAM,WAAW,eAAgB,SAAQ,qBAAqB,CAAC,QAAQ,CAAC;IACtE,OAAO,CAAC,EAAE,iBAAiB,CAAA;IAC3B,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;8DAC0D;IAC1D,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,gBAAgB,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAA;IACnC;sEACkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,kCAAkC;IAClC,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED,wBAAgB,UAAU,CAAC,EACzB,OAAiB,EACjB,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,cAAc,EACd,OAAO,EACP,eAAe,EACf,gBAAgB,EAChB,IAAe,EACf,GAAG,KAAK,EACT,EAAE,eAAe,+BAoDjB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SearchInput.d.ts","sourceRoot":"","sources":["../src/SearchInput.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,mBAAmB,EAAE,MAAM,OAAO,CAAA;AAKhD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,WAAW,CAAC;IAChG,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAuB,EAAE,GAAG,KAAK,EAAE,EAAE,gBAAgB,+
|
|
1
|
+
{"version":3,"file":"SearchInput.d.ts","sourceRoot":"","sources":["../src/SearchInput.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,mBAAmB,EAAE,MAAM,OAAO,CAAA;AAKhD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,gBAAiB,SAAQ,IAAI,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,WAAW,CAAC;IAChG,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAuB,EAAE,GAAG,KAAK,EAAE,EAAE,gBAAgB,+BAoBvG"}
|
package/dist/TextInput.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextInput.d.ts","sourceRoot":"","sources":["../src/TextInput.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,mBAAmB,EAAE,MAAM,OAAO,CAAA;AAI5D;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,cAAe,SAAQ,IAAI,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACzF;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,SAAS,GAAG,OAAO,CAAA;CAC3B;AAED,eAAO,MAAM,SAAS,
|
|
1
|
+
{"version":3,"file":"TextInput.d.ts","sourceRoot":"","sources":["../src/TextInput.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,mBAAmB,EAAE,MAAM,OAAO,CAAA;AAI5D;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,cAAe,SAAQ,IAAI,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACzF;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,SAAS,GAAG,OAAO,CAAA;CAC3B;AAED,eAAO,MAAM,SAAS,6GAsBpB,CAAA"}
|
package/dist/Textarea.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Textarea.d.ts","sourceRoot":"","sources":["../src/Textarea.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,sBAAsB,EAAE,MAAM,OAAO,CAAA;AAI/D;;;;;;;;;;GAUG;AACH,MAAM,MAAM,aAAa,GAAG,sBAAsB,CAAC,mBAAmB,CAAC,CAAA;AAEvE,eAAO,MAAM,QAAQ,+
|
|
1
|
+
{"version":3,"file":"Textarea.d.ts","sourceRoot":"","sources":["../src/Textarea.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,sBAAsB,EAAE,MAAM,OAAO,CAAA;AAI/D;;;;;;;;;;GAUG;AACH,MAAM,MAAM,aAAa,GAAG,sBAAsB,CAAC,mBAAmB,CAAC,CAAA;AAEvE,eAAO,MAAM,QAAQ,+GAmBnB,CAAA"}
|
package/dist/eslint/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ declare const plugin: {
|
|
|
12
12
|
'no-hand-rolled-behaviour': import("eslint").Rule.RuleModule;
|
|
13
13
|
'component-has-a-page': import("eslint").Rule.RuleModule;
|
|
14
14
|
'component-has-a-story': import("eslint").Rule.RuleModule;
|
|
15
|
-
'no-raw-
|
|
15
|
+
'no-raw-element': import("eslint").Rule.RuleModule;
|
|
16
16
|
};
|
|
17
17
|
configs: {
|
|
18
18
|
recommended: Linter.Config;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/eslint/index.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAM5C,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAA;AAIlG,2FAA2F;AAC3F,eAAO,MAAM,UAAU,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/eslint/index.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAM5C,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,UAAU,CAAA;AAIlG,2FAA2F;AAC3F,eAAO,MAAM,UAAU,WAAW,CAAA;AA+BlC,QAAA,MAAM,MAAM;;;;;;;;;;;;aAGK;QAAE,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC,MAAM,CAAA;KAAE;CACrE,CAAA;AAEzB,qFAAqF;AACrF,eAAO,MAAM,YAAY,UAA+D,CAAA;AACxF,wFAAwF;AACxF,eAAO,MAAM,gBAAgB,UAAmE,CAAA;AA0BhG,eAAe,MAAM,CAAA;AAErB,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAA;IACpC;;;OAGG;IACH,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC/D;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,UAAU,EAAE,EAAE,IAAI,GAAE,SAAS,MAAM,EAAiB,GAAG,SAAS,CAmB1G"}
|
package/dist/eslint/index.js
CHANGED
|
@@ -158,14 +158,92 @@ var noHandRolledBehaviour = {
|
|
|
158
158
|
}
|
|
159
159
|
};
|
|
160
160
|
|
|
161
|
-
// src/eslint/no-raw-
|
|
162
|
-
var
|
|
161
|
+
// src/eslint/no-raw-element.ts
|
|
162
|
+
var RAW_ELEMENT_PARTS = {
|
|
163
|
+
a: { use: "Link", more: " For a chip, `InlineChip`; for a whole card, `Card` with `href`." },
|
|
164
|
+
button: { use: "Button" },
|
|
165
|
+
textarea: { use: "Textarea" },
|
|
166
|
+
select: { use: "Select" },
|
|
167
|
+
dialog: { use: "DialogShell", more: " To ask yes or no, `ConfirmDialog`." },
|
|
168
|
+
label: { use: "Field", more: " For words beside a tick box, `Checkbox` with `label`." },
|
|
169
|
+
details: { use: "CollapsibleSection" },
|
|
170
|
+
progress: { use: "ProgressBar" },
|
|
171
|
+
form: { use: "Form" },
|
|
172
|
+
meter: null,
|
|
173
|
+
iframe: null,
|
|
174
|
+
embed: null,
|
|
175
|
+
object: null,
|
|
176
|
+
audio: null,
|
|
177
|
+
video: null,
|
|
178
|
+
img: null
|
|
179
|
+
};
|
|
180
|
+
var RAW_INPUT_PARTS = {
|
|
181
|
+
search: { use: "SearchInput" },
|
|
182
|
+
checkbox: { use: "Checkbox" },
|
|
183
|
+
file: { use: "FilePicker" },
|
|
184
|
+
submit: { use: "Button" },
|
|
185
|
+
button: { use: "Button" },
|
|
186
|
+
reset: { use: "Button" },
|
|
187
|
+
image: { use: "Button" },
|
|
188
|
+
radio: null,
|
|
189
|
+
range: null,
|
|
190
|
+
color: null,
|
|
191
|
+
date: null,
|
|
192
|
+
"datetime-local": null,
|
|
193
|
+
month: null,
|
|
194
|
+
week: null,
|
|
195
|
+
time: null
|
|
196
|
+
};
|
|
197
|
+
var TEXT_INPUT = { use: "TextInput" };
|
|
198
|
+
var ANY_INPUT = {
|
|
199
|
+
use: "TextInput",
|
|
200
|
+
more: " For a search, `SearchInput`; for a tick box, `Checkbox`; to pick files, `FilePicker`."
|
|
201
|
+
};
|
|
202
|
+
function attribute(element, name) {
|
|
203
|
+
return element.attributes.find((a) => a.type === "JSXAttribute" && a.name.name === name);
|
|
204
|
+
}
|
|
205
|
+
function literal(attr) {
|
|
206
|
+
const value = attr.value;
|
|
207
|
+
if (value === null) return true;
|
|
208
|
+
if (value.type === "Literal") return typeof value.value === "string" ? value.value : void 0;
|
|
209
|
+
if (value.type !== "JSXExpressionContainer" || !value.expression) return void 0;
|
|
210
|
+
const expression = value.expression;
|
|
211
|
+
if (expression.type === "Literal" && (typeof expression.value === "string" || typeof expression.value === "boolean")) return expression.value;
|
|
212
|
+
if (expression.type === "TemplateLiteral" && expression.expressions?.length === 0) return expression.quasis?.[0]?.value.cooked ?? void 0;
|
|
213
|
+
return void 0;
|
|
214
|
+
}
|
|
215
|
+
function isOn(element, name) {
|
|
216
|
+
const attr = attribute(element, name);
|
|
217
|
+
return attr !== void 0 && literal(attr) !== false;
|
|
218
|
+
}
|
|
219
|
+
function classify(element) {
|
|
220
|
+
if (element.name.type !== "JSXIdentifier" || !element.name.name) return null;
|
|
221
|
+
const name = element.name.name;
|
|
222
|
+
if (name === "input") {
|
|
223
|
+
const attr = attribute(element, "type");
|
|
224
|
+
const type = attr ? literal(attr) : "text";
|
|
225
|
+
if (typeof type !== "string") return { report: "raw", element: "<input>", part: ANY_INPUT };
|
|
226
|
+
const key = type.toLowerCase();
|
|
227
|
+
if (key === "hidden") return null;
|
|
228
|
+
if (!(key in RAW_INPUT_PARTS)) return { report: "raw", element: "<input>", part: TEXT_INPUT };
|
|
229
|
+
const part2 = RAW_INPUT_PARTS[key];
|
|
230
|
+
const shown = `<input type="${key}">`;
|
|
231
|
+
return part2 ? { report: "raw", element: shown, part: part2 } : { report: "noPart", element: shown };
|
|
232
|
+
}
|
|
233
|
+
if (!Object.hasOwn(RAW_ELEMENT_PARTS, name)) return null;
|
|
234
|
+
if ((name === "audio" || name === "video") && !isOn(element, "controls")) return null;
|
|
235
|
+
if (name === "img" && !attribute(element, "usemap") && !attribute(element, "useMap")) return null;
|
|
236
|
+
const part = RAW_ELEMENT_PARTS[name];
|
|
237
|
+
return part ? { report: "raw", element: `<${name}>`, part } : { report: "noPart", element: `<${name}>` };
|
|
238
|
+
}
|
|
239
|
+
var noRawElement = {
|
|
163
240
|
meta: {
|
|
164
241
|
type: "problem",
|
|
165
|
-
docs: { description: "A raw
|
|
242
|
+
docs: { description: "A raw interactive element where @estiva-app/ui has a part, or where it needs one" },
|
|
166
243
|
schema: [],
|
|
167
244
|
messages: {
|
|
168
|
-
raw: "Use `
|
|
245
|
+
raw: "Use `{{use}}` from @estiva-app/ui instead of a raw {{element}}.{{more}}",
|
|
246
|
+
noPart: "@estiva-app/ui has no part for a raw {{element}} yet. Do not build one here: ask Katerina, and it gets made in @estiva-app/ui.",
|
|
169
247
|
...ESCAPE_MESSAGES
|
|
170
248
|
}
|
|
171
249
|
},
|
|
@@ -174,9 +252,11 @@ var noRawButton = {
|
|
|
174
252
|
// ESLint's types know ESTree's nodes, not JSX's; the parser hands this one over.
|
|
175
253
|
JSXOpeningElement(node) {
|
|
176
254
|
const element = node;
|
|
177
|
-
|
|
255
|
+
const found = classify(element);
|
|
256
|
+
if (!found) return;
|
|
178
257
|
if (isEscaped(context, element)) return;
|
|
179
|
-
context.report({ loc: element.loc, messageId: "
|
|
258
|
+
if (found.report === "noPart") context.report({ loc: element.loc, messageId: "noPart", data: { element: found.element } });
|
|
259
|
+
else context.report({ loc: element.loc, messageId: "raw", data: { element: found.element, use: found.part.use, more: found.part.more ?? "" } });
|
|
180
260
|
}
|
|
181
261
|
};
|
|
182
262
|
}
|
|
@@ -222,7 +302,7 @@ var rawElementOutsideAWrapper = {
|
|
|
222
302
|
var { version } = createRequire(import.meta.url)("../../package.json");
|
|
223
303
|
var PLUGIN_KEY = "estiva";
|
|
224
304
|
var appRules = {
|
|
225
|
-
"no-raw-
|
|
305
|
+
"no-raw-element": noRawElement
|
|
226
306
|
};
|
|
227
307
|
var packageRules = {
|
|
228
308
|
"raw-element-outside-a-wrapper": rawElementOutsideAWrapper,
|
|
@@ -241,7 +321,7 @@ var PACKAGE_RULE_IDS = Object.keys(packageRules).map((name) => `${PLUGIN_KEY}/${
|
|
|
241
321
|
plugin.configs.recommended = {
|
|
242
322
|
name: "@estiva-app/ui/recommended",
|
|
243
323
|
plugins: { [PLUGIN_KEY]: plugin },
|
|
244
|
-
rules: { [`${PLUGIN_KEY}/no-raw-
|
|
324
|
+
rules: { [`${PLUGIN_KEY}/no-raw-element`]: "error" }
|
|
245
325
|
};
|
|
246
326
|
plugin.configs.strict = {
|
|
247
327
|
name: "@estiva-app/ui/strict",
|
package/dist/eslint/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../src/eslint/index.ts", "../../src/eslint/has-a-page-and-a-story.ts", "../../src/eslint/escape.ts", "../../src/eslint/no-hand-rolled-behaviour.ts", "../../src/eslint/no-raw-
|
|
4
|
-
"sourcesContent": ["/// <reference types=\"node\" />\n/**\n * `@estiva-app/ui/eslint` \u2014 the UI Guardrails' lint rules, as a plugin (UIG-3,\n * seam S1 in docs/GATES.md \u00A716).\n *\n * A plugin rather than config, so every app \u2014 Peek, Ship, Leaf \u2014 gets a new\n * rule through an ordinary version bump, and an app's editor hook runs the\n * very rule code its CI runs.\n *\n * import estiva from '@estiva-app/ui/eslint'\n * export default [{ files: ['src/**\\/*.tsx'], ...estiva.configs.recommended }]\n *\n * Register it under `estiva` (the configs do): the rule ids are\n * `estiva/<rule>`, and `countGates` counts those ids.\n *\n * Built on its own by build.mjs, for Node, into `dist/eslint/`; nothing here\n * reaches the components' browser bundle.\n */\nimport { createRequire } from 'node:module'\nimport type { ESLint, Linter } from 'eslint'\nimport { componentHasAPage, componentHasAStory } from './has-a-page-and-a-story'\nimport { noHandRolledBehaviour } from './no-hand-rolled-behaviour'\nimport { noRawButton } from './no-raw-button'\nimport { rawElementOutsideAWrapper } from './raw-element-outside-a-wrapper'\n\nexport { ESCAPE_MARKER, MIN_REASON, SETTINGS_KEY, isEscaped, type EstivaSettings } from './escape'\n\nconst { version } = createRequire(import.meta.url)('../../package.json') as { version: string }\n\n/** The name the configs register the plugin under, so every rule id is `estiva/<rule>`. */\nexport const PLUGIN_KEY = 'estiva'\n\n/**\n * The rules an **app** runs: they say an app must not build what the package\n * already has. `recommended` and `strict` carry these and only these.\n */\nconst appRules = {\n 'no-raw-button': noRawButton,\n}\n\n/**\n * The rules the **package itself** runs, pointed inward (UIG-5): don't bury a\n * raw element inside a component, don't rebuild what Base UI owns, don't ship a\n * component without a page or a story.\n *\n * They are in `configs.package`, never in `recommended`, on purpose. Peek and\n * Ship spread `recommended`, so a rule added here must not arrive in an app\n * with the next version bump: an app is full of raw elements it is allowed to\n * have until UIG-7, and has no `.mdx` pages at all. `index.test.ts` holds the\n * apps' list to exactly the app rules.\n */\nconst packageRules = {\n 'raw-element-outside-a-wrapper': rawElementOutsideAWrapper,\n 'no-hand-rolled-behaviour': noHandRolledBehaviour,\n 'component-has-a-page': componentHasAPage,\n 'component-has-a-story': componentHasAStory,\n}\n\nconst rules = { ...appRules, ...packageRules }\n\nconst plugin = {\n meta: { name: '@estiva-app/ui/eslint', version },\n rules,\n configs: {} as { recommended: Linter.Config; strict: Linter.Config; package: Linter.Config },\n} satisfies ESLint.Plugin\n\n/** The ids `recommended` and `strict` carry \u2014 what an app's gate runs and counts. */\nexport const APP_RULE_IDS = Object.keys(appRules).map((name) => `${PLUGIN_KEY}/${name}`)\n/** The ids `package` carries \u2014 what this package's own gate runs and counts (UIG-5). */\nexport const PACKAGE_RULE_IDS = Object.keys(packageRules).map((name) => `${PLUGIN_KEY}/${name}`)\n\n/**\n * `recommended` switches every **app** rule on at the level it was ruled at: an\n * error blocks, a warning is reported and never blocks. `strict` makes every app\n * rule an error. With one rule, an error, the two are the same today; they part\n * when the first warning-level rule arrives (UIG-25).\n *\n * `package` is the inward set (UIG-5), which only this package runs.\n */\nplugin.configs.recommended = {\n name: '@estiva-app/ui/recommended',\n plugins: { [PLUGIN_KEY]: plugin },\n rules: { [`${PLUGIN_KEY}/no-raw-button`]: 'error' },\n}\nplugin.configs.strict = {\n name: '@estiva-app/ui/strict',\n plugins: { [PLUGIN_KEY]: plugin },\n rules: Object.fromEntries(APP_RULE_IDS.map((id) => [id, 'error'])),\n}\nplugin.configs.package = {\n name: '@estiva-app/ui/package',\n plugins: { [PLUGIN_KEY]: plugin },\n rules: Object.fromEntries(PACKAGE_RULE_IDS.map((id) => [id, 'error'])),\n}\n\nexport default plugin\n\nexport interface GateRuleCount {\n errors: number\n warnings: number\n escapes: number\n}\n\nexport interface GateCount {\n /** Per rule id, including rules with nothing to report. */\n rules: Record<string, GateRuleCount>\n /**\n * Reports of these rules that an `eslint-disable` directive silenced. Not\n * an escape: a gate fails on any of these (docs/GATES.md \u00A716, S4).\n */\n disabled: { filePath: string; line: number; ruleId: string }[]\n}\n\n/**\n * Count what a lint of these rules found, for `.gates-count.json` (seam S3).\n *\n * Run the lint with `settings: { estiva: { reportEscapes: true } }` for the\n * escapes to be counted; without it they are silent and count 0. A marker\n * with no reason, or inside a directive, counts as an error of its rule.\n *\n * `seed` is which rules the count lists when they found nothing, and it is the\n * app rules unless a caller says otherwise: an app's count file must not gain\n * rows for the inward rules (UIG-5) that its gate does not run. This package's\n * own count script passes `PACKAGE_RULE_IDS`.\n */\nexport function countGates(results: ESLint.LintResult[], seed: readonly string[] = APP_RULE_IDS): GateCount {\n const counts: Record<string, GateRuleCount> = Object.fromEntries(seed.map((id) => [id, { errors: 0, warnings: 0, escapes: 0 }]))\n const disabled: GateCount['disabled'] = []\n const ours = (ruleId: string | null | undefined): ruleId is string => typeof ruleId === 'string' && ruleId.startsWith(`${PLUGIN_KEY}/`)\n for (const result of results) {\n for (const message of result.messages) {\n // Seeded or not: every rule of this plugin that reported is counted, so a\n // lint of a set the caller did not name is still counted in full.\n if (!ours(message.ruleId)) continue\n const count = (counts[message.ruleId] ??= { errors: 0, warnings: 0, escapes: 0 })\n if (message.messageId === 'escaped') count.escapes += 1\n else if (message.severity === 2) count.errors += 1\n else count.warnings += 1\n }\n for (const message of result.suppressedMessages ?? []) {\n if (ours(message.ruleId)) disabled.push({ filePath: result.filePath, line: message.line, ruleId: message.ruleId })\n }\n }\n return { rules: counts, disabled }\n}\n", "import { existsSync } from 'node:fs'\nimport type { Rule } from 'eslint'\nimport { ESCAPE_MESSAGES, isEscaped } from './escape'\n\n/**\n * A component of the package with no page, and one with no story (UIG-5, P2 and\n * P3).\n *\n * A component nobody can read about is a component nobody uses correctly, and\n * one nobody can look at is one nobody reviews. Both were at zero when these\n * were switched on; they are here so the next component cannot arrive without\n * them.\n *\n * The pair is by file name, beside the component, which is how this package is\n * laid out: `Button.tsx`, `Button.mdx`, `Button.stories.tsx`, `Button.test.tsx`\n * side by side in `src/`.\n *\n * **The false positive to avoid** (UIG-1 confirmed it is real): `FieldLine` and\n * `MenuItem` have a page and a story but no `.tsx` of their own \u2014 they are\n * exported from a sibling. These rules read a component file and ask for its\n * page and its story, never the other way round, so a page without a component\n * is not this rule's business.\n *\n * A file that stays without one says why at its top: `// @estiva-escape: <why>`.\n */\ninterface ProgramNode {\n body: { loc?: Rule.Node['loc']; range?: [number, number] }[]\n loc: NonNullable<Rule.Node['loc']>\n range: [number, number]\n}\n\n/** A component file: a `.tsx` in the package's source that is not a story or a test. */\nfunction componentFile(filename: string): boolean {\n return filename.endsWith('.tsx') && !/\\.(stories|test)\\.tsx$/.test(filename)\n}\n\nfunction sibling(filename: string, extension: string): string {\n return filename.replace(/\\.tsx$/, extension)\n}\n\n/**\n * Report on the file's first statement, so the escape is a comment at the top of\n * the file \u2014 the only place a reason for a missing page could go.\n */\nfunction reportOnFile(context: Rule.RuleContext, program: ProgramNode, messageId: string, data: Record<string, string>): void {\n const anchor = program.body[0] ?? program\n if (anchor.loc && anchor.range && isEscaped(context, { loc: anchor.loc, range: anchor.range })) return\n context.report({ loc: anchor.loc ?? program.loc, messageId, data })\n}\n\nexport const componentHasAPage: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: { description: 'Every component of the package has a usage page beside it' },\n schema: [],\n messages: {\n missing: '{{name}} has no page. Write {{page}} beside it \u2014 what it is, when, when not, how, and what it owns \u2014 or say why not at the top of the file.',\n ...ESCAPE_MESSAGES,\n },\n },\n create(context) {\n return {\n Program(node) {\n const filename = context.filename\n if (!componentFile(filename)) return\n const page = sibling(filename, '.mdx')\n if (existsSync(page)) return\n const name = filename.split(/[\\\\/]/).pop() ?? filename\n reportOnFile(context, node as unknown as ProgramNode, 'missing', { name, page: page.split(/[\\\\/]/).pop() ?? page })\n },\n }\n },\n}\n\nexport const componentHasAStory: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: { description: 'Every component of the package has a story beside it' },\n schema: [],\n messages: {\n missing: '{{name}} has no story. Write {{story}} beside it, so it can be seen and reviewed, or say why not at the top of the file.',\n ...ESCAPE_MESSAGES,\n },\n },\n create(context) {\n return {\n Program(node) {\n const filename = context.filename\n if (!componentFile(filename)) return\n const story = sibling(filename, '.stories.tsx')\n if (existsSync(story)) return\n const name = filename.split(/[\\\\/]/).pop() ?? filename\n reportOnFile(context, node as unknown as ProgramNode, 'missing', { name, story: story.split(/[\\\\/]/).pop() ?? story })\n },\n }\n },\n}\n", "import type { AST, Rule } from 'eslint'\n\n/**\n * The escape marker (UIG-3, seam S4 in docs/GATES.md \u00A716): one written reason\n * why one element stays as it is.\n *\n * A line comment directly above the element,\n *\n * // @estiva-escape: <reason>\n *\n * or, where the element is a JSX child, the JSX comment on the line above,\n *\n * {/* @estiva-escape: <reason> *\\/}\n *\n * with at least ten characters of reason, spaces not counted.\n *\n * Not `eslint-disable`. A directive switches a rule off without saying it was\n * meant, and the count of escapes (`.gates-count.json`) could not see it. A\n * marker written inside a directive that switches one of these rules off is\n * reported for that reason.\n *\n * The token lint's older notes (`eslint-disable-next-line <rule> -- @estiva-escape:\n * <reason>`, Katerina's ruling A2 of 15 September) are not read here: they\n * escape the token lint's rules, which are not this plugin's, and they stay as\n * they are (Katerina, 15 September).\n */\nexport const ESCAPE_MARKER = '@estiva-escape'\n\n/** Characters of reason, spaces not counted. */\nexport const MIN_REASON = 10\n\n/** The key under ESLint's `settings` this plugin reads. */\nexport const SETTINGS_KEY = 'estiva'\n\n/**\n * Every rule spreads these into its `meta.messages`, because `isEscaped`\n * reports through the rule that called it.\n */\nexport const ESCAPE_MESSAGES = {\n escapeWithoutReason: `An escape needs its reason, at least ${MIN_REASON} characters: \\`// ${ESCAPE_MARKER}: <why this stays>\\`. An escape with no reason hides nothing.`,\n escapeInDirective: `Write the escape as its own comment on the line above: \\`// ${ESCAPE_MARKER}: <reason>\\`. Inside an eslint-disable comment it switches the rule off instead of recording why.`,\n escaped: 'Escaped: {{reason}}',\n} as const\n\nexport interface EstivaSettings {\n /**\n * Report every sanctioned escape as a message with the id `escaped`, so a\n * count can read them. Off in the lint anyone runs; on only in the count.\n */\n reportEscapes?: boolean\n}\n\ninterface Located {\n loc?: AST.SourceLocation | null\n range?: [number, number]\n}\n\nconst DIRECTIVE = /^\\s*eslint-disable(?:-next-line|-line)?(?=\\s|$)([^]*)$/\n\n/** The rules a directive names: what comes before its ` -- ` description. None means every rule. */\nfunction directiveRules(rest: string): string[] {\n const dashes = rest.search(/(?:^|\\s)--(?:\\s|$)/)\n return (dashes === -1 ? rest : rest.slice(0, dashes)).split(/[\\s,]+/).filter(Boolean)\n}\n\n/** Only spaces and a JSX expression's closing brace between the marker and the element. */\nconst BETWEEN = /^[\\s}]*$/\n\n/**\n * Whether the element at `node` carries a valid escape on the line above.\n *\n * Every rule of this plugin calls it before it reports. It reports, through\n * that rule, a marker with too short a reason and a marker inside an\n * eslint-disable directive for this rule; either way the element is not\n * escaped, so the rule reports it too.\n */\nexport function isEscaped(context: Rule.RuleContext, node: Located): boolean {\n const { sourceCode } = context\n if (!node.loc || !node.range) return false\n const line = node.loc.start.line\n const nodeStart = node.range[0]\n\n const comment = sourceCode\n .getAllComments()\n .find((c) => c.loc && c.range && c.loc.end.line === line - 1 && BETWEEN.test(sourceCode.text.slice(c.range[1], nodeStart)))\n if (!comment?.loc) return false\n\n const at = comment.value.indexOf(ESCAPE_MARKER)\n if (at === -1) return false\n\n const directive = DIRECTIVE.exec(comment.value)\n if (directive) {\n const rules = directiveRules(directive[1])\n // A directive for other rules (the token lint's notes) is not an escape of this one.\n if (rules.length > 0 && !rules.includes(context.id)) return false\n context.report({ loc: comment.loc, messageId: 'escapeInDirective' })\n return false\n }\n\n const reason = comment.value\n .slice(at + ESCAPE_MARKER.length)\n .replace(/^:/, '')\n .trim()\n if (reason.replace(/\\s/g, '').length < MIN_REASON) {\n context.report({ loc: comment.loc, messageId: 'escapeWithoutReason' })\n return false\n }\n\n const settings = context.settings[SETTINGS_KEY] as EstivaSettings | undefined\n if (settings?.reportEscapes) context.report({ loc: comment.loc, messageId: 'escaped', data: { reason } })\n return true\n}\n", "import type { Rule } from 'eslint'\nimport { ESCAPE_MESSAGES, isEscaped } from './escape'\n\n/**\n * Behaviour Base UI already owns, written by hand inside the package (UIG-5,\n * P1 \u2014 the migration's standing rule D6 as a machine).\n *\n * This is the rule with the most to lose. A hand-made portal or a global\n * listener inside `DialogShell` is inherited by every caller in every app, and\n * nobody sees it from there.\n *\n * Two facts, both mechanical, both the signature of a floating part rebuilt by\n * hand:\n *\n * - **`createPortal`**, or importing it from `react-dom`. Base UI's parts carry\n * their own portal.\n * - **a listener on `window` or `document`** for the events a floating part\n * lives on \u2014 resize, scroll, an outside press, a key. Base UI's Positioner\n * follows the anchor and its Root closes on an outside press and on Escape.\n *\n * What it deliberately does not try to read: measuring arithmetic. `Popover`\n * hands Base UI a virtual anchor with a `getBoundingClientRect`, which is the\n * documented way to anchor to a rectangle rather than an element, and a rule\n * that guessed at arithmetic would report it. Anything that shape is caught by\n * a reader, not by this.\n *\n * A place that keeps one says why on the line above: `// @estiva-escape: <why>`.\n */\nconst FLOATING_EVENTS = new Set(['resize', 'scroll', 'mousedown', 'pointerdown', 'keydown', 'keyup', 'focusin', 'focusout', 'click'])\n\ninterface Node {\n type: string\n loc: NonNullable<Rule.Node['loc']>\n range: [number, number]\n}\n\ninterface Identifier extends Node {\n name: string\n}\n\ninterface ImportDeclaration extends Node {\n source: { value: unknown }\n specifiers: { type: string; imported?: { name?: string }; local?: { name?: string }; loc?: Rule.Node['loc']; range?: [number, number] }[]\n}\n\ninterface CallExpression extends Node {\n callee: { type: string; name?: string; object?: { type: string; name?: string }; property?: { type: string; name?: string } }\n arguments: { type: string; value?: unknown }[]\n parent?: { type: string; loc?: Rule.Node['loc']; range?: [number, number]; parent?: CallExpression['parent'] }\n}\n\n/**\n * The statement a call sits in, which is what an escape is written above:\n * `return createPortal(\u2026)` puts the call in the middle of its line, and a\n * comment above the line is above the statement.\n */\nfunction statement(node: { loc: NonNullable<Rule.Node['loc']>; range: [number, number]; parent?: CallExpression['parent'] }): {\n loc: NonNullable<Rule.Node['loc']>\n range: [number, number]\n} {\n for (let p = node.parent; p; p = p.parent) {\n if (!/(Statement|Declaration)$/.test(p.type)) continue\n if (p.loc && p.range) return { loc: p.loc, range: p.range }\n break\n }\n return { loc: node.loc, range: node.range }\n}\n\nexport const noHandRolledBehaviour: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: { description: 'Overlay behaviour written by hand where Base UI has a part (D6)' },\n schema: [],\n messages: {\n portal:\n 'A portal written by hand. Base UI\\'s parts carry their own (Popover, Menu, Select, Tooltip, Dialog, Toast): use the part, or say why this one stays.',\n listener:\n 'A `{{event}}` listener on `{{target}}`. A floating part follows its anchor and closes on an outside press and on Escape by itself (Base UI Positioner and Root): use the part, or say why this one stays.',\n ...ESCAPE_MESSAGES,\n },\n },\n create(context) {\n return {\n ImportDeclaration(node: Rule.Node) {\n const declaration = node as unknown as ImportDeclaration\n if (declaration.source.value !== 'react-dom') return\n for (const specifier of declaration.specifiers) {\n if (specifier.type !== 'ImportSpecifier' || specifier.imported?.name !== 'createPortal') continue\n // The escape goes above the import line, not above the name inside it.\n if (isEscaped(context, declaration)) return\n context.report({ loc: declaration.loc, messageId: 'portal' })\n return\n }\n },\n CallExpression(node: Rule.Node) {\n const call = node as unknown as CallExpression\n const { callee } = call\n\n if (callee.type === 'Identifier' && callee.name === 'createPortal') {\n if (isEscaped(context, statement(call))) return\n context.report({ loc: call.loc, messageId: 'portal' })\n return\n }\n\n if (callee.type !== 'MemberExpression' || callee.property?.name !== 'addEventListener') return\n const target = callee.object?.type === 'Identifier' ? callee.object.name : undefined\n if (target !== 'window' && target !== 'document') return\n const [first] = call.arguments\n const event = first?.type === 'Literal' && typeof first.value === 'string' ? first.value : undefined\n if (!event || !FLOATING_EVENTS.has(event)) return\n if (isEscaped(context, statement(call))) return\n context.report({ loc: call.loc, messageId: 'listener', data: { event, target } })\n },\n }\n },\n}\n", "import type { Rule } from 'eslint'\nimport { ESCAPE_MESSAGES, isEscaped } from './escape'\n\ninterface JSXOpeningElement {\n name: { type: string; name?: string }\n loc: NonNullable<Rule.Node['loc']>\n range: [number, number]\n}\n\n/**\n * A raw `<button>` in an app (UIG-3, the tracer). The package's `Button`,\n * `IconButton`, `MenuItem` and chips are buttons already; an app that writes\n * its own has a look and a behaviour nobody else keeps in step with.\n *\n * Only the JSX element named `button`. `<Button>`, `<Foo.button>` and\n * `createElement('button')` are not this rule's business.\n */\nexport const noRawButton: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: { description: 'A raw <button> where @estiva-app/ui has one' },\n schema: [],\n messages: {\n raw: 'Use `Button` from @estiva-app/ui instead of a raw <button>.',\n ...ESCAPE_MESSAGES,\n },\n },\n create(context) {\n return {\n // ESLint's types know ESTree's nodes, not JSX's; the parser hands this one over.\n JSXOpeningElement(node: Rule.Node) {\n const element = node as unknown as JSXOpeningElement\n if (element.name.type !== 'JSXIdentifier' || element.name.name !== 'button') return\n if (isEscaped(context, element)) return\n context.report({ loc: element.loc, messageId: 'raw' })\n },\n }\n },\n}\n", "import type { Rule } from 'eslint'\nimport { ESCAPE_MESSAGES, isEscaped } from './escape'\n\ninterface JSXOpeningElement {\n type: string\n name: { type: string; name?: string }\n loc: NonNullable<Rule.Node['loc']>\n range: [number, number]\n parent?: Parent\n}\n\ninterface Parent {\n type: string\n name?: { name?: string }\n openingElement?: unknown\n parent?: Parent\n}\n\n/**\n * The elements this package writes by hand. The set the count used (UIG-1,\n * docs/GATES.md \u00A75 and \u00A77): every element that carries behaviour or a role of\n * its own. A `div` or a `span` is layout and is nobody's business here.\n */\nexport const RAW_ELEMENTS = new Set(['a', 'button', 'input', 'textarea', 'select', 'dialog', 'form', 'label'])\n\n/**\n * A raw element nested inside another element, in the package itself (UIG-5,\n * the inward tracer).\n *\n * The package is allowed to write raw elements \u2014 that is what a primitive is.\n * Two shapes are right, and both are facts about the code rather than a\n * judgement:\n *\n * - **the component's own outermost element.** `NavItem` is an `<a>`, `Link` is\n * an `<a>`, a `MenuItem` outside a `Menu` is a `<button>`. A primitive owning\n * its element is the whole point of it.\n * - **handed to a Base UI `render` prop.** `render={<button type=\"button\" />}`\n * tells a Base UI part which element to be; the behaviour stays Base UI's.\n *\n * Anything else is an element buried inside a bigger component, where its look\n * and its behaviour are kept in step by nobody \u2014 the package's own version of\n * the rule the apps get from UIG-3 and UIG-7.\n *\n * It keeps its reason on the line above if it stays: `// @estiva-escape: <why>`.\n */\nexport const rawElementOutsideAWrapper: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: { description: 'A raw element nested inside a component, rather than the component itself' },\n schema: [],\n messages: {\n nested:\n 'A raw <{{name}}> inside another element. In this package a raw element is either the component\\'s own outermost element or handed to a Base UI `render` prop; one buried inside belongs in a component of its own (Link, Button, MenuItem...).',\n ...ESCAPE_MESSAGES,\n },\n },\n create(context) {\n return {\n // ESLint's types know ESTree's nodes, not JSX's; the parser hands this one over.\n JSXOpeningElement(node: Rule.Node) {\n const element = node as unknown as JSXOpeningElement\n const name = element.name.name\n if (element.name.type !== 'JSXIdentifier' || !name || !RAW_ELEMENTS.has(name)) return\n\n // An opening tag's own parent is its JSXElement: that is this element,\n // not an element around it.\n let parent = element.parent\n if (parent?.type === 'JSXElement' && parent.openingElement === (element as unknown)) parent = parent.parent\n\n // Whichever comes first going outward decides: an element around it\n // means it is nested; a `render` prop means Base UI draws it.\n for (let p = parent; p; p = p.parent) {\n if (p.type === 'JSXAttribute') {\n if (p.name?.name === 'render') return\n continue\n }\n if (p.type !== 'JSXElement' && p.type !== 'JSXFragment') continue\n if (isEscaped(context, element)) return\n context.report({ loc: element.loc, messageId: 'nested', data: { name } })\n return\n }\n },\n }\n },\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
6
|
-
"names": ["rules"]
|
|
3
|
+
"sources": ["../../src/eslint/index.ts", "../../src/eslint/has-a-page-and-a-story.ts", "../../src/eslint/escape.ts", "../../src/eslint/no-hand-rolled-behaviour.ts", "../../src/eslint/no-raw-element.ts", "../../src/eslint/raw-element-outside-a-wrapper.ts"],
|
|
4
|
+
"sourcesContent": ["/// <reference types=\"node\" />\n/**\n * `@estiva-app/ui/eslint` \u2014 the UI Guardrails' lint rules, as a plugin (UIG-3,\n * seam S1 in docs/GATES.md \u00A716).\n *\n * A plugin rather than config, so every app \u2014 Peek, Ship, Leaf \u2014 gets a new\n * rule through an ordinary version bump, and an app's editor hook runs the\n * very rule code its CI runs.\n *\n * import estiva from '@estiva-app/ui/eslint'\n * export default [{ files: ['src/**\\/*.tsx'], ...estiva.configs.recommended }]\n *\n * Register it under `estiva` (the configs do): the rule ids are\n * `estiva/<rule>`, and `countGates` counts those ids.\n *\n * Built on its own by build.mjs, for Node, into `dist/eslint/`; nothing here\n * reaches the components' browser bundle.\n */\nimport { createRequire } from 'node:module'\nimport type { ESLint, Linter } from 'eslint'\nimport { componentHasAPage, componentHasAStory } from './has-a-page-and-a-story'\nimport { noHandRolledBehaviour } from './no-hand-rolled-behaviour'\nimport { noRawElement } from './no-raw-element'\nimport { rawElementOutsideAWrapper } from './raw-element-outside-a-wrapper'\n\nexport { ESCAPE_MARKER, MIN_REASON, SETTINGS_KEY, isEscaped, type EstivaSettings } from './escape'\n\nconst { version } = createRequire(import.meta.url)('../../package.json') as { version: string }\n\n/** The name the configs register the plugin under, so every rule id is `estiva/<rule>`. */\nexport const PLUGIN_KEY = 'estiva'\n\n/**\n * The rules an **app** runs: they say an app must not build what the package\n * already has. `recommended` and `strict` carry these and only these.\n */\nconst appRules = {\n 'no-raw-element': noRawElement,\n}\n\n/**\n * The rules the **package itself** runs, pointed inward (UIG-5): don't bury a\n * raw element inside a component, don't rebuild what Base UI owns, don't ship a\n * component without a page or a story.\n *\n * They are in `configs.package`, never in `recommended`, on purpose. Peek and\n * Ship spread `recommended`, so a rule added here must not arrive in an app\n * with the next version bump: an app's components are not primitives, so\n * \"buried inside a component\" means nothing there (`no-raw-element` is the\n * app's version, UIG-7), and an app has no `.mdx` pages at all. `index.test.ts` holds the\n * apps' list to exactly the app rules.\n */\nconst packageRules = {\n 'raw-element-outside-a-wrapper': rawElementOutsideAWrapper,\n 'no-hand-rolled-behaviour': noHandRolledBehaviour,\n 'component-has-a-page': componentHasAPage,\n 'component-has-a-story': componentHasAStory,\n}\n\nconst rules = { ...appRules, ...packageRules }\n\nconst plugin = {\n meta: { name: '@estiva-app/ui/eslint', version },\n rules,\n configs: {} as { recommended: Linter.Config; strict: Linter.Config; package: Linter.Config },\n} satisfies ESLint.Plugin\n\n/** The ids `recommended` and `strict` carry \u2014 what an app's gate runs and counts. */\nexport const APP_RULE_IDS = Object.keys(appRules).map((name) => `${PLUGIN_KEY}/${name}`)\n/** The ids `package` carries \u2014 what this package's own gate runs and counts (UIG-5). */\nexport const PACKAGE_RULE_IDS = Object.keys(packageRules).map((name) => `${PLUGIN_KEY}/${name}`)\n\n/**\n * `recommended` switches every **app** rule on at the level it was ruled at: an\n * error blocks, a warning is reported and never blocks. `strict` makes every app\n * rule an error. With one rule, an error, the two are the same today; they part\n * when the first warning-level rule arrives (UIG-25).\n *\n * `package` is the inward set (UIG-5), which only this package runs.\n */\nplugin.configs.recommended = {\n name: '@estiva-app/ui/recommended',\n plugins: { [PLUGIN_KEY]: plugin },\n rules: { [`${PLUGIN_KEY}/no-raw-element`]: 'error' },\n}\nplugin.configs.strict = {\n name: '@estiva-app/ui/strict',\n plugins: { [PLUGIN_KEY]: plugin },\n rules: Object.fromEntries(APP_RULE_IDS.map((id) => [id, 'error'])),\n}\nplugin.configs.package = {\n name: '@estiva-app/ui/package',\n plugins: { [PLUGIN_KEY]: plugin },\n rules: Object.fromEntries(PACKAGE_RULE_IDS.map((id) => [id, 'error'])),\n}\n\nexport default plugin\n\nexport interface GateRuleCount {\n errors: number\n warnings: number\n escapes: number\n}\n\nexport interface GateCount {\n /** Per rule id, including rules with nothing to report. */\n rules: Record<string, GateRuleCount>\n /**\n * Reports of these rules that an `eslint-disable` directive silenced. Not\n * an escape: a gate fails on any of these (docs/GATES.md \u00A716, S4).\n */\n disabled: { filePath: string; line: number; ruleId: string }[]\n}\n\n/**\n * Count what a lint of these rules found, for `.gates-count.json` (seam S3).\n *\n * Run the lint with `settings: { estiva: { reportEscapes: true } }` for the\n * escapes to be counted; without it they are silent and count 0. A marker\n * with no reason, or inside a directive, counts as an error of its rule.\n *\n * `seed` is which rules the count lists when they found nothing, and it is the\n * app rules unless a caller says otherwise: an app's count file must not gain\n * rows for the inward rules (UIG-5) that its gate does not run. This package's\n * own count script passes `PACKAGE_RULE_IDS`.\n */\nexport function countGates(results: ESLint.LintResult[], seed: readonly string[] = APP_RULE_IDS): GateCount {\n const counts: Record<string, GateRuleCount> = Object.fromEntries(seed.map((id) => [id, { errors: 0, warnings: 0, escapes: 0 }]))\n const disabled: GateCount['disabled'] = []\n const ours = (ruleId: string | null | undefined): ruleId is string => typeof ruleId === 'string' && ruleId.startsWith(`${PLUGIN_KEY}/`)\n for (const result of results) {\n for (const message of result.messages) {\n // Seeded or not: every rule of this plugin that reported is counted, so a\n // lint of a set the caller did not name is still counted in full.\n if (!ours(message.ruleId)) continue\n const count = (counts[message.ruleId] ??= { errors: 0, warnings: 0, escapes: 0 })\n if (message.messageId === 'escaped') count.escapes += 1\n else if (message.severity === 2) count.errors += 1\n else count.warnings += 1\n }\n for (const message of result.suppressedMessages ?? []) {\n if (ours(message.ruleId)) disabled.push({ filePath: result.filePath, line: message.line, ruleId: message.ruleId })\n }\n }\n return { rules: counts, disabled }\n}\n", "import { existsSync } from 'node:fs'\nimport type { Rule } from 'eslint'\nimport { ESCAPE_MESSAGES, isEscaped } from './escape'\n\n/**\n * A component of the package with no page, and one with no story (UIG-5, P2 and\n * P3).\n *\n * A component nobody can read about is a component nobody uses correctly, and\n * one nobody can look at is one nobody reviews. Both were at zero when these\n * were switched on; they are here so the next component cannot arrive without\n * them.\n *\n * The pair is by file name, beside the component, which is how this package is\n * laid out: `Button.tsx`, `Button.mdx`, `Button.stories.tsx`, `Button.test.tsx`\n * side by side in `src/`.\n *\n * **The false positive to avoid** (UIG-1 confirmed it is real): `FieldLine` and\n * `MenuItem` have a page and a story but no `.tsx` of their own \u2014 they are\n * exported from a sibling. These rules read a component file and ask for its\n * page and its story, never the other way round, so a page without a component\n * is not this rule's business.\n *\n * A file that stays without one says why at its top: `// @estiva-escape: <why>`.\n */\ninterface ProgramNode {\n body: { loc?: Rule.Node['loc']; range?: [number, number] }[]\n loc: NonNullable<Rule.Node['loc']>\n range: [number, number]\n}\n\n/** A component file: a `.tsx` in the package's source that is not a story or a test. */\nfunction componentFile(filename: string): boolean {\n return filename.endsWith('.tsx') && !/\\.(stories|test)\\.tsx$/.test(filename)\n}\n\nfunction sibling(filename: string, extension: string): string {\n return filename.replace(/\\.tsx$/, extension)\n}\n\n/**\n * Report on the file's first statement, so the escape is a comment at the top of\n * the file \u2014 the only place a reason for a missing page could go.\n */\nfunction reportOnFile(context: Rule.RuleContext, program: ProgramNode, messageId: string, data: Record<string, string>): void {\n const anchor = program.body[0] ?? program\n if (anchor.loc && anchor.range && isEscaped(context, { loc: anchor.loc, range: anchor.range })) return\n context.report({ loc: anchor.loc ?? program.loc, messageId, data })\n}\n\nexport const componentHasAPage: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: { description: 'Every component of the package has a usage page beside it' },\n schema: [],\n messages: {\n missing: '{{name}} has no page. Write {{page}} beside it \u2014 what it is, when, when not, how, and what it owns \u2014 or say why not at the top of the file.',\n ...ESCAPE_MESSAGES,\n },\n },\n create(context) {\n return {\n Program(node) {\n const filename = context.filename\n if (!componentFile(filename)) return\n const page = sibling(filename, '.mdx')\n if (existsSync(page)) return\n const name = filename.split(/[\\\\/]/).pop() ?? filename\n reportOnFile(context, node as unknown as ProgramNode, 'missing', { name, page: page.split(/[\\\\/]/).pop() ?? page })\n },\n }\n },\n}\n\nexport const componentHasAStory: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: { description: 'Every component of the package has a story beside it' },\n schema: [],\n messages: {\n missing: '{{name}} has no story. Write {{story}} beside it, so it can be seen and reviewed, or say why not at the top of the file.',\n ...ESCAPE_MESSAGES,\n },\n },\n create(context) {\n return {\n Program(node) {\n const filename = context.filename\n if (!componentFile(filename)) return\n const story = sibling(filename, '.stories.tsx')\n if (existsSync(story)) return\n const name = filename.split(/[\\\\/]/).pop() ?? filename\n reportOnFile(context, node as unknown as ProgramNode, 'missing', { name, story: story.split(/[\\\\/]/).pop() ?? story })\n },\n }\n },\n}\n", "import type { AST, Rule } from 'eslint'\n\n/**\n * The escape marker (UIG-3, seam S4 in docs/GATES.md \u00A716): one written reason\n * why one element stays as it is.\n *\n * A line comment directly above the element,\n *\n * // @estiva-escape: <reason>\n *\n * or, where the element is a JSX child, the JSX comment on the line above,\n *\n * {/* @estiva-escape: <reason> *\\/}\n *\n * with at least ten characters of reason, spaces not counted.\n *\n * Not `eslint-disable`. A directive switches a rule off without saying it was\n * meant, and the count of escapes (`.gates-count.json`) could not see it. A\n * marker written inside a directive that switches one of these rules off is\n * reported for that reason.\n *\n * The token lint's older notes (`eslint-disable-next-line <rule> -- @estiva-escape:\n * <reason>`, Katerina's ruling A2 of 15 September) are not read here: they\n * escape the token lint's rules, which are not this plugin's, and they stay as\n * they are (Katerina, 15 September).\n */\nexport const ESCAPE_MARKER = '@estiva-escape'\n\n/** Characters of reason, spaces not counted. */\nexport const MIN_REASON = 10\n\n/** The key under ESLint's `settings` this plugin reads. */\nexport const SETTINGS_KEY = 'estiva'\n\n/**\n * Every rule spreads these into its `meta.messages`, because `isEscaped`\n * reports through the rule that called it.\n */\nexport const ESCAPE_MESSAGES = {\n escapeWithoutReason: `An escape needs its reason, at least ${MIN_REASON} characters: \\`// ${ESCAPE_MARKER}: <why this stays>\\`. An escape with no reason hides nothing.`,\n escapeInDirective: `Write the escape as its own comment on the line above: \\`// ${ESCAPE_MARKER}: <reason>\\`. Inside an eslint-disable comment it switches the rule off instead of recording why.`,\n escaped: 'Escaped: {{reason}}',\n} as const\n\nexport interface EstivaSettings {\n /**\n * Report every sanctioned escape as a message with the id `escaped`, so a\n * count can read them. Off in the lint anyone runs; on only in the count.\n */\n reportEscapes?: boolean\n}\n\ninterface Located {\n loc?: AST.SourceLocation | null\n range?: [number, number]\n}\n\nconst DIRECTIVE = /^\\s*eslint-disable(?:-next-line|-line)?(?=\\s|$)([^]*)$/\n\n/** The rules a directive names: what comes before its ` -- ` description. None means every rule. */\nfunction directiveRules(rest: string): string[] {\n const dashes = rest.search(/(?:^|\\s)--(?:\\s|$)/)\n return (dashes === -1 ? rest : rest.slice(0, dashes)).split(/[\\s,]+/).filter(Boolean)\n}\n\n/** Only spaces and a JSX expression's closing brace between the marker and the element. */\nconst BETWEEN = /^[\\s}]*$/\n\n/**\n * Whether the element at `node` carries a valid escape on the line above.\n *\n * Every rule of this plugin calls it before it reports. It reports, through\n * that rule, a marker with too short a reason and a marker inside an\n * eslint-disable directive for this rule; either way the element is not\n * escaped, so the rule reports it too.\n */\nexport function isEscaped(context: Rule.RuleContext, node: Located): boolean {\n const { sourceCode } = context\n if (!node.loc || !node.range) return false\n const line = node.loc.start.line\n const nodeStart = node.range[0]\n\n const comment = sourceCode\n .getAllComments()\n .find((c) => c.loc && c.range && c.loc.end.line === line - 1 && BETWEEN.test(sourceCode.text.slice(c.range[1], nodeStart)))\n if (!comment?.loc) return false\n\n const at = comment.value.indexOf(ESCAPE_MARKER)\n if (at === -1) return false\n\n const directive = DIRECTIVE.exec(comment.value)\n if (directive) {\n const rules = directiveRules(directive[1])\n // A directive for other rules (the token lint's notes) is not an escape of this one.\n if (rules.length > 0 && !rules.includes(context.id)) return false\n context.report({ loc: comment.loc, messageId: 'escapeInDirective' })\n return false\n }\n\n const reason = comment.value\n .slice(at + ESCAPE_MARKER.length)\n .replace(/^:/, '')\n .trim()\n if (reason.replace(/\\s/g, '').length < MIN_REASON) {\n context.report({ loc: comment.loc, messageId: 'escapeWithoutReason' })\n return false\n }\n\n const settings = context.settings[SETTINGS_KEY] as EstivaSettings | undefined\n if (settings?.reportEscapes) context.report({ loc: comment.loc, messageId: 'escaped', data: { reason } })\n return true\n}\n", "import type { Rule } from 'eslint'\nimport { ESCAPE_MESSAGES, isEscaped } from './escape'\n\n/**\n * Behaviour Base UI already owns, written by hand inside the package (UIG-5,\n * P1 \u2014 the migration's standing rule D6 as a machine).\n *\n * This is the rule with the most to lose. A hand-made portal or a global\n * listener inside `DialogShell` is inherited by every caller in every app, and\n * nobody sees it from there.\n *\n * Two facts, both mechanical, both the signature of a floating part rebuilt by\n * hand:\n *\n * - **`createPortal`**, or importing it from `react-dom`. Base UI's parts carry\n * their own portal.\n * - **a listener on `window` or `document`** for the events a floating part\n * lives on \u2014 resize, scroll, an outside press, a key. Base UI's Positioner\n * follows the anchor and its Root closes on an outside press and on Escape.\n *\n * What it deliberately does not try to read: measuring arithmetic. `Popover`\n * hands Base UI a virtual anchor with a `getBoundingClientRect`, which is the\n * documented way to anchor to a rectangle rather than an element, and a rule\n * that guessed at arithmetic would report it. Anything that shape is caught by\n * a reader, not by this.\n *\n * A place that keeps one says why on the line above: `// @estiva-escape: <why>`.\n */\nconst FLOATING_EVENTS = new Set(['resize', 'scroll', 'mousedown', 'pointerdown', 'keydown', 'keyup', 'focusin', 'focusout', 'click'])\n\ninterface Node {\n type: string\n loc: NonNullable<Rule.Node['loc']>\n range: [number, number]\n}\n\ninterface Identifier extends Node {\n name: string\n}\n\ninterface ImportDeclaration extends Node {\n source: { value: unknown }\n specifiers: { type: string; imported?: { name?: string }; local?: { name?: string }; loc?: Rule.Node['loc']; range?: [number, number] }[]\n}\n\ninterface CallExpression extends Node {\n callee: { type: string; name?: string; object?: { type: string; name?: string }; property?: { type: string; name?: string } }\n arguments: { type: string; value?: unknown }[]\n parent?: { type: string; loc?: Rule.Node['loc']; range?: [number, number]; parent?: CallExpression['parent'] }\n}\n\n/**\n * The statement a call sits in, which is what an escape is written above:\n * `return createPortal(\u2026)` puts the call in the middle of its line, and a\n * comment above the line is above the statement.\n */\nfunction statement(node: { loc: NonNullable<Rule.Node['loc']>; range: [number, number]; parent?: CallExpression['parent'] }): {\n loc: NonNullable<Rule.Node['loc']>\n range: [number, number]\n} {\n for (let p = node.parent; p; p = p.parent) {\n if (!/(Statement|Declaration)$/.test(p.type)) continue\n if (p.loc && p.range) return { loc: p.loc, range: p.range }\n break\n }\n return { loc: node.loc, range: node.range }\n}\n\nexport const noHandRolledBehaviour: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: { description: 'Overlay behaviour written by hand where Base UI has a part (D6)' },\n schema: [],\n messages: {\n portal:\n 'A portal written by hand. Base UI\\'s parts carry their own (Popover, Menu, Select, Tooltip, Dialog, Toast): use the part, or say why this one stays.',\n listener:\n 'A `{{event}}` listener on `{{target}}`. A floating part follows its anchor and closes on an outside press and on Escape by itself (Base UI Positioner and Root): use the part, or say why this one stays.',\n ...ESCAPE_MESSAGES,\n },\n },\n create(context) {\n return {\n ImportDeclaration(node: Rule.Node) {\n const declaration = node as unknown as ImportDeclaration\n if (declaration.source.value !== 'react-dom') return\n for (const specifier of declaration.specifiers) {\n if (specifier.type !== 'ImportSpecifier' || specifier.imported?.name !== 'createPortal') continue\n // The escape goes above the import line, not above the name inside it.\n if (isEscaped(context, declaration)) return\n context.report({ loc: declaration.loc, messageId: 'portal' })\n return\n }\n },\n CallExpression(node: Rule.Node) {\n const call = node as unknown as CallExpression\n const { callee } = call\n\n if (callee.type === 'Identifier' && callee.name === 'createPortal') {\n if (isEscaped(context, statement(call))) return\n context.report({ loc: call.loc, messageId: 'portal' })\n return\n }\n\n if (callee.type !== 'MemberExpression' || callee.property?.name !== 'addEventListener') return\n const target = callee.object?.type === 'Identifier' ? callee.object.name : undefined\n if (target !== 'window' && target !== 'document') return\n const [first] = call.arguments\n const event = first?.type === 'Literal' && typeof first.value === 'string' ? first.value : undefined\n if (!event || !FLOATING_EVENTS.has(event)) return\n if (isEscaped(context, statement(call))) return\n context.report({ loc: call.loc, messageId: 'listener', data: { event, target } })\n },\n }\n },\n}\n", "import type { Rule } from 'eslint'\nimport { ESCAPE_MESSAGES, isEscaped } from './escape'\n\ninterface JSXAttributeValue {\n type: string\n value?: unknown\n expression?: { type: string; value?: unknown; expressions?: unknown[]; quasis?: { value: { cooked: string | null } }[] }\n}\n\ninterface JSXAttribute {\n type: 'JSXAttribute'\n name: { type: string; name?: string }\n value: JSXAttributeValue | null\n}\n\ninterface JSXOpeningElement {\n name: { type: string; name?: string }\n attributes: (JSXAttribute | { type: 'JSXSpreadAttribute' })[]\n loc: NonNullable<Rule.Node['loc']>\n range: [number, number]\n}\n\n/**\n * What the package has instead of one raw element: the component the message\n * names, and where one element does more than one job, the others after it.\n * `null` is an element the package has no part for yet.\n */\nexport interface RawElementPart {\n use: string\n more?: string\n}\n\n/**\n * The mapping (UIG-7), derived from the package's exports: every interactive\n * element in the HTML standard's list (\"interactive content\"), plus `<dialog>`,\n * `<form>` and `<progress>`, which have a part here. docs/GATES.md prints it.\n *\n * Not here, on purpose: `<summary>` (it lives inside a `<details>`, which is\n * reported), `<option>`, `<optgroup>` and `<datalist>` (they live inside a\n * `<select>` or beside an input), `<fieldset>` and `<legend>` (a group's frame,\n * no control of its own), and `<img>` without `usemap`, `<hr>`, `<kbd>`, which\n * are not controls. An `<audio>` or `<video>` is a control only with `controls`.\n */\nexport const RAW_ELEMENT_PARTS: Record<string, RawElementPart | null> = {\n a: { use: 'Link', more: ' For a chip, `InlineChip`; for a whole card, `Card` with `href`.' },\n button: { use: 'Button' },\n textarea: { use: 'Textarea' },\n select: { use: 'Select' },\n dialog: { use: 'DialogShell', more: ' To ask yes or no, `ConfirmDialog`.' },\n label: { use: 'Field', more: ' For words beside a tick box, `Checkbox` with `label`.' },\n details: { use: 'CollapsibleSection' },\n progress: { use: 'ProgressBar' },\n form: { use: 'Form' },\n meter: null,\n iframe: null,\n embed: null,\n object: null,\n audio: null,\n video: null,\n img: null,\n}\n\n/**\n * An `<input>` is as many controls as its `type`. A type missing from here\n * (text, email, password, url, tel, number, or one the browser does not know)\n * is a text box.\n */\nexport const RAW_INPUT_PARTS: Record<string, RawElementPart | null> = {\n search: { use: 'SearchInput' },\n checkbox: { use: 'Checkbox' },\n file: { use: 'FilePicker' },\n submit: { use: 'Button' },\n button: { use: 'Button' },\n reset: { use: 'Button' },\n image: { use: 'Button' },\n radio: null,\n range: null,\n color: null,\n date: null,\n 'datetime-local': null,\n month: null,\n week: null,\n time: null,\n}\n\nconst TEXT_INPUT: RawElementPart = { use: 'TextInput' }\nconst ANY_INPUT: RawElementPart = {\n use: 'TextInput',\n more: ' For a search, `SearchInput`; for a tick box, `Checkbox`; to pick files, `FilePicker`.',\n}\n\nfunction attribute(element: JSXOpeningElement, name: string): JSXAttribute | undefined {\n return element.attributes.find((a): a is JSXAttribute => a.type === 'JSXAttribute' && a.name.name === name)\n}\n\n/** An attribute's value when the code spells it out, `undefined` when it is computed. */\nfunction literal(attr: JSXAttribute): string | boolean | undefined {\n const value = attr.value\n if (value === null) return true\n if (value.type === 'Literal') return typeof value.value === 'string' ? value.value : undefined\n if (value.type !== 'JSXExpressionContainer' || !value.expression) return undefined\n const expression = value.expression\n if (expression.type === 'Literal' && (typeof expression.value === 'string' || typeof expression.value === 'boolean')) return expression.value\n if (expression.type === 'TemplateLiteral' && expression.expressions?.length === 0) return expression.quasis?.[0]?.value.cooked ?? undefined\n return undefined\n}\n\n/** Present, and not written as `={false}`. */\nfunction isOn(element: JSXOpeningElement, name: string): boolean {\n const attr = attribute(element, name)\n return attr !== undefined && literal(attr) !== false\n}\n\ntype Found = { report: 'raw'; element: string; part: RawElementPart } | { report: 'noPart'; element: string } | null\n\n/** What this element is to the rule: a raw element with a part, one with none yet, or none of its business. */\nexport function classify(element: JSXOpeningElement): Found {\n if (element.name.type !== 'JSXIdentifier' || !element.name.name) return null\n const name = element.name.name\n\n if (name === 'input') {\n const attr = attribute(element, 'type')\n const type = attr ? literal(attr) : 'text'\n if (typeof type !== 'string') return { report: 'raw', element: '<input>', part: ANY_INPUT }\n const key = type.toLowerCase()\n // A hidden input draws nothing and takes no focus: it is data, not a control.\n if (key === 'hidden') return null\n if (!(key in RAW_INPUT_PARTS)) return { report: 'raw', element: '<input>', part: TEXT_INPUT }\n const part = RAW_INPUT_PARTS[key]\n const shown = `<input type=\"${key}\">`\n return part ? { report: 'raw', element: shown, part } : { report: 'noPart', element: shown }\n }\n\n if (!Object.hasOwn(RAW_ELEMENT_PARTS, name)) return null\n if ((name === 'audio' || name === 'video') && !isOn(element, 'controls')) return null\n if (name === 'img' && !attribute(element, 'usemap') && !attribute(element, 'useMap')) return null\n\n const part = RAW_ELEMENT_PARTS[name]\n return part ? { report: 'raw', element: `<${name}>`, part } : { report: 'noPart', element: `<${name}>` }\n}\n\n/**\n * A raw interactive element in an app, where the package has a part for it\n * (UIG-3 began it with `<button>`; UIG-7 made it every element). A control\n * the app writes itself has a look, a focus ring and keys that nobody else\n * keeps in step with \u2014 and it looks nearly right, which is why nobody catches\n * it by eye. Every message names the part to use.\n *\n * An element the package has no part for yet is refused too, with a message\n * that says so: the part is made in the package, not by hand in an app\n * (Katerina, 16 September).\n *\n * Only a JSX element written with a lowercase name. `<Button>`,\n * `<Foo.button>` and `createElement('button')` are not this rule's business.\n */\nexport const noRawElement: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: { description: 'A raw interactive element where @estiva-app/ui has a part, or where it needs one' },\n schema: [],\n messages: {\n raw: 'Use `{{use}}` from @estiva-app/ui instead of a raw {{element}}.{{more}}',\n noPart: '@estiva-app/ui has no part for a raw {{element}} yet. Do not build one here: ask Katerina, and it gets made in @estiva-app/ui.',\n ...ESCAPE_MESSAGES,\n },\n },\n create(context) {\n return {\n // ESLint's types know ESTree's nodes, not JSX's; the parser hands this one over.\n JSXOpeningElement(node: Rule.Node) {\n const element = node as unknown as JSXOpeningElement\n const found = classify(element)\n if (!found) return\n if (isEscaped(context, element)) return\n if (found.report === 'noPart') context.report({ loc: element.loc, messageId: 'noPart', data: { element: found.element } })\n else context.report({ loc: element.loc, messageId: 'raw', data: { element: found.element, use: found.part.use, more: found.part.more ?? '' } })\n },\n }\n },\n}\n", "import type { Rule } from 'eslint'\nimport { ESCAPE_MESSAGES, isEscaped } from './escape'\n\ninterface JSXOpeningElement {\n type: string\n name: { type: string; name?: string }\n loc: NonNullable<Rule.Node['loc']>\n range: [number, number]\n parent?: Parent\n}\n\ninterface Parent {\n type: string\n name?: { name?: string }\n openingElement?: unknown\n parent?: Parent\n}\n\n/**\n * The elements this package writes by hand. The set the count used (UIG-1,\n * docs/GATES.md \u00A75 and \u00A77): every element that carries behaviour or a role of\n * its own. A `div` or a `span` is layout and is nobody's business here.\n */\nexport const RAW_ELEMENTS = new Set(['a', 'button', 'input', 'textarea', 'select', 'dialog', 'form', 'label'])\n\n/**\n * A raw element nested inside another element, in the package itself (UIG-5,\n * the inward tracer).\n *\n * The package is allowed to write raw elements \u2014 that is what a primitive is.\n * Two shapes are right, and both are facts about the code rather than a\n * judgement:\n *\n * - **the component's own outermost element.** `NavItem` is an `<a>`, `Link` is\n * an `<a>`, a `MenuItem` outside a `Menu` is a `<button>`. A primitive owning\n * its element is the whole point of it.\n * - **handed to a Base UI `render` prop.** `render={<button type=\"button\" />}`\n * tells a Base UI part which element to be; the behaviour stays Base UI's.\n *\n * Anything else is an element buried inside a bigger component, where its look\n * and its behaviour are kept in step by nobody \u2014 the package's own version of\n * the rule the apps get from UIG-3 and UIG-7.\n *\n * It keeps its reason on the line above if it stays: `// @estiva-escape: <why>`.\n */\nexport const rawElementOutsideAWrapper: Rule.RuleModule = {\n meta: {\n type: 'problem',\n docs: { description: 'A raw element nested inside a component, rather than the component itself' },\n schema: [],\n messages: {\n nested:\n 'A raw <{{name}}> inside another element. In this package a raw element is either the component\\'s own outermost element or handed to a Base UI `render` prop; one buried inside belongs in a component of its own (Link, Button, MenuItem...).',\n ...ESCAPE_MESSAGES,\n },\n },\n create(context) {\n return {\n // ESLint's types know ESTree's nodes, not JSX's; the parser hands this one over.\n JSXOpeningElement(node: Rule.Node) {\n const element = node as unknown as JSXOpeningElement\n const name = element.name.name\n if (element.name.type !== 'JSXIdentifier' || !name || !RAW_ELEMENTS.has(name)) return\n\n // An opening tag's own parent is its JSXElement: that is this element,\n // not an element around it.\n let parent = element.parent\n if (parent?.type === 'JSXElement' && parent.openingElement === (element as unknown)) parent = parent.parent\n\n // Whichever comes first going outward decides: an element around it\n // means it is nested; a `render` prop means Base UI draws it.\n for (let p = parent; p; p = p.parent) {\n if (p.type === 'JSXAttribute') {\n if (p.name?.name === 'render') return\n continue\n }\n if (p.type !== 'JSXElement' && p.type !== 'JSXFragment') continue\n if (isEscaped(context, element)) return\n context.report({ loc: element.loc, messageId: 'nested', data: { name } })\n return\n }\n },\n }\n },\n}\n"],
|
|
5
|
+
"mappings": ";AAkBA,SAAS,qBAAqB;;;AClB9B,SAAS,kBAAkB;;;AC0BpB,IAAM,gBAAgB;AAGtB,IAAM,aAAa;AAGnB,IAAM,eAAe;AAMrB,IAAM,kBAAkB;AAAA,EAC7B,qBAAqB,wCAAwC,UAAU,qBAAqB,aAAa;AAAA,EACzG,mBAAmB,+DAA+D,aAAa;AAAA,EAC/F,SAAS;AACX;AAeA,IAAM,YAAY;AAGlB,SAAS,eAAe,MAAwB;AAC9C,QAAM,SAAS,KAAK,OAAO,oBAAoB;AAC/C,UAAQ,WAAW,KAAK,OAAO,KAAK,MAAM,GAAG,MAAM,GAAG,MAAM,QAAQ,EAAE,OAAO,OAAO;AACtF;AAGA,IAAM,UAAU;AAUT,SAAS,UAAU,SAA2B,MAAwB;AAC3E,QAAM,EAAE,WAAW,IAAI;AACvB,MAAI,CAAC,KAAK,OAAO,CAAC,KAAK,MAAO,QAAO;AACrC,QAAM,OAAO,KAAK,IAAI,MAAM;AAC5B,QAAM,YAAY,KAAK,MAAM,CAAC;AAE9B,QAAM,UAAU,WACb,eAAe,EACf,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,IAAI,SAAS,OAAO,KAAK,QAAQ,KAAK,WAAW,KAAK,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC;AAC5H,MAAI,CAAC,SAAS,IAAK,QAAO;AAE1B,QAAM,KAAK,QAAQ,MAAM,QAAQ,aAAa;AAC9C,MAAI,OAAO,GAAI,QAAO;AAEtB,QAAM,YAAY,UAAU,KAAK,QAAQ,KAAK;AAC9C,MAAI,WAAW;AACb,UAAMA,SAAQ,eAAe,UAAU,CAAC,CAAC;AAEzC,QAAIA,OAAM,SAAS,KAAK,CAACA,OAAM,SAAS,QAAQ,EAAE,EAAG,QAAO;AAC5D,YAAQ,OAAO,EAAE,KAAK,QAAQ,KAAK,WAAW,oBAAoB,CAAC;AACnE,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,QAAQ,MACpB,MAAM,KAAK,cAAc,MAAM,EAC/B,QAAQ,MAAM,EAAE,EAChB,KAAK;AACR,MAAI,OAAO,QAAQ,OAAO,EAAE,EAAE,SAAS,YAAY;AACjD,YAAQ,OAAO,EAAE,KAAK,QAAQ,KAAK,WAAW,sBAAsB,CAAC;AACrE,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,QAAQ,SAAS,YAAY;AAC9C,MAAI,UAAU,cAAe,SAAQ,OAAO,EAAE,KAAK,QAAQ,KAAK,WAAW,WAAW,MAAM,EAAE,OAAO,EAAE,CAAC;AACxG,SAAO;AACT;;;AD/EA,SAAS,cAAc,UAA2B;AAChD,SAAO,SAAS,SAAS,MAAM,KAAK,CAAC,yBAAyB,KAAK,QAAQ;AAC7E;AAEA,SAAS,QAAQ,UAAkB,WAA2B;AAC5D,SAAO,SAAS,QAAQ,UAAU,SAAS;AAC7C;AAMA,SAAS,aAAa,SAA2B,SAAsB,WAAmB,MAAoC;AAC5H,QAAM,SAAS,QAAQ,KAAK,CAAC,KAAK;AAClC,MAAI,OAAO,OAAO,OAAO,SAAS,UAAU,SAAS,EAAE,KAAK,OAAO,KAAK,OAAO,OAAO,MAAM,CAAC,EAAG;AAChG,UAAQ,OAAO,EAAE,KAAK,OAAO,OAAO,QAAQ,KAAK,WAAW,KAAK,CAAC;AACpE;AAEO,IAAM,oBAAqC;AAAA,EAChD,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM,EAAE,aAAa,4DAA4D;AAAA,IACjF,QAAQ,CAAC;AAAA,IACT,UAAU;AAAA,MACR,SAAS;AAAA,MACT,GAAG;AAAA,IACL;AAAA,EACF;AAAA,EACA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,QAAQ,MAAM;AACZ,cAAM,WAAW,QAAQ;AACzB,YAAI,CAAC,cAAc,QAAQ,EAAG;AAC9B,cAAM,OAAO,QAAQ,UAAU,MAAM;AACrC,YAAI,WAAW,IAAI,EAAG;AACtB,cAAM,OAAO,SAAS,MAAM,OAAO,EAAE,IAAI,KAAK;AAC9C,qBAAa,SAAS,MAAgC,WAAW,EAAE,MAAM,MAAM,KAAK,MAAM,OAAO,EAAE,IAAI,KAAK,KAAK,CAAC;AAAA,MACpH;AAAA,IACF;AAAA,EACF;AACF;AAEO,IAAM,qBAAsC;AAAA,EACjD,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM,EAAE,aAAa,uDAAuD;AAAA,IAC5E,QAAQ,CAAC;AAAA,IACT,UAAU;AAAA,MACR,SAAS;AAAA,MACT,GAAG;AAAA,IACL;AAAA,EACF;AAAA,EACA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,QAAQ,MAAM;AACZ,cAAM,WAAW,QAAQ;AACzB,YAAI,CAAC,cAAc,QAAQ,EAAG;AAC9B,cAAM,QAAQ,QAAQ,UAAU,cAAc;AAC9C,YAAI,WAAW,KAAK,EAAG;AACvB,cAAM,OAAO,SAAS,MAAM,OAAO,EAAE,IAAI,KAAK;AAC9C,qBAAa,SAAS,MAAgC,WAAW,EAAE,MAAM,OAAO,MAAM,MAAM,OAAO,EAAE,IAAI,KAAK,MAAM,CAAC;AAAA,MACvH;AAAA,IACF;AAAA,EACF;AACF;;;AEpEA,IAAM,kBAAkB,oBAAI,IAAI,CAAC,UAAU,UAAU,aAAa,eAAe,WAAW,SAAS,WAAW,YAAY,OAAO,CAAC;AA4BpI,SAAS,UAAU,MAGjB;AACA,WAAS,IAAI,KAAK,QAAQ,GAAG,IAAI,EAAE,QAAQ;AACzC,QAAI,CAAC,2BAA2B,KAAK,EAAE,IAAI,EAAG;AAC9C,QAAI,EAAE,OAAO,EAAE,MAAO,QAAO,EAAE,KAAK,EAAE,KAAK,OAAO,EAAE,MAAM;AAC1D;AAAA,EACF;AACA,SAAO,EAAE,KAAK,KAAK,KAAK,OAAO,KAAK,MAAM;AAC5C;AAEO,IAAM,wBAAyC;AAAA,EACpD,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM,EAAE,aAAa,kEAAkE;AAAA,IACvF,QAAQ,CAAC;AAAA,IACT,UAAU;AAAA,MACR,QACE;AAAA,MACF,UACE;AAAA,MACF,GAAG;AAAA,IACL;AAAA,EACF;AAAA,EACA,OAAO,SAAS;AACd,WAAO;AAAA,MACL,kBAAkB,MAAiB;AACjC,cAAM,cAAc;AACpB,YAAI,YAAY,OAAO,UAAU,YAAa;AAC9C,mBAAW,aAAa,YAAY,YAAY;AAC9C,cAAI,UAAU,SAAS,qBAAqB,UAAU,UAAU,SAAS,eAAgB;AAEzF,cAAI,UAAU,SAAS,WAAW,EAAG;AACrC,kBAAQ,OAAO,EAAE,KAAK,YAAY,KAAK,WAAW,SAAS,CAAC;AAC5D;AAAA,QACF;AAAA,MACF;AAAA,MACA,eAAe,MAAiB;AAC9B,cAAM,OAAO;AACb,cAAM,EAAE,OAAO,IAAI;AAEnB,YAAI,OAAO,SAAS,gBAAgB,OAAO,SAAS,gBAAgB;AAClE,cAAI,UAAU,SAAS,UAAU,IAAI,CAAC,EAAG;AACzC,kBAAQ,OAAO,EAAE,KAAK,KAAK,KAAK,WAAW,SAAS,CAAC;AACrD;AAAA,QACF;AAEA,YAAI,OAAO,SAAS,sBAAsB,OAAO,UAAU,SAAS,mBAAoB;AACxF,cAAM,SAAS,OAAO,QAAQ,SAAS,eAAe,OAAO,OAAO,OAAO;AAC3E,YAAI,WAAW,YAAY,WAAW,WAAY;AAClD,cAAM,CAAC,KAAK,IAAI,KAAK;AACrB,cAAM,QAAQ,OAAO,SAAS,aAAa,OAAO,MAAM,UAAU,WAAW,MAAM,QAAQ;AAC3F,YAAI,CAAC,SAAS,CAAC,gBAAgB,IAAI,KAAK,EAAG;AAC3C,YAAI,UAAU,SAAS,UAAU,IAAI,CAAC,EAAG;AACzC,gBAAQ,OAAO,EAAE,KAAK,KAAK,KAAK,WAAW,YAAY,MAAM,EAAE,OAAO,OAAO,EAAE,CAAC;AAAA,MAClF;AAAA,IACF;AAAA,EACF;AACF;;;ACxEO,IAAM,oBAA2D;AAAA,EACtE,GAAG,EAAE,KAAK,QAAQ,MAAM,mEAAmE;AAAA,EAC3F,QAAQ,EAAE,KAAK,SAAS;AAAA,EACxB,UAAU,EAAE,KAAK,WAAW;AAAA,EAC5B,QAAQ,EAAE,KAAK,SAAS;AAAA,EACxB,QAAQ,EAAE,KAAK,eAAe,MAAM,sCAAsC;AAAA,EAC1E,OAAO,EAAE,KAAK,SAAS,MAAM,yDAAyD;AAAA,EACtF,SAAS,EAAE,KAAK,qBAAqB;AAAA,EACrC,UAAU,EAAE,KAAK,cAAc;AAAA,EAC/B,MAAM,EAAE,KAAK,OAAO;AAAA,EACpB,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,OAAO;AAAA,EACP,KAAK;AACP;AAOO,IAAM,kBAAyD;AAAA,EACpE,QAAQ,EAAE,KAAK,cAAc;AAAA,EAC7B,UAAU,EAAE,KAAK,WAAW;AAAA,EAC5B,MAAM,EAAE,KAAK,aAAa;AAAA,EAC1B,QAAQ,EAAE,KAAK,SAAS;AAAA,EACxB,QAAQ,EAAE,KAAK,SAAS;AAAA,EACxB,OAAO,EAAE,KAAK,SAAS;AAAA,EACvB,OAAO,EAAE,KAAK,SAAS;AAAA,EACvB,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,MAAM;AAAA,EACN,kBAAkB;AAAA,EAClB,OAAO;AAAA,EACP,MAAM;AAAA,EACN,MAAM;AACR;AAEA,IAAM,aAA6B,EAAE,KAAK,YAAY;AACtD,IAAM,YAA4B;AAAA,EAChC,KAAK;AAAA,EACL,MAAM;AACR;AAEA,SAAS,UAAU,SAA4B,MAAwC;AACrF,SAAO,QAAQ,WAAW,KAAK,CAAC,MAAyB,EAAE,SAAS,kBAAkB,EAAE,KAAK,SAAS,IAAI;AAC5G;AAGA,SAAS,QAAQ,MAAkD;AACjE,QAAM,QAAQ,KAAK;AACnB,MAAI,UAAU,KAAM,QAAO;AAC3B,MAAI,MAAM,SAAS,UAAW,QAAO,OAAO,MAAM,UAAU,WAAW,MAAM,QAAQ;AACrF,MAAI,MAAM,SAAS,4BAA4B,CAAC,MAAM,WAAY,QAAO;AACzE,QAAM,aAAa,MAAM;AACzB,MAAI,WAAW,SAAS,cAAc,OAAO,WAAW,UAAU,YAAY,OAAO,WAAW,UAAU,WAAY,QAAO,WAAW;AACxI,MAAI,WAAW,SAAS,qBAAqB,WAAW,aAAa,WAAW,EAAG,QAAO,WAAW,SAAS,CAAC,GAAG,MAAM,UAAU;AAClI,SAAO;AACT;AAGA,SAAS,KAAK,SAA4B,MAAuB;AAC/D,QAAM,OAAO,UAAU,SAAS,IAAI;AACpC,SAAO,SAAS,UAAa,QAAQ,IAAI,MAAM;AACjD;AAKO,SAAS,SAAS,SAAmC;AAC1D,MAAI,QAAQ,KAAK,SAAS,mBAAmB,CAAC,QAAQ,KAAK,KAAM,QAAO;AACxE,QAAM,OAAO,QAAQ,KAAK;AAE1B,MAAI,SAAS,SAAS;AACpB,UAAM,OAAO,UAAU,SAAS,MAAM;AACtC,UAAM,OAAO,OAAO,QAAQ,IAAI,IAAI;AACpC,QAAI,OAAO,SAAS,SAAU,QAAO,EAAE,QAAQ,OAAO,SAAS,WAAW,MAAM,UAAU;AAC1F,UAAM,MAAM,KAAK,YAAY;AAE7B,QAAI,QAAQ,SAAU,QAAO;AAC7B,QAAI,EAAE,OAAO,iBAAkB,QAAO,EAAE,QAAQ,OAAO,SAAS,WAAW,MAAM,WAAW;AAC5F,UAAMC,QAAO,gBAAgB,GAAG;AAChC,UAAM,QAAQ,gBAAgB,GAAG;AACjC,WAAOA,QAAO,EAAE,QAAQ,OAAO,SAAS,OAAO,MAAAA,MAAK,IAAI,EAAE,QAAQ,UAAU,SAAS,MAAM;AAAA,EAC7F;AAEA,MAAI,CAAC,OAAO,OAAO,mBAAmB,IAAI,EAAG,QAAO;AACpD,OAAK,SAAS,WAAW,SAAS,YAAY,CAAC,KAAK,SAAS,UAAU,EAAG,QAAO;AACjF,MAAI,SAAS,SAAS,CAAC,UAAU,SAAS,QAAQ,KAAK,CAAC,UAAU,SAAS,QAAQ,EAAG,QAAO;AAE7F,QAAM,OAAO,kBAAkB,IAAI;AACnC,SAAO,OAAO,EAAE,QAAQ,OAAO,SAAS,IAAI,IAAI,KAAK,KAAK,IAAI,EAAE,QAAQ,UAAU,SAAS,IAAI,IAAI,IAAI;AACzG;AAgBO,IAAM,eAAgC;AAAA,EAC3C,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM,EAAE,aAAa,mFAAmF;AAAA,IACxG,QAAQ,CAAC;AAAA,IACT,UAAU;AAAA,MACR,KAAK;AAAA,MACL,QAAQ;AAAA,MACR,GAAG;AAAA,IACL;AAAA,EACF;AAAA,EACA,OAAO,SAAS;AACd,WAAO;AAAA;AAAA,MAEL,kBAAkB,MAAiB;AACjC,cAAM,UAAU;AAChB,cAAM,QAAQ,SAAS,OAAO;AAC9B,YAAI,CAAC,MAAO;AACZ,YAAI,UAAU,SAAS,OAAO,EAAG;AACjC,YAAI,MAAM,WAAW,SAAU,SAAQ,OAAO,EAAE,KAAK,QAAQ,KAAK,WAAW,UAAU,MAAM,EAAE,SAAS,MAAM,QAAQ,EAAE,CAAC;AAAA,YACpH,SAAQ,OAAO,EAAE,KAAK,QAAQ,KAAK,WAAW,OAAO,MAAM,EAAE,SAAS,MAAM,SAAS,KAAK,MAAM,KAAK,KAAK,MAAM,MAAM,KAAK,QAAQ,GAAG,EAAE,CAAC;AAAA,MAChJ;AAAA,IACF;AAAA,EACF;AACF;;;AC5JO,IAAM,eAAe,oBAAI,IAAI,CAAC,KAAK,UAAU,SAAS,YAAY,UAAU,UAAU,QAAQ,OAAO,CAAC;AAsBtG,IAAM,4BAA6C;AAAA,EACxD,MAAM;AAAA,IACJ,MAAM;AAAA,IACN,MAAM,EAAE,aAAa,4EAA4E;AAAA,IACjG,QAAQ,CAAC;AAAA,IACT,UAAU;AAAA,MACR,QACE;AAAA,MACF,GAAG;AAAA,IACL;AAAA,EACF;AAAA,EACA,OAAO,SAAS;AACd,WAAO;AAAA;AAAA,MAEL,kBAAkB,MAAiB;AACjC,cAAM,UAAU;AAChB,cAAM,OAAO,QAAQ,KAAK;AAC1B,YAAI,QAAQ,KAAK,SAAS,mBAAmB,CAAC,QAAQ,CAAC,aAAa,IAAI,IAAI,EAAG;AAI/E,YAAI,SAAS,QAAQ;AACrB,YAAI,QAAQ,SAAS,gBAAgB,OAAO,mBAAoB,QAAqB,UAAS,OAAO;AAIrG,iBAAS,IAAI,QAAQ,GAAG,IAAI,EAAE,QAAQ;AACpC,cAAI,EAAE,SAAS,gBAAgB;AAC7B,gBAAI,EAAE,MAAM,SAAS,SAAU;AAC/B;AAAA,UACF;AACA,cAAI,EAAE,SAAS,gBAAgB,EAAE,SAAS,cAAe;AACzD,cAAI,UAAU,SAAS,OAAO,EAAG;AACjC,kBAAQ,OAAO,EAAE,KAAK,QAAQ,KAAK,WAAW,UAAU,MAAM,EAAE,KAAK,EAAE,CAAC;AACxE;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;ALzDA,IAAM,EAAE,QAAQ,IAAI,cAAc,YAAY,GAAG,EAAE,oBAAoB;AAGhE,IAAM,aAAa;AAM1B,IAAM,WAAW;AAAA,EACf,kBAAkB;AACpB;AAcA,IAAM,eAAe;AAAA,EACnB,iCAAiC;AAAA,EACjC,4BAA4B;AAAA,EAC5B,wBAAwB;AAAA,EACxB,yBAAyB;AAC3B;AAEA,IAAM,QAAQ,EAAE,GAAG,UAAU,GAAG,aAAa;AAE7C,IAAM,SAAS;AAAA,EACb,MAAM,EAAE,MAAM,yBAAyB,QAAQ;AAAA,EAC/C;AAAA,EACA,SAAS,CAAC;AACZ;AAGO,IAAM,eAAe,OAAO,KAAK,QAAQ,EAAE,IAAI,CAAC,SAAS,GAAG,UAAU,IAAI,IAAI,EAAE;AAEhF,IAAM,mBAAmB,OAAO,KAAK,YAAY,EAAE,IAAI,CAAC,SAAS,GAAG,UAAU,IAAI,IAAI,EAAE;AAU/F,OAAO,QAAQ,cAAc;AAAA,EAC3B,MAAM;AAAA,EACN,SAAS,EAAE,CAAC,UAAU,GAAG,OAAO;AAAA,EAChC,OAAO,EAAE,CAAC,GAAG,UAAU,iBAAiB,GAAG,QAAQ;AACrD;AACA,OAAO,QAAQ,SAAS;AAAA,EACtB,MAAM;AAAA,EACN,SAAS,EAAE,CAAC,UAAU,GAAG,OAAO;AAAA,EAChC,OAAO,OAAO,YAAY,aAAa,IAAI,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,CAAC;AACnE;AACA,OAAO,QAAQ,UAAU;AAAA,EACvB,MAAM;AAAA,EACN,SAAS,EAAE,CAAC,UAAU,GAAG,OAAO;AAAA,EAChC,OAAO,OAAO,YAAY,iBAAiB,IAAI,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,CAAC;AACvE;AAEA,IAAO,gBAAQ;AA8BR,SAAS,WAAW,SAA8B,OAA0B,cAAyB;AAC1G,QAAM,SAAwC,OAAO,YAAY,KAAK,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,GAAG,SAAS,EAAE,CAAC,CAAC,CAAC;AAC/H,QAAM,WAAkC,CAAC;AACzC,QAAM,OAAO,CAAC,WAAwD,OAAO,WAAW,YAAY,OAAO,WAAW,GAAG,UAAU,GAAG;AACtI,aAAW,UAAU,SAAS;AAC5B,eAAW,WAAW,OAAO,UAAU;AAGrC,UAAI,CAAC,KAAK,QAAQ,MAAM,EAAG;AAC3B,YAAM,QAAS,OAAO,QAAQ,MAAM,MAAM,EAAE,QAAQ,GAAG,UAAU,GAAG,SAAS,EAAE;AAC/E,UAAI,QAAQ,cAAc,UAAW,OAAM,WAAW;AAAA,eAC7C,QAAQ,aAAa,EAAG,OAAM,UAAU;AAAA,UAC5C,OAAM,YAAY;AAAA,IACzB;AACA,eAAW,WAAW,OAAO,sBAAsB,CAAC,GAAG;AACrD,UAAI,KAAK,QAAQ,MAAM,EAAG,UAAS,KAAK,EAAE,UAAU,OAAO,UAAU,MAAM,QAAQ,MAAM,QAAQ,QAAQ,OAAO,CAAC;AAAA,IACnH;AAAA,EACF;AACA,SAAO,EAAE,OAAO,QAAQ,SAAS;AACnC;",
|
|
6
|
+
"names": ["rules", "part"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Rule } from 'eslint';
|
|
2
|
+
interface JSXAttributeValue {
|
|
3
|
+
type: string;
|
|
4
|
+
value?: unknown;
|
|
5
|
+
expression?: {
|
|
6
|
+
type: string;
|
|
7
|
+
value?: unknown;
|
|
8
|
+
expressions?: unknown[];
|
|
9
|
+
quasis?: {
|
|
10
|
+
value: {
|
|
11
|
+
cooked: string | null;
|
|
12
|
+
};
|
|
13
|
+
}[];
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
interface JSXAttribute {
|
|
17
|
+
type: 'JSXAttribute';
|
|
18
|
+
name: {
|
|
19
|
+
type: string;
|
|
20
|
+
name?: string;
|
|
21
|
+
};
|
|
22
|
+
value: JSXAttributeValue | null;
|
|
23
|
+
}
|
|
24
|
+
interface JSXOpeningElement {
|
|
25
|
+
name: {
|
|
26
|
+
type: string;
|
|
27
|
+
name?: string;
|
|
28
|
+
};
|
|
29
|
+
attributes: (JSXAttribute | {
|
|
30
|
+
type: 'JSXSpreadAttribute';
|
|
31
|
+
})[];
|
|
32
|
+
loc: NonNullable<Rule.Node['loc']>;
|
|
33
|
+
range: [number, number];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* What the package has instead of one raw element: the component the message
|
|
37
|
+
* names, and where one element does more than one job, the others after it.
|
|
38
|
+
* `null` is an element the package has no part for yet.
|
|
39
|
+
*/
|
|
40
|
+
export interface RawElementPart {
|
|
41
|
+
use: string;
|
|
42
|
+
more?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The mapping (UIG-7), derived from the package's exports: every interactive
|
|
46
|
+
* element in the HTML standard's list ("interactive content"), plus `<dialog>`,
|
|
47
|
+
* `<form>` and `<progress>`, which have a part here. docs/GATES.md prints it.
|
|
48
|
+
*
|
|
49
|
+
* Not here, on purpose: `<summary>` (it lives inside a `<details>`, which is
|
|
50
|
+
* reported), `<option>`, `<optgroup>` and `<datalist>` (they live inside a
|
|
51
|
+
* `<select>` or beside an input), `<fieldset>` and `<legend>` (a group's frame,
|
|
52
|
+
* no control of its own), and `<img>` without `usemap`, `<hr>`, `<kbd>`, which
|
|
53
|
+
* are not controls. An `<audio>` or `<video>` is a control only with `controls`.
|
|
54
|
+
*/
|
|
55
|
+
export declare const RAW_ELEMENT_PARTS: Record<string, RawElementPart | null>;
|
|
56
|
+
/**
|
|
57
|
+
* An `<input>` is as many controls as its `type`. A type missing from here
|
|
58
|
+
* (text, email, password, url, tel, number, or one the browser does not know)
|
|
59
|
+
* is a text box.
|
|
60
|
+
*/
|
|
61
|
+
export declare const RAW_INPUT_PARTS: Record<string, RawElementPart | null>;
|
|
62
|
+
type Found = {
|
|
63
|
+
report: 'raw';
|
|
64
|
+
element: string;
|
|
65
|
+
part: RawElementPart;
|
|
66
|
+
} | {
|
|
67
|
+
report: 'noPart';
|
|
68
|
+
element: string;
|
|
69
|
+
} | null;
|
|
70
|
+
/** What this element is to the rule: a raw element with a part, one with none yet, or none of its business. */
|
|
71
|
+
export declare function classify(element: JSXOpeningElement): Found;
|
|
72
|
+
/**
|
|
73
|
+
* A raw interactive element in an app, where the package has a part for it
|
|
74
|
+
* (UIG-3 began it with `<button>`; UIG-7 made it every element). A control
|
|
75
|
+
* the app writes itself has a look, a focus ring and keys that nobody else
|
|
76
|
+
* keeps in step with — and it looks nearly right, which is why nobody catches
|
|
77
|
+
* it by eye. Every message names the part to use.
|
|
78
|
+
*
|
|
79
|
+
* An element the package has no part for yet is refused too, with a message
|
|
80
|
+
* that says so: the part is made in the package, not by hand in an app
|
|
81
|
+
* (Katerina, 16 September).
|
|
82
|
+
*
|
|
83
|
+
* Only a JSX element written with a lowercase name. `<Button>`,
|
|
84
|
+
* `<Foo.button>` and `createElement('button')` are not this rule's business.
|
|
85
|
+
*/
|
|
86
|
+
export declare const noRawElement: Rule.RuleModule;
|
|
87
|
+
export {};
|
|
88
|
+
//# sourceMappingURL=no-raw-element.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-raw-element.d.ts","sourceRoot":"","sources":["../../src/eslint/no-raw-element.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAGlC,UAAU,iBAAiB;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,UAAU,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,EAAE,CAAC;QAAC,MAAM,CAAC,EAAE;YAAE,KAAK,EAAE;gBAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;aAAE,CAAA;SAAE,EAAE,CAAA;KAAE,CAAA;CACzH;AAED,UAAU,YAAY;IACpB,IAAI,EAAE,cAAc,CAAA;IACpB,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACrC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAA;CAChC;AAED,UAAU,iBAAiB;IACzB,IAAI,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACrC,UAAU,EAAE,CAAC,YAAY,GAAG;QAAE,IAAI,EAAE,oBAAoB,CAAA;KAAE,CAAC,EAAE,CAAA;IAC7D,GAAG,EAAE,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAA;IAClC,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACxB;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAiBnE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAgBjE,CAAA;AA8BD,KAAK,KAAK,GAAG;IAAE,MAAM,EAAE,KAAK,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,cAAc,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAA;AAEpH,+GAA+G;AAC/G,wBAAgB,QAAQ,CAAC,OAAO,EAAE,iBAAiB,GAAG,KAAK,CAuB1D;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,EAAE,IAAI,CAAC,UAwB/B,CAAA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a `Form` around this is busy (UIG-7).
|
|
3
|
+
*
|
|
4
|
+
* `Form` switches its children off with a disabled `<fieldset>`, which the
|
|
5
|
+
* browser applies to every native control inside — but a part that draws its
|
|
6
|
+
* look from Base UI's own `disabled` (Button, IconButton, Checkbox) is not
|
|
7
|
+
* told, so it kept its normal look while it could not be pressed, and a
|
|
8
|
+
* Checkbox, which is a `<span>`, could still be ticked (Katerina saw the busy
|
|
9
|
+
* story's button unchanged, 16 September). Those parts read this and switch
|
|
10
|
+
* off the way their own `disabled` does. Not exported: it is the package's
|
|
11
|
+
* wiring between Form and its parts.
|
|
12
|
+
*/
|
|
13
|
+
export declare const FormBusyContext: import("react").Context<boolean>;
|
|
14
|
+
export declare function useFormBusy(): boolean;
|
|
15
|
+
//# sourceMappingURL=formBusy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"formBusy.d.ts","sourceRoot":"","sources":["../src/formBusy.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,kCAAuB,CAAA;AAEnD,wBAAgB,WAAW,IAAI,OAAO,CAErC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export { Kbd, type KbdProps } from './Kbd';
|
|
|
27
27
|
export { IdentityMenu, IdentityPanel, type Identity, type IdentityMenuProps, type IdentityPanelProps } from './IdentityMenu';
|
|
28
28
|
export { Tooltip, TooltipProvider, WithTooltip, type TooltipProps, type TooltipProviderProps, type WithTooltipProps } from './Tooltip';
|
|
29
29
|
export { Field, FieldLine, type FieldLineProps, type FieldLineTone, type FieldProps } from './Field';
|
|
30
|
+
export { FilePicker, type FilePickerProps } from './FilePicker';
|
|
31
|
+
export { Form, type FormProps } from './Form';
|
|
30
32
|
export { Select, type SelectOption, type SelectProps } from './Select';
|
|
31
33
|
export { TextInput, type TextInputProps } from './TextInput';
|
|
32
34
|
export { Textarea, type TextareaProps } from './Textarea';
|