@reinvented/design 0.2.1 → 0.3.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/README.md CHANGED
@@ -21,6 +21,110 @@ import { Button, Card, Input, Badge, Dialog } from '@reinvented/design'
21
21
  - **Styling**: [Tailwind CSS](https://tailwindcss.com/) with design tokens
22
22
  - **Icons**: [Lucide Vue Next](https://lucide.dev/) (no emojis, ever)
23
23
  - **Typography**: Inter (via Google Fonts)
24
+ - **Linting**: Built-in ESLint plugin enforcing design rules
25
+
26
+ ---
27
+
28
+ ## Consumer Setup
29
+
30
+ ### 1. Install dependencies
31
+
32
+ ```bash
33
+ # The design system (includes all component + ESLint deps)
34
+ npm install @reinvented/design
35
+
36
+ # Required dev dependencies for Tailwind CSS processing
37
+ npm install -D tailwindcss@^3.4 postcss@^8.4 autoprefixer@^10.4 tailwindcss-animate@^1.0
38
+ ```
39
+
40
+ ### 2. Tailwind CSS config
41
+
42
+ Create `tailwind.config.js` in your app root. **Extend** the DS config — this gives you all design tokens, theme colors, animations, and font family:
43
+
44
+ ```js
45
+ import baseConfig from '@reinvented/design/tailwind.config.js'
46
+
47
+ export default {
48
+ ...baseConfig,
49
+ content: [
50
+ './index.html',
51
+ './src/**/*.{vue,js,ts,jsx,tsx}',
52
+ // Include DS source so Tailwind picks up classes used inside components
53
+ './node_modules/@reinvented/design/src/**/*.{vue,ts}',
54
+ ],
55
+ }
56
+ ```
57
+
58
+ ### 3. PostCSS config
59
+
60
+ Create `postcss.config.js`:
61
+
62
+ ```js
63
+ export default {
64
+ plugins: {
65
+ tailwindcss: {},
66
+ autoprefixer: {},
67
+ },
68
+ }
69
+ ```
70
+
71
+ ### 4. CSS entry point
72
+
73
+ Create `src/assets/main.css` (or similar) and import the DS styles. This loads the design tokens (CSS variables) and Tailwind directives:
74
+
75
+ ```css
76
+ @import '@reinvented/design/styles';
77
+ ```
78
+
79
+ Then import it in your app entry (`main.js` or `main.ts`):
80
+
81
+ ```js
82
+ import './assets/main.css'
83
+ ```
84
+
85
+ ### 5. Dark mode
86
+
87
+ The DS uses the `class` strategy for dark mode. Add `class="dark"` to `<html>` for dark theme:
88
+
89
+ ```html
90
+ <html lang="en" class="dark">
91
+ ```
92
+
93
+ All color tokens automatically switch between light and dark values.
94
+
95
+ ### 6. ESLint (recommended)
96
+
97
+ The DS ships an ESLint plugin that enforces design system rules. Create `eslint.config.js`:
98
+
99
+ ```js
100
+ import recommended from '@reinvented/design/eslint/recommended'
101
+
102
+ export default [
103
+ ...recommended,
104
+ // Your custom overrides (optional)
105
+ ]
106
+ ```
107
+
108
+ This enables:
109
+
110
+ | Rule | Severity | What it catches |
111
+ |------|----------|-----------------|
112
+ | `no-raw-button` | error | Raw `<button>` → use `<Button>` |
113
+ | `no-raw-input` | error | Raw `<input>` → use `<Input>` |
114
+ | `no-raw-select` | error | Raw `<select>` → use `<Select>` |
115
+ | `no-raw-textarea` | error | Raw `<textarea>` → use `<Textarea>` |
116
+ | `no-browser-dialogs` | error | `alert()`, `confirm()`, `prompt()` → use Dialog/Toast |
117
+ | `no-emoji` | error | Emoji characters → use Lucide icons |
118
+ | `no-hardcoded-colors` | warn | Inline hex/rgb colors → use token variables |
119
+ | `no-arbitrary-tailwind` | warn | `text-[#333]` → use token classes |
120
+
121
+ Install ESLint if not already present:
122
+
123
+ ```bash
124
+ npm install -D eslint
125
+ ```
126
+
127
+ ---
24
128
 
25
129
  ## Structure
26
130
 
@@ -31,6 +135,7 @@ src/
31
135
  layouts/ Page-level layout components with slots
32
136
  lib/ Utilities (cn, etc.)
33
137
  styles/ Tokens, CSS variables
138
+ eslint/ ESLint plugin + recommended config
34
139
  docs/
35
140
  rules.md Hard design rules
36
141
  conventions.md Opinionated UX decisions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reinvented/design",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "exports": {
@@ -10,7 +10,9 @@
10
10
  "./src/styles/*": "./src/styles/*",
11
11
  "./src/*": "./src/*",
12
12
  "./tailwind.config.js": "./tailwind.config.js",
13
- "./patterns/*": "./src/patterns/*"
13
+ "./patterns/*": "./src/patterns/*",
14
+ "./eslint": "./src/eslint/index.js",
15
+ "./eslint/recommended": "./src/eslint/recommended.js"
14
16
  },
15
17
  "files": [
16
18
  "src/",
@@ -40,7 +42,9 @@
40
42
  "zod": "^3.23.0",
41
43
  "vue-sonner": "^1.2.0",
42
44
  "v-calendar": "^3.1.0",
43
- "@tanstack/vue-table": "^8.21.0"
45
+ "@tanstack/vue-table": "^8.21.0",
46
+ "eslint-plugin-vue": "^9.28.0",
47
+ "vue-eslint-parser": "^9.4.0"
44
48
  },
45
49
  "devDependencies": {
46
50
  "vue": "^3.5.0",
@@ -0,0 +1,192 @@
1
+ /**
2
+ * @reinvented/design ESLint Plugin
3
+ *
4
+ * Enforces the Reinvented design system rules across all consumer apps.
5
+ * See docs/rules.md for the full list.
6
+ *
7
+ * Usage (flat config):
8
+ * import recommended from '@reinvented/design/eslint/recommended'
9
+ * export default [...recommended]
10
+ */
11
+
12
+ // ── Helpers ────────────────────────────────────────────────────────
13
+
14
+ /** Matches emoji characters (Emoji_Presentation + common ranges) */
15
+ const EMOJI_REGEX = /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{FE00}-\u{FE0F}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{200D}\u{20E3}\u{E0020}-\u{E007F}\u{231A}-\u{231B}\u{23E9}-\u{23F3}\u{23F8}-\u{23FA}\u{25AA}-\u{25AB}\u{25B6}\u{25C0}\u{25FB}-\u{25FE}\u{2614}-\u{2615}\u{2648}-\u{2653}\u{267F}\u{2693}\u{26A1}\u{26AA}-\u{26AB}\u{26BD}-\u{26BE}\u{26C4}-\u{26C5}\u{26CE}\u{26D4}\u{26EA}\u{26F2}-\u{26F3}\u{26F5}\u{26FA}\u{26FD}\u{2702}\u{2705}\u{2708}-\u{270D}\u{270F}]/u
16
+
17
+ /**
18
+ * Creates a rule that bans a specific raw HTML element in Vue templates.
19
+ * @param {string} element - The raw element name to ban (e.g. 'button')
20
+ * @param {string} component - The DS component to use instead (e.g. 'Button')
21
+ */
22
+ function createNoRawElementRule(element, component) {
23
+ return {
24
+ meta: {
25
+ type: 'suggestion',
26
+ docs: {
27
+ description: `Disallow raw <${element}> — use <${component}> from @reinvented/design`,
28
+ recommended: true,
29
+ },
30
+ messages: {
31
+ noRawElement: `Use <${component}> from @reinvented/design instead of raw <${element}>. See docs/rules.md.`,
32
+ },
33
+ schema: [],
34
+ },
35
+ create(context) {
36
+ // Works with eslint-plugin-vue's template AST
37
+ return context.parserServices?.defineTemplateBodyVisitor
38
+ ? context.parserServices.defineTemplateBodyVisitor({
39
+ VElement(node) {
40
+ if (node.rawName === element) {
41
+ context.report({ node, messageId: 'noRawElement' })
42
+ }
43
+ },
44
+ })
45
+ : {}
46
+ },
47
+ }
48
+ }
49
+
50
+ // ── Rules ──────────────────────────────────────────────────────────
51
+
52
+ const rules = {
53
+ // Rule 1-4: No raw form elements
54
+ 'no-raw-button': createNoRawElementRule('button', 'Button'),
55
+ 'no-raw-input': createNoRawElementRule('input', 'Input'),
56
+ 'no-raw-select': createNoRawElementRule('select', 'Select'),
57
+ 'no-raw-textarea': createNoRawElementRule('textarea', 'Textarea'),
58
+
59
+ // Rule 5: No browser dialog APIs
60
+ 'no-browser-dialogs': {
61
+ meta: {
62
+ type: 'problem',
63
+ docs: {
64
+ description: 'Disallow alert(), confirm(), prompt() — use Dialog/AlertDialog/Toast from @reinvented/design',
65
+ recommended: true,
66
+ },
67
+ messages: {
68
+ noBrowserDialog: 'Use Dialog, AlertDialog, or Toast from @reinvented/design instead of {{ name }}(). See docs/rules.md.',
69
+ },
70
+ schema: [],
71
+ },
72
+ create(context) {
73
+ const banned = new Set(['alert', 'confirm', 'prompt'])
74
+ return {
75
+ CallExpression(node) {
76
+ // Direct calls: alert(), confirm(), prompt()
77
+ if (node.callee.type === 'Identifier' && banned.has(node.callee.name)) {
78
+ context.report({ node, messageId: 'noBrowserDialog', data: { name: node.callee.name } })
79
+ }
80
+ // window.alert(), window.confirm(), window.prompt()
81
+ if (
82
+ node.callee.type === 'MemberExpression' &&
83
+ node.callee.object.type === 'Identifier' &&
84
+ node.callee.object.name === 'window' &&
85
+ node.callee.property.type === 'Identifier' &&
86
+ banned.has(node.callee.property.name)
87
+ ) {
88
+ context.report({ node, messageId: 'noBrowserDialog', data: { name: node.callee.property.name } })
89
+ }
90
+ },
91
+ }
92
+ },
93
+ },
94
+
95
+ // Rule 10: No emojis in templates
96
+ 'no-emoji': {
97
+ meta: {
98
+ type: 'suggestion',
99
+ docs: {
100
+ description: 'Disallow emoji characters in Vue templates — use Lucide icons from lucide-vue-next',
101
+ recommended: true,
102
+ },
103
+ messages: {
104
+ noEmoji: 'Use a Lucide icon from lucide-vue-next instead of emoji characters. See docs/rules.md.',
105
+ },
106
+ schema: [],
107
+ },
108
+ create(context) {
109
+ return context.parserServices?.defineTemplateBodyVisitor
110
+ ? context.parserServices.defineTemplateBodyVisitor({
111
+ 'VText'(node) {
112
+ if (EMOJI_REGEX.test(node.value)) {
113
+ context.report({ node, messageId: 'noEmoji' })
114
+ }
115
+ },
116
+ })
117
+ : {}
118
+ },
119
+ },
120
+
121
+ // Rule 6: No hardcoded colors
122
+ 'no-hardcoded-colors': {
123
+ meta: {
124
+ type: 'suggestion',
125
+ docs: {
126
+ description: 'Disallow hardcoded color values in inline styles — use CSS variables via Tailwind tokens',
127
+ recommended: true,
128
+ },
129
+ messages: {
130
+ noHardcodedColor: 'Use design token CSS variables (e.g. bg-background, text-foreground) instead of hardcoded colors. See docs/rules.md.',
131
+ },
132
+ schema: [],
133
+ },
134
+ create(context) {
135
+ const COLOR_REGEX = /#[0-9a-fA-F]{3,8}\b|rgba?\s*\(|hsla?\s*\(/
136
+ return context.parserServices?.defineTemplateBodyVisitor
137
+ ? context.parserServices.defineTemplateBodyVisitor({
138
+ 'VAttribute[key.name="style"]'(node) {
139
+ const value = node.value?.value
140
+ if (value && COLOR_REGEX.test(value)) {
141
+ context.report({ node, messageId: 'noHardcodedColor' })
142
+ }
143
+ },
144
+ })
145
+ : {}
146
+ },
147
+ },
148
+
149
+ // Rule 7: No arbitrary Tailwind values
150
+ 'no-arbitrary-tailwind': {
151
+ meta: {
152
+ type: 'suggestion',
153
+ docs: {
154
+ description: 'Disallow arbitrary Tailwind values like text-[#333] or p-[13px] — use token-based utilities',
155
+ recommended: true,
156
+ },
157
+ messages: {
158
+ noArbitrary: 'Use token-based Tailwind utilities instead of arbitrary values (e.g. {{ value }}). See docs/rules.md.',
159
+ },
160
+ schema: [],
161
+ },
162
+ create(context) {
163
+ // Matches Tailwind arbitrary value syntax: word-[value]
164
+ const ARBITRARY_REGEX = /\b(?:text|bg|border|ring|shadow|p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|w|h|gap|space|rounded|top|right|bottom|left|inset|min-w|min-h|max-w|max-h)-\[.+?\]/
165
+ return context.parserServices?.defineTemplateBodyVisitor
166
+ ? context.parserServices.defineTemplateBodyVisitor({
167
+ 'VAttribute[key.name="class"]'(node) {
168
+ const value = node.value?.value
169
+ if (value) {
170
+ const match = value.match(ARBITRARY_REGEX)
171
+ if (match) {
172
+ context.report({ node, messageId: 'noArbitrary', data: { value: match[0] } })
173
+ }
174
+ }
175
+ },
176
+ })
177
+ : {}
178
+ },
179
+ },
180
+ }
181
+
182
+ // ── Plugin Export ──────────────────────────────────────────────────
183
+
184
+ const plugin = {
185
+ meta: {
186
+ name: '@reinvented/design',
187
+ version: '0.3.0',
188
+ },
189
+ rules,
190
+ }
191
+
192
+ export default plugin
@@ -0,0 +1,64 @@
1
+ /**
2
+ * @reinvented/design — Recommended ESLint Flat Config
3
+ *
4
+ * Usage:
5
+ * import { recommended } from '@reinvented/design/eslint/recommended'
6
+ * export default [...recommended]
7
+ *
8
+ * Or combine with your own config:
9
+ * export default [...recommended, { rules: { /* overrides */ } }]
10
+ */
11
+
12
+ import vuePlugin from 'eslint-plugin-vue'
13
+ import vueParser from 'vue-eslint-parser'
14
+ import designPlugin from './index.js'
15
+
16
+ const PREFIX = 'reinvented-design'
17
+
18
+ export const recommended = [
19
+ // ── Base Vue support (essential rules) ──────────────────────────
20
+ ...vuePlugin.configs['flat/essential'],
21
+
22
+ // ── Design system enforcement ───────────────────────────────────
23
+ {
24
+ name: '@reinvented/design/recommended',
25
+ files: ['**/*.vue'],
26
+ languageOptions: {
27
+ parser: vueParser,
28
+ },
29
+ plugins: {
30
+ [PREFIX]: designPlugin,
31
+ },
32
+ rules: {
33
+ // Component usage — error on raw HTML elements
34
+ [`${PREFIX}/no-raw-button`]: 'error',
35
+ [`${PREFIX}/no-raw-input`]: 'error',
36
+ [`${PREFIX}/no-raw-select`]: 'error',
37
+ [`${PREFIX}/no-raw-textarea`]: 'error',
38
+
39
+ // No browser dialogs
40
+ [`${PREFIX}/no-browser-dialogs`]: 'error',
41
+
42
+ // No emojis — use Lucide icons
43
+ [`${PREFIX}/no-emoji`]: 'error',
44
+
45
+ // Styling hygiene
46
+ [`${PREFIX}/no-hardcoded-colors`]: 'warn',
47
+ [`${PREFIX}/no-arbitrary-tailwind`]: 'warn',
48
+ },
49
+ },
50
+
51
+ // ── JS/TS files: ban browser dialogs everywhere ─────────────────
52
+ {
53
+ name: '@reinvented/design/recommended-scripts',
54
+ files: ['**/*.{js,ts,jsx,tsx,vue}'],
55
+ plugins: {
56
+ [PREFIX]: designPlugin,
57
+ },
58
+ rules: {
59
+ [`${PREFIX}/no-browser-dialogs`]: 'error',
60
+ },
61
+ },
62
+ ]
63
+
64
+ export default recommended