@botiverse/raft-ui 0.73.0 → 0.75.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 +38 -0
- package/dist/components/activity-inbox/activity-inbox-shell.recipe.mjs +1 -1
- package/dist/components/composer/composer.recipe.mjs +1 -1
- package/dist/components/description-list/description-list.d.mts +1 -1
- package/dist/components/description-list/description-list.mjs +15 -5
- package/dist/components/message-action-card/message-action-card-content.recipe.mjs +4 -4
- package/dist/components/message-action-card/message-action-card.d.mts +1 -1
- package/dist/components/message-action-card/message-action-card.mjs +13 -3
- package/dist/components/scroll-area/scroll-area.css +27 -10
- package/dist/components/scroll-area/types.d.mts +3 -1
- package/dist/components/scroll-area/use-scroll-area-container.d.mts +2 -1
- package/dist/fonts.css +2 -2
- package/dist/lib/label-value-fit.mjs +117 -0
- package/dist/lint/prefer-scroll-area.d.mts +75 -0
- package/dist/lint/prefer-scroll-area.mjs +157 -0
- package/dist/lint/recommended.json +6 -0
- package/dist/lint.d.mts +22 -0
- package/dist/lint.mjs +29 -0
- package/package.json +6 -1
- package/skills/raft-ui-guide/SKILL.md +1 -0
- package/skills/raft-ui-guide/rules/technical-overview.md +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
## [0.75.0](https://github.com/botiverse/raft-design/compare/v0.74.0...v0.75.0) (2026-11-08)
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
|
|
5
|
+
- **A horizontal DescriptionList puts its labels above their values only when a value doesn't fit**: the list measures its labels and values and keeps each label beside its value while every value fits on one line there; once one doesn't (but would on a line of its own), or the value column would be narrower than 160px, the whole list stacks. It no longer stacks below a fixed 24rem (384px), and is no longer a size container. MessageActionCard's properties follow the same rule. ([#1065](https://github.com/botiverse/raft-design/pull/1065))
|
|
6
|
+
- Why: an agent's Role, Computer and Model stacked in a 370px profile though they fit beside their labels; the switch should follow the content, not a width picked for the longest label (Artea, #triage:428e3ce6).
|
|
7
|
+
- Affects: short facts in panels from about 300px to 384px, and on a phone, now stand beside their labels. A list whose values are wide still stacks, at the width they stop fitting.
|
|
8
|
+
- Docs: https://raft-design.botiverse.build/components/description-list/#labels-go-above-only-when-a-value-doesnt-fit
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Chinese text on Linux uses Noto Sans CJK**: the Simplified Chinese fallback lists `Noto Sans CJK SC`, the name Linux distributions install it under, after `Noto Sans SC`, as the Traditional Chinese list already did with `Noto Sans CJK TC`. ([#1063](https://github.com/botiverse/raft-design/pull/1063))
|
|
13
|
+
- Why: on a Linux machine with only the distribution's CJK font, Chinese and its full-width punctuation fell to the system's last-resort face, whose colon sits centered with space around it (Remy, #proj-raft-ui:0c3f4e06; seen in the read line's screenshots from CI).
|
|
14
|
+
- Affects: Linux desktops and CI screenshots; Mac and Windows already matched PingFang SC or Microsoft YaHei first.
|
|
15
|
+
- Docs: https://raft-design.botiverse.build/foundations/typography/
|
|
16
|
+
|
|
17
|
+
## [0.74.0](https://github.com/botiverse/raft-design/compare/v0.73.0...v0.74.0) (2026-11-08)
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **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))
|
|
22
|
+
- 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).
|
|
23
|
+
- 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.
|
|
24
|
+
- Docs: https://raft-design.botiverse.build/about/use-raft-ui/#lint-rules
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- **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))
|
|
29
|
+
- 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).
|
|
30
|
+
- 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.
|
|
31
|
+
- Docs: https://raft-design.botiverse.build/components/scroll-area/#your-own-container
|
|
32
|
+
|
|
33
|
+
### Design System
|
|
34
|
+
|
|
35
|
+
- **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))
|
|
36
|
+
- Why: a long thread buries the few messages people wrote under agents' work (Artea, #proj-raft-ui:#206).
|
|
37
|
+
- Docs: https://raft-design.botiverse.build/patterns/conversation/#a-threads-timeline
|
|
38
|
+
|
|
1
39
|
## [0.73.0](https://github.com/botiverse/raft-design/compare/v0.72.0...v0.73.0) (2026-11-08)
|
|
2
40
|
|
|
3
41
|
### 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
|
|
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
|
|
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"
|
|
@@ -49,7 +49,7 @@ declare const descriptionList: import("tailwind-variants/lite").TVReturnType<{
|
|
|
49
49
|
}>>;
|
|
50
50
|
export type DescriptionListDirection = NonNullable<VariantProps<typeof descriptionList>["direction"]>;
|
|
51
51
|
export type DescriptionListProps = useRender.ComponentProps<"dl"> & Pick<VariantProps<typeof descriptionList>, "direction">;
|
|
52
|
-
export declare function DescriptionList({ direction, className, render, ...props }: DescriptionListProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
52
|
+
export declare function DescriptionList({ direction, className, render, ref, ...props }: DescriptionListProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
53
53
|
export type DescriptionItemProps = useRender.ComponentProps<"div">;
|
|
54
54
|
export declare function DescriptionItem({ className, render, ...props }: DescriptionItemProps): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
55
55
|
export type DescriptionTermProps = useRender.ComponentProps<"dt">;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { tv } from "../../lib/tv.mjs";
|
|
2
|
+
import { useLabelValueFit } from "../../lib/label-value-fit.mjs";
|
|
3
|
+
import { useCallback, useState } from "react";
|
|
2
4
|
import { mergeProps } from "@base-ui/react/merge-props";
|
|
3
5
|
import { useRender } from "@base-ui/react/use-render";
|
|
4
6
|
//#region src/components/description-list/description-list.tsx
|
|
@@ -21,10 +23,10 @@ const descriptionList = tv({
|
|
|
21
23
|
variants: { direction: {
|
|
22
24
|
vertical: {},
|
|
23
25
|
horizontal: { root: [
|
|
24
|
-
"
|
|
25
|
-
"[
|
|
26
|
-
"[
|
|
27
|
-
"[
|
|
26
|
+
"w-full grid items-baseline",
|
|
27
|
+
"[&[data-stacked]>[data-slot=description-item]>*]:col-span-full",
|
|
28
|
+
"[&[data-stacked]>[data-slot=description-item]>[data-slot=description-details]]:min-h-0",
|
|
29
|
+
"[&[data-stacked]>[data-slot=description-item]+[data-slot=description-item]>[data-slot=description-term]]:mt-2",
|
|
28
30
|
"*:data-[slot=description-item]:contents",
|
|
29
31
|
"[&>[data-slot=description-item]>[data-slot=description-term]]:mb-0",
|
|
30
32
|
"[&>[data-slot=description-item]>[data-slot=description-term]]:max-w-40",
|
|
@@ -52,12 +54,20 @@ const descriptionList = tv({
|
|
|
52
54
|
} },
|
|
53
55
|
defaultVariants: { direction: "vertical" }
|
|
54
56
|
});
|
|
55
|
-
|
|
57
|
+
const listParts = {
|
|
58
|
+
label: ":scope > [data-slot=description-item] > [data-slot=description-term]",
|
|
59
|
+
value: ":scope > [data-slot=description-item] > [data-slot=description-details]"
|
|
60
|
+
};
|
|
61
|
+
function DescriptionList({ direction = "vertical", className, render, ref, ...props }) {
|
|
62
|
+
const [list, setList] = useState(null);
|
|
63
|
+
const listRef = useCallback((element) => setList(element), []);
|
|
64
|
+
useLabelValueFit(list, direction === "horizontal", listParts);
|
|
56
65
|
const { root } = descriptionList({ direction });
|
|
57
66
|
const defaultProps = { className: root({ className }) };
|
|
58
67
|
return useRender({
|
|
59
68
|
defaultTagName: "dl",
|
|
60
69
|
render,
|
|
70
|
+
ref: ref ? [listRef, ref] : listRef,
|
|
61
71
|
props: mergeProps(defaultProps, props),
|
|
62
72
|
state: { slot: "description-list" }
|
|
63
73
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tv } from "../../lib/tv.mjs";
|
|
2
2
|
//#region src/components/message-action-card/message-action-card-content.recipe.ts
|
|
3
3
|
const messageActionCardContent = tv({ slots: {
|
|
4
|
-
root: ["p-3", "
|
|
4
|
+
root: ["p-3", "flex flex-col gap-2 px-3 pt-0 pb-3"],
|
|
5
5
|
panel: [
|
|
6
6
|
"h-(--collapsible-panel-height) overflow-hidden",
|
|
7
7
|
"transition-[height] duration-200 ease-out motion-reduce:transition-none",
|
|
@@ -9,19 +9,19 @@ const messageActionCardContent = tv({ slots: {
|
|
|
9
9
|
],
|
|
10
10
|
properties: [
|
|
11
11
|
"m-0 grid",
|
|
12
|
-
"
|
|
12
|
+
"data-stacked:grid-cols-1",
|
|
13
13
|
"grid-cols-[minmax(88px,max-content)_minmax(0,1fr)] items-baseline gap-x-4 gap-y-0.5"
|
|
14
14
|
],
|
|
15
15
|
property: ["contents"],
|
|
16
16
|
propertyLabel: [
|
|
17
17
|
"max-w-40 min-w-0 wrap-break-word",
|
|
18
18
|
"text-footnote font-medium text-foreground-hint",
|
|
19
|
-
"[[data-slot=message-action-card-property]+[data-slot=message-action-card-property]>&]
|
|
19
|
+
"[[data-stacked]>[data-slot=message-action-card-property]+[data-slot=message-action-card-property]>&]:mt-2"
|
|
20
20
|
],
|
|
21
21
|
propertyValue: [
|
|
22
22
|
"m-0 min-w-0 wrap-break-word",
|
|
23
23
|
"min-h-8 py-1.5 text-callout text-foreground",
|
|
24
|
-
"
|
|
24
|
+
"[[data-stacked]>[data-slot=message-action-card-property]>&]:min-h-0 [[data-stacked]>[data-slot=message-action-card-property]>&]:py-0"
|
|
25
25
|
],
|
|
26
26
|
scopeList: ["mt-0 stack-y-1.5"],
|
|
27
27
|
scopeListViewport: ["max-h-[136px]"],
|
|
@@ -32,7 +32,7 @@ export declare function MessageActionCardStatus({ className, appearance, ...prop
|
|
|
32
32
|
export declare function MessageActionCardCollapseTrigger({ className, ...props }: MessageActionCardCollapseTriggerProps): import("react").JSX.Element;
|
|
33
33
|
export declare function MessageActionCardHeader({ className, render, ...props }: useRender.ComponentProps<"div">): import("react").JSX.Element;
|
|
34
34
|
export declare function MessageActionCardHeaderStatus({ className, render, ...props }: useRender.ComponentProps<"span">): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
35
|
-
export declare function MessageActionCardProperties({ className, render, ...props }: useRender.ComponentProps<"dl">): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
35
|
+
export declare function MessageActionCardProperties({ className, render, ref, ...props }: useRender.ComponentProps<"dl">): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
36
36
|
export declare function MessageActionCardProperty({ className, render, ...props }: useRender.ComponentProps<"div">): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
37
37
|
export declare function MessageActionCardPropertyLabel({ className, render, ...props }: useRender.ComponentProps<"dt">): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
38
38
|
export declare function MessageActionCardPropertyValue({ className, render, ...props }: useRender.ComponentProps<"dd">): import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { ScrollArea } from "../scroll-area/scroll-area.mjs";
|
|
2
2
|
import { Badge } from "../badge/badge.mjs";
|
|
3
|
+
import { useLabelValueFit } from "../../lib/label-value-fit.mjs";
|
|
3
4
|
import { MessageEmbed, MessageEmbedContent, MessageEmbedHeader } from "../message-embed/message-embed.mjs";
|
|
4
5
|
import { messageActionCard } from "./message-action-card.recipe.mjs";
|
|
5
6
|
import { messageActionCardContent } from "./message-action-card-content.recipe.mjs";
|
|
6
7
|
import { ChevronDown, Info, Zap } from "lucide-react";
|
|
7
|
-
import {
|
|
8
|
+
import { useCallback, useState } from "react";
|
|
9
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
10
|
import { mergeProps } from "@base-ui/react/merge-props";
|
|
9
11
|
import { useRender } from "@base-ui/react/use-render";
|
|
10
12
|
import { Collapsible } from "@base-ui/react/collapsible";
|
|
@@ -127,11 +129,19 @@ function MessageActionCardHeaderStatus({ className, render, ...props }) {
|
|
|
127
129
|
state: { slot: "message-action-card-header-status" }
|
|
128
130
|
});
|
|
129
131
|
}
|
|
130
|
-
|
|
132
|
+
const propertyParts = {
|
|
133
|
+
label: ":scope > [data-slot=message-action-card-property] > [data-slot=message-action-card-property-label]",
|
|
134
|
+
value: ":scope > [data-slot=message-action-card-property] > [data-slot=message-action-card-property-value]"
|
|
135
|
+
};
|
|
136
|
+
function MessageActionCardProperties({ className, render, ref, ...props }) {
|
|
137
|
+
const [list, setList] = useState(null);
|
|
138
|
+
const listRef = useCallback((element) => setList(element), []);
|
|
139
|
+
useLabelValueFit(list, true, propertyParts);
|
|
131
140
|
const { properties } = messageActionCardContent();
|
|
132
141
|
return useRender({
|
|
133
142
|
defaultTagName: "dl",
|
|
134
143
|
render,
|
|
144
|
+
ref: ref ? [listRef, ref] : listRef,
|
|
135
145
|
props: mergeProps({ className: properties({ className }) }, props),
|
|
136
146
|
state: { slot: "message-action-card-properties" }
|
|
137
147
|
});
|
|
@@ -249,7 +259,7 @@ function MessageActionCardNotice({ className, render, icon, children, ...props }
|
|
|
249
259
|
render,
|
|
250
260
|
props: mergeProps({
|
|
251
261
|
className: notice({ className }),
|
|
252
|
-
children: /* @__PURE__ */ jsxs(Fragment, { children: [icon ?? /* @__PURE__ */ jsx(Info, { "aria-hidden": true }), /* @__PURE__ */ jsx("span", { children })] })
|
|
262
|
+
children: /* @__PURE__ */ jsxs(Fragment$1, { children: [icon ?? /* @__PURE__ */ jsx(Info, { "aria-hidden": true }), /* @__PURE__ */ jsx("span", { children })] })
|
|
253
263
|
}, props),
|
|
254
264
|
state: { slot: "message-action-card-notice" }
|
|
255
265
|
});
|
|
@@ -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
|
|
28
|
-
*
|
|
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
|
|
220
|
-
*
|
|
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
|
-
/*
|
|
246
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
*
|
package/dist/fonts.css
CHANGED
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
:where(:root) {
|
|
12
12
|
--heading-font:
|
|
13
13
|
"Space Grotesk", Arial, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
|
|
14
|
-
"Noto Sans SC", system-ui, sans-serif;
|
|
14
|
+
"Noto Sans SC", "Noto Sans CJK SC", system-ui, sans-serif;
|
|
15
15
|
--sans-font:
|
|
16
16
|
"Space Grotesk", Arial, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
|
|
17
|
-
"Noto Sans SC", system-ui, sans-serif;
|
|
17
|
+
"Noto Sans SC", "Noto Sans CJK SC", system-ui, sans-serif;
|
|
18
18
|
--mono-font: "Space Mono", ui-monospace, "SF Mono", Menlo, monospace;
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useLayoutEffect } from "react";
|
|
2
|
+
//#region src/lib/label-value-fit.ts
|
|
3
|
+
/** The narrowest the label column gets: `minmax(88px, max-content)` in both lists. */
|
|
4
|
+
const minLabelColumn = 88;
|
|
5
|
+
/**
|
|
6
|
+
* The narrowest the value column may be beside its labels: as wide as a label
|
|
7
|
+
* may grow (`max-w-40`). A value column narrower than its label column reads
|
|
8
|
+
* as the label's footnote, not its answer.
|
|
9
|
+
*/
|
|
10
|
+
const minValueColumn = 160;
|
|
11
|
+
/**
|
|
12
|
+
* Reads every element's width with nothing wrapping inside it: all of them
|
|
13
|
+
* go to one-line layout, are read in one pass and get their inline style
|
|
14
|
+
* back, so the list is laid out once for the read, not once per element.
|
|
15
|
+
*/
|
|
16
|
+
function oneLineWidths(els) {
|
|
17
|
+
const saved = els.map(({ style }) => [
|
|
18
|
+
style.width,
|
|
19
|
+
style.maxWidth,
|
|
20
|
+
style.whiteSpace,
|
|
21
|
+
style.flexWrap
|
|
22
|
+
]);
|
|
23
|
+
for (const { style } of els) {
|
|
24
|
+
style.width = "max-content";
|
|
25
|
+
style.maxWidth = "none";
|
|
26
|
+
style.whiteSpace = "nowrap";
|
|
27
|
+
style.flexWrap = "nowrap";
|
|
28
|
+
}
|
|
29
|
+
const widths = els.map((el) => el.getBoundingClientRect().width);
|
|
30
|
+
els.forEach(({ style }, i) => {
|
|
31
|
+
[style.width, style.maxWidth, style.whiteSpace, style.flexWrap] = saved[i];
|
|
32
|
+
});
|
|
33
|
+
return widths;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Whether a two-column label/value list has to put its labels above their
|
|
37
|
+
* values: some value doesn't fit on one line beside the label column, but
|
|
38
|
+
* would on a line of its own, or the value column would be narrower than
|
|
39
|
+
* `minValueColumn`. A value too long for a line of its own (a paragraph)
|
|
40
|
+
* wraps either way and doesn't decide. Measured in one-line layout whatever
|
|
41
|
+
* the list shows now, so the answer depends only on the list's width and its
|
|
42
|
+
* content and can't flap at the boundary.
|
|
43
|
+
*/
|
|
44
|
+
function labelsGoAbove(list, parts) {
|
|
45
|
+
const style = getComputedStyle(list);
|
|
46
|
+
const width = list.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
|
|
47
|
+
const gap = parseFloat(style.columnGap) || 0;
|
|
48
|
+
const labels = Array.from(list.querySelectorAll(parts.label));
|
|
49
|
+
const values = Array.from(list.querySelectorAll(parts.value));
|
|
50
|
+
const caps = labels.map((label) => parseFloat(getComputedStyle(label).maxWidth));
|
|
51
|
+
const widths = oneLineWidths([...labels, ...values]);
|
|
52
|
+
let labelColumn = minLabelColumn;
|
|
53
|
+
labels.forEach((_, i) => {
|
|
54
|
+
const natural = widths[i];
|
|
55
|
+
const cap = caps[i];
|
|
56
|
+
labelColumn = Math.max(labelColumn, Number.isNaN(cap) ? natural : Math.min(natural, cap));
|
|
57
|
+
});
|
|
58
|
+
const valueColumn = width - labelColumn - gap;
|
|
59
|
+
if (valueColumn < minValueColumn) return true;
|
|
60
|
+
for (const natural of widths.slice(labels.length)) if (natural - valueColumn > .5 && natural - width <= .5) return true;
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Sets `data-stacked` on a two-column label/value list while its labels have
|
|
65
|
+
* to go above their values (`labelsGoAbove`), and takes it off when they fit
|
|
66
|
+
* beside them again: the list's CSS does the rest. The decision is taken
|
|
67
|
+
* synchronously, in the layout effect and the observers' callbacks, so the
|
|
68
|
+
* first frame and every resize paint the right layout; again when the list
|
|
69
|
+
* or a value resizes, a label or value changes, or a web font arrives
|
|
70
|
+
* (design/patterns/responsive.md#when-a-row-runs-out-of-room).
|
|
71
|
+
*/
|
|
72
|
+
function useLabelValueFit(list, enabled, parts) {
|
|
73
|
+
const { label, value } = parts;
|
|
74
|
+
useLayoutEffect(() => {
|
|
75
|
+
if (!list || !enabled || typeof ResizeObserver === "undefined") return void 0;
|
|
76
|
+
const selectors = {
|
|
77
|
+
label,
|
|
78
|
+
value
|
|
79
|
+
};
|
|
80
|
+
const fit = () => {
|
|
81
|
+
const stacked = labelsGoAbove(list, selectors);
|
|
82
|
+
if (stacked !== list.hasAttribute("data-stacked")) list.toggleAttribute("data-stacked", stacked);
|
|
83
|
+
};
|
|
84
|
+
const resize = new ResizeObserver(fit);
|
|
85
|
+
const observeAll = () => {
|
|
86
|
+
resize.disconnect();
|
|
87
|
+
resize.observe(list);
|
|
88
|
+
for (const el of Array.from(list.querySelectorAll(`${label}, ${value}`))) for (const child of Array.from(el.children)) resize.observe(child);
|
|
89
|
+
};
|
|
90
|
+
observeAll();
|
|
91
|
+
const mutation = new MutationObserver(() => {
|
|
92
|
+
observeAll();
|
|
93
|
+
fit();
|
|
94
|
+
});
|
|
95
|
+
mutation.observe(list, {
|
|
96
|
+
childList: true,
|
|
97
|
+
subtree: true,
|
|
98
|
+
characterData: true
|
|
99
|
+
});
|
|
100
|
+
const fonts = typeof document === "undefined" ? void 0 : document.fonts;
|
|
101
|
+
fonts?.addEventListener("loadingdone", fit);
|
|
102
|
+
fit();
|
|
103
|
+
return () => {
|
|
104
|
+
resize.disconnect();
|
|
105
|
+
mutation.disconnect();
|
|
106
|
+
fonts?.removeEventListener("loadingdone", fit);
|
|
107
|
+
list.removeAttribute("data-stacked");
|
|
108
|
+
};
|
|
109
|
+
}, [
|
|
110
|
+
list,
|
|
111
|
+
enabled,
|
|
112
|
+
label,
|
|
113
|
+
value
|
|
114
|
+
]);
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
export { labelsGoAbove, useLabelValueFit };
|
|
@@ -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 };
|
package/dist/lint.d.mts
ADDED
|
@@ -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.
|
|
3
|
+
"version": "0.75.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
|
|