@estiva-app/ui 0.19.0 → 0.21.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 +38 -2
- package/dist/AttachmentCard.d.ts.map +1 -1
- package/dist/Breadcrumb.d.ts.map +1 -1
- package/dist/Button.d.ts +6 -1
- package/dist/Button.d.ts.map +1 -1
- package/dist/Card.d.ts +6 -1
- package/dist/Card.d.ts.map +1 -1
- package/dist/DialogShell.d.ts.map +1 -1
- package/dist/Divider.d.ts +7 -0
- package/dist/Divider.d.ts.map +1 -1
- package/dist/IconButton.d.ts +12 -2
- package/dist/IconButton.d.ts.map +1 -1
- package/dist/IdentityMenu.d.ts.map +1 -1
- package/dist/Link.d.ts +6 -1
- package/dist/Link.d.ts.map +1 -1
- package/dist/Menu.d.ts.map +1 -1
- package/dist/Popover.d.ts.map +1 -1
- package/dist/ReactionPicker.d.ts.map +1 -1
- package/dist/SectionLabel.d.ts +6 -1
- package/dist/SectionLabel.d.ts.map +1 -1
- package/dist/Tooltip.d.ts.map +1 -1
- package/dist/eslint/index.d.ts +2 -0
- package/dist/eslint/index.d.ts.map +1 -1
- package/dist/eslint/index.js +514 -6
- package/dist/eslint/index.js.map +4 -4
- package/dist/eslint/no-restyled-part.d.ts +81 -0
- package/dist/eslint/no-restyled-part.d.ts.map +1 -0
- package/dist/gates/app-checks.d.ts +40 -0
- package/dist/gates/app-checks.d.ts.map +1 -0
- package/dist/gates/chunk-AWO7SZSA.js +477 -0
- package/dist/gates/chunk-AWO7SZSA.js.map +7 -0
- package/dist/gates/chunk-ZGJ2J5NU.js +754 -0
- package/dist/gates/chunk-ZGJ2J5NU.js.map +7 -0
- package/dist/gates/cli.d.ts +2 -0
- package/dist/gates/cli.d.ts.map +1 -0
- package/dist/gates/cli.js +46 -0
- package/dist/gates/cli.js.map +7 -0
- package/dist/gates/count.d.ts +20 -0
- package/dist/gates/count.d.ts.map +1 -0
- package/dist/gates/create-app-cli.d.ts +2 -0
- package/dist/gates/create-app-cli.d.ts.map +1 -0
- package/dist/gates/create-app.d.ts +23 -0
- package/dist/gates/create-app.d.ts.map +1 -0
- package/dist/gates/create-app.js +30 -0
- package/dist/gates/create-app.js.map +7 -0
- package/dist/gates/gate-config.d.ts +44 -0
- package/dist/gates/gate-config.d.ts.map +1 -0
- package/dist/gates/hook.d.ts +18 -0
- package/dist/gates/hook.d.ts.map +1 -0
- package/dist/gates/index.d.ts +30 -0
- package/dist/gates/index.d.ts.map +1 -0
- package/dist/gates/index.js +415 -0
- package/dist/gates/index.js.map +7 -0
- package/dist/gates/status.d.ts +84 -0
- package/dist/gates/status.d.ts.map +1 -0
- package/dist/gates/token-lint.d.ts +26 -0
- package/dist/gates/token-lint.d.ts.map +1 -0
- package/dist/index.js +290 -266
- package/dist/index.js.map +4 -4
- package/package.json +30 -4
- package/src/AttachmentCard.tsx +7 -2
- package/src/Banner.tsx +2 -2
- package/src/Breadcrumb.tsx +9 -4
- package/src/Button.mdx +7 -0
- package/src/Button.stories.tsx +4 -1
- package/src/Button.tsx +10 -2
- package/src/Card.mdx +7 -0
- package/src/Card.stories.tsx +11 -0
- package/src/Card.tsx +8 -0
- package/src/CommandPalette.tsx +1 -1
- package/src/ConfirmDialog.tsx +2 -2
- package/src/DialogShell.tsx +6 -7
- package/src/Divider.mdx +3 -0
- package/src/Divider.tsx +12 -2
- package/src/EditableText.mdx +5 -0
- package/src/IconButton.mdx +9 -0
- package/src/IconButton.stories.tsx +10 -1
- package/src/IconButton.tsx +22 -3
- package/src/IdentityMenu.tsx +4 -1
- package/src/Link.mdx +6 -0
- package/src/Link.stories.tsx +12 -0
- package/src/Link.tsx +7 -2
- package/src/Menu.stories.tsx +1 -1
- package/src/Menu.test.tsx +2 -2
- package/src/Menu.tsx +6 -3
- package/src/Person.stories.tsx +5 -5
- package/src/Popover.test.tsx +2 -2
- package/src/Popover.tsx +3 -2
- package/src/PreviewCard.tsx +1 -1
- package/src/Property.stories.tsx +2 -2
- package/src/ReactionPicker.tsx +6 -3
- package/src/ScrollArea.stories.tsx +21 -7
- package/src/SectionLabel.mdx +3 -2
- package/src/SectionLabel.stories.tsx +3 -0
- package/src/SectionLabel.tsx +7 -2
- package/src/Tooltip.tsx +12 -14
- package/src/eslint/index.test.ts +32 -7
- package/src/eslint/index.ts +10 -1
- package/src/eslint/no-restyled-part.test.ts +301 -0
- package/src/eslint/no-restyled-part.ts +647 -0
- package/src/gates/app-checks.ts +224 -0
- package/src/gates/cli.ts +55 -0
- package/src/gates/count.ts +76 -0
- package/src/gates/create-app-cli.ts +25 -0
- package/src/gates/create-app.test.ts +72 -0
- package/src/gates/create-app.ts +797 -0
- package/src/gates/gate-config.ts +78 -0
- package/src/gates/gates.test.ts +183 -0
- package/src/gates/hook.ts +111 -0
- package/src/gates/index.ts +30 -0
- package/src/gates/status.ts +532 -0
- package/src/gates/token-lint.ts +231 -0
package/src/Person.stories.tsx
CHANGED
|
@@ -26,15 +26,15 @@ export const OwnFallback: Story = {
|
|
|
26
26
|
parameters: { a11y: { config: { rules: [{ id: 'color-contrast', enabled: false }] } } },
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/** The text size is the
|
|
29
|
+
/** The text size is where it sits: the box around it sets the words, `size` sets the face. */
|
|
30
30
|
export const Sizes: Story = {
|
|
31
31
|
parameters: { controls: { disable: true } },
|
|
32
32
|
render: () => (
|
|
33
33
|
<div className="flex flex-col items-start gap-3">
|
|
34
|
-
<Person name="Ana Duarte" size={16}
|
|
35
|
-
<Person name="Ana Duarte" size={20}
|
|
36
|
-
<Person name="Ravi Mehta" size={22}
|
|
37
|
-
<Person name="Ravi Mehta" size={28}
|
|
34
|
+
<div className="flex text-caption"><Person name="Ana Duarte" size={16} /></div>
|
|
35
|
+
<div className="flex text-body-2"><Person name="Ana Duarte" size={20} /></div>
|
|
36
|
+
<div className="flex text-body-2"><Person name="Ravi Mehta" size={22} /></div>
|
|
37
|
+
<div className="flex text-body-1"><Person name="Ravi Mehta" size={28} /></div>
|
|
38
38
|
</div>
|
|
39
39
|
),
|
|
40
40
|
}
|
package/src/Popover.test.tsx
CHANGED
|
@@ -233,8 +233,8 @@ describe('Popover, centred on its anchor', () => {
|
|
|
233
233
|
* as on 0.12.5.
|
|
234
234
|
*/
|
|
235
235
|
describe('Popover, padding', () => {
|
|
236
|
-
// The scrolling content is the box
|
|
237
|
-
const contentOf = (child: HTMLElement) => child.
|
|
236
|
+
// The scrolling content is the box the children sit in directly.
|
|
237
|
+
const contentOf = (child: HTMLElement) => child.parentElement as HTMLElement
|
|
238
238
|
|
|
239
239
|
it('pads its content 8px by default', async () => {
|
|
240
240
|
render(
|
package/src/Popover.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useMemo, type ReactElement, type ReactNode, type RefObject } from 'react'
|
|
2
2
|
import { Popover as BasePopover } from '@base-ui/react/popover'
|
|
3
3
|
import { cn } from './cn'
|
|
4
|
+
import { DividerInPanel } from './Divider'
|
|
4
5
|
import { triggerDisabled } from './triggerDisabled'
|
|
5
6
|
import { ScrollArea } from './ScrollArea'
|
|
6
7
|
import { MenuPanel } from './Menu'
|
|
@@ -200,9 +201,9 @@ export function Popover({ trigger, anchor, align = 'left', side = 'bottom', open
|
|
|
200
201
|
— see the props. */}
|
|
201
202
|
<ScrollArea
|
|
202
203
|
viewportClassName={maxHeight ?? 'max-h-[var(--available-height)]'}
|
|
203
|
-
contentClassName={cn('flex flex-col p-2
|
|
204
|
+
contentClassName={cn('flex flex-col p-2', contentClassName)}
|
|
204
205
|
>
|
|
205
|
-
{children}
|
|
206
|
+
<DividerInPanel.Provider value={true}>{children}</DividerInPanel.Provider>
|
|
206
207
|
</ScrollArea>
|
|
207
208
|
</BasePopover.Popup>
|
|
208
209
|
</BasePopover.Positioner>
|
package/src/PreviewCard.tsx
CHANGED
|
@@ -83,7 +83,7 @@ export function PreviewCard({ content, children, side = 'right', delay = OPEN_DE
|
|
|
83
83
|
className={cn('w-[360px] p-3 outline-none', className)}
|
|
84
84
|
render={<MenuPanel />}
|
|
85
85
|
>
|
|
86
|
-
<ScrollArea viewportClassName="max-h-[calc(min(300px,var(--available-height))_-_1.5rem)]" contentClassName="flex flex-col gap-3
|
|
86
|
+
<ScrollArea viewportClassName="max-h-[calc(min(300px,var(--available-height))_-_1.5rem)]" contentClassName="flex flex-col gap-3">
|
|
87
87
|
{content}
|
|
88
88
|
</ScrollArea>
|
|
89
89
|
</BasePreviewCard.Popup>
|
package/src/Property.stories.tsx
CHANGED
|
@@ -28,7 +28,7 @@ export const APanel: Story = {
|
|
|
28
28
|
<Chip type="success" label="Done" />
|
|
29
29
|
</Property>
|
|
30
30
|
<Property label="Lead">
|
|
31
|
-
<Person name="Ana Duarte" size={16}
|
|
31
|
+
<div className="flex text-caption text-text-primary"><Person name="Ana Duarte" size={16} /></div>
|
|
32
32
|
</Property>
|
|
33
33
|
<Property label="Updated">
|
|
34
34
|
<span className="text-caption text-text-secondary">2d ago</span>
|
|
@@ -42,7 +42,7 @@ export const Stacked: Story = {
|
|
|
42
42
|
args: {
|
|
43
43
|
label: 'Assignee',
|
|
44
44
|
layout: 'stacked',
|
|
45
|
-
children: <
|
|
45
|
+
children: <div className="flex text-body-2 text-text-primary"><Person name="Ravi Mehta" size={20} /></div>,
|
|
46
46
|
},
|
|
47
47
|
}
|
|
48
48
|
|
package/src/ReactionPicker.tsx
CHANGED
|
@@ -77,10 +77,13 @@ export function ReactionPicker({ options, onSelect, 'aria-label': ariaLabel = 'R
|
|
|
77
77
|
/* Peek's geometry, verbatim: a 28px square rather than the
|
|
78
78
|
IconButton's 24, and the emoji at 18px — larger than a `Reaction`
|
|
79
79
|
pill's 16, because here the emoji is the whole control. */
|
|
80
|
-
className="size-7
|
|
80
|
+
className="size-7"
|
|
81
81
|
>
|
|
82
|
-
{/* Decorative, as on `Reaction`: the control is named above.
|
|
83
|
-
|
|
82
|
+
{/* Decorative, as on `Reaction`: the control is named above. Its size is
|
|
83
|
+
on the emoji itself, not pushed into the button (UIG-9). */}
|
|
84
|
+
<span aria-hidden="true" className="text-h3 leading-none">
|
|
85
|
+
{option.emoji}
|
|
86
|
+
</span>
|
|
84
87
|
</ToolbarButton>
|
|
85
88
|
))}
|
|
86
89
|
</Toolbar>
|
|
@@ -17,7 +17,8 @@ const rows = Array.from({ length: 40 }, (_, i) => `Row ${i + 1}`)
|
|
|
17
17
|
/** A list taller than its box. The bar shows while the pointer is over it or the list is moving. */
|
|
18
18
|
export const Default: Story = {
|
|
19
19
|
render: () => (
|
|
20
|
-
<
|
|
20
|
+
<div className="h-[240px] w-[280px] rounded-lg border border-border-default bg-bg-surface">
|
|
21
|
+
<ScrollArea className="h-full" viewportClassName="p-2">
|
|
21
22
|
<ul className="flex flex-col gap-px">
|
|
22
23
|
{rows.map((row) => (
|
|
23
24
|
<li key={row} className="rounded-md px-2 py-1.5 text-body-2 text-text-primary">
|
|
@@ -26,13 +27,15 @@ export const Default: Story = {
|
|
|
26
27
|
))}
|
|
27
28
|
</ul>
|
|
28
29
|
</ScrollArea>
|
|
30
|
+
</div>
|
|
29
31
|
),
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/** Rows that stick to the top as the list moves under them. The bar stays above them. */
|
|
33
35
|
export const StickyHeadings: Story = {
|
|
34
36
|
render: () => (
|
|
35
|
-
<
|
|
37
|
+
<div className="h-[240px] w-[280px] rounded-lg border border-border-default bg-bg-surface">
|
|
38
|
+
<ScrollArea className="h-full">
|
|
36
39
|
{['Group one', 'Group two'].map((group) => (
|
|
37
40
|
<div key={group} className="flex flex-col">
|
|
38
41
|
<p className="sticky top-0 z-10 bg-bg-surface px-4 py-2 text-caption text-text-secondary">{group}</p>
|
|
@@ -44,13 +47,15 @@ export const StickyHeadings: Story = {
|
|
|
44
47
|
</div>
|
|
45
48
|
))}
|
|
46
49
|
</ScrollArea>
|
|
50
|
+
</div>
|
|
47
51
|
),
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
/** Nothing to scroll: the region draws exactly as a plain box would, and no bar. */
|
|
51
55
|
export const Fits: Story = {
|
|
52
56
|
render: () => (
|
|
53
|
-
<
|
|
57
|
+
<div className="h-[240px] w-[280px] rounded-lg border border-border-default bg-bg-surface">
|
|
58
|
+
<ScrollArea className="h-full" viewportClassName="p-2">
|
|
54
59
|
<ul className="flex flex-col gap-px">
|
|
55
60
|
{rows.slice(0, 4).map((row) => (
|
|
56
61
|
<li key={row} className="rounded-md px-2 py-1.5 text-body-2 text-text-primary">
|
|
@@ -59,13 +64,15 @@ export const Fits: Story = {
|
|
|
59
64
|
))}
|
|
60
65
|
</ul>
|
|
61
66
|
</ScrollArea>
|
|
67
|
+
</div>
|
|
62
68
|
),
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
/** A row wider than its box — a table, a board. */
|
|
66
72
|
export const Horizontal: Story = {
|
|
67
73
|
render: () => (
|
|
68
|
-
<
|
|
74
|
+
<div className="w-[280px] rounded-lg border border-border-default bg-bg-surface">
|
|
75
|
+
<ScrollArea orientation="horizontal" viewportClassName="p-2">
|
|
69
76
|
<div className="flex w-max gap-2">
|
|
70
77
|
{rows.slice(0, 12).map((row) => (
|
|
71
78
|
<div key={row} className="w-[120px] shrink-0 rounded-md bg-bg-inset px-2 py-1.5 text-body-2 text-text-primary">
|
|
@@ -74,13 +81,15 @@ export const Horizontal: Story = {
|
|
|
74
81
|
))}
|
|
75
82
|
</div>
|
|
76
83
|
</ScrollArea>
|
|
84
|
+
</div>
|
|
77
85
|
),
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
/** Both ways, with the corner where the two bars would meet. */
|
|
81
89
|
export const Both: Story = {
|
|
82
90
|
render: () => (
|
|
83
|
-
<
|
|
91
|
+
<div className="h-[240px] w-[280px] rounded-lg border border-border-default bg-bg-surface">
|
|
92
|
+
<ScrollArea orientation="both" className="h-full" viewportClassName="p-2">
|
|
84
93
|
<div className="flex w-max flex-col gap-px">
|
|
85
94
|
{rows.map((row) => (
|
|
86
95
|
<div key={row} className="w-[480px] rounded-md px-2 py-1.5 text-body-2 text-text-primary">
|
|
@@ -89,20 +98,23 @@ export const Both: Story = {
|
|
|
89
98
|
))}
|
|
90
99
|
</div>
|
|
91
100
|
</ScrollArea>
|
|
101
|
+
</div>
|
|
92
102
|
),
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
/** A sideways region inside a scrolling page — a table in a content column. A wheel down over it moves the page; a swipe sideways moves the region. */
|
|
96
106
|
export const SidewaysInsideAPage: Story = {
|
|
97
107
|
render: () => (
|
|
98
|
-
<
|
|
108
|
+
<div className="h-[240px] w-[280px] rounded-lg border border-border-default bg-bg-surface">
|
|
109
|
+
<ScrollArea className="h-full" viewportClassName="p-2">
|
|
99
110
|
<div className="flex flex-col gap-px">
|
|
100
111
|
{rows.slice(0, 3).map((row) => (
|
|
101
112
|
<p key={row} className="rounded-md px-2 py-1.5 text-body-2 text-text-primary">
|
|
102
113
|
{row}
|
|
103
114
|
</p>
|
|
104
115
|
))}
|
|
105
|
-
<
|
|
116
|
+
<div className="my-1 rounded-md border border-border-default">
|
|
117
|
+
<ScrollArea orientation="horizontal" viewportClassName="p-2">
|
|
106
118
|
<div className="flex w-max gap-2">
|
|
107
119
|
{rows.slice(0, 12).map((row) => (
|
|
108
120
|
<div key={row} className="w-[120px] shrink-0 rounded-md bg-bg-inset px-2 py-1.5 text-body-2 text-text-primary">
|
|
@@ -111,6 +123,7 @@ export const SidewaysInsideAPage: Story = {
|
|
|
111
123
|
))}
|
|
112
124
|
</div>
|
|
113
125
|
</ScrollArea>
|
|
126
|
+
</div>
|
|
114
127
|
{rows.slice(3).map((row) => (
|
|
115
128
|
<p key={row} className="rounded-md px-2 py-1.5 text-body-2 text-text-primary">
|
|
116
129
|
{row}
|
|
@@ -118,5 +131,6 @@ export const SidewaysInsideAPage: Story = {
|
|
|
118
131
|
))}
|
|
119
132
|
</div>
|
|
120
133
|
</ScrollArea>
|
|
134
|
+
</div>
|
|
121
135
|
),
|
|
122
136
|
}
|
package/src/SectionLabel.mdx
CHANGED
|
@@ -34,8 +34,9 @@ import { SectionLabel } from '@estiva-app/ui'
|
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
It is a bare span: the caller owns the row it sits in and the spacing
|
|
37
|
-
around it. Read it `secondary`
|
|
38
|
-
|
|
37
|
+
around it. Read it `tone="secondary"` where it labels rather than leads — menu
|
|
38
|
+
headings and the command palette's groups do (Katerina, 2026-09-01). A colour
|
|
39
|
+
passed in `className` is refused by the lint (UIG-9).
|
|
39
40
|
|
|
40
41
|
## Props
|
|
41
42
|
|
package/src/SectionLabel.tsx
CHANGED
|
@@ -9,16 +9,21 @@ import { cn } from './cn'
|
|
|
9
9
|
* micro-label under Signal. Metrics as arbitrary values for the tw-merge
|
|
10
10
|
* reason the README records.
|
|
11
11
|
*
|
|
12
|
+
* `tone="secondary"` is a heading inside a menu or a list of results: it labels
|
|
13
|
+
* the rows, it is not one of them (Katerina, 2026-09-01). `MenuSection` and
|
|
14
|
+
* `CommandPalette` passed it as a class before UIG-9 (17 September).
|
|
15
|
+
*
|
|
12
16
|
* Deliberately NOT the same thing as Property's stacked field label (the
|
|
13
17
|
* 9px `menu`-token one): they were merged for a day and unmerged by
|
|
14
18
|
* Katerina's ruling (2026-09-01) — a section title and a field label are
|
|
15
19
|
* different voices, at different sizes, on purpose.
|
|
16
20
|
*/
|
|
17
|
-
export function SectionLabel({ children, className }: { children: ReactNode; className?: string }) {
|
|
21
|
+
export function SectionLabel({ children, tone = 'primary', className }: { children: ReactNode; tone?: 'primary' | 'secondary'; className?: string }) {
|
|
18
22
|
return (
|
|
19
23
|
<span
|
|
20
24
|
className={cn(
|
|
21
|
-
'text-h5 leading-3
|
|
25
|
+
'text-h5 leading-3 signal:font-mono signal:text-small signal:uppercase signal:tracking-widest',
|
|
26
|
+
tone === 'secondary' ? 'text-text-secondary' : 'text-text-primary',
|
|
22
27
|
className,
|
|
23
28
|
)}
|
|
24
29
|
>
|
package/src/Tooltip.tsx
CHANGED
|
@@ -111,11 +111,8 @@ function TooltipSurface({ label, shortcut, placement }: { label: string; shortcu
|
|
|
111
111
|
className="pointer-events-none z-[9999]"
|
|
112
112
|
>
|
|
113
113
|
<BaseTooltip.Popup
|
|
114
|
-
render={
|
|
115
|
-
|
|
116
|
-
label={label}
|
|
117
|
-
shortcut={shortcut}
|
|
118
|
-
/*
|
|
114
|
+
render={<Tooltip label={label} shortcut={shortcut} />}
|
|
115
|
+
/*
|
|
119
116
|
* The motion (Katerina, D26, 2026-09-07). It had none — it was
|
|
120
117
|
* mounted and instantly there — and Base UI is what makes one
|
|
121
118
|
* possible: `data-starting-style` on the opening frame,
|
|
@@ -132,16 +129,17 @@ function TooltipSurface({ label, shortcut, placement }: { label: string; shortcu
|
|
|
132
129
|
* tooltip was dismissed. Sweeping a toolbar therefore moves the
|
|
133
130
|
* pill without re-animating it, which is the whole point of the
|
|
134
131
|
* shared delay (D23).
|
|
132
|
+
*
|
|
133
|
+
* On Base UI's popup, which hands its class to the pill it renders,
|
|
134
|
+
* rather than pushed into the pill from outside (UIG-9).
|
|
135
135
|
*/
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
/>
|
|
144
|
-
}
|
|
136
|
+
className={cn(
|
|
137
|
+
'transition-[opacity,transform] duration-[120ms] ease-out motion-reduce:transition-none',
|
|
138
|
+
'data-[ending-style]:duration-[80ms] data-[instant]:duration-0',
|
|
139
|
+
'data-[starting-style]:opacity-0 data-[ending-style]:opacity-0',
|
|
140
|
+
'data-[side=top]:data-[starting-style]:translate-y-[4px] data-[side=top]:data-[ending-style]:translate-y-[4px]',
|
|
141
|
+
'data-[side=bottom]:data-[starting-style]:-translate-y-[4px] data-[side=bottom]:data-[ending-style]:-translate-y-[4px]',
|
|
142
|
+
)}
|
|
145
143
|
/>
|
|
146
144
|
</BaseTooltip.Positioner>
|
|
147
145
|
</BaseTooltip.Portal>
|
package/src/eslint/index.test.ts
CHANGED
|
@@ -36,6 +36,7 @@ describe('the plugin object', () => {
|
|
|
36
36
|
expect(Object.keys(estiva.rules)).toEqual([
|
|
37
37
|
'no-raw-element',
|
|
38
38
|
'no-rebuilt-behaviour',
|
|
39
|
+
'no-restyled-part',
|
|
39
40
|
'raw-element-outside-a-wrapper',
|
|
40
41
|
'no-hand-rolled-behaviour',
|
|
41
42
|
'component-has-a-page',
|
|
@@ -55,21 +56,27 @@ describe('the plugin object', () => {
|
|
|
55
56
|
it('gives an app only the app rules, as errors, under estiva/', () => {
|
|
56
57
|
for (const config of [estiva.configs.recommended, estiva.configs.strict]) {
|
|
57
58
|
expect(config.plugins?.[PLUGIN_KEY]).toBe(estiva)
|
|
58
|
-
expect(config.rules).toEqual({ 'estiva/no-raw-element': 'error', 'estiva/no-rebuilt-behaviour': 'error' })
|
|
59
|
+
expect(config.rules).toEqual({ 'estiva/no-raw-element': 'error', 'estiva/no-rebuilt-behaviour': 'error', 'estiva/no-restyled-part': 'error' })
|
|
59
60
|
}
|
|
60
|
-
expect(APP_RULE_IDS).toEqual(['estiva/no-raw-element', 'estiva/no-rebuilt-behaviour'])
|
|
61
|
+
expect(APP_RULE_IDS).toEqual(['estiva/no-raw-element', 'estiva/no-rebuilt-behaviour', 'estiva/no-restyled-part'])
|
|
61
62
|
})
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
/**
|
|
65
|
+
* `no-restyled-part` is the one rule in both sets (UIG-9): the package restyles
|
|
66
|
+
* none of its own parts either (Katerina, 17 September). Every other inward
|
|
67
|
+
* rule still reaches no app.
|
|
68
|
+
*/
|
|
69
|
+
it('gives this package its own set, as errors, and only the shared rule reaches an app config', () => {
|
|
64
70
|
expect(estiva.configs.package.plugins?.[PLUGIN_KEY]).toBe(estiva)
|
|
65
71
|
expect(estiva.configs.package.rules).toEqual({
|
|
66
72
|
'estiva/raw-element-outside-a-wrapper': 'error',
|
|
67
73
|
'estiva/no-hand-rolled-behaviour': 'error',
|
|
68
74
|
'estiva/component-has-a-page': 'error',
|
|
69
75
|
'estiva/component-has-a-story': 'error',
|
|
76
|
+
'estiva/no-restyled-part': 'error',
|
|
70
77
|
})
|
|
71
78
|
expect(PACKAGE_RULE_IDS).toEqual(Object.keys(estiva.configs.package.rules ?? {}))
|
|
72
|
-
for (const id of PACKAGE_RULE_IDS) {
|
|
79
|
+
for (const id of PACKAGE_RULE_IDS.filter((id) => id !== 'estiva/no-restyled-part')) {
|
|
73
80
|
expect(estiva.configs.recommended.rules?.[id]).toBeUndefined()
|
|
74
81
|
expect(estiva.configs.strict.rules?.[id]).toBeUndefined()
|
|
75
82
|
}
|
|
@@ -110,6 +117,22 @@ describe('an app lint with configs.recommended', () => {
|
|
|
110
117
|
expect(result.messages.filter((m) => m.messageId === 'role' || m.messageId === 'roleNoPart')).toEqual([])
|
|
111
118
|
})
|
|
112
119
|
|
|
120
|
+
/**
|
|
121
|
+
* The probe is linted inside this package's folder, so its part is a sibling
|
|
122
|
+
* (`./Link`), the way the package's own files import one; in an app the same
|
|
123
|
+
* rule reads an import from `@estiva-app/ui` (no-restyled-part.test.ts).
|
|
124
|
+
*/
|
|
125
|
+
it('reports a part restyled, naming its look props', async () => {
|
|
126
|
+
const [result] = await lint(`import { Link } from './Link'\n${component(' <Link href="/x" className="mt-2 text-h2">x</Link>')}`)
|
|
127
|
+
expect(result.messages.map((m) => [m.ruleId, m.severity, m.message])).toEqual([
|
|
128
|
+
[
|
|
129
|
+
'estiva/no-restyled-part',
|
|
130
|
+
2,
|
|
131
|
+
'`text-h2` on `Link` changes how it looks. A part of @estiva-app/ui is placed from outside, never restyled: only space, size, flex and grid, and position pass in. Use its `variant` or `truncate`. A look for what is around it goes on your own element around it.',
|
|
132
|
+
],
|
|
133
|
+
])
|
|
134
|
+
})
|
|
135
|
+
|
|
113
136
|
it('passes the same element under an escape', async () => {
|
|
114
137
|
const [result] = await lint(component(' // @estiva-escape: a preview drawn from its own palette\n <button type="button">x</button>'))
|
|
115
138
|
expect(result.messages).toEqual([])
|
|
@@ -120,8 +143,8 @@ describe('countGates', () => {
|
|
|
120
143
|
it('counts an error, and an escape only when the lint reports escapes', async () => {
|
|
121
144
|
const code = component(' <div>\n <button>x</button>\n {/* @estiva-escape: a preview drawn from its own palette */}\n <button>y</button>\n </div>')
|
|
122
145
|
const none = { errors: 0, warnings: 0, escapes: 0 }
|
|
123
|
-
expect(countGates(await lint(code)).rules).toEqual({ 'estiva/no-raw-element': { errors: 1, warnings: 0, escapes: 0 }, 'estiva/no-rebuilt-behaviour': none })
|
|
124
|
-
expect(countGates(await lint(code, [countMode])).rules).toEqual({ 'estiva/no-raw-element': { errors: 1, warnings: 0, escapes: 1 }, 'estiva/no-rebuilt-behaviour': none })
|
|
146
|
+
expect(countGates(await lint(code)).rules).toEqual({ 'estiva/no-raw-element': { errors: 1, warnings: 0, escapes: 0 }, 'estiva/no-rebuilt-behaviour': none, 'estiva/no-restyled-part': none })
|
|
147
|
+
expect(countGates(await lint(code, [countMode])).rules).toEqual({ 'estiva/no-raw-element': { errors: 1, warnings: 0, escapes: 1 }, 'estiva/no-rebuilt-behaviour': none, 'estiva/no-restyled-part': none })
|
|
125
148
|
})
|
|
126
149
|
|
|
127
150
|
it('lists a report an eslint-disable silenced, and counts it as neither an error nor an escape', async () => {
|
|
@@ -140,7 +163,9 @@ describe('countGates', () => {
|
|
|
140
163
|
|
|
141
164
|
it('lists every rule of the plugin, even with nothing found', () => {
|
|
142
165
|
expect(countGates([])).toEqual({
|
|
143
|
-
rules: { 'estiva/no-raw-element': { errors: 0, warnings: 0, escapes: 0 }, 'estiva/no-rebuilt-behaviour': { errors: 0, warnings: 0, escapes: 0 }
|
|
166
|
+
rules: { 'estiva/no-raw-element': { errors: 0, warnings: 0, escapes: 0 }, 'estiva/no-rebuilt-behaviour': { errors: 0, warnings: 0, escapes: 0 },
|
|
167
|
+
'estiva/no-restyled-part': { errors: 0, warnings: 0, escapes: 0 },
|
|
168
|
+
},
|
|
144
169
|
disabled: [],
|
|
145
170
|
})
|
|
146
171
|
})
|
package/src/eslint/index.ts
CHANGED
|
@@ -22,10 +22,12 @@ import { componentHasAPage, componentHasAStory } from './has-a-page-and-a-story'
|
|
|
22
22
|
import { noHandRolledBehaviour } from './no-hand-rolled-behaviour'
|
|
23
23
|
import { noRawElement } from './no-raw-element'
|
|
24
24
|
import { noRebuiltBehaviour } from './no-rebuilt-behaviour'
|
|
25
|
+
import { noRestyledPart } from './no-restyled-part'
|
|
25
26
|
import { rawElementOutsideAWrapper } from './raw-element-outside-a-wrapper'
|
|
26
27
|
|
|
27
28
|
export { ESCAPE_MARKER, MIN_REASON, SETTINGS_KEY, isEscaped, type EstivaSettings } from './escape'
|
|
28
29
|
export { OWNED_BEHAVIOURS, type OwnedBehaviour } from './no-rebuilt-behaviour'
|
|
30
|
+
export { PART_LOOK_PROPS, PLACEMENT } from './no-restyled-part'
|
|
29
31
|
|
|
30
32
|
const { version } = createRequire(import.meta.url)('../../package.json') as { version: string }
|
|
31
33
|
|
|
@@ -35,11 +37,13 @@ export const PLUGIN_KEY = 'estiva'
|
|
|
35
37
|
/**
|
|
36
38
|
* The rules an **app** runs: they say an app must not build what the package
|
|
37
39
|
* already has — a raw control (UIG-7), or a behaviour one of its parts owns
|
|
38
|
-
* (UIG-8). `recommended` and `strict`
|
|
40
|
+
* (UIG-8) — nor restyle a part it uses (UIG-9). `recommended` and `strict`
|
|
41
|
+
* carry these and only these.
|
|
39
42
|
*/
|
|
40
43
|
const appRules = {
|
|
41
44
|
'no-raw-element': noRawElement,
|
|
42
45
|
'no-rebuilt-behaviour': noRebuiltBehaviour,
|
|
46
|
+
'no-restyled-part': noRestyledPart,
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
/**
|
|
@@ -53,12 +57,16 @@ const appRules = {
|
|
|
53
57
|
* "buried inside a component" means nothing there (`no-raw-element` is the
|
|
54
58
|
* app's version, UIG-7), and an app has no `.mdx` pages at all. `index.test.ts` holds the
|
|
55
59
|
* apps' list to exactly the app rules.
|
|
60
|
+
*
|
|
61
|
+
* One rule is in both sets: `no-restyled-part` (UIG-9). The package restyles
|
|
62
|
+
* none of its own parts either (Katerina, 17 September).
|
|
56
63
|
*/
|
|
57
64
|
const packageRules = {
|
|
58
65
|
'raw-element-outside-a-wrapper': rawElementOutsideAWrapper,
|
|
59
66
|
'no-hand-rolled-behaviour': noHandRolledBehaviour,
|
|
60
67
|
'component-has-a-page': componentHasAPage,
|
|
61
68
|
'component-has-a-story': componentHasAStory,
|
|
69
|
+
'no-restyled-part': noRestyledPart,
|
|
62
70
|
}
|
|
63
71
|
|
|
64
72
|
const rules = { ...appRules, ...packageRules }
|
|
@@ -88,6 +96,7 @@ plugin.configs.recommended = {
|
|
|
88
96
|
rules: {
|
|
89
97
|
[`${PLUGIN_KEY}/no-raw-element`]: 'error',
|
|
90
98
|
[`${PLUGIN_KEY}/no-rebuilt-behaviour`]: 'error',
|
|
99
|
+
[`${PLUGIN_KEY}/no-restyled-part`]: 'error',
|
|
91
100
|
},
|
|
92
101
|
}
|
|
93
102
|
plugin.configs.strict = {
|