@estiva-app/ui 0.49.0 → 0.50.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 +2 -0
- package/dist/eslint/index.js +26 -0
- package/dist/eslint/index.js.map +2 -2
- package/dist/eslint/no-restyled-part.d.ts.map +1 -1
- package/package.json +1 -1
- package/registry.json +1 -1
- package/src/Form.mdx +6 -0
- package/src/eslint/no-restyled-part.test.ts +16 -0
- package/src/eslint/no-restyled-part.ts +37 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"no-restyled-part.d.ts","sourceRoot":"","sources":["../../src/eslint/no-restyled-part.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAA;AAGzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAIH,6FAA6F;AAC7F,eAAO,MAAM,SAAS,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAUxD,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,QACke,CAAA;AAS9f,oGAAoG;AACpG,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAajF;AAED,4CAA4C;AAC5C,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAIlD;
|
|
1
|
+
{"version":3,"file":"no-restyled-part.d.ts","sourceRoot":"","sources":["../../src/eslint/no-restyled-part.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/registry.json
CHANGED
package/src/Form.mdx
CHANGED
|
@@ -63,6 +63,11 @@ import { Button, Field, Form, TextInput } from '@estiva-app/ui'
|
|
|
63
63
|
September), so every form reads the same. Pass no layout of your own: the
|
|
64
64
|
gate lint refuses a gap, a direction or a grid on a `Form`. `className`
|
|
65
65
|
still places the form itself: its space around, its size.
|
|
66
|
+
- **Words before the fields go inside the form**, as one block (a title and
|
|
67
|
+
its sentence, 8px apart): the form then puts the same 24px between them
|
|
68
|
+
and the first field. A box of your own around the words and the form, with
|
|
69
|
+
its own gap, sets that space instead, and the gate lint refuses it
|
|
70
|
+
(Katerina, 1 October: Archive read 12px where Move read 24px).
|
|
66
71
|
- **`layout="free"`** is for a form that is not a list of fields: a field and
|
|
67
72
|
its button, a composer. It places nothing, and `className` lays it out.
|
|
68
73
|
- The form draws no box, and neither does the `<fieldset>` inside it.
|
|
@@ -77,6 +82,7 @@ It does these by itself. Write none of them around it.
|
|
|
77
82
|
| Leaves Enter alone where it means something else: a Textarea, a list, a people picker | exceptions of your own | |
|
|
78
83
|
| Does nothing while it is sending: no second send, and nothing inside takes focus | a busy flag on every control | |
|
|
79
84
|
| Stacks its fields in one column, 24px apart | a gap, a direction or a grid on the form | `no-restyled-part` |
|
|
85
|
+
| Puts 24px between the words before the fields and the first one | a spaced box around the words and the form | `no-restyled-part` |
|
|
80
86
|
|
|
81
87
|
## Keys
|
|
82
88
|
|
|
@@ -77,6 +77,10 @@ tester.run('no-restyled-part', noRestyledPart, {
|
|
|
77
77
|
{ name: 'Form placed, its fields its own', filename: app, code: ui('Form', ' <Form onSubmit={send} className="min-h-0 flex-1 px-5">x</Form>') },
|
|
78
78
|
{ name: 'a free Form lays itself out', filename: app, code: ui('Form', ' <Form onSubmit={send} layout="free" className="flex items-center gap-2">x</Form>') },
|
|
79
79
|
{ name: 'a Form whose layout the rule cannot read passes', filename: app, code: ui('Form', ' <Form onSubmit={send} layout={how} className="flex gap-2">x</Form>') },
|
|
80
|
+
{ name: "Peek's MoveDialog: the sentences one block inside the Form", filename: app, code: ui('Form', ' <Form onSubmit={send}><div className="flex flex-col gap-2"><p>Notes</p><p>Everyone who can read it now still can.</p></div>x</Form>') },
|
|
81
|
+
{ name: 'a spaced box with only the Form in it spaces nothing', filename: app, code: ui('Form', ' <div className="flex flex-col gap-3"><Form onSubmit={send}>x</Form></div>') },
|
|
82
|
+
{ name: 'a box beside the Form with no spacing of its own', filename: app, code: ui('Form', ' <div className="flex flex-col"><p>Hides it.</p><Form onSubmit={send}>x</Form></div>') },
|
|
83
|
+
{ name: 'a free Form in a spaced row is laid out by its row', filename: app, code: ui('Form', ' <div className="flex items-center gap-2"><p>Name</p><Form onSubmit={send} layout="free">x</Form></div>') },
|
|
80
84
|
// escapes
|
|
81
85
|
{ name: 'escaped, with its reason', filename: app, code: ui('Link', ' // @estiva-escape(no-restyled-part): the whole row is the link, and no part does that yet\n <Link href="/x" className="after:absolute after:inset-0">x</Link>') },
|
|
82
86
|
// the package itself
|
|
@@ -189,6 +193,18 @@ tester.run('no-restyled-part', noRestyledPart, {
|
|
|
189
193
|
code: "import { Link, type LinkProps } from '@estiva-app/ui'\nfunction RouterLink({ href, ...props }: LinkProps) {\n return <Link href={href} {...props} />\n}\nexport function Probe() {\n return <RouterLink href=\"/x\" className=\"text-h5\">x</RouterLink>\n}\n",
|
|
190
194
|
errors: [{ message: restyled('`text-h5`', '`RouterLink` (it hands its props to `Link`)', ' Use its `variant`, `truncate` or `cover`.') }],
|
|
191
195
|
},
|
|
196
|
+
{
|
|
197
|
+
name: "Peek's and Ship's ArchiveDialog at 0.49.0: sentences and the Form in a gap-3 box, 12px above the field",
|
|
198
|
+
filename: app,
|
|
199
|
+
code: ui('Form', ' <div className="flex flex-col gap-3 text-body-2 text-text-primary"><p>This hides the topic.</p><p>Nothing is deleted.</p><Form id="f" onSubmit={send}>x</Form></div>'),
|
|
200
|
+
errors: [{ message: '`gap-3` on the box around `Form` spaces the form from what is beside it, so the gap above its first field is not the form’s 24px. Put what is beside it inside the `Form`, as one block before the fields, and drop the box: the Form puts 24px between them (Katerina, 1 October; MoveDialog in Peek does this).' }],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "a DialogShell's body spaced around the Form and a sentence",
|
|
204
|
+
filename: app,
|
|
205
|
+
code: ui('DialogShell, Form', ' <DialogShell title="Note" onClose={send} bodyClassName="flex flex-col space-y-4"><p>Say why.</p><Form onSubmit={send}>x</Form></DialogShell>'),
|
|
206
|
+
errors: [{ message: '`space-y-4` on the box around `Form` spaces the form from what is beside it, so the gap above its first field is not the form’s 24px. Put what is beside it inside the `Form`, as one block before the fields, and drop the box: the Form puts 24px between them (Katerina, 1 October; MoveDialog in Peek does this).' }],
|
|
207
|
+
},
|
|
192
208
|
{
|
|
193
209
|
name: 'a Form laid out by hand',
|
|
194
210
|
filename: app,
|
|
@@ -105,6 +105,12 @@ const isFieldsLayout = (token: string) => {
|
|
|
105
105
|
return !variants.some((variant) => REACHING_VARIANT.test(variant)) && FIELDS_LAYOUT.test(utility)
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/** Room between a box's children, down the page or across it: what spaces a Form from its neighbours. */
|
|
109
|
+
const isSpacing = (token: string) => {
|
|
110
|
+
const { variants, utility } = splitClass(token)
|
|
111
|
+
return !variants.some((variant) => REACHING_VARIANT.test(variant)) && /^(?:gap|gap-x|gap-y|space-x|space-y)-/.test(utility)
|
|
112
|
+
}
|
|
113
|
+
|
|
108
114
|
const isPadding = (token: string) => {
|
|
109
115
|
const { variants, utility } = splitClass(token)
|
|
110
116
|
return !variants.some((variant) => REACHING_VARIANT.test(variant)) && /^p[xytrblse]?-/.test(utility)
|
|
@@ -623,6 +629,8 @@ export const noRestyledPart: Rule.RuleModule = {
|
|
|
623
629
|
'{{classes}} pads {{where}}. EmptyState takes no padding (Katerina, 14 September): its room comes from the box its rows live in, so put the padding on that box.',
|
|
624
630
|
formLayout:
|
|
625
631
|
'{{classes}} lays out the fields of {{where}}. A Form places its own fields, 24px apart (Katerina, 28 September), so every form reads the same: remove it. A form that is not a list of fields (a field and its button, a composer) says `layout="free"` and lays itself out.',
|
|
632
|
+
formSpacedFromOutside:
|
|
633
|
+
'{{classes}} on the box around `Form` spaces the form from what is beside it, so the gap above its first field is not the form’s 24px. Put what is beside it inside the `Form`, as one block before the fields, and drop the box: the Form puts 24px between them (Katerina, 1 October; MoveDialog in Peek does this).',
|
|
626
634
|
...ESCAPE_MESSAGES,
|
|
627
635
|
},
|
|
628
636
|
},
|
|
@@ -685,6 +693,34 @@ export const noRestyledPart: Rule.RuleModule = {
|
|
|
685
693
|
context.report({ loc: at.loc as NonNullable<Rule.Node['loc']>, messageId: 'restyledStyle', data: { keys: code([...new Set(keys)]), where, use } })
|
|
686
694
|
}
|
|
687
695
|
|
|
696
|
+
/**
|
|
697
|
+
* A Form with something beside it, in a box that spaces them: the gap
|
|
698
|
+
* above the first field is then the box's, not the Form's 24px (Peek's and
|
|
699
|
+
* Ship's ArchiveDialog, 1 October: 12px). Read on the box's own class
|
|
700
|
+
* props, `className` or a part's `bodyClassName`; a box with only the Form
|
|
701
|
+
* in it spaces nothing.
|
|
702
|
+
*/
|
|
703
|
+
const checkSpacedFromOutside = (opening: Node) => {
|
|
704
|
+
const form = opening.parent as Node | undefined
|
|
705
|
+
const box = form?.parent as Node | undefined
|
|
706
|
+
if (form?.type !== 'JSXElement' || box?.type !== 'JSXElement') return
|
|
707
|
+
const beside = children(box, 'children').filter((node) => {
|
|
708
|
+
if (!node || node === form) return false
|
|
709
|
+
if (node.type === 'JSXText') return String(node.value).trim() !== ''
|
|
710
|
+
if (node.type === 'JSXExpressionContainer') return child(node, 'expression')?.type !== 'JSXEmptyExpression'
|
|
711
|
+
return true
|
|
712
|
+
})
|
|
713
|
+
if (!beside.length) return
|
|
714
|
+
const boxOpening = child(box, 'openingElement') as Node
|
|
715
|
+
const spacing = children(boxOpening, 'attributes').flatMap((attribute) => {
|
|
716
|
+
if (attribute?.type !== 'JSXAttribute') return []
|
|
717
|
+
const prop = nameOf(child(attribute, 'name'))
|
|
718
|
+
return prop === 'className' || prop === 'bodyClassName' ? classesOf(child(attribute, 'value'), scopeOf).filter(isSpacing) : []
|
|
719
|
+
})
|
|
720
|
+
if (!spacing.length || escaped(opening)) return
|
|
721
|
+
context.report({ loc: opening.loc as NonNullable<Rule.Node['loc']>, messageId: 'formSpacedFromOutside', data: { classes: code([...new Set(spacing)]) } })
|
|
722
|
+
}
|
|
723
|
+
|
|
688
724
|
return {
|
|
689
725
|
Program(program) {
|
|
690
726
|
locals = moduleParts(file, program as unknown as Node, parser, new Set([file])).locals
|
|
@@ -693,6 +729,7 @@ export const noRestyledPart: Rule.RuleModule = {
|
|
|
693
729
|
const element = ruleNode as unknown as Node
|
|
694
730
|
const binding = locals && bindingOfTag(child(element, 'name'), locals)
|
|
695
731
|
if (!binding) return
|
|
732
|
+
if (binding.part === 'Form' && binding.props === 'all' && placesItsFields(element)) checkSpacedFromOutside(element)
|
|
696
733
|
for (const attribute of children(element, 'attributes')) {
|
|
697
734
|
if (!attribute) continue
|
|
698
735
|
if (attribute.type === 'JSXAttribute') {
|