@estiva-app/ui 0.35.2 → 0.37.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.
Files changed (143) hide show
  1. package/dist/Panel.d.ts +44 -0
  2. package/dist/Panel.d.ts.map +1 -0
  3. package/dist/ScrollArea.d.ts +7 -2
  4. package/dist/ScrollArea.d.ts.map +1 -1
  5. package/dist/eslint/escape.d.ts +23 -6
  6. package/dist/eslint/escape.d.ts.map +1 -1
  7. package/dist/eslint/index.js +86 -12
  8. package/dist/eslint/index.js.map +3 -3
  9. package/dist/eslint/no-handmade-header.d.ts +5 -0
  10. package/dist/eslint/no-handmade-header.d.ts.map +1 -1
  11. package/dist/eslint/no-native-title.d.ts +1 -1
  12. package/dist/eslint/no-native-title.d.ts.map +1 -1
  13. package/dist/eslint/no-raw-element.d.ts +7 -0
  14. package/dist/eslint/no-raw-element.d.ts.map +1 -1
  15. package/dist/eslint/no-restyled-part.d.ts +8 -0
  16. package/dist/eslint/no-restyled-part.d.ts.map +1 -1
  17. package/dist/gates/app-checks.d.ts +1 -1
  18. package/dist/gates/app-checks.d.ts.map +1 -1
  19. package/dist/gates/{chunk-DRIHHXQK.js → chunk-CC6L4B5U.js} +23 -17
  20. package/dist/gates/chunk-CC6L4B5U.js.map +7 -0
  21. package/dist/gates/chunk-SYVKGOWM.js +1131 -0
  22. package/dist/gates/chunk-SYVKGOWM.js.map +7 -0
  23. package/dist/gates/ci.d.ts +18 -0
  24. package/dist/gates/ci.d.ts.map +1 -0
  25. package/dist/gates/cli.js +20 -3
  26. package/dist/gates/cli.js.map +2 -2
  27. package/dist/gates/create-app.d.ts.map +1 -1
  28. package/dist/gates/create-app.js +1 -1
  29. package/dist/gates/escapes.d.ts +23 -0
  30. package/dist/gates/escapes.d.ts.map +1 -0
  31. package/dist/gates/gate-config.d.ts.map +1 -1
  32. package/dist/gates/hook.d.ts +2 -0
  33. package/dist/gates/hook.d.ts.map +1 -1
  34. package/dist/gates/index.d.ts +5 -1
  35. package/dist/gates/index.d.ts.map +1 -1
  36. package/dist/gates/index.js +42 -185
  37. package/dist/gates/index.js.map +4 -4
  38. package/dist/gates/name-escapes.d.ts +14 -0
  39. package/dist/gates/name-escapes.d.ts.map +1 -0
  40. package/dist/gates/status.d.ts +3 -1
  41. package/dist/gates/status.d.ts.map +1 -1
  42. package/dist/gates/token-lint.d.ts.map +1 -1
  43. package/dist/gates/token-style.d.ts +21 -0
  44. package/dist/gates/token-style.d.ts.map +1 -0
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +76 -49
  48. package/dist/index.js.map +4 -4
  49. package/dist/registry/{app-3ALZE6XO.js → app-YMABQII6.js} +3 -3
  50. package/dist/registry/app.d.ts.map +1 -1
  51. package/dist/registry/behaviours.d.ts +71 -0
  52. package/dist/registry/behaviours.d.ts.map +1 -0
  53. package/dist/registry/{build-NDXMVFQ5.js → build-3EANARLQ.js} +2 -2
  54. package/dist/registry/build.d.ts.map +1 -1
  55. package/dist/registry/{chunk-3WJ7ZZ7Z.js → chunk-EVJW26UR.js} +51 -4
  56. package/dist/registry/chunk-EVJW26UR.js.map +7 -0
  57. package/dist/registry/{chunk-XVII7P6G.js → chunk-SIB44CES.js} +276 -90
  58. package/dist/registry/chunk-SIB44CES.js.map +7 -0
  59. package/dist/registry/{chunk-5K2TRR4X.js → chunk-XK747STJ.js} +18 -3
  60. package/dist/registry/chunk-XK747STJ.js.map +7 -0
  61. package/dist/registry/cli.js +7 -6
  62. package/dist/registry/cli.js.map +2 -2
  63. package/dist/registry/contract.d.ts +13 -1
  64. package/dist/registry/contract.d.ts.map +1 -1
  65. package/dist/registry/index.d.ts +1 -1
  66. package/dist/registry/index.d.ts.map +1 -1
  67. package/dist/registry/index.js +11 -5
  68. package/package.json +4 -1
  69. package/registry.json +369 -19
  70. package/skill/estiva-ui.md +2 -1
  71. package/src/AppShell.mdx +2 -1
  72. package/src/AttachmentCard.tsx +7 -7
  73. package/src/Button.tsx +1 -1
  74. package/src/Card.tsx +2 -2
  75. package/src/ChipInput.mdx +2 -0
  76. package/src/ChipInput.tsx +1 -1
  77. package/src/CommandPalette.tsx +3 -3
  78. package/src/DialogShell.tsx +1 -1
  79. package/src/EmptyState.mdx +4 -3
  80. package/src/Lightbox.tsx +1 -1
  81. package/src/NavItem.tsx +1 -1
  82. package/src/Panel.mdx +78 -0
  83. package/src/Panel.stories.tsx +98 -0
  84. package/src/Panel.tsx +80 -0
  85. package/src/PersonTrigger.tsx +1 -1
  86. package/src/Reaction.tsx +1 -1
  87. package/src/ScrollArea.mdx +6 -2
  88. package/src/ScrollArea.stories.tsx +1 -1
  89. package/src/ScrollArea.tsx +9 -4
  90. package/src/Select.tsx +1 -1
  91. package/src/TopBar.mdx +1 -1
  92. package/src/eslint/escape.ts +43 -14
  93. package/src/eslint/has-a-page-and-a-story.test.ts +1 -1
  94. package/src/eslint/index.test.ts +2 -2
  95. package/src/eslint/no-copied-look.test.ts +1 -1
  96. package/src/eslint/no-hand-rolled-behaviour.test.ts +3 -3
  97. package/src/eslint/no-handmade-empty-state.test.ts +2 -2
  98. package/src/eslint/no-handmade-header.test.ts +11 -2
  99. package/src/eslint/no-handmade-header.ts +24 -2
  100. package/src/eslint/no-native-title.test.ts +2 -2
  101. package/src/eslint/no-native-title.ts +1 -1
  102. package/src/eslint/no-raw-element.test.ts +47 -15
  103. package/src/eslint/no-raw-element.ts +53 -3
  104. package/src/eslint/no-rebuilt-behaviour.test.ts +9 -9
  105. package/src/eslint/no-restyled-part.test.ts +21 -2
  106. package/src/eslint/no-restyled-part.ts +30 -0
  107. package/src/eslint/raw-element-outside-a-wrapper.test.ts +3 -3
  108. package/src/gates/app-checks.ts +9 -6
  109. package/src/gates/ci.ts +152 -0
  110. package/src/gates/cli.ts +19 -3
  111. package/src/gates/count.ts +1 -1
  112. package/src/gates/create-app.test.ts +2 -2
  113. package/src/gates/create-app.ts +22 -16
  114. package/src/gates/escapes.ts +111 -0
  115. package/src/gates/gate-config.ts +2 -0
  116. package/src/gates/gates.test.ts +86 -1
  117. package/src/gates/hook.ts +38 -4
  118. package/src/gates/index.ts +5 -1
  119. package/src/gates/name-escapes.ts +77 -0
  120. package/src/gates/status.ts +45 -2
  121. package/src/gates/token-lint.ts +11 -4
  122. package/src/gates/token-style.ts +79 -0
  123. package/src/how.test.ts +282 -0
  124. package/src/index.ts +1 -0
  125. package/src/pages.test.ts +6 -0
  126. package/src/registry/app.test.ts +37 -1
  127. package/src/registry/app.ts +52 -2
  128. package/src/registry/behaviours.ts +223 -0
  129. package/src/registry/build.ts +36 -0
  130. package/src/registry/cli.ts +5 -3
  131. package/src/registry/contract.test.ts +27 -2
  132. package/src/registry/contract.ts +28 -3
  133. package/src/registry/index.ts +1 -1
  134. package/src/registry/registry.test.ts +18 -4
  135. package/stories/Choosing.mdx +4 -3
  136. package/dist/gates/chunk-DBMUQAR6.js +0 -607
  137. package/dist/gates/chunk-DBMUQAR6.js.map +0 -7
  138. package/dist/gates/chunk-DRIHHXQK.js.map +0 -7
  139. package/dist/registry/chunk-3WJ7ZZ7Z.js.map +0 -7
  140. package/dist/registry/chunk-5K2TRR4X.js.map +0 -7
  141. package/dist/registry/chunk-XVII7P6G.js.map +0 -7
  142. /package/dist/registry/{app-3ALZE6XO.js.map → app-YMABQII6.js.map} +0 -0
  143. /package/dist/registry/{build-NDXMVFQ5.js.map → build-3EANARLQ.js.map} +0 -0
@@ -0,0 +1,44 @@
1
+ import type { ReactNode, Ref, UIEventHandler } from 'react';
2
+ /**
3
+ * A panel of a page: its name in a `ContainerHeader`, a body under it that
4
+ * scrolls whenever it is taller than the room it has, and, when there is one,
5
+ * a row fixed at the bottom that never scrolls — a reply box.
6
+ *
7
+ * Made so that scrolling comes from the package, not from each app remembering
8
+ * it (Katerina, 26 September 2026). The apps built their panels from the
9
+ * package's header and a box of their own, and a box without a `ScrollArea`
10
+ * simply cut off what did not fit: Peek's widget panel on a Ship file lost the
11
+ * end of a long description and the status control under it, with no bar and
12
+ * nothing to scroll. Here the body is always a `ScrollArea`, so the bar shows
13
+ * as soon as there is more, and an empty panel or a short one has none.
14
+ *
15
+ * It takes the room its column gives it (`flex-1 min-h-0`), never its content's
16
+ * height, so the body is what scrolls, not the page. Two panels in one column
17
+ * share it; a `max-h-*` in `className` caps one of them.
18
+ *
19
+ * It keeps a crash inside itself, as ListColumn does: if its body, its actions
20
+ * or its footer break, the panel keeps its title and shows `ErrorBoundary`'s
21
+ * message in the room the body had.
22
+ */
23
+ export interface PanelProps {
24
+ /** The panel's name, in its header. A node for a title that is more than words. */
25
+ title: ReactNode;
26
+ /** A chevron after the title, as `ContainerHeader` draws it. */
27
+ chevron?: boolean;
28
+ /** The panel's own actions, at the header's right — a `Toolbar` of `ToolbarButton`s, or one `IconButton`. */
29
+ actions?: ReactNode;
30
+ /** The body. It scrolls when it is taller than the panel. */
31
+ children: ReactNode;
32
+ /** A row fixed under the body that never scrolls — a reply box. */
33
+ footer?: ReactNode;
34
+ /** The body's own box: its padding, gap and layout — `px-4 pt-3 pb-4`, `flex flex-col gap-2`. */
35
+ bodyClassName?: string;
36
+ /** The body's scrolling box, for a panel that reads or drives its scroll: a thread that opens at its newest reply. */
37
+ bodyRef?: Ref<HTMLDivElement>;
38
+ /** Fires as the body scrolls. */
39
+ onBodyScroll?: UIEventHandler<HTMLDivElement>;
40
+ /** Placement only: a cap such as `max-h-[60%]` when it shares a column. */
41
+ className?: string;
42
+ }
43
+ export declare function Panel(props: PanelProps): import("react").JSX.Element;
44
+ //# sourceMappingURL=Panel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Panel.d.ts","sourceRoot":"","sources":["../src/Panel.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAM3D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,UAAU;IACzB,mFAAmF;IACnF,KAAK,EAAE,SAAS,CAAA;IAChB,gEAAgE;IAChE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,6GAA6G;IAC7G,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,6DAA6D;IAC7D,QAAQ,EAAE,SAAS,CAAA;IACnB,mEAAmE;IACnE,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB,iGAAiG;IACjG,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,sHAAsH;IACtH,OAAO,CAAC,EAAE,GAAG,CAAC,cAAc,CAAC,CAAA;IAC7B,iCAAiC;IACjC,YAAY,CAAC,EAAE,cAAc,CAAC,cAAc,CAAC,CAAA;IAC7C,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAmBD,wBAAgB,KAAK,CAAC,KAAK,EAAE,UAAU,+BActC"}
@@ -7,8 +7,13 @@ import type { ReactNode, Ref, UIEventHandler } from 'react';
7
7
  * right-hand padding looks wider than the left. Katerina saw it in every
8
8
  * scrolling surface of both apps (2026-09-08). Base UI's `ScrollArea` hides
9
9
  * the native bar and draws its own over the content, so nothing moves when
10
- * it appears; the bar shows while the pointer is over the region or the
11
- * content is moving, and fades otherwise.
10
+ * it appears.
11
+ *
12
+ * The bar shows whenever the region has more to scroll to, at rest too
13
+ * (Katerina, 26 September: a long folder list in Peek's side column looked
14
+ * complete, because the bar waited for the pointer). Base UI writes
15
+ * `data-has-overflow-y` / `-x` on the bar while that axis overflows; a region
16
+ * with nothing more to show keeps no bar at all.
12
17
  *
13
18
  * Three boxes, and each class prop lands on one:
14
19
  *
@@ -1 +1 @@
1
- {"version":3,"file":"ScrollArea.d.ts","sourceRoot":"","sources":["../src/ScrollArea.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAI3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,EAAE,UAAU,GAAG,YAAY,GAAG,MAAM,CAAA;IAChD,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,GAAG,CAAC,cAAc,CAAC,CAAA;IACjC,uFAAuF;IACvF,QAAQ,CAAC,EAAE,cAAc,CAAC,cAAc,CAAC,CAAA;IACzC,QAAQ,EAAE,SAAS,CAAA;CACpB;AAKD,wBAAgB,UAAU,CAAC,EAAE,WAAwB,EAAE,SAAS,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,eAAe,+BA4CxJ"}
1
+ {"version":3,"file":"ScrollArea.d.ts","sourceRoot":"","sources":["../src/ScrollArea.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAI3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,EAAE,UAAU,GAAG,YAAY,GAAG,MAAM,CAAA;IAChD,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,8DAA8D;IAC9D,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,GAAG,CAAC,cAAc,CAAC,CAAA;IACjC,uFAAuF;IACvF,QAAQ,CAAC,EAAE,cAAc,CAAC,cAAc,CAAC,CAAA;IACzC,QAAQ,EAAE,SAAS,CAAA;CACpB;AAKD,wBAAgB,UAAU,CAAC,EAAE,WAAwB,EAAE,SAAS,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,EAAE,eAAe,+BA4CxJ"}
@@ -3,15 +3,21 @@ import type { AST, Rule } from 'eslint';
3
3
  * The escape marker (UIG-3, seam S4 in docs/GATES.md §16): one written reason
4
4
  * why one element stays as it is.
5
5
  *
6
- * A line comment directly above the element,
6
+ * A line comment directly above the element, naming the rule it keeps off,
7
7
  *
8
- * // @estiva-escape: <reason>
8
+ * // @estiva-escape(no-raw-element): <reason>
9
9
  *
10
10
  * or, where the element is a JSX child, the JSX comment on the line above,
11
11
  *
12
- * {/* @estiva-escape: <reason> *\/}
12
+ * {/* @estiva-escape(no-raw-element): <reason> *\/}
13
13
  *
14
- * with at least ten characters of reason, spaces not counted.
14
+ * with at least ten characters of reason, spaces not counted. It escapes only
15
+ * the rules it names (Katerina's ruling B3, 25 September: one reason used to
16
+ * switch off every rule on the element, so a raw button that also wrote a
17
+ * dialog's role by hand passed under one reason). Two rules on one element are
18
+ * named together, `@estiva-escape(no-raw-element, no-rebuilt-behaviour)`, each
19
+ * on purpose. A marker that names no rule escapes nothing, and says which name
20
+ * to write.
15
21
  *
16
22
  * Not `eslint-disable`. A directive switches a rule off without saying it was
17
23
  * meant, and the count of escapes (`.gates-count.json`) could not see it. A
@@ -33,10 +39,21 @@ export declare const SETTINGS_KEY = "estiva";
33
39
  * reports through the rule that called it.
34
40
  */
35
41
  export declare const ESCAPE_MESSAGES: {
36
- readonly escapeWithoutReason: "An escape needs its reason, at least 10 characters: `// @estiva-escape: <why this stays>`. An escape with no reason hides nothing.";
37
- readonly escapeInDirective: "Write the escape as its own comment on the line above: `// @estiva-escape: <reason>`. Inside an eslint-disable comment it switches the rule off instead of recording why.";
42
+ readonly escapeWithoutReason: "An escape needs its reason, at least 10 characters: `// @estiva-escape({{rule}}): <why this stays>`. An escape with no reason hides nothing.";
43
+ readonly escapeInDirective: "Write the escape as its own comment on the line above: `// @estiva-escape({{rule}}): <reason>`. Inside an eslint-disable comment it switches the rule off instead of recording why.";
44
+ readonly escapeWithoutRule: "Name the rule this escape keeps off: `@estiva-escape({{rule}}): <reason>`. An escape now keeps off only the rules it names.";
38
45
  readonly escaped: "Escaped: {{reason}}";
39
46
  };
47
+ /** A rule's name as a marker writes it: `no-raw-element` for `estiva/no-raw-element`. */
48
+ export declare const shortRuleName: (id: string) => string;
49
+ /**
50
+ * What a marker says, read from a comment's text: the rules it names (none when
51
+ * it names none) and its reason. Null when the comment carries no marker.
52
+ */
53
+ export declare function readMarker(text: string): {
54
+ rules: string[];
55
+ reason: string;
56
+ } | null;
40
57
  export interface EstivaSettings {
41
58
  /**
42
59
  * Report every sanctioned escape as a message with the id `escaped`, so a
@@ -1 +1 @@
1
- {"version":3,"file":"escape.d.ts","sourceRoot":"","sources":["../../src/eslint/escape.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAEvC;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,aAAa,mBAAmB,CAAA;AAE7C,gDAAgD;AAChD,eAAO,MAAM,UAAU,KAAK,CAAA;AAE5B,2DAA2D;AAC3D,eAAO,MAAM,YAAY,WAAW,CAAA;AAEpC;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;CAIlB,CAAA;AAEV,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,UAAU,OAAO;IACf,GAAG,CAAC,EAAE,GAAG,CAAC,cAAc,GAAG,IAAI,CAAA;IAC/B,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACzB;AAaD;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAmC3E"}
1
+ {"version":3,"file":"escape.d.ts","sourceRoot":"","sources":["../../src/eslint/escape.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAEvC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,aAAa,mBAAmB,CAAA;AAE7C,gDAAgD;AAChD,eAAO,MAAM,UAAU,KAAK,CAAA;AAE5B,2DAA2D;AAC3D,eAAO,MAAM,YAAY,WAAW,CAAA;AAEpC;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;;CAKlB,CAAA;AAEV,yFAAyF;AACzF,eAAO,MAAM,aAAa,GAAI,IAAI,MAAM,WAAsC,CAAA;AAE9E;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAQnF;AAED,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAA;CACxB;AAED,UAAU,OAAO;IACf,GAAG,CAAC,EAAE,GAAG,CAAC,cAAc,GAAG,IAAI,CAAA;IAC/B,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACzB;AAaD;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAwC3E"}
@@ -9,10 +9,21 @@ var ESCAPE_MARKER = "@estiva-escape";
9
9
  var MIN_REASON = 10;
10
10
  var SETTINGS_KEY = "estiva";
11
11
  var ESCAPE_MESSAGES = {
12
- escapeWithoutReason: `An escape needs its reason, at least ${MIN_REASON} characters: \`// ${ESCAPE_MARKER}: <why this stays>\`. An escape with no reason hides nothing.`,
13
- 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.`,
12
+ escapeWithoutReason: `An escape needs its reason, at least ${MIN_REASON} characters: \`// ${ESCAPE_MARKER}({{rule}}): <why this stays>\`. An escape with no reason hides nothing.`,
13
+ 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.`,
14
+ escapeWithoutRule: `Name the rule this escape keeps off: \`${ESCAPE_MARKER}({{rule}}): <reason>\`. An escape now keeps off only the rules it names.`,
14
15
  escaped: "Escaped: {{reason}}"
15
16
  };
17
+ var shortRuleName = (id) => id.slice(id.lastIndexOf("/") + 1);
18
+ function readMarker(text) {
19
+ const at = text.indexOf(ESCAPE_MARKER);
20
+ if (at === -1) return null;
21
+ const rest = text.slice(at + ESCAPE_MARKER.length);
22
+ const named = /^\s*\(([^)]*)\)/.exec(rest);
23
+ const rules2 = named ? named[1].split(/[\s,]+/).filter(Boolean).map(shortRuleName) : [];
24
+ const reason = (named ? rest.slice(named[0].length) : rest).replace(/^\s*:/, "").trim();
25
+ return { rules: rules2, reason };
26
+ }
16
27
  var DIRECTIVE = /^\s*eslint-disable(?:-next-line|-line)?(?=\s|$)([^]*)$/;
17
28
  function directiveRules(rest) {
18
29
  const dashes = rest.search(/(?:^|\s)--(?:\s|$)/);
@@ -26,18 +37,24 @@ function isEscaped(context, node) {
26
37
  const nodeStart = node.range[0];
27
38
  const comment = sourceCode.getAllComments().find((c) => c.loc && c.range && c.loc.end.line === line - 1 && BETWEEN.test(sourceCode.text.slice(c.range[1], nodeStart)));
28
39
  if (!comment?.loc) return false;
29
- const at = comment.value.indexOf(ESCAPE_MARKER);
30
- if (at === -1) return false;
40
+ const marker = readMarker(comment.value);
41
+ if (!marker) return false;
42
+ const rule = shortRuleName(context.id);
31
43
  const directive = DIRECTIVE.exec(comment.value);
32
44
  if (directive) {
33
45
  const rules2 = directiveRules(directive[1]);
34
46
  if (rules2.length > 0 && !rules2.includes(context.id)) return false;
35
- context.report({ loc: comment.loc, messageId: "escapeInDirective" });
47
+ context.report({ loc: comment.loc, messageId: "escapeInDirective", data: { rule } });
48
+ return false;
49
+ }
50
+ if (marker.rules.length > 0 && !marker.rules.includes(rule)) return false;
51
+ if (marker.rules.length === 0) {
52
+ context.report({ loc: comment.loc, messageId: "escapeWithoutRule", data: { rule } });
36
53
  return false;
37
54
  }
38
- const reason = comment.value.slice(at + ESCAPE_MARKER.length).replace(/^:/, "").trim();
55
+ const { reason } = marker;
39
56
  if (reason.replace(/\s/g, "").length < MIN_REASON) {
40
- context.report({ loc: comment.loc, messageId: "escapeWithoutReason" });
57
+ context.report({ loc: comment.loc, messageId: "escapeWithoutReason", data: { rule } });
41
58
  return false;
42
59
  }
43
60
  const settings = context.settings[SETTINGS_KEY];
@@ -175,6 +192,7 @@ var PLACEMENT = [
175
192
  // Draws nothing: it names the part, so what is inside can answer its hover (`group-hover:`).
176
193
  { what: "a name for hover", pattern: /^(?:group|peer)(?:\/[\w-]+)?$/ }
177
194
  ];
195
+ var PLACEMENT_STYLE = /^(?:margin|padding|inset|scrollMargin)(?:Top|Right|Bottom|Left|Block|Inline|BlockStart|BlockEnd|InlineStart|InlineEnd)?$|^(?:width|height|minWidth|minHeight|maxWidth|maxHeight|inlineSize|blockSize|minInlineSize|minBlockSize|maxInlineSize|maxBlockSize)$|^(?:display|flex|flexGrow|flexShrink|flexBasis|order|gap|rowGap|columnGap|alignSelf|justifySelf|placeSelf|gridArea|gridColumn|gridColumnStart|gridColumnEnd|gridRow|gridRowStart|gridRowEnd)$|^(?:position|top|right|bottom|left|zIndex|transform|translate)$/;
178
196
  var REACHING_VARIANT = /^\[|^(?:before|after|placeholder|file|marker|selection|first-letter|first-line|backdrop|\*|\*\*)$/;
179
197
  function splitClass(token) {
180
198
  const variants = [];
@@ -606,6 +624,7 @@ var noRestyledPart = {
606
624
  schema: [],
607
625
  messages: {
608
626
  restyled: "{{classes}} on {{where}} 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}} A look for what is around it goes on your own element around it.",
627
+ restyledStyle: "{{keys}} in the `style` of {{where}} changes how it looks. A part of @estiva-app/ui is placed from outside, never restyled, by class or by style: only space, size, flex and grid, and position pass in.{{use}} A look for what is around it goes on your own element around it.",
609
628
  emptyStatePadding: "{{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.",
610
629
  ...ESCAPE_MESSAGES
611
630
  }
@@ -639,6 +658,19 @@ var noRestyledPart = {
639
658
  }
640
659
  if (padding.length) context.report({ loc: at.loc, messageId: "emptyStatePadding", data: { classes: code(padding), where } });
641
660
  };
661
+ const checkStyle = (element, binding, value, at) => {
662
+ if (binding.props !== "all" || value?.type !== "JSXExpressionContainer") return;
663
+ let object = child(value, "expression");
664
+ if (object?.type === "Identifier") object = constantOf(object, scopeOf(object));
665
+ while (object?.type === "TSAsExpression" || object?.type === "TSSatisfiesExpression") object = child(object, "expression");
666
+ if (object?.type !== "ObjectExpression") return;
667
+ const keys = children(object, "properties").map((property) => property?.type === "Property" && !property.computed ? nameOf(child(property, "key")) : void 0).filter((key) => !!key && !key.startsWith("--") && !PLACEMENT_STYLE.test(key));
668
+ if (!keys.length || escaped(element)) return;
669
+ const props = PART_LOOK_PROPS[binding.part];
670
+ const use = props ? ` Use its ${or(props)}.` : ` \`${binding.part}\` has no prop for this yet: ask Katerina, and it gets made in @estiva-app/ui.`;
671
+ const where = binding.via && binding.via !== binding.part ? `\`${binding.via}\` (it hands its props to \`${binding.part}\`)` : `\`${binding.part}\``;
672
+ context.report({ loc: at.loc, messageId: "restyledStyle", data: { keys: code([...new Set(keys)]), where, use } });
673
+ };
642
674
  return {
643
675
  Program(program) {
644
676
  locals = moduleParts(file, program, parser, /* @__PURE__ */ new Set([file])).locals;
@@ -652,6 +684,7 @@ var noRestyledPart = {
652
684
  if (attribute3.type === "JSXAttribute") {
653
685
  const prop = nameOf(child(attribute3, "name"));
654
686
  if (prop && CLASS_PROP.test(prop)) check(element, binding, prop, child(attribute3, "value"), attribute3);
687
+ else if (prop === "style") checkStyle(element, binding, child(attribute3, "value"), attribute3);
655
688
  continue;
656
689
  }
657
690
  let spread = child(attribute3, "argument");
@@ -958,15 +991,28 @@ var INTERACTIVE = ["onClick", "href", "role", "onKeyDown", "onPointerDown"];
958
991
  var noHandmadeHeader = {
959
992
  meta: {
960
993
  type: "problem",
961
- docs: { description: "A column's header bar drawn by hand where ContainerHeader is the part" },
994
+ docs: { description: "A column's header bar drawn by hand, or the package's header on its own in an app, where Panel is the part" },
962
995
  schema: [],
963
996
  messages: {
964
- handmade: "A column's header bar drawn by hand. Use `ContainerHeader` from @estiva-app/ui: 48px, the title, the column's buttons at the right edge (`actions`), a hairline under it.",
997
+ handmade: "A column's header bar drawn by hand. Use `Panel` from @estiva-app/ui: its header (48px, the title, the column's buttons at the right edge in `actions`, a hairline under it) and a body under it that scrolls. For a list beside the page, `ListColumn`.",
998
+ bare: "A panel put together by hand: `ContainerHeader` over a box of your own, which cuts off whatever does not fit. Use `Panel` from @estiva-app/ui: the same header (`title`, `actions`), a body that scrolls whenever it is taller, and a `footer` that stays put. For a list beside the page, `ListColumn`.",
965
999
  ...ESCAPE_MESSAGES
966
1000
  }
967
1001
  },
968
1002
  create(context) {
1003
+ const bare2 = /* @__PURE__ */ new Set();
969
1004
  return {
1005
+ ImportDeclaration(node) {
1006
+ const declaration = node;
1007
+ if (declaration.source.value !== "@estiva-app/ui") return;
1008
+ for (const specifier of declaration.specifiers) if (specifier.type === "ImportSpecifier" && specifier.imported?.name === "ContainerHeader") bare2.add(specifier.local.name);
1009
+ },
1010
+ JSXOpeningElement(node) {
1011
+ const element = node;
1012
+ if (element.name.type !== "JSXIdentifier" || !bare2.has(element.name.name ?? "")) return;
1013
+ if (isEscaped(context, element)) return;
1014
+ context.report({ loc: element.loc, messageId: "bare" });
1015
+ },
970
1016
  JSXElement(node) {
971
1017
  const column = node;
972
1018
  const kids = meaningful(column);
@@ -1356,7 +1402,7 @@ var noCopiedLook = {
1356
1402
  };
1357
1403
 
1358
1404
  // src/eslint/no-native-title.ts
1359
- var PARTS_WITH_A_TITLE = ["CollapsibleSection", "ConfirmDialog", "ContainerHeader", "DialogShell", "ListColumn", "SectionHeader"];
1405
+ var PARTS_WITH_A_TITLE = ["CollapsibleSection", "ConfirmDialog", "ContainerHeader", "DialogShell", "ListColumn", "Panel", "SectionHeader"];
1360
1406
  var child3 = (node, key) => node[key];
1361
1407
  var nameOf4 = (node) => node?.type === "JSXIdentifier" || node?.type === "Identifier" ? node.name : void 0;
1362
1408
  var noNativeTitle = {
@@ -1452,6 +1498,28 @@ function isOn(element, name) {
1452
1498
  const attr = attribute2(element, name);
1453
1499
  return attr !== void 0 && literal(attr) !== false;
1454
1500
  }
1501
+ var CLICK_HANDLERS = ["onClick", "onMouseDown", "onMouseUp", "onPointerDown", "onPointerUp", "onDoubleClick"];
1502
+ function onlyStops(handler) {
1503
+ if (!handler || handler.type !== "ArrowFunctionExpression" && handler.type !== "FunctionExpression") return false;
1504
+ const body = handler.body;
1505
+ const calls = body.type === "BlockStatement" ? body.body.map((s) => s.type === "ExpressionStatement" ? s.expression : s) : [body];
1506
+ return calls.length > 0 && calls.every((call) => {
1507
+ if (call.type !== "CallExpression") return false;
1508
+ const callee = call.callee;
1509
+ const property = callee.type === "MemberExpression" ? callee.property : void 0;
1510
+ return property?.type === "Identifier" && (property.name === "stopPropagation" || property.name === "preventDefault");
1511
+ });
1512
+ }
1513
+ function clickHandler(element) {
1514
+ for (const name of CLICK_HANDLERS) {
1515
+ const attr = attribute2(element, name);
1516
+ if (!attr) continue;
1517
+ const value = attr.value;
1518
+ const handler = value?.type === "JSXExpressionContainer" ? value.expression : void 0;
1519
+ if (!onlyStops(handler)) return name;
1520
+ }
1521
+ return void 0;
1522
+ }
1455
1523
  function classify(element) {
1456
1524
  if (element.name.type !== "JSXIdentifier" || !element.name.name) return null;
1457
1525
  const name = element.name.name;
@@ -1466,7 +1534,11 @@ function classify(element) {
1466
1534
  const shown = `<input type="${key}">`;
1467
1535
  return part2 ? { report: "raw", element: shown, part: part2 } : { report: "noPart", element: shown };
1468
1536
  }
1469
- if (!Object.hasOwn(RAW_ELEMENT_PARTS, name)) return null;
1537
+ if (!Object.hasOwn(RAW_ELEMENT_PARTS, name)) {
1538
+ if (!/^[a-z][a-z0-9]*$/.test(name) || name === "option" || name === "summary") return null;
1539
+ const handler = clickHandler(element);
1540
+ return handler ? { report: "clickable", element: `<${name} ${handler}>` } : null;
1541
+ }
1470
1542
  if ((name === "audio" || name === "video") && !isOn(element, "controls")) return null;
1471
1543
  if (name === "img" && !attribute2(element, "usemap") && !attribute2(element, "useMap")) return null;
1472
1544
  const part = RAW_ELEMENT_PARTS[name];
@@ -1479,6 +1551,7 @@ var noRawElement = {
1479
1551
  schema: [],
1480
1552
  messages: {
1481
1553
  raw: "Use `{{use}}` from @estiva-app/ui instead of a raw {{element}}.{{more}}",
1554
+ clickable: "A clickable {{element}} 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.",
1482
1555
  noPart: "@estiva-app/ui has no part for a raw {{element}} yet. Do not build one here: ask Katerina, and it gets made in @estiva-app/ui.",
1483
1556
  ...ESCAPE_MESSAGES
1484
1557
  }
@@ -1491,7 +1564,8 @@ var noRawElement = {
1491
1564
  const found = classify(element);
1492
1565
  if (!found) return;
1493
1566
  if (isEscaped(context, element)) return;
1494
- if (found.report === "noPart") context.report({ loc: element.loc, messageId: "noPart", data: { element: found.element } });
1567
+ if (found.report === "clickable") context.report({ loc: element.loc, messageId: "clickable", data: { element: found.element } });
1568
+ else if (found.report === "noPart") context.report({ loc: element.loc, messageId: "noPart", data: { element: found.element } });
1495
1569
  else context.report({ loc: element.loc, messageId: "raw", data: { element: found.element, use: found.part.use, more: found.part.more ?? "" } });
1496
1570
  }
1497
1571
  };