@botiverse/raft-ui 0.73.0 → 0.74.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/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ## [0.74.0](https://github.com/botiverse/raft-design/compare/v0.73.0...v0.74.0) (2026-11-08)
2
+
3
+ ### Added
4
+
5
+ - **Lint rules for apps**: `@botiverse/raft-ui/lint` is a lint plugin with the design system's rules for app code, in the ESLint plugin format, which Oxlint loads as a JS plugin. Its first rule, `raft-ui/prefer-scroll-area`, reports a class or an inline style that makes an element scroll natively (`overflow-auto`, `overflow-scroll` and their `-x-` and `-y-` forms, with any variant) where an app writes it, unless a `// raft-ui: native-scroll <reason>` comment on the line above says which exception it is. The comment covers the string on the next line only, not the rest of a multi-line `cn(...)`, so a class added to the list later is still checked. Turn on every rule at once by extending `./node_modules/@botiverse/raft-ui/dist/lint/recommended.json` from `.oxlintrc.json` (or `configs.recommended` in an ESLint flat config); a rule added later reaches the app with the upgrade. ([#1058](https://github.com/botiverse/raft-design/pull/1058), [#1059](https://github.com/botiverse/raft-design/pull/1059))
6
+ - Why: the design system's rules should reach the apps, and the agents working in them, with raft-ui itself rather than through each repository's instructions; every region that scrolls is a ScrollArea, and nothing outside raft-ui checked it (Artea, #proj-raft-ui:#209).
7
+ - Affects: nothing until an app turns the plugin on. Then each native scroller in its code is reported: make it a ScrollArea (or `PanelScrollViewport` in a Panel), or add the comment with its reason.
8
+ - Docs: https://raft-design.botiverse.build/about/use-raft-ui/#lint-rules
9
+
10
+ ### Changed
11
+
12
+ - **useScrollAreaContainer gives its container the overflow**: the classes in `containerProps.className` make the element scroll in the `orientation`, as ScrollArea's own container does (vertical: `overflow-y: auto` and `overflow-x: hidden`; horizontal: the other way round; both: `auto`), so the element needs no overflow class of its own and nothing for `raft-ui/prefer-scroll-area` to report. `ComposerInput` and `ActivityInboxViewport` no longer carry `overflow-y-auto`. ([#1059](https://github.com/botiverse/raft-design/pull/1059))
13
+ - Why: the hook asked the app to add a native overflow class, which the new lint rule reports (Jasper's review of the lint rule).
14
+ - Affects: apps can drop the overflow class from a `useScrollAreaContainer` element. One they keep still wins (the hook's rule has no specificity), so nothing breaks. A vertical container that had only `overflow-y-auto` now clips sideways (`overflow-x: hidden`) instead of scrolling sideways with no scrollbar; pass `orientation="both"` for content that scrolls both ways.
15
+ - Docs: https://raft-design.botiverse.build/components/scroll-area/#your-own-container
16
+
17
+ ### Design System
18
+
19
+ - **A thread's timeline**: conversation.md lists what a thread's timeline shows (the message it started from and every message a person wrote), when it appears, the rail for a mouse, the Timeline entry in the thread's More menu on every device, a half-height sheet on a phone and a popover wider, and how its items are marked. No raft-ui component changed. ([#1057](https://github.com/botiverse/raft-design/pull/1057))
20
+ - Why: a long thread buries the few messages people wrote under agents' work (Artea, #proj-raft-ui:#206).
21
+ - Docs: https://raft-design.botiverse.build/patterns/conversation/#a-threads-timeline
22
+
1
23
  ## [0.73.0](https://github.com/botiverse/raft-design/compare/v0.72.0...v0.73.0) (2026-11-08)
2
24
 
3
25
  ### Changed
@@ -9,7 +9,7 @@ const activityInboxShell = tv({ slots: {
9
9
  ],
10
10
  frame: ["min-h-0 flex-1"],
11
11
  viewport: [
12
- "relative flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto *:shrink-0",
12
+ "relative flex min-h-0 flex-1 flex-col gap-0.5 *:shrink-0",
13
13
  "outline-none focus-visible:shadow-focus",
14
14
  "bg-layer-canvas px-0 pt-2 pb-[calc(8px+env(safe-area-inset-bottom,0px))]"
15
15
  ],
@@ -12,7 +12,7 @@ const composer = tv({
12
12
  inputFrame: ["w-full min-w-0", "[--r-scroll-area-resize-grip-size:0px]"],
13
13
  input: [
14
14
  "max-h-32 w-full resize-none bg-transparent outline-none",
15
- "field-sizing-content overflow-y-auto",
15
+ "field-sizing-content",
16
16
  "min-h-6 px-0.5",
17
17
  "font-sans text-body font-normal text-foreground",
18
18
  "placeholder:text-foreground-hint"
@@ -24,8 +24,8 @@
24
24
  * `--overflow-{y,x}-{start,end}` (more content past that edge; `x` is
25
25
  * inline; these and `--rtl` are set by the script with `classList`, never
26
26
  * rendered) and `--textarea`; `__container` (with absolute gutters, the
27
- * container), `__viewport` (ScrollArea's own `<div>` container: its
28
- * overflow), `__content`, `__textarea`, `__gutters` (with `--scrolling`,
27
+ * container), `__viewport` (ScrollArea's own `<div>` container),
28
+ * `__content`, `__textarea`, `__gutters` (with `--scrolling`,
29
29
  * from the first scroll until `hideDelay` after the last, and `--dragging`,
30
30
  * both set by the script: on the layer, so toggling them restyles only the
31
31
  * gutters, not the content under the root), `__gutter` (`--vertical`,
@@ -216,9 +216,9 @@
216
216
  }
217
217
 
218
218
  /*
219
- * The container. Its own overflow, layout and focus are its own (a list,
220
- * a textarea); the overlay scrollbars replace its native ones. With sticky
221
- * gutters it is the root.
219
+ * The container. Its layout and focus are its own (a list, a textarea);
220
+ * its overflow is the orientation's (below), and the overlay scrollbars
221
+ * replace its native ones. With sticky gutters it is the root.
222
222
  */
223
223
  .r-scroll-area.r-scroll-area--sticky-gutters,
224
224
  .r-scroll-area.r-scroll-area--absolute-gutters > .r-scroll-area__container {
@@ -242,17 +242,34 @@
242
242
  position: relative;
243
243
  }
244
244
 
245
- /* ScrollArea's own `<div>` container (always sticky) scrolls in the orientation. */
246
- .r-scroll-area.r-scroll-area--sticky-gutters.r-scroll-area__viewport {
245
+ /*
246
+ * Every container scrolls in the orientation: ScrollArea's own `<div>` and
247
+ * a `useScrollAreaContainer` element alike, so a caller never writes the
248
+ * overflow. With no specificity (`:where`), so a component's own rule for
249
+ * its scroller (VirtualList's) or an overflow in the caller's `className`
250
+ * still wins over it.
251
+ */
252
+ :where(.r-scroll-area.r-scroll-area--sticky-gutters),
253
+ :where(.r-scroll-area.r-scroll-area--absolute-gutters > .r-scroll-area__container) {
247
254
  overflow: hidden auto;
248
- position: relative;
249
255
  }
250
- .r-scroll-area.r-scroll-area--sticky-gutters.r-scroll-area--horizontal-orientation.r-scroll-area__viewport {
256
+ :where(.r-scroll-area.r-scroll-area--sticky-gutters.r-scroll-area--horizontal-orientation),
257
+ :where(
258
+ .r-scroll-area.r-scroll-area--absolute-gutters.r-scroll-area--horizontal-orientation
259
+ > .r-scroll-area__container
260
+ ) {
251
261
  overflow: auto hidden;
252
262
  }
253
- .r-scroll-area.r-scroll-area--sticky-gutters.r-scroll-area--both-orientation.r-scroll-area__viewport {
263
+ :where(.r-scroll-area.r-scroll-area--sticky-gutters.r-scroll-area--both-orientation),
264
+ :where(
265
+ .r-scroll-area.r-scroll-area--absolute-gutters.r-scroll-area--both-orientation
266
+ > .r-scroll-area__container
267
+ ) {
254
268
  overflow: auto;
255
269
  }
270
+ .r-scroll-area.r-scroll-area--sticky-gutters.r-scroll-area__viewport {
271
+ position: relative;
272
+ }
256
273
 
257
274
  /*
258
275
  * A scroll that reaches the end stops there instead of scrolling the page:
@@ -116,7 +116,9 @@ export type ScrollAreaContainerProps<E extends HTMLElement = HTMLElement> = {
116
116
  ref: RefCallback<E>;
117
117
  /**
118
118
  * Before your own classes. With sticky gutters the container is the root, so these are the
119
- * root's classes and modifiers; with absolute gutters, `r-scroll-area__container`.
119
+ * root's classes and modifiers; with absolute gutters, `r-scroll-area__container`. They give the
120
+ * element the orientation's overflow (vertical: `overflow-y: auto`, `overflow-x: hidden`), so
121
+ * it needs no overflow class of its own; one of yours still wins over it.
120
122
  */
121
123
  className: string;
122
124
  /** With sticky gutters, the root's variables (`railInset`, the timelines); merge your style after it. */
@@ -6,7 +6,8 @@ import { ScrollAreaContainer, ScrollAreaContainerOptions, ScrollAreaFrameProps }
6
6
  * its scroll, and keeps the state; you render the elements.
7
7
  *
8
8
  * Spread `containerProps` on the element that scrolls (a `<ul>`, a `<textarea>`, VirtualList's
9
- * scroller through `domRef`), merging your own `className` and `style` with them (pass your own
9
+ * scroller through `domRef`), merging your own `className` and `style` with them; its classes give
10
+ * the element the orientation's overflow, so it needs no overflow class (pass your own
10
11
  * refs as `containerRef` and `frameRef`: the returned `ref`s already forward to them), and render
11
12
  * `<ScrollAreaGutters {...guttersProps} />` where `guttersPosition` says:
12
13
  *
@@ -0,0 +1,75 @@
1
+ //#region src/lint/prefer-scroll-area.d.ts
2
+ /**
3
+ * raft-ui/prefer-scroll-area: every region that scrolls is a ScrollArea
4
+ * (scroll-area.md#native-scrolling), so a native overflow that makes an
5
+ * element scroll is reported where an app writes it.
6
+ *
7
+ * Reported:
8
+ * - a native-scrolling Tailwind class (`overflow`, `overflow-x` or `overflow-y`
9
+ * set to auto or scroll, with any variant prefix, `!`, or as an arbitrary
10
+ * property) in a string or template literal that is a class: the value of a
11
+ * `className`, `class` or `*ClassName` JSX attribute, or an argument of
12
+ * `cn`, `clsx`, `cx`, `tv`, `cva`, `twMerge`, `twJoin` or `classNames`,
13
+ * through conditionals, `&&`, arrays, objects (keys and values) and `+`;
14
+ * - an inline `style={{ overflow | overflowX | overflowY: "auto" | "scroll" }}`.
15
+ *
16
+ * Allowed with a `raft-ui: native-scroll <reason>` comment, line or block, on
17
+ * the line above the reported string (or at the end of its line): it covers
18
+ * the string that starts on that line only, not the rest of the call or the
19
+ * attribute it is in, so a class added to a multi-line `cn(...)` later is
20
+ * still reported. For an inline style, the line of its property. A comment
21
+ * without a reason doesn't count.
22
+ *
23
+ * It looks only at literals, with no scope or type information: a class built
24
+ * in a variable outside these calls isn't seen.
25
+ */
26
+ export interface Position {
27
+ line: number;
28
+ column: number;
29
+ }
30
+ export interface Node {
31
+ type: string;
32
+ parent?: Node | null;
33
+ loc?: {
34
+ start: Position;
35
+ end: Position;
36
+ };
37
+ range?: [number, number];
38
+ [key: string]: unknown;
39
+ }
40
+ export interface Comment {
41
+ type: string;
42
+ value: string;
43
+ loc?: {
44
+ start: Position;
45
+ end: Position;
46
+ };
47
+ range?: [number, number];
48
+ }
49
+ export interface SourceCode {
50
+ getAllComments(): Comment[];
51
+ getLocFromIndex(index: number): Position;
52
+ }
53
+ export interface RuleContext {
54
+ sourceCode?: SourceCode;
55
+ getSourceCode?: () => SourceCode;
56
+ report(descriptor: {
57
+ node: Node;
58
+ messageId: string;
59
+ }): void;
60
+ }
61
+ export type RuleVisitor = Record<string, (node: Node) => void>;
62
+ /** An ESLint rule, the format Oxlint's JS plugins take. */
63
+ export interface RuleModule {
64
+ meta: {
65
+ type: "problem" | "suggestion" | "layout";
66
+ docs: {
67
+ description: string;
68
+ url: string;
69
+ };
70
+ messages: Record<string, string>;
71
+ schema: readonly unknown[];
72
+ };
73
+ create(context: RuleContext): RuleVisitor;
74
+ }
75
+ //#endregion
@@ -0,0 +1,157 @@
1
+ //#region src/lint/prefer-scroll-area.ts
2
+ const NATIVE_SCROLL_CLASS = /(?<![\w-])overflow(?:-[xy])?-(?:auto|scroll)(?![\w-])/;
3
+ const NATIVE_SCROLL_ARBITRARY = /\[overflow(?:-[xy])?:[^\]]*\b(?:auto|scroll)\b[^\]]*\]/;
4
+ const NATIVE_SCROLL_STYLE_KEYS = /* @__PURE__ */ new Set([
5
+ "overflow",
6
+ "overflowX",
7
+ "overflowY"
8
+ ]);
9
+ const NATIVE_SCROLL_STYLE_VALUES = /* @__PURE__ */ new Set(["auto", "scroll"]);
10
+ const CLASS_FUNCTIONS = /* @__PURE__ */ new Set([
11
+ "cn",
12
+ "clsx",
13
+ "cx",
14
+ "tv",
15
+ "cva",
16
+ "twMerge",
17
+ "twJoin",
18
+ "classNames",
19
+ "classnames"
20
+ ]);
21
+ const CLASS_ATTRIBUTE = /^(?:class|className|\w+ClassName)$/;
22
+ const PASS_THROUGH = /* @__PURE__ */ new Set([
23
+ "ArrayExpression",
24
+ "BinaryExpression",
25
+ "ConditionalExpression",
26
+ "JSXExpressionContainer",
27
+ "LogicalExpression",
28
+ "ObjectExpression",
29
+ "ParenthesizedExpression",
30
+ "Property",
31
+ "SpreadElement",
32
+ "TemplateLiteral",
33
+ "TSAsExpression",
34
+ "TSNonNullExpression",
35
+ "TSSatisfiesExpression"
36
+ ]);
37
+ const ESCAPE = /\braft-ui:\s*native-scroll\s+\S/;
38
+ /** Whether a class string has a native-scrolling class in it. */
39
+ function hasNativeScrollClass(value) {
40
+ if (!value.includes("overflow")) return false;
41
+ return NATIVE_SCROLL_CLASS.test(value) || NATIVE_SCROLL_ARBITRARY.test(value);
42
+ }
43
+ function attributeName(node) {
44
+ const name = node.name;
45
+ return name?.type === "JSXIdentifier" ? name.name : void 0;
46
+ }
47
+ /** The attribute or call that makes a string a class, or undefined when it isn't one. */
48
+ function classContext(node) {
49
+ let child = node;
50
+ let parent = node.parent;
51
+ while (parent) {
52
+ if (parent.type === "JSXAttribute") {
53
+ const name = attributeName(parent);
54
+ return name !== void 0 && CLASS_ATTRIBUTE.test(name) ? parent : void 0;
55
+ }
56
+ if (parent.type === "CallExpression") {
57
+ const callee = parent.callee;
58
+ return callee.type === "Identifier" && CLASS_FUNCTIONS.has(callee.name) && parent.arguments.includes(child) ? parent : void 0;
59
+ }
60
+ if (!PASS_THROUGH.has(parent.type)) return void 0;
61
+ child = parent;
62
+ parent = parent.parent;
63
+ }
64
+ }
65
+ function stringValue(node) {
66
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
67
+ if (node.type === "TemplateLiteral" && node.expressions.length === 0) {
68
+ const [quasi] = node.quasis;
69
+ const value = quasi?.value;
70
+ return value?.cooked ?? value?.raw;
71
+ }
72
+ }
73
+ /** The string literals an inline style value can be: `"auto"`, `a ? "auto" : "hidden"`, `a && "auto"`. */
74
+ function styleValueStrings(node, out) {
75
+ if (node.type === "ConditionalExpression") {
76
+ styleValueStrings(node.consequent, out);
77
+ styleValueStrings(node.alternate, out);
78
+ } else if (node.type === "LogicalExpression") {
79
+ styleValueStrings(node.left, out);
80
+ styleValueStrings(node.right, out);
81
+ } else if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression") styleValueStrings(node.expression, out);
82
+ else if (stringValue(node) !== void 0) out.push(node);
83
+ return out;
84
+ }
85
+ function propertyKey(property) {
86
+ const key = property.key;
87
+ if (property.computed) return stringValue(key);
88
+ return key.type === "Identifier" ? key.name : stringValue(key);
89
+ }
90
+ const preferScrollArea = {
91
+ meta: {
92
+ type: "suggestion",
93
+ docs: {
94
+ description: "Scrolling regions use ScrollArea from @botiverse/raft-ui, not a native overflow.",
95
+ url: "https://raft-design.botiverse.build/about/use-raft-ui/#lint-rules"
96
+ },
97
+ messages: { preferScrollArea: "Scrolling regions use ScrollArea from @botiverse/raft-ui (or PanelScrollViewport inside a Panel). If this one must scroll natively, say why in an `// raft-ui: native-scroll <reason>` comment on the line above. https://raft-design.botiverse.build/components/scroll-area/" },
98
+ schema: []
99
+ },
100
+ create(context) {
101
+ const sourceCode = context.sourceCode ?? context.getSourceCode?.();
102
+ let escapeLines;
103
+ function line(index) {
104
+ return sourceCode.getLocFromIndex(index).line;
105
+ }
106
+ /** The lines an escape comment covers: the one it ends on and the next. */
107
+ function escaped() {
108
+ if (escapeLines) return escapeLines;
109
+ escapeLines = /* @__PURE__ */ new Set();
110
+ for (const comment of sourceCode?.getAllComments() ?? []) {
111
+ if (!ESCAPE.test(comment.value)) continue;
112
+ const end = comment.loc?.end.line ?? line(comment.range[1]);
113
+ escapeLines.add(end);
114
+ escapeLines.add(end + 1);
115
+ }
116
+ return escapeLines;
117
+ }
118
+ function startLine(node) {
119
+ return node.loc?.start.line ?? line(node.range[0]);
120
+ }
121
+ /** Reports `node` unless an escape comment covers the line `anchor` starts on. */
122
+ function report(node, anchor) {
123
+ const lines = escaped();
124
+ if (lines.size > 0 && lines.has(startLine(anchor))) return;
125
+ context.report({
126
+ node,
127
+ messageId: "preferScrollArea"
128
+ });
129
+ }
130
+ function checkClassString(node, value) {
131
+ if (!hasNativeScrollClass(value)) return;
132
+ if (classContext(node)) report(node, node);
133
+ }
134
+ return {
135
+ Literal(node) {
136
+ if (typeof node.value === "string") checkClassString(node, node.value);
137
+ },
138
+ TemplateLiteral(node) {
139
+ checkClassString(node, node.quasis.map((quasi) => quasi.value.raw).join(" "));
140
+ },
141
+ JSXAttribute(node) {
142
+ if (attributeName(node) !== "style") return;
143
+ const value = node.value;
144
+ const expression = value?.type === "JSXExpressionContainer" ? value.expression : void 0;
145
+ if (expression?.type !== "ObjectExpression") return;
146
+ for (const property of expression.properties) {
147
+ if (property.type !== "Property") continue;
148
+ const key = propertyKey(property);
149
+ if (key === void 0 || !NATIVE_SCROLL_STYLE_KEYS.has(key)) continue;
150
+ for (const literal of styleValueStrings(property.value, [])) if (NATIVE_SCROLL_STYLE_VALUES.has(stringValue(literal).trim())) report(literal, property);
151
+ }
152
+ }
153
+ };
154
+ }
155
+ };
156
+ //#endregion
157
+ export { hasNativeScrollClass, preferScrollArea };
@@ -0,0 +1,6 @@
1
+ {
2
+ "jsPlugins": ["@botiverse/raft-ui/lint"],
3
+ "rules": {
4
+ "raft-ui/prefer-scroll-area": "error"
5
+ }
6
+ }
@@ -0,0 +1,22 @@
1
+ import { RuleContext, RuleModule, RuleVisitor } from "./lint/prefer-scroll-area.mjs";
2
+ //#region src/lint/index.d.ts
3
+ export interface RaftUiLintConfig {
4
+ name: string;
5
+ plugins: Record<string, RaftUiLintPlugin>;
6
+ rules: Record<string, "error" | "warn" | "off">;
7
+ }
8
+ export interface RaftUiLintPlugin {
9
+ meta: {
10
+ name: string;
11
+ };
12
+ rules: Record<string, RuleModule>;
13
+ configs: Record<string, RaftUiLintConfig>;
14
+ }
15
+ export declare const rules: Record<string, RuleModule>;
16
+ /** Every rule of the plugin with its severity, as `recommended.json` lists them. */
17
+ export declare const recommendedRules: Record<string, "error">;
18
+ declare const plugin: RaftUiLintPlugin;
19
+ /** ESLint flat config: `export default [raftUi.configs.recommended]`. */
20
+ export declare const recommended: RaftUiLintConfig;
21
+ //#endregion
22
+ export { type RuleContext, type RuleModule, type RuleVisitor, plugin as default };
package/dist/lint.mjs ADDED
@@ -0,0 +1,29 @@
1
+ import { preferScrollArea } from "./lint/prefer-scroll-area.mjs";
2
+ //#region src/lint/index.ts
3
+ /**
4
+ * raft-ui's lint plugin: the Raft Design System's rules for the code of an app
5
+ * built on raft-ui, in the ESLint plugin format, which Oxlint loads as a JS
6
+ * plugin (`jsPlugins`). Published as `@botiverse/raft-ui/lint`.
7
+ *
8
+ * Enable every rule at once by extending `recommended.json` from the same
9
+ * package (Oxlint) or adding `configs.recommended` (ESLint flat config), so a
10
+ * rule added in a later release reaches the app with the upgrade. See
11
+ * design/about/use-raft-ui.md#lint-rules.
12
+ */
13
+ const rules = { "prefer-scroll-area": preferScrollArea };
14
+ /** Every rule of the plugin with its severity, as `recommended.json` lists them. */
15
+ const recommendedRules = { "raft-ui/prefer-scroll-area": "error" };
16
+ const plugin = {
17
+ meta: { name: "raft-ui" },
18
+ rules,
19
+ configs: {}
20
+ };
21
+ /** ESLint flat config: `export default [raftUi.configs.recommended]`. */
22
+ const recommended = {
23
+ name: "raft-ui/recommended",
24
+ plugins: { "raft-ui": plugin },
25
+ rules: recommendedRules
26
+ };
27
+ plugin.configs.recommended = recommended;
28
+ //#endregion
29
+ export { plugin as default, recommended, recommendedRules, rules };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botiverse/raft-ui",
3
- "version": "0.73.0",
3
+ "version": "0.74.0",
4
4
  "description": "React components and CSS foundations for the Raft Design System.",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -46,6 +46,11 @@
46
46
  "types": "./dist/wip.d.mts",
47
47
  "default": "./dist/wip.mjs"
48
48
  },
49
+ "./lint": {
50
+ "types": "./dist/lint.d.mts",
51
+ "default": "./dist/lint.mjs"
52
+ },
53
+ "./lint/recommended.json": "./dist/lint/recommended.json",
49
54
  "./fonts.css": "./dist/fonts.css",
50
55
  "./styles.css": "./dist/styles.css",
51
56
  "./package.json": "./package.json",
@@ -90,6 +90,7 @@ When reviewing or editing code that uses raft-ui, check:
90
90
  - [ ] Menu rows are `…Item` parts, not raw `<button>`s; raw buttons drop out of keyboard navigation
91
91
  - [ ] Product state (upload, permissions, read/unread) lives in the app; the component renders parts, not a `state` prop
92
92
  - [ ] No shadow written as `shadow-[var(--…)]`: named utility or fully literal
93
+ - [ ] Every region that scrolls is a `ScrollArea` (or `PanelScrollViewport` in a Panel), not `overflow-auto` / `overflow-scroll`; a native scroller kept on purpose has a `// raft-ui: native-scroll <reason>` comment. The `raft-ui/prefer-scroll-area` lint rule checks it: https://raft-design.botiverse.build/about/use-raft-ui.md#lint-rules
93
94
 
94
95
  ## Stability
95
96
 
@@ -19,6 +19,7 @@ description: "raft-ui's technical foundations, package contents, and integration
19
19
  | `@botiverse/raft-ui/foundation.css` | Native CSS theme variables |
20
20
  | `@botiverse/raft-ui/styles.css` | Tailwind integration |
21
21
  | `@botiverse/raft-ui/fonts.css` | Optional web fonts |
22
+ | `@botiverse/raft-ui/lint` | Lint plugin with the DS rules for apps |
22
23
 
23
24
  Component declarations live in `dist/index.d.mts` (see [API lookup](./lookup.md)). Version-matched Markdown guides and optional `raft-ui-guide` / `raft-ui-critique` skills ship in `skills/`.
24
25