react-email 6.1.5 → 6.2.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/CHANGELOG.md +24 -0
- package/dist/cli/index.mjs +133 -12
- package/dist/index.cjs +27 -4
- package/dist/index.d.cts +18 -6
- package/dist/index.d.mts +18 -6
- package/dist/index.mjs +27 -4
- package/package.json +1 -1
- package/src/components/tailwind/tailwind.spec.tsx +315 -0
- package/src/components/tailwind/tailwind.tsx +18 -9
- package/src/components/tailwind/utils/css/extract-rules-per-class.spec.ts +24 -20
- package/src/components/tailwind/utils/tailwindcss/setup-tailwind.spec.ts +7 -0
- package/src/components/tailwind/utils/tailwindcss/setup-tailwind.ts +41 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# react-email
|
|
2
2
|
|
|
3
|
+
## 6.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 192d82a: Add `theme` and `utility` props to `<Tailwind>` for Tailwind v4 CSS-first configuration. Both accept a CSS string and can be combined with the existing `config` prop.
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import themeCss from "./theme.css?inline";
|
|
11
|
+
|
|
12
|
+
<Tailwind theme={themeCss}>
|
|
13
|
+
<div className="bg-brand font-display">Custom themed content</div>
|
|
14
|
+
</Tailwind>;
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Empty strings are no-ops. The base Tailwind theme and utilities are still loaded — `theme` and `utility` layer on top.
|
|
18
|
+
|
|
19
|
+
The preview server, `email export`, and the caniemail compatibility check all understand the Vite-style `?inline` and `?raw` suffixes on CSS imports, so the pattern above works the same in your project and inside the preview UI. The compatibility check also extracts the `theme` and `utility` props (in addition to `config`) when analyzing your template, so any caniemail incompatibilities in CSS produced by those props will surface as warnings.
|
|
20
|
+
|
|
21
|
+
Internal note: the exported `setupTailwind` helper now takes `{ config, cssConfigs }` instead of a positional `TailwindConfig`. Calling it with the old shape throws with a migration hint.
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- 06f1d05: Watch directories targeted by dynamic `import()` template literals so changes to runtime-resolved files trigger preview reloads.
|
|
26
|
+
|
|
3
27
|
## 6.1.5
|
|
4
28
|
|
|
5
29
|
### Patch Changes
|
package/dist/cli/index.mjs
CHANGED
|
@@ -6523,7 +6523,7 @@ const getEmailsDirectoryMetadata = async (absolutePathToEmailsDirectory, keepFil
|
|
|
6523
6523
|
//#region package.json
|
|
6524
6524
|
var package_default = {
|
|
6525
6525
|
name: "react-email",
|
|
6526
|
-
version: "6.
|
|
6526
|
+
version: "6.2.0",
|
|
6527
6527
|
description: "A live preview of your emails right in your browser.",
|
|
6528
6528
|
bin: { "email": "./dist/cli/index.mjs" },
|
|
6529
6529
|
type: "module",
|
|
@@ -6864,7 +6864,8 @@ const build$1 = async ({ dir: emailsDirRelativePath, packageManager }) => {
|
|
|
6864
6864
|
//#region src/cli/utils/preview/hot-reloading/get-imported-modules.ts
|
|
6865
6865
|
const traverse = typeof traverseModule === "function" ? traverseModule : traverseModule.default;
|
|
6866
6866
|
const getImportedModules = (contents) => {
|
|
6867
|
-
const
|
|
6867
|
+
const staticImports = [];
|
|
6868
|
+
const dynamicImportPrefixes = [];
|
|
6868
6869
|
traverse(parse(contents, {
|
|
6869
6870
|
sourceType: "unambiguous",
|
|
6870
6871
|
strictMode: false,
|
|
@@ -6876,27 +6877,49 @@ const getImportedModules = (contents) => {
|
|
|
6876
6877
|
]
|
|
6877
6878
|
}), {
|
|
6878
6879
|
ImportDeclaration({ node }) {
|
|
6879
|
-
|
|
6880
|
+
staticImports.push(node.source.value);
|
|
6880
6881
|
},
|
|
6881
6882
|
ExportAllDeclaration({ node }) {
|
|
6882
|
-
|
|
6883
|
+
staticImports.push(node.source.value);
|
|
6883
6884
|
},
|
|
6884
6885
|
ExportNamedDeclaration({ node }) {
|
|
6885
|
-
if (node.source)
|
|
6886
|
+
if (node.source) staticImports.push(node.source.value);
|
|
6886
6887
|
},
|
|
6887
6888
|
TSExternalModuleReference({ node }) {
|
|
6888
|
-
|
|
6889
|
+
staticImports.push(node.expression.value);
|
|
6889
6890
|
},
|
|
6890
6891
|
CallExpression({ node }) {
|
|
6891
6892
|
if ("name" in node.callee && node.callee.name === "require") {
|
|
6892
6893
|
if (node.arguments.length === 1) {
|
|
6893
6894
|
const importPathNode = node.arguments[0];
|
|
6894
|
-
if (importPathNode.type === "StringLiteral")
|
|
6895
|
+
if (importPathNode.type === "StringLiteral") staticImports.push(importPathNode.value);
|
|
6896
|
+
}
|
|
6897
|
+
return;
|
|
6898
|
+
}
|
|
6899
|
+
if (node.callee.type === "Import" && node.arguments.length === 1) {
|
|
6900
|
+
const argument = node.arguments[0];
|
|
6901
|
+
if (argument.type === "StringLiteral") {
|
|
6902
|
+
staticImports.push(argument.value);
|
|
6903
|
+
return;
|
|
6904
|
+
}
|
|
6905
|
+
if (argument.type === "TemplateLiteral" && argument.quasis.length > 0) {
|
|
6906
|
+
if (argument.expressions.length === 0) {
|
|
6907
|
+
const onlyQuasi = argument.quasis[0];
|
|
6908
|
+
const staticPath = onlyQuasi.value.cooked ?? onlyQuasi.value.raw;
|
|
6909
|
+
if (staticPath.length > 0) staticImports.push(staticPath);
|
|
6910
|
+
return;
|
|
6911
|
+
}
|
|
6912
|
+
const firstQuasi = argument.quasis[0];
|
|
6913
|
+
const leadingStatic = firstQuasi.value.cooked ?? firstQuasi.value.raw;
|
|
6914
|
+
if (leadingStatic.length > 0) dynamicImportPrefixes.push(leadingStatic);
|
|
6895
6915
|
}
|
|
6896
6916
|
}
|
|
6897
6917
|
}
|
|
6898
6918
|
});
|
|
6899
|
-
return
|
|
6919
|
+
return {
|
|
6920
|
+
staticImports,
|
|
6921
|
+
dynamicImportPrefixes
|
|
6922
|
+
};
|
|
6900
6923
|
};
|
|
6901
6924
|
//#endregion
|
|
6902
6925
|
//#region src/cli/utils/preview/hot-reloading/resolve-path-aliases.ts
|
|
@@ -6919,6 +6942,25 @@ const resolvePathAliases = (importPaths, projectPath) => {
|
|
|
6919
6942
|
}
|
|
6920
6943
|
return importPaths;
|
|
6921
6944
|
};
|
|
6945
|
+
const isExistingDirectory = (candidate) => {
|
|
6946
|
+
try {
|
|
6947
|
+
return statSync(candidate).isDirectory();
|
|
6948
|
+
} catch (_) {
|
|
6949
|
+
return false;
|
|
6950
|
+
}
|
|
6951
|
+
};
|
|
6952
|
+
const resolveAliasedDirectoryPrefix = (prefix, projectPath) => {
|
|
6953
|
+
const configLoadResult = loadConfig(projectPath);
|
|
6954
|
+
if (configLoadResult.resultType !== "success") return void 0;
|
|
6955
|
+
return createMatchPath(configLoadResult.absoluteBaseUrl, configLoadResult.paths)(prefix, void 0, isExistingDirectory, [
|
|
6956
|
+
".tsx",
|
|
6957
|
+
".ts",
|
|
6958
|
+
".js",
|
|
6959
|
+
".jsx",
|
|
6960
|
+
".cjs",
|
|
6961
|
+
".mjs"
|
|
6962
|
+
]) ?? void 0;
|
|
6963
|
+
};
|
|
6922
6964
|
//#endregion
|
|
6923
6965
|
//#region src/cli/utils/preview/hot-reloading/create-dependency-graph.ts
|
|
6924
6966
|
const readAllFilesInsideDirectory = async (directory) => {
|
|
@@ -6951,6 +6993,34 @@ const checkFileExtensionsUntilItExists = (pathWithoutExtension) => {
|
|
|
6951
6993
|
if (existsSync(`${pathWithoutExtension}.mjs`)) return `${pathWithoutExtension}.mjs`;
|
|
6952
6994
|
if (existsSync(`${pathWithoutExtension}.cjs`)) return `${pathWithoutExtension}.cjs`;
|
|
6953
6995
|
};
|
|
6996
|
+
const isUnderDirectory = (filePath, directoryPath) => {
|
|
6997
|
+
if (filePath === directoryPath) return true;
|
|
6998
|
+
const prefix = directoryPath.endsWith(path.sep) ? directoryPath : directoryPath + path.sep;
|
|
6999
|
+
return filePath.startsWith(prefix);
|
|
7000
|
+
};
|
|
7001
|
+
const resolveDynamicImportDirectory = (prefix, filePath) => {
|
|
7002
|
+
const moduleDirectory = path.dirname(filePath);
|
|
7003
|
+
const normalizedPrefix = path.normalize(prefix);
|
|
7004
|
+
const endsWithSeparator = normalizedPrefix.endsWith(path.sep);
|
|
7005
|
+
const trimmed = endsWithSeparator ? normalizedPrefix.slice(0, -path.sep.length) : normalizedPrefix;
|
|
7006
|
+
let resolvedPrefix;
|
|
7007
|
+
if (prefix.startsWith(".") || path.isAbsolute(prefix)) resolvedPrefix = path.resolve(moduleDirectory, normalizedPrefix);
|
|
7008
|
+
else {
|
|
7009
|
+
if (trimmed.length === 0) return void 0;
|
|
7010
|
+
const aliased = resolveAliasedDirectoryPrefix(trimmed, moduleDirectory);
|
|
7011
|
+
if (aliased === void 0) return;
|
|
7012
|
+
resolvedPrefix = aliased;
|
|
7013
|
+
}
|
|
7014
|
+
const directory = endsWithSeparator ? resolvedPrefix : path.dirname(resolvedPrefix);
|
|
7015
|
+
if (isUnderDirectory(moduleDirectory, directory)) return void 0;
|
|
7016
|
+
if (!existsSync(directory)) return void 0;
|
|
7017
|
+
try {
|
|
7018
|
+
if (!statSync(directory).isDirectory()) return void 0;
|
|
7019
|
+
} catch (_) {
|
|
7020
|
+
return;
|
|
7021
|
+
}
|
|
7022
|
+
return directory;
|
|
7023
|
+
};
|
|
6954
7024
|
/**
|
|
6955
7025
|
* Creates a stateful dependency graph that is structured in a way that you can get
|
|
6956
7026
|
* the dependents of a module from its path.
|
|
@@ -6965,11 +7035,17 @@ const createDependencyGraph = async (directory) => {
|
|
|
6965
7035
|
path,
|
|
6966
7036
|
dependencyPaths: [],
|
|
6967
7037
|
dependentPaths: [],
|
|
7038
|
+
dynamicDependencyDirectories: [],
|
|
6968
7039
|
moduleDependencies: []
|
|
6969
7040
|
}]));
|
|
6970
7041
|
const getDependencyPaths = async (filePath) => {
|
|
6971
7042
|
const contents = await promises.readFile(filePath, "utf8");
|
|
6972
|
-
const
|
|
7043
|
+
const imports = isJavascriptModule(filePath) ? getImportedModules(contents) : {
|
|
7044
|
+
staticImports: [],
|
|
7045
|
+
dynamicImportPrefixes: []
|
|
7046
|
+
};
|
|
7047
|
+
const importedPathsRelativeToDirectory = (isJavascriptModule(filePath) ? resolvePathAliases(imports.staticImports, path.dirname(filePath)) : []).map((rawDependencyPath) => {
|
|
7048
|
+
const dependencyPath = rawDependencyPath.split("?")[0];
|
|
6973
7049
|
if (!dependencyPath.startsWith(".") || path.isAbsolute(dependencyPath)) return dependencyPath;
|
|
6974
7050
|
let pathToDependencyFromDirectory = path.resolve(path.dirname(filePath), dependencyPath);
|
|
6975
7051
|
let isDirectory = false;
|
|
@@ -6994,7 +7070,8 @@ const createDependencyGraph = async (directory) => {
|
|
|
6994
7070
|
const moduleDependencies = importedPathsRelativeToDirectory.filter((dependencyPath) => !dependencyPath.startsWith(".") && !path.isAbsolute(dependencyPath));
|
|
6995
7071
|
return {
|
|
6996
7072
|
dependencyPaths: importedPathsRelativeToDirectory.filter((dependencyPath) => dependencyPath.startsWith(".") || path.isAbsolute(dependencyPath)),
|
|
6997
|
-
moduleDependencies
|
|
7073
|
+
moduleDependencies,
|
|
7074
|
+
dynamicDependencyDirectories: Array.from(new Set(imports.dynamicImportPrefixes.map((prefix) => resolveDynamicImportDirectory(prefix, filePath)).filter((d) => typeof d === "string")))
|
|
6998
7075
|
};
|
|
6999
7076
|
};
|
|
7000
7077
|
const updateModuleDependenciesInGraph = async (moduleFilePath) => {
|
|
@@ -7002,10 +7079,12 @@ const createDependencyGraph = async (directory) => {
|
|
|
7002
7079
|
path: moduleFilePath,
|
|
7003
7080
|
dependencyPaths: [],
|
|
7004
7081
|
dependentPaths: [],
|
|
7082
|
+
dynamicDependencyDirectories: [],
|
|
7005
7083
|
moduleDependencies: []
|
|
7006
7084
|
};
|
|
7007
|
-
const { moduleDependencies, dependencyPaths: newDependencyPaths } = await getDependencyPaths(moduleFilePath);
|
|
7085
|
+
const { moduleDependencies, dependencyPaths: newDependencyPaths, dynamicDependencyDirectories: newDynamicDependencyDirectories } = await getDependencyPaths(moduleFilePath);
|
|
7008
7086
|
graph[moduleFilePath].moduleDependencies = moduleDependencies;
|
|
7087
|
+
graph[moduleFilePath].dynamicDependencyDirectories = newDynamicDependencyDirectories;
|
|
7009
7088
|
for (const dependencyPath of graph[moduleFilePath].dependencyPaths) {
|
|
7010
7089
|
if (newDependencyPaths.includes(dependencyPath)) continue;
|
|
7011
7090
|
const dependencyModule = graph[dependencyPath];
|
|
@@ -7055,6 +7134,10 @@ const createDependencyGraph = async (directory) => {
|
|
|
7055
7134
|
{ resolveDependentsOf: function resolveDependentsOf(pathToModule) {
|
|
7056
7135
|
const dependentPaths = /* @__PURE__ */ new Set();
|
|
7057
7136
|
const stack = [pathToModule];
|
|
7137
|
+
for (const module of Object.values(graph)) for (const directory of module.dynamicDependencyDirectories) if (isUnderDirectory(pathToModule, directory) && module.path !== pathToModule && !dependentPaths.has(module.path)) {
|
|
7138
|
+
dependentPaths.add(module.path);
|
|
7139
|
+
stack.push(module.path);
|
|
7140
|
+
}
|
|
7058
7141
|
while (stack.length > 0) {
|
|
7059
7142
|
const moduleEntry = graph[stack.pop()];
|
|
7060
7143
|
if (!moduleEntry) continue;
|
|
@@ -7094,6 +7177,13 @@ const setupHotreloading = async (devServer, emailDirRelativePath) => {
|
|
|
7094
7177
|
const getFilesOutsideEmailsDirectory = () => Object.keys(dependencyGraph).filter((p) => path.relative(absolutePathToEmailsDirectory, p).startsWith(".."));
|
|
7095
7178
|
let filesOutsideEmailsDirectory = getFilesOutsideEmailsDirectory();
|
|
7096
7179
|
for (const p of filesOutsideEmailsDirectory) watcher.add(p);
|
|
7180
|
+
const getDynamicDependencyDirectories = () => {
|
|
7181
|
+
const directories = /* @__PURE__ */ new Set();
|
|
7182
|
+
for (const module of Object.values(dependencyGraph)) for (const directory of module.dynamicDependencyDirectories) directories.add(directory);
|
|
7183
|
+
return [...directories];
|
|
7184
|
+
};
|
|
7185
|
+
let dynamicDependencyDirectories = getDynamicDependencyDirectories();
|
|
7186
|
+
for (const directory of dynamicDependencyDirectories) watcher.add(directory);
|
|
7097
7187
|
const exit = async () => {
|
|
7098
7188
|
await watcher.close();
|
|
7099
7189
|
};
|
|
@@ -7107,6 +7197,10 @@ const setupHotreloading = async (devServer, emailDirRelativePath) => {
|
|
|
7107
7197
|
for (const p of filesOutsideEmailsDirectory) if (!newFilesOutsideEmailsDirectory.includes(p)) watcher.unwatch(p);
|
|
7108
7198
|
for (const p of newFilesOutsideEmailsDirectory) if (!filesOutsideEmailsDirectory.includes(p)) watcher.add(p);
|
|
7109
7199
|
filesOutsideEmailsDirectory = newFilesOutsideEmailsDirectory;
|
|
7200
|
+
const newDynamicDependencyDirectories = getDynamicDependencyDirectories();
|
|
7201
|
+
for (const directory of dynamicDependencyDirectories) if (!newDynamicDependencyDirectories.includes(directory)) watcher.unwatch(directory);
|
|
7202
|
+
for (const directory of newDynamicDependencyDirectories) if (!dynamicDependencyDirectories.includes(directory)) watcher.add(directory);
|
|
7203
|
+
dynamicDependencyDirectories = newDynamicDependencyDirectories;
|
|
7110
7204
|
changes.push({
|
|
7111
7205
|
event,
|
|
7112
7206
|
filename: relativePathToChangeTarget
|
|
@@ -7352,6 +7446,33 @@ const dev = async ({ dir: emailsDirRelativePath, port }) => {
|
|
|
7352
7446
|
}
|
|
7353
7447
|
};
|
|
7354
7448
|
//#endregion
|
|
7449
|
+
//#region src/cli/utils/esbuild/inline-css-loader.ts
|
|
7450
|
+
const inlineCssLoader = () => ({
|
|
7451
|
+
name: "inline-css-loader",
|
|
7452
|
+
setup(build) {
|
|
7453
|
+
const namespace = "inline-css";
|
|
7454
|
+
build.onResolve({ filter: /\?(inline|raw)(&|$)/ }, (args) => {
|
|
7455
|
+
const [pathWithoutSuffix] = args.path.split("?");
|
|
7456
|
+
if (!pathWithoutSuffix) return null;
|
|
7457
|
+
return {
|
|
7458
|
+
path: path.isAbsolute(pathWithoutSuffix) ? pathWithoutSuffix : path.resolve(args.resolveDir, pathWithoutSuffix),
|
|
7459
|
+
namespace
|
|
7460
|
+
};
|
|
7461
|
+
});
|
|
7462
|
+
build.onLoad({
|
|
7463
|
+
filter: /.*/,
|
|
7464
|
+
namespace
|
|
7465
|
+
}, async (args) => {
|
|
7466
|
+
const contents = await promises.readFile(args.path, "utf8");
|
|
7467
|
+
return {
|
|
7468
|
+
contents: `export default ${JSON.stringify(contents)};`,
|
|
7469
|
+
loader: "js",
|
|
7470
|
+
watchFiles: [args.path]
|
|
7471
|
+
};
|
|
7472
|
+
});
|
|
7473
|
+
}
|
|
7474
|
+
});
|
|
7475
|
+
//#endregion
|
|
7355
7476
|
//#region src/cli/utils/esbuild/escape-string-for-regex.ts
|
|
7356
7477
|
function escapeStringForRegex(string) {
|
|
7357
7478
|
return string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
|
|
@@ -7470,7 +7591,7 @@ const exportTemplates = async (pathToWhereEmailMarkupShouldBeDumped, emailsDirec
|
|
|
7470
7591
|
outExtension: { ".js": ".cjs" },
|
|
7471
7592
|
outdir: pathToWhereEmailMarkupShouldBeDumped,
|
|
7472
7593
|
platform: "node",
|
|
7473
|
-
plugins: [renderingUtilitiesExporter(batch)],
|
|
7594
|
+
plugins: [inlineCssLoader(), renderingUtilitiesExporter(batch)],
|
|
7474
7595
|
write: true
|
|
7475
7596
|
});
|
|
7476
7597
|
await stop();
|
package/dist/index.cjs
CHANGED
|
@@ -40434,11 +40434,17 @@ const css = `
|
|
|
40434
40434
|
`;
|
|
40435
40435
|
//#endregion
|
|
40436
40436
|
//#region src/components/tailwind/utils/tailwindcss/setup-tailwind.ts
|
|
40437
|
-
|
|
40437
|
+
const SETUP_TAILWIND_KEYS = new Set(["config", "cssConfigs"]);
|
|
40438
|
+
async function setupTailwind(props = {}) {
|
|
40439
|
+
const stray = Object.keys(props).filter((k) => !SETUP_TAILWIND_KEYS.has(k));
|
|
40440
|
+
if (stray.length > 0) throw new Error(`setupTailwind now takes { config, cssConfigs } — received unexpected keys: ${stray.join(", ")}. If you used to call setupTailwind(config), wrap it: setupTailwind({ config }).`);
|
|
40441
|
+
const { config, cssConfigs } = props;
|
|
40438
40442
|
const baseCss = `
|
|
40439
40443
|
@layer theme, base, components, utilities;
|
|
40440
40444
|
@import "tailwindcss/theme.css" layer(theme);
|
|
40441
40445
|
@import "tailwindcss/utilities.css" layer(utilities);
|
|
40446
|
+
${cssConfigs?.theme ? "@import \"custom-theme.css\" layer(theme);" : ""}
|
|
40447
|
+
${cssConfigs?.utility ? "@import \"custom-utilities.css\" layer(utilities);" : ""}
|
|
40442
40448
|
@config;
|
|
40443
40449
|
`;
|
|
40444
40450
|
const compiler = await (0, tailwindcss.compile)(baseCss, {
|
|
@@ -40446,7 +40452,7 @@ async function setupTailwind(config) {
|
|
|
40446
40452
|
if (resourceHint === "config") return {
|
|
40447
40453
|
path: id,
|
|
40448
40454
|
base,
|
|
40449
|
-
module: config
|
|
40455
|
+
module: config ?? {}
|
|
40450
40456
|
};
|
|
40451
40457
|
throw new Error(`NO-OP: should we implement support for ${resourceHint}?`);
|
|
40452
40458
|
},
|
|
@@ -40472,6 +40478,16 @@ async function setupTailwind(config) {
|
|
|
40472
40478
|
path: id,
|
|
40473
40479
|
content: css
|
|
40474
40480
|
};
|
|
40481
|
+
if (id === "custom-theme.css") return {
|
|
40482
|
+
base,
|
|
40483
|
+
path: id,
|
|
40484
|
+
content: cssConfigs?.theme ?? ""
|
|
40485
|
+
};
|
|
40486
|
+
if (id === "custom-utilities.css") return {
|
|
40487
|
+
base,
|
|
40488
|
+
path: id,
|
|
40489
|
+
content: cssConfigs?.utility ?? ""
|
|
40490
|
+
};
|
|
40475
40491
|
throw new Error("stylesheet not supported, you can only import the ones from tailwindcss");
|
|
40476
40492
|
}
|
|
40477
40493
|
});
|
|
@@ -40541,8 +40557,15 @@ const pixelBasedPreset = { theme: { extend: {
|
|
|
40541
40557
|
96: "384px"
|
|
40542
40558
|
}
|
|
40543
40559
|
} } };
|
|
40544
|
-
function Tailwind({ children, config }) {
|
|
40545
|
-
const
|
|
40560
|
+
function Tailwind({ children, config, theme, utility }) {
|
|
40561
|
+
const twConfigData = {
|
|
40562
|
+
config,
|
|
40563
|
+
cssConfigs: {
|
|
40564
|
+
theme,
|
|
40565
|
+
utility
|
|
40566
|
+
}
|
|
40567
|
+
};
|
|
40568
|
+
const tailwindSetup = useSuspensedPromise(() => setupTailwind(twConfigData), JSON.stringify(twConfigData, (_key, value) => typeof value === "function" ? value.toString() : value));
|
|
40546
40569
|
let classesUsed = [];
|
|
40547
40570
|
let mappedChildren = mapReactTree(children, (node) => {
|
|
40548
40571
|
if (react.isValidElement(node)) {
|
package/dist/index.d.cts
CHANGED
|
@@ -5085,24 +5085,36 @@ declare function sanitizeStyleSheet(styleSheet: StyleSheet): void;
|
|
|
5085
5085
|
//#endregion
|
|
5086
5086
|
//#region src/components/tailwind/tailwind.d.ts
|
|
5087
5087
|
type TailwindConfig = Omit<Config, 'content'>;
|
|
5088
|
-
interface TailwindProps {
|
|
5089
|
-
children: React$2.ReactNode;
|
|
5090
|
-
config?: TailwindConfig;
|
|
5091
|
-
}
|
|
5092
5088
|
interface EmailElementProps {
|
|
5093
5089
|
children?: React$2.ReactNode;
|
|
5094
5090
|
className?: string;
|
|
5095
5091
|
style?: React$2.CSSProperties;
|
|
5096
5092
|
}
|
|
5097
5093
|
declare const pixelBasedPreset: TailwindConfig;
|
|
5094
|
+
interface TailwindProps {
|
|
5095
|
+
children: React$2.ReactNode;
|
|
5096
|
+
config?: TailwindConfig;
|
|
5097
|
+
theme?: string;
|
|
5098
|
+
utility?: string;
|
|
5099
|
+
}
|
|
5098
5100
|
declare function Tailwind({
|
|
5099
5101
|
children,
|
|
5100
|
-
config
|
|
5102
|
+
config,
|
|
5103
|
+
theme,
|
|
5104
|
+
utility
|
|
5101
5105
|
}: TailwindProps): React$2.ReactNode;
|
|
5102
5106
|
//#endregion
|
|
5103
5107
|
//#region src/components/tailwind/utils/tailwindcss/setup-tailwind.d.ts
|
|
5104
5108
|
type TailwindSetup = Awaited<ReturnType<typeof setupTailwind>>;
|
|
5105
|
-
|
|
5109
|
+
interface CSSConfigs {
|
|
5110
|
+
theme?: string;
|
|
5111
|
+
utility?: string;
|
|
5112
|
+
}
|
|
5113
|
+
interface SetupTailwindProps {
|
|
5114
|
+
config?: TailwindConfig;
|
|
5115
|
+
cssConfigs?: CSSConfigs;
|
|
5116
|
+
}
|
|
5117
|
+
declare function setupTailwind(props?: SetupTailwindProps): Promise<{
|
|
5106
5118
|
addUtilities: (candidates: string[]) => void;
|
|
5107
5119
|
getStyleSheet: () => StyleSheet;
|
|
5108
5120
|
}>;
|
package/dist/index.d.mts
CHANGED
|
@@ -5085,24 +5085,36 @@ declare function sanitizeStyleSheet(styleSheet: StyleSheet): void;
|
|
|
5085
5085
|
//#endregion
|
|
5086
5086
|
//#region src/components/tailwind/tailwind.d.ts
|
|
5087
5087
|
type TailwindConfig = Omit<Config, 'content'>;
|
|
5088
|
-
interface TailwindProps {
|
|
5089
|
-
children: React$2.ReactNode;
|
|
5090
|
-
config?: TailwindConfig;
|
|
5091
|
-
}
|
|
5092
5088
|
interface EmailElementProps {
|
|
5093
5089
|
children?: React$2.ReactNode;
|
|
5094
5090
|
className?: string;
|
|
5095
5091
|
style?: React$2.CSSProperties;
|
|
5096
5092
|
}
|
|
5097
5093
|
declare const pixelBasedPreset: TailwindConfig;
|
|
5094
|
+
interface TailwindProps {
|
|
5095
|
+
children: React$2.ReactNode;
|
|
5096
|
+
config?: TailwindConfig;
|
|
5097
|
+
theme?: string;
|
|
5098
|
+
utility?: string;
|
|
5099
|
+
}
|
|
5098
5100
|
declare function Tailwind({
|
|
5099
5101
|
children,
|
|
5100
|
-
config
|
|
5102
|
+
config,
|
|
5103
|
+
theme,
|
|
5104
|
+
utility
|
|
5101
5105
|
}: TailwindProps): React$2.ReactNode;
|
|
5102
5106
|
//#endregion
|
|
5103
5107
|
//#region src/components/tailwind/utils/tailwindcss/setup-tailwind.d.ts
|
|
5104
5108
|
type TailwindSetup = Awaited<ReturnType<typeof setupTailwind>>;
|
|
5105
|
-
|
|
5109
|
+
interface CSSConfigs {
|
|
5110
|
+
theme?: string;
|
|
5111
|
+
utility?: string;
|
|
5112
|
+
}
|
|
5113
|
+
interface SetupTailwindProps {
|
|
5114
|
+
config?: TailwindConfig;
|
|
5115
|
+
cssConfigs?: CSSConfigs;
|
|
5116
|
+
}
|
|
5117
|
+
declare function setupTailwind(props?: SetupTailwindProps): Promise<{
|
|
5106
5118
|
addUtilities: (candidates: string[]) => void;
|
|
5107
5119
|
getStyleSheet: () => StyleSheet;
|
|
5108
5120
|
}>;
|
package/dist/index.mjs
CHANGED
|
@@ -40413,11 +40413,17 @@ const css = `
|
|
|
40413
40413
|
`;
|
|
40414
40414
|
//#endregion
|
|
40415
40415
|
//#region src/components/tailwind/utils/tailwindcss/setup-tailwind.ts
|
|
40416
|
-
|
|
40416
|
+
const SETUP_TAILWIND_KEYS = new Set(["config", "cssConfigs"]);
|
|
40417
|
+
async function setupTailwind(props = {}) {
|
|
40418
|
+
const stray = Object.keys(props).filter((k) => !SETUP_TAILWIND_KEYS.has(k));
|
|
40419
|
+
if (stray.length > 0) throw new Error(`setupTailwind now takes { config, cssConfigs } — received unexpected keys: ${stray.join(", ")}. If you used to call setupTailwind(config), wrap it: setupTailwind({ config }).`);
|
|
40420
|
+
const { config, cssConfigs } = props;
|
|
40417
40421
|
const baseCss = `
|
|
40418
40422
|
@layer theme, base, components, utilities;
|
|
40419
40423
|
@import "tailwindcss/theme.css" layer(theme);
|
|
40420
40424
|
@import "tailwindcss/utilities.css" layer(utilities);
|
|
40425
|
+
${cssConfigs?.theme ? "@import \"custom-theme.css\" layer(theme);" : ""}
|
|
40426
|
+
${cssConfigs?.utility ? "@import \"custom-utilities.css\" layer(utilities);" : ""}
|
|
40421
40427
|
@config;
|
|
40422
40428
|
`;
|
|
40423
40429
|
const compiler = await compile(baseCss, {
|
|
@@ -40425,7 +40431,7 @@ async function setupTailwind(config) {
|
|
|
40425
40431
|
if (resourceHint === "config") return {
|
|
40426
40432
|
path: id,
|
|
40427
40433
|
base,
|
|
40428
|
-
module: config
|
|
40434
|
+
module: config ?? {}
|
|
40429
40435
|
};
|
|
40430
40436
|
throw new Error(`NO-OP: should we implement support for ${resourceHint}?`);
|
|
40431
40437
|
},
|
|
@@ -40451,6 +40457,16 @@ async function setupTailwind(config) {
|
|
|
40451
40457
|
path: id,
|
|
40452
40458
|
content: css
|
|
40453
40459
|
};
|
|
40460
|
+
if (id === "custom-theme.css") return {
|
|
40461
|
+
base,
|
|
40462
|
+
path: id,
|
|
40463
|
+
content: cssConfigs?.theme ?? ""
|
|
40464
|
+
};
|
|
40465
|
+
if (id === "custom-utilities.css") return {
|
|
40466
|
+
base,
|
|
40467
|
+
path: id,
|
|
40468
|
+
content: cssConfigs?.utility ?? ""
|
|
40469
|
+
};
|
|
40454
40470
|
throw new Error("stylesheet not supported, you can only import the ones from tailwindcss");
|
|
40455
40471
|
}
|
|
40456
40472
|
});
|
|
@@ -40520,8 +40536,15 @@ const pixelBasedPreset = { theme: { extend: {
|
|
|
40520
40536
|
96: "384px"
|
|
40521
40537
|
}
|
|
40522
40538
|
} } };
|
|
40523
|
-
function Tailwind({ children, config }) {
|
|
40524
|
-
const
|
|
40539
|
+
function Tailwind({ children, config, theme, utility }) {
|
|
40540
|
+
const twConfigData = {
|
|
40541
|
+
config,
|
|
40542
|
+
cssConfigs: {
|
|
40543
|
+
theme,
|
|
40544
|
+
utility
|
|
40545
|
+
}
|
|
40546
|
+
};
|
|
40547
|
+
const tailwindSetup = useSuspensedPromise(() => setupTailwind(twConfigData), JSON.stringify(twConfigData, (_key, value) => typeof value === "function" ? value.toString() : value));
|
|
40525
40548
|
let classesUsed = [];
|
|
40526
40549
|
let mappedChildren = mapReactTree(children, (node) => {
|
|
40527
40550
|
if (React$1.isValidElement(node)) {
|
package/package.json
CHANGED
|
@@ -899,6 +899,321 @@ describe('Tailwind component', () => {
|
|
|
899
899
|
});
|
|
900
900
|
});
|
|
901
901
|
|
|
902
|
+
describe('with css configuration', () => {
|
|
903
|
+
it('handles empty theme string', async () => {
|
|
904
|
+
const actualOutput = await render(
|
|
905
|
+
<Tailwind theme="">
|
|
906
|
+
<div className="bg-red-500 text-white">Default utilities</div>
|
|
907
|
+
</Tailwind>,
|
|
908
|
+
).then(pretty);
|
|
909
|
+
|
|
910
|
+
expect(actualOutput).toMatchInlineSnapshot(`
|
|
911
|
+
"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
912
|
+
<!--$-->
|
|
913
|
+
<div style="background-color:rgb(251,44,54);color:rgb(255,255,255)">
|
|
914
|
+
Default utilities
|
|
915
|
+
</div>
|
|
916
|
+
<!--/$-->
|
|
917
|
+
"
|
|
918
|
+
`);
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it('supports custom colors', async () => {
|
|
922
|
+
const theme = `
|
|
923
|
+
@theme {
|
|
924
|
+
--color-custom: #1fb6ff;
|
|
925
|
+
}
|
|
926
|
+
`;
|
|
927
|
+
|
|
928
|
+
const actualOutput = await render(
|
|
929
|
+
<Tailwind theme={theme}>
|
|
930
|
+
<div className="bg-custom text-custom" />
|
|
931
|
+
</Tailwind>,
|
|
932
|
+
).then(pretty);
|
|
933
|
+
|
|
934
|
+
expect(actualOutput).toMatchInlineSnapshot(`
|
|
935
|
+
"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
936
|
+
<!--$-->
|
|
937
|
+
<div style="background-color:rgb(31,182,255);color:rgb(31,182,255)"></div>
|
|
938
|
+
<!--/$-->
|
|
939
|
+
"
|
|
940
|
+
`);
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
it('supports custom fonts', async () => {
|
|
944
|
+
const theme = `
|
|
945
|
+
@theme {
|
|
946
|
+
--font-sans: "Graphik", sans-serif;
|
|
947
|
+
--font-serif: "Merriweather", serif;
|
|
948
|
+
}
|
|
949
|
+
`;
|
|
950
|
+
|
|
951
|
+
const actualOutput = await render(
|
|
952
|
+
<Tailwind theme={theme}>
|
|
953
|
+
<div className="font-sans" />
|
|
954
|
+
<div className="font-serif" />
|
|
955
|
+
</Tailwind>,
|
|
956
|
+
).then(pretty);
|
|
957
|
+
|
|
958
|
+
expect(actualOutput).toMatchInlineSnapshot(`
|
|
959
|
+
"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
960
|
+
<!--$-->
|
|
961
|
+
<div style='font-family:"Graphik",sans-serif'></div>
|
|
962
|
+
<div style='font-family:"Merriweather",serif'></div>
|
|
963
|
+
<!--/$-->
|
|
964
|
+
"
|
|
965
|
+
`);
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
it('supports custom spacing', async () => {
|
|
969
|
+
const theme = `
|
|
970
|
+
@theme {
|
|
971
|
+
--spacing-8xl: 96rem;
|
|
972
|
+
}
|
|
973
|
+
`;
|
|
974
|
+
|
|
975
|
+
const actualOutput = await render(
|
|
976
|
+
<Tailwind theme={theme}>
|
|
977
|
+
<div className="m-8xl" />
|
|
978
|
+
</Tailwind>,
|
|
979
|
+
).then(pretty);
|
|
980
|
+
expect(actualOutput).toMatchInlineSnapshot(`
|
|
981
|
+
"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
982
|
+
<!--$-->
|
|
983
|
+
<div style="margin:96rem"></div>
|
|
984
|
+
<!--/$-->
|
|
985
|
+
"
|
|
986
|
+
`);
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
it('supports custom border radius', async () => {
|
|
990
|
+
const theme = `
|
|
991
|
+
@theme {
|
|
992
|
+
--border-radius-4xl: 2rem;
|
|
993
|
+
}
|
|
994
|
+
`;
|
|
995
|
+
|
|
996
|
+
const actualOutput = await render(
|
|
997
|
+
<Tailwind theme={theme}>
|
|
998
|
+
<div className="rounded-4xl" />
|
|
999
|
+
</Tailwind>,
|
|
1000
|
+
).then(pretty);
|
|
1001
|
+
expect(actualOutput).toMatchInlineSnapshot(`
|
|
1002
|
+
"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
1003
|
+
<!--$-->
|
|
1004
|
+
<div style="border-radius:2rem"></div>
|
|
1005
|
+
<!--/$-->
|
|
1006
|
+
"
|
|
1007
|
+
`);
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
it('supports custom text alignment', async () => {
|
|
1011
|
+
const theme = `
|
|
1012
|
+
@theme {
|
|
1013
|
+
--text-align-justify: justify;
|
|
1014
|
+
}
|
|
1015
|
+
`;
|
|
1016
|
+
|
|
1017
|
+
const actualOutput = await render(
|
|
1018
|
+
<Tailwind theme={theme}>
|
|
1019
|
+
<div className="text-justify" />
|
|
1020
|
+
</Tailwind>,
|
|
1021
|
+
).then(pretty);
|
|
1022
|
+
|
|
1023
|
+
expect(actualOutput).toMatchInlineSnapshot(`
|
|
1024
|
+
"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
1025
|
+
<!--$-->
|
|
1026
|
+
<div style="text-align:justify"></div>
|
|
1027
|
+
<!--/$-->
|
|
1028
|
+
"
|
|
1029
|
+
`);
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
it('supports both config and theme props together', async () => {
|
|
1033
|
+
const customConfig = {
|
|
1034
|
+
theme: {
|
|
1035
|
+
extend: {
|
|
1036
|
+
colors: {
|
|
1037
|
+
primary: '#ff0000',
|
|
1038
|
+
},
|
|
1039
|
+
},
|
|
1040
|
+
},
|
|
1041
|
+
} satisfies TailwindConfig;
|
|
1042
|
+
|
|
1043
|
+
const customTheme = `
|
|
1044
|
+
@theme {
|
|
1045
|
+
--color-secondary: #00ff00;
|
|
1046
|
+
}
|
|
1047
|
+
`;
|
|
1048
|
+
|
|
1049
|
+
const actualOutput = await render(
|
|
1050
|
+
<Tailwind config={customConfig} theme={customTheme}>
|
|
1051
|
+
<div className="bg-primary text-secondary">Both config and theme</div>
|
|
1052
|
+
</Tailwind>,
|
|
1053
|
+
).then(pretty);
|
|
1054
|
+
|
|
1055
|
+
expect(actualOutput).toMatchInlineSnapshot(`
|
|
1056
|
+
"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
1057
|
+
<!--$-->
|
|
1058
|
+
<div style="background-color:rgb(255,0,0);color:rgb(0,255,0)">
|
|
1059
|
+
Both config and theme
|
|
1060
|
+
</div>
|
|
1061
|
+
<!--/$-->
|
|
1062
|
+
"
|
|
1063
|
+
`);
|
|
1064
|
+
});
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
describe('with utilities', () => {
|
|
1068
|
+
it('handles empty utilities string', async () => {
|
|
1069
|
+
const actualOutput = await render(
|
|
1070
|
+
<Tailwind utility="">
|
|
1071
|
+
<div className="bg-red-500 text-white">Default utilities</div>
|
|
1072
|
+
</Tailwind>,
|
|
1073
|
+
).then(pretty);
|
|
1074
|
+
|
|
1075
|
+
expect(actualOutput).toMatchInlineSnapshot(`
|
|
1076
|
+
"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
1077
|
+
<!--$-->
|
|
1078
|
+
<div style="background-color:rgb(251,44,54);color:rgb(255,255,255)">
|
|
1079
|
+
Default utilities
|
|
1080
|
+
</div>
|
|
1081
|
+
<!--/$-->
|
|
1082
|
+
"
|
|
1083
|
+
`);
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
it('supports custom utilities', async () => {
|
|
1087
|
+
const utilities = `
|
|
1088
|
+
.custom-shadow {
|
|
1089
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
1090
|
+
border-radius: 8px;
|
|
1091
|
+
padding: 16px;
|
|
1092
|
+
}
|
|
1093
|
+
`;
|
|
1094
|
+
|
|
1095
|
+
const actualOutput = await render(
|
|
1096
|
+
<Tailwind utility={utilities}>
|
|
1097
|
+
<div className="custom-shadow" />
|
|
1098
|
+
</Tailwind>,
|
|
1099
|
+
).then(pretty);
|
|
1100
|
+
|
|
1101
|
+
expect(actualOutput).toMatchInlineSnapshot(`
|
|
1102
|
+
"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
1103
|
+
<!--$-->
|
|
1104
|
+
<div
|
|
1105
|
+
style="box-shadow:0 4px 6px rgba(0,0,0,0.1);border-radius:8px;padding:16px"></div>
|
|
1106
|
+
<!--/$-->
|
|
1107
|
+
"
|
|
1108
|
+
`);
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
it('supports animations', async () => {
|
|
1112
|
+
const utilities = `
|
|
1113
|
+
.pulse-animation {
|
|
1114
|
+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
1115
|
+
}
|
|
1116
|
+
`;
|
|
1117
|
+
|
|
1118
|
+
const actualOutput = await render(
|
|
1119
|
+
<Tailwind utility={utilities}>
|
|
1120
|
+
<div className="pulse-animation" />
|
|
1121
|
+
</Tailwind>,
|
|
1122
|
+
).then(pretty);
|
|
1123
|
+
|
|
1124
|
+
expect(actualOutput).toMatchInlineSnapshot(`
|
|
1125
|
+
"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
1126
|
+
<!--$-->
|
|
1127
|
+
<div style="animation:pulse 2s cubic-bezier(0.4,0,0.6,1) infinite"></div>
|
|
1128
|
+
<!--/$-->
|
|
1129
|
+
"
|
|
1130
|
+
`);
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
it('supports both config and utilities props together', async () => {
|
|
1134
|
+
const customConfig = {
|
|
1135
|
+
theme: {
|
|
1136
|
+
extend: {
|
|
1137
|
+
colors: {
|
|
1138
|
+
primary: '#ff0000',
|
|
1139
|
+
},
|
|
1140
|
+
},
|
|
1141
|
+
},
|
|
1142
|
+
} satisfies TailwindConfig;
|
|
1143
|
+
|
|
1144
|
+
const customUtilities = `
|
|
1145
|
+
.card-base {
|
|
1146
|
+
border: 1px solid #e5e7eb;
|
|
1147
|
+
padding: 20px;
|
|
1148
|
+
}
|
|
1149
|
+
`;
|
|
1150
|
+
|
|
1151
|
+
const actualOutput = await render(
|
|
1152
|
+
<Tailwind config={customConfig} utility={customUtilities}>
|
|
1153
|
+
<div className="bg-primary card-base">Config and utilities</div>
|
|
1154
|
+
</Tailwind>,
|
|
1155
|
+
).then(pretty);
|
|
1156
|
+
|
|
1157
|
+
expect(actualOutput).toMatchInlineSnapshot(`
|
|
1158
|
+
"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
1159
|
+
<!--$-->
|
|
1160
|
+
<div
|
|
1161
|
+
style="background-color:rgb(255,0,0);border:1px solid rgb(229,231,235);padding:20px">
|
|
1162
|
+
Config and utilities
|
|
1163
|
+
</div>
|
|
1164
|
+
<!--/$-->
|
|
1165
|
+
"
|
|
1166
|
+
`);
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
it('supports config, theme, and utilities together', async () => {
|
|
1170
|
+
const customConfig = {
|
|
1171
|
+
theme: {
|
|
1172
|
+
extend: {
|
|
1173
|
+
colors: {
|
|
1174
|
+
primary: '#ff0000',
|
|
1175
|
+
},
|
|
1176
|
+
},
|
|
1177
|
+
},
|
|
1178
|
+
} satisfies TailwindConfig;
|
|
1179
|
+
|
|
1180
|
+
const customTheme = `
|
|
1181
|
+
@theme {
|
|
1182
|
+
--color-secondary: #00ff00;
|
|
1183
|
+
}
|
|
1184
|
+
`;
|
|
1185
|
+
|
|
1186
|
+
const customUtilities = `
|
|
1187
|
+
.special-border {
|
|
1188
|
+
border: 2px dashed #0000ff;
|
|
1189
|
+
}
|
|
1190
|
+
`;
|
|
1191
|
+
|
|
1192
|
+
const actualOutput = await render(
|
|
1193
|
+
<Tailwind
|
|
1194
|
+
config={customConfig}
|
|
1195
|
+
theme={customTheme}
|
|
1196
|
+
utility={customUtilities}
|
|
1197
|
+
>
|
|
1198
|
+
<div className="bg-primary text-secondary special-border">
|
|
1199
|
+
All three props
|
|
1200
|
+
</div>
|
|
1201
|
+
</Tailwind>,
|
|
1202
|
+
).then(pretty);
|
|
1203
|
+
|
|
1204
|
+
expect(actualOutput).toMatchInlineSnapshot(`
|
|
1205
|
+
"<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
1206
|
+
<!--$-->
|
|
1207
|
+
<div
|
|
1208
|
+
style="background-color:rgb(255,0,0);color:rgb(0,255,0);border:2px dashed rgb(0,0,255)">
|
|
1209
|
+
All three props
|
|
1210
|
+
</div>
|
|
1211
|
+
<!--/$-->
|
|
1212
|
+
"
|
|
1213
|
+
`);
|
|
1214
|
+
});
|
|
1215
|
+
});
|
|
1216
|
+
|
|
902
1217
|
describe('with custom plugins config', () => {
|
|
903
1218
|
const config = {
|
|
904
1219
|
plugins: [
|
|
@@ -13,11 +13,6 @@ import { setupTailwind } from './utils/tailwindcss/setup-tailwind.js';
|
|
|
13
13
|
|
|
14
14
|
export type TailwindConfig = Omit<Config, 'content'>;
|
|
15
15
|
|
|
16
|
-
export interface TailwindProps {
|
|
17
|
-
children: React.ReactNode;
|
|
18
|
-
config?: TailwindConfig;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
16
|
export interface EmailElementProps {
|
|
22
17
|
children?: React.ReactNode;
|
|
23
18
|
className?: string;
|
|
@@ -83,15 +78,29 @@ export const pixelBasedPreset: TailwindConfig = {
|
|
|
83
78
|
},
|
|
84
79
|
};
|
|
85
80
|
|
|
86
|
-
export
|
|
81
|
+
export interface TailwindProps {
|
|
82
|
+
children: React.ReactNode;
|
|
83
|
+
config?: TailwindConfig;
|
|
84
|
+
theme?: string;
|
|
85
|
+
utility?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function Tailwind({ children, config, theme, utility }: TailwindProps) {
|
|
89
|
+
const twConfigData = {
|
|
90
|
+
config,
|
|
91
|
+
cssConfigs: {
|
|
92
|
+
theme,
|
|
93
|
+
utility,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
87
96
|
const tailwindSetup = useSuspensedPromise(
|
|
88
|
-
() => setupTailwind(
|
|
89
|
-
JSON.stringify(
|
|
97
|
+
() => setupTailwind(twConfigData),
|
|
98
|
+
JSON.stringify(twConfigData, (_key, value) =>
|
|
90
99
|
typeof value === 'function' ? value.toString() : value,
|
|
91
100
|
),
|
|
92
101
|
);
|
|
93
|
-
let classesUsed: string[] = [];
|
|
94
102
|
|
|
103
|
+
let classesUsed: string[] = [];
|
|
95
104
|
let mappedChildren: React.ReactNode = mapReactTree(children, (node) => {
|
|
96
105
|
if (React.isValidElement<EmailElementProps>(node)) {
|
|
97
106
|
if (node.props.className) {
|
|
@@ -80,17 +80,19 @@ describe('extractRulesPerClass()', async () => {
|
|
|
80
80
|
|
|
81
81
|
it('splits mixed rules (base + media) into inlinable base and non-inlinable media', async () => {
|
|
82
82
|
const tailwind = await setupTailwind({
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
api
|
|
87
|
-
|
|
88
|
-
'
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
config: {
|
|
84
|
+
plugins: [
|
|
85
|
+
{
|
|
86
|
+
handler: (api) => {
|
|
87
|
+
api.addUtilities({
|
|
88
|
+
'.text-body': {
|
|
89
|
+
'@apply text-[green] sm:text-[darkgreen]': {},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
},
|
|
91
93
|
},
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
],
|
|
95
|
+
},
|
|
94
96
|
});
|
|
95
97
|
const classes = ['text-body'];
|
|
96
98
|
tailwind.addUtilities(classes);
|
|
@@ -115,17 +117,19 @@ describe('extractRulesPerClass()', async () => {
|
|
|
115
117
|
|
|
116
118
|
it('treats rules with pseudo-selectors as fully non-inlinable', async () => {
|
|
117
119
|
const tailwind = await setupTailwind({
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
api
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
120
|
+
config: {
|
|
121
|
+
plugins: [
|
|
122
|
+
{
|
|
123
|
+
handler: (api) => {
|
|
124
|
+
api.addUtilities({
|
|
125
|
+
'.btn:hover': {
|
|
126
|
+
color: 'red',
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
},
|
|
126
130
|
},
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
],
|
|
132
|
+
},
|
|
129
133
|
});
|
|
130
134
|
const classes = ['btn'];
|
|
131
135
|
tailwind.addUtilities(classes);
|
|
@@ -16,3 +16,10 @@ test('setupTailwind() and addUtilities()', async () => {
|
|
|
16
16
|
`"/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-red-100: oklch(93.6% 0.032 17.717);--color-red-500: oklch(63.7% 0.237 25.331);--color-blue-300: oklch(80.9% 0.105 251.813);--color-slate-900: oklch(20.8% 0.042 265.755)}}@layer utilities{.bg-red-100{background-color:var(--color-red-100)}.bg-slate-900{background-color:var(--color-slate-900)}.text-red-500{color:var(--color-red-500)}.sm\\:bg-blue-300{@media (width>=40rem){background-color:var(--color-blue-300)}}}"`,
|
|
17
17
|
);
|
|
18
18
|
});
|
|
19
|
+
|
|
20
|
+
test('setupTailwind() throws on legacy positional config shape', async () => {
|
|
21
|
+
await expect(
|
|
22
|
+
// @ts-expect-error — intentionally calling with the old shape to verify the guard
|
|
23
|
+
setupTailwind({ plugins: [], theme: { extend: {} } }),
|
|
24
|
+
).rejects.toThrowError(/setupTailwind now takes \{ config, cssConfigs \}/);
|
|
25
|
+
});
|
|
@@ -8,20 +8,43 @@ import utilitiesCss from './tailwind-stylesheets/utilities.js';
|
|
|
8
8
|
|
|
9
9
|
export type TailwindSetup = Awaited<ReturnType<typeof setupTailwind>>;
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
interface CSSConfigs {
|
|
12
|
+
theme?: string;
|
|
13
|
+
utility?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SetupTailwindProps {
|
|
17
|
+
config?: TailwindConfig;
|
|
18
|
+
cssConfigs?: CSSConfigs;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const SETUP_TAILWIND_KEYS = new Set(['config', 'cssConfigs']);
|
|
22
|
+
|
|
23
|
+
export async function setupTailwind(props: SetupTailwindProps = {}) {
|
|
24
|
+
const stray = Object.keys(props).filter((k) => !SETUP_TAILWIND_KEYS.has(k));
|
|
25
|
+
if (stray.length > 0) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`setupTailwind now takes { config, cssConfigs } — received unexpected keys: ${stray.join(', ')}. ` +
|
|
28
|
+
'If you used to call setupTailwind(config), wrap it: setupTailwind({ config }).',
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const { config, cssConfigs } = props;
|
|
12
32
|
const baseCss = `
|
|
13
33
|
@layer theme, base, components, utilities;
|
|
14
34
|
@import "tailwindcss/theme.css" layer(theme);
|
|
15
35
|
@import "tailwindcss/utilities.css" layer(utilities);
|
|
36
|
+
${cssConfigs?.theme ? '@import "custom-theme.css" layer(theme);' : ''}
|
|
37
|
+
${cssConfigs?.utility ? '@import "custom-utilities.css" layer(utilities);' : ''}
|
|
16
38
|
@config;
|
|
17
39
|
`;
|
|
40
|
+
|
|
18
41
|
const compiler = await compile(baseCss, {
|
|
19
42
|
async loadModule(id, base, resourceHint) {
|
|
20
43
|
if (resourceHint === 'config') {
|
|
21
44
|
return {
|
|
22
45
|
path: id,
|
|
23
46
|
base: base,
|
|
24
|
-
module: config,
|
|
47
|
+
module: config ?? {},
|
|
25
48
|
};
|
|
26
49
|
}
|
|
27
50
|
|
|
@@ -63,6 +86,22 @@ export async function setupTailwind(config: TailwindConfig) {
|
|
|
63
86
|
};
|
|
64
87
|
}
|
|
65
88
|
|
|
89
|
+
if (id === 'custom-theme.css') {
|
|
90
|
+
return {
|
|
91
|
+
base,
|
|
92
|
+
path: id,
|
|
93
|
+
content: cssConfigs?.theme ?? '',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (id === 'custom-utilities.css') {
|
|
98
|
+
return {
|
|
99
|
+
base,
|
|
100
|
+
path: id,
|
|
101
|
+
content: cssConfigs?.utility ?? '',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
66
105
|
throw new Error(
|
|
67
106
|
'stylesheet not supported, you can only import the ones from tailwindcss',
|
|
68
107
|
);
|