@karni/ui 0.0.0-stage → 0.4.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/LICENSE +201 -0
- package/README.md +928 -2
- package/dist/index.cjs +9 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +8699 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.json +102 -0
- package/dist/mockServiceWorker.js +361 -0
- package/dist/script-BVJ9BbPm.cjs +2 -0
- package/dist/script-BVJ9BbPm.cjs.map +1 -0
- package/dist/script-D1fn4Dm4.js +6 -0
- package/dist/script-D1fn4Dm4.js.map +1 -0
- package/dist/src/auth/AuthGuard.d.ts +77 -0
- package/dist/src/auth/LoginForm.d.ts +63 -0
- package/dist/src/auth/engine.d.ts +101 -0
- package/dist/src/auth/provider.d.ts +43 -0
- package/dist/src/auth/storage.d.ts +26 -0
- package/dist/src/auth/types.d.ts +32 -0
- package/dist/src/auth/usePermission.d.ts +7 -0
- package/dist/src/components/brand/karni-mark.d.ts +21 -0
- package/dist/src/components/brand/mark-paths.d.ts +8 -0
- package/dist/src/components/chart/data-chart.d.ts +129 -0
- package/dist/src/components/empty-state/empty-state.d.ts +40 -0
- package/dist/src/components/file-upload/FileUpload.d.ts +108 -0
- package/dist/src/components/layout/layout.d.ts +150 -0
- package/dist/src/components/layout/route-focus.d.ts +43 -0
- package/dist/src/components/modal/modal.d.ts +86 -0
- package/dist/src/components/page/page.d.ts +60 -0
- package/dist/src/components/primitives/button.d.ts +45 -0
- package/dist/src/components/primitives/checkbox.d.ts +8 -0
- package/dist/src/components/primitives/dropdown-menu.d.ts +24 -0
- package/dist/src/components/primitives/icons.d.ts +19 -0
- package/dist/src/components/primitives/input.d.ts +7 -0
- package/dist/src/components/primitives/label.d.ts +8 -0
- package/dist/src/components/primitives/select.d.ts +94 -0
- package/dist/src/components/primitives/skeleton.d.ts +7 -0
- package/dist/src/components/primitives/switch.d.ts +8 -0
- package/dist/src/components/primitives/tabs.d.ts +10 -0
- package/dist/src/components/primitives/textarea.d.ts +7 -0
- package/dist/src/components/resource/ResourceDetail.d.ts +7 -0
- package/dist/src/components/resource/ResourceForm.d.ts +8 -0
- package/dist/src/components/resource/ResourceSelect.d.ts +9 -0
- package/dist/src/components/resource/ResourceTable.d.ts +17 -0
- package/dist/src/components/resource/audit-fields.d.ts +17 -0
- package/dist/src/components/resource/detail/ResourceDetail.d.ts +128 -0
- package/dist/src/components/resource/displayLabel.d.ts +12 -0
- package/dist/src/components/resource/empty-value.d.ts +17 -0
- package/dist/src/components/resource/form/ResourceForm.d.ts +109 -0
- package/dist/src/components/resource/form/field-inputs.d.ts +34 -0
- package/dist/src/components/resource/form/fixtures.d.ts +24 -0
- package/dist/src/components/resource/form/form-states.d.ts +87 -0
- package/dist/src/components/resource/form/prepare.d.ts +54 -0
- package/dist/src/components/resource/form/server-errors.d.ts +14 -0
- package/dist/src/components/resource/history/ChangeHistory.d.ts +60 -0
- package/dist/src/components/resource/import/ResourceImport.d.ts +84 -0
- package/dist/src/components/resource/nouns.d.ts +12 -0
- package/dist/src/components/resource/query-state.d.ts +20 -0
- package/dist/src/components/resource/resource-names.d.ts +23 -0
- package/dist/src/components/resource/select/ResourceSelect.d.ts +67 -0
- package/dist/src/components/resource/select/select-states.d.ts +68 -0
- package/dist/src/components/resource/select/use-debounced-value.d.ts +2 -0
- package/dist/src/components/resource/select/use-option-pages.d.ts +37 -0
- package/dist/src/components/resource/select/venue-backend.d.ts +144 -0
- package/dist/src/components/resource/table/BulkActionsBar.d.ts +77 -0
- package/dist/src/components/resource/table/CardList.d.ts +31 -0
- package/dist/src/components/resource/table/ConfirmDialog.d.ts +28 -0
- package/dist/src/components/resource/table/ExportButtons.d.ts +17 -0
- package/dist/src/components/resource/table/FilterBar.d.ts +29 -0
- package/dist/src/components/resource/table/PaginationControls.d.ts +31 -0
- package/dist/src/components/resource/table/ResourceTable.d.ts +137 -0
- package/dist/src/components/resource/table/RowCard.d.ts +31 -0
- package/dist/src/components/resource/table/TableView.d.ts +29 -0
- package/dist/src/components/resource/table/ViewOptions.d.ts +19 -0
- package/dist/src/components/resource/table/access.d.ts +12 -0
- package/dist/src/components/resource/table/demo-fixtures.d.ts +25 -0
- package/dist/src/components/resource/table/derive-columns.d.ts +33 -0
- package/dist/src/components/resource/table/grid-navigation.d.ts +23 -0
- package/dist/src/components/resource/table/layout.d.ts +37 -0
- package/dist/src/components/resource/table/preferences.d.ts +12 -0
- package/dist/src/components/resource/table/slots.d.ts +41 -0
- package/dist/src/components/resource/table/states.d.ts +91 -0
- package/dist/src/components/resource/table/story-fixtures.d.ts +25 -0
- package/dist/src/components/resource/table/structural-columns.d.ts +25 -0
- package/dist/src/components/resource/table/table-icons.d.ts +16 -0
- package/dist/src/components/resource/table/table-utils.d.ts +48 -0
- package/dist/src/components/resource/table/url-state.d.ts +67 -0
- package/dist/src/components/resource/table/use-container-width.d.ts +10 -0
- package/dist/src/components/resource/table/use-debounced-value.d.ts +1 -0
- package/dist/src/components/resource/testapp-fixtures.d.ts +78 -0
- package/dist/src/components/stat/stat.d.ts +54 -0
- package/dist/src/components/toast/toast-store.d.ts +36 -0
- package/dist/src/components/toast/toaster.d.ts +66 -0
- package/dist/src/components/toast/use-toast.d.ts +24 -0
- package/dist/src/hooks/keys.d.ts +24 -0
- package/dist/src/hooks/multipart.d.ts +44 -0
- package/dist/src/hooks/params.d.ts +25 -0
- package/dist/src/hooks/types.d.ts +41 -0
- package/dist/src/hooks/useChangeHistory.d.ts +30 -0
- package/dist/src/hooks/useResourceImport.d.ts +107 -0
- package/dist/src/hooks/useResourceItem.d.ts +4 -0
- package/dist/src/hooks/useResourceList.d.ts +4 -0
- package/dist/src/hooks/useResourceMutations.d.ts +13 -0
- package/dist/src/hooks/useUnsavedChangesGuard.d.ts +31 -0
- package/dist/src/index.d.ts +113 -0
- package/dist/src/lib/cn.d.ts +8 -0
- package/dist/src/lib/roles.d.ts +45 -0
- package/dist/src/lib/variants.d.ts +33 -0
- package/dist/src/primitives/registry.d.ts +81 -0
- package/dist/src/schema/client.d.ts +90 -0
- package/dist/src/schema/parse.d.ts +101 -0
- package/dist/src/schema/provider.d.ts +20 -0
- package/dist/src/schema/types.d.ts +93 -0
- package/dist/src/schema/useSchema.d.ts +21 -0
- package/dist/src/schema/zod.d.ts +4 -0
- package/dist/src/states/ErrorState.d.ts +43 -0
- package/dist/src/states/PendingButton.d.ts +16 -0
- package/dist/src/states/describe-error.d.ts +25 -0
- package/dist/src/states/hooks.d.ts +10 -0
- package/dist/src/theme/script.d.ts +12 -0
- package/dist/src/theme/theme.d.ts +45 -0
- package/dist/styles.css +248 -0
- package/dist/tailwind-preset.cjs +2 -0
- package/dist/tailwind-preset.cjs.map +1 -0
- package/dist/tailwind-preset.d.ts +140 -0
- package/dist/tailwind-preset.js +183 -0
- package/dist/tailwind-preset.js.map +1 -0
- package/dist/tailwind-v4.css +31 -0
- package/dist/vite-plugin.cjs +2 -0
- package/dist/vite-plugin.cjs.map +1 -0
- package/dist/vite-plugin.d.ts +19 -0
- package/dist/vite-plugin.js +16 -0
- package/dist/vite-plugin.js.map +1 -0
- package/eslint-plugin/index.js +76 -0
- package/eslint-plugin/lib/docs.js +11 -0
- package/eslint-plugin/lib/glob.js +51 -0
- package/eslint-plugin/rules/max-page-lines.js +65 -0
- package/eslint-plugin/rules/no-deep-imports.js +61 -0
- package/eslint-plugin/rules/no-direct-http.js +128 -0
- package/eslint-plugin/rules/no-raw-elements.js +65 -0
- package/eslint-plugin/rules/no-style-literals.js +222 -0
- package/eslint-plugin/rules.json +32 -0
- package/package.json +156 -4
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* karni/no-direct-http — direct HTTP/WS clients (fetch, axios,
|
|
3
|
+
* XMLHttpRequest, new WebSocket) are banned outside the generated client
|
|
4
|
+
* directory (allowPatterns, default **-slash-api). Data access goes through
|
|
5
|
+
* the generated typed hooks and the KarniClient from @karni/ui.
|
|
6
|
+
*/
|
|
7
|
+
import { docsUrl } from "../lib/docs.js";
|
|
8
|
+
import { matchesAny } from "../lib/glob.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_ALLOW_PATTERNS = ["**/api/**"];
|
|
11
|
+
const GLOBAL_OBJECTS = new Set(["window", "globalThis", "self"]);
|
|
12
|
+
|
|
13
|
+
/** True when `name` resolves to a declared (shadowing) binding in scope. */
|
|
14
|
+
function isShadowed(sourceCode, node, name) {
|
|
15
|
+
let scope = sourceCode.getScope(node);
|
|
16
|
+
while (scope) {
|
|
17
|
+
const variable = scope.set.get(name);
|
|
18
|
+
if (variable && variable.defs.length > 0) return true;
|
|
19
|
+
scope = scope.upper;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Matches `fetch(...)`, `window.fetch(...)`, `globalThis.fetch(...)`. */
|
|
25
|
+
function calleeIsGlobal(sourceCode, callee, name) {
|
|
26
|
+
if (callee.type === "Identifier" && callee.name === name) {
|
|
27
|
+
return !isShadowed(sourceCode, callee, name);
|
|
28
|
+
}
|
|
29
|
+
if (
|
|
30
|
+
callee.type === "MemberExpression" &&
|
|
31
|
+
!callee.computed &&
|
|
32
|
+
callee.object.type === "Identifier" &&
|
|
33
|
+
GLOBAL_OBJECTS.has(callee.object.name) &&
|
|
34
|
+
callee.property.type === "Identifier" &&
|
|
35
|
+
callee.property.name === name
|
|
36
|
+
) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function checkAxiosSource(context, sourceNode) {
|
|
43
|
+
if (!sourceNode || sourceNode.type !== "Literal") return;
|
|
44
|
+
if (typeof sourceNode.value !== "string") return;
|
|
45
|
+
if (sourceNode.value !== "axios" && !sourceNode.value.startsWith("axios/")) return;
|
|
46
|
+
context.report({ node: sourceNode, messageId: "axios" });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default {
|
|
50
|
+
meta: {
|
|
51
|
+
type: "problem",
|
|
52
|
+
docs: {
|
|
53
|
+
url: docsUrl("no-direct-http"),
|
|
54
|
+
description:
|
|
55
|
+
"Ban fetch/axios/XMLHttpRequest/new WebSocket outside the generated api client directory: use the generated typed hooks or KarniClient.",
|
|
56
|
+
},
|
|
57
|
+
schema: [
|
|
58
|
+
{
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
allowPatterns: {
|
|
62
|
+
type: "array",
|
|
63
|
+
items: { type: "string" },
|
|
64
|
+
description:
|
|
65
|
+
"Glob patterns of file paths where direct HTTP is allowed (the generated client's home).",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
messages: {
|
|
72
|
+
fetch:
|
|
73
|
+
"Direct fetch() outside the api client directory. Use the generated typed hooks " +
|
|
74
|
+
"(useResourceList/useResourceItem/useResourceMutations) or the KarniClient from " +
|
|
75
|
+
"@karni/ui. Raw HTTP lives only under the allowed api/ paths. " +
|
|
76
|
+
"Opt out only with an explained disable: " +
|
|
77
|
+
"// eslint-disable-next-line karni/no-direct-http -- <reason>",
|
|
78
|
+
axios:
|
|
79
|
+
"Do not add axios. Karni already ships an HTTP layer: the generated typed hooks and " +
|
|
80
|
+
"createKarniClient from @karni/ui. " +
|
|
81
|
+
"Opt out only with an explained disable: " +
|
|
82
|
+
"// eslint-disable-next-line karni/no-direct-http -- <reason>",
|
|
83
|
+
xhr:
|
|
84
|
+
"XMLHttpRequest outside the api client directory. Use the generated typed hooks or " +
|
|
85
|
+
"the KarniClient from @karni/ui instead. " +
|
|
86
|
+
"Opt out only with an explained disable: " +
|
|
87
|
+
"// eslint-disable-next-line karni/no-direct-http -- <reason>",
|
|
88
|
+
websocket:
|
|
89
|
+
"new WebSocket() outside the api client directory. Wrap realtime transports in the " +
|
|
90
|
+
"api/ layer next to the KarniClient so every consumer shares one implementation. " +
|
|
91
|
+
"Opt out only with an explained disable: " +
|
|
92
|
+
"// eslint-disable-next-line karni/no-direct-http -- <reason>",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
create(context) {
|
|
97
|
+
const options = context.options[0] ?? {};
|
|
98
|
+
const allowPatterns = options.allowPatterns ?? DEFAULT_ALLOW_PATTERNS;
|
|
99
|
+
if (matchesAny(context.filename, allowPatterns)) {
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
const { sourceCode } = context;
|
|
103
|
+
return {
|
|
104
|
+
CallExpression(node) {
|
|
105
|
+
if (calleeIsGlobal(sourceCode, node.callee, "fetch")) {
|
|
106
|
+
context.report({ node, messageId: "fetch" });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (node.callee.type === "Identifier" && node.callee.name === "require") {
|
|
110
|
+
checkAxiosSource(context, node.arguments[0]);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
NewExpression(node) {
|
|
114
|
+
if (calleeIsGlobal(sourceCode, node.callee, "XMLHttpRequest")) {
|
|
115
|
+
context.report({ node, messageId: "xhr" });
|
|
116
|
+
} else if (calleeIsGlobal(sourceCode, node.callee, "WebSocket")) {
|
|
117
|
+
context.report({ node, messageId: "websocket" });
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
ImportDeclaration(node) {
|
|
121
|
+
checkAxiosSource(context, node.source);
|
|
122
|
+
},
|
|
123
|
+
ImportExpression(node) {
|
|
124
|
+
checkAxiosSource(context, node.source);
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* karni/no-raw-elements — raw data-entry/table JSX elements are banned in
|
|
3
|
+
* project code; @karni/ui is their only home. Each message names the
|
|
4
|
+
* component to compose instead.
|
|
5
|
+
*/
|
|
6
|
+
import { docsUrl } from "../lib/docs.js";
|
|
7
|
+
import { matchesAny } from "../lib/glob.js";
|
|
8
|
+
|
|
9
|
+
const REPLACEMENTS = {
|
|
10
|
+
table: "ResourceTable",
|
|
11
|
+
form: "ResourceForm",
|
|
12
|
+
input: "Input (or ResourceForm, which renders the right field for you)",
|
|
13
|
+
select: "Select / ResourceSelect",
|
|
14
|
+
textarea: "Textarea (or ResourceForm)",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
meta: {
|
|
19
|
+
type: "problem",
|
|
20
|
+
docs: {
|
|
21
|
+
url: docsUrl("no-raw-elements"),
|
|
22
|
+
description:
|
|
23
|
+
"Ban raw <table>/<form>/<input>/<select>/<textarea> JSX outside @karni/ui: compose the shipped components instead of rebuilding them.",
|
|
24
|
+
},
|
|
25
|
+
schema: [
|
|
26
|
+
{
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
allow: {
|
|
30
|
+
type: "array",
|
|
31
|
+
items: { type: "string" },
|
|
32
|
+
description: "Glob patterns of file paths where raw elements are allowed.",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
messages: {
|
|
39
|
+
rawElement:
|
|
40
|
+
"Raw <{{element}}> is Karni boilerplate. Compose {{replacement}} from @karni/ui instead. " +
|
|
41
|
+
"Opt out only with an explained disable: " +
|
|
42
|
+
"// eslint-disable-next-line karni/no-raw-elements -- <reason>",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
create(context) {
|
|
47
|
+
const options = context.options[0] ?? {};
|
|
48
|
+
const allow = options.allow ?? [];
|
|
49
|
+
if (allow.length > 0 && matchesAny(context.filename, allow)) {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
JSXOpeningElement(node) {
|
|
54
|
+
if (node.name.type !== "JSXIdentifier") return;
|
|
55
|
+
const element = node.name.name;
|
|
56
|
+
if (!Object.hasOwn(REPLACEMENTS, element)) return;
|
|
57
|
+
context.report({
|
|
58
|
+
node: node.name,
|
|
59
|
+
messageId: "rawElement",
|
|
60
|
+
data: { element, replacement: REPLACEMENTS[element] },
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* karni/no-style-literals — every visual value resolves from a design token.
|
|
3
|
+
*
|
|
4
|
+
* @karni/ui's preset maps Tailwind's scales (colours, spacing, type, radius,
|
|
5
|
+
* shadow, stacking, opacity, motion) onto --karni-* custom properties, so a
|
|
6
|
+
* project retheming those properties retheming every component. A class
|
|
7
|
+
* outside those scales compiles to a literal the theme cannot reach: an
|
|
8
|
+
* arbitrary value (`w-[13px]`, `bg-[#fff]`), Tailwind's palette
|
|
9
|
+
* (`bg-slate-500`), an opacity modifier on a colour (`bg-primary/90`), a step
|
|
10
|
+
* the spacing scale does not have, or a literal in an inline style.
|
|
11
|
+
*
|
|
12
|
+
* The rule checks the whitespace-separated class tokens of every string in
|
|
13
|
+
* the file and the values of inline `style={{ ... }}` objects. Computed style
|
|
14
|
+
* values (data-driven widths, percentages) are allowed: only literals fail.
|
|
15
|
+
*/
|
|
16
|
+
import { docsUrl } from "../lib/docs.js";
|
|
17
|
+
|
|
18
|
+
/** The preset's spacing steps (keep in sync with SPACE_STEPS in tailwind-preset.ts). */
|
|
19
|
+
const SPACE_STEPS = new Set([
|
|
20
|
+
"0",
|
|
21
|
+
"px",
|
|
22
|
+
"0.5",
|
|
23
|
+
"1",
|
|
24
|
+
"1.5",
|
|
25
|
+
"2",
|
|
26
|
+
"2.5",
|
|
27
|
+
"3",
|
|
28
|
+
"3.5",
|
|
29
|
+
"4",
|
|
30
|
+
"5",
|
|
31
|
+
"6",
|
|
32
|
+
"7",
|
|
33
|
+
"8",
|
|
34
|
+
"9",
|
|
35
|
+
"10",
|
|
36
|
+
"11",
|
|
37
|
+
"12",
|
|
38
|
+
"14",
|
|
39
|
+
"16",
|
|
40
|
+
"20",
|
|
41
|
+
"24",
|
|
42
|
+
"28",
|
|
43
|
+
"32",
|
|
44
|
+
"36",
|
|
45
|
+
"40",
|
|
46
|
+
"44",
|
|
47
|
+
"48",
|
|
48
|
+
"52",
|
|
49
|
+
"56",
|
|
50
|
+
"60",
|
|
51
|
+
"64",
|
|
52
|
+
"72",
|
|
53
|
+
"80",
|
|
54
|
+
"96",
|
|
55
|
+
"128",
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
const SPACING_UTILITY =
|
|
59
|
+
/^-?(?:p[xytrblse]?|m[xytrblse]?|gap(?:-[xy])?|space-[xy]|w|h|size|min-w|min-h|max-w|max-h|inset(?:-[xy])?|top|right|bottom|left|start|end|translate-[xy]|scroll-[mp][xytrblse]?|basis|indent)-(\d+(?:\.\d+)?)$/;
|
|
60
|
+
|
|
61
|
+
const PALETTE =
|
|
62
|
+
"slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|black|white";
|
|
63
|
+
const COLOR_UTILITY =
|
|
64
|
+
"bg|text|border(?:-[xytrblse])?|ring|ring-offset|outline|fill|stroke|from|via|to|divide|placeholder|decoration|accent|caret|shadow";
|
|
65
|
+
const PALETTE_CLASS = new RegExp(`^(?:${COLOR_UTILITY})-(?:${PALETTE})(?:-\\d+)?(?:/\\d+)?$`);
|
|
66
|
+
const OPACITY_MODIFIER = new RegExp(`^(?:${COLOR_UTILITY})-[a-z][\\w-]*/\\d+$`);
|
|
67
|
+
|
|
68
|
+
/** Classes whose value comes from Tailwind's default theme, not a --karni-* token. */
|
|
69
|
+
const UNMAPPED = [
|
|
70
|
+
[
|
|
71
|
+
/^max-w-(?:xs|sm|md|lg|xl|[2-7]xl|prose|screen-\w+)$/,
|
|
72
|
+
"a named max-width (use a spacing step, e.g. max-w-96)",
|
|
73
|
+
],
|
|
74
|
+
[/^text-(?:[3-9]xl)$/, "a type size outside the --karni-font-size-* scale (xs to 2xl)"],
|
|
75
|
+
[/^-?z-\d+$/, "a numeric z-index (use z-raised, z-drawer, z-popover, z-modal or z-toast)"],
|
|
76
|
+
[/^opacity-(?!(?:0|50|60|70|90|100)$)\d+$/, "an opacity outside the --karni-opacity-* steps"],
|
|
77
|
+
[/^(?:duration|delay)-\d+$/, "a literal duration (transitions read --karni-duration)"],
|
|
78
|
+
[
|
|
79
|
+
/^rounded(?:-[tblrse]{1,2})?-(?:2xl|3xl)$/,
|
|
80
|
+
"a radius outside --karni-radius-* (sm to xl, full)",
|
|
81
|
+
],
|
|
82
|
+
[/^shadow-(?:xl|2xl|inner)$/, "a shadow outside --karni-shadow-* (sm, md, lg)"],
|
|
83
|
+
[/^font-(?:thin|extralight|light|extrabold|black)$/, "a weight outside --karni-font-weight-*"],
|
|
84
|
+
[/^leading-(?!none$)[\w.]+$/, "a line height outside the type scale"],
|
|
85
|
+
[/^tracking-(?:tighter|wider|widest|normal)$/, "letter spacing outside --karni-font-tracking-*"],
|
|
86
|
+
[
|
|
87
|
+
/^(?:border(?:-[xytrblse])?|ring-offset|outline)-(?!0$|2$)\d+$/,
|
|
88
|
+
"a line width outside --karni-border-width / --karni-ring-*",
|
|
89
|
+
],
|
|
90
|
+
[/^ring-(?!0$|1$|2$)\d+$/, "a ring width outside --karni-ring-width*"],
|
|
91
|
+
[/^underline-offset-(?!4$)\d+$/, "an underline offset outside --karni-font-underline-offset"],
|
|
92
|
+
[/^divide-[xy](?:-\d+)?$/, "a divider whose width Tailwind 4 hard-codes (use [&>*+*]:border-t)"],
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const HEX = /#[0-9a-fA-F]{3,8}\b/;
|
|
96
|
+
const LITERAL_IN_VALUE = /\d|#|\b(?:rgb|rgba|hsl|hsla|oklch|oklab|lab|lch|color)\(/;
|
|
97
|
+
|
|
98
|
+
/** The base utility of a class token: variants (`hover:`, `md:`, `data-[x]:`) and `!` stripped. */
|
|
99
|
+
function utilityOf(token) {
|
|
100
|
+
let depth = 0;
|
|
101
|
+
let start = 0;
|
|
102
|
+
for (let i = 0; i < token.length; i += 1) {
|
|
103
|
+
const ch = token[i];
|
|
104
|
+
if (ch === "[") depth += 1;
|
|
105
|
+
else if (ch === "]") depth -= 1;
|
|
106
|
+
else if (ch === ":" && depth === 0) start = i + 1;
|
|
107
|
+
}
|
|
108
|
+
return token.slice(start).replace(/^!/, "");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Why `utility` is a literal, or null when it resolves from a token. */
|
|
112
|
+
function problemOf(utility) {
|
|
113
|
+
const arbitrary = /^-?[\w-]*\[(.+)\]$/.exec(utility);
|
|
114
|
+
if (arbitrary) {
|
|
115
|
+
const value = arbitrary[1]
|
|
116
|
+
.replace(/var\(--[\w-]+(?:,[^)]*)?\)/g, "")
|
|
117
|
+
.replace(/\d+fr\b/g, "")
|
|
118
|
+
.replace(/\b(?:min|max|fit)-content\b/g, "");
|
|
119
|
+
return LITERAL_IN_VALUE.test(value) ? "an arbitrary literal value" : null;
|
|
120
|
+
}
|
|
121
|
+
if (PALETTE_CLASS.test(utility)) return "a Tailwind palette colour (use a --karni-color-* token)";
|
|
122
|
+
if (OPACITY_MODIFIER.test(utility)) {
|
|
123
|
+
return "an opacity modifier on a colour (use a token such as bg-primary-hover or bg-muted-subtle)";
|
|
124
|
+
}
|
|
125
|
+
const spacing = SPACING_UTILITY.exec(utility);
|
|
126
|
+
if (spacing && !SPACE_STEPS.has(spacing[1]))
|
|
127
|
+
return "a step the --karni-space-* scale does not have";
|
|
128
|
+
for (const [pattern, why] of UNMAPPED) if (pattern.test(utility)) return why;
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function checkClasses(context, node, text) {
|
|
133
|
+
if (HEX.test(text)) {
|
|
134
|
+
context.report({
|
|
135
|
+
node,
|
|
136
|
+
messageId: "literal",
|
|
137
|
+
data: { value: HEX.exec(text)[0], why: "a hex colour" },
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
for (const token of text.split(/\s+/)) {
|
|
141
|
+
if (!token) continue;
|
|
142
|
+
const why = problemOf(utilityOf(token));
|
|
143
|
+
if (why) context.report({ node, messageId: "literal", data: { value: token, why } });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function literalStyleValue(node) {
|
|
148
|
+
if (node.type === "Literal") {
|
|
149
|
+
if (typeof node.value === "number") return node.value !== 0;
|
|
150
|
+
return (
|
|
151
|
+
typeof node.value === "string" &&
|
|
152
|
+
LITERAL_IN_VALUE.test(node.value.replace(/var\(--[\w-]+\)/g, ""))
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (node.type === "TemplateLiteral" && node.expressions.length === 0) {
|
|
156
|
+
return LITERAL_IN_VALUE.test(node.quasis[0].value.cooked ?? "");
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export default {
|
|
162
|
+
meta: {
|
|
163
|
+
type: "problem",
|
|
164
|
+
docs: {
|
|
165
|
+
url: docsUrl("no-style-literals"),
|
|
166
|
+
description:
|
|
167
|
+
"Every visual value resolves from a --karni-* token: no arbitrary literal values, palette colours, colour opacity modifiers or literal inline styles.",
|
|
168
|
+
},
|
|
169
|
+
schema: [],
|
|
170
|
+
messages: {
|
|
171
|
+
literal:
|
|
172
|
+
'"{{value}}" is {{why}}: the theme cannot reach it. Use a class from the @karni/ui preset ' +
|
|
173
|
+
"(it resolves from --karni-* tokens), or add a token to styles.css and the preset. " +
|
|
174
|
+
"Opt out only with an explained disable: " +
|
|
175
|
+
"// eslint-disable-next-line karni/no-style-literals -- <reason>",
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
create(context) {
|
|
180
|
+
return {
|
|
181
|
+
Literal(node) {
|
|
182
|
+
if (typeof node.value !== "string") return;
|
|
183
|
+
if (
|
|
184
|
+
node.parent?.type === "ImportDeclaration" ||
|
|
185
|
+
node.parent?.type === "ExportNamedDeclaration"
|
|
186
|
+
)
|
|
187
|
+
return;
|
|
188
|
+
if (node.parent?.type === "Property" && node.parent.key === node) return;
|
|
189
|
+
if (insideStyleObject(node)) return;
|
|
190
|
+
checkClasses(context, node, node.value);
|
|
191
|
+
},
|
|
192
|
+
TemplateElement(node) {
|
|
193
|
+
if (insideStyleObject(node)) return;
|
|
194
|
+
checkClasses(context, node, node.value.cooked ?? "");
|
|
195
|
+
},
|
|
196
|
+
JSXAttribute(node) {
|
|
197
|
+
if (node.name.name !== "style") return;
|
|
198
|
+
const expression =
|
|
199
|
+
node.value?.type === "JSXExpressionContainer" ? node.value.expression : null;
|
|
200
|
+
if (expression?.type !== "ObjectExpression") return;
|
|
201
|
+
for (const property of expression.properties) {
|
|
202
|
+
if (property.type === "Property" && literalStyleValue(property.value)) {
|
|
203
|
+
context.report({
|
|
204
|
+
node: property.value,
|
|
205
|
+
messageId: "literal",
|
|
206
|
+
data: { value: context.sourceCode.getText(property), why: "a literal inline style" },
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/** Inline style values are checked by the JSXAttribute visitor, not as class strings. */
|
|
216
|
+
function insideStyleObject(node) {
|
|
217
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
218
|
+
if (current.type === "JSXAttribute") return current.name.name === "style";
|
|
219
|
+
if (current.type === "JSXElement" || current.type === "Program") return false;
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "karni/no-raw-elements",
|
|
4
|
+
"title": "Raw <table>/<form>/<input>/<select>/<textarea> in project code",
|
|
5
|
+
"why": "@karni/ui ships these as schema-driven components (sorting, pagination, validation, accessibility). Rebuilding them per page is the boilerplate Karni exists to remove, and the copies drift.",
|
|
6
|
+
"fix": "Compose the component the message names: ResourceTable for <table>, ResourceForm for <form>, Input / Select / ResourceSelect / Textarea for fields (ResourceForm renders the right field for you). Opt out only with `// eslint-disable-next-line karni/no-raw-elements -- <reason>`."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "karni/max-page-lines",
|
|
10
|
+
"title": "A page file is longer than its cap (50 lines by default)",
|
|
11
|
+
"why": "Pages compose @karni/ui components. A long page is rebuilding something Karni ships.",
|
|
12
|
+
"fix": "Compose Page / ResourceTable / ResourceForm / ResourceDetail and their props, or move logic into hooks and components. Opt out only with `/* eslint-disable karni/max-page-lines -- <reason> */`."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "karni/no-deep-imports",
|
|
16
|
+
"title": "An import reaches into @karni/ui internals",
|
|
17
|
+
"why": "Only the package root is public API (docs/stability.md). Internals change without notice, and a vendored copy forks the component.",
|
|
18
|
+
"fix": "Import from \"@karni/ui\" and compose or extend the exported component. Opt out only with `// eslint-disable-next-line karni/no-deep-imports -- <reason>`."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "karni/no-direct-http",
|
|
22
|
+
"title": "fetch / axios / XMLHttpRequest / WebSocket outside the api client",
|
|
23
|
+
"why": "The generated typed hooks and KarniClient handle auth, errors, retries and types in one place. Ad-hoc HTTP calls bypass all of it.",
|
|
24
|
+
"fix": "Use the generated typed hooks (useResourceList / useResourceItem / useResourceMutations) or createKarniClient from @karni/ui, and keep transports under the api/ directory. Opt out only with `// eslint-disable-next-line karni/no-direct-http -- <reason>`."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "karni/no-style-literals",
|
|
28
|
+
"title": "A class or inline style holds a literal value instead of a design token",
|
|
29
|
+
"why": "@karni/ui's preset resolves every colour, spacing step, radius, type size, shadow, z-index, opacity and duration from a --karni-* custom property, so one stylesheet (or `karni ui detect`) rethemes every component. A literal (`w-[13px]`, `bg-slate-500`, `bg-primary/90`, `style={{ color: \"#333\" }}`) is out of the theme's reach.",
|
|
30
|
+
"fix": "Use a class from the @karni/ui preset (`bg-primary-hover`, `bg-muted-subtle`, `max-w-96`, `z-popover`), or add the token to styles.css and the preset. @karni/ui enables this rule for its own source. A project opts in with `\"karni/no-style-literals\": \"error\"`. Opt out only with `// eslint-disable-next-line karni/no-style-literals -- <reason>`."
|
|
31
|
+
}
|
|
32
|
+
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,158 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karni/ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "React components for Karni: schema-driven tables, forms, detail views and auth. Karni is an open-source Django + React framework for building with AI coding agents. Your agent writes only your domain. The API, admin, permissions and React screens come from a package you upgrade, not code it rewrites in each project. One rule set covers Claude Code, Cursor and Copilot. The build fails when an agent hand-writes what Karni ships.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"django",
|
|
8
|
+
"django-rest-framework",
|
|
9
|
+
"openapi",
|
|
10
|
+
"typescript",
|
|
11
|
+
"components",
|
|
12
|
+
"ui",
|
|
13
|
+
"tanstack-query",
|
|
14
|
+
"react-hook-form",
|
|
15
|
+
"zod",
|
|
16
|
+
"tailwind",
|
|
17
|
+
"radix",
|
|
18
|
+
"ai-agents",
|
|
19
|
+
"karni"
|
|
20
|
+
],
|
|
21
|
+
"license": "Apache-2.0",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/cbsshekhawat18-lab/karni.git",
|
|
25
|
+
"directory": "packages/karni-ui"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://github.com/cbsshekhawat18-lab/karni/tree/main/packages/karni-ui#readme",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/cbsshekhawat18-lab/karni/issues"
|
|
30
|
+
},
|
|
31
|
+
"type": "module",
|
|
32
|
+
"sideEffects": [
|
|
33
|
+
"**/*.css"
|
|
34
|
+
],
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"eslint-plugin",
|
|
38
|
+
"!eslint-plugin/**/*.test.*",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20"
|
|
43
|
+
},
|
|
44
|
+
"exports": {
|
|
45
|
+
".": {
|
|
46
|
+
"types": "./dist/src/index.d.ts",
|
|
47
|
+
"import": "./dist/index.js",
|
|
48
|
+
"require": "./dist/index.cjs"
|
|
49
|
+
},
|
|
50
|
+
"./styles.css": "./dist/styles.css",
|
|
51
|
+
"./tailwind-v4.css": "./dist/tailwind-v4.css",
|
|
52
|
+
"./tailwind-preset": {
|
|
53
|
+
"types": "./dist/tailwind-preset.d.ts",
|
|
54
|
+
"import": "./dist/tailwind-preset.js",
|
|
55
|
+
"require": "./dist/tailwind-preset.cjs"
|
|
56
|
+
},
|
|
57
|
+
"./vite": {
|
|
58
|
+
"types": "./dist/vite-plugin.d.ts",
|
|
59
|
+
"import": "./dist/vite-plugin.js",
|
|
60
|
+
"require": "./dist/vite-plugin.cjs"
|
|
61
|
+
},
|
|
62
|
+
"./manifest.json": "./dist/manifest.json",
|
|
63
|
+
"./eslint-plugin": "./eslint-plugin/index.js",
|
|
64
|
+
"./package.json": "./package.json"
|
|
65
|
+
},
|
|
66
|
+
"main": "./dist/index.cjs",
|
|
67
|
+
"module": "./dist/index.js",
|
|
68
|
+
"types": "./dist/src/index.d.ts",
|
|
69
|
+
"scripts": {
|
|
70
|
+
"dev": "vite build --watch",
|
|
71
|
+
"build": "tsc --noEmit && eslint src --max-warnings 0 && vite build && node scripts/generate-manifest.mjs",
|
|
72
|
+
"test": "vitest run",
|
|
73
|
+
"test:watch": "vitest",
|
|
74
|
+
"lint": "eslint .",
|
|
75
|
+
"typecheck": "tsc --noEmit",
|
|
76
|
+
"format": "prettier --write .",
|
|
77
|
+
"format:check": "prettier --check .",
|
|
78
|
+
"storybook": "storybook dev -p 6006",
|
|
79
|
+
"build-storybook": "storybook build"
|
|
80
|
+
},
|
|
81
|
+
"msw": {
|
|
82
|
+
"workerDirectory": [
|
|
83
|
+
"public"
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
"peerDependencies": {
|
|
87
|
+
"@hookform/resolvers": "^3.0.0",
|
|
88
|
+
"@tanstack/react-query": "^5.0.0",
|
|
89
|
+
"@tanstack/react-table": "^8.0.0",
|
|
90
|
+
"eslint": ">=9.0.0",
|
|
91
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
92
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
93
|
+
"react-hook-form": "^7.0.0",
|
|
94
|
+
"zod": "^3.23.0"
|
|
95
|
+
},
|
|
96
|
+
"dependencies": {
|
|
97
|
+
"@eslint-community/eslint-plugin-eslint-comments": "^4.8.1",
|
|
98
|
+
"@radix-ui/react-checkbox": "^1.4.1",
|
|
99
|
+
"@radix-ui/react-dialog": "^1.3.1",
|
|
100
|
+
"@radix-ui/react-dropdown-menu": "^2.2.1",
|
|
101
|
+
"@radix-ui/react-label": "^2.1.17",
|
|
102
|
+
"@radix-ui/react-popover": "^1.3.1",
|
|
103
|
+
"@radix-ui/react-select": "^2.4.1",
|
|
104
|
+
"@radix-ui/react-switch": "^1.4.1",
|
|
105
|
+
"@radix-ui/react-tabs": "^1.2.1",
|
|
106
|
+
"@radix-ui/react-toast": "^1.3.1",
|
|
107
|
+
"@radix-ui/react-visually-hidden": "^1.1.0",
|
|
108
|
+
"clsx": "^2.1.1",
|
|
109
|
+
"tailwind-merge": "^2.5.2"
|
|
110
|
+
},
|
|
111
|
+
"devDependencies": {
|
|
112
|
+
"@eslint/js": "^9.39.5",
|
|
113
|
+
"@hookform/resolvers": "^3.9.0",
|
|
114
|
+
"@storybook/react": "^10.6.3",
|
|
115
|
+
"@storybook/react-vite": "^10.6.3",
|
|
116
|
+
"@tailwindcss/postcss": "^4.3.3",
|
|
117
|
+
"@tanstack/react-query": "^5.104.3",
|
|
118
|
+
"@tanstack/react-table": "^8.20.5",
|
|
119
|
+
"@testing-library/jest-dom": "^6.5.0",
|
|
120
|
+
"@testing-library/react": "^16.0.1",
|
|
121
|
+
"@testing-library/user-event": "^14.6.8",
|
|
122
|
+
"@types/node": "^24.0.0",
|
|
123
|
+
"@types/react": "^18.3.5",
|
|
124
|
+
"@types/react-dom": "^18.3.0",
|
|
125
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
126
|
+
"axe-core": "^4.14.0",
|
|
127
|
+
"eslint": "^9.39.5",
|
|
128
|
+
"eslint-config-prettier": "^9.1.0",
|
|
129
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
130
|
+
"globals": "^15.15.0",
|
|
131
|
+
"jsdom": "^25.0.0",
|
|
132
|
+
"msw": "^2.4.4",
|
|
133
|
+
"msw-storybook-addon": "^3.0.3",
|
|
134
|
+
"playwright": "^1.64.1",
|
|
135
|
+
"postcss": "^8.5.30",
|
|
136
|
+
"prettier": "^3.9.10",
|
|
137
|
+
"react": "^18.3.1",
|
|
138
|
+
"react-dom": "^18.3.1",
|
|
139
|
+
"react-hook-form": "^7.90.0",
|
|
140
|
+
"storybook": "^10.6.3",
|
|
141
|
+
"tailwindcss": "^4.3.3",
|
|
142
|
+
"typescript": "^5.6.2",
|
|
143
|
+
"typescript-eslint": "^8.71.2",
|
|
144
|
+
"vite": "^8.3.4",
|
|
145
|
+
"vite-plugin-dts": "^5.1.2",
|
|
146
|
+
"vitest": "^5.0.5",
|
|
147
|
+
"zod": "^3.23.8"
|
|
148
|
+
},
|
|
149
|
+
"peerDependenciesMeta": {
|
|
150
|
+
"eslint": {
|
|
151
|
+
"optional": true
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"publishConfig": {
|
|
155
|
+
"access": "public",
|
|
156
|
+
"provenance": true
|
|
157
|
+
}
|
|
158
|
+
}
|