Repository: lynx-family/lynx-stack
Pull request #745: feat(tailwind-preset): Basic tests and README for contributors
Author: login: Huxpro
Created: 2025-05-09T00:57:22Z | Merged: 2025-05-09T04:40:42Z

This diff provides a starting point for how we may
maintain the preset with the community.

The test is not ideal as it's not statically
sync-ed against the source of truth, but it serves
as a starting point.


Diff:
diff --git a/.changeset/wild-sheep-dream.md b/.changeset/wild-sheep-dream.md
new file mode 100644
index 0000000000..a71a91af9b
--- /dev/null
+++ b/.changeset/wild-sheep-dream.md
@@ -0,0 +1,5 @@
+---
+"@lynx-js/tailwind-preset": patch
+---
+
+Support `hidden`, `no-underline` and `line-through` utilities.
diff --git a/packages/third-party/tailwind-preset/README.md b/packages/third-party/tailwind-preset/README.md
new file mode 100644
index 0000000000..83804b8331
--- /dev/null
+++ b/packages/third-party/tailwind-preset/README.md
@@ -0,0 +1,58 @@
+# Lynx Tailwind Preset (V3)
+
+A [Tailwind V3](https://v3.tailwindcss.com/) CSS preset specifically designed for Lynx, ensuring that only CSS properties supported by Lynx are available as Tailwind utilities.
+
+> **⚠️ Experimental**\
+> This preset is currently in experimental stage as we are still exploring the best possible DX to write Tailwind upon Lynx. We welcome and encourage contributions from the community to help shape its future development. Your feedback, bug reports, and pull requests are invaluable in making this preset more robust and feature-complete.
+
+## Structure
+
+- `src/lynx.ts`: Main preset configuration that reverse-engineered [Tailwind's core plugins](https://github.com/tailwindlabs/tailwindcss/blob/v3/src/corePlugins.js).
+- `src/plugins/lynx/`: Custom plugins as replacement when per-class customization are needed.
+- `src/__tests__/`: Test files to ensure correct utility generation
+
+## Contributing
+
+### Getting Started
+
+```bash
+# Install dependencies
+pnpm install
+
+# Run tests
+pnpm test
+```
+
+### Adding New Utilities
+
+#### 1. Check if the CSS property is supported by Lynx
+
+This can be verified in three ways:
+
+1. [`@lynx-js/css-defines`](https://www.npmjs.com/package/@lynx-js/css-defines), this is the most accurate list of CSS properties supported by Lynx, directly generated from the source of Lynx internal definitions and released along with each Lynx releases.
+2. `csstype.d.ts` in `@lynx-js/types`, this is used as the types of inline styles (e.g. `<view style>`) but this is currently maintained manually.
+3. Lynx's runtime behaviors.
+
+#### 2. Add/Remove it from the preset
+
+If it's part of a core Tailwind plugin:
+
+- Add it to `corePlugins` in `src/lynx.ts`
+- Add the property to `supportedProperties` in `src/__tests__/config.test.ts`
+- Add the utility mapping to `cssPropertyValueToTailwindUtility`
+
+If it needs custom handling e.g. Lynx only support a partial set of the core plugin defined classes, or we need extensions e.g. `.linear`:
+
+- Create a new plugin in `src/plugins/lynx/`
+- Export it in `src/plugins/lynx/index.ts`
+- Add it to the plugins array in `src/lynx.ts`
+
+#### 3. Adding Tests
+
+We test by using Tailwind CLI to build `src/__tests__/` demo project with our preset, then extracting all properties used in the generated utilities and verify if all used properties are allowed according to `@lynx-js/types`.
+
+To test new Tailwind utilities:
+
+1. Modify `testClasses` in `src/__tests__/test-content.tsx`
+2. Modify `supportedProperties` or `allowedUnsupportedProperties` in `config.test.ts`
+3. Run tests with `pnpm test` to verify with Vitest.
diff --git a/packages/third-party/tailwind-preset/package.json b/packages/third-party/tailwind-preset/package.json
index 9b393b6ddb..b56c7bc62d 100644
--- a/packages/third-party/tailwind-preset/package.json
+++ b/packages/third-party/tailwind-preset/package.json
@@ -12,11 +12,6 @@
     "url": "https://github.com/lynx-family/lynx-stack.git",
     "directory": "packages/third-party/tailwind-preset"
   },
-  "license": "Apache-2.0",
-  "author": {
-    "name": "Qingyu Wang",
-    "email": "colinwang.0616@gmail.com"
-  },
   "type": "module",
   "exports": null,
   "main": "./lib/lynx.js",
@@ -26,7 +21,13 @@
     "CHANGELOG.md",
     "README.md"
   ],
+  "scripts": {
+    "test": "vitest",
+    "test:coverage": "vitest run --coverage"
+  },
   "devDependencies": {
+    "@lynx-js/types": "^3.2.1",
+    "postcss": "^8.4.35",
     "tailwindcss": "^3.4.17"
   }
 }
diff --git a/packages/third-party/tailwind-preset/src/__tests__/config.test.ts b/packages/third-party/tailwind-preset/src/__tests__/config.test.ts
new file mode 100644
index 0000000000..f11825bfb0
--- /dev/null
+++ b/packages/third-party/tailwind-preset/src/__tests__/config.test.ts
@@ -0,0 +1,146 @@
+// Copyright 2024 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+import { execSync } from 'node:child_process';
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import { beforeAll, describe, expect, test } from 'vitest';
+
+import type { CSSProperties } from '@lynx-js/types';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+describe('Lynx Tailwind Preset', () => {
+  let compiledCSS = '';
+  let usedProperties = new Set<string>();
+
+  beforeAll(() => {
+    try {
+      const cwd = path.resolve(__dirname, '../../');
+      const configPath = path.resolve(__dirname, 'tailwind.config.ts');
+      const inputPath = path.resolve(__dirname, 'styles.css');
+      const outputPath = path.resolve(__dirname, 'output.css');
+
+      // console.log('Working directory:', cwd);
+      // console.log('Config path:', configPath);
+      // console.log('Input path:', inputPath);
+      // console.log('Output path:', outputPath);
+
+      // Use Tailwind CLI to build CSS
+      execSync(
+        `npx tailwindcss -i ${inputPath} -o ${outputPath} -c ${configPath}`,
+        {
+          cwd,
+        },
+      );
+
+      // Read the generated CSS
+      compiledCSS = fs.readFileSync(outputPath, 'utf-8');
+
+      // Extract classes and properties
+      usedProperties = extractPropertiesFromCSS(compiledCSS);
+
+      // Cleanup only if file exists
+      if (fs.existsSync(outputPath)) {
+        fs.unlinkSync(outputPath);
+      }
+    } catch (error) {
+      console.error('Failed to generate CSS:', error);
+      throw error;
+    }
+  });
+
+  describe('Test against allowed CSS Properties', () => {
+    test('all used properties are supported', () => {
+      const allowedProperties = [
+        ...supportedProperties,
+        ...allowedUnsupportedProperties,
+      ];
+
+      // Check that all used properties are supported
+      for (const property of usedProperties) {
+        expect(allowedProperties).toContain(property);
+      }
+    });
+  });
+});
+
+// Helper function to convert kebab-case to camelCase
+function kebabToCamel(str: string): string {
+  return str.replace(
+    /-([a-z])/g,
+    (_: string, letter: string) => letter.toUpperCase(),
+  );
+}
+
+// Helper function to extract CSS property names from generated utilities
+function extractPropertiesFromCSS(css: string): Set<string> {
+  const properties = new Set<string>();
+  const propertyRegex = /([a-z-]+):/gi;
+  let match;
+
+  while ((match = propertyRegex.exec(css)) !== null) {
+    if (match[1] && !match[1].startsWith('--tw-')) {
+      properties.add(kebabToCamel(match[1]));
+    }
+  }
+
+  return properties;
+}
+
+/**
+ * Get all supported CSS properties from @lynx-js/types
+ * ideally this should be generated from the {@link CSSProperties} type
+ * or {@link https://www.npmjs.com/package/@lynx-js/css-define}
+ */
+const supportedProperties: Array<keyof CSSProperties> = [
+  'position',
+  'boxSizing',
+  'display',
+  'overflow',
+  'whiteSpace',
+  'textAlign',
+  'textOverflow',
+  'fontWeight',
+  'flexDirection',
+  'flexWrap',
+  'alignContent',
+  'alignItems',
+  'justifyContent',
+  'fontStyle',
+  'transform',
+  'animationTimingFunction',
+  'borderStyle',
+  'transformOrigin',
+  'linearOrientation',
+  'linearGravity',
+  'linearLayoutGravity',
+  'layoutAnimationCreateTimingFunction',
+  'layoutAnimationCreateProperty',
+  'layoutAnimationDeleteTimingFunction',
+  'layoutAnimationDeleteProperty',
+  'layoutAnimationUpdateTimingFunction',
+  'textDecoration',
+  'visibility',
+  'transitionProperty',
+  'transitionTimingFunction',
+  'borderLeftStyle',
+  'borderRightStyle',
+  'borderTopStyle',
+  'borderBottomStyle',
+  'overflowX',
+  'overflowY',
+  'wordBreak',
+  'outlineStyle',
+  'verticalAlign',
+  'direction',
+  'relativeCenter',
+  'linearCrossGravity',
+  'listMainAxisGap',
+];
+
+const allowedUnsupportedProperties = [
+  'overflowWrap',
+];
diff --git a/packages/third-party/tailwind-preset/src/__tests__/styles.css b/packages/third-party/tailwind-preset/src/__tests__/styles.css
new file mode 100644
index 0000000000..b5c61c9567
--- /dev/null
+++ b/packages/third-party/tailwind-preset/src/__tests__/styles.css
@@ -0,0 +1,3 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/packages/third-party/tailwind-preset/src/__tests__/tailwind.config.ts b/packages/third-party/tailwind-preset/src/__tests__/tailwind.config.ts
new file mode 100644
index 0000000000..65c69baad4
--- /dev/null
+++ b/packages/third-party/tailwind-preset/src/__tests__/tailwind.config.ts
@@ -0,0 +1,18 @@
+// Copyright 2024 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import lynxPreset from '../lynx.js';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+export default {
+  presets: [lynxPreset],
+  content: [
+    path.resolve(__dirname, 'test-content.tsx'),
+    path.resolve(__dirname, 'styles.css'),
+  ],
+};
diff --git a/packages/third-party/tailwind-preset/src/__tests__/test-content.tsx b/packages/third-party/tailwind-preset/src/__tests__/test-content.tsx
new file mode 100644
index 0000000000..d80cb50d07
--- /dev/null
+++ b/packages/third-party/tailwind-preset/src/__tests__/test-content.tsx
@@ -0,0 +1,6 @@
+// Copyright 2024 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+
+export const testClasses =
+  'box-border box-content fixed absolute relative sticky flex grid flex-row flex-row-reverse flex-col flex-col-reverse flex-wrap flex-wrap-reverse items-stretch items-start items-center items-end items-baseline content-start content-center content-end content-between content-around justify-start justify-center justify-end justify-between justify-around visible invisible hidden block overflow-hidden overflow-visible overflow-x-hidden overflow-x-visible overflow-y-hidden overflow-y-visible italic not-italic font-normal font-bold text-left text-center text-right underline line-through no-underline break-normal break-all truncate whitespace-normal border-solid border-dashed border-dotted border-double border-none flex-nowrap content-stretch justify-evenly justify-stretch text-start text-end whitespace-nowrap break-keep collapse ltr rtl';
diff --git a/packages/third-party/tailwind-preset/src/lynx.ts b/packages/third-party/tailwind-preset/src/lynx.ts
index bc36476a3d..c54c1a790e 100644
--- a/packages/third-party/tailwind-preset/src/lynx.ts
+++ b/packages/third-party/tailwind-preset/src/lynx.ts
@@ -2,13 +2,13 @@
 // Licensed under the Apache License Version 2.0 that can be found in the
 // LICENSE file in the root directory of this source tree.
 
-import { display, position, underline } from './plugins/lynx/index.js';
+import { display, position, textDecoration } from './plugins/lynx/index.js';
 
 /**
  * Should be used with Tailwind JIT and configured with purge, otherwise the bundle will be too large.
  */
 export default {
-  plugins: [position, underline, display],
+  plugins: [position, textDecoration, display],
   corePlugins: [
     // 'preflight',
     'alignContent',
@@ -76,7 +76,7 @@ export default {
     'zIndex',
 
     'textAlign',
-    // 'textDecoration', // Defined using plugin
+    // 'textDecoration', // Replaced with plugin
     'textOverflow',
 
     'transformOrigin',
@@ -93,6 +93,7 @@ export default {
 
     'visibility',
     'whitespace',
+    'wordBreak',
 
     'gridColumn',
     'gridColumnStart',
@@ -151,8 +152,6 @@ export default {
 
 // 'overscrollBehavior'
 
-// 'wordBreak'
-
 // 'backgroundOpacity'
 // 'gradientColorStops'
 // 'boxDecorationBreak'
diff --git a/packages/third-party/tailwind-preset/src/plugins/lynx/display.ts b/packages/third-party/tailwind-preset/src/plugins/lynx/display.ts
index b6b5624c61..030f5bb81d 100644
--- a/packages/third-party/tailwind-preset/src/plugins/lynx/display.ts
+++ b/packages/third-party/tailwind-preset/src/plugins/lynx/display.ts
@@ -12,7 +12,10 @@ export const display: void = createPlugin(({ addUtilities, variants }) => {
       '.block': { display: 'block' },
       '.flex': { display: 'flex' },
       '.grid': { display: 'grid' },
+      '.hidden': { display: 'none' },
       // Below are not supported by Lynx:
+      // - anything with 'inline'
+      // - anything with 'table'
       // '.inline': { display: 'inline', },
       // '.list-item': { display: 'list-item', },
     },
diff --git a/packages/third-party/tailwind-preset/src/plugins/lynx/index.ts b/packages/third-party/tailwind-preset/src/plugins/lynx/index.ts
index 46452203cb..146ec847e9 100644
--- a/packages/third-party/tailwind-preset/src/plugins/lynx/index.ts
+++ b/packages/third-party/tailwind-preset/src/plugins/lynx/index.ts
@@ -2,6 +2,6 @@
 // Licensed under the Apache License Version 2.0 that can be found in the
 // LICENSE file in the root directory of this source tree.
 
-export * from './position.js';
-export * from './underline.js';
-export * from './display.js';
+export { display } from './display.js';
+export { position } from './position.js';
+export { textDecoration } from './textDecoration.js';
diff --git a/packages/third-party/tailwind-preset/src/plugins/lynx/underline.ts b/packages/third-party/tailwind-preset/src/plugins/lynx/textDecoration.ts
similarity index 64%
rename from packages/third-party/tailwind-preset/src/plugins/lynx/underline.ts
rename to packages/third-party/tailwind-preset/src/plugins/lynx/textDecoration.ts
index f4ddbc9134..37ebf45e39 100644
--- a/packages/third-party/tailwind-preset/src/plugins/lynx/underline.ts
+++ b/packages/third-party/tailwind-preset/src/plugins/lynx/textDecoration.ts
@@ -5,9 +5,15 @@
 /* eslint-disable @typescript-eslint/unbound-method */
 import { createPlugin } from '../../helpers.js';
 
-export const underline: void = createPlugin(({ addUtilities }) => {
+export const textDecoration: void = createPlugin(({ addUtilities }) => {
   addUtilities(
     {
+      '.no-underline': {
+        'text-decoration': 'none',
+      },
+      '.line-through': {
+        'text-decoration': 'line-through',
+      },
       '.underline': {
         'text-decoration': 'underline',
       },
diff --git a/packages/third-party/tailwind-preset/vitest.config.ts b/packages/third-party/tailwind-preset/vitest.config.ts
new file mode 100644
index 0000000000..b029760026
--- /dev/null
+++ b/packages/third-party/tailwind-preset/vitest.config.ts
@@ -0,0 +1,10 @@
+import { defineProject } from 'vitest/config';
+
+export default defineProject({
+  test: {
+    name: 'tools/tailwind-preset',
+    environment: 'node',
+    include: ['src/**/*.{test,spec}.{js,ts}'],
+    globals: true,
+  },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 69638059de..58092fcff5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -587,6 +587,12 @@ importers:
 
   packages/third-party/tailwind-preset:
     devDependencies:
+      '@lynx-js/types':
+        specifier: ^3.2.1
+        version: 3.2.1
+      postcss:
+        specifier: ^8.4.35
+        version: 8.5.3
       tailwindcss:
         specifier: ^3.4.17
         version: 3.4.17
diff --git a/vitest.workspace.json b/vitest.workspace.json
index 9fd0bc750a..3872a87532 100644
--- a/vitest.workspace.json
+++ b/vitest.workspace.json
@@ -4,5 +4,6 @@
   "packages/tools/*/vitest.config.ts",
   "packages/webpack/*/vitest.config.ts",
   "packages/testing-library/*/vitest.config.mts",
-  "packages/testing-library/examples/*/vitest.config.ts"
+  "packages/testing-library/examples/*/vitest.config.ts",
+  "packages/third-party/*/vitest.config.ts"
 ]


Reviews:
user: login: colinaaa
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T01:55:13Z
user: login: Huxpro
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T01:58:48Z
user: login: Huxpro
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T02:00:34Z
user: login: Huxpro
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T02:33:59Z
user: login: colinaaa
body: I've added a changeset for you. Make sure you pull before pushing again :)
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-05-09T02:59:49Z
user: login: upupming
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T03:15:59Z
user: login: Huxpro
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T03:17:56Z
user: login: Huxpro
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T03:37:03Z
user: login: Huxpro
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T03:37:58Z
user: login: upupming
state: DISMISSED
author_association: COLLABORATOR
submitted_at: 2025-05-09T03:40:47Z
user: login: upupming
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T03:43:59Z
user: login: Huxpro
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-09T03:53:26Z
user: login: upupming
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-09T04:05:59Z

Review comments:
diff_hunk: @@ -0,0 +1,9 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
path: packages/third-party/tailwind-preset/vitest.config.ts
user: login: colinaaa
body: We are using the Vitest workspace. Use `defineProject` instead. See: https://vitest.dev/guide/workspace.html

Also, add `tailwind-preset` to `vitest.workspace.json`.
created_at: 2025-05-09T01:39:34Z
author_association: COLLABORATOR
diff_hunk: @@ -26,7 +21,15 @@
     "CHANGELOG.md",
     "README.md"
   ],
+  "scripts": {
+    "test": "vitest",
+    "test:coverage": "vitest run --coverage",
+    "test:watch": "vitest watch"
+  },
   "devDependencies": {
-    "tailwindcss": "^3.4.17"
+    "@lynx-js/types": "^3.2.1",
+    "postcss": "^8.4.35",
+    "tailwindcss": "^3.4.17",
+    "vitest": "^1.3.1"
path: packages/third-party/tailwind-preset/package.json
user: login: colinaaa
body: nitpick: We can remove this and use the `vitest` from workspace root, since there are also `@vitest/coverage-v8` and `@vitest/ui`
created_at: 2025-05-09T01:40:51Z
author_association: COLLABORATOR
diff_hunk: @@ -26,7 +21,15 @@
     "CHANGELOG.md",
     "README.md"
   ],
+  "scripts": {
+    "test": "vitest",
+    "test:coverage": "vitest run --coverage",
+    "test:watch": "vitest watch"
path: packages/third-party/tailwind-preset/package.json
user: login: colinaaa
body: nitpick: I think `vitest` would watch in non-CI environment :)
created_at: 2025-05-09T01:41:28Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,54 @@
+# Lynx Tailwind Preset
+
+A Tailwind CSS preset specifically designed for Lynx, ensuring that only CSS properties supported by Lynx are available as Tailwind utilities.
path: packages/third-party/tailwind-preset/README.md
user: login: colinaaa
body: ```suggestion
A Tailwind CSS V3 preset specifically designed for Lynx, ensuring that only CSS properties supported by Lynx are available as Tailwind utilities.
```
created_at: 2025-05-09T01:41:48Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,54 @@
+# Lynx Tailwind Preset
+
+A Tailwind CSS preset specifically designed for Lynx, ensuring that only CSS properties supported by Lynx are available as Tailwind utilities.
+
+> **⚠️ Experimental & Unstable**\
+> This preset is currently in experimental stage and may undergo significant changes. We welcome and encourage contributions from the community to help shape its future development. Your feedback, bug reports, and pull requests are invaluable in making this preset more robust and feature-complete.
+
+## Structure
+
+- `src/lynx.ts`: Main preset configuration that reverse-engineered [Tailwind's core plugins](https://github.com/tailwindlabs/tailwindcss/blob/v3/src/corePlugins.js).
+- `src/plugins/lynx/`: Custom plugins as replacement when per-class customization are needed.
+- `src/__tests__/`: Test files to ensure correct utility generation
+
+## Contributing
+
+### Getting Started
path: packages/third-party/tailwind-preset/README.md
user: login: colinaaa
body: It would be much easier if this package is in a separate repository.
created_at: 2025-05-09T01:44:58Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,302 @@
+// Copyright 2024 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+
+import { execSync } from 'node:child_process';
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import { beforeAll, describe, expect, test } from 'vitest';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+describe('Lynx Tailwind Preset', () => {
+  let compiledCSS = '';
+  let generatedClasses = new Set<string>();
path: packages/third-party/tailwind-preset/src/__tests__/config.test.ts
user: login: colinaaa
body: It seems like the tests are not finish yet, since this value is never read.
created_at: 2025-05-09T01:51:11Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,302 @@
+// Copyright 2024 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+
+import { execSync } from 'node:child_process';
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import { beforeAll, describe, expect, test } from 'vitest';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+describe('Lynx Tailwind Preset', () => {
+  let compiledCSS = '';
+  let generatedClasses = new Set<string>();
+  let usedProperties = new Set<string>();
+
+  beforeAll(() => {
+    try {
+      const cwd = path.resolve(__dirname, '../../');
+      const configPath = path.resolve(__dirname, 'tailwind.config.ts');
+      const inputPath = path.resolve(__dirname, 'styles.css');
+      const outputPath = path.resolve(__dirname, 'output.css');
+
+      // console.log('Working directory:', cwd);
+      // console.log('Config path:', configPath);
+      // console.log('Input path:', inputPath);
+      // console.log('Output path:', outputPath);
+
+      // Use Tailwind CLI to build CSS
+      execSync(
+        `npx tailwindcss -i ${inputPath} -o ${outputPath} -c ${configPath}`,
+        {
+          cwd,
+        },
+      );
+
+      // Read the generated CSS
+      compiledCSS = fs.readFileSync(outputPath, 'utf-8');
+      console.log('Generated CSS length:', compiledCSS.length);
+
+      // Extract classes and properties
+      generatedClasses = extractClassesFromCSS(compiledCSS);
+      usedProperties = extractPropertiesFromCSS(compiledCSS);
+
+      // Cleanup only if file exists
+      if (fs.existsSync(outputPath)) {
+        fs.unlinkSync(outputPath);
+      }
+    } catch (error) {
+      console.error('Failed to generate CSS:', error);
+      throw error;
+    }
+  });
+
+  describe('CSS Properties Coverage', () => {
+    test('each supported CSS property value has a corresponding Tailwind utility', () => {
+      // For each supported CSS property and value, verify its utility class exists
+      for (
+        const [property, valueMap] of Object.entries(
path: packages/third-party/tailwind-preset/src/__tests__/config.test.ts
user: login: colinaaa
body: ```suggestion
        const [_, valueMap] of Object.entries(
```
created_at: 2025-05-09T01:51:25Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,302 @@
+// Copyright 2024 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+
+import { execSync } from 'node:child_process';
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import { beforeAll, describe, expect, test } from 'vitest';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+describe('Lynx Tailwind Preset', () => {
+  let compiledCSS = '';
+  let generatedClasses = new Set<string>();
+  let usedProperties = new Set<string>();
+
+  beforeAll(() => {
+    try {
+      const cwd = path.resolve(__dirname, '../../');
+      const configPath = path.resolve(__dirname, 'tailwind.config.ts');
+      const inputPath = path.resolve(__dirname, 'styles.css');
+      const outputPath = path.resolve(__dirname, 'output.css');
+
+      // console.log('Working directory:', cwd);
+      // console.log('Config path:', configPath);
+      // console.log('Input path:', inputPath);
+      // console.log('Output path:', outputPath);
+
+      // Use Tailwind CLI to build CSS
+      execSync(
+        `npx tailwindcss -i ${inputPath} -o ${outputPath} -c ${configPath}`,
+        {
+          cwd,
+        },
+      );
+
+      // Read the generated CSS
+      compiledCSS = fs.readFileSync(outputPath, 'utf-8');
+      console.log('Generated CSS length:', compiledCSS.length);
+
+      // Extract classes and properties
+      generatedClasses = extractClassesFromCSS(compiledCSS);
+      usedProperties = extractPropertiesFromCSS(compiledCSS);
+
+      // Cleanup only if file exists
+      if (fs.existsSync(outputPath)) {
+        fs.unlinkSync(outputPath);
+      }
+    } catch (error) {
+      console.error('Failed to generate CSS:', error);
+      throw error;
+    }
+  });
+
+  describe('CSS Properties Coverage', () => {
+    test('each supported CSS property value has a corresponding Tailwind utility', () => {
+      // For each supported CSS property and value, verify its utility class exists
+      for (
+        const [property, valueMap] of Object.entries(
path: packages/third-party/tailwind-preset/src/__tests__/config.test.ts
user: login: colinaaa
body: Or
```suggestion
        const valueMap of Object.values(
```
created_at: 2025-05-09T01:51:41Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,302 @@
+// Copyright 2024 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+
+import { execSync } from 'node:child_process';
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import { beforeAll, describe, expect, test } from 'vitest';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+describe('Lynx Tailwind Preset', () => {
+  let compiledCSS = '';
+  let generatedClasses = new Set<string>();
+  let usedProperties = new Set<string>();
+
+  beforeAll(() => {
+    try {
+      const cwd = path.resolve(__dirname, '../../');
+      const configPath = path.resolve(__dirname, 'tailwind.config.ts');
+      const inputPath = path.resolve(__dirname, 'styles.css');
+      const outputPath = path.resolve(__dirname, 'output.css');
+
+      // console.log('Working directory:', cwd);
+      // console.log('Config path:', configPath);
+      // console.log('Input path:', inputPath);
+      // console.log('Output path:', outputPath);
+
+      // Use Tailwind CLI to build CSS
+      execSync(
+        `npx tailwindcss -i ${inputPath} -o ${outputPath} -c ${configPath}`,
+        {
+          cwd,
+        },
+      );
+
+      // Read the generated CSS
+      compiledCSS = fs.readFileSync(outputPath, 'utf-8');
+      console.log('Generated CSS length:', compiledCSS.length);
+
+      // Extract classes and properties
+      generatedClasses = extractClassesFromCSS(compiledCSS);
+      usedProperties = extractPropertiesFromCSS(compiledCSS);
+
+      // Cleanup only if file exists
+      if (fs.existsSync(outputPath)) {
+        fs.unlinkSync(outputPath);
+      }
+    } catch (error) {
+      console.error('Failed to generate CSS:', error);
+      throw error;
+    }
+  });
+
+  describe('CSS Properties Coverage', () => {
+    test('each supported CSS property value has a corresponding Tailwind utility', () => {
+      // For each supported CSS property and value, verify its utility class exists
+      for (
+        const [property, valueMap] of Object.entries(
+          cssPropertyValueToTailwindUtility,
+        )
+      ) {
+        for (const [value, utility] of Object.entries(valueMap)) {
path: packages/third-party/tailwind-preset/src/__tests__/config.test.ts
user: login: colinaaa
body: ditto.
```suggestion
        for (const [_, utility] of Object.entries(valueMap)) {
```
created_at: 2025-05-09T01:51:52Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,302 @@
+// Copyright 2024 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+
+import { execSync } from 'node:child_process';
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import { beforeAll, describe, expect, test } from 'vitest';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+describe('Lynx Tailwind Preset', () => {
+  let compiledCSS = '';
+  let generatedClasses = new Set<string>();
+  let usedProperties = new Set<string>();
+
+  beforeAll(() => {
+    try {
+      const cwd = path.resolve(__dirname, '../../');
+      const configPath = path.resolve(__dirname, 'tailwind.config.ts');
+      const inputPath = path.resolve(__dirname, 'styles.css');
+      const outputPath = path.resolve(__dirname, 'output.css');
+
+      // console.log('Working directory:', cwd);
+      // console.log('Config path:', configPath);
+      // console.log('Input path:', inputPath);
+      // console.log('Output path:', outputPath);
+
+      // Use Tailwind CLI to build CSS
+      execSync(
+        `npx tailwindcss -i ${inputPath} -o ${outputPath} -c ${configPath}`,
+        {
+          cwd,
+        },
+      );
+
+      // Read the generated CSS
+      compiledCSS = fs.readFileSync(outputPath, 'utf-8');
+      console.log('Generated CSS length:', compiledCSS.length);
+
+      // Extract classes and properties
+      generatedClasses = extractClassesFromCSS(compiledCSS);
+      usedProperties = extractPropertiesFromCSS(compiledCSS);
+
+      // Cleanup only if file exists
+      if (fs.existsSync(outputPath)) {
+        fs.unlinkSync(outputPath);
+      }
+    } catch (error) {
+      console.error('Failed to generate CSS:', error);
+      throw error;
+    }
+  });
+
+  describe('CSS Properties Coverage', () => {
+    test('each supported CSS property value has a corresponding Tailwind utility', () => {
+      // For each supported CSS property and value, verify its utility class exists
+      for (
+        const [property, valueMap] of Object.entries(
+          cssPropertyValueToTailwindUtility,
+        )
+      ) {
+        for (const [value, utility] of Object.entries(valueMap)) {
path: packages/third-party/tailwind-preset/src/__tests__/config.test.ts
user: login: colinaaa
body: Or
```suggestion
        for (const utility of Object.values(valueMap)) {
```
created_at: 2025-05-09T01:52:03Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,9 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  test: {
+    environment: 'node',
path: packages/third-party/tailwind-preset/vitest.config.ts
user: login: colinaaa
body: You would also need a `name` for this test so that it could be run on CI.

```suggestion
    name: 'tools/tailwind-preset',
    environment: 'node',
```

This is slightly tricky since we are separating the tests using `--project` in the CI pipeline.

https://github.com/lynx-family/lynx-stack/blob/54861d0050c16625cb432e5ba8e02be3f25ed2ac/.github/workflows/test.yml#L163
created_at: 2025-05-09T01:55:08Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,54 @@
+# Lynx Tailwind Preset
+
+A Tailwind CSS preset specifically designed for Lynx, ensuring that only CSS properties supported by Lynx are available as Tailwind utilities.
+
+> **⚠️ Experimental & Unstable**\
+> This preset is currently in experimental stage and may undergo significant changes. We welcome and encourage contributions from the community to help shape its future development. Your feedback, bug reports, and pull requests are invaluable in making this preset more robust and feature-complete.
+
+## Structure
+
+- `src/lynx.ts`: Main preset configuration that reverse-engineered [Tailwind's core plugins](https://github.com/tailwindlabs/tailwindcss/blob/v3/src/corePlugins.js).
+- `src/plugins/lynx/`: Custom plugins as replacement when per-class customization are needed.
+- `src/__tests__/`: Test files to ensure correct utility generation
+
+## Contributing
+
+### Getting Started
path: packages/third-party/tailwind-preset/README.md
user: login: Huxpro
body: @colinaaa well that's just an excuse that the monorepo DX is not good enough?

created_at: 2025-05-09T01:58:48Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,9 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
path: packages/third-party/tailwind-preset/vitest.config.ts
user: login: Huxpro
body: @colinaaa feeling like we need a guide on how to properly add a new package in the monorepo? Otherwise you'd have to repeat this again and again
created_at: 2025-05-09T02:00:34Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,54 @@
+# Lynx Tailwind Preset
+
+A Tailwind CSS preset specifically designed for Lynx, ensuring that only CSS properties supported by Lynx are available as Tailwind utilities.
path: packages/third-party/tailwind-preset/README.md
user: login: Huxpro
body: will do this later. I'll append another PR moving this into `packages` so this PR is easier to review
created_at: 2025-05-09T02:33:59Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,6 @@
+// Copyright 2024 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+
+export const testClasses =
+  'box-border box-content fixed absolute relative sticky flex grid flex-row flex-row-reverse flex-col flex-col-reverse flex-wrap flex-wrap-reverse items-stretch items-start items-center items-end items-baseline content-start content-center content-end content-between content-around justify-start justify-center justify-end justify-between justify-around visible invisible hidden block overflow-hidden overflow-visible overflow-x-hidden overflow-x-visible overflow-y-hidden overflow-y-visible italic not-italic font-normal font-bold text-left text-center text-right underline line-through no-underline break-normal break-all truncate whitespace-normal border-solid border-dashed border-dotted border-double border-none flex-nowrap content-stretch justify-evenly justify-stretch text-start text-end whitespace-nowrap break-keep collapse ltr rtl';
path: packages/third-party/tailwind-preset/src/__tests__/test-content.tsx
user: login: upupming
body: Can we maintain this list as an array  named `supportedUtilities` instead of a string?
created_at: 2025-05-09T03:08:41Z
author_association: COLLABORATOR
diff_hunk: @@ -2,13 +2,14 @@
 // Licensed under the Apache License Version 2.0 that can be found in the
 // LICENSE file in the root directory of this source tree.
 
-import { display, position, underline } from './plugins/lynx/index.js';
+import { display, position, textDecoration } from './plugins/lynx/index.js';
 
 /**
  * Should be used with Tailwind JIT and configured with purge, otherwise the bundle will be too large.
  */
 export default {
-  plugins: [position, underline, display],
+  content: ['./**/*.{js,ts,jsx,tsx,css}'],
path: packages/third-party/tailwind-preset/src/lynx.ts
user: login: upupming
body: Why do we need to add `*.css` to content?
created_at: 2025-05-09T03:14:48Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,6 @@
+// Copyright 2024 The Lynx Authors. All rights reserved.
+// Licensed under the Apache License Version 2.0 that can be found in the
+// LICENSE file in the root directory of this source tree.
+
+export const testClasses =
+  'box-border box-content fixed absolute relative sticky flex grid flex-row flex-row-reverse flex-col flex-col-reverse flex-wrap flex-wrap-reverse items-stretch items-start items-center items-end items-baseline content-start content-center content-end content-between content-around justify-start justify-center justify-end justify-between justify-around visible invisible hidden block overflow-hidden overflow-visible overflow-x-hidden overflow-x-visible overflow-y-hidden overflow-y-visible italic not-italic font-normal font-bold text-left text-center text-right underline line-through no-underline break-normal break-all truncate whitespace-normal border-solid border-dashed border-dotted border-double border-none flex-nowrap content-stretch justify-evenly justify-stretch text-start text-end whitespace-nowrap break-keep collapse ltr rtl';
path: packages/third-party/tailwind-preset/src/__tests__/test-content.tsx
user: login: Huxpro
body: > Can we maintain this list as an array named `supportedUtilities` instead of a string?

this is the source of test case, i.e. `<view classnames={testClasses}>`
created_at: 2025-05-09T03:17:56Z
author_association: COLLABORATOR
diff_hunk: @@ -2,13 +2,14 @@
 // Licensed under the Apache License Version 2.0 that can be found in the
 // LICENSE file in the root directory of this source tree.
 
-import { display, position, underline } from './plugins/lynx/index.js';
+import { display, position, textDecoration } from './plugins/lynx/index.js';
 
 /**
  * Should be used with Tailwind JIT and configured with purge, otherwise the bundle will be too large.
  */
 export default {
-  plugins: [position, underline, display],
+  content: ['./**/*.{js,ts,jsx,tsx,css}'],
path: packages/third-party/tailwind-preset/src/lynx.ts
user: login: Huxpro
body: @upupming up 

there is a `.css` file under `__test__`


created_at: 2025-05-09T03:37:03Z
author_association: COLLABORATOR
diff_hunk: @@ -2,13 +2,14 @@
 // Licensed under the Apache License Version 2.0 that can be found in the
 // LICENSE file in the root directory of this source tree.
 
-import { display, position, underline } from './plugins/lynx/index.js';
+import { display, position, textDecoration } from './plugins/lynx/index.js';
 
 /**
  * Should be used with Tailwind JIT and configured with purge, otherwise the bundle will be too large.
  */
 export default {
-  plugins: [position, underline, display],
+  content: ['./**/*.{js,ts,jsx,tsx,css}'],
path: packages/third-party/tailwind-preset/src/lynx.ts
user: login: Huxpro
body: > Why do we need to add `*.css` to content?

packages/third-party/tailwind-preset/src/__tests__/styles.css
created_at: 2025-05-09T03:37:58Z
author_association: COLLABORATOR
diff_hunk: @@ -2,13 +2,14 @@
 // Licensed under the Apache License Version 2.0 that can be found in the
 // LICENSE file in the root directory of this source tree.
 
-import { display, position, underline } from './plugins/lynx/index.js';
+import { display, position, textDecoration } from './plugins/lynx/index.js';
 
 /**
  * Should be used with Tailwind JIT and configured with purge, otherwise the bundle will be too large.
  */
 export default {
-  plugins: [position, underline, display],
+  content: ['./**/*.{js,ts,jsx,tsx,css}'],
path: packages/third-party/tailwind-preset/src/lynx.ts
user: login: upupming
body: ```suggestion
  content: ['./**/*.{js,ts,jsx,tsx}'],
```
created_at: 2025-05-09T03:43:58Z
author_association: COLLABORATOR
diff_hunk: @@ -2,13 +2,14 @@
 // Licensed under the Apache License Version 2.0 that can be found in the
 // LICENSE file in the root directory of this source tree.
 
-import { display, position, underline } from './plugins/lynx/index.js';
+import { display, position, textDecoration } from './plugins/lynx/index.js';
 
 /**
  * Should be used with Tailwind JIT and configured with purge, otherwise the bundle will be too large.
  */
 export default {
-  plugins: [position, underline, display],
+  content: ['./**/*.{js,ts,jsx,tsx,css}'],
path: packages/third-party/tailwind-preset/src/lynx.ts
user: login: Huxpro
body: Resolved over chat: the preset shouldn't include `content` at all --- AI did this :)
created_at: 2025-05-09T03:53:26Z
author_association: COLLABORATOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-05-09T00:57:25Z
body: ###  🦋  Changeset detected

Latest commit: e679cc603605d72676a57c77c0b1268546dd9cc5

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                     | Type  |
| ------------------------ | ----- |
| @lynx-js/tailwind-preset | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/Huxpro/lynx-stack/new/main?filename=.changeset/modern-owls-clean.md&value=---%0A%22%40lynx-js%2Ftailwind-preset%22%3A%20patch%0A---%0A%0Afeat(tailwind-preset)%3A%20Basic%20tests%20and%20README%20for%20contributors%0A)


author_association: NONE
user: login: codecov[bot]
created_at: 2025-05-09T02:50:19Z
body: ## [Bundle](https://app.codecov.io/gh/lynx-family/lynx-stack/pull/745?dropdown=bundle) Report

Changes will increase total bundle size by 3.21kB (0.5%) :arrow_up:. This is within the [configured](https://docs.codecov.com/docs/javascript-bundle-analysis#main-features) threshold :white_check_mark:

<details><summary>Detailed changes</summary>

| Bundle name | Size | Change |
| ----------- | ---- | ------ |
| @lynx-js/example-react-lynx-cjs | 321.0kB | 3.21kB (1.01%) :arrow_up: |

</details>

### Affected Assets, Files, and Routes:

<details>
<summary>view changes for bundle: @lynx-js/example-react-lynx-cjs</summary>

#### **Assets Changed:**
| Asset Name | Size Change | Total Size | Change (%) |
| ---------- | ----------- | ---------- | ---------- |
| ```main.lynx.bundle``` | 1.67kB | 89.8kB | 1.89%  |
| ```.rspeedy/main/main-thread.js``` | 728 bytes | 40.78kB | 1.82%  |
| ```.rspeedy/main/background.*.js``` | 810 bytes | 39.86kB | 2.07%  |










</details>

author_association: NONE
user: login: codspeed-hq[bot]
created_at: 2025-05-09T02:50:45Z
body: <!-- __CODSPEED_PERFORMANCE_REPORT_COMMENT__ -->
<!-- __CODSPEED_INSTRUMENTATION_PERFORMANCE_REPORT_COMMENT__ -->

## [CodSpeed Performance Report](https://codspeed.io/lynx-family/lynx-stack/branches/Huxpro%3Amain)

### Merging #745 will **not alter performance**

<sub>Comparing <code>Huxpro:main</code> (e679cc6) with <code>main</code> (21860ed)</sub>



### Summary

`✅ 1` untouched benchmarks  




author_association: NONE