@botiverse/raft-ui 0.95.4 → 0.96.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 +126 -98
- package/dist/components/combobox/combobox.mjs +1 -1
- package/dist/components/combobox-select/combobox-select.mjs +1 -1
- package/dist/components/context-menu/context-menu.mjs +1 -1
- package/dist/components/dialog/dialog-sheet-context.mjs +4 -2
- package/dist/components/dialog/dialog-sheet.mjs +9 -1
- package/dist/components/dialog/dialog.mjs +22 -10
- package/dist/components/drawer/drawer.d.mts +1 -1
- package/dist/components/drawer/drawer.mjs +17 -4
- package/dist/components/dropdown-menu/dropdown-menu.mjs +1 -1
- package/dist/components/lightbox/lightbox.d.mts +1 -1
- package/dist/components/lightbox/lightbox.mjs +11 -9
- package/dist/components/notification-center/notification-center.mjs +1 -1
- package/dist/components/popover/popover.mjs +1 -1
- package/dist/components/preview-card/preview-card.mjs +1 -1
- package/dist/components/select/select.mjs +1 -1
- package/dist/components/tooltip/tooltip.mjs +1 -1
- package/dist/lib/frame-insets.mjs +56 -12
- package/dist/lib/overlay-depth.d.mts +5 -0
- package/dist/lib/overlay-depth.mjs +30 -11
- package/dist/lib/popup-boundary.d.mts +24 -0
- package/dist/lib/popup-boundary.mjs +256 -0
- package/dist/lint/no-raw-z-index.mjs +4 -4
- package/dist/lint/prefer-scroll-area.d.mts +60 -19
- package/dist/lint/prefer-scroll-area.mjs +393 -38
- package/dist/wip-exports.d.mts +5 -4
- package/dist/wip-exports.mjs +4 -3
- package/dist/wip.d.mts +2 -1
- package/package.json +1 -1
- package/skills/raft-ui-guide/SKILL.md +3 -1
- package/skills/raft-ui-guide/rules/layout.md +1 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { createContext, useContext, useMemo } from "react";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
// src/lib/popup-boundary.tsx
|
|
4
|
+
/** WebKit, by floating-ui's own test (`isWebKit` in @floating-ui/utils). */
|
|
5
|
+
function isWebKit() {
|
|
6
|
+
return typeof CSS !== "undefined" && CSS.supports?.("-webkit-backdrop-filter", "none") === true;
|
|
7
|
+
}
|
|
8
|
+
const NOT_NONE = (value) => !!value && value !== "none";
|
|
9
|
+
const WILL_CHANGE = /transform|translate|scale|rotate|perspective|filter/;
|
|
10
|
+
const CONTAIN = /paint|layout|strict|content/;
|
|
11
|
+
/**
|
|
12
|
+
* Whether floating-ui takes the window as `popup`'s offset parent: it is
|
|
13
|
+
* `position: fixed` and no ancestor is a containing block for fixed boxes
|
|
14
|
+
* (`getOffsetParent` and `isContainingBlock` in @floating-ui 1.8, as they
|
|
15
|
+
* read in WebKit, where a filter doesn't count). With no popup to look at,
|
|
16
|
+
* yes: a raft-ui popup is fixed and portaled beside its dialog or to the body.
|
|
17
|
+
*/
|
|
18
|
+
function offsetParentIsWindow(popup) {
|
|
19
|
+
if (!popup) return true;
|
|
20
|
+
if (getComputedStyle(popup).position !== "fixed") return false;
|
|
21
|
+
for (let node = popup.parentElement; node; node = node.parentElement) {
|
|
22
|
+
const css = getComputedStyle(node);
|
|
23
|
+
if (NOT_NONE(css.transform) || NOT_NONE(css.translate) || NOT_NONE(css.scale) || NOT_NONE(css.rotate) || NOT_NONE(css.perspective) || WILL_CHANGE.test(css.willChange || "") || CONTAIN.test(css.contain || "")) return false;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* How far to move a boundary rect so floating-ui (@floating-ui/dom 1.8, under
|
|
29
|
+
* Base UI) reads it where the window draws it: 0 outside WebKit, and in WebKit
|
|
30
|
+
* the visual viewport's offset, twice for a fixed popup whose offset parent is
|
|
31
|
+
* the window and once for any other.
|
|
32
|
+
*
|
|
33
|
+
* In WebKit (floating-ui's own test: `-webkit-backdrop-filter` is supported)
|
|
34
|
+
* the visual viewport has an offset while the page is panned under it: the
|
|
35
|
+
* on-screen keyboard is up on iOS, or the page is pinch-zoomed. floating-ui
|
|
36
|
+
* SUBTRACTS that offset from a boundary given as a rect
|
|
37
|
+
* (`getClientRectFromClippingAncestor`), whatever the popup. For a
|
|
38
|
+
* `position: fixed` popup whose offset parent is the window, which every
|
|
39
|
+
* raft-ui popup is by default, it also ADDS the offset to the trigger's rect
|
|
40
|
+
* (`getBoundingClientRect` with `shouldAddVisualOffsets`). A rect in window
|
|
41
|
+
* coordinates therefore lands twice the offset away from the trigger it
|
|
42
|
+
* bounds: with the keyboard up, a list bounded under a sheet's head ran under
|
|
43
|
+
* the head again. For a popup with `positionMethod="absolute"`, or one
|
|
44
|
+
* portaled into an element that is a containing block for fixed boxes (a
|
|
45
|
+
* drawer's popup, which is transformed), the trigger gets no offset and the
|
|
46
|
+
* rect is off by once. An element as the boundary gets no offset at all, so it
|
|
47
|
+
* is no way around this.
|
|
48
|
+
*
|
|
49
|
+
* `popup` is the positioner being placed; without one the popup is taken to be
|
|
50
|
+
* fixed to the window.
|
|
51
|
+
*/
|
|
52
|
+
function visualViewportShift(popup) {
|
|
53
|
+
if (typeof window === "undefined") return {
|
|
54
|
+
x: 0,
|
|
55
|
+
y: 0
|
|
56
|
+
};
|
|
57
|
+
const viewport = window.visualViewport;
|
|
58
|
+
if (!viewport || !isWebKit()) return {
|
|
59
|
+
x: 0,
|
|
60
|
+
y: 0
|
|
61
|
+
};
|
|
62
|
+
if (viewport.offsetLeft === 0 && viewport.offsetTop === 0) return {
|
|
63
|
+
x: 0,
|
|
64
|
+
y: 0
|
|
65
|
+
};
|
|
66
|
+
const times = offsetParentIsWindow(popup) ? 2 : 1;
|
|
67
|
+
return {
|
|
68
|
+
x: times * viewport.offsetLeft,
|
|
69
|
+
y: times * viewport.offsetTop
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The element an open popup is anchored to, found from its positioner, or
|
|
74
|
+
* `null` when it can't be told. Base UI keeps this in each primitive's own
|
|
75
|
+
* store, so it is read from the DOM Base UI renders:
|
|
76
|
+
*
|
|
77
|
+
* - a menu, a submenu, a popover, a select or a combobox: the open trigger
|
|
78
|
+
* (`data-popup-open`) whose `aria-controls` names an element inside the
|
|
79
|
+
* positioner;
|
|
80
|
+
* - a tooltip or a preview card, which nothing names: the open trigger of its
|
|
81
|
+
* kind, when exactly one is open.
|
|
82
|
+
*
|
|
83
|
+
* A context menu is anchored to a point and has none. An `anchor` passed to
|
|
84
|
+
* the popup isn't read here: sheetPopupBoundary takes it before this lookup.
|
|
85
|
+
*/
|
|
86
|
+
function findPopupAnchor(positioner) {
|
|
87
|
+
if (!positioner) return null;
|
|
88
|
+
const root = positioner.ownerDocument;
|
|
89
|
+
for (const trigger of root.querySelectorAll("[data-popup-open][aria-controls]")) {
|
|
90
|
+
const controlled = root.getElementById(trigger.getAttribute("aria-controls") ?? "");
|
|
91
|
+
if (controlled && positioner.contains(controlled)) return trigger;
|
|
92
|
+
}
|
|
93
|
+
const unnamed = positioner.querySelector("[data-slot=\"tooltip-content\"]") ? "[data-base-ui-tooltip-trigger][data-popup-open]" : positioner.querySelector("[data-slot=\"preview-card-content\"]") ? "[data-slot=\"preview-card-trigger\"][data-popup-open]" : null;
|
|
94
|
+
if (!unnamed) return null;
|
|
95
|
+
const open = root.querySelectorAll(unnamed);
|
|
96
|
+
return open.length === 1 ? open[0] : null;
|
|
97
|
+
}
|
|
98
|
+
const UNBOUNDED = 1e6;
|
|
99
|
+
const isElement = (value) => typeof value.nodeType === "number";
|
|
100
|
+
/** The element or virtual element an `anchor` prop stands for now, or `null`: an empty ref, or none passed. */
|
|
101
|
+
function resolvePopupAnchor(anchor) {
|
|
102
|
+
const value = typeof anchor === "function" ? anchor() : anchor;
|
|
103
|
+
if (!value) return null;
|
|
104
|
+
if (!isElement(value) && "current" in value) return value.current;
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
/** Runs `clear` before the next frame is drawn, or as soon as the caller is done where there are no frames. */
|
|
108
|
+
function nextFrame(clear) {
|
|
109
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(clear);
|
|
110
|
+
else queueMicrotask(clear);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The part of a sheet a popup opened in it may cover: from the bottom of the
|
|
114
|
+
* sheet's head to the bottom of what is visible of the sheet (its own bottom
|
|
115
|
+
* edge, or the visual viewport's when the on-screen keyboard covers it), the
|
|
116
|
+
* window's full width. A list bounded by it flips and shrinks under the head
|
|
117
|
+
* instead of opening over it and past the sheet's top edge.
|
|
118
|
+
*
|
|
119
|
+
* Decided for each popup, each time Base UI places it (it copies the rect's
|
|
120
|
+
* fields, which are getters), so the rect follows the sheet as the keyboard
|
|
121
|
+
* resizes it. The sheet bounds nothing, and the popup keeps to the window as
|
|
122
|
+
* outside a sheet, when:
|
|
123
|
+
*
|
|
124
|
+
* - the popup's anchor is inside the head (a tooltip or a menu on a head
|
|
125
|
+
* button belongs above or over the head, not under it);
|
|
126
|
+
* - neither side of the anchor has `POPUP_ROOM_MIN_PX` of the area: a list
|
|
127
|
+
* held there would show a row or two, so it opens over the head instead;
|
|
128
|
+
* - the sheet has no head, or nothing visible under it.
|
|
129
|
+
*
|
|
130
|
+
* The anchor is the popup's own `anchor` prop when `anchor` returns one that
|
|
131
|
+
* resolves to something (resolvePopupAnchor), and the trigger found in the
|
|
132
|
+
* page otherwise (findPopupAnchor). A virtual anchor has no element, so it is
|
|
133
|
+
* inside the head when the middle of its rect is. A popup whose anchor can't
|
|
134
|
+
* be found stays bounded.
|
|
135
|
+
*
|
|
136
|
+
* Floating-ui reads `y` and `height` one after the other, several times in
|
|
137
|
+
* one placement (once for each of flip, shift, size and hide). The two reads
|
|
138
|
+
* share one measurement: it is kept until the caller yields (a microtask), so
|
|
139
|
+
* a sheet that resized or a keyboard that came up is measured again by the
|
|
140
|
+
* next read. The trigger found in the page is kept longer, until the next
|
|
141
|
+
* frame, for the same positioner and while it is still in the page and open:
|
|
142
|
+
* which element a popup is anchored to doesn't change with layout, and finding
|
|
143
|
+
* it is a query over the whole document. A lookup that found nothing isn't
|
|
144
|
+
* kept.
|
|
145
|
+
*
|
|
146
|
+
* The rect is moved by `visualViewportShift`, so it is in the coordinates
|
|
147
|
+
* floating-ui reads it in, not the window's.
|
|
148
|
+
*/
|
|
149
|
+
function sheetPopupBoundary(sheet, popup = () => null, anchor = () => void 0) {
|
|
150
|
+
let found = null;
|
|
151
|
+
const lookUp = (positioner) => {
|
|
152
|
+
if (found && found.positioner === positioner && found.anchor.isConnected && found.anchor.hasAttribute("data-popup-open")) return found.anchor;
|
|
153
|
+
found = null;
|
|
154
|
+
const trigger = findPopupAnchor(positioner);
|
|
155
|
+
if (trigger && positioner) {
|
|
156
|
+
found = {
|
|
157
|
+
positioner,
|
|
158
|
+
anchor: trigger
|
|
159
|
+
};
|
|
160
|
+
nextFrame(() => {
|
|
161
|
+
found = null;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return trigger;
|
|
165
|
+
};
|
|
166
|
+
const measure = () => {
|
|
167
|
+
const head = sheet.head();
|
|
168
|
+
const headBox = head?.getBoundingClientRect();
|
|
169
|
+
const surfaceBox = sheet.surface()?.getBoundingClientRect();
|
|
170
|
+
if (!head || !headBox || !surfaceBox) return null;
|
|
171
|
+
const viewport = window.visualViewport;
|
|
172
|
+
const top = headBox.bottom;
|
|
173
|
+
const bottom = viewport ? Math.min(surfaceBox.bottom, viewport.offsetTop + viewport.height) : surfaceBox.bottom;
|
|
174
|
+
if (bottom <= top) return null;
|
|
175
|
+
const positioner = popup();
|
|
176
|
+
const target = resolvePopupAnchor(anchor()) ?? lookUp(positioner);
|
|
177
|
+
if (target) {
|
|
178
|
+
const anchorBox = target.getBoundingClientRect();
|
|
179
|
+
if (isElement(target)) {
|
|
180
|
+
if (head.contains(target)) return null;
|
|
181
|
+
} else {
|
|
182
|
+
const x = (anchorBox.left + anchorBox.right) / 2;
|
|
183
|
+
const y = (anchorBox.top + anchorBox.bottom) / 2;
|
|
184
|
+
if (x >= headBox.left && x <= headBox.right && y >= headBox.top && y <= headBox.bottom) return null;
|
|
185
|
+
}
|
|
186
|
+
const above = Math.min(anchorBox.top, bottom) - top;
|
|
187
|
+
const below = bottom - Math.max(anchorBox.bottom, top);
|
|
188
|
+
if (Math.max(above, below) < 150) return null;
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
top: top + visualViewportShift(positioner).y,
|
|
192
|
+
height: bottom - top
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
let measured = null;
|
|
196
|
+
const read = () => {
|
|
197
|
+
if (!measured) {
|
|
198
|
+
measured = { value: measure() };
|
|
199
|
+
queueMicrotask(() => {
|
|
200
|
+
measured = null;
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return measured.value;
|
|
204
|
+
};
|
|
205
|
+
return {
|
|
206
|
+
x: -1e6,
|
|
207
|
+
width: 2 * UNBOUNDED,
|
|
208
|
+
get y() {
|
|
209
|
+
return read()?.top ?? -1e6;
|
|
210
|
+
},
|
|
211
|
+
get height() {
|
|
212
|
+
return read()?.height ?? 2 * UNBOUNDED;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* The sheet around a popup, or `undefined` outside one. Set by a phone sheet
|
|
218
|
+
* (`DialogSheetContent`), a `Drawer` and `SheetPopupBoundary`; every modal
|
|
219
|
+
* surface (a dialog, an alert dialog, a drawer, a lightbox, an app's own under
|
|
220
|
+
* `OverlayLayerProvider`) and every sheet's head start without one.
|
|
221
|
+
*/
|
|
222
|
+
const PopupSheetContext = createContext(void 0);
|
|
223
|
+
/** The sheet a popup is in, whose area it takes when its caller passes no `collisionBoundary`. */
|
|
224
|
+
function usePopupSheet() {
|
|
225
|
+
return useContext(PopupSheetContext);
|
|
226
|
+
}
|
|
227
|
+
/** Starts its children outside any sheet: a new surface, or a sheet's head. */
|
|
228
|
+
function PopupSheetReset({ children }) {
|
|
229
|
+
return /* @__PURE__ */ jsx(PopupSheetContext.Provider, {
|
|
230
|
+
value: void 0,
|
|
231
|
+
children
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* For a sheet an app draws itself (its own surface, a head that stays put and
|
|
236
|
+
* a body that scrolls): every raft-ui popup opened under it (Select, Combobox,
|
|
237
|
+
* ComboboxSelect, DropdownMenu, ContextMenu, Popover, PreviewCard, Tooltip,
|
|
238
|
+
* NotificationCenter) flips and sizes inside the sheet, under its head
|
|
239
|
+
* (sheetPopupBoundary says when it doesn't). Wrap the sheet's body in it, or
|
|
240
|
+
* the head and the body: a popup anchored in the head is left alone either
|
|
241
|
+
* way. Put it inside the sheet's own `OverlayLayerProvider`, which starts
|
|
242
|
+
* without one. raft-ui's own sheets, `DialogSheetContent` on a phone and
|
|
243
|
+
* `Drawer`, do this by themselves, and a `collisionBoundary` passed to a popup
|
|
244
|
+
* still wins. It renders no element.
|
|
245
|
+
*/
|
|
246
|
+
function SheetPopupBoundary({ head, surface, children }) {
|
|
247
|
+
const sheet = useMemo(() => ({
|
|
248
|
+
head: () => head.current,
|
|
249
|
+
surface: () => surface ? surface.current : head.current?.parentElement
|
|
250
|
+
}), [head, surface]);
|
|
251
|
+
return /* @__PURE__ */ jsx(PopupSheetContext.Provider, {
|
|
252
|
+
value: sheet,
|
|
253
|
+
children
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
export { PopupSheetContext, PopupSheetReset, SheetPopupBoundary, findPopupAnchor, resolvePopupAnchor, sheetPopupBoundary, usePopupSheet, visualViewportShift };
|
|
@@ -9,10 +9,10 @@ import { classContext, constDeclarator, propertyKey, stringValue } from "./prefe
|
|
|
9
9
|
* Reported:
|
|
10
10
|
* - a Tailwind z-index class with a number or an arbitrary value (`z-10`,
|
|
11
11
|
* `-z-1`, `z-[60]`, `z-(--my-z)`, `[z-index:5]`, with any variant prefix or
|
|
12
|
-
* `!`) in a string or template literal that is a class
|
|
13
|
-
* `
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* `!`) in a string or template literal that is a class: a `className`,
|
|
13
|
+
* `class` or `*ClassName` attribute or property, an argument of `cn`, `tv`
|
|
14
|
+
* and the like, or a `const` of the same file used as a class (not the
|
|
15
|
+
* wider set `raft-ui/prefer-scroll-area` reads). `z-auto` and the
|
|
16
16
|
* named layers (`z-raised`, `max-md:z-chrome`) are not reported;
|
|
17
17
|
* - a `zIndex` property whose value is a number or a numeric string
|
|
18
18
|
* (`style={{ zIndex: 10 }}`, `const s = { zIndex: open ? 20 : "5" }`), a
|
|
@@ -5,28 +5,56 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Reported:
|
|
7
7
|
* - a native-scrolling Tailwind class (`overflow`, `overflow-x` or `overflow-y`
|
|
8
|
-
* set to auto or scroll, with any variant prefix, `!`,
|
|
9
|
-
*
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* set to auto or scroll, with any variant prefix, `!`, an arbitrary value
|
|
9
|
+
* `overflow-y-[scroll]`, `overflow-[hidden_auto]` or an arbitrary property
|
|
10
|
+
* `[overflow:overlay]`) in a string or template literal that is a class:
|
|
11
|
+
* - the value of a JSX attribute with a class name (`className`, `class`,
|
|
12
|
+
* `classNames`, `*ClassName`, `*ClassNames`, `*Class`), a property with
|
|
13
|
+
* such a name (a recipe slot's option, `content({ className: "…" })`) or
|
|
14
|
+
* the default of a parameter with one (`{ className = "…" }`), through
|
|
15
|
+
* conditionals, `&&`, arrays, objects, members, `+`, a method called on
|
|
16
|
+
* it (`[…].join(" ")`), a function that returns it (Base UI's
|
|
17
|
+
* `className={(state) => …}`) and one call that isn't a class helper
|
|
18
|
+
* (`className={wrap("…")}`);
|
|
19
|
+
* - an argument of a class helper (`cn`, `clsx`, `cx`, `tv`, `cva`,
|
|
20
|
+
* `twMerge`, `twJoin`, `classNames`), through the same, except a call
|
|
21
|
+
* that isn't a class helper: what `t("…")` takes inside `cn(…)` is its
|
|
22
|
+
* own, in a class attribute or outside one;
|
|
23
|
+
* - an argument of the call a JSX spread spreads (`{...guard("…")}`), but
|
|
24
|
+
* not under a key that isn't a class name, nor in a function;
|
|
15
25
|
* - such a string in a `const` of the same file that is used as a class
|
|
16
|
-
* (`const log = "max-h-64 …"`, then `className={log}` or
|
|
17
|
-
*
|
|
18
|
-
*
|
|
26
|
+
* (`const log = "max-h-64 …"`, then `className={log}` or `cn(log)`),
|
|
27
|
+
* wherever in the file it is declared: a string, a list joined into one, a
|
|
28
|
+
* const built from another (`const b = \`p-2 ${a}\``), or an object or
|
|
29
|
+
* variant map, of which only the members read as a class count
|
|
30
|
+
* (`cls.body`), all of them when it is read by a computed key
|
|
31
|
+
* (`VARIANTS[kind]`) or used whole;
|
|
32
|
+
* - a `style` whose `overflow`, `overflowX` or `overflowY` has `auto`,
|
|
33
|
+
* `scroll` or `overlay` in it (`"hidden auto"` too): in a `style` attribute,
|
|
34
|
+
* under a `style` key (`{...{ style: { … } }}`), or an object in a `const`
|
|
35
|
+
* of the same file that a `style` uses (`style={s}`, `style={styles.pre}`).
|
|
19
36
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* without a reason doesn't count.
|
|
37
|
+
* The message follows the direction (down, sideways, both ways) and the
|
|
38
|
+
* element the hit is on: a `<textarea>` gets ScrollAreaTextarea; `<pre>`,
|
|
39
|
+
* `<html>`, `<body>` and `<iframe>` get no replacement, only the comment; a
|
|
40
|
+
* `classNames` object, whose classes go to another component's elements,
|
|
41
|
+
* gets `useScrollAreaContainer`; a ScrollArea's own class is to be removed.
|
|
26
42
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
43
|
+
* Allowed with a `raft-ui: native-scroll <reason>` comment, line or block:
|
|
44
|
+
* on a line of its own right above the reported string, or at the end of
|
|
45
|
+
* the string's own line. It covers the string that starts on that line only,
|
|
46
|
+
* not the rest of the call or the attribute it is in, so a class added to a
|
|
47
|
+
* multi-line `cn(...)` later is still reported; a comment at the end of a
|
|
48
|
+
* line doesn't reach the next line. For a style, the line of its property. A
|
|
49
|
+
* comment without a reason doesn't count.
|
|
50
|
+
*
|
|
51
|
+
* A const is resolved by scope where the linter gives one (a parameter of
|
|
52
|
+
* the same name is not the const), and by its name in the file otherwise.
|
|
53
|
+
*
|
|
54
|
+
* Known misses: a class added by script (`el.classList.add(…)`,
|
|
55
|
+
* `el.className = …`), a style set by script (`el.style.overflowY = "auto"`),
|
|
56
|
+
* a const or object imported from another file, a `let`, a string a function
|
|
57
|
+
* returns to a const, and CSS files.
|
|
30
58
|
*/
|
|
31
59
|
export interface Position {
|
|
32
60
|
line: number;
|
|
@@ -51,9 +79,22 @@ export interface Comment {
|
|
|
51
79
|
};
|
|
52
80
|
range?: [number, number];
|
|
53
81
|
}
|
|
82
|
+
/** The part of a scope this rule reads, to find the declaration a name refers to. */
|
|
83
|
+
export interface Scope {
|
|
84
|
+
set?: Map<string, {
|
|
85
|
+
defs?: {
|
|
86
|
+
type: string;
|
|
87
|
+
node: Node;
|
|
88
|
+
parent?: Node | null;
|
|
89
|
+
}[];
|
|
90
|
+
}>;
|
|
91
|
+
upper?: Scope | null;
|
|
92
|
+
}
|
|
54
93
|
export interface SourceCode {
|
|
94
|
+
text?: string;
|
|
55
95
|
getAllComments(): Comment[];
|
|
56
96
|
getLocFromIndex(index: number): Position;
|
|
97
|
+
getScope?(node: Node): Scope | null;
|
|
57
98
|
}
|
|
58
99
|
export interface RuleContext {
|
|
59
100
|
sourceCode?: SourceCode;
|