@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.
@@ -1 +1 @@
1
- {"version":3,"file":"no-restyled-part.d.ts","sourceRoot":"","sources":["../../src/eslint/no-restyled-part.ts"],"names":[],"mappings":"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"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estiva-app/ui",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "description": "Estiva's design tokens (the contract) and a small set of primitives (a convenience) for every Estiva app.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/registry.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "kind": "package",
5
5
  "repo": "estiva-ui",
6
6
  "package": "@estiva-app/ui",
7
- "packageVersion": "0.49.0",
7
+ "packageVersion": "0.50.0",
8
8
  "exports": 104,
9
9
  "typeExports": 113,
10
10
  "files": null
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') {