@karni/ui 0.0.0-stage → 0.3.1
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 +288 -2
- package/dist/index.cjs +9 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +4764 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.json +85 -0
- package/dist/mockServiceWorker.js +361 -0
- package/dist/src/auth/AuthGuard.d.ts +12 -0
- package/dist/src/auth/LoginForm.d.ts +29 -0
- package/dist/src/auth/engine.d.ts +99 -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 +74 -0
- package/dist/src/components/empty-state/empty-state.d.ts +20 -0
- package/dist/src/components/file-upload/FileUpload.d.ts +44 -0
- package/dist/src/components/layout/layout.d.ts +55 -0
- package/dist/src/components/modal/modal.d.ts +45 -0
- package/dist/src/components/page/page.d.ts +29 -0
- package/dist/src/components/primitives/button.d.ts +44 -0
- package/dist/src/components/primitives/checkbox.d.ts +5 -0
- package/dist/src/components/primitives/dropdown-menu.d.ts +24 -0
- package/dist/src/components/primitives/icons.d.ts +11 -0
- package/dist/src/components/primitives/input.d.ts +4 -0
- package/dist/src/components/primitives/label.d.ts +5 -0
- package/dist/src/components/primitives/select.d.ts +63 -0
- package/dist/src/components/primitives/skeleton.d.ts +4 -0
- package/dist/src/components/primitives/switch.d.ts +5 -0
- package/dist/src/components/primitives/tabs.d.ts +10 -0
- package/dist/src/components/primitives/textarea.d.ts +4 -0
- package/dist/src/components/resource/ResourceDetail.d.ts +7 -0
- package/dist/src/components/resource/ResourceForm.d.ts +7 -0
- package/dist/src/components/resource/ResourceSelect.d.ts +7 -0
- package/dist/src/components/resource/ResourceTable.d.ts +11 -0
- package/dist/src/components/resource/audit-fields.d.ts +17 -0
- package/dist/src/components/resource/detail/ResourceDetail.d.ts +30 -0
- package/dist/src/components/resource/displayLabel.d.ts +12 -0
- package/dist/src/components/resource/form/ResourceForm.d.ts +50 -0
- package/dist/src/components/resource/form/field-inputs.d.ts +20 -0
- package/dist/src/components/resource/form/fixtures.d.ts +24 -0
- package/dist/src/components/resource/form/prepare.d.ts +54 -0
- package/dist/src/components/resource/history/ChangeHistory.d.ts +18 -0
- package/dist/src/components/resource/import/ResourceImport.d.ts +18 -0
- package/dist/src/components/resource/select/ResourceSelect.d.ts +36 -0
- package/dist/src/components/resource/select/use-debounced-value.d.ts +2 -0
- package/dist/src/components/resource/table/BulkActionsBar.d.ts +20 -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 +19 -0
- package/dist/src/components/resource/table/ResourceTable.d.ts +72 -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/table-utils.d.ts +24 -0
- package/dist/src/components/resource/table/use-debounced-value.d.ts +1 -0
- package/dist/src/components/stat/stat.d.ts +24 -0
- package/dist/src/components/toast/toast-store.d.ts +36 -0
- package/dist/src/components/toast/toaster.d.ts +33 -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 +29 -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/index.d.ts +95 -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/schema/client.d.ts +90 -0
- package/dist/src/schema/parse.d.ts +101 -0
- package/dist/src/schema/provider.d.ts +13 -0
- package/dist/src/schema/types.d.ts +84 -0
- package/dist/src/schema/useSchema.d.ts +17 -0
- package/dist/src/schema/zod.d.ts +4 -0
- package/dist/styles.css +87 -0
- package/dist/tailwind-preset.cjs +2 -0
- package/dist/tailwind-preset.cjs.map +1 -0
- package/dist/tailwind-preset.d.ts +58 -0
- package/dist/tailwind-preset.js +64 -0
- package/dist/tailwind-preset.js.map +1 -0
- package/eslint-plugin/index.js +72 -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.json +26 -0
- package/package.json +153 -4
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { ReactElement, ReactNode } from 'react';
|
|
3
|
+
import { KarniClient } from './client';
|
|
4
|
+
export interface KarniProviderProps {
|
|
5
|
+
client: KarniClient;
|
|
6
|
+
/** Bring your own QueryClient; one is created (once) when omitted. */
|
|
7
|
+
queryClient?: QueryClient;
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
/** Provides the Karni client and a TanStack QueryClient to everything inside it; render it once at the app root. */
|
|
11
|
+
export declare function KarniProvider(props: KarniProviderProps): ReactElement;
|
|
12
|
+
/** Returns the Karni client from `KarniProvider`; throws a descriptive error outside one. */
|
|
13
|
+
export declare function useKarniClient(): KarniClient;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized schema registry types (karni-ui-contract.md "Schema client" + D1).
|
|
3
|
+
*
|
|
4
|
+
* The OpenAPI document served by the karni backend (drf-spectacular, OAS 3.0.3,
|
|
5
|
+
* COMPONENT_SPLIT_REQUEST=True per api contract B14) is parsed ONCE into these
|
|
6
|
+
* shapes; every generic component consumes FieldDef/ResourceSchema, never raw
|
|
7
|
+
* OpenAPI.
|
|
8
|
+
*/
|
|
9
|
+
export type FieldKind = "string" | "text" | "number" | "integer" | "boolean" | "date" | "datetime" | "enum" | "relation" | "file" | "json" | "array" | "unknown";
|
|
10
|
+
/** Derived from the x-karni-relation vendor extension (api contract B13). */
|
|
11
|
+
export interface RelationRef {
|
|
12
|
+
resource: string;
|
|
13
|
+
many: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface FieldDef {
|
|
16
|
+
name: string;
|
|
17
|
+
/** Humanized from the property name (or the schema `title` when present). */
|
|
18
|
+
label: string;
|
|
19
|
+
kind: FieldKind;
|
|
20
|
+
/** From the CREATE request body's required array only (D1). */
|
|
21
|
+
required: boolean;
|
|
22
|
+
/** Flagged readOnly OR present only in the response component (D1). */
|
|
23
|
+
readOnly: boolean;
|
|
24
|
+
/** Flagged writeOnly OR present only in the request component (D1). */
|
|
25
|
+
writeOnly: boolean;
|
|
26
|
+
nullable: boolean;
|
|
27
|
+
/** Enum members; BlankEnum's "" and NullEnum's null are never members (D1/D3). */
|
|
28
|
+
enumValues?: ReadonlyArray<string | number>;
|
|
29
|
+
/** Choice labels by value, from drf-spectacular's "* `value` - Label" description lines. */
|
|
30
|
+
enumLabels?: Readonly<Record<string, string>>;
|
|
31
|
+
/** The schema's `default` (a model default): forms start from it on create. */
|
|
32
|
+
default?: unknown;
|
|
33
|
+
maxLength?: number;
|
|
34
|
+
min?: number;
|
|
35
|
+
max?: number;
|
|
36
|
+
format?: string;
|
|
37
|
+
/** Schema `description`, surfaced as field help text. */
|
|
38
|
+
description?: string;
|
|
39
|
+
/** Present only when x-karni-relation was emitted (degrades gracefully). */
|
|
40
|
+
relation?: RelationRef;
|
|
41
|
+
/**
|
|
42
|
+
* Underlying pk value type of a relation field, derived from the property's
|
|
43
|
+
* own emitted type/format — never assumed (D3). Drives zodFor's relation
|
|
44
|
+
* value schema (uuid string vs integer).
|
|
45
|
+
*/
|
|
46
|
+
relationValueType?: "uuid" | "integer" | "string";
|
|
47
|
+
/** Declaration order in the schema (response props first, request-only appended). */
|
|
48
|
+
order: number;
|
|
49
|
+
}
|
|
50
|
+
export interface OperationDef {
|
|
51
|
+
path: string;
|
|
52
|
+
method: "get" | "post" | "put" | "patch" | "delete";
|
|
53
|
+
operationId?: string;
|
|
54
|
+
/** Declared query parameter names — the filter bar derives from these (D5). */
|
|
55
|
+
queryParams: readonly string[];
|
|
56
|
+
/** Allowed expand keys when the expand param declares an enum (D5). */
|
|
57
|
+
expandEnum?: readonly string[];
|
|
58
|
+
/** True when the operation declares a `search` query parameter (api B14). */
|
|
59
|
+
hasSearch?: boolean;
|
|
60
|
+
}
|
|
61
|
+
export interface ResourceSchema {
|
|
62
|
+
/** Router prefix, e.g. "events" (from x-karni-resource). */
|
|
63
|
+
resource: string;
|
|
64
|
+
operations: {
|
|
65
|
+
list?: OperationDef;
|
|
66
|
+
detail?: OperationDef;
|
|
67
|
+
create?: OperationDef;
|
|
68
|
+
update?: OperationDef;
|
|
69
|
+
remove?: OperationDef;
|
|
70
|
+
/** `GET /<resource>/export/` — the list as a CSV/XLSX file (karni.transfer, P6). */
|
|
71
|
+
export?: OperationDef;
|
|
72
|
+
/** `POST /<resource>/import/` — the import flow (upload, preview, commit). */
|
|
73
|
+
import?: OperationDef;
|
|
74
|
+
};
|
|
75
|
+
/** Ordered by declaration; unknown constructs land as kind "unknown". */
|
|
76
|
+
fields: readonly FieldDef[];
|
|
77
|
+
}
|
|
78
|
+
/** The whole parsed registry; produced once per schema fetch by parseOpenApi. */
|
|
79
|
+
export interface ParsedSchema {
|
|
80
|
+
/** Keyed by router prefix (x-karni-resource). */
|
|
81
|
+
resources: Record<string, ResourceSchema>;
|
|
82
|
+
/** From the schema root's x-karni-auth-mode (api B14); undefined when absent. */
|
|
83
|
+
authMode?: "jwt" | "session";
|
|
84
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { UseQueryResult } from '@tanstack/react-query';
|
|
2
|
+
import { ParsedSchema, ResourceSchema } from './types';
|
|
3
|
+
/** D4-pinned query key for the parsed schema document. */
|
|
4
|
+
export declare const SCHEMA_QUERY_KEY: readonly ["karni", "$schema"];
|
|
5
|
+
/**
|
|
6
|
+
* The whole parsed registry (resources + root x-karni-auth-mode). Internal to
|
|
7
|
+
* the package — the auth layer reads `data.authMode` from here; apps use
|
|
8
|
+
* useSchema(resource).
|
|
9
|
+
*/
|
|
10
|
+
export declare function useParsedSchema(): UseQueryResult<ParsedSchema>;
|
|
11
|
+
export interface UseSchemaResult {
|
|
12
|
+
data: ResourceSchema | undefined;
|
|
13
|
+
isLoading: boolean;
|
|
14
|
+
error: unknown;
|
|
15
|
+
}
|
|
16
|
+
/** Returns one resource's parsed schema (fields, types, relations, operations) from the backend's OpenAPI document, fetched once per app. */
|
|
17
|
+
export declare function useSchema(resource: string): UseSchemaResult;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { ZodObject, ZodRawShape } from 'zod';
|
|
2
|
+
import { ResourceSchema } from './types';
|
|
3
|
+
/** Builds a Zod validator for a resource's create or update payload from its schema. */
|
|
4
|
+
export declare function zodFor(schema: ResourceSchema, mode: "create" | "update"): ZodObject<ZodRawShape>;
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @karni/ui design tokens.
|
|
3
|
+
*
|
|
4
|
+
* Every color used by @karni/ui components resolves to one of these
|
|
5
|
+
* custom properties (HSL triplets, consumed as `hsl(var(--karni-*) / <alpha>)`
|
|
6
|
+
* by the Tailwind preset). Override any token in your own stylesheet to
|
|
7
|
+
* retheme the whole library; add the `dark` class to <html> (or any ancestor)
|
|
8
|
+
* to switch to the dark palette.
|
|
9
|
+
*/
|
|
10
|
+
:root {
|
|
11
|
+
color-scheme: light;
|
|
12
|
+
|
|
13
|
+
--karni-background: 0 0% 100%;
|
|
14
|
+
--karni-foreground: 224 71.4% 4.1%;
|
|
15
|
+
|
|
16
|
+
--karni-card: 0 0% 100%;
|
|
17
|
+
--karni-card-foreground: 224 71.4% 4.1%;
|
|
18
|
+
|
|
19
|
+
--karni-popover: 0 0% 100%;
|
|
20
|
+
--karni-popover-foreground: 224 71.4% 4.1%;
|
|
21
|
+
|
|
22
|
+
--karni-primary: 220.9 39.3% 11%;
|
|
23
|
+
--karni-primary-foreground: 210 20% 98%;
|
|
24
|
+
|
|
25
|
+
--karni-secondary: 220 14.3% 95.9%;
|
|
26
|
+
--karni-secondary-foreground: 220.9 39.3% 11%;
|
|
27
|
+
|
|
28
|
+
--karni-muted: 220 14.3% 95.9%;
|
|
29
|
+
--karni-muted-foreground: 220 8.9% 46.1%;
|
|
30
|
+
|
|
31
|
+
--karni-accent: 220 14.3% 95.9%;
|
|
32
|
+
--karni-accent-foreground: 220.9 39.3% 11%;
|
|
33
|
+
|
|
34
|
+
--karni-destructive: 0 84.2% 60.2%;
|
|
35
|
+
--karni-destructive-foreground: 210 20% 98%;
|
|
36
|
+
|
|
37
|
+
--karni-border: 220 13% 91%;
|
|
38
|
+
--karni-input: 220 13% 91%;
|
|
39
|
+
--karni-ring: 224 71.4% 4.1%;
|
|
40
|
+
|
|
41
|
+
--karni-radius: 0.5rem;
|
|
42
|
+
|
|
43
|
+
/* DataChart series colors (chart-1 .. chart-5). */
|
|
44
|
+
--karni-chart-1: 220 70% 50%;
|
|
45
|
+
--karni-chart-2: 160 60% 45%;
|
|
46
|
+
--karni-chart-3: 30 80% 55%;
|
|
47
|
+
--karni-chart-4: 280 65% 60%;
|
|
48
|
+
--karni-chart-5: 340 75% 55%;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.dark {
|
|
52
|
+
color-scheme: dark;
|
|
53
|
+
|
|
54
|
+
--karni-background: 224 71.4% 4.1%;
|
|
55
|
+
--karni-foreground: 210 20% 98%;
|
|
56
|
+
|
|
57
|
+
--karni-card: 224 71.4% 4.1%;
|
|
58
|
+
--karni-card-foreground: 210 20% 98%;
|
|
59
|
+
|
|
60
|
+
--karni-popover: 224 71.4% 4.1%;
|
|
61
|
+
--karni-popover-foreground: 210 20% 98%;
|
|
62
|
+
|
|
63
|
+
--karni-primary: 210 20% 98%;
|
|
64
|
+
--karni-primary-foreground: 220.9 39.3% 11%;
|
|
65
|
+
|
|
66
|
+
--karni-secondary: 215 27.9% 16.9%;
|
|
67
|
+
--karni-secondary-foreground: 210 20% 98%;
|
|
68
|
+
|
|
69
|
+
--karni-muted: 215 27.9% 16.9%;
|
|
70
|
+
--karni-muted-foreground: 217.9 10.6% 64.9%;
|
|
71
|
+
|
|
72
|
+
--karni-accent: 215 27.9% 16.9%;
|
|
73
|
+
--karni-accent-foreground: 210 20% 98%;
|
|
74
|
+
|
|
75
|
+
--karni-destructive: 0 62.8% 30.6%;
|
|
76
|
+
--karni-destructive-foreground: 210 20% 98%;
|
|
77
|
+
|
|
78
|
+
--karni-border: 215 27.9% 16.9%;
|
|
79
|
+
--karni-input: 215 27.9% 16.9%;
|
|
80
|
+
--karni-ring: 216 12.2% 83.9%;
|
|
81
|
+
|
|
82
|
+
--karni-chart-1: 217 91% 60%;
|
|
83
|
+
--karni-chart-2: 160 60% 45%;
|
|
84
|
+
--karni-chart-3: 30 80% 55%;
|
|
85
|
+
--karni-chart-4: 280 65% 60%;
|
|
86
|
+
--karni-chart-5: 340 75% 55%;
|
|
87
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const a={darkMode:"class",theme:{extend:{colors:{background:"hsl(var(--karni-background) / <alpha-value>)",foreground:"hsl(var(--karni-foreground) / <alpha-value>)",card:{DEFAULT:"hsl(var(--karni-card) / <alpha-value>)",foreground:"hsl(var(--karni-card-foreground) / <alpha-value>)"},popover:{DEFAULT:"hsl(var(--karni-popover) / <alpha-value>)",foreground:"hsl(var(--karni-popover-foreground) / <alpha-value>)"},primary:{DEFAULT:"hsl(var(--karni-primary) / <alpha-value>)",foreground:"hsl(var(--karni-primary-foreground) / <alpha-value>)"},secondary:{DEFAULT:"hsl(var(--karni-secondary) / <alpha-value>)",foreground:"hsl(var(--karni-secondary-foreground) / <alpha-value>)"},muted:{DEFAULT:"hsl(var(--karni-muted) / <alpha-value>)",foreground:"hsl(var(--karni-muted-foreground) / <alpha-value>)"},accent:{DEFAULT:"hsl(var(--karni-accent) / <alpha-value>)",foreground:"hsl(var(--karni-accent-foreground) / <alpha-value>)"},destructive:{DEFAULT:"hsl(var(--karni-destructive) / <alpha-value>)",foreground:"hsl(var(--karni-destructive-foreground) / <alpha-value>)"},border:"hsl(var(--karni-border) / <alpha-value>)",input:"hsl(var(--karni-input) / <alpha-value>)",ring:"hsl(var(--karni-ring) / <alpha-value>)",chart:{1:"hsl(var(--karni-chart-1) / <alpha-value>)",2:"hsl(var(--karni-chart-2) / <alpha-value>)",3:"hsl(var(--karni-chart-3) / <alpha-value>)",4:"hsl(var(--karni-chart-4) / <alpha-value>)",5:"hsl(var(--karni-chart-5) / <alpha-value>)"}},borderColor:{DEFAULT:"hsl(var(--karni-border) / <alpha-value>)"},borderRadius:{lg:"var(--karni-radius)",md:"calc(var(--karni-radius) - 2px)",sm:"calc(var(--karni-radius) - 4px)"}}}};exports.default=a;
|
|
2
|
+
//# sourceMappingURL=tailwind-preset.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tailwind-preset.cjs","sources":["../tailwind-preset.ts"],"sourcesContent":["/**\n * Tailwind (v3) preset for @karni/ui consumers.\n *\n * Usage (tailwind.config.ts of the consuming app):\n *\n * ```ts\n * import karniPreset from \"@karni/ui/tailwind-preset\";\n *\n * export default {\n * presets: [karniPreset],\n * content: [\n * \"./src/**\\/*.{ts,tsx}\",\n * \"./node_modules/@karni/ui/dist/**\\/*.js\",\n * ],\n * };\n * ```\n *\n * Also import \"@karni/ui/styles.css\" once (it defines the --karni-* tokens).\n * Tailwind v4 users: see the README recipe (@source into dist) instead.\n */\nimport type { Config } from \"tailwindcss\";\n\nconst karniPreset = {\n // `as const` keeps the literal type so the preset stays assignable to\n // Config[\"presets\"] when consumers spread it into their own config.\n darkMode: \"class\" as const,\n theme: {\n extend: {\n colors: {\n background: \"hsl(var(--karni-background) / <alpha-value>)\",\n foreground: \"hsl(var(--karni-foreground) / <alpha-value>)\",\n card: {\n DEFAULT: \"hsl(var(--karni-card) / <alpha-value>)\",\n foreground: \"hsl(var(--karni-card-foreground) / <alpha-value>)\",\n },\n popover: {\n DEFAULT: \"hsl(var(--karni-popover) / <alpha-value>)\",\n foreground: \"hsl(var(--karni-popover-foreground) / <alpha-value>)\",\n },\n primary: {\n DEFAULT: \"hsl(var(--karni-primary) / <alpha-value>)\",\n foreground: \"hsl(var(--karni-primary-foreground) / <alpha-value>)\",\n },\n secondary: {\n DEFAULT: \"hsl(var(--karni-secondary) / <alpha-value>)\",\n foreground: \"hsl(var(--karni-secondary-foreground) / <alpha-value>)\",\n },\n muted: {\n DEFAULT: \"hsl(var(--karni-muted) / <alpha-value>)\",\n foreground: \"hsl(var(--karni-muted-foreground) / <alpha-value>)\",\n },\n accent: {\n DEFAULT: \"hsl(var(--karni-accent) / <alpha-value>)\",\n foreground: \"hsl(var(--karni-accent-foreground) / <alpha-value>)\",\n },\n destructive: {\n DEFAULT: \"hsl(var(--karni-destructive) / <alpha-value>)\",\n foreground: \"hsl(var(--karni-destructive-foreground) / <alpha-value>)\",\n },\n border: \"hsl(var(--karni-border) / <alpha-value>)\",\n input: \"hsl(var(--karni-input) / <alpha-value>)\",\n ring: \"hsl(var(--karni-ring) / <alpha-value>)\",\n chart: {\n \"1\": \"hsl(var(--karni-chart-1) / <alpha-value>)\",\n \"2\": \"hsl(var(--karni-chart-2) / <alpha-value>)\",\n \"3\": \"hsl(var(--karni-chart-3) / <alpha-value>)\",\n \"4\": \"hsl(var(--karni-chart-4) / <alpha-value>)\",\n \"5\": \"hsl(var(--karni-chart-5) / <alpha-value>)\",\n },\n },\n borderColor: {\n // A bare `border` class uses the token, never Tailwind's default gray.\n DEFAULT: \"hsl(var(--karni-border) / <alpha-value>)\",\n },\n borderRadius: {\n lg: \"var(--karni-radius)\",\n md: \"calc(var(--karni-radius) - 2px)\",\n sm: \"calc(var(--karni-radius) - 4px)\",\n },\n },\n },\n} satisfies Omit<Config, \"content\">;\n\nexport default karniPreset;\n"],"names":["karniPreset"],"mappings":"4GAsBA,MAAMA,EAAc,CAGlB,SAAU,QACV,MAAO,CACL,OAAQ,CACN,OAAQ,CACN,WAAY,+CACZ,WAAY,+CACZ,KAAM,CACJ,QAAS,yCACT,WAAY,mDAAA,EAEd,QAAS,CACP,QAAS,4CACT,WAAY,sDAAA,EAEd,QAAS,CACP,QAAS,4CACT,WAAY,sDAAA,EAEd,UAAW,CACT,QAAS,8CACT,WAAY,wDAAA,EAEd,MAAO,CACL,QAAS,0CACT,WAAY,oDAAA,EAEd,OAAQ,CACN,QAAS,2CACT,WAAY,qDAAA,EAEd,YAAa,CACX,QAAS,gDACT,WAAY,0DAAA,EAEd,OAAQ,2CACR,MAAO,0CACP,KAAM,yCACN,MAAO,CACL,EAAK,4CACL,EAAK,4CACL,EAAK,4CACL,EAAK,4CACL,EAAK,2CAAA,CACP,EAEF,YAAa,CAEX,QAAS,0CAAA,EAEX,aAAc,CACZ,GAAI,sBACJ,GAAI,kCACJ,GAAI,iCAAA,CACN,CACF,CAEJ"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
declare const karniPreset: {
|
|
2
|
+
darkMode: "class";
|
|
3
|
+
theme: {
|
|
4
|
+
extend: {
|
|
5
|
+
colors: {
|
|
6
|
+
background: string;
|
|
7
|
+
foreground: string;
|
|
8
|
+
card: {
|
|
9
|
+
DEFAULT: string;
|
|
10
|
+
foreground: string;
|
|
11
|
+
};
|
|
12
|
+
popover: {
|
|
13
|
+
DEFAULT: string;
|
|
14
|
+
foreground: string;
|
|
15
|
+
};
|
|
16
|
+
primary: {
|
|
17
|
+
DEFAULT: string;
|
|
18
|
+
foreground: string;
|
|
19
|
+
};
|
|
20
|
+
secondary: {
|
|
21
|
+
DEFAULT: string;
|
|
22
|
+
foreground: string;
|
|
23
|
+
};
|
|
24
|
+
muted: {
|
|
25
|
+
DEFAULT: string;
|
|
26
|
+
foreground: string;
|
|
27
|
+
};
|
|
28
|
+
accent: {
|
|
29
|
+
DEFAULT: string;
|
|
30
|
+
foreground: string;
|
|
31
|
+
};
|
|
32
|
+
destructive: {
|
|
33
|
+
DEFAULT: string;
|
|
34
|
+
foreground: string;
|
|
35
|
+
};
|
|
36
|
+
border: string;
|
|
37
|
+
input: string;
|
|
38
|
+
ring: string;
|
|
39
|
+
chart: {
|
|
40
|
+
"1": string;
|
|
41
|
+
"2": string;
|
|
42
|
+
"3": string;
|
|
43
|
+
"4": string;
|
|
44
|
+
"5": string;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
borderColor: {
|
|
48
|
+
DEFAULT: string;
|
|
49
|
+
};
|
|
50
|
+
borderRadius: {
|
|
51
|
+
lg: string;
|
|
52
|
+
md: string;
|
|
53
|
+
sm: string;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
export default karniPreset;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const a = {
|
|
2
|
+
// `as const` keeps the literal type so the preset stays assignable to
|
|
3
|
+
// Config["presets"] when consumers spread it into their own config.
|
|
4
|
+
darkMode: "class",
|
|
5
|
+
theme: {
|
|
6
|
+
extend: {
|
|
7
|
+
colors: {
|
|
8
|
+
background: "hsl(var(--karni-background) / <alpha-value>)",
|
|
9
|
+
foreground: "hsl(var(--karni-foreground) / <alpha-value>)",
|
|
10
|
+
card: {
|
|
11
|
+
DEFAULT: "hsl(var(--karni-card) / <alpha-value>)",
|
|
12
|
+
foreground: "hsl(var(--karni-card-foreground) / <alpha-value>)"
|
|
13
|
+
},
|
|
14
|
+
popover: {
|
|
15
|
+
DEFAULT: "hsl(var(--karni-popover) / <alpha-value>)",
|
|
16
|
+
foreground: "hsl(var(--karni-popover-foreground) / <alpha-value>)"
|
|
17
|
+
},
|
|
18
|
+
primary: {
|
|
19
|
+
DEFAULT: "hsl(var(--karni-primary) / <alpha-value>)",
|
|
20
|
+
foreground: "hsl(var(--karni-primary-foreground) / <alpha-value>)"
|
|
21
|
+
},
|
|
22
|
+
secondary: {
|
|
23
|
+
DEFAULT: "hsl(var(--karni-secondary) / <alpha-value>)",
|
|
24
|
+
foreground: "hsl(var(--karni-secondary-foreground) / <alpha-value>)"
|
|
25
|
+
},
|
|
26
|
+
muted: {
|
|
27
|
+
DEFAULT: "hsl(var(--karni-muted) / <alpha-value>)",
|
|
28
|
+
foreground: "hsl(var(--karni-muted-foreground) / <alpha-value>)"
|
|
29
|
+
},
|
|
30
|
+
accent: {
|
|
31
|
+
DEFAULT: "hsl(var(--karni-accent) / <alpha-value>)",
|
|
32
|
+
foreground: "hsl(var(--karni-accent-foreground) / <alpha-value>)"
|
|
33
|
+
},
|
|
34
|
+
destructive: {
|
|
35
|
+
DEFAULT: "hsl(var(--karni-destructive) / <alpha-value>)",
|
|
36
|
+
foreground: "hsl(var(--karni-destructive-foreground) / <alpha-value>)"
|
|
37
|
+
},
|
|
38
|
+
border: "hsl(var(--karni-border) / <alpha-value>)",
|
|
39
|
+
input: "hsl(var(--karni-input) / <alpha-value>)",
|
|
40
|
+
ring: "hsl(var(--karni-ring) / <alpha-value>)",
|
|
41
|
+
chart: {
|
|
42
|
+
1: "hsl(var(--karni-chart-1) / <alpha-value>)",
|
|
43
|
+
2: "hsl(var(--karni-chart-2) / <alpha-value>)",
|
|
44
|
+
3: "hsl(var(--karni-chart-3) / <alpha-value>)",
|
|
45
|
+
4: "hsl(var(--karni-chart-4) / <alpha-value>)",
|
|
46
|
+
5: "hsl(var(--karni-chart-5) / <alpha-value>)"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
borderColor: {
|
|
50
|
+
// A bare `border` class uses the token, never Tailwind's default gray.
|
|
51
|
+
DEFAULT: "hsl(var(--karni-border) / <alpha-value>)"
|
|
52
|
+
},
|
|
53
|
+
borderRadius: {
|
|
54
|
+
lg: "var(--karni-radius)",
|
|
55
|
+
md: "calc(var(--karni-radius) - 2px)",
|
|
56
|
+
sm: "calc(var(--karni-radius) - 4px)"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
export {
|
|
62
|
+
a as default
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=tailwind-preset.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tailwind-preset.js","sources":["../tailwind-preset.ts"],"sourcesContent":["/**\n * Tailwind (v3) preset for @karni/ui consumers.\n *\n * Usage (tailwind.config.ts of the consuming app):\n *\n * ```ts\n * import karniPreset from \"@karni/ui/tailwind-preset\";\n *\n * export default {\n * presets: [karniPreset],\n * content: [\n * \"./src/**\\/*.{ts,tsx}\",\n * \"./node_modules/@karni/ui/dist/**\\/*.js\",\n * ],\n * };\n * ```\n *\n * Also import \"@karni/ui/styles.css\" once (it defines the --karni-* tokens).\n * Tailwind v4 users: see the README recipe (@source into dist) instead.\n */\nimport type { Config } from \"tailwindcss\";\n\nconst karniPreset = {\n // `as const` keeps the literal type so the preset stays assignable to\n // Config[\"presets\"] when consumers spread it into their own config.\n darkMode: \"class\" as const,\n theme: {\n extend: {\n colors: {\n background: \"hsl(var(--karni-background) / <alpha-value>)\",\n foreground: \"hsl(var(--karni-foreground) / <alpha-value>)\",\n card: {\n DEFAULT: \"hsl(var(--karni-card) / <alpha-value>)\",\n foreground: \"hsl(var(--karni-card-foreground) / <alpha-value>)\",\n },\n popover: {\n DEFAULT: \"hsl(var(--karni-popover) / <alpha-value>)\",\n foreground: \"hsl(var(--karni-popover-foreground) / <alpha-value>)\",\n },\n primary: {\n DEFAULT: \"hsl(var(--karni-primary) / <alpha-value>)\",\n foreground: \"hsl(var(--karni-primary-foreground) / <alpha-value>)\",\n },\n secondary: {\n DEFAULT: \"hsl(var(--karni-secondary) / <alpha-value>)\",\n foreground: \"hsl(var(--karni-secondary-foreground) / <alpha-value>)\",\n },\n muted: {\n DEFAULT: \"hsl(var(--karni-muted) / <alpha-value>)\",\n foreground: \"hsl(var(--karni-muted-foreground) / <alpha-value>)\",\n },\n accent: {\n DEFAULT: \"hsl(var(--karni-accent) / <alpha-value>)\",\n foreground: \"hsl(var(--karni-accent-foreground) / <alpha-value>)\",\n },\n destructive: {\n DEFAULT: \"hsl(var(--karni-destructive) / <alpha-value>)\",\n foreground: \"hsl(var(--karni-destructive-foreground) / <alpha-value>)\",\n },\n border: \"hsl(var(--karni-border) / <alpha-value>)\",\n input: \"hsl(var(--karni-input) / <alpha-value>)\",\n ring: \"hsl(var(--karni-ring) / <alpha-value>)\",\n chart: {\n \"1\": \"hsl(var(--karni-chart-1) / <alpha-value>)\",\n \"2\": \"hsl(var(--karni-chart-2) / <alpha-value>)\",\n \"3\": \"hsl(var(--karni-chart-3) / <alpha-value>)\",\n \"4\": \"hsl(var(--karni-chart-4) / <alpha-value>)\",\n \"5\": \"hsl(var(--karni-chart-5) / <alpha-value>)\",\n },\n },\n borderColor: {\n // A bare `border` class uses the token, never Tailwind's default gray.\n DEFAULT: \"hsl(var(--karni-border) / <alpha-value>)\",\n },\n borderRadius: {\n lg: \"var(--karni-radius)\",\n md: \"calc(var(--karni-radius) - 2px)\",\n sm: \"calc(var(--karni-radius) - 4px)\",\n },\n },\n },\n} satisfies Omit<Config, \"content\">;\n\nexport default karniPreset;\n"],"names":["karniPreset"],"mappings":"AAsBA,MAAMA,IAAc;AAAA;AAAA;AAAA,EAGlB,UAAU;AAAA,EACV,OAAO;AAAA,IACL,QAAQ;AAAA,MACN,QAAQ;AAAA,QACN,YAAY;AAAA,QACZ,YAAY;AAAA,QACZ,MAAM;AAAA,UACJ,SAAS;AAAA,UACT,YAAY;AAAA,QAAA;AAAA,QAEd,SAAS;AAAA,UACP,SAAS;AAAA,UACT,YAAY;AAAA,QAAA;AAAA,QAEd,SAAS;AAAA,UACP,SAAS;AAAA,UACT,YAAY;AAAA,QAAA;AAAA,QAEd,WAAW;AAAA,UACT,SAAS;AAAA,UACT,YAAY;AAAA,QAAA;AAAA,QAEd,OAAO;AAAA,UACL,SAAS;AAAA,UACT,YAAY;AAAA,QAAA;AAAA,QAEd,QAAQ;AAAA,UACN,SAAS;AAAA,UACT,YAAY;AAAA,QAAA;AAAA,QAEd,aAAa;AAAA,UACX,SAAS;AAAA,UACT,YAAY;AAAA,QAAA;AAAA,QAEd,QAAQ;AAAA,QACR,OAAO;AAAA,QACP,MAAM;AAAA,QACN,OAAO;AAAA,UACL,GAAK;AAAA,UACL,GAAK;AAAA,UACL,GAAK;AAAA,UACL,GAAK;AAAA,UACL,GAAK;AAAA,QAAA;AAAA,MACP;AAAA,MAEF,aAAa;AAAA;AAAA,QAEX,SAAS;AAAA,MAAA;AAAA,MAEX,cAAc;AAAA,QACZ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,MAAA;AAAA,IACN;AAAA,EACF;AAEJ;"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @karni/ui/eslint-plugin — Karni's frontend enforcement rules.
|
|
3
|
+
*
|
|
4
|
+
* Plain ESM, no build step; ships inside the @karni/ui package (enforcement
|
|
5
|
+
* contract §2 + F1, F15) and is consumed from flat config (ESLint >= 9):
|
|
6
|
+
*
|
|
7
|
+
* // eslint.config.js
|
|
8
|
+
* import karni from "@karni/ui/eslint-plugin";
|
|
9
|
+
* export default [...karni.configs.recommended];
|
|
10
|
+
*
|
|
11
|
+
* Every rule fails the build ("error", never "warn"), and every rule's
|
|
12
|
+
* documented opt-out is ESLint's own disable comment WITH a required reason:
|
|
13
|
+
*
|
|
14
|
+
* // eslint-disable-next-line karni/no-direct-http -- <reason>
|
|
15
|
+
*
|
|
16
|
+
* The reason requirement is enforced by
|
|
17
|
+
* @eslint-community/eslint-comments/require-description (a regular dependency
|
|
18
|
+
* of @karni/ui — eslint itself is only a peer), and unused disable directives
|
|
19
|
+
* are themselves errors (linterOptions.reportUnusedDisableDirectives).
|
|
20
|
+
*/
|
|
21
|
+
import eslintComments from "@eslint-community/eslint-plugin-eslint-comments";
|
|
22
|
+
|
|
23
|
+
import maxPageLines from "./rules/max-page-lines.js";
|
|
24
|
+
import noDeepImports from "./rules/no-deep-imports.js";
|
|
25
|
+
import noDirectHttp from "./rules/no-direct-http.js";
|
|
26
|
+
import noRawElements from "./rules/no-raw-elements.js";
|
|
27
|
+
|
|
28
|
+
const plugin = {
|
|
29
|
+
meta: {
|
|
30
|
+
name: "@karni/ui/eslint-plugin",
|
|
31
|
+
version: "0.1.0",
|
|
32
|
+
},
|
|
33
|
+
rules: {
|
|
34
|
+
"no-raw-elements": noRawElements,
|
|
35
|
+
"max-page-lines": maxPageLines,
|
|
36
|
+
"no-deep-imports": noDeepImports,
|
|
37
|
+
"no-direct-http": noDirectHttp,
|
|
38
|
+
},
|
|
39
|
+
// Filled below so the config entries can reference the plugin object itself.
|
|
40
|
+
configs: {},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
plugin.configs.recommended = [
|
|
44
|
+
{
|
|
45
|
+
name: "karni/recommended",
|
|
46
|
+
plugins: {
|
|
47
|
+
karni: plugin,
|
|
48
|
+
"@eslint-community/eslint-comments": eslintComments,
|
|
49
|
+
},
|
|
50
|
+
linterOptions: {
|
|
51
|
+
reportUnusedDisableDirectives: "error",
|
|
52
|
+
},
|
|
53
|
+
rules: {
|
|
54
|
+
"karni/no-raw-elements": "error",
|
|
55
|
+
"karni/max-page-lines": "error",
|
|
56
|
+
"karni/no-deep-imports": "error",
|
|
57
|
+
"karni/no-direct-http": "error",
|
|
58
|
+
// Every eslint-disable needs a `-- reason`; a bare opt-out is a failure.
|
|
59
|
+
"@eslint-community/eslint-comments/require-description": "error",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
// Tests, mocks and vitest setup files stub the network by design.
|
|
64
|
+
name: "karni/recommended-test-files",
|
|
65
|
+
files: ["**/*.test.*", "**/mocks/**", "**/vitest.setup.*"],
|
|
66
|
+
rules: {
|
|
67
|
+
"karni/no-direct-http": "off",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
export default plugin;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Docs links for the rules. DOCS_BASE_URL must equal karni.errorcodes.DOCS_BASE_URL
|
|
3
|
+
* (packages/karni/tests/test_errorcodes.py checks it), and each rule's anchor is
|
|
4
|
+
* generated into docs/errors.md from ../rules.json.
|
|
5
|
+
*/
|
|
6
|
+
export const DOCS_BASE_URL = "https://cbsshekhawat18-lab.github.io/karni/";
|
|
7
|
+
|
|
8
|
+
/** @param {string} rule rule name without the plugin prefix, e.g. "no-raw-elements" */
|
|
9
|
+
export function docsUrl(rule) {
|
|
10
|
+
return `${DOCS_BASE_URL}errors/#karni-${rule}`;
|
|
11
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal glob matching for rule file-path options (no runtime dependency).
|
|
3
|
+
*
|
|
4
|
+
* Supported syntax (enough for the shipped defaults and documented options):
|
|
5
|
+
* - `**` matches any number of path segments (crosses `/`).
|
|
6
|
+
* - `*` matches within a single path segment.
|
|
7
|
+
* - `?` matches a single character within a segment.
|
|
8
|
+
*
|
|
9
|
+
* Paths are normalized to posix separators before matching, and a pattern
|
|
10
|
+
* matches if it matches the full path or any trailing sub-path, so a
|
|
11
|
+
* "**" + "/pages/" + "**" pattern works for absolute and relative filenames.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const SPECIALS = /[.+^${}()|[\]\\]/g;
|
|
15
|
+
|
|
16
|
+
/** @param {string} glob @returns {RegExp} */
|
|
17
|
+
export function globToRegExp(glob) {
|
|
18
|
+
let out = "";
|
|
19
|
+
let i = 0;
|
|
20
|
+
while (i < glob.length) {
|
|
21
|
+
const ch = glob[i];
|
|
22
|
+
if (ch === "*") {
|
|
23
|
+
if (glob[i + 1] === "*") {
|
|
24
|
+
if (glob[i + 2] === "/") {
|
|
25
|
+
// `**/` — zero or more whole segments.
|
|
26
|
+
out += "(?:.*/)?";
|
|
27
|
+
i += 3;
|
|
28
|
+
} else {
|
|
29
|
+
out += ".*";
|
|
30
|
+
i += 2;
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
out += "[^/]*";
|
|
34
|
+
i += 1;
|
|
35
|
+
}
|
|
36
|
+
} else if (ch === "?") {
|
|
37
|
+
out += "[^/]";
|
|
38
|
+
i += 1;
|
|
39
|
+
} else {
|
|
40
|
+
out += ch.replace(SPECIALS, "\\$&");
|
|
41
|
+
i += 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return new RegExp(`(?:^|/)${out}$`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @param {string} filename @param {readonly string[]} patterns @returns {boolean} */
|
|
48
|
+
export function matchesAny(filename, patterns) {
|
|
49
|
+
const path = filename.replaceAll("\\", "/");
|
|
50
|
+
return patterns.some((pattern) => globToRegExp(pattern).test(path));
|
|
51
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* karni/max-page-lines — page files cap at 50 physical lines. A longer page
|
|
3
|
+
* is rebuilding something @karni/ui ships; reach for the component's props
|
|
4
|
+
* (columns, fields, slots) instead.
|
|
5
|
+
*/
|
|
6
|
+
import { docsUrl } from "../lib/docs.js";
|
|
7
|
+
import { matchesAny } from "../lib/glob.js";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_MAX_LINES = 50;
|
|
10
|
+
const DEFAULT_FILES = ["**/pages/**"];
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
meta: {
|
|
14
|
+
type: "problem",
|
|
15
|
+
docs: {
|
|
16
|
+
url: docsUrl("max-page-lines"),
|
|
17
|
+
description:
|
|
18
|
+
"Fail page files (default **/pages/**) with more than 50 physical lines: pages compose @karni/ui components, they do not reimplement them.",
|
|
19
|
+
},
|
|
20
|
+
schema: [
|
|
21
|
+
{
|
|
22
|
+
type: "object",
|
|
23
|
+
properties: {
|
|
24
|
+
maxLines: { type: "integer", minimum: 1 },
|
|
25
|
+
files: {
|
|
26
|
+
type: "array",
|
|
27
|
+
items: { type: "string" },
|
|
28
|
+
description: "Glob patterns of the files the cap applies to.",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
additionalProperties: false,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
messages: {
|
|
35
|
+
tooLong:
|
|
36
|
+
"Page file is {{count}} physical lines (max {{max}}). Past the cap you are rebuilding " +
|
|
37
|
+
"something @karni/ui ships — compose Page/ResourceTable/ResourceForm/ResourceDetail and " +
|
|
38
|
+
"their props, or move logic into hooks/components. " +
|
|
39
|
+
"Opt out only with an explained disable: /* eslint-disable karni/max-page-lines -- <reason> */",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
create(context) {
|
|
44
|
+
const options = context.options[0] ?? {};
|
|
45
|
+
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
46
|
+
const files = options.files ?? DEFAULT_FILES;
|
|
47
|
+
if (!matchesAny(context.filename, files)) {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
Program(node) {
|
|
52
|
+
const lines = context.sourceCode.lines;
|
|
53
|
+
// A trailing newline produces one empty final element — not a line.
|
|
54
|
+
const count = lines.at(-1) === "" ? lines.length - 1 : lines.length;
|
|
55
|
+
if (count <= maxLines) return;
|
|
56
|
+
context.report({
|
|
57
|
+
node,
|
|
58
|
+
loc: { start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
|
|
59
|
+
messageId: "tooLong",
|
|
60
|
+
data: { count: String(count), max: String(maxLines) },
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* karni/no-deep-imports — importing @karni/ui internals (dist/, src/) is
|
|
3
|
+
* banned. Deep imports are how component code gets vendored and forked;
|
|
4
|
+
* compose the public API instead, and if it does not fit, extend it through
|
|
5
|
+
* its props/overrides (or upstream the change).
|
|
6
|
+
*/
|
|
7
|
+
import { docsUrl } from "../lib/docs.js";
|
|
8
|
+
const DEEP_IMPORT = /^@karni\/ui\/(?:dist|src)(?:\/|$)/;
|
|
9
|
+
|
|
10
|
+
function checkSource(context, sourceNode) {
|
|
11
|
+
if (!sourceNode || sourceNode.type !== "Literal") return;
|
|
12
|
+
if (typeof sourceNode.value !== "string") return;
|
|
13
|
+
if (!DEEP_IMPORT.test(sourceNode.value)) return;
|
|
14
|
+
context.report({
|
|
15
|
+
node: sourceNode,
|
|
16
|
+
messageId: "deepImport",
|
|
17
|
+
data: { source: sourceNode.value },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
meta: {
|
|
23
|
+
type: "problem",
|
|
24
|
+
docs: {
|
|
25
|
+
url: docsUrl("no-deep-imports"),
|
|
26
|
+
description:
|
|
27
|
+
"Ban imports from @karni/ui/dist/* and @karni/ui/src/*: compose the public API, never vendor a copy of an internal.",
|
|
28
|
+
},
|
|
29
|
+
schema: [],
|
|
30
|
+
messages: {
|
|
31
|
+
deepImport:
|
|
32
|
+
'Deep import "{{source}}" reaches into @karni/ui internals. Import from the public ' +
|
|
33
|
+
'barrel "@karni/ui" and compose/extend the exported component — vendoring an internal ' +
|
|
34
|
+
"copy forks it. " +
|
|
35
|
+
"Opt out only with an explained disable: " +
|
|
36
|
+
"// eslint-disable-next-line karni/no-deep-imports -- <reason>",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
create(context) {
|
|
41
|
+
return {
|
|
42
|
+
ImportDeclaration(node) {
|
|
43
|
+
checkSource(context, node.source);
|
|
44
|
+
},
|
|
45
|
+
ExportNamedDeclaration(node) {
|
|
46
|
+
checkSource(context, node.source);
|
|
47
|
+
},
|
|
48
|
+
ExportAllDeclaration(node) {
|
|
49
|
+
checkSource(context, node.source);
|
|
50
|
+
},
|
|
51
|
+
ImportExpression(node) {
|
|
52
|
+
checkSource(context, node.source);
|
|
53
|
+
},
|
|
54
|
+
CallExpression(node) {
|
|
55
|
+
if (node.callee.type === "Identifier" && node.callee.name === "require") {
|
|
56
|
+
checkSource(context, node.arguments[0]);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
};
|