@vadimcomanescu/nadicode-design-system 2.0.1 → 2.0.2
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/dist/catalog.json +2 -2
- package/dist/{chunk-ALUL3PF7.js → chunk-6MFAZU4B.js} +7 -7
- package/dist/components/blocks/AuthLayout.js +2 -1
- package/dist/components/blocks/LoginBlock.js +2 -1
- package/dist/components/blocks/PasswordRecoveryBlock.js +4 -3
- package/dist/components/blocks/ResetPasswordBlock.js +6 -5
- package/dist/components/blocks/SignUpBlock.js +10 -9
- package/dist/components/blocks/WizardBlock.js +10 -9
- package/dist/components/blocks/user/InviteUserModal.js +6 -5
- package/eslint-rules/nadicode/config.js +1 -0
- package/eslint-rules/nadicode/index.js +2 -0
- package/eslint-rules/nadicode/rules/__tests__/no-handcoded-field.test.js +110 -0
- package/eslint-rules/nadicode/rules/no-handcoded-field.js +120 -0
- package/package.json +1 -1
package/dist/catalog.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { StaggerChildren } from './chunk-DQPK2XRL.js';
|
|
2
|
+
import { Field, FieldLabel } from './chunk-RX5EUODB.js';
|
|
2
3
|
import { Heading } from './chunk-WI547C47.js';
|
|
3
4
|
import { Input } from './chunk-AP3XXYAY.js';
|
|
4
|
-
import { Label } from './chunk-LIBXYD5Q.js';
|
|
5
5
|
import { GoogleIcon } from './chunk-DJTF3XFB.js';
|
|
6
6
|
import { Card, CardHeader, CardDescription, CardContent, CardFooter } from './chunk-AH6YSYYT.js';
|
|
7
7
|
import { Button } from './chunk-7KIDDF3I.js';
|
|
@@ -82,8 +82,8 @@ function LoginBlock({
|
|
|
82
82
|
] }),
|
|
83
83
|
!!error && /* @__PURE__ */ jsx("div", { id: "login-error", role: "alert", className: "rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive", children: error }),
|
|
84
84
|
/* @__PURE__ */ jsx("form", { action, onSubmit: action ? void 0 : onLogin, "aria-describedby": error ? "login-error" : void 0, className: "grid gap-4", children: /* @__PURE__ */ jsxs(StaggerChildren, { staggerMs: FIELD_STAGGER_MS, className: "grid gap-4", children: [
|
|
85
|
-
type === "signup" && /* @__PURE__ */ jsxs(
|
|
86
|
-
/* @__PURE__ */ jsx(
|
|
85
|
+
type === "signup" && /* @__PURE__ */ jsxs(Field, { children: [
|
|
86
|
+
/* @__PURE__ */ jsx(FieldLabel, { htmlFor: "fullName", children: labels.fullName }),
|
|
87
87
|
/* @__PURE__ */ jsx(
|
|
88
88
|
Input,
|
|
89
89
|
{
|
|
@@ -96,8 +96,8 @@ function LoginBlock({
|
|
|
96
96
|
}
|
|
97
97
|
)
|
|
98
98
|
] }),
|
|
99
|
-
/* @__PURE__ */ jsxs(
|
|
100
|
-
/* @__PURE__ */ jsx(
|
|
99
|
+
/* @__PURE__ */ jsxs(Field, { children: [
|
|
100
|
+
/* @__PURE__ */ jsx(FieldLabel, { htmlFor: "email", children: labels.emailAddress }),
|
|
101
101
|
/* @__PURE__ */ jsx(
|
|
102
102
|
Input,
|
|
103
103
|
{
|
|
@@ -113,9 +113,9 @@ function LoginBlock({
|
|
|
113
113
|
}
|
|
114
114
|
)
|
|
115
115
|
] }),
|
|
116
|
-
/* @__PURE__ */ jsxs(
|
|
116
|
+
/* @__PURE__ */ jsxs(Field, { children: [
|
|
117
117
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
118
|
-
/* @__PURE__ */ jsx(
|
|
118
|
+
/* @__PURE__ */ jsx(FieldLabel, { htmlFor: "password", children: labels.password }),
|
|
119
119
|
type === "login" && /* @__PURE__ */ jsx(
|
|
120
120
|
"a",
|
|
121
121
|
{
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { LoginBlock } from '../../chunk-
|
|
2
|
+
import { LoginBlock } from '../../chunk-6MFAZU4B.js';
|
|
3
3
|
import { StaggerChildren } from '../../chunk-DQPK2XRL.js';
|
|
4
|
+
import '../../chunk-RX5EUODB.js';
|
|
4
5
|
import { Heading } from '../../chunk-WI547C47.js';
|
|
5
6
|
import { siteConfig } from '../../chunk-A7NUWD76.js';
|
|
6
7
|
import '../../chunk-AP3XXYAY.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
export { LoginBlock } from '../../chunk-
|
|
2
|
+
export { LoginBlock } from '../../chunk-6MFAZU4B.js';
|
|
3
3
|
import '../../chunk-DQPK2XRL.js';
|
|
4
|
+
import '../../chunk-RX5EUODB.js';
|
|
4
5
|
import '../../chunk-WI547C47.js';
|
|
5
6
|
import '../../chunk-AP3XXYAY.js';
|
|
6
7
|
import '../../chunk-LIBXYD5Q.js';
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useSafeTimeout } from '../../chunk-MDAYDDTC.js';
|
|
3
3
|
import { Spinner } from '../../chunk-ZLSWCV55.js';
|
|
4
|
+
import { Field, FieldLabel } from '../../chunk-RX5EUODB.js';
|
|
4
5
|
import { Separator } from '../../chunk-CUZJIDU7.js';
|
|
5
6
|
import { Input } from '../../chunk-AP3XXYAY.js';
|
|
6
|
-
import
|
|
7
|
+
import '../../chunk-LIBXYD5Q.js';
|
|
7
8
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '../../chunk-AH6YSYYT.js';
|
|
8
9
|
import { Button } from '../../chunk-7KIDDF3I.js';
|
|
9
10
|
import { m, fadeInUp, scaleIn } from '../../chunk-PD2YEH3H.js';
|
|
@@ -110,8 +111,8 @@ function PasswordRecoveryBlock({
|
|
|
110
111
|
/* @__PURE__ */ jsx(CardDescription, { children: resolvedDescription })
|
|
111
112
|
] }),
|
|
112
113
|
/* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsxs("form", { noValidate: true, onSubmit: handleSubmit, className: "grid gap-4", children: [
|
|
113
|
-
/* @__PURE__ */ jsxs(
|
|
114
|
-
/* @__PURE__ */ jsx(
|
|
114
|
+
/* @__PURE__ */ jsxs(Field, { children: [
|
|
115
|
+
/* @__PURE__ */ jsx(FieldLabel, { htmlFor: "recovery-email", children: tShared("email") }),
|
|
115
116
|
/* @__PURE__ */ jsx(
|
|
116
117
|
Input,
|
|
117
118
|
{
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { Spinner } from '../../chunk-ZLSWCV55.js';
|
|
3
3
|
import { PasswordInput } from '../../chunk-UJDEGCCZ.js';
|
|
4
|
+
import { Field, FieldLabel } from '../../chunk-RX5EUODB.js';
|
|
4
5
|
import '../../chunk-AP3XXYAY.js';
|
|
5
|
-
import
|
|
6
|
+
import '../../chunk-LIBXYD5Q.js';
|
|
6
7
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '../../chunk-AH6YSYYT.js';
|
|
7
8
|
import { Button } from '../../chunk-7KIDDF3I.js';
|
|
8
9
|
import { m, scaleIn } from '../../chunk-PD2YEH3H.js';
|
|
@@ -62,8 +63,8 @@ function ResetPasswordBlock({
|
|
|
62
63
|
/* @__PURE__ */ jsx(CardDescription, { children: resolvedDescription })
|
|
63
64
|
] }),
|
|
64
65
|
/* @__PURE__ */ jsx(CardContent, { children: /* @__PURE__ */ jsxs("form", { noValidate: true, onSubmit: handleSubmit, className: "grid gap-4", children: [
|
|
65
|
-
/* @__PURE__ */ jsxs(
|
|
66
|
-
/* @__PURE__ */ jsx(
|
|
66
|
+
/* @__PURE__ */ jsxs(Field, { children: [
|
|
67
|
+
/* @__PURE__ */ jsx(FieldLabel, { htmlFor: "new-password", children: t("newPassword") }),
|
|
67
68
|
/* @__PURE__ */ jsx(
|
|
68
69
|
PasswordInput,
|
|
69
70
|
{
|
|
@@ -76,8 +77,8 @@ function ResetPasswordBlock({
|
|
|
76
77
|
}
|
|
77
78
|
)
|
|
78
79
|
] }),
|
|
79
|
-
/* @__PURE__ */ jsxs(
|
|
80
|
-
/* @__PURE__ */ jsx(
|
|
80
|
+
/* @__PURE__ */ jsxs(Field, { children: [
|
|
81
|
+
/* @__PURE__ */ jsx(FieldLabel, { htmlFor: "confirm-password", children: t("confirmPassword") }),
|
|
81
82
|
/* @__PURE__ */ jsx(
|
|
82
83
|
PasswordInput,
|
|
83
84
|
{
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { LogoIcon } from '../../chunk-GHIBG7OM.js';
|
|
3
|
+
import { Field, FieldLabel } from '../../chunk-RX5EUODB.js';
|
|
3
4
|
import { Heading } from '../../chunk-WI547C47.js';
|
|
4
5
|
import { siteConfig } from '../../chunk-A7NUWD76.js';
|
|
5
6
|
import { Input } from '../../chunk-AP3XXYAY.js';
|
|
6
|
-
import
|
|
7
|
+
import '../../chunk-LIBXYD5Q.js';
|
|
7
8
|
import { GoogleIcon, MicrosoftIcon } from '../../chunk-DJTF3XFB.js';
|
|
8
9
|
import { Button } from '../../chunk-7KIDDF3I.js';
|
|
9
10
|
import '../../chunk-PD2YEH3H.js';
|
|
@@ -72,9 +73,9 @@ function LoginPage({
|
|
|
72
73
|
] }),
|
|
73
74
|
/* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
74
75
|
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
75
|
-
/* @__PURE__ */ jsxs(
|
|
76
|
+
/* @__PURE__ */ jsxs(Field, { children: [
|
|
76
77
|
/* @__PURE__ */ jsx(
|
|
77
|
-
|
|
78
|
+
FieldLabel,
|
|
78
79
|
{
|
|
79
80
|
htmlFor: "firstname",
|
|
80
81
|
className: "block text-sm",
|
|
@@ -91,9 +92,9 @@ function LoginPage({
|
|
|
91
92
|
}
|
|
92
93
|
)
|
|
93
94
|
] }),
|
|
94
|
-
/* @__PURE__ */ jsxs(
|
|
95
|
+
/* @__PURE__ */ jsxs(Field, { children: [
|
|
95
96
|
/* @__PURE__ */ jsx(
|
|
96
|
-
|
|
97
|
+
FieldLabel,
|
|
97
98
|
{
|
|
98
99
|
htmlFor: "lastname",
|
|
99
100
|
className: "block text-sm",
|
|
@@ -111,9 +112,9 @@ function LoginPage({
|
|
|
111
112
|
)
|
|
112
113
|
] })
|
|
113
114
|
] }),
|
|
114
|
-
/* @__PURE__ */ jsxs(
|
|
115
|
+
/* @__PURE__ */ jsxs(Field, { children: [
|
|
115
116
|
/* @__PURE__ */ jsx(
|
|
116
|
-
|
|
117
|
+
FieldLabel,
|
|
117
118
|
{
|
|
118
119
|
htmlFor: "email",
|
|
119
120
|
className: "block text-sm",
|
|
@@ -130,9 +131,9 @@ function LoginPage({
|
|
|
130
131
|
}
|
|
131
132
|
)
|
|
132
133
|
] }),
|
|
133
|
-
/* @__PURE__ */ jsxs(
|
|
134
|
+
/* @__PURE__ */ jsxs(Field, { children: [
|
|
134
135
|
/* @__PURE__ */ jsx(
|
|
135
|
-
|
|
136
|
+
FieldLabel,
|
|
136
137
|
{
|
|
137
138
|
htmlFor: "pwd",
|
|
138
139
|
className: "text-sm",
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import '../../chunk-3NYTIDKZ.js';
|
|
3
3
|
import { Step } from '../../chunk-HS7QNBCO.js';
|
|
4
4
|
import { Stepper } from '../../chunk-2WEUTBDI.js';
|
|
5
|
+
import { Field, FieldLabel } from '../../chunk-RX5EUODB.js';
|
|
5
6
|
import { Heading } from '../../chunk-WI547C47.js';
|
|
6
7
|
import { Input } from '../../chunk-AP3XXYAY.js';
|
|
7
|
-
import
|
|
8
|
+
import '../../chunk-LIBXYD5Q.js';
|
|
8
9
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '../../chunk-AH6YSYYT.js';
|
|
9
10
|
import { Button } from '../../chunk-7KIDDF3I.js';
|
|
10
11
|
import '../../chunk-PD2YEH3H.js';
|
|
@@ -161,22 +162,22 @@ function WizardBlock({
|
|
|
161
162
|
)) }),
|
|
162
163
|
/* @__PURE__ */ jsxs("div", { className: "min-h-[200px] border rounded-lg p-6 bg-muted/20 animate-in fade-in slide-in-from-bottom-2 duration-300", children: [
|
|
163
164
|
activeStep === 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
164
|
-
/* @__PURE__ */ jsxs(
|
|
165
|
-
/* @__PURE__ */ jsx(
|
|
165
|
+
/* @__PURE__ */ jsxs(Field, { children: [
|
|
166
|
+
/* @__PURE__ */ jsx(FieldLabel, { children: "Username" }),
|
|
166
167
|
/* @__PURE__ */ jsx(Input, { placeholder: "jdoe" })
|
|
167
168
|
] }),
|
|
168
|
-
/* @__PURE__ */ jsxs(
|
|
169
|
-
/* @__PURE__ */ jsx(
|
|
169
|
+
/* @__PURE__ */ jsxs(Field, { children: [
|
|
170
|
+
/* @__PURE__ */ jsx(FieldLabel, { children: "Email" }),
|
|
170
171
|
/* @__PURE__ */ jsx(Input, { placeholder: "john@example.com" })
|
|
171
172
|
] })
|
|
172
173
|
] }),
|
|
173
174
|
activeStep === 1 && /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
174
|
-
/* @__PURE__ */ jsxs(
|
|
175
|
-
/* @__PURE__ */ jsx(
|
|
175
|
+
/* @__PURE__ */ jsxs(Field, { children: [
|
|
176
|
+
/* @__PURE__ */ jsx(FieldLabel, { children: "Full Name" }),
|
|
176
177
|
/* @__PURE__ */ jsx(Input, { placeholder: "John Doe" })
|
|
177
178
|
] }),
|
|
178
|
-
/* @__PURE__ */ jsxs(
|
|
179
|
-
/* @__PURE__ */ jsx(
|
|
179
|
+
/* @__PURE__ */ jsxs(Field, { children: [
|
|
180
|
+
/* @__PURE__ */ jsx(FieldLabel, { children: "Bio" }),
|
|
180
181
|
/* @__PURE__ */ jsx(Input, { placeholder: "Tell us about yourself" })
|
|
181
182
|
] })
|
|
182
183
|
] }),
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { RoleBadge } from '../../../chunk-OHCQPI3W.js';
|
|
3
3
|
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '../../../chunk-WH62BE24.js';
|
|
4
|
+
import { Field, FieldLabel } from '../../../chunk-RX5EUODB.js';
|
|
4
5
|
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '../../../chunk-W73JAOHW.js';
|
|
5
6
|
import { Input } from '../../../chunk-AP3XXYAY.js';
|
|
6
|
-
import
|
|
7
|
+
import '../../../chunk-LIBXYD5Q.js';
|
|
7
8
|
import '../../../chunk-KRBLVZII.js';
|
|
8
9
|
import { Button } from '../../../chunk-7KIDDF3I.js';
|
|
9
10
|
import '../../../chunk-6FOHUNXR.js';
|
|
@@ -180,8 +181,8 @@ function InviteUserModal({ onInvite, onInvited }) {
|
|
|
180
181
|
/* @__PURE__ */ jsx(DialogDescription, { children: "Send an invitation to join your workspace. They will receive an email with a magic link." })
|
|
181
182
|
] }),
|
|
182
183
|
/* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit(onSubmit), className: "space-y-6 pt-4", children: [
|
|
183
|
-
/* @__PURE__ */ jsxs(
|
|
184
|
-
/* @__PURE__ */ jsx(
|
|
184
|
+
/* @__PURE__ */ jsxs(Field, { children: [
|
|
185
|
+
/* @__PURE__ */ jsx(FieldLabel, { htmlFor: "email", children: "Email Address" }),
|
|
185
186
|
/* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
186
187
|
/* @__PURE__ */ jsx(MailIcon, { size: 16, className: "absolute left-4 top-2.5 text-text-tertiary" }),
|
|
187
188
|
/* @__PURE__ */ jsx(
|
|
@@ -196,8 +197,8 @@ function InviteUserModal({ onInvite, onInvited }) {
|
|
|
196
197
|
] }),
|
|
197
198
|
errors.email && /* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-destructive", children: errors.email.message })
|
|
198
199
|
] }),
|
|
199
|
-
/* @__PURE__ */ jsxs(
|
|
200
|
-
/* @__PURE__ */ jsx(
|
|
200
|
+
/* @__PURE__ */ jsxs(Field, { children: [
|
|
201
|
+
/* @__PURE__ */ jsx(FieldLabel, { children: "Role" }),
|
|
201
202
|
/* @__PURE__ */ jsxs(
|
|
202
203
|
Select,
|
|
203
204
|
{
|
|
@@ -67,6 +67,7 @@ import noDiyBackdropBlur from "./rules/no-diy-backdrop-blur.js";
|
|
|
67
67
|
import noHandcodedBadge from "./rules/no-handcoded-badge.js";
|
|
68
68
|
import noHandcodedHeading from "./rules/no-handcoded-heading.js";
|
|
69
69
|
import noHandcodedEmptyState from "./rules/no-handcoded-empty-state.js";
|
|
70
|
+
import noHandcodedField from "./rules/no-handcoded-field.js";
|
|
70
71
|
|
|
71
72
|
export { nadicodeRules, createAllowlistOverrides } from "./config.js";
|
|
72
73
|
|
|
@@ -141,5 +142,6 @@ export const nadicodePlugin = {
|
|
|
141
142
|
"no-handcoded-badge": noHandcodedBadge,
|
|
142
143
|
"no-handcoded-heading": noHandcodedHeading,
|
|
143
144
|
"no-handcoded-empty-state": noHandcodedEmptyState,
|
|
145
|
+
"no-handcoded-field": noHandcodedField,
|
|
144
146
|
},
|
|
145
147
|
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { RuleTester } from "eslint";
|
|
2
|
+
import { describe } from "vitest";
|
|
3
|
+
|
|
4
|
+
import rule from "../no-handcoded-field.js";
|
|
5
|
+
|
|
6
|
+
const tester = new RuleTester({
|
|
7
|
+
languageOptions: {
|
|
8
|
+
ecmaVersion: "latest",
|
|
9
|
+
sourceType: "module",
|
|
10
|
+
parserOptions: {
|
|
11
|
+
ecmaFeatures: { jsx: true },
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const ERROR_MESSAGE =
|
|
17
|
+
'Use <Field> and <FieldLabel> instead of hand-coded label+control wrappers. Import from "@/components/ui/Field".';
|
|
18
|
+
|
|
19
|
+
describe("no-handcoded-field", () => {
|
|
20
|
+
tester.run("no-handcoded-field", rule, {
|
|
21
|
+
valid: [
|
|
22
|
+
{
|
|
23
|
+
code: "const x = <Field><FieldLabel>Email</FieldLabel><Input /></Field>;",
|
|
24
|
+
filename: "/repo/src/app/page.tsx",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
code: 'const x = <div><Checkbox id="x" /><Label htmlFor="x">Accept</Label></div>;',
|
|
28
|
+
filename: "/repo/src/app/page.tsx",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
code: "const x = <div><Switch id=\"s\" /><Label htmlFor=\"s\">Toggle</Label></div>;",
|
|
32
|
+
filename: "/repo/src/app/page.tsx",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
code: "const x = <div><RadioGroupItem value=\"a\" /><Label>Option A</Label></div>;",
|
|
36
|
+
filename: "/repo/src/app/page.tsx",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
code: "const x = <div><Label>Name</Label><Input /></div>;",
|
|
40
|
+
filename: "/repo/src/components/ui/Field.tsx",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
code: "const x = <div><Label>Name</Label><Input /></div>;",
|
|
44
|
+
filename: "/repo/src/__tests__/form.test.tsx",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
code: "const x = <div><Label>Name</Label><Input /></div>;",
|
|
48
|
+
filename: "/repo/src/app/form.test.tsx",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
code: "const x = <div><Label>Name</Label><Input /></div>;",
|
|
52
|
+
filename: "/repo/src/test/form-check.tsx",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
code: "const x = <div><Label>Just a label</Label></div>;",
|
|
56
|
+
filename: "/repo/src/app/page.tsx",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
code: "const x = <FormField><Label>Email</Label><Input /></FormField>;",
|
|
60
|
+
filename: "/repo/src/app/page.tsx",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
code: "const x = <FormItem><Label>Email</Label><Input /></FormItem>;",
|
|
64
|
+
filename: "/repo/src/app/page.tsx",
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
invalid: [
|
|
68
|
+
{
|
|
69
|
+
code: 'const x = <div className="space-y-2"><Label>Email</Label><Input /></div>;',
|
|
70
|
+
filename: "/repo/src/app/page.tsx",
|
|
71
|
+
errors: [{ message: ERROR_MESSAGE }],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
code: "const x = <div><Label>Bio</Label><Textarea /></div>;",
|
|
75
|
+
filename: "/repo/src/app/page.tsx",
|
|
76
|
+
errors: [{ message: ERROR_MESSAGE }],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
code: "const x = <div><Label>Country</Label><Select><SelectTrigger /></Select></div>;",
|
|
80
|
+
filename: "/repo/src/app/page.tsx",
|
|
81
|
+
errors: [{ message: ERROR_MESSAGE }],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
code: "const x = <div><Label>Country</Label><NativeSelect /></div>;",
|
|
85
|
+
filename: "/repo/src/app/page.tsx",
|
|
86
|
+
errors: [{ message: ERROR_MESSAGE }],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
code: "const x = <div><Label>OTP</Label><InputOTP /></div>;",
|
|
90
|
+
filename: "/repo/src/app/page.tsx",
|
|
91
|
+
errors: [{ message: ERROR_MESSAGE }],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
code: "const x = <div><Label>Date</Label><DatePicker /></div>;",
|
|
95
|
+
filename: "/repo/src/app/page.tsx",
|
|
96
|
+
errors: [{ message: ERROR_MESSAGE }],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
code: "const x = <div><Label>Volume</Label><Slider /></div>;",
|
|
100
|
+
filename: "/repo/src/app/page.tsx",
|
|
101
|
+
errors: [{ message: ERROR_MESSAGE }],
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
code: "const x = <div><Label>Password</Label><PasswordInput /></div>;",
|
|
105
|
+
filename: "/repo/src/app/page.tsx",
|
|
106
|
+
errors: [{ message: ERROR_MESSAGE }],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { getFilename, isTestFile } from "../utils.js";
|
|
2
|
+
|
|
3
|
+
const FORM_CONTROLS = new Set([
|
|
4
|
+
"Input",
|
|
5
|
+
"Textarea",
|
|
6
|
+
"Select",
|
|
7
|
+
"SelectTrigger",
|
|
8
|
+
"NativeSelect",
|
|
9
|
+
"Combobox",
|
|
10
|
+
"InputOTP",
|
|
11
|
+
"RadioGroup",
|
|
12
|
+
"DatePicker",
|
|
13
|
+
"DateRangePicker",
|
|
14
|
+
"Slider",
|
|
15
|
+
"FileUpload",
|
|
16
|
+
"PasswordInput",
|
|
17
|
+
"InputGroup",
|
|
18
|
+
"TagInput",
|
|
19
|
+
"Calendar",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const INLINE_LABEL_CONTROLS = new Set([
|
|
23
|
+
"Checkbox",
|
|
24
|
+
"Switch",
|
|
25
|
+
"RadioGroupItem",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const FIELD_WRAPPERS = new Set([
|
|
29
|
+
"Field",
|
|
30
|
+
"FormField",
|
|
31
|
+
"FormItem",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const EXEMPT_DIRS = [
|
|
35
|
+
"/components/ui/",
|
|
36
|
+
"/components/animate-ui/",
|
|
37
|
+
"/components/layout/",
|
|
38
|
+
"/src/test/",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const EXEMPT_FILES = [
|
|
42
|
+
"Field.tsx",
|
|
43
|
+
"FormField.tsx",
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
function isExempt(filename) {
|
|
47
|
+
if (EXEMPT_FILES.some((file) => filename.endsWith(`/${file}`))) return true;
|
|
48
|
+
return EXEMPT_DIRS.some((dir) => filename.includes(dir));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getJSXElementName(node) {
|
|
52
|
+
if (node.type === "JSXIdentifier") return node.name;
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getChildElementNames(jsxElement) {
|
|
57
|
+
const names = [];
|
|
58
|
+
const children = jsxElement.children || [];
|
|
59
|
+
|
|
60
|
+
for (const child of children) {
|
|
61
|
+
if (child.type === "JSXElement" && child.openingElement) {
|
|
62
|
+
const name = getJSXElementName(child.openingElement.name);
|
|
63
|
+
if (name) names.push(name);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return names;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const rule = {
|
|
71
|
+
meta: {
|
|
72
|
+
type: "suggestion",
|
|
73
|
+
docs: {
|
|
74
|
+
description:
|
|
75
|
+
"Disallow hand-coded label+control wrappers. Use Field and FieldLabel instead.",
|
|
76
|
+
},
|
|
77
|
+
schema: [],
|
|
78
|
+
},
|
|
79
|
+
create(context) {
|
|
80
|
+
const filename = getFilename(context);
|
|
81
|
+
if (isExempt(filename) || isTestFile(filename)) return {};
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
JSXElement(node) {
|
|
85
|
+
const opening = node.openingElement;
|
|
86
|
+
if (!opening || opening.name.type !== "JSXIdentifier") return;
|
|
87
|
+
|
|
88
|
+
const tagName = opening.name.name;
|
|
89
|
+
|
|
90
|
+
if (FIELD_WRAPPERS.has(tagName)) return;
|
|
91
|
+
|
|
92
|
+
const isPlainDiv = tagName === "div";
|
|
93
|
+
if (!isPlainDiv) return;
|
|
94
|
+
|
|
95
|
+
const childNames = getChildElementNames(node);
|
|
96
|
+
|
|
97
|
+
const hasLabel = childNames.includes("Label");
|
|
98
|
+
if (!hasLabel) return;
|
|
99
|
+
|
|
100
|
+
const hasInlineLabelControl = childNames.some((name) =>
|
|
101
|
+
INLINE_LABEL_CONTROLS.has(name),
|
|
102
|
+
);
|
|
103
|
+
if (hasInlineLabelControl) return;
|
|
104
|
+
|
|
105
|
+
const hasFormControl = childNames.some((name) =>
|
|
106
|
+
FORM_CONTROLS.has(name),
|
|
107
|
+
);
|
|
108
|
+
if (!hasFormControl) return;
|
|
109
|
+
|
|
110
|
+
context.report({
|
|
111
|
+
node: opening,
|
|
112
|
+
message:
|
|
113
|
+
'Use <Field> and <FieldLabel> instead of hand-coded label+control wrappers. Import from "@/components/ui/Field".',
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export default rule;
|