@vadimcomanescu/nadicode-design-system 2.0.0 → 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.
@@ -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("div", { className: "grid gap-2", children: [
86
- /* @__PURE__ */ jsx(Label, { htmlFor: "fullName", children: labels.fullName }),
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("div", { className: "grid gap-2", children: [
100
- /* @__PURE__ */ jsx(Label, { htmlFor: "email", children: labels.emailAddress }),
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("div", { className: "grid gap-2", children: [
116
+ /* @__PURE__ */ jsxs(Field, { children: [
117
117
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
118
- /* @__PURE__ */ jsx(Label, { htmlFor: "password", children: labels.password }),
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-ALUL3PF7.js';
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-ALUL3PF7.js';
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 { Label } from '../../chunk-LIBXYD5Q.js';
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("div", { className: "grid gap-2", children: [
114
- /* @__PURE__ */ jsx(Label, { htmlFor: "recovery-email", children: tShared("email") }),
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 { Label } from '../../chunk-LIBXYD5Q.js';
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("div", { className: "grid gap-2", children: [
66
- /* @__PURE__ */ jsx(Label, { htmlFor: "new-password", children: t("newPassword") }),
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("div", { className: "grid gap-2", children: [
80
- /* @__PURE__ */ jsx(Label, { htmlFor: "confirm-password", children: t("confirmPassword") }),
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 { Label } from '../../chunk-LIBXYD5Q.js';
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("div", { className: "space-y-2", children: [
76
+ /* @__PURE__ */ jsxs(Field, { children: [
76
77
  /* @__PURE__ */ jsx(
77
- Label,
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("div", { className: "space-y-2", children: [
95
+ /* @__PURE__ */ jsxs(Field, { children: [
95
96
  /* @__PURE__ */ jsx(
96
- Label,
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("div", { className: "space-y-2", children: [
115
+ /* @__PURE__ */ jsxs(Field, { children: [
115
116
  /* @__PURE__ */ jsx(
116
- Label,
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("div", { className: "space-y-2", children: [
134
+ /* @__PURE__ */ jsxs(Field, { children: [
134
135
  /* @__PURE__ */ jsx(
135
- Label,
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 { Label } from '../../chunk-LIBXYD5Q.js';
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("div", { className: "grid gap-2", children: [
165
- /* @__PURE__ */ jsx(Label, { children: "Username" }),
165
+ /* @__PURE__ */ jsxs(Field, { children: [
166
+ /* @__PURE__ */ jsx(FieldLabel, { children: "Username" }),
166
167
  /* @__PURE__ */ jsx(Input, { placeholder: "jdoe" })
167
168
  ] }),
168
- /* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
169
- /* @__PURE__ */ jsx(Label, { children: "Email" }),
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("div", { className: "grid gap-2", children: [
175
- /* @__PURE__ */ jsx(Label, { children: "Full Name" }),
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("div", { className: "grid gap-2", children: [
179
- /* @__PURE__ */ jsx(Label, { children: "Bio" }),
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 { Label } from '../../../chunk-LIBXYD5Q.js';
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("div", { className: "space-y-2", children: [
184
- /* @__PURE__ */ jsx(Label, { htmlFor: "email", children: "Email Address" }),
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("div", { className: "space-y-2", children: [
200
- /* @__PURE__ */ jsx(Label, { children: "Role" }),
200
+ /* @__PURE__ */ jsxs(Field, { children: [
201
+ /* @__PURE__ */ jsx(FieldLabel, { children: "Role" }),
201
202
  /* @__PURE__ */ jsxs(
202
203
  Select,
203
204
  {
@@ -80,6 +80,7 @@ export const nadicodeRules = {
80
80
  "nadicode/no-handcoded-badge": "error",
81
81
  "nadicode/no-handcoded-heading": "error",
82
82
  "nadicode/no-handcoded-empty-state": "error",
83
+ "nadicode/no-handcoded-field": "error",
83
84
  };
84
85
 
85
86
  function normalizePattern(pattern) {
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vadimcomanescu/nadicode-design-system",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "registry": "https://registry.npmjs.org/"
@@ -27,7 +27,7 @@
27
27
  "build": "next build",
28
28
  "start": "next start",
29
29
  "build:types": "tsc -p tsconfig.types.json && node scripts/rewrite-dist-types.mjs",
30
- "build:lib": "node scripts/build-tokens-css.mjs && tsup && npm run build:types && node scripts/check-export-targets.mjs",
30
+ "build:lib": "node scripts/build-tokens-css.mjs && tsup && npm run build:types && node scripts/generate-catalog.mjs && node scripts/check-export-targets.mjs",
31
31
  "pack:artifact": "node scripts/pack-package-artifact.mjs",
32
32
  "exports:generate": "node scripts/generate-exports.mjs",
33
33
  "test:distribution": "node scripts/package-smoke-test.mjs",
@@ -41,8 +41,10 @@
41
41
  "ds:validate": "npm run ds:check && npm run lint && tsc --noEmit && node scripts/check-tokens-freshness.mjs && npm run build:lib && npm run test:distribution && npm run build",
42
42
  "ds:check": "node scripts/ds-check.mjs",
43
43
  "ds:task-pack": "node scripts/ds-generate-task-pack.mjs",
44
+ "catalog:generate": "node scripts/generate-catalog.mjs",
45
+ "catalog:check": "node scripts/generate-catalog.mjs --check",
44
46
  "docs:api": "node scripts/docs-generate-api.mjs",
45
- "docs:check": "node scripts/docs-check.mjs",
47
+ "docs:check": "node scripts/docs-check.mjs && node scripts/generate-catalog.mjs --check",
46
48
  "docs:inventory": "node scripts/docs-inventory.mjs",
47
49
  "release:check": "npm run docs:check && npm run test:all",
48
50
  "release:verify": "node scripts/verify-package-release.mjs",
@@ -175,6 +177,7 @@
175
177
  "import": "./dist/index.js",
176
178
  "types": "./dist/lib-index.d.ts"
177
179
  },
180
+ "./catalog": "./dist/catalog.json",
178
181
  "./accordion": {
179
182
  "import": "./dist/components/ui/Accordion.js",
180
183
  "types": "./dist/components/ui/Accordion.d.ts"