@rdna/radiants 0.1.3 → 0.1.5
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/base.css +1 -1
- package/components/core/AppWindow/AppWindow.meta.ts +69 -0
- package/components/core/AppWindow/AppWindow.schema.json +55 -0
- package/components/core/AppWindow/AppWindow.test.tsx +150 -0
- package/components/core/AppWindow/AppWindow.tsx +830 -0
- package/components/core/Button/Button.test.tsx +18 -0
- package/components/core/Button/Button.tsx +26 -16
- package/components/core/DialPanel/DialPanel.tsx +1 -1
- package/components/core/Separator/Separator.tsx +1 -1
- package/components/core/Tabs/Tabs.tsx +14 -2
- package/components/core/__tests__/smoke.test.tsx +2 -0
- package/components/core/index.ts +1 -0
- package/contract/system.ts +18 -4
- package/dark.css +11 -1
- package/eslint/contract.mjs +1 -1
- package/eslint/index.mjs +10 -0
- package/eslint/rules/no-raw-font-family.mjs +91 -0
- package/eslint/rules/no-raw-line-height.mjs +119 -0
- package/fonts/.gitkeep +0 -0
- package/fonts/Mondwest-Bold.woff2 +0 -0
- package/fonts/Mondwest.woff2 +0 -0
- package/fonts/PixeloidSans-Bold.woff2 +0 -0
- package/fonts/PixeloidSans.woff2 +0 -0
- package/fonts/WavesBlackletterCPC-Base.woff2 +0 -0
- package/fonts/WavesTinyCPC-Extended.woff2 +0 -0
- package/fonts-core.css +70 -0
- package/fonts-editorial.css +45 -0
- package/fonts.css +19 -89
- package/generated/ai-contract.json +11 -2
- package/generated/contract.freshness.json +2 -1
- package/generated/eslint-contract.json +35 -4
- package/generated/figma/contracts/app-window.contract.json +82 -0
- package/generated/figma/primitive/color.tokens.json +9 -0
- package/generated/figma/primitive/shape.tokens.json +0 -4
- package/generated/figma/primitive/typography.tokens.json +16 -4
- package/generated/figma/rdna.tokens.json +28 -11
- package/generated/figma/semantic/semantic.tokens.json +3 -3
- package/generated/figma/tokens.d.ts +1 -1
- package/generated/figma/validation-report.json +1 -1
- package/icons/DesktopIcons.tsx +4 -3
- package/icons/Icon.tsx +10 -2
- package/icons/types.ts +7 -1
- package/meta/index.ts +6 -0
- package/package.json +6 -5
- package/patterns/pretext-type-scale.ts +115 -0
- package/pixel-corners.generated.css +15 -0
- package/schemas/index.ts +2 -0
- package/tokens.css +47 -21
- package/typography.css +10 -5
- package/fonts/PixelCode-Black-Italic.woff2 +0 -0
- package/fonts/PixelCode-Black.woff2 +0 -0
- package/fonts/PixelCode-DemiBold-Italic.woff2 +0 -0
- package/fonts/PixelCode-DemiBold.woff2 +0 -0
- package/fonts/PixelCode-ExtraBlack-Italic.woff2 +0 -0
- package/fonts/PixelCode-ExtraBlack.woff2 +0 -0
- package/fonts/PixelCode-ExtraBold-Italic.woff2 +0 -0
- package/fonts/PixelCode-ExtraBold.woff2 +0 -0
- package/fonts/PixelCode-ExtraLight-Italic.woff2 +0 -0
- package/fonts/PixelCode-ExtraLight.woff2 +0 -0
- package/fonts/PixelCode-Thin-Italic.woff2 +0 -0
- package/fonts/PixelCode-Thin.woff2 +0 -0
|
@@ -35,4 +35,22 @@ describe('Button', () => {
|
|
|
35
35
|
expect(btn).not.toBeDisabled();
|
|
36
36
|
expect(btn).toHaveAttribute('aria-disabled', 'true');
|
|
37
37
|
});
|
|
38
|
+
|
|
39
|
+
test('forwards anchor props when rendered as a link', () => {
|
|
40
|
+
render(
|
|
41
|
+
<Button
|
|
42
|
+
href="https://example.com"
|
|
43
|
+
target="_blank"
|
|
44
|
+
rel="noopener noreferrer"
|
|
45
|
+
aria-label="Open docs"
|
|
46
|
+
>
|
|
47
|
+
Docs
|
|
48
|
+
</Button>,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const link = screen.getByRole('link', { name: 'Open docs' });
|
|
52
|
+
expect(link).toHaveAttribute('href', 'https://example.com');
|
|
53
|
+
expect(link).toHaveAttribute('target', '_blank');
|
|
54
|
+
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
|
|
55
|
+
});
|
|
38
56
|
});
|
|
@@ -39,19 +39,27 @@ interface ButtonOwnProps {
|
|
|
39
39
|
compact?: boolean;
|
|
40
40
|
/** Icon — RDNA icon name (string) or custom ReactNode */
|
|
41
41
|
icon?: string | React.ReactNode;
|
|
42
|
-
/** URL for navigation — renders as anchor element */
|
|
43
|
-
href?: string;
|
|
44
|
-
/** Target for link navigation */
|
|
45
|
-
target?: string;
|
|
46
42
|
/** Additional className applied to the face element */
|
|
47
43
|
className?: string;
|
|
44
|
+
/** Applies disabled styling and disables button-mode interaction */
|
|
45
|
+
disabled?: boolean;
|
|
48
46
|
/** Keep button focusable while disabled — use for loading states */
|
|
49
47
|
focusableWhenDisabled?: boolean;
|
|
50
48
|
children?: React.ReactNode;
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
type
|
|
54
|
-
Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof ButtonOwnProps
|
|
51
|
+
type ButtonButtonProps = ButtonOwnProps &
|
|
52
|
+
Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof ButtonOwnProps | 'href' | 'target'> & {
|
|
53
|
+
href?: undefined;
|
|
54
|
+
target?: never;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
type ButtonAnchorProps = ButtonOwnProps &
|
|
58
|
+
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof ButtonOwnProps> & {
|
|
59
|
+
href: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type ButtonProps = ButtonButtonProps | ButtonAnchorProps;
|
|
55
63
|
|
|
56
64
|
// ============================================================================
|
|
57
65
|
// CVA Variants
|
|
@@ -159,13 +167,11 @@ export function Button({
|
|
|
159
167
|
flush = false,
|
|
160
168
|
quiet = false,
|
|
161
169
|
icon,
|
|
162
|
-
href,
|
|
163
|
-
target,
|
|
164
170
|
children,
|
|
165
171
|
className = '',
|
|
166
|
-
disabled,
|
|
172
|
+
disabled = false,
|
|
167
173
|
focusableWhenDisabled,
|
|
168
|
-
...
|
|
174
|
+
...elementProps
|
|
169
175
|
}: ButtonProps) {
|
|
170
176
|
const dataState = active ? 'selected' : 'default';
|
|
171
177
|
|
|
@@ -221,11 +227,14 @@ export function Button({
|
|
|
221
227
|
</span>
|
|
222
228
|
);
|
|
223
229
|
|
|
224
|
-
if (href) {
|
|
230
|
+
if ('href' in elementProps && typeof elementProps.href === 'string') {
|
|
231
|
+
const { href, target, ...anchorProps } = elementProps;
|
|
232
|
+
|
|
225
233
|
return (
|
|
226
234
|
<a
|
|
227
235
|
href={href}
|
|
228
236
|
target={target}
|
|
237
|
+
{...anchorProps}
|
|
229
238
|
className={rootClasses}
|
|
230
239
|
data-rdna="button"
|
|
231
240
|
data-slot="button-root"
|
|
@@ -252,7 +261,7 @@ export function Button({
|
|
|
252
261
|
{...(quiet ? { 'data-quiet': '' } : {})}
|
|
253
262
|
disabled={isDisabled}
|
|
254
263
|
focusableWhenDisabled={focusableWhenDisabled}
|
|
255
|
-
{...
|
|
264
|
+
{...elementProps}
|
|
256
265
|
>
|
|
257
266
|
{face}
|
|
258
267
|
</BaseButton>
|
|
@@ -270,10 +279,11 @@ interface IconButtonOwnProps extends Omit<ButtonOwnProps, 'children' | 'icon' |
|
|
|
270
279
|
'aria-label': string;
|
|
271
280
|
}
|
|
272
281
|
|
|
273
|
-
type IconButtonProps =
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
282
|
+
type IconButtonProps = Omit<
|
|
283
|
+
ButtonButtonProps,
|
|
284
|
+
'children' | 'icon' | 'iconOnly' | 'textOnly' | 'fullWidth' | 'href' | 'target'
|
|
285
|
+
> &
|
|
286
|
+
IconButtonOwnProps;
|
|
277
287
|
|
|
278
288
|
/**
|
|
279
289
|
* A square button that shows only an icon.
|
|
@@ -74,8 +74,8 @@ export function DialPanel({
|
|
|
74
74
|
}: DialPanelProps) {
|
|
75
75
|
return (
|
|
76
76
|
<div className={`${width} shrink-0 border-r border-rule flex flex-col overflow-hidden ${className}`}>
|
|
77
|
-
{header}
|
|
78
77
|
<div className="flex-1 min-h-0 overflow-y-auto [&_.dialkit-panel]:!bg-transparent [&_.dialkit-panel]:!border-0 [&_.dialkit-panel]:!shadow-none">
|
|
78
|
+
{header}
|
|
79
79
|
<DialRoot
|
|
80
80
|
mode={mode}
|
|
81
81
|
{...(position ? { position } : {})}
|
|
@@ -283,7 +283,19 @@ function Trigger({ value, children, icon, settings, compact, className = '' }: T
|
|
|
283
283
|
if (layout === 'accordion') {
|
|
284
284
|
const isActive = activeTab === value;
|
|
285
285
|
return (
|
|
286
|
-
<div
|
|
286
|
+
<div
|
|
287
|
+
data-slot="tab-trigger"
|
|
288
|
+
data-mode="accordion"
|
|
289
|
+
data-state={isActive ? 'selected' : 'default'}
|
|
290
|
+
className={isActive
|
|
291
|
+
? 'bg-card border-r border-r-card relative z-10'
|
|
292
|
+
: ''
|
|
293
|
+
}
|
|
294
|
+
style={isActive
|
|
295
|
+
? { transform: 'translateY(-1px)', filter: 'drop-shadow(0 1px 0 var(--color-ink))' }
|
|
296
|
+
: { transform: 'translateY(1px)', filter: 'drop-shadow(0 -1px 0 var(--color-ink))' }
|
|
297
|
+
}
|
|
298
|
+
>
|
|
287
299
|
<div className="flex items-center">
|
|
288
300
|
<Button
|
|
289
301
|
mode={compact ? 'pattern' : 'flat'}
|
|
@@ -315,7 +327,7 @@ function Trigger({ value, children, icon, settings, compact, className = '' }: T
|
|
|
315
327
|
<BaseCollapsible.Panel
|
|
316
328
|
className="h-[var(--collapsible-panel-height)] overflow-hidden transition-[height] duration-200 ease-out data-[ending-style]:h-0 data-[starting-style]:h-0"
|
|
317
329
|
>
|
|
318
|
-
<div className="p-2 space-y-2 bg-
|
|
330
|
+
<div className="p-2 space-y-2 bg-card">
|
|
319
331
|
{settings}
|
|
320
332
|
</div>
|
|
321
333
|
</BaseCollapsible.Panel>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
2
|
import {
|
|
3
|
+
AppWindow,
|
|
3
4
|
Button,
|
|
4
5
|
Select,
|
|
5
6
|
Dialog,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
test('core exports render', () => {
|
|
27
28
|
render(<Button>Test</Button>);
|
|
28
29
|
expect(screen.getByRole('button', { name: 'Test' })).toBeInTheDocument();
|
|
30
|
+
expect(AppWindow).toBeTruthy();
|
|
29
31
|
expect(Select).toBeTruthy();
|
|
30
32
|
expect(Dialog).toBeTruthy();
|
|
31
33
|
});
|
package/components/core/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Core component exports
|
|
2
2
|
export { Alert, alertVariants } from './Alert/Alert';
|
|
3
|
+
export { AppWindow, AppWindowBody, AppWindowSplitView, AppWindowPane } from './AppWindow/AppWindow';
|
|
3
4
|
export { Badge, badgeVariants } from './Badge/Badge';
|
|
4
5
|
export { Breadcrumbs } from './Breadcrumbs/Breadcrumbs';
|
|
5
6
|
export { Button, IconButton, buttonRootVariants, buttonFaceVariants } from './Button/Button';
|
package/contract/system.ts
CHANGED
|
@@ -20,7 +20,7 @@ export const radiantsSystemContract = {
|
|
|
20
20
|
"#fcc383": "sunset-fuzz",
|
|
21
21
|
"#ff6b63": "sun-red",
|
|
22
22
|
"#cef5ca": "mint",
|
|
23
|
-
"#
|
|
23
|
+
"#fffcf3": "pure-white",
|
|
24
24
|
"#22c55e": "success-mint",
|
|
25
25
|
},
|
|
26
26
|
hexToSemantic: {
|
|
@@ -29,7 +29,7 @@ export const radiantsSystemContract = {
|
|
|
29
29
|
"#95bad2": { text: "link" },
|
|
30
30
|
"#ff6b63": { bg: "danger", text: "danger" },
|
|
31
31
|
"#cef5ca": { bg: "success", text: "success" },
|
|
32
|
-
"#
|
|
32
|
+
"#fffcf3": { bg: "card" },
|
|
33
33
|
"#22c55e": { text: "success" },
|
|
34
34
|
},
|
|
35
35
|
oklchToSemantic: {
|
|
@@ -40,7 +40,7 @@ export const radiantsSystemContract = {
|
|
|
40
40
|
"oklch(0.8546 0.1039 68.93)": { bg: "action-accent" },
|
|
41
41
|
"oklch(0.7102 0.1823 25.87)": { bg: "action-destructive", text: "status-error" },
|
|
42
42
|
"oklch(0.9312 0.0702 142.51)": { bg: "status-success", text: "status-success" },
|
|
43
|
-
"oklch(
|
|
43
|
+
"oklch(0.9909 0.0123 91.51)": { bg: "card" },
|
|
44
44
|
"oklch(0.7227 0.1920 149.58)": { text: "status-success" },
|
|
45
45
|
},
|
|
46
46
|
removedAliases: [
|
|
@@ -134,11 +134,25 @@ export const radiantsSystemContract = {
|
|
|
134
134
|
typography: {
|
|
135
135
|
validSizes: [
|
|
136
136
|
"text-xs", "text-sm", "text-base", "text-lg",
|
|
137
|
-
"text-xl", "text-2xl", "text-3xl",
|
|
137
|
+
"text-xl", "text-2xl", "text-3xl", "text-4xl", "text-5xl",
|
|
138
|
+
"text-fluid-sm", "text-fluid-base", "text-fluid-lg",
|
|
139
|
+
"text-fluid-xl", "text-fluid-2xl", "text-fluid-3xl", "text-fluid-4xl",
|
|
138
140
|
],
|
|
139
141
|
validWeights: [
|
|
140
142
|
"font-normal", "font-medium", "font-semibold", "font-bold",
|
|
141
143
|
],
|
|
144
|
+
validLeading: [
|
|
145
|
+
"leading-tight", "leading-heading", "leading-snug",
|
|
146
|
+
"leading-normal", "leading-relaxed", "leading-none",
|
|
147
|
+
],
|
|
148
|
+
validTracking: [
|
|
149
|
+
"tracking-tight", "tracking-normal", "tracking-wide",
|
|
150
|
+
],
|
|
151
|
+
validFontFamilies: [
|
|
152
|
+
"font-sans", "font-heading", "font-mono",
|
|
153
|
+
"font-display", "font-caption",
|
|
154
|
+
"font-mondwest", "font-joystix",
|
|
155
|
+
],
|
|
142
156
|
},
|
|
143
157
|
|
|
144
158
|
textLikeInputTypes: ["text", "email", "password", "search", "url", "tel", "number"],
|
package/dark.css
CHANGED
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
--color-content-secondary: oklch(0.9780 0.0295 94.34 / 0.85); /* was rgba(254, 248, 226, 0.85) */
|
|
39
39
|
--color-content-inverted: var(--color-ink);
|
|
40
40
|
--color-content-muted: oklch(0.9780 0.0295 94.34 / 0.6); /* was rgba(254, 248, 226, 0.6) — cream at 60% */
|
|
41
|
-
--color-content-link: var(--color-sky-blue); /* Links
|
|
41
|
+
--color-content-link: var(--color-sky-blue); /* Links: lighter sky-blue has good contrast on dark bg */
|
|
42
42
|
|
|
43
43
|
/* ============================================
|
|
44
44
|
EDGE TOKENS - Adjusted for dark backgrounds
|
|
@@ -94,6 +94,16 @@
|
|
|
94
94
|
--color-success: var(--color-mint);
|
|
95
95
|
--color-warning: var(--color-sun-yellow);
|
|
96
96
|
|
|
97
|
+
/* ============================================
|
|
98
|
+
DARK MODE — Typography Adjustments
|
|
99
|
+
Text appears heavier on dark backgrounds.
|
|
100
|
+
Antialiased rendering + lighter code weight.
|
|
101
|
+
============================================ */
|
|
102
|
+
|
|
103
|
+
code, pre {
|
|
104
|
+
font-weight: 300; /* PixelCode Light — compensates for perceived weight gain on dark bg */
|
|
105
|
+
}
|
|
106
|
+
|
|
97
107
|
/* ============================================
|
|
98
108
|
TEXT GLOW - Warm phosphor halo on all text
|
|
99
109
|
Inherits down the tree via text-shadow
|
package/eslint/contract.mjs
CHANGED
|
@@ -16,7 +16,7 @@ export const EMPTY_CONTRACT = Object.freeze({
|
|
|
16
16
|
themeVariants: [],
|
|
17
17
|
motion: { maxDurationMs: 0, allowedEasings: [], durationTokens: [], easingTokens: [] },
|
|
18
18
|
shadows: { validStandard: [], validPixel: [], validGlow: [] },
|
|
19
|
-
typography: { validSizes: [], validWeights: [] },
|
|
19
|
+
typography: { validSizes: [], validWeights: [], validLeading: [], validFontFamilies: [] },
|
|
20
20
|
textLikeInputTypes: [],
|
|
21
21
|
});
|
|
22
22
|
|
package/eslint/index.mjs
CHANGED
|
@@ -21,6 +21,8 @@ import noMixedStyleAuthority from './rules/no-mixed-style-authority.mjs';
|
|
|
21
21
|
import noBroadRdnaDisables from './rules/no-broad-rdna-disables.mjs';
|
|
22
22
|
import noClippedShadow from './rules/no-clipped-shadow.mjs';
|
|
23
23
|
import noPixelBorder from './rules/no-pixel-border.mjs';
|
|
24
|
+
import noRawLineHeight from './rules/no-raw-line-height.mjs';
|
|
25
|
+
import noRawFontFamily from './rules/no-raw-font-family.mjs';
|
|
24
26
|
|
|
25
27
|
const plugin = {
|
|
26
28
|
meta: {
|
|
@@ -42,6 +44,8 @@ const plugin = {
|
|
|
42
44
|
'no-broad-rdna-disables': noBroadRdnaDisables,
|
|
43
45
|
'no-clipped-shadow': noClippedShadow,
|
|
44
46
|
'no-pixel-border': noPixelBorder,
|
|
47
|
+
'no-raw-line-height': noRawLineHeight,
|
|
48
|
+
'no-raw-font-family': noRawFontFamily,
|
|
45
49
|
},
|
|
46
50
|
configs: {},
|
|
47
51
|
};
|
|
@@ -62,6 +66,8 @@ plugin.configs.recommended = {
|
|
|
62
66
|
'rdna/no-hardcoded-motion': 'warn',
|
|
63
67
|
'rdna/no-clipped-shadow': 'warn',
|
|
64
68
|
'rdna/no-pixel-border': 'warn',
|
|
69
|
+
'rdna/no-raw-line-height': 'warn',
|
|
70
|
+
'rdna/no-raw-font-family': 'warn',
|
|
65
71
|
},
|
|
66
72
|
};
|
|
67
73
|
|
|
@@ -78,6 +84,8 @@ plugin.configs.internals = {
|
|
|
78
84
|
'rdna/no-hardcoded-motion': 'warn',
|
|
79
85
|
'rdna/no-clipped-shadow': 'warn',
|
|
80
86
|
'rdna/no-pixel-border': 'warn',
|
|
87
|
+
'rdna/no-raw-line-height': 'warn',
|
|
88
|
+
'rdna/no-raw-font-family': 'warn',
|
|
81
89
|
},
|
|
82
90
|
};
|
|
83
91
|
|
|
@@ -95,6 +103,8 @@ plugin.configs['recommended-strict'] = {
|
|
|
95
103
|
'rdna/no-hardcoded-motion': 'error',
|
|
96
104
|
'rdna/no-clipped-shadow': 'error',
|
|
97
105
|
'rdna/no-pixel-border': 'error',
|
|
106
|
+
'rdna/no-raw-line-height': 'error',
|
|
107
|
+
'rdna/no-raw-font-family': 'error',
|
|
98
108
|
// no-viewport-breakpoints-in-window-layout is intentionally excluded —
|
|
99
109
|
// it is RadOS-specific and must be scoped via eslint.rdna.config.mjs
|
|
100
110
|
},
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rdna/no-raw-font-family
|
|
3
|
+
* Bans hardcoded font-family values in style props.
|
|
4
|
+
* Allows CSS variable references (var(--font-*)) and Tailwind font classes.
|
|
5
|
+
* Exempts files that import from @chenglou/pretext (canvas measurement needs literal names).
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
getObjectPropertyKey,
|
|
9
|
+
getStaticStringValue,
|
|
10
|
+
getStyleObjectExpression,
|
|
11
|
+
isAllowedCssVar,
|
|
12
|
+
isDynamicTemplateLiteral,
|
|
13
|
+
} from '../utils.mjs';
|
|
14
|
+
|
|
15
|
+
// Valid Tailwind font-family utility classes (not checked here —
|
|
16
|
+
// className font-* classes are already semantic aliases and always valid).
|
|
17
|
+
// This rule focuses on style={{ fontFamily: ... }} enforcement.
|
|
18
|
+
|
|
19
|
+
const rule = {
|
|
20
|
+
meta: {
|
|
21
|
+
type: 'problem',
|
|
22
|
+
docs: {
|
|
23
|
+
description: 'Ban hardcoded font-family in style props; require RDNA font tokens',
|
|
24
|
+
},
|
|
25
|
+
messages: {
|
|
26
|
+
hardcodedFontFamily:
|
|
27
|
+
'Hardcoded font-family in style prop. Use a CSS variable: var(--font-sans), var(--font-heading), var(--font-mono), var(--font-blackletter), var(--font-pixeloid).',
|
|
28
|
+
},
|
|
29
|
+
schema: [],
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
create(context) {
|
|
33
|
+
// Check if this file imports from @chenglou/pretext — if so, exempt it entirely.
|
|
34
|
+
// Pretext needs literal font names for canvas measurement.
|
|
35
|
+
let hasPretextImport = false;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
ImportDeclaration(node) {
|
|
39
|
+
if (
|
|
40
|
+
node.source &&
|
|
41
|
+
typeof node.source.value === 'string' &&
|
|
42
|
+
node.source.value.startsWith('@chenglou/pretext')
|
|
43
|
+
) {
|
|
44
|
+
hasPretextImport = true;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
CallExpression(node) {
|
|
48
|
+
// Also check dynamic require('@chenglou/pretext')
|
|
49
|
+
if (
|
|
50
|
+
node.callee.name === 'require' &&
|
|
51
|
+
node.arguments.length > 0 &&
|
|
52
|
+
node.arguments[0].type === 'Literal' &&
|
|
53
|
+
typeof node.arguments[0].value === 'string' &&
|
|
54
|
+
node.arguments[0].value.startsWith('@chenglou/pretext')
|
|
55
|
+
) {
|
|
56
|
+
hasPretextImport = true;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
JSXAttribute(node) {
|
|
60
|
+
if (hasPretextImport) return;
|
|
61
|
+
if (node.name.name === 'style') checkStyleObject(context, node.value);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function checkStyleObject(context, valueNode) {
|
|
68
|
+
const expr = getStyleObjectExpression(valueNode);
|
|
69
|
+
if (!expr) return;
|
|
70
|
+
|
|
71
|
+
for (const prop of expr.properties) {
|
|
72
|
+
const key = getObjectPropertyKey(prop);
|
|
73
|
+
if (key !== 'fontFamily') continue;
|
|
74
|
+
|
|
75
|
+
const val = prop.value;
|
|
76
|
+
|
|
77
|
+
const staticString = getStaticStringValue(val);
|
|
78
|
+
if (staticString !== null) {
|
|
79
|
+
// Allow any var(--font-*) reference
|
|
80
|
+
if (isAllowedCssVar(staticString, 'font-')) continue;
|
|
81
|
+
context.report({ node: val, messageId: 'hardcodedFontFamily' });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (isDynamicTemplateLiteral(val)) {
|
|
86
|
+
context.report({ node: val, messageId: 'hardcodedFontFamily' });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default rule;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rdna/no-raw-line-height
|
|
3
|
+
* Bans arbitrary line-height values in className and style props.
|
|
4
|
+
* Allows only RDNA token-mapped leading-* classes and CSS variable references.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
getClassNameStrings,
|
|
8
|
+
getObjectPropertyKey,
|
|
9
|
+
getStaticStringValue,
|
|
10
|
+
getStyleObjectExpression,
|
|
11
|
+
isAllowedCssVar,
|
|
12
|
+
isDynamicTemplateLiteral,
|
|
13
|
+
isInsideClassNameAttribute,
|
|
14
|
+
} from '../utils.mjs';
|
|
15
|
+
|
|
16
|
+
// Optional Tailwind modifier prefix: hover:, dark:, md:, focus:, etc. (stackable)
|
|
17
|
+
const MOD = '(?:[\\w-]+:)*';
|
|
18
|
+
|
|
19
|
+
// Matches arbitrary leading values: leading-[24px], leading-[1.4], leading-[1.4rem], etc.
|
|
20
|
+
const ARBITRARY_LEADING_CLASS = new RegExp(`${MOD}leading-\\[[^\\]]+\\]`, 'g');
|
|
21
|
+
|
|
22
|
+
// Token-mapped leading classes that are allowed
|
|
23
|
+
const VALID_LEADING_CLASSES = new Set([
|
|
24
|
+
'leading-tight',
|
|
25
|
+
'leading-heading',
|
|
26
|
+
'leading-snug',
|
|
27
|
+
'leading-normal',
|
|
28
|
+
'leading-relaxed',
|
|
29
|
+
'leading-none',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const validSuggestion = [...VALID_LEADING_CLASSES].join(', ');
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Strip all Tailwind modifier prefixes (hover:, dark:, md:, etc.) from a class.
|
|
36
|
+
*/
|
|
37
|
+
function stripModifiers(cls) {
|
|
38
|
+
return cls.replace(/^(?:[\w-]+:)+/, '');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const rule = {
|
|
42
|
+
meta: {
|
|
43
|
+
type: 'problem',
|
|
44
|
+
docs: {
|
|
45
|
+
description: 'Ban arbitrary line-height values; require RDNA leading tokens',
|
|
46
|
+
},
|
|
47
|
+
messages: {
|
|
48
|
+
arbitraryLeading:
|
|
49
|
+
`Arbitrary line-height "{{raw}}" in className. Use an RDNA leading token: ${validSuggestion}.`,
|
|
50
|
+
hardcodedLineHeightStyle:
|
|
51
|
+
`Hardcoded line-height in style prop. Use a CSS variable: var(--leading-*).`,
|
|
52
|
+
},
|
|
53
|
+
schema: [],
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
create(context) {
|
|
57
|
+
return {
|
|
58
|
+
JSXAttribute(node) {
|
|
59
|
+
if (node.name.name === 'className') checkClassName(context, node.value);
|
|
60
|
+
if (node.name.name === 'style') checkStyleObject(context, node.value);
|
|
61
|
+
},
|
|
62
|
+
CallExpression(node) {
|
|
63
|
+
if (!isInsideClassNameAttribute(node)) checkClassName(context, node);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function checkClassName(context, valueNode) {
|
|
70
|
+
if (!valueNode) return;
|
|
71
|
+
|
|
72
|
+
const strings = getClassNameStrings(valueNode);
|
|
73
|
+
for (const { value, node } of strings) {
|
|
74
|
+
ARBITRARY_LEADING_CLASS.lastIndex = 0;
|
|
75
|
+
let match;
|
|
76
|
+
while ((match = ARBITRARY_LEADING_CLASS.exec(value)) !== null) {
|
|
77
|
+
const bare = stripModifiers(match[0]);
|
|
78
|
+
// Allow if it's a valid token-mapped class (shouldn't match the regex, but defensive)
|
|
79
|
+
if (VALID_LEADING_CLASSES.has(bare)) continue;
|
|
80
|
+
context.report({
|
|
81
|
+
node,
|
|
82
|
+
messageId: 'arbitraryLeading',
|
|
83
|
+
data: { raw: match[0] },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function checkStyleObject(context, valueNode) {
|
|
90
|
+
const expr = getStyleObjectExpression(valueNode);
|
|
91
|
+
if (!expr) return;
|
|
92
|
+
|
|
93
|
+
for (const prop of expr.properties) {
|
|
94
|
+
const key = getObjectPropertyKey(prop);
|
|
95
|
+
if (key !== 'lineHeight') continue;
|
|
96
|
+
|
|
97
|
+
const val = prop.value;
|
|
98
|
+
|
|
99
|
+
// Allow numeric literals that are unitless (CSS lineHeight as number)
|
|
100
|
+
// — still flag them; RDNA wants token usage
|
|
101
|
+
if (val.type === 'Literal' && typeof val.value === 'number') {
|
|
102
|
+
context.report({ node: val, messageId: 'hardcodedLineHeightStyle' });
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const staticString = getStaticStringValue(val);
|
|
107
|
+
if (staticString !== null) {
|
|
108
|
+
if (isAllowedCssVar(staticString, 'leading-')) continue;
|
|
109
|
+
context.report({ node: val, messageId: 'hardcodedLineHeightStyle' });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (isDynamicTemplateLiteral(val)) {
|
|
114
|
+
context.report({ node: val, messageId: 'hardcodedLineHeightStyle' });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export default rule;
|
package/fonts/.gitkeep
ADDED
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/fonts-core.css
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* =============================================================================
|
|
2
|
+
fonts-core.css - Core token-system fonts (initial load)
|
|
3
|
+
Mondwest (body), Joystix (heading), PixelCode (mono)
|
|
4
|
+
============================================================================= */
|
|
5
|
+
|
|
6
|
+
/* Mondwest - Body font (must be downloaded separately)
|
|
7
|
+
Get it at: https://pangrampangram.com/products/bitmap-mondwest
|
|
8
|
+
----------------------------------------------------------------------------- */
|
|
9
|
+
|
|
10
|
+
@font-face {
|
|
11
|
+
font-family: 'Mondwest';
|
|
12
|
+
src: url('./fonts/Mondwest.woff2') format('woff2');
|
|
13
|
+
font-weight: 400;
|
|
14
|
+
font-style: normal;
|
|
15
|
+
font-display: swap;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@font-face {
|
|
19
|
+
font-family: 'Mondwest';
|
|
20
|
+
src: url('./fonts/Mondwest-Bold.woff2') format('woff2');
|
|
21
|
+
font-weight: 700;
|
|
22
|
+
font-style: normal;
|
|
23
|
+
font-display: swap;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Joystix - Heading font
|
|
27
|
+
----------------------------------------------------------------------------- */
|
|
28
|
+
|
|
29
|
+
@font-face {
|
|
30
|
+
font-family: 'Joystix Monospace';
|
|
31
|
+
src: url('./fonts/Joystix.woff2') format('woff2');
|
|
32
|
+
font-weight: 400;
|
|
33
|
+
font-style: normal;
|
|
34
|
+
font-display: swap;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* PixelCode - Monospace font
|
|
38
|
+
----------------------------------------------------------------------------- */
|
|
39
|
+
|
|
40
|
+
@font-face {
|
|
41
|
+
font-family: 'PixelCode';
|
|
42
|
+
src: url('./fonts/PixelCode.woff2') format('woff2');
|
|
43
|
+
font-weight: 400;
|
|
44
|
+
font-style: normal;
|
|
45
|
+
font-display: swap;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@font-face {
|
|
49
|
+
font-family: 'PixelCode';
|
|
50
|
+
src: url('./fonts/PixelCode-Italic.woff2') format('woff2');
|
|
51
|
+
font-weight: 400;
|
|
52
|
+
font-style: italic;
|
|
53
|
+
font-display: swap;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@font-face {
|
|
57
|
+
font-family: 'PixelCode';
|
|
58
|
+
src: url('./fonts/PixelCode-Bold.woff2') format('woff2');
|
|
59
|
+
font-weight: 700;
|
|
60
|
+
font-style: normal;
|
|
61
|
+
font-display: swap;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@font-face {
|
|
65
|
+
font-family: 'PixelCode';
|
|
66
|
+
src: url('./fonts/PixelCode-Bold-Italic.woff2') format('woff2');
|
|
67
|
+
font-weight: 700;
|
|
68
|
+
font-style: italic;
|
|
69
|
+
font-display: swap;
|
|
70
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* =============================================================================
|
|
2
|
+
fonts-editorial.css - Display/editorial fonts (lazy load on app-window open)
|
|
3
|
+
Waves Blackletter CPC, Waves Tiny CPC, Pixeloid Sans
|
|
4
|
+
============================================================================= */
|
|
5
|
+
|
|
6
|
+
/* Waves Blackletter CPC - Decorative blackletter
|
|
7
|
+
----------------------------------------------------------------------------- */
|
|
8
|
+
|
|
9
|
+
@font-face {
|
|
10
|
+
font-family: 'Waves Blackletter CPC';
|
|
11
|
+
src: url('./fonts/WavesBlackletterCPC-Base.woff2') format('woff2');
|
|
12
|
+
font-weight: 400;
|
|
13
|
+
font-style: normal;
|
|
14
|
+
font-display: optional;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Waves Tiny CPC - Decorative pixel caption
|
|
18
|
+
----------------------------------------------------------------------------- */
|
|
19
|
+
|
|
20
|
+
@font-face {
|
|
21
|
+
font-family: 'Waves Tiny CPC';
|
|
22
|
+
src: url('./fonts/WavesTinyCPC-Extended.woff2') format('woff2');
|
|
23
|
+
font-weight: 400;
|
|
24
|
+
font-style: normal;
|
|
25
|
+
font-display: optional;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Pixeloid Sans - Informational pixel text (bylines, labels)
|
|
29
|
+
----------------------------------------------------------------------------- */
|
|
30
|
+
|
|
31
|
+
@font-face {
|
|
32
|
+
font-family: 'Pixeloid Sans';
|
|
33
|
+
src: url('./fonts/PixeloidSans.woff2') format('woff2');
|
|
34
|
+
font-weight: 400;
|
|
35
|
+
font-style: normal;
|
|
36
|
+
font-display: swap;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@font-face {
|
|
40
|
+
font-family: 'Pixeloid Sans';
|
|
41
|
+
src: url('./fonts/PixeloidSans-Bold.woff2') format('woff2');
|
|
42
|
+
font-weight: 700;
|
|
43
|
+
font-style: normal;
|
|
44
|
+
font-display: swap;
|
|
45
|
+
}
|