openxiangda 1.0.28 → 1.0.29
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/openxiangda-skills/references/component-guide.md +9 -9
- package/openxiangda-skills/references/style-system.md +182 -459
- package/openxiangda-skills/skills/openxiangda-page/SKILL.md +1 -1
- package/package.json +1 -1
- package/packages/sdk/src/build-source/scripts/build-forms.mjs +2 -0
- package/packages/sdk/src/build-source/scripts/build-pages.mjs +2 -0
- package/packages/sdk/src/build-source/scripts/utils/tailwind-config.mjs +9 -6
- package/packages/sdk/src/build-source/scripts/utils/tailwind-config.test.ts +20 -4
- package/packages/sdk/src/build-source/scripts/utils/tailwind-token-warnings.mjs +146 -0
- package/packages/sdk/src/build-source/scripts/utils/tailwind-token-warnings.test.ts +122 -0
- package/templates/sy-lowcode-app-workspace/examples/best-practices/design-style.md +9 -6
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/interactive-workbench/styles.css +5 -5
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/mobile-portal-shell/styles.css +2 -2
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/pc-portal-shell/styles.css +10 -10
- package/templates/sy-lowcode-app-workspace/examples/best-practices/src/pages/service-ticket-ops/styles.css +9 -9
- package/templates/sy-lowcode-app-workspace/src/index.css +2 -4
- package/templates/sy-lowcode-app-workspace/tailwind.config.cjs +7 -6
|
@@ -82,7 +82,7 @@ Read these references only when editing page code:
|
|
|
82
82
|
- Do not scatter hardcoded `/view/...&isRenderNav=false` URLs through page code. Use the runtime navigation API or the local route helper generated for the app shell.
|
|
83
83
|
- Platform menus should bind only the formal app-shell code page for user-facing entry points. Original forms, workflows, and native view pages may remain as development / maintenance resources or permission targets, but should not become the product navigation shell.
|
|
84
84
|
- For prod, explicitly run with `--profile prod`; never rely on the current profile for release operations.
|
|
85
|
-
- Follow the style system in `../../references/style-system.md`:
|
|
85
|
+
- Follow the style system in `../../references/style-system.md`: default to native Tailwind utilities and arbitrary values for business pages (`bg-white`, `border`, `border-slate-200`, `text-slate-600`, `grid-cols-[240px_1fr]`, etc.). Keep the page CSS namespace and platform component styles, but do not treat platform token classes as the default authoring pattern. Do not use shadcn token classes such as `bg-card`, `text-muted-foreground`, or `text-foreground` unless the workspace explicitly configures them.
|
|
86
86
|
- For list / detail / CRUD pages, follow `../../references/architecture-patterns.md` (e.g. `DataManagementList`) before writing custom data-fetching loops.
|
|
87
87
|
- Query pages must use pagination with structured conditions. Do not fetch a large `pageSize` and filter in the browser; avoid default `searchKeyWord`, and build multi-field fuzzy search with explicit `filterGroup` + `OR`.
|
|
88
88
|
- Pick components per `../../references/component-guide.md`: prefer the platform component, fall back to Ant Design, and only build a custom component when neither fits.
|
package/package.json
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
extractLargeDataUrlAssets,
|
|
24
24
|
formatExtractedAssetSummary,
|
|
25
25
|
} from "./utils/static-assets.mjs";
|
|
26
|
+
import { warnShadcnTailwindTokens } from "./utils/tailwind-token-warnings.mjs";
|
|
26
27
|
|
|
27
28
|
process.env.NODE_ENV = "production";
|
|
28
29
|
process.env.BABEL_ENV = "production";
|
|
@@ -980,6 +981,7 @@ async function main() {
|
|
|
980
981
|
printPlan(plan, "forms");
|
|
981
982
|
if (args.dryRun) return;
|
|
982
983
|
if (plan.changed.length === 0) return;
|
|
984
|
+
warnShadcnTailwindTokens(rootDir);
|
|
983
985
|
|
|
984
986
|
if (plan.fullRebuild && requested.length === 0 && fs.existsSync(distDir)) {
|
|
985
987
|
fs.rmSync(distDir, { recursive: true, force: true });
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
extractLargeDataUrlAssets,
|
|
23
23
|
formatExtractedAssetSummary,
|
|
24
24
|
} from "./utils/static-assets.mjs";
|
|
25
|
+
import { warnShadcnTailwindTokens } from "./utils/tailwind-token-warnings.mjs";
|
|
25
26
|
|
|
26
27
|
process.env.NODE_ENV = "production";
|
|
27
28
|
process.env.BABEL_ENV = "production";
|
|
@@ -823,6 +824,7 @@ async function main() {
|
|
|
823
824
|
printPlan(plan, "pages");
|
|
824
825
|
if (args.dryRun) return;
|
|
825
826
|
if (plan.changed.length === 0) return;
|
|
827
|
+
warnShadcnTailwindTokens(rootDir);
|
|
826
828
|
|
|
827
829
|
if (plan.fullRebuild && requested.length === 0) {
|
|
828
830
|
await fs.rm(path.join(distRoot, "pages"), { recursive: true, force: true });
|
|
@@ -20,9 +20,7 @@ const openxiangdaContent = resolveOpenXiangdaContent();
|
|
|
20
20
|
`;
|
|
21
21
|
const tailwindDirectives = ["base", "components", "utilities"];
|
|
22
22
|
const requiredBlocklistEntries = ['"[-:T]"', '"[-:TZ.]"'];
|
|
23
|
-
const layeredTailwindCss = `@
|
|
24
|
-
|
|
25
|
-
@layer tailwind-base {
|
|
23
|
+
const layeredTailwindCss = `@layer tailwind-base {
|
|
26
24
|
@tailwind base;
|
|
27
25
|
}
|
|
28
26
|
|
|
@@ -73,7 +71,6 @@ export function isWorkspaceTailwindConfigCurrent(content) {
|
|
|
73
71
|
|
|
74
72
|
export function isWorkspaceTailwindCssCurrent(content) {
|
|
75
73
|
return (
|
|
76
|
-
/@import\s+["']openxiangda\/styles\/tokens\.css["'];/.test(content) &&
|
|
77
74
|
/@layer\s+tailwind-base\s*\{\s*@tailwind\s+base\s*;\s*\}/s.test(content) &&
|
|
78
75
|
!/@layer\s+tailwind-base\s*,\s*antd\s*;/.test(content) &&
|
|
79
76
|
tailwindDirectives
|
|
@@ -188,6 +185,11 @@ export function patchWorkspaceTailwindConfig(content) {
|
|
|
188
185
|
export function patchWorkspaceTailwindCss(content) {
|
|
189
186
|
if (isWorkspaceTailwindCssCurrent(content)) return content;
|
|
190
187
|
|
|
188
|
+
const hasExistingTokenImport =
|
|
189
|
+
/@import\s+["']openxiangda\/styles\/tokens\.css["'];/.test(content);
|
|
190
|
+
const optionalTokenImport = hasExistingTokenImport
|
|
191
|
+
? '@import "openxiangda/styles/tokens.css";\n\n'
|
|
192
|
+
: "";
|
|
191
193
|
const customCss = content
|
|
192
194
|
.replace(/@import\s+["']openxiangda\/styles\/tokens\.css["'];\s*/g, "")
|
|
193
195
|
.replace(/@layer\s+tailwind-base\s*,\s*antd\s*;\s*/g, "")
|
|
@@ -195,7 +197,8 @@ export function patchWorkspaceTailwindCss(content) {
|
|
|
195
197
|
.replace(/@tailwind\s+(?:base|components|utilities)\s*;\s*/g, "")
|
|
196
198
|
.trimStart();
|
|
197
199
|
|
|
198
|
-
|
|
200
|
+
const baseCss = `${optionalTokenImport}${layeredTailwindCss}`;
|
|
201
|
+
return customCss ? `${baseCss}\n${customCss}` : baseCss;
|
|
199
202
|
}
|
|
200
203
|
|
|
201
204
|
function isStandardPostcssConfig(content) {
|
|
@@ -271,7 +274,7 @@ export function validateWorkspaceTailwindConfig(workspaceRoot) {
|
|
|
271
274
|
: "";
|
|
272
275
|
if (!isWorkspaceTailwindCssCurrent(cssContent)) {
|
|
273
276
|
return [
|
|
274
|
-
"src/index.css must
|
|
277
|
+
"src/index.css must keep Tailwind base in @layer tailwind-base, include components/utilities, and avoid declaring antd layer",
|
|
275
278
|
];
|
|
276
279
|
}
|
|
277
280
|
return [];
|
|
@@ -60,7 +60,7 @@ describe("workspace Tailwind config helpers", () => {
|
|
|
60
60
|
expect(cssContent).toContain("@tailwind utilities;");
|
|
61
61
|
expect(postcssContent).toContain("createOpenXiangdaNamespaceCssPlugin");
|
|
62
62
|
expect(postcssContent).toContain('require("openxiangda/build")');
|
|
63
|
-
expect(cssContent).toContain('@import "openxiangda/styles/tokens.css";');
|
|
63
|
+
expect(cssContent).not.toContain('@import "openxiangda/styles/tokens.css";');
|
|
64
64
|
expect(validateWorkspaceTailwindConfig(workspaceRoot)).toEqual([]);
|
|
65
65
|
});
|
|
66
66
|
|
|
@@ -146,7 +146,7 @@ module.exports = {
|
|
|
146
146
|
expect(patchWorkspaceTailwindCss(source)).toBe(source);
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
-
it("
|
|
149
|
+
it("keeps current Tailwind directives without requiring design token import", () => {
|
|
150
150
|
const nextContent = patchWorkspaceTailwindCss(`@layer tailwind-base {
|
|
151
151
|
@tailwind base;
|
|
152
152
|
}
|
|
@@ -155,9 +155,25 @@ module.exports = {
|
|
|
155
155
|
@tailwind utilities;
|
|
156
156
|
`);
|
|
157
157
|
|
|
158
|
-
expect(nextContent).toContain('@import "openxiangda/styles/tokens.css";');
|
|
158
|
+
expect(nextContent).not.toContain('@import "openxiangda/styles/tokens.css";');
|
|
159
|
+
expect(nextContent).toContain("@layer tailwind-base");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("preserves an existing optional OpenXiangda design token import during css patching", () => {
|
|
163
|
+
const nextContent = patchWorkspaceTailwindCss(`@import "openxiangda/styles/tokens.css";
|
|
164
|
+
|
|
165
|
+
@tailwind base;
|
|
166
|
+
@tailwind components;
|
|
167
|
+
@tailwind utilities;
|
|
168
|
+
|
|
169
|
+
#root {
|
|
170
|
+
min-height: 100vh;
|
|
171
|
+
}
|
|
172
|
+
`);
|
|
173
|
+
|
|
159
174
|
expect(nextContent.match(/openxiangda\/styles\/tokens\.css/g)).toHaveLength(1);
|
|
160
175
|
expect(nextContent).toContain("@layer tailwind-base");
|
|
176
|
+
expect(nextContent).toContain("#root");
|
|
161
177
|
});
|
|
162
178
|
|
|
163
179
|
it("patches standard PostCSS config with openxiangda namespace css plugin", () => {
|
|
@@ -227,7 +243,7 @@ module.exports = { plugins: [custom()] };
|
|
|
227
243
|
);
|
|
228
244
|
|
|
229
245
|
expect(validateWorkspaceTailwindConfig(workspaceRoot)).toEqual([
|
|
230
|
-
"src/index.css must
|
|
246
|
+
"src/index.css must keep Tailwind base in @layer tailwind-base, include components/utilities, and avoid declaring antd layer",
|
|
231
247
|
]);
|
|
232
248
|
});
|
|
233
249
|
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const sourceExtensions = new Set([
|
|
5
|
+
".js",
|
|
6
|
+
".jsx",
|
|
7
|
+
".ts",
|
|
8
|
+
".tsx",
|
|
9
|
+
".mjs",
|
|
10
|
+
".cjs",
|
|
11
|
+
".html",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const ignoredDirs = new Set([
|
|
15
|
+
".git",
|
|
16
|
+
".openxiangda",
|
|
17
|
+
".tmp",
|
|
18
|
+
"dist",
|
|
19
|
+
"node_modules",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const shadcnTokenClassNames = [
|
|
23
|
+
"bg-background",
|
|
24
|
+
"text-foreground",
|
|
25
|
+
"bg-card",
|
|
26
|
+
"text-card",
|
|
27
|
+
"text-card-foreground",
|
|
28
|
+
"bg-popover",
|
|
29
|
+
"text-popover",
|
|
30
|
+
"text-popover-foreground",
|
|
31
|
+
"bg-muted",
|
|
32
|
+
"text-muted",
|
|
33
|
+
"text-muted-foreground",
|
|
34
|
+
"bg-accent",
|
|
35
|
+
"text-accent",
|
|
36
|
+
"text-accent-foreground",
|
|
37
|
+
"bg-destructive",
|
|
38
|
+
"text-destructive",
|
|
39
|
+
"text-destructive-foreground",
|
|
40
|
+
"border-input",
|
|
41
|
+
"border-ring",
|
|
42
|
+
"ring-ring",
|
|
43
|
+
"placeholder:text-muted-foreground",
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const shadcnTokenCssVariablePattern =
|
|
47
|
+
/--(?:background|foreground|card|card-foreground|popover|popover-foreground|muted|muted-foreground|accent|accent-foreground|destructive|destructive-foreground|input|ring)\b/;
|
|
48
|
+
const shadcnTokenTailwindConfigPatterns = [
|
|
49
|
+
/["']?(?:background|foreground|card|popover|muted|accent|destructive|input|ring)["']?\s*:/,
|
|
50
|
+
/["']?(?:card-foreground|popover-foreground|muted-foreground|accent-foreground|destructive-foreground)["']?\s*:/,
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function readTextIfExists(filePath) {
|
|
54
|
+
if (!fs.existsSync(filePath)) return "";
|
|
55
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isShadcnTokenThemeConfigured(workspaceRoot) {
|
|
59
|
+
const tailwindConfig = readTextIfExists(
|
|
60
|
+
path.join(workspaceRoot, "tailwind.config.cjs"),
|
|
61
|
+
);
|
|
62
|
+
const indexCss = readTextIfExists(path.join(workspaceRoot, "src", "index.css"));
|
|
63
|
+
return (
|
|
64
|
+
shadcnTokenCssVariablePattern.test(indexCss) ||
|
|
65
|
+
shadcnTokenTailwindConfigPatterns.some((pattern) =>
|
|
66
|
+
pattern.test(tailwindConfig),
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function walkSourceFiles(dirPath, result = []) {
|
|
72
|
+
if (!fs.existsSync(dirPath)) return result;
|
|
73
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
if (ignoredDirs.has(entry.name)) continue;
|
|
76
|
+
const entryPath = path.join(dirPath, entry.name);
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
walkSourceFiles(entryPath, result);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!entry.isFile()) continue;
|
|
82
|
+
if (!sourceExtensions.has(path.extname(entry.name))) continue;
|
|
83
|
+
result.push(entryPath);
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hasClassToken(content, className) {
|
|
89
|
+
const escaped = className.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
90
|
+
const pattern = new RegExp(
|
|
91
|
+
`(?:^|[^A-Za-z0-9_:/-])(?:!?[-A-Za-z0-9_\\[\\]=()./#&]+:)*${escaped}(?:/[0-9]+)?(?=$|[^A-Za-z0-9_:/-])`,
|
|
92
|
+
);
|
|
93
|
+
return pattern.test(content);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getShadcnTailwindTokenWarnings(workspaceRoot) {
|
|
97
|
+
if (isShadcnTokenThemeConfigured(workspaceRoot)) {
|
|
98
|
+
return {
|
|
99
|
+
configured: true,
|
|
100
|
+
files: [],
|
|
101
|
+
tokens: [],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const files = [];
|
|
106
|
+
const tokenSet = new Set();
|
|
107
|
+
for (const filePath of walkSourceFiles(path.join(workspaceRoot, "src"))) {
|
|
108
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
109
|
+
const tokens = shadcnTokenClassNames.filter((className) =>
|
|
110
|
+
hasClassToken(content, className),
|
|
111
|
+
);
|
|
112
|
+
if (tokens.length === 0) continue;
|
|
113
|
+
tokens.forEach((token) => tokenSet.add(token));
|
|
114
|
+
files.push({
|
|
115
|
+
path: path.relative(workspaceRoot, filePath).split(path.sep).join("/"),
|
|
116
|
+
tokens,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
configured: false,
|
|
122
|
+
files,
|
|
123
|
+
tokens: Array.from(tokenSet).sort(),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function warnShadcnTailwindTokens(workspaceRoot, options = {}) {
|
|
128
|
+
const logger = options.logger ?? console;
|
|
129
|
+
const warning = getShadcnTailwindTokenWarnings(workspaceRoot);
|
|
130
|
+
if (warning.configured || warning.files.length === 0) return warning;
|
|
131
|
+
|
|
132
|
+
const fileLines = warning.files
|
|
133
|
+
.slice(0, 8)
|
|
134
|
+
.map((item) => ` - ${item.path}: ${item.tokens.join(", ")}`)
|
|
135
|
+
.join("\n");
|
|
136
|
+
const more =
|
|
137
|
+
warning.files.length > 8
|
|
138
|
+
? `\n ... and ${warning.files.length - 8} more file(s)`
|
|
139
|
+
: "";
|
|
140
|
+
|
|
141
|
+
logger.warn(
|
|
142
|
+
`[style warning] 检测到 shadcn 风格 Tailwind token 类,但当前项目未配置这些 token:\n${fileLines}${more}\n` +
|
|
143
|
+
"这些不是 Tailwind 原生类,OpenXiangda 业务页面也不再强制使用平台 token。请改成原生 Tailwind 类(例如 bg-white border border-slate-200 text-slate-600)、Tailwind 任意值(例如 bg-[#1677ff]),或在 tailwind.config.cjs 中显式配置这些 token。",
|
|
144
|
+
);
|
|
145
|
+
return warning;
|
|
146
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getShadcnTailwindTokenWarnings,
|
|
9
|
+
warnShadcnTailwindTokens,
|
|
10
|
+
} from "./tailwind-token-warnings.mjs";
|
|
11
|
+
|
|
12
|
+
let tempDirs: string[] = [];
|
|
13
|
+
|
|
14
|
+
function createTempDir() {
|
|
15
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "workspace-token-warn-"));
|
|
16
|
+
tempDirs.push(dir);
|
|
17
|
+
fs.mkdirSync(path.join(dir, "src"), { recursive: true });
|
|
18
|
+
fs.writeFileSync(
|
|
19
|
+
path.join(dir, "tailwind.config.cjs"),
|
|
20
|
+
"module.exports = { theme: { extend: {} } };\n",
|
|
21
|
+
"utf-8",
|
|
22
|
+
);
|
|
23
|
+
fs.writeFileSync(
|
|
24
|
+
path.join(dir, "src", "index.css"),
|
|
25
|
+
"body { background: #f8fafc; }\n@tailwind utilities;\n",
|
|
26
|
+
"utf-8",
|
|
27
|
+
);
|
|
28
|
+
return dir;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
for (const dir of tempDirs) {
|
|
33
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
tempDirs = [];
|
|
36
|
+
vi.restoreAllMocks();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("shadcn Tailwind token warnings", () => {
|
|
40
|
+
it("detects shadcn token classes when the workspace does not configure them", () => {
|
|
41
|
+
const workspaceRoot = createTempDir();
|
|
42
|
+
fs.writeFileSync(
|
|
43
|
+
path.join(workspaceRoot, "src", "Page.tsx"),
|
|
44
|
+
`export function Page() {
|
|
45
|
+
return <div className="bg-card text-muted-foreground hover:text-foreground data-[state=open]:bg-accent" />;
|
|
46
|
+
}
|
|
47
|
+
`,
|
|
48
|
+
"utf-8",
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const warning = getShadcnTailwindTokenWarnings(workspaceRoot);
|
|
52
|
+
|
|
53
|
+
expect(warning.configured).toBe(false);
|
|
54
|
+
expect(warning.tokens).toEqual([
|
|
55
|
+
"bg-accent",
|
|
56
|
+
"bg-card",
|
|
57
|
+
"text-foreground",
|
|
58
|
+
"text-muted-foreground",
|
|
59
|
+
]);
|
|
60
|
+
expect(warning.files[0]).toEqual({
|
|
61
|
+
path: "src/Page.tsx",
|
|
62
|
+
tokens: [
|
|
63
|
+
"text-foreground",
|
|
64
|
+
"bg-card",
|
|
65
|
+
"text-muted-foreground",
|
|
66
|
+
"bg-accent",
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("does not warn for native Tailwind classes or OpenXiangda compatibility classes", () => {
|
|
72
|
+
const workspaceRoot = createTempDir();
|
|
73
|
+
fs.writeFileSync(
|
|
74
|
+
path.join(workspaceRoot, "src", "Page.tsx"),
|
|
75
|
+
`export function Page() {
|
|
76
|
+
return <div className="bg-white border border-slate-200 text-slate-600 text-primary" />;
|
|
77
|
+
}
|
|
78
|
+
`,
|
|
79
|
+
"utf-8",
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const logger = { warn: vi.fn() };
|
|
83
|
+
const warning = warnShadcnTailwindTokens(workspaceRoot, { logger });
|
|
84
|
+
|
|
85
|
+
expect(warning.files).toEqual([]);
|
|
86
|
+
expect(logger.warn).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("does not warn when shadcn tokens are explicitly configured", () => {
|
|
90
|
+
const workspaceRoot = createTempDir();
|
|
91
|
+
fs.writeFileSync(
|
|
92
|
+
path.join(workspaceRoot, "tailwind.config.cjs"),
|
|
93
|
+
`module.exports = {
|
|
94
|
+
theme: {
|
|
95
|
+
extend: {
|
|
96
|
+
colors: {
|
|
97
|
+
card: "hsl(var(--card))",
|
|
98
|
+
"muted-foreground": "hsl(var(--muted-foreground))",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
`,
|
|
104
|
+
"utf-8",
|
|
105
|
+
);
|
|
106
|
+
fs.writeFileSync(
|
|
107
|
+
path.join(workspaceRoot, "src", "Page.tsx"),
|
|
108
|
+
`export function Page() {
|
|
109
|
+
return <div className="bg-card text-muted-foreground" />;
|
|
110
|
+
}
|
|
111
|
+
`,
|
|
112
|
+
"utf-8",
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const logger = { warn: vi.fn() };
|
|
116
|
+
const warning = warnShadcnTailwindTokens(workspaceRoot, { logger });
|
|
117
|
+
|
|
118
|
+
expect(warning.configured).toBe(true);
|
|
119
|
+
expect(warning.files).toEqual([]);
|
|
120
|
+
expect(logger.warn).not.toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -22,12 +22,15 @@ OpenXiangda business apps should feel like focused operational tools.
|
|
|
22
22
|
|
|
23
23
|
## Styling Rules
|
|
24
24
|
|
|
25
|
-
- Use
|
|
26
|
-
antd-mobile.
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
- Use Tailwind native utilities and arbitrary values as the default styling
|
|
26
|
+
vocabulary, plus Ant Design and antd-mobile for mature controls.
|
|
27
|
+
- Platform token classes are compatibility helpers for platform components and
|
|
28
|
+
theme overrides. Do not make them the main pattern for business pages.
|
|
29
|
+
- Prefer concrete Tailwind classes such as `bg-white`, `border`,
|
|
30
|
+
`border-slate-200`, `text-slate-600`, `shadow-sm`, and
|
|
31
|
+
`grid-cols-[240px_1fr]`. Do not use shadcn token classes such as `bg-card`,
|
|
32
|
+
`text-muted-foreground`, or `text-foreground` unless the workspace explicitly
|
|
33
|
+
configures them.
|
|
31
34
|
- Use mature packages for mature interactions: antd controls instead of native
|
|
32
35
|
inputs, ECharts for charts, GSAP for complex animation timelines, and
|
|
33
36
|
maintained drag/drop or virtual-list libraries when those behaviors are
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
.bp-workbench {
|
|
2
2
|
display: grid;
|
|
3
|
-
gap:
|
|
3
|
+
gap: 24px;
|
|
4
4
|
min-height: 100%;
|
|
5
|
-
padding:
|
|
6
|
-
background:
|
|
5
|
+
padding: 24px;
|
|
6
|
+
background: #f8fafc;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
.bp-workbench__header {
|
|
10
10
|
display: flex;
|
|
11
11
|
align-items: center;
|
|
12
12
|
justify-content: space-between;
|
|
13
|
-
gap:
|
|
13
|
+
gap: 16px;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
.bp-workbench__grid {
|
|
17
17
|
display: grid;
|
|
18
18
|
grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
|
|
19
|
-
gap:
|
|
19
|
+
gap: 16px;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
.bp-workbench__panel {
|
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
.bp-pc-shell {
|
|
2
2
|
min-height: 100%;
|
|
3
|
-
background:
|
|
3
|
+
background: #f8fafc;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
.bp-pc-shell__sider {
|
|
7
|
-
border-right: 1px solid
|
|
8
|
-
background:
|
|
7
|
+
border-right: 1px solid #e2e8f0;
|
|
8
|
+
background: #ffffff;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
.bp-pc-shell__brand {
|
|
12
12
|
margin: 0;
|
|
13
|
-
padding:
|
|
13
|
+
padding: 24px;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
.bp-pc-shell__content {
|
|
17
|
-
padding:
|
|
17
|
+
padding: 24px;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
.bp-pc-shell__module {
|
|
21
21
|
display: grid;
|
|
22
|
-
gap:
|
|
22
|
+
gap: 16px;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
.bp-pc-shell__card,
|
|
26
26
|
.bp-portal-metric {
|
|
27
|
-
border-radius:
|
|
27
|
+
border-radius: 8px;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
.bp-portal-metric__value {
|
|
31
|
-
margin:
|
|
32
|
-
color:
|
|
33
|
-
font-size:
|
|
31
|
+
margin: 8px 0;
|
|
32
|
+
color: #0f172a;
|
|
33
|
+
font-size: 24px;
|
|
34
34
|
font-weight: 600;
|
|
35
35
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
.bp-ticket-ops {
|
|
2
2
|
min-height: 100%;
|
|
3
|
-
padding:
|
|
4
|
-
background:
|
|
3
|
+
padding: 24px;
|
|
4
|
+
background: #f8fafc;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
.bp-ticket-ops__header {
|
|
8
8
|
display: flex;
|
|
9
9
|
align-items: center;
|
|
10
10
|
justify-content: space-between;
|
|
11
|
-
gap:
|
|
12
|
-
margin-bottom:
|
|
11
|
+
gap: 16px;
|
|
12
|
+
margin-bottom: 16px;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
.bp-ticket-ops__title {
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
|
|
19
19
|
.bp-ticket-detail {
|
|
20
20
|
display: grid;
|
|
21
|
-
gap:
|
|
21
|
+
gap: 16px;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
.bp-ticket-detail__description {
|
|
25
|
-
padding:
|
|
26
|
-
border: 1px solid
|
|
27
|
-
border-radius:
|
|
28
|
-
background:
|
|
25
|
+
padding: 16px;
|
|
26
|
+
border: 1px solid #e2e8f0;
|
|
27
|
+
border-radius: 8px;
|
|
28
|
+
background: #ffffff;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
.bp-query-state {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
@import "openxiangda/styles/tokens.css";
|
|
2
|
-
|
|
3
1
|
@layer tailwind-base {
|
|
4
2
|
@tailwind base;
|
|
5
3
|
}
|
|
@@ -14,8 +12,8 @@
|
|
|
14
12
|
body {
|
|
15
13
|
margin: 0;
|
|
16
14
|
min-width: 320px;
|
|
17
|
-
background:
|
|
18
|
-
color:
|
|
15
|
+
background: #f8fafc;
|
|
16
|
+
color: #0f172a;
|
|
19
17
|
font-family:
|
|
20
18
|
-apple-system,
|
|
21
19
|
BlinkMacSystemFont,
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
const
|
|
1
|
+
const openxiangdaPath = require("node:path");
|
|
2
|
+
const openxiangdaPresetModule = require("openxiangda/tailwind-preset");
|
|
3
|
+
const openxiangdaPreset =
|
|
4
|
+
openxiangdaPresetModule.default ?? openxiangdaPresetModule;
|
|
4
5
|
|
|
5
6
|
function resolveOpenXiangdaContent() {
|
|
6
7
|
try {
|
|
7
8
|
const packagePath = require.resolve("openxiangda");
|
|
8
|
-
const distDir =
|
|
9
|
-
return [
|
|
9
|
+
const distDir = openxiangdaPath.dirname(packagePath);
|
|
10
|
+
return [openxiangdaPath.join(distDir, "..", "**/*.{js,mjs,cjs}")];
|
|
10
11
|
} catch {
|
|
11
12
|
return [];
|
|
12
13
|
}
|
|
@@ -22,7 +23,7 @@ module.exports = {
|
|
|
22
23
|
...openxiangdaContent,
|
|
23
24
|
],
|
|
24
25
|
blocklist: ["[-:T]", "[-:TZ.]"],
|
|
25
|
-
presets: [
|
|
26
|
+
presets: [openxiangdaPreset],
|
|
26
27
|
theme: {
|
|
27
28
|
extend: {},
|
|
28
29
|
},
|