@siyahi/tools 0.1.9-next.32 → 0.1.9-next.34
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/README.md +10 -1
- package/contrast.mjs +19 -3
- package/lint/index.mjs +99 -12
- package/package.json +1 -1
- package/types/lint/index.d.mts +1 -1
package/README.md
CHANGED
|
@@ -49,6 +49,14 @@ flex-col gap-4">`, `<TableCell><span className="font-mono">`,
|
|
|
49
49
|
A Vite+ or ESLint config can spread every rule at once:
|
|
50
50
|
`import { recommended } from "@siyahi/tools/lint"`.
|
|
51
51
|
|
|
52
|
+
One more rule is for a component library's own types rather than for pages, so
|
|
53
|
+
it runs in every file it is turned on for and is not in `recommended`:
|
|
54
|
+
`siyahi/optional-accepts-undefined` rejects an optional property that does not
|
|
55
|
+
take undefined, such as `tone?: Tone`, and `--fix` writes
|
|
56
|
+
`tone?: Tone | undefined`. Under TypeScript's `exactOptionalPropertyTypes` a
|
|
57
|
+
page can only pass `tone: loss ? "negative" : undefined` to the second.
|
|
58
|
+
Siyahi runs it on its own package.
|
|
59
|
+
|
|
52
60
|
## Font preload
|
|
53
61
|
|
|
54
62
|
A Vite plugin that adds `<link rel="preload" as="font">` tags to the built
|
|
@@ -170,6 +178,7 @@ the surface under it. It fails unless text reaches 4.5:1 on the page, on its own
|
|
|
170
178
|
fill and on the tints Siyahi's components put it on (Callout washes, the
|
|
171
179
|
destructive tint and its hover, code-line fills, treemap heat steps), a field's
|
|
172
180
|
edge, the focus ring, a selected tab and every chart and meaning colour reach
|
|
173
|
-
3:1 against what is next to them,
|
|
181
|
+
3:1 against what is next to them, every bar colour's tint (`--bar-tint`) reaches
|
|
182
|
+
1.5:1 against the card and each slot's against the next slot's, and every colour sits inside sRGB. Slots two
|
|
174
183
|
apart share a lightness and are told apart by hue, so series take slots in
|
|
175
184
|
order.
|
package/contrast.mjs
CHANGED
|
@@ -35,6 +35,12 @@ import { readTheme } from "./theme.mjs";
|
|
|
35
35
|
|
|
36
36
|
const TEXT = 4.5;
|
|
37
37
|
const MARK = 3;
|
|
38
|
+
// A fill told apart from the fill or card beside it, inside a 1px edge that already carries the mark's 3:1. Only a
|
|
39
|
+
// nearly solid tint reaches 3:1 against its neighbour's: the solid slots next to each other sit just over 3:1, and
|
|
40
|
+
// mixing both into the card narrows that. At 25% a stack's pale segments sat at 1.2:1 against each other and read as
|
|
41
|
+
// one, and a pale slot's lone bar at 1.3:1 against the card. 1.5:1 is a guard against sliding back there, not a
|
|
42
|
+
// published threshold.
|
|
43
|
+
const FILL = 1.5;
|
|
38
44
|
const SURFACES = ["background", "card"];
|
|
39
45
|
const CHARTS = ["chart-1", "chart-2", "chart-3", "chart-4", "chart-5"];
|
|
40
46
|
|
|
@@ -145,11 +151,21 @@ const PAIRS = [
|
|
|
145
151
|
// The selected tab's fill against its list.
|
|
146
152
|
{ fg: "foreground", bg: "muted", min: MARK },
|
|
147
153
|
// Chart marks on the canvas and the card. A bar is read by its full-colour
|
|
148
|
-
// edge, so these are the edges too
|
|
149
|
-
//
|
|
150
|
-
// block's marks (greyed, at 65%) are exempt: stale is meant to read faintly.
|
|
154
|
+
// edge, so these are the edges too. A stale block's marks (greyed, at 65%)
|
|
155
|
+
// are exempt: stale is meant to read faintly.
|
|
151
156
|
...[...CHARTS, "positive", "negative"].map((fg) => ({ fg, bg: "", min: MARK })),
|
|
152
157
|
...CHARTS.slice(0, -1).map((fg, i) => ({ fg, bg: CHARTS[i + 1] ?? "", min: MARK })),
|
|
158
|
+
// A bar's tint (--bar-tint) carries its colour where the bar is short, such as
|
|
159
|
+
// a thin stacked segment that is mostly edge: every bar colour's tint against
|
|
160
|
+
// the card, and each slot's against the next slot's, its neighbour in a stack.
|
|
161
|
+
// A dimmed bar's fainter tint has no pair: a selection means it to fade.
|
|
162
|
+
...[...CHARTS, "positive", "negative"].map((fg) => ({ fg: `${fg}@bar-tint`, bg: "", min: FILL, surface: "card" })),
|
|
163
|
+
...CHARTS.slice(0, -1).map((slot, i) => ({
|
|
164
|
+
fg: `${slot}@bar-tint`,
|
|
165
|
+
bg: `${CHARTS[i + 1]}@bar-tint`,
|
|
166
|
+
min: FILL,
|
|
167
|
+
surface: "card",
|
|
168
|
+
})),
|
|
153
169
|
// Every bar colour against the 40% muted band a chart card lays behind a
|
|
154
170
|
// hovered category (lib/chart-frame).
|
|
155
171
|
...[...CHARTS, "positive", "negative"].map((fg) => ({ fg, bg: "muted/40", min: MARK, surface: "card" })),
|
package/lint/index.mjs
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// Oxlint (and ESLint) plugin `siyahi`. Every rule runs only in files that
|
|
3
3
|
// import `@siyahi/ui`, so pages written before Siyahi stay outside the rules
|
|
4
4
|
// without any configuration. Three rules come from @shadcn/lint; the other
|
|
5
|
-
// four cover what it does not check.
|
|
5
|
+
// four cover what it does not check. One more, optional-accepts-undefined, is
|
|
6
|
+
// for a component library's own types: unscoped, and not in `recommended`.
|
|
6
7
|
import { plugin as shadcn } from "@shadcn/lint";
|
|
7
8
|
|
|
8
9
|
/** @typedef {{ sourceCode: { text: string }, report: (problem: { node: unknown, message: string }) => void }} Context */
|
|
@@ -138,6 +139,88 @@ const noChartThemeMap = {
|
|
|
138
139
|
}),
|
|
139
140
|
};
|
|
140
141
|
|
|
142
|
+
/** Types that already hold undefined, so they need no `| undefined`. */
|
|
143
|
+
const HOLDS_UNDEFINED = new Set(["TSUndefinedKeyword", "TSAnyKeyword", "TSUnknownKeyword", "TSVoidKeyword"]);
|
|
144
|
+
/** Types that bind looser than `|`, so a union with undefined wraps them in brackets. */
|
|
145
|
+
const LOOSE = new Set(["TSFunctionType", "TSConstructorType", "TSConditionalType"]);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @param {{ type: string, types?: { type: string }[], typeName?: { type: string, name?: string, right?: { name?: string } } }} type
|
|
149
|
+
* @returns {boolean}
|
|
150
|
+
*/
|
|
151
|
+
function holdsUndefined(type) {
|
|
152
|
+
if (HOLDS_UNDEFINED.has(type.type)) return true;
|
|
153
|
+
if (type.type === "TSUnionType") return (type.types ?? []).some(holdsUndefined);
|
|
154
|
+
// React's ReactNode includes undefined.
|
|
155
|
+
const name = type.typeName?.type === "TSQualifiedName" ? type.typeName.right?.name : type.typeName?.name;
|
|
156
|
+
return type.type === "TSTypeReference" && name === "ReactNode";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Whether a node sits in the pattern a conditional type matches against, `T extends { x?: infer U } ? U : never`,
|
|
161
|
+
* where adding undefined would change what matches and what `infer` takes.
|
|
162
|
+
* @param {any} node
|
|
163
|
+
* @returns {boolean}
|
|
164
|
+
*/
|
|
165
|
+
function inPattern(node) {
|
|
166
|
+
for (let child = node; child.parent; child = child.parent) {
|
|
167
|
+
if (child.parent.type === "TSConditionalType" && child.parent.extendsType === child) return true;
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @type {Rule} */
|
|
173
|
+
const optionalAcceptsUndefined = {
|
|
174
|
+
meta: {
|
|
175
|
+
type: "problem",
|
|
176
|
+
fixable: "code",
|
|
177
|
+
docs: { description: "Require `| undefined` on an optional property, for pages under exactOptionalPropertyTypes." },
|
|
178
|
+
},
|
|
179
|
+
// Under exactOptionalPropertyTypes, `tone?: Tone` rejects `tone: loss ? "negative" : undefined`, the way a page
|
|
180
|
+
// writes a prop it sets only sometimes; `tone?: Tone | undefined` takes it.
|
|
181
|
+
create: (context) => {
|
|
182
|
+
const message =
|
|
183
|
+
"An optional property must also accept undefined: add `| undefined`, for pages under exactOptionalPropertyTypes.";
|
|
184
|
+
/**
|
|
185
|
+
* @param {any} node a property signature, class field or mapped type
|
|
186
|
+
* @param {any} type its type
|
|
187
|
+
*/
|
|
188
|
+
function check(node, type) {
|
|
189
|
+
if (!type || holdsUndefined(type) || inPattern(node)) return;
|
|
190
|
+
context.report({
|
|
191
|
+
node,
|
|
192
|
+
message,
|
|
193
|
+
fix: (/** @type {any} */ fixer) =>
|
|
194
|
+
LOOSE.has(type.type)
|
|
195
|
+
? [fixer.insertTextBefore(type, "("), fixer.insertTextAfter(type, ") | undefined")]
|
|
196
|
+
: fixer.insertTextAfter(type, " | undefined"),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
/** @param {any} node */
|
|
201
|
+
TSPropertySignature(node) {
|
|
202
|
+
if (node.optional) check(node, node.typeAnnotation?.typeAnnotation);
|
|
203
|
+
},
|
|
204
|
+
/** @param {any} node */
|
|
205
|
+
PropertyDefinition(node) {
|
|
206
|
+
if (node.optional) check(node, node.typeAnnotation?.typeAnnotation);
|
|
207
|
+
},
|
|
208
|
+
/** @param {any} node `[K in keyof T]?: T[K]` */
|
|
209
|
+
TSMappedType(node) {
|
|
210
|
+
if (node.optional === true || node.optional === "+") check(node, node.typeAnnotation);
|
|
211
|
+
},
|
|
212
|
+
/** @param {any} node `onSelect?(): void`, which has no type to add undefined to */
|
|
213
|
+
TSMethodSignature(node) {
|
|
214
|
+
if (!node.optional || inPattern(node)) return;
|
|
215
|
+
context.report({
|
|
216
|
+
node,
|
|
217
|
+
message: `${message} Write it as a property: \`onSelect?: (() => void) | undefined\`.`,
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
141
224
|
/**
|
|
142
225
|
* Options for shadcn's no-restyle, so every app gets the same policy from
|
|
143
226
|
* `"siyahi/no-restyle": "error"` alone. Pages may place a component (margin,
|
|
@@ -163,24 +246,28 @@ function withOptions(rule, options) {
|
|
|
163
246
|
};
|
|
164
247
|
}
|
|
165
248
|
|
|
249
|
+
/** Rules for pages that use Siyahi. */
|
|
250
|
+
const pageRules = {
|
|
251
|
+
"no-color-literal": scoped(noColorLiteral),
|
|
252
|
+
"no-scheme-variant": scoped(noSchemeVariant),
|
|
253
|
+
"no-numeric-font-size": scoped(noNumericFontSize),
|
|
254
|
+
"no-chart-theme-map": scoped(noChartThemeMap),
|
|
255
|
+
"no-raw-colors": scoped(shadcn.rules["no-raw-colors"]),
|
|
256
|
+
"no-arbitrary-values": scoped(shadcn.rules["no-arbitrary-values"]),
|
|
257
|
+
"no-restyle": scoped(withOptions(shadcn.rules["no-restyle"], RESTYLE)),
|
|
258
|
+
};
|
|
259
|
+
|
|
166
260
|
/** @type {{ meta: { name: string }, rules: Record<string, Rule> }} */
|
|
167
261
|
const plugin = {
|
|
168
262
|
meta: { name: "siyahi" },
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
"no-scheme-variant": scoped(noSchemeVariant),
|
|
172
|
-
"no-numeric-font-size": scoped(noNumericFontSize),
|
|
173
|
-
"no-chart-theme-map": scoped(noChartThemeMap),
|
|
174
|
-
"no-raw-colors": scoped(shadcn.rules["no-raw-colors"]),
|
|
175
|
-
"no-arbitrary-values": scoped(shadcn.rules["no-arbitrary-values"]),
|
|
176
|
-
"no-restyle": scoped(withOptions(shadcn.rules["no-restyle"], RESTYLE)),
|
|
177
|
-
},
|
|
263
|
+
// optional-accepts-undefined is for a component library's own types, not for pages, which only use them.
|
|
264
|
+
rules: { ...pageRules, "optional-accepts-undefined": optionalAcceptsUndefined },
|
|
178
265
|
};
|
|
179
266
|
|
|
180
267
|
/**
|
|
181
|
-
* Every rule at "error", for a Vite+ or ESLint config to spread.
|
|
268
|
+
* Every page rule at "error", for a Vite+ or ESLint config to spread.
|
|
182
269
|
* @type {Record<string, "error">}
|
|
183
270
|
*/
|
|
184
|
-
export const recommended = Object.fromEntries(Object.keys(
|
|
271
|
+
export const recommended = Object.fromEntries(Object.keys(pageRules).map((name) => [`siyahi/${name}`, "error"]));
|
|
185
272
|
|
|
186
273
|
export default plugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@siyahi/tools",
|
|
3
|
-
"version": "0.1.9-next.
|
|
3
|
+
"version": "0.1.9-next.34",
|
|
4
4
|
"description": "Checks and build helpers for Siyahi pages: a lint plugin, a font preload plugin, a theme contrast check, and screenshot and layout shift scripts.",
|
|
5
5
|
"homepage": "https://siyahi.jatinbansal.com",
|
|
6
6
|
"license": "MIT",
|
package/types/lint/index.d.mts
CHANGED
|
@@ -19,7 +19,7 @@ declare const plugin: {
|
|
|
19
19
|
rules: Record<string, Rule>;
|
|
20
20
|
};
|
|
21
21
|
/**
|
|
22
|
-
* Every rule at "error", for a Vite+ or ESLint config to spread.
|
|
22
|
+
* Every page rule at "error", for a Vite+ or ESLint config to spread.
|
|
23
23
|
* @type {Record<string, "error">}
|
|
24
24
|
*/
|
|
25
25
|
export declare const recommended: Record<string, "error">;
|