@estiva-app/ui 0.35.1 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/eslint/escape.d.ts +23 -6
- package/dist/eslint/escape.d.ts.map +1 -1
- package/dist/eslint/index.js +70 -9
- package/dist/eslint/index.js.map +2 -2
- package/dist/eslint/no-raw-element.d.ts +7 -0
- package/dist/eslint/no-raw-element.d.ts.map +1 -1
- package/dist/eslint/no-restyled-part.d.ts +8 -0
- package/dist/eslint/no-restyled-part.d.ts.map +1 -1
- package/dist/gates/app-checks.d.ts.map +1 -1
- package/dist/gates/{chunk-DRIHHXQK.js → chunk-CC6L4B5U.js} +23 -17
- package/dist/gates/chunk-CC6L4B5U.js.map +7 -0
- package/dist/gates/chunk-SYVKGOWM.js +1131 -0
- package/dist/gates/chunk-SYVKGOWM.js.map +7 -0
- package/dist/gates/ci.d.ts +18 -0
- package/dist/gates/ci.d.ts.map +1 -0
- package/dist/gates/cli.js +20 -3
- package/dist/gates/cli.js.map +2 -2
- package/dist/gates/create-app.d.ts.map +1 -1
- package/dist/gates/create-app.js +1 -1
- package/dist/gates/escapes.d.ts +23 -0
- package/dist/gates/escapes.d.ts.map +1 -0
- package/dist/gates/gate-config.d.ts.map +1 -1
- package/dist/gates/hook.d.ts +2 -0
- package/dist/gates/hook.d.ts.map +1 -1
- package/dist/gates/index.d.ts +5 -1
- package/dist/gates/index.d.ts.map +1 -1
- package/dist/gates/index.js +41 -185
- package/dist/gates/index.js.map +4 -4
- package/dist/gates/name-escapes.d.ts +14 -0
- package/dist/gates/name-escapes.d.ts.map +1 -0
- package/dist/gates/status.d.ts +3 -1
- package/dist/gates/status.d.ts.map +1 -1
- package/dist/gates/token-lint.d.ts.map +1 -1
- package/dist/gates/token-style.d.ts +21 -0
- package/dist/gates/token-style.d.ts.map +1 -0
- package/dist/index.js +13 -13
- package/dist/index.js.map +1 -1
- package/dist/registry/{app-3ALZE6XO.js → app-YMABQII6.js} +3 -3
- package/dist/registry/app.d.ts.map +1 -1
- package/dist/registry/behaviours.d.ts +71 -0
- package/dist/registry/behaviours.d.ts.map +1 -0
- package/dist/registry/{build-NDXMVFQ5.js → build-3EANARLQ.js} +2 -2
- package/dist/registry/build.d.ts.map +1 -1
- package/dist/registry/{chunk-3WJ7ZZ7Z.js → chunk-EVJW26UR.js} +51 -4
- package/dist/registry/chunk-EVJW26UR.js.map +7 -0
- package/dist/registry/{chunk-XVII7P6G.js → chunk-SIB44CES.js} +276 -90
- package/dist/registry/chunk-SIB44CES.js.map +7 -0
- package/dist/registry/{chunk-KMZIMB3G.js → chunk-XK747STJ.js} +46 -3
- package/dist/registry/chunk-XK747STJ.js.map +7 -0
- package/dist/registry/cli.js +93 -34
- package/dist/registry/cli.js.map +3 -3
- package/dist/registry/contract.d.ts +27 -1
- package/dist/registry/contract.d.ts.map +1 -1
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +11 -5
- package/dist/registry/siblings.d.ts +9 -0
- package/dist/registry/siblings.d.ts.map +1 -1
- package/dist/registry/storymap.d.ts +19 -5
- package/dist/registry/storymap.d.ts.map +1 -1
- package/package.json +4 -1
- package/registry.json +293 -17
- package/skill/estiva-ui.md +2 -1
- package/src/AppShell.mdx +2 -1
- package/src/AttachmentCard.tsx +7 -7
- package/src/Button.tsx +1 -1
- package/src/Card.tsx +2 -2
- package/src/ChipInput.mdx +2 -0
- package/src/ChipInput.tsx +1 -1
- package/src/CommandPalette.tsx +3 -3
- package/src/DialogShell.tsx +1 -1
- package/src/EmptyState.mdx +4 -3
- package/src/Lightbox.tsx +1 -1
- package/src/NavItem.tsx +1 -1
- package/src/PersonTrigger.tsx +1 -1
- package/src/Reaction.tsx +1 -1
- package/src/ScrollArea.mdx +4 -0
- package/src/Select.tsx +1 -1
- package/src/TopBar.mdx +1 -1
- package/src/eslint/escape.ts +43 -14
- package/src/eslint/has-a-page-and-a-story.test.ts +1 -1
- package/src/eslint/index.test.ts +2 -2
- package/src/eslint/no-copied-look.test.ts +1 -1
- package/src/eslint/no-hand-rolled-behaviour.test.ts +3 -3
- package/src/eslint/no-handmade-empty-state.test.ts +2 -2
- package/src/eslint/no-handmade-header.test.ts +2 -2
- package/src/eslint/no-native-title.test.ts +2 -2
- package/src/eslint/no-raw-element.test.ts +47 -15
- package/src/eslint/no-raw-element.ts +53 -3
- package/src/eslint/no-rebuilt-behaviour.test.ts +9 -9
- package/src/eslint/no-restyled-part.test.ts +21 -2
- package/src/eslint/no-restyled-part.ts +30 -0
- package/src/eslint/raw-element-outside-a-wrapper.test.ts +3 -3
- package/src/gates/app-checks.ts +4 -3
- package/src/gates/ci.ts +152 -0
- package/src/gates/cli.ts +19 -3
- package/src/gates/count.ts +1 -1
- package/src/gates/create-app.test.ts +2 -2
- package/src/gates/create-app.ts +22 -16
- package/src/gates/escapes.ts +111 -0
- package/src/gates/gate-config.ts +6 -2
- package/src/gates/gates.test.ts +93 -1
- package/src/gates/hook.ts +44 -6
- package/src/gates/index.ts +5 -1
- package/src/gates/name-escapes.ts +77 -0
- package/src/gates/status.ts +45 -2
- package/src/gates/token-lint.ts +11 -4
- package/src/gates/token-style.ts +79 -0
- package/src/how.test.ts +282 -0
- package/src/pages.test.ts +17 -0
- package/src/registry/app.test.ts +34 -0
- package/src/registry/app.ts +52 -2
- package/src/registry/behaviours.ts +223 -0
- package/src/registry/build.ts +36 -0
- package/src/registry/cli.ts +37 -18
- package/src/registry/contract.test.ts +63 -3
- package/src/registry/contract.ts +75 -2
- package/src/registry/index.ts +1 -1
- package/src/registry/registry.test.ts +14 -0
- package/src/registry/siblings.test.ts +24 -1
- package/src/registry/siblings.ts +23 -0
- package/src/registry/storymap.test.ts +52 -31
- package/src/registry/storymap.ts +76 -37
- package/stories/Choosing.mdx +8 -2
- package/dist/gates/chunk-DRIHHXQK.js.map +0 -7
- package/dist/gates/chunk-GI6M4P7H.js +0 -606
- package/dist/gates/chunk-GI6M4P7H.js.map +0 -7
- package/dist/registry/chunk-3WJ7ZZ7Z.js.map +0 -7
- package/dist/registry/chunk-KMZIMB3G.js.map +0 -7
- package/dist/registry/chunk-XVII7P6G.js.map +0 -7
- /package/dist/registry/{app-3ALZE6XO.js.map → app-YMABQII6.js.map} +0 -0
- /package/dist/registry/{build-NDXMVFQ5.js.map → build-3EANARLQ.js.map} +0 -0
package/src/AttachmentCard.tsx
CHANGED
|
@@ -89,7 +89,7 @@ const formatBytes = (bytes: number) => {
|
|
|
89
89
|
const typeLabelOf = (name: string) => extensionOf(name).toUpperCase() || 'FILE'
|
|
90
90
|
|
|
91
91
|
const TILE_CLASSES = 'size-9 rounded-md bg-bg-active flex items-center justify-center shrink-0 text-text-secondary'
|
|
92
|
-
// @estiva-escape: A6 (Katerina, 24 September: record only): a 12px medium name, the text style InputChip's label also uses, not a copy of a part
|
|
92
|
+
// @estiva-escape(no-copied-look): A6 (Katerina, 24 September: record only): a 12px medium name, the text style InputChip's label also uses, not a copy of a part
|
|
93
93
|
const NAME_CLASSES = 'text-caption font-medium text-text-primary truncate'
|
|
94
94
|
const NOTE_CLASSES = 'text-small tracking-wide leading-tight truncate'
|
|
95
95
|
|
|
@@ -359,7 +359,7 @@ export function AttachmentCard({
|
|
|
359
359
|
const failed = state === 'failed'
|
|
360
360
|
const warning = state === 'warning'
|
|
361
361
|
return (
|
|
362
|
-
// @estiva-escape: a failed or warning file's strong hairline is AttachmentCard's own state; Card's attention hairlines are the soft ones (UIG-9, Katerina 17 September)
|
|
362
|
+
// @estiva-escape(no-restyled-part): a failed or warning file's strong hairline is AttachmentCard's own state; Card's attention hairlines are the soft ones (UIG-9, Katerina 17 September)
|
|
363
363
|
<Card
|
|
364
364
|
fill="elevated"
|
|
365
365
|
className={cn('group relative flex items-center gap-2 w-[200px] p-1.5 pr-3', failed ? 'border-error-default' : warning && 'border-warning-default', className)}
|
|
@@ -390,7 +390,7 @@ export function AttachmentCard({
|
|
|
390
390
|
{/* On Base UI's Button, as InputChip's ✕ is (Katerina, 2026-09-14): IconButton is a 24px square
|
|
391
391
|
that fills on hover, and this is Peek's 20px round badge on the card's corner. */}
|
|
392
392
|
{onRemove && (
|
|
393
|
-
// @estiva-escape: N4 (Katerina, 24 September: record only): the small round button Lightbox's close also draws; a part if a third appears
|
|
393
|
+
// @estiva-escape(no-copied-look): N4 (Katerina, 24 September: record only): the small round button Lightbox's close also draws; a part if a third appears
|
|
394
394
|
<BaseButton
|
|
395
395
|
type="button"
|
|
396
396
|
aria-label={`Remove ${name}`}
|
|
@@ -411,7 +411,7 @@ export function AttachmentCard({
|
|
|
411
411
|
|
|
412
412
|
// Fades in with the card's hover or keyboard focus, and keeps its slot when hidden, so revealing it never shifts the row.
|
|
413
413
|
const downloadControl = saving ? (
|
|
414
|
-
// @estiva-escape: Download shows when its card is pointed at or focused, AttachmentCard's own action; IconButton has no reveal on its card's hover (UIG-9, Katerina 17 September)
|
|
414
|
+
// @estiva-escape(no-restyled-part): Download shows when its card is pointed at or focused, AttachmentCard's own action; IconButton has no reveal on its card's hover (UIG-9, Katerina 17 September)
|
|
415
415
|
<IconButton
|
|
416
416
|
variant="muted"
|
|
417
417
|
tooltip="Download"
|
|
@@ -430,7 +430,7 @@ export function AttachmentCard({
|
|
|
430
430
|
return (
|
|
431
431
|
// `role="status"`: a plain box may not carry a name (axe, aria-prohibited-attr), and a
|
|
432
432
|
// status says what it is doing — loading — to a reader that cannot see the pulse.
|
|
433
|
-
// @estiva-escape: the loading pulse is AttachmentCard's own state; Card has no loading state (UIG-9, Katerina 17 September)
|
|
433
|
+
// @estiva-escape(no-restyled-part): the loading pulse is AttachmentCard's own state; Card has no loading state (UIG-9, Katerina 17 September)
|
|
434
434
|
<Card role="status" fill="inset" aria-busy="true" aria-label={`Loading ${name}`} className={cn('w-[180px] h-28 animate-pulse', className)} {...props}>
|
|
435
435
|
{null}
|
|
436
436
|
</Card>
|
|
@@ -439,7 +439,7 @@ export function AttachmentCard({
|
|
|
439
439
|
|
|
440
440
|
if (drawnState === 'unreadable') {
|
|
441
441
|
return (
|
|
442
|
-
// @estiva-escape: a file that could not be read is faded as well as dashed, AttachmentCard's own state; Card has no faded state (UIG-9, Katerina 17 September)
|
|
442
|
+
// @estiva-escape(no-restyled-part): a file that could not be read is faded as well as dashed, AttachmentCard's own state; Card has no faded state (UIG-9, Katerina 17 September)
|
|
443
443
|
<Card fill="inset" unreadable className={cn('flex items-center gap-2 w-[240px] p-1.5 opacity-70', className)} {...props}>
|
|
444
444
|
<div className={TILE_CLASSES}>
|
|
445
445
|
<TypeIcon name={name} />
|
|
@@ -494,7 +494,7 @@ export function AttachmentCard({
|
|
|
494
494
|
</>
|
|
495
495
|
)
|
|
496
496
|
return (
|
|
497
|
-
// @estiva-escape: a file that cannot be opened is faded, AttachmentCard's own state; Card has no faded state (UIG-9, Katerina 17 September)
|
|
497
|
+
// @estiva-escape(no-restyled-part): a file that cannot be opened is faded, AttachmentCard's own state; Card has no faded state (UIG-9, Katerina 17 September)
|
|
498
498
|
<Card fill="inset" hover={address ? 'hairline' : 'none'} className={cn('group flex items-center gap-2 w-[240px] p-1.5 pr-1', !address && 'opacity-70', className)} {...props}>
|
|
499
499
|
{address ? (
|
|
500
500
|
<Link href={address} external variant="plain" className="flex items-center gap-2 min-w-0 flex-1" onClick={(event) => event.stopPropagation()}>
|
package/src/Button.tsx
CHANGED
|
@@ -92,7 +92,7 @@ export function Button({
|
|
|
92
92
|
size === 'default' && (hasLeadingIcon ? 'pl-2 pr-3' : 'px-2'),
|
|
93
93
|
size === 'small' && (hasLeadingIcon ? 'pl-1.5 pr-2' : 'px-1.5'),
|
|
94
94
|
!state.disabled && (variant === 'primary' || variant === 'resolve') && 'bg-accent-primary hover:bg-accent-hover text-text-inverse cursor-pointer signal:font-semibold',
|
|
95
|
-
// @estiva-escape: W2 (Katerina, 24 September: leave alone): an outlined button shares four words with Reaction's pill by coincidence
|
|
95
|
+
// @estiva-escape(no-copied-look): W2 (Katerina, 24 September: leave alone): an outlined button shares four words with Reaction's pill by coincidence
|
|
96
96
|
!state.disabled && variant === 'outlined' && 'border border-border-default hover:bg-bg-hover text-text-primary cursor-pointer',
|
|
97
97
|
!state.disabled && variant === 'muted' && 'hover:bg-bg-hover text-text-primary cursor-pointer',
|
|
98
98
|
!state.disabled && variant === 'destructive' && 'hover:bg-error-muted text-error-default cursor-pointer',
|
package/src/Card.tsx
CHANGED
|
@@ -127,14 +127,14 @@ export function Card({
|
|
|
127
127
|
if (href !== undefined) {
|
|
128
128
|
const { ref, ...anchorProps } = props as ComponentPropsWithRef<'a'>
|
|
129
129
|
return (
|
|
130
|
-
// @estiva-escape: a card that leads somewhere is drawn on Link's anchor (UIG-27 ruling 1), so this frame is Card's own look, not Link restyled
|
|
130
|
+
// @estiva-escape(no-copied-look, no-restyled-part): a card that leads somewhere is drawn on Link's anchor (UIG-27 ruling 1), so this frame is Card's own look, not Link restyled
|
|
131
131
|
<Link href={href} variant="plain" ref={ref} className={classes} {...anchorProps}>
|
|
132
132
|
{children}
|
|
133
133
|
</Link>
|
|
134
134
|
)
|
|
135
135
|
}
|
|
136
136
|
return (
|
|
137
|
-
// @estiva-escape: N6 (Katerina, 24 September: leave alone): a card that cuts its content to its corners shares four words with the palette's window by coincidence
|
|
137
|
+
// @estiva-escape(no-copied-look): N6 (Katerina, 24 September: leave alone): a card that cuts its content to its corners shares four words with the palette's window by coincidence
|
|
138
138
|
<div className={classes} {...props}>
|
|
139
139
|
{children}
|
|
140
140
|
</div>
|
package/src/ChipInput.mdx
CHANGED
package/src/ChipInput.tsx
CHANGED
|
@@ -62,7 +62,7 @@ export function InputChip({ label, leading, onRemove, removeLabel, truncate, cla
|
|
|
62
62
|
return (
|
|
63
63
|
<div className={cn(CHIP_BOX, chipPadding(!!leading, !!onRemove), className)}>
|
|
64
64
|
{leading && <span className="flex shrink-0 items-center">{leading}</span>}
|
|
65
|
-
{/* @estiva-escape: A6 (Katerina, 24 September: record only): a 12px medium label, the text style AttachmentCard's name also uses, not a copy of a part */}
|
|
65
|
+
{/* @estiva-escape(no-copied-look): A6 (Katerina, 24 September: record only): a 12px medium label, the text style AttachmentCard's name also uses, not a copy of a part */}
|
|
66
66
|
<span className={cn(CHIP_LABEL, truncate && 'min-w-0 truncate')}>{label}</span>
|
|
67
67
|
{/* Base UI's `Button`, as every button in the package is (D6). Base UI
|
|
68
68
|
has no chip of its own — its only chips are `Combobox.Chip` and
|
package/src/CommandPalette.tsx
CHANGED
|
@@ -123,7 +123,7 @@ export function CommandPalette({ open, onOpenChange, label, where, modKey = 'Ctr
|
|
|
123
123
|
<Dialog.Portal>
|
|
124
124
|
<Dialog.Backdrop className="fixed inset-0 z-40 bg-scrim" />
|
|
125
125
|
<Dialog.Viewport className="fixed inset-0 z-50 flex items-start justify-center pt-[16vh]">
|
|
126
|
-
{/* @estiva-escape: A3 (Katerina, 24 September: leave CommandPalette alone): the palette's window keeps its own border */}
|
|
126
|
+
{/* @estiva-escape(no-copied-look): A3 (Katerina, 24 September: leave CommandPalette alone): the palette's window keeps its own border */}
|
|
127
127
|
<Dialog.Popup
|
|
128
128
|
ref={popupRef}
|
|
129
129
|
aria-label={label}
|
|
@@ -374,7 +374,7 @@ export function CommandPaletteSearch({ query, onQueryChange, placeholder, groups
|
|
|
374
374
|
<div className="flex h-12 shrink-0 items-center gap-3 border-b border-border-subtle px-5">
|
|
375
375
|
<IconSearch size={16} stroke={1.5} className="shrink-0 text-text-secondary" />
|
|
376
376
|
{chip && <LevelChip chip={chip} onBack={back} />}
|
|
377
|
-
{/* @estiva-escape: A5 (Katerina, 24 September: leave CommandPalette alone): the palette's search field keeps its own classes */}
|
|
377
|
+
{/* @estiva-escape(no-copied-look): A5 (Katerina, 24 September: leave CommandPalette alone): the palette's search field keeps its own classes */}
|
|
378
378
|
<Autocomplete.Input
|
|
379
379
|
ref={inputRef}
|
|
380
380
|
data-command-palette-field=""
|
|
@@ -555,7 +555,7 @@ export function CommandPaletteForm({ chip, icon, submitLabel, onSubmit, submitWa
|
|
|
555
555
|
|
|
556
556
|
return (
|
|
557
557
|
<div ref={frameRef} tabIndex={-1} onKeyDown={onKeyDown} onFocus={measure} onBlur={measure} onInput={measure} className="flex min-h-0 flex-col outline-none">
|
|
558
|
-
{/* @estiva-escape: the palette's own input bar, the twin of its search row; the level chip stands where the field was */}
|
|
558
|
+
{/* @estiva-escape(no-handmade-header): the palette's own input bar, the twin of its search row; the level chip stands where the field was */}
|
|
559
559
|
<div className="flex h-12 shrink-0 items-center gap-3 border-b border-border-subtle px-5">
|
|
560
560
|
{icon != null && <span className="flex shrink-0 items-center text-text-secondary">{icon}</span>}
|
|
561
561
|
<LevelChip chip={chip} onBack={back} />
|
package/src/DialogShell.tsx
CHANGED
|
@@ -86,7 +86,7 @@ export function DialogShell({ title, onClose, headerContent, footer, children, b
|
|
|
86
86
|
|
|
87
87
|
{/* Dialog */}
|
|
88
88
|
<div className="fixed inset-0 z-50 flex items-center justify-center pointer-events-none">
|
|
89
|
-
{/* @estiva-escape: A3 (Katerina, 24 September: leave CommandPalette alone): a dialog's window and the palette's are left as they are */}
|
|
89
|
+
{/* @estiva-escape(no-copied-look): A3 (Katerina, 24 September: leave CommandPalette alone): a dialog's window and the palette's are left as they are */}
|
|
90
90
|
<Parts.Popup
|
|
91
91
|
ref={popupRef}
|
|
92
92
|
/*
|
package/src/EmptyState.mdx
CHANGED
|
@@ -36,9 +36,10 @@ need a height from the caller (`className="h-full"`).
|
|
|
36
36
|
|
|
37
37
|
## When not
|
|
38
38
|
|
|
39
|
-
- Data is still on its way → **Skeleton
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
- Data is still on its way → **Skeleton**: **SkeletonList**, or a shape of
|
|
40
|
+
your own from **SkeletonBar**; empty and loading are different truths.
|
|
41
|
+
When the shape of what arrives is unknown, an EmptyState that says so —
|
|
42
|
+
"Opening…" — is fine.
|
|
42
43
|
- Something went wrong → say what failed, in words. An EmptyState can carry
|
|
43
44
|
it, in its usual look — never red. An empty state that hides a failure
|
|
44
45
|
misleads.
|
package/src/Lightbox.tsx
CHANGED
|
@@ -74,7 +74,7 @@ export function Lightbox({ src, alt, onClose, className }: LightboxProps) {
|
|
|
74
74
|
onClick={(e) => e.stopPropagation()}
|
|
75
75
|
className={cn('max-w-full max-h-full object-contain rounded-lg', className)}
|
|
76
76
|
/>
|
|
77
|
-
{/* @estiva-escape: N4 (Katerina, 24 September: record only): the small round button AttachmentCard's remove also draws; a part if a third appears */}
|
|
77
|
+
{/* @estiva-escape(no-copied-look): N4 (Katerina, 24 September: record only): the small round button AttachmentCard's remove also draws; a part if a third appears */}
|
|
78
78
|
<Dialog.Close
|
|
79
79
|
aria-label="Close"
|
|
80
80
|
className="absolute top-4 right-4 size-8 rounded-full bg-bg-elevated border border-border-strong flex items-center justify-center text-text-secondary hover:text-text-primary"
|
package/src/NavItem.tsx
CHANGED
|
@@ -28,7 +28,7 @@ export interface NavItemProps extends Omit<ComponentPropsWithoutRef<'a'>, 'href'
|
|
|
28
28
|
|
|
29
29
|
export function NavItem({ label, href, count, countLabel, active = false, icon, className, ...props }: NavItemProps) {
|
|
30
30
|
return (
|
|
31
|
-
// @estiva-escape: N5 (Katerina, 24 September: record only): a row that fills on hover, as PersonTrigger's row also does
|
|
31
|
+
// @estiva-escape(no-copied-look): N5 (Katerina, 24 September: record only): a row that fills on hover, as PersonTrigger's row also does
|
|
32
32
|
<a
|
|
33
33
|
href={href}
|
|
34
34
|
aria-current={active ? 'page' : undefined}
|
package/src/PersonTrigger.tsx
CHANGED
|
@@ -60,7 +60,7 @@ export function PersonTrigger({ name, picture, fallback, size, open = false, com
|
|
|
60
60
|
)
|
|
61
61
|
}
|
|
62
62
|
return (
|
|
63
|
-
// @estiva-escape: N5 (Katerina, 24 September: record only): a row that fills on hover, as NavItem also does
|
|
63
|
+
// @estiva-escape(no-copied-look): N5 (Katerina, 24 September: record only): a row that fills on hover, as NavItem also does
|
|
64
64
|
<BaseButton
|
|
65
65
|
type="button"
|
|
66
66
|
aria-haspopup="menu"
|
package/src/Reaction.tsx
CHANGED
|
@@ -63,7 +63,7 @@ export interface ReactionProps extends Omit<ButtonHTMLAttributes<HTMLButtonEleme
|
|
|
63
63
|
|
|
64
64
|
export function Reaction({ emoji, count, pressed = false, className, ...props }: ReactionProps) {
|
|
65
65
|
return (
|
|
66
|
-
// @estiva-escape: W1 (Katerina, 24 September: leave alone): a reaction pill shares four words with a field's box by coincidence
|
|
66
|
+
// @estiva-escape(no-copied-look): W1 (Katerina, 24 September: leave alone): a reaction pill shares four words with a field's box by coincidence
|
|
67
67
|
<Toggle
|
|
68
68
|
// Controlled: a reaction is yours when the data says so, not when the
|
|
69
69
|
// pill was last pressed — a write can fail, and another client can
|
package/src/ScrollArea.mdx
CHANGED
|
@@ -27,6 +27,10 @@ the content is moving; nothing moves when it appears.
|
|
|
27
27
|
- A box that never overflows. A ScrollArea around it changes nothing, and
|
|
28
28
|
is one more element.
|
|
29
29
|
- The page itself. The browser owns that bar.
|
|
30
|
+
- A whole list column → **ListColumn**, which scrolls its rows itself.
|
|
31
|
+
|
|
32
|
+
No alternative for the first two: a box that never overflows needs nothing
|
|
33
|
+
around it, and the page's bar is the browser's.
|
|
30
34
|
|
|
31
35
|
## How
|
|
32
36
|
|
package/src/Select.tsx
CHANGED
|
@@ -74,7 +74,7 @@ export function Select({ value, onChange, options, size = 'default', ariaLabel,
|
|
|
74
74
|
// a native `disabled` button, which Tab skips and a tooltip cannot open on.
|
|
75
75
|
const held = Boolean(disabledReason)
|
|
76
76
|
const trigger = (
|
|
77
|
-
// @estiva-escape: N1 (Katerina, 24 September: record only): Select's box drops the hover while held, so it cannot take FIELD_SHELL_CLASSES as it is
|
|
77
|
+
// @estiva-escape(no-copied-look): N1 (Katerina, 24 September: record only): Select's box drops the hover while held, so it cannot take FIELD_SHELL_CLASSES as it is
|
|
78
78
|
<BaseSelect.Trigger
|
|
79
79
|
aria-label={ariaLabel}
|
|
80
80
|
aria-required={aria['aria-required']}
|
package/src/TopBar.mdx
CHANGED
|
@@ -33,7 +33,7 @@ reachable.
|
|
|
33
33
|
## When not
|
|
34
34
|
|
|
35
35
|
- Page-level headers inside the content → not this; this is the frame's
|
|
36
|
-
bar, once per app.
|
|
36
|
+
bar, once per app. The top of a column is **ContainerHeader**.
|
|
37
37
|
- Anything the bar would need to know — who you are, what is searched —
|
|
38
38
|
stays outside: pass composed slots.
|
|
39
39
|
|
package/src/eslint/escape.ts
CHANGED
|
@@ -4,15 +4,21 @@ import type { AST, Rule } from 'eslint'
|
|
|
4
4
|
* The escape marker (UIG-3, seam S4 in docs/GATES.md §16): one written reason
|
|
5
5
|
* why one element stays as it is.
|
|
6
6
|
*
|
|
7
|
-
* A line comment directly above the element,
|
|
7
|
+
* A line comment directly above the element, naming the rule it keeps off,
|
|
8
8
|
*
|
|
9
|
-
* // @estiva-escape: <reason>
|
|
9
|
+
* // @estiva-escape(no-raw-element): <reason>
|
|
10
10
|
*
|
|
11
11
|
* or, where the element is a JSX child, the JSX comment on the line above,
|
|
12
12
|
*
|
|
13
|
-
* {/* @estiva-escape: <reason> *\/}
|
|
13
|
+
* {/* @estiva-escape(no-raw-element): <reason> *\/}
|
|
14
14
|
*
|
|
15
|
-
* with at least ten characters of reason, spaces not counted.
|
|
15
|
+
* with at least ten characters of reason, spaces not counted. It escapes only
|
|
16
|
+
* the rules it names (Katerina's ruling B3, 25 September: one reason used to
|
|
17
|
+
* switch off every rule on the element, so a raw button that also wrote a
|
|
18
|
+
* dialog's role by hand passed under one reason). Two rules on one element are
|
|
19
|
+
* named together, `@estiva-escape(no-raw-element, no-rebuilt-behaviour)`, each
|
|
20
|
+
* on purpose. A marker that names no rule escapes nothing, and says which name
|
|
21
|
+
* to write.
|
|
16
22
|
*
|
|
17
23
|
* Not `eslint-disable`. A directive switches a rule off without saying it was
|
|
18
24
|
* meant, and the count of escapes (`.gates-count.json`) could not see it. A
|
|
@@ -37,11 +43,29 @@ export const SETTINGS_KEY = 'estiva'
|
|
|
37
43
|
* reports through the rule that called it.
|
|
38
44
|
*/
|
|
39
45
|
export const ESCAPE_MESSAGES = {
|
|
40
|
-
escapeWithoutReason: `An escape needs its reason, at least ${MIN_REASON} characters: \`// ${ESCAPE_MARKER}: <why this stays>\`. An escape with no reason hides nothing.`,
|
|
41
|
-
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.`,
|
|
46
|
+
escapeWithoutReason: `An escape needs its reason, at least ${MIN_REASON} characters: \`// ${ESCAPE_MARKER}({{rule}}): <why this stays>\`. An escape with no reason hides nothing.`,
|
|
47
|
+
escapeInDirective: `Write the escape as its own comment on the line above: \`// ${ESCAPE_MARKER}({{rule}}): <reason>\`. Inside an eslint-disable comment it switches the rule off instead of recording why.`,
|
|
48
|
+
escapeWithoutRule: `Name the rule this escape keeps off: \`${ESCAPE_MARKER}({{rule}}): <reason>\`. An escape now keeps off only the rules it names.`,
|
|
42
49
|
escaped: 'Escaped: {{reason}}',
|
|
43
50
|
} as const
|
|
44
51
|
|
|
52
|
+
/** A rule's name as a marker writes it: `no-raw-element` for `estiva/no-raw-element`. */
|
|
53
|
+
export const shortRuleName = (id: string) => id.slice(id.lastIndexOf('/') + 1)
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* What a marker says, read from a comment's text: the rules it names (none when
|
|
57
|
+
* it names none) and its reason. Null when the comment carries no marker.
|
|
58
|
+
*/
|
|
59
|
+
export function readMarker(text: string): { rules: string[]; reason: string } | null {
|
|
60
|
+
const at = text.indexOf(ESCAPE_MARKER)
|
|
61
|
+
if (at === -1) return null
|
|
62
|
+
const rest = text.slice(at + ESCAPE_MARKER.length)
|
|
63
|
+
const named = /^\s*\(([^)]*)\)/.exec(rest)
|
|
64
|
+
const rules = named ? named[1].split(/[\s,]+/).filter(Boolean).map(shortRuleName) : []
|
|
65
|
+
const reason = (named ? rest.slice(named[0].length) : rest).replace(/^\s*:/, '').trim()
|
|
66
|
+
return { rules, reason }
|
|
67
|
+
}
|
|
68
|
+
|
|
45
69
|
export interface EstivaSettings {
|
|
46
70
|
/**
|
|
47
71
|
* Report every sanctioned escape as a message with the id `escaped`, so a
|
|
@@ -85,24 +109,29 @@ export function isEscaped(context: Rule.RuleContext, node: Located): boolean {
|
|
|
85
109
|
.find((c) => c.loc && c.range && c.loc.end.line === line - 1 && BETWEEN.test(sourceCode.text.slice(c.range[1], nodeStart)))
|
|
86
110
|
if (!comment?.loc) return false
|
|
87
111
|
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
112
|
+
const marker = readMarker(comment.value)
|
|
113
|
+
if (!marker) return false
|
|
114
|
+
const rule = shortRuleName(context.id)
|
|
90
115
|
|
|
91
116
|
const directive = DIRECTIVE.exec(comment.value)
|
|
92
117
|
if (directive) {
|
|
93
118
|
const rules = directiveRules(directive[1])
|
|
94
119
|
// A directive for other rules (the token lint's notes) is not an escape of this one.
|
|
95
120
|
if (rules.length > 0 && !rules.includes(context.id)) return false
|
|
96
|
-
context.report({ loc: comment.loc, messageId: 'escapeInDirective' })
|
|
121
|
+
context.report({ loc: comment.loc, messageId: 'escapeInDirective', data: { rule } })
|
|
122
|
+
return false
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Named, but not this rule: another rule on the element may be the one it keeps off.
|
|
126
|
+
if (marker.rules.length > 0 && !marker.rules.includes(rule)) return false
|
|
127
|
+
if (marker.rules.length === 0) {
|
|
128
|
+
context.report({ loc: comment.loc, messageId: 'escapeWithoutRule', data: { rule } })
|
|
97
129
|
return false
|
|
98
130
|
}
|
|
99
131
|
|
|
100
|
-
const reason =
|
|
101
|
-
.slice(at + ESCAPE_MARKER.length)
|
|
102
|
-
.replace(/^:/, '')
|
|
103
|
-
.trim()
|
|
132
|
+
const { reason } = marker
|
|
104
133
|
if (reason.replace(/\s/g, '').length < MIN_REASON) {
|
|
105
|
-
context.report({ loc: comment.loc, messageId: 'escapeWithoutReason' })
|
|
134
|
+
context.report({ loc: comment.loc, messageId: 'escapeWithoutReason', data: { rule } })
|
|
106
135
|
return false
|
|
107
136
|
}
|
|
108
137
|
|
|
@@ -17,7 +17,7 @@ const tester = new RuleTester({
|
|
|
17
17
|
/** Real files of this package, so the rules are tested against what they read. */
|
|
18
18
|
const src = (name: string) => fileURLToPath(new URL(`../${name}`, import.meta.url))
|
|
19
19
|
const code = 'export function Probe() {\n return null\n}\n'
|
|
20
|
-
const escaped = '// @estiva-escape: a probe file that documents itself in its own page\nexport function Probe() {\n return null\n}\n'
|
|
20
|
+
const escaped = '// @estiva-escape(component-has-a-page, component-has-a-story): a probe file that documents itself in its own page\nexport function Probe() {\n return null\n}\n'
|
|
21
21
|
|
|
22
22
|
describe('the files these rules read', () => {
|
|
23
23
|
it('Button has a page and a story beside it, and FieldLine and MenuItem have both without a component file', () => {
|
package/src/eslint/index.test.ts
CHANGED
|
@@ -145,14 +145,14 @@ describe('an app lint with configs.recommended', () => {
|
|
|
145
145
|
})
|
|
146
146
|
|
|
147
147
|
it('passes the same element under an escape', async () => {
|
|
148
|
-
const [result] = await lint(component(' // @estiva-escape: a preview drawn from its own palette\n <button type="button">x</button>'))
|
|
148
|
+
const [result] = await lint(component(' // @estiva-escape(no-raw-element): a preview drawn from its own palette\n <button type="button">x</button>'))
|
|
149
149
|
expect(result.messages).toEqual([])
|
|
150
150
|
})
|
|
151
151
|
})
|
|
152
152
|
|
|
153
153
|
describe('countGates', () => {
|
|
154
154
|
it('counts an error, and an escape only when the lint reports escapes', async () => {
|
|
155
|
-
const code = component(' <div>\n <button>x</button>\n {/* @estiva-escape: a preview drawn from its own palette */}\n <button>y</button>\n </div>')
|
|
155
|
+
const code = component(' <div>\n <button>x</button>\n {/* @estiva-escape(no-raw-element): a preview drawn from its own palette */}\n <button>y</button>\n </div>')
|
|
156
156
|
const none = { errors: 0, warnings: 0, escapes: 0 }
|
|
157
157
|
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, 'estiva/no-handmade-header': none, 'estiva/no-handmade-empty-state': none, 'estiva/no-native-title': none, 'estiva/no-copied-look': none })
|
|
158
158
|
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, 'estiva/no-handmade-header': none, 'estiva/no-handmade-empty-state': none, 'estiva/no-native-title': none, 'estiva/no-copied-look': none })
|
|
@@ -65,7 +65,7 @@ tester.run('no-copied-look', noCopiedLook, {
|
|
|
65
65
|
{
|
|
66
66
|
name: 'an escape with its reason',
|
|
67
67
|
filename: app,
|
|
68
|
-
code: probe(`<>\n {/* @estiva-escape: recorded in GATES.md, a label strip Katerina left as it is */}\n <span className="${SECTION_LABEL}">x</span>\n </>`),
|
|
68
|
+
code: probe(`<>\n {/* @estiva-escape(no-copied-look): recorded in GATES.md, a label strip Katerina left as it is */}\n <span className="${SECTION_LABEL}">x</span>\n </>`),
|
|
69
69
|
},
|
|
70
70
|
],
|
|
71
71
|
invalid: [
|
|
@@ -29,15 +29,15 @@ tester.run('no-hand-rolled-behaviour', noHandRolledBehaviour, {
|
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
name: 'the import of a portal, kept with its reason',
|
|
32
|
-
code: "// @estiva-escape: the toast keeps its own portal until migration stage 6\nimport { createPortal } from 'react-dom'\nexport const Probe = () => null\n",
|
|
32
|
+
code: "// @estiva-escape(no-hand-rolled-behaviour): the toast keeps its own portal until migration stage 6\nimport { createPortal } from 'react-dom'\nexport const Probe = () => null\n",
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
name: 'the call, kept with its reason',
|
|
36
|
-
code: 'export function probe(to: HTMLElement) {\n // @estiva-escape: the toast keeps its own portal until migration stage 6\n return createPortal(null, to)\n}\n',
|
|
36
|
+
code: 'export function probe(to: HTMLElement) {\n // @estiva-escape(no-hand-rolled-behaviour): the toast keeps its own portal until migration stage 6\n return createPortal(null, to)\n}\n',
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
name: 'a global listener kept with its reason',
|
|
40
|
-
code: 'export function probe() {\n // @estiva-escape: the frame measures the window, which no floating part owns\n window.addEventListener("resize", () => {})\n}\n',
|
|
40
|
+
code: 'export function probe() {\n // @estiva-escape(no-hand-rolled-behaviour): the frame measures the window, which no floating part owns\n window.addEventListener("resize", () => {})\n}\n',
|
|
41
41
|
},
|
|
42
42
|
],
|
|
43
43
|
invalid: [
|
|
@@ -91,7 +91,7 @@ tester.run('no-handmade-empty-state', noHandmadeEmptyState, {
|
|
|
91
91
|
{ name: 'a box holding more than the line', code: jsx('<div>{rows.length === 0 ? <div className="px-3"><p>Nothing yet.</p><Button>Add one</Button></div> : rows.map((r) => <p key={r}>{r}</p>)}</div>') },
|
|
92
92
|
{
|
|
93
93
|
name: 'an escape with its reason',
|
|
94
|
-
code: jsx('<div>{rows.length === 0 ? (\n// @estiva-escape: a printed report keeps its own empty line, set in the print style\n<p>Nothing to print.</p>\n) : rows.map((r) => <p key={r}>{r}</p>)}</div>'),
|
|
94
|
+
code: jsx('<div>{rows.length === 0 ? (\n// @estiva-escape(no-handmade-empty-state): a printed report keeps its own empty line, set in the print style\n<p>Nothing to print.</p>\n) : rows.map((r) => <p key={r}>{r}</p>)}</div>'),
|
|
95
95
|
},
|
|
96
96
|
|
|
97
97
|
// the right scope
|
|
@@ -118,7 +118,7 @@ tester.run('no-handmade-empty-state', noHandmadeEmptyState, {
|
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
120
|
name: 'an escape with no reason is not an escape',
|
|
121
|
-
code: jsx('<div>{rows.length === 0 ? (\n// @estiva-escape\n<p>Nothing to print.</p>\n) : rows.map((r) => <p key={r}>{r}</p>)}</div>'),
|
|
121
|
+
code: jsx('<div>{rows.length === 0 ? (\n// @estiva-escape(no-handmade-empty-state)\n<p>Nothing to print.</p>\n) : rows.map((r) => <p key={r}>{r}</p>)}</div>'),
|
|
122
122
|
errors: [{ messageId: 'escapeWithoutReason' }, ...handmade],
|
|
123
123
|
},
|
|
124
124
|
|
|
@@ -33,7 +33,7 @@ tester.run('no-handmade-header', noHandmadeHeader, {
|
|
|
33
33
|
{ name: 'a column of rows', code: component('<div className="flex h-full flex-col">\n<div className="flex flex-col px-3"><span>{title}</span></div>\n<div />\n</div>') },
|
|
34
34
|
{
|
|
35
35
|
name: 'an escape with its reason',
|
|
36
|
-
code: component('<div className="flex h-full flex-col">\n{/* @estiva-escape: a print header that must not follow the theme */}\n<div className="flex items-center px-3 py-2"><span>{title}</span></div>\n<div />\n</div>'),
|
|
36
|
+
code: component('<div className="flex h-full flex-col">\n{/* @estiva-escape(no-handmade-header): a print header that must not follow the theme */}\n<div className="flex items-center px-3 py-2"><span>{title}</span></div>\n<div />\n</div>'),
|
|
37
37
|
},
|
|
38
38
|
],
|
|
39
39
|
invalid: [
|
|
@@ -82,7 +82,7 @@ tester.run('no-handmade-header', noHandmadeHeader, {
|
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
name: 'an escape with no reason escapes nothing',
|
|
85
|
-
code: component('<div className="flex h-full flex-col">\n{/* @estiva-escape: */}\n<div className="flex items-center px-3 py-2"><span>{title}</span></div>\n<div />\n</div>'),
|
|
85
|
+
code: component('<div className="flex h-full flex-col">\n{/* @estiva-escape(no-handmade-header): */}\n<div className="flex items-center px-3 py-2"><span>{title}</span></div>\n<div />\n</div>'),
|
|
86
86
|
errors: [{ messageId: 'escapeWithoutReason' }, { messageId: 'handmade' }],
|
|
87
87
|
},
|
|
88
88
|
],
|
|
@@ -40,7 +40,7 @@ tester.run('no-native-title', noNativeTitle, {
|
|
|
40
40
|
{ name: "the app's own component with a title of its own (Peek's NewTopicBanner)", filename: app, code: component('<NewTopicBanner title={name} />', "import { NewTopicBanner } from '@/components/Banner'\n") },
|
|
41
41
|
{ name: "a story's title is the Storybook's, an object key", code: "const meta = { title: 'Primitives/Tooltip', component: Tooltip }\nexport default meta\n" },
|
|
42
42
|
{ name: 'a prop of another name', code: component('<span aria-label={name} data-title={name}>{name}</span>') },
|
|
43
|
-
{ name: 'an escape with its reason', code: component('<>\n{/* @estiva-escape: the print view has no pointer, and the address must survive a copy */}\n<span title={at}>{at}</span>\n</>') },
|
|
43
|
+
{ name: 'an escape with its reason', code: component('<>\n{/* @estiva-escape(no-native-title): the print view has no pointer, and the address must survive a copy */}\n<span title={at}>{at}</span>\n</>') },
|
|
44
44
|
],
|
|
45
45
|
invalid: [
|
|
46
46
|
{
|
|
@@ -54,7 +54,7 @@ tester.run('no-native-title', noNativeTitle, {
|
|
|
54
54
|
{ name: "a part that hands it on to its element: the apps' status probe", code: component('<Button title="Delete">x</Button>', "import { Button } from '@estiva-app/ui'\n"), errors: native },
|
|
55
55
|
{ name: "through the app's re-export", filename: app, code: component('<IconButton title="Close" aria-label="Close">x</IconButton>', "import { IconButton } from '@/components/ui/IconButton'\n"), errors: native },
|
|
56
56
|
{ name: "through the app's wrapper that hands its props on", filename: app, code: component('<Button title="Delete">x</Button>', "import { Button } from '@/components/ui/Button'\n"), errors: native },
|
|
57
|
-
{ name: 'an escape with no reason is not an escape', code: component('<>\n{/* @estiva-escape */}\n<span title={at}>{at}</span>\n</>'), errors: [{ messageId: 'escapeWithoutReason' }, ...native] },
|
|
57
|
+
{ name: 'an escape with no reason is not an escape', code: component('<>\n{/* @estiva-escape(no-native-title) */}\n<span title={at}>{at}</span>\n</>'), errors: [{ messageId: 'escapeWithoutReason' }, ...native] },
|
|
58
58
|
],
|
|
59
59
|
})
|
|
60
60
|
|
|
@@ -25,6 +25,10 @@ tester.run('no-raw-element', noRawElement, {
|
|
|
25
25
|
{ name: "the package's Button", code: component('<Button>Save</Button>') },
|
|
26
26
|
{ name: 'a member expression named button', code: component('<Foo.button>Save</Foo.button>') },
|
|
27
27
|
{ name: 'a name that starts with button', code: component('<buttonish />') },
|
|
28
|
+
// R5: a handler that only stops the click passes; a part may take one
|
|
29
|
+
{ name: 'a wrapper that only stops the click', code: component('<div onClick={(e) => e.stopPropagation()} onPointerDown={(event) => { event.stopPropagation(); event.preventDefault() }}><Button>Save</Button></div>') },
|
|
30
|
+
{ name: 'a press handler on a part', code: component('<Card onClick={open}>x</Card>') },
|
|
31
|
+
{ name: 'a clickable element, escaped with its reason', code: component(' // @estiva-escape(no-raw-element): a surface that holds headings and lists, which a button cannot hold\n <div onClick={edit}>x</div>') },
|
|
28
32
|
{
|
|
29
33
|
name: 'the elements that are not controls',
|
|
30
34
|
code: component(' <section>\n <h1>Title</h1>\n <p>Text <strong>and</strong> <em>more</em></p>\n <ul><li>One</li></ul>\n <img src="x.png" alt="" />\n <hr />\n <kbd>Enter</kbd>\n <svg><path d="M0 0" /></svg>\n <time dateTime="2026-09-16">today</time>\n </section>'),
|
|
@@ -37,31 +41,45 @@ tester.run('no-raw-element', noRawElement, {
|
|
|
37
41
|
{ name: 'controls written as false', code: component('<audio src="a.mp3" controls={false} />') },
|
|
38
42
|
{
|
|
39
43
|
name: 'a line comment escape directly above',
|
|
40
|
-
code: component(' // @estiva-escape: a preview drawn from its own palette\n <button type="button">x</button>'),
|
|
44
|
+
code: component(' // @estiva-escape(no-raw-element): a preview drawn from its own palette\n <button type="button">x</button>'),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'a marker naming two rules keeps off each of them, written with or without the plugin name',
|
|
48
|
+
code: component(' // @estiva-escape(no-rebuilt-behaviour, estiva/no-raw-element): a preview drawn from its own palette\n <button type="button">x</button>'),
|
|
41
49
|
},
|
|
42
50
|
{
|
|
43
51
|
name: 'a JSX comment escape on the line above a child',
|
|
44
|
-
code: component(' <div>\n {/* @estiva-escape: a preview drawn from its own palette */}\n <button type="button">x</button>\n </div>'),
|
|
52
|
+
code: component(' <div>\n {/* @estiva-escape(no-raw-element): a preview drawn from its own palette */}\n <button type="button">x</button>\n </div>'),
|
|
45
53
|
},
|
|
46
54
|
{
|
|
47
55
|
name: 'a JSX comment escape over several lines',
|
|
48
|
-
code: component(' <div>\n {/*\n @estiva-escape: a preview drawn from its own palette\n */}\n <button type="button">x</button>\n </div>'),
|
|
56
|
+
code: component(' <div>\n {/*\n @estiva-escape(no-raw-element): a preview drawn from its own palette\n */}\n <button type="button">x</button>\n </div>'),
|
|
49
57
|
},
|
|
50
58
|
{
|
|
51
59
|
name: 'an escape above an opening tag that spans lines',
|
|
52
|
-
code: component(' // @estiva-escape: a preview drawn from its own palette\n <button\n type="button"\n onClick={() => {}}\n >\n x\n </button>'),
|
|
60
|
+
code: component(' // @estiva-escape(no-raw-element): a preview drawn from its own palette\n <button\n type="button"\n onClick={() => {}}\n >\n x\n </button>'),
|
|
53
61
|
},
|
|
54
62
|
{
|
|
55
63
|
name: 'exactly ten characters of reason, spaces not counted',
|
|
56
|
-
code: component(' // @estiva-escape: ab cd ef gh ij\n <button>x</button>'),
|
|
64
|
+
code: component(' // @estiva-escape(no-raw-element): ab cd ef gh ij\n <button>x</button>'),
|
|
57
65
|
},
|
|
58
66
|
{
|
|
59
67
|
name: 'an element with no part, escaped',
|
|
60
|
-
code: component(' // @estiva-escape: a map from another service, until the package has a frame\n <iframe src="https://example.com" title="Map" />'),
|
|
68
|
+
code: component(' // @estiva-escape(no-raw-element): a map from another service, until the package has a frame\n <iframe src="https://example.com" title="Map" />'),
|
|
61
69
|
},
|
|
62
70
|
],
|
|
63
71
|
invalid: [
|
|
64
72
|
{ name: 'a raw button', code: component(' <button type="button">x</button>'), errors: [{ messageId: 'raw', line: 3 }] },
|
|
73
|
+
{
|
|
74
|
+
name: 'a clickable div names Button and Card with href (R5)',
|
|
75
|
+
code: component(' <div onClick={open}>x</div>'),
|
|
76
|
+
errors: [{ message: 'A clickable <div onClick> is a button made by hand: it takes no focus and answers no key. Use `Button` from @estiva-app/ui; for a whole card that opens something, `Card` with `href`. A handler that only stops the click (`e.stopPropagation()`) is fine.' }],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'a press that does more than stop the click, on any plain element',
|
|
80
|
+
code: component(' <li onMouseDown={(e) => { e.preventDefault(); onSearch() }}>\n <span onPointerDown={handle} />\n </li>'),
|
|
81
|
+
errors: [{ messageId: 'clickable', data: { element: '<li onMouseDown>' } }, { messageId: 'clickable', data: { element: '<span onPointerDown>' } }],
|
|
82
|
+
},
|
|
65
83
|
{
|
|
66
84
|
name: 'an opening tag that spans lines (what grep misses)',
|
|
67
85
|
code: component(' <input\n type="text"\n value={x}\n />'),
|
|
@@ -106,7 +124,7 @@ tester.run('no-raw-element', noRawElement, {
|
|
|
106
124
|
|
|
107
125
|
{
|
|
108
126
|
name: 'an escape with no reason is an error, and hides nothing',
|
|
109
|
-
code: component(' // @estiva-escape:\n <button>x</button>'),
|
|
127
|
+
code: component(' // @estiva-escape(no-raw-element):\n <button>x</button>'),
|
|
110
128
|
errors: [
|
|
111
129
|
{ messageId: 'escapeWithoutReason', line: 3 },
|
|
112
130
|
{ messageId: 'raw', line: 4 },
|
|
@@ -114,22 +132,36 @@ tester.run('no-raw-element', noRawElement, {
|
|
|
114
132
|
},
|
|
115
133
|
{
|
|
116
134
|
name: 'nine characters of reason is too short',
|
|
117
|
-
code: component(' <div>\n {/* @estiva-escape: ab cd ef gh i */}\n <button>x</button>\n </div>'),
|
|
135
|
+
code: component(' <div>\n {/* @estiva-escape(no-raw-element): ab cd ef gh i */}\n <button>x</button>\n </div>'),
|
|
118
136
|
errors: [{ messageId: 'escapeWithoutReason' }, { messageId: 'raw' }],
|
|
119
137
|
},
|
|
120
138
|
{
|
|
121
139
|
name: 'a marker with no colon and no reason',
|
|
122
|
-
code: component(' // @estiva-escape\n <button>x</button>'),
|
|
140
|
+
code: component(' // @estiva-escape(no-raw-element)\n <button>x</button>'),
|
|
123
141
|
errors: [{ messageId: 'escapeWithoutReason' }, { messageId: 'raw' }],
|
|
124
142
|
},
|
|
143
|
+
// B3 (Katerina, 25 September): a marker keeps off only the rules it names.
|
|
144
|
+
{
|
|
145
|
+
name: 'a marker that names no rule escapes nothing, and says which name to write',
|
|
146
|
+
code: component(' // @estiva-escape: a preview drawn from its own palette\n <button>x</button>'),
|
|
147
|
+
errors: [
|
|
148
|
+
{ line: 3, message: 'Name the rule this escape keeps off: `@estiva-escape(no-raw-element): <reason>`. An escape now keeps off only the rules it names.' },
|
|
149
|
+
{ messageId: 'raw', line: 4 },
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'a marker that names another rule does not keep this one off',
|
|
154
|
+
code: component(' // @estiva-escape(no-rebuilt-behaviour): a preview drawn from its own palette\n <button>x</button>'),
|
|
155
|
+
errors: [{ messageId: 'raw', line: 4 }],
|
|
156
|
+
},
|
|
125
157
|
{
|
|
126
158
|
name: 'an escape two lines above does not reach the element',
|
|
127
|
-
code: component(' // @estiva-escape: a preview drawn from its own palette\n\n <button>x</button>'),
|
|
159
|
+
code: component(' // @estiva-escape(no-raw-element): a preview drawn from its own palette\n\n <button>x</button>'),
|
|
128
160
|
errors: [{ messageId: 'raw', line: 5 }],
|
|
129
161
|
},
|
|
130
162
|
{
|
|
131
163
|
name: 'an escape above a sibling does not reach the next element',
|
|
132
|
-
code: component(' <div>\n {/* @estiva-escape: a preview drawn from its own palette */}\n <span />\n <button>x</button>\n </div>'),
|
|
164
|
+
code: component(' <div>\n {/* @estiva-escape(no-raw-element): a preview drawn from its own palette */}\n <span />\n <button>x</button>\n </div>'),
|
|
133
165
|
errors: [{ messageId: 'raw', line: 6 }],
|
|
134
166
|
},
|
|
135
167
|
{
|
|
@@ -139,23 +171,23 @@ tester.run('no-raw-element', noRawElement, {
|
|
|
139
171
|
},
|
|
140
172
|
{
|
|
141
173
|
name: 'a marker inside a directive that switches every rule off is refused',
|
|
142
|
-
code: component(' // eslint-disable-next-line -- @estiva-escape: a preview drawn from its own palette\n <button>x</button>'),
|
|
174
|
+
code: component(' // eslint-disable-next-line -- @estiva-escape(no-raw-element): a preview drawn from its own palette\n <button>x</button>'),
|
|
143
175
|
// The directive silences the element's own report; the refusal sits on the directive's line.
|
|
144
176
|
errors: [{ messageId: 'escapeInDirective', line: 3 }],
|
|
145
177
|
},
|
|
146
178
|
{
|
|
147
179
|
name: 'a marker inside a directive that names this rule is refused',
|
|
148
|
-
code: component(' // eslint-disable-next-line rule-to-test/no-raw-element -- @estiva-escape: a preview drawn from its own palette\n <button>x</button>'),
|
|
180
|
+
code: component(' // eslint-disable-next-line rule-to-test/no-raw-element -- @estiva-escape(no-raw-element): a preview drawn from its own palette\n <button>x</button>'),
|
|
149
181
|
errors: [{ messageId: 'escapeInDirective', line: 3 }],
|
|
150
182
|
},
|
|
151
183
|
{
|
|
152
184
|
name: "the token lint's note for another rule is not an escape of this one",
|
|
153
|
-
code: component(' // eslint-disable-next-line no-console -- @estiva-escape: its hand-written type waits for that\n <button>x</button>'),
|
|
185
|
+
code: component(' // eslint-disable-next-line no-console -- @estiva-escape(no-raw-element): its hand-written type waits for that\n <button>x</button>'),
|
|
154
186
|
errors: [{ messageId: 'raw', line: 4 }],
|
|
155
187
|
},
|
|
156
188
|
{
|
|
157
189
|
name: 'with reportEscapes on, a sanctioned escape is reported for the count',
|
|
158
|
-
code: component(' // @estiva-escape: a preview drawn from its own palette\n <button>x</button>'),
|
|
190
|
+
code: component(' // @estiva-escape(no-raw-element): a preview drawn from its own palette\n <button>x</button>'),
|
|
159
191
|
settings: { estiva: { reportEscapes: true } },
|
|
160
192
|
errors: [{ messageId: 'escaped', data: { reason: 'a preview drawn from its own palette' }, line: 3 }],
|
|
161
193
|
},
|