dnanocss 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/index.js +13 -0
- package/package.json +34 -0
- package/src/constants.js +63 -0
- package/src/engine.js +187 -0
- package/src/parser.js +137 -0
- package/src/utilities.js +199 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dnano
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# dnanoCSS ⚡
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
> A lightweight utility-first CSS engine powered by Vanilla JavaScript.
|
|
7
|
+
|
|
8
|
+
dnanoCSS is a lightweight runtime utility-first CSS engine built with Vanilla JavaScript. It scans your DOM, parses utility classes, and dynamically generates real CSS without any build step or external dependency.
|
|
9
|
+
|
|
10
|
+
## Why dnanoCSS?
|
|
11
|
+
|
|
12
|
+
Most utility-first frameworks require build tools, configuration, or compilation steps.
|
|
13
|
+
|
|
14
|
+
dnanoCSS explores a different approach:
|
|
15
|
+
utility classes are interpreted directly in the browser at runtime using pure JavaScript.
|
|
16
|
+
|
|
17
|
+
The goal of this project is to deeply understand:
|
|
18
|
+
|
|
19
|
+
- DOM traversal
|
|
20
|
+
- token parsing
|
|
21
|
+
- dynamic CSS generation
|
|
22
|
+
- runtime styling systems
|
|
23
|
+
- utility-first architecture
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- **Stylesheet Injection** — Generates real CSS rules in a `<style>` tag (not inline styles)
|
|
30
|
+
- **60+ Utilities** — Spacing, flexbox, colors, typography, borders, sizing, and more
|
|
31
|
+
- **Color Palette** — 30+ named colors + semantic tokens (`primary`, `danger`, `success`)
|
|
32
|
+
- **Responsive Breakpoints** — `dn-md-p-20` → `@media (min-width:768px)`
|
|
33
|
+
- **Pseudo-States** — `dn-hover-bg-primary`, `dn-focus-border-blue`
|
|
34
|
+
- **MutationObserver** — Auto-styles dynamically added DOM elements
|
|
35
|
+
- **Modular Architecture** — `engine`, `parser`, `utilities`, `constants` separated cleanly
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install dnanocss
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import "dnanocss";
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Environment Support
|
|
52
|
+
|
|
53
|
+
dnanoCSS currently works best in modern ESM/bundler environments such as:
|
|
54
|
+
|
|
55
|
+
- Vite
|
|
56
|
+
- Webpack
|
|
57
|
+
- Parcel
|
|
58
|
+
- Next.js
|
|
59
|
+
|
|
60
|
+
For direct browser usage without a bundler, use relative module imports or a future CDN build.
|
|
61
|
+
|
|
62
|
+
## Quick Start
|
|
63
|
+
|
|
64
|
+
```html
|
|
65
|
+
<!-- 1. Add the script -->
|
|
66
|
+
<script type="module" src="./index.js"></script>
|
|
67
|
+
|
|
68
|
+
<!-- 2. Use utility classes -->
|
|
69
|
+
<div class="dn-flex dn-items-center dn-justify-center dn-bg-primary dn-text-white dn-p-24 dn-rounded-12">
|
|
70
|
+
Hello dnanoCSS!
|
|
71
|
+
</div>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Utility Reference
|
|
77
|
+
|
|
78
|
+
### Spacing
|
|
79
|
+
```
|
|
80
|
+
dn-p-{n} dn-pt-{n} dn-pb-{n} dn-pl-{n} dn-pr-{n}
|
|
81
|
+
dn-px-{n} dn-py-{n}
|
|
82
|
+
dn-m-{n} dn-mt-{n} dn-mb-{n} dn-ml-{n} dn-mr-{n}
|
|
83
|
+
dn-mx-{n} dn-my-{n} dn-mx-auto
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Colors
|
|
87
|
+
```
|
|
88
|
+
dn-bg-{color} dn-text-{color} dn-border-{color}
|
|
89
|
+
```
|
|
90
|
+
Available: `black`, `white`, `primary`, `secondary`, `danger`, `success`, `warning`, `info`, `red`, `blue`, `green`, `purple`, `pink`, `teal`, `gray`, `gray-100`…`gray-900`, and more.
|
|
91
|
+
|
|
92
|
+
### Typography
|
|
93
|
+
```
|
|
94
|
+
dn-fs-{n} dn-fw-{weight} dn-lh-{n}
|
|
95
|
+
dn-text-center dn-text-left dn-text-right
|
|
96
|
+
dn-uppercase dn-lowercase dn-capitalize
|
|
97
|
+
dn-italic dn-underline dn-truncate
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Layout / Flexbox
|
|
101
|
+
```
|
|
102
|
+
dn-flex dn-col dn-grid dn-block dn-hidden
|
|
103
|
+
dn-justify-center dn-justify-between dn-justify-evenly
|
|
104
|
+
dn-items-center dn-items-start dn-items-end
|
|
105
|
+
dn-gap-{n} dn-wrap dn-nowrap
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Borders
|
|
109
|
+
```
|
|
110
|
+
dn-border dn-border-2 dn-border-4 dn-border-0
|
|
111
|
+
dn-rounded-{n}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Sizing
|
|
115
|
+
```
|
|
116
|
+
dn-w-{n} dn-h-{n} dn-w-full dn-h-screen dn-w-screen
|
|
117
|
+
dn-max-w-{n} dn-min-h-{n}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Responsive
|
|
121
|
+
Prefix any utility with a breakpoint:
|
|
122
|
+
```
|
|
123
|
+
dn-sm-p-20 → @media (min-width: 640px) { padding: 20px }
|
|
124
|
+
dn-md-p-20 → @media (min-width: 768px) { padding: 20px }
|
|
125
|
+
dn-lg-p-20 → @media (min-width: 1024px) { padding: 20px }
|
|
126
|
+
dn-xl-p-20 → @media (min-width: 1280px) { padding: 20px }
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Pseudo-States
|
|
130
|
+
```
|
|
131
|
+
dn-hover-bg-primary → .dn-hover-bg-primary:hover { background-color: #2563eb }
|
|
132
|
+
dn-focus-border-blue → .dn-focus-border-blue:focus { border-color: #3b82f6 }
|
|
133
|
+
dn-active-text-white → .dn-active-text-white:active { color: #ffffff }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Architecture
|
|
139
|
+
|
|
140
|
+
```txt
|
|
141
|
+
dnanoCSS/
|
|
142
|
+
├── index.js Entry point, boots engine on DOMContentLoaded
|
|
143
|
+
│
|
|
144
|
+
├── demo/
|
|
145
|
+
│ ├── index.html Main demo page showcasing utilities and features
|
|
146
|
+
│ ├── style.css Demo-specific styles
|
|
147
|
+
│ └── examples.html Utility showcase
|
|
148
|
+
│
|
|
149
|
+
├── src/
|
|
150
|
+
│ ├── engine.js DOM scanner, style injector, MutationObserver
|
|
151
|
+
│ ├── parser.js Tokenizer — converts "dn-p-20" → { padding: "20px" }
|
|
152
|
+
│ ├── utilities.js All utility mappings
|
|
153
|
+
│ └── constants.js Colors, breakpoints, config
|
|
154
|
+
│
|
|
155
|
+
├── package.json
|
|
156
|
+
├── README.md
|
|
157
|
+
├── LICENSE
|
|
158
|
+
└── .gitignore
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### How the Engine Works
|
|
162
|
+
|
|
163
|
+
1. **DOM Ready** — `DOMContentLoaded` fires, `initDnanoCSS()` is called
|
|
164
|
+
2. **Scan** — Every element's `classList` is checked for `dn-` prefixed classes
|
|
165
|
+
3. **Parse** — `parser.js` splits tokens, extracts breakpoints, pseudo-states, and values
|
|
166
|
+
4. **Generate** — Valid CSS rule strings are built (`camelCase` → `kebab-case`)
|
|
167
|
+
5. **Inject** — Rules are inserted into a single `<style id="dnano-styles">` tag
|
|
168
|
+
6. **Observe** — `MutationObserver` handles new elements added dynamically
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Future Improvements
|
|
173
|
+
|
|
174
|
+
- [ ] Dark mode variant (`dn-dark-bg-gray-900`)
|
|
175
|
+
- [ ] CSS custom property output (`--dn-spacing-4: 16px`)
|
|
176
|
+
- [ ] CLI extractor for static HTML → pure CSS file
|
|
177
|
+
- [ ] Plugin system for custom utilities
|
|
178
|
+
- [ ] Caching layer with localStorage for repeat visits
|
|
179
|
+
- [ ] `!important` modifier (`dn-!p-0`)
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT — free to use, modify, and extend.
|
package/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dnanoCSS — app.js
|
|
3
|
+
* Entry point. Boots the engine once the DOM is ready.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { initDnanoCSS } from "./src/engine.js";
|
|
7
|
+
|
|
8
|
+
if (document.readyState === "loading") {
|
|
9
|
+
document.addEventListener("DOMContentLoaded", initDnanoCSS);
|
|
10
|
+
} else {
|
|
11
|
+
// DOM already ready (script loaded with defer / at end of body)
|
|
12
|
+
initDnanoCSS();
|
|
13
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dnanocss",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "> A lightweight utility-first CSS engine powered by Vanilla JavaScript.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"index.js",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"css",
|
|
18
|
+
"utility-css",
|
|
19
|
+
"runtime-css",
|
|
20
|
+
"css-framework",
|
|
21
|
+
"tailwindcss",
|
|
22
|
+
"utility-first"
|
|
23
|
+
],
|
|
24
|
+
"author": "Dnyaneshwar More",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/dnano-more/dnanocss"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/dnano-more/dnanocss/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/dnano-more/dnanocss#readme"
|
|
34
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dnanoCSS — constants.js
|
|
3
|
+
* Color palette, breakpoints, and global config.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const COLOR_PALETTE = {
|
|
7
|
+
// Basic
|
|
8
|
+
black: "#0a0a0a",
|
|
9
|
+
white: "#ffffff",
|
|
10
|
+
transparent: "transparent",
|
|
11
|
+
|
|
12
|
+
// Grays
|
|
13
|
+
gray: "#6b7280",
|
|
14
|
+
"gray-100": "#f3f4f6",
|
|
15
|
+
"gray-200": "#e5e7eb",
|
|
16
|
+
"gray-300": "#d1d5db",
|
|
17
|
+
"gray-400": "#9ca3af",
|
|
18
|
+
"gray-500": "#6b7280",
|
|
19
|
+
"gray-600": "#4b5563",
|
|
20
|
+
"gray-700": "#374151",
|
|
21
|
+
"gray-800": "#1f2937",
|
|
22
|
+
"gray-900": "#111827",
|
|
23
|
+
|
|
24
|
+
// Brand / Semantic
|
|
25
|
+
primary: "#2563eb",
|
|
26
|
+
secondary:"#7c3aed",
|
|
27
|
+
success: "#16a34a",
|
|
28
|
+
danger: "#dc2626",
|
|
29
|
+
warning: "#d97706",
|
|
30
|
+
info: "#0891b2",
|
|
31
|
+
|
|
32
|
+
// Named colors
|
|
33
|
+
red: "#ef4444",
|
|
34
|
+
orange: "#f97316",
|
|
35
|
+
yellow: "#eab308",
|
|
36
|
+
green: "#22c55e",
|
|
37
|
+
teal: "#14b8a6",
|
|
38
|
+
blue: "#3b82f6",
|
|
39
|
+
indigo: "#6366f1",
|
|
40
|
+
purple: "#a855f7",
|
|
41
|
+
pink: "#ec4899",
|
|
42
|
+
rose: "#f43f5e",
|
|
43
|
+
sky: "#0ea5e9",
|
|
44
|
+
lime: "#84cc16",
|
|
45
|
+
amber: "#f59e0b",
|
|
46
|
+
cyan: "#06b6d4",
|
|
47
|
+
slate: "#64748b",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const BREAKPOINTS = {
|
|
51
|
+
sm: 640,
|
|
52
|
+
md: 768,
|
|
53
|
+
lg: 1024,
|
|
54
|
+
xl: 1280,
|
|
55
|
+
"2xl": 1536,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const CONFIG = {
|
|
59
|
+
prefix: "dn-",
|
|
60
|
+
unit: "px", // default numeric unit
|
|
61
|
+
remBase: 16,
|
|
62
|
+
styleTagId:"dnano-styles",
|
|
63
|
+
};
|
package/src/engine.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dnanoCSS — engine.js
|
|
3
|
+
* Runtime engine: scans the DOM, generates CSS rules, injects a <style> tag.
|
|
4
|
+
* Supports hover/focus/active pseudo-states and responsive breakpoints.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { parseClass } from "./parser.js";
|
|
8
|
+
import { BREAKPOINTS, CONFIG } from "./constants.js";
|
|
9
|
+
|
|
10
|
+
// ────────────────────────────────────────────────────────────────────
|
|
11
|
+
// Internal state
|
|
12
|
+
// ────────────────────────────────────────────────────────────────────
|
|
13
|
+
/** Set of class strings already processed → avoids duplicate rules */
|
|
14
|
+
const processedClasses = new Set();
|
|
15
|
+
|
|
16
|
+
/** The injected <style> element */
|
|
17
|
+
let styleTag = null;
|
|
18
|
+
|
|
19
|
+
// ────────────────────────────────────────────────────────────────────
|
|
20
|
+
// Public API
|
|
21
|
+
// ────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Bootstrap the engine.
|
|
25
|
+
* Call once; re-calling is safe (idempotent).
|
|
26
|
+
*/
|
|
27
|
+
export function initDnanoCSS() {
|
|
28
|
+
ensureStyleTag();
|
|
29
|
+
scanAndGenerate(document.documentElement);
|
|
30
|
+
attachMutationObserver();
|
|
31
|
+
console.info(`[dnanoCSS] ✓ Engine initialised. ${processedClasses.size} class(es) processed.`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Manually process a container element (useful after dynamic content loads).
|
|
36
|
+
* @param {Element} root
|
|
37
|
+
*/
|
|
38
|
+
export function processDnano(root = document.documentElement) {
|
|
39
|
+
scanAndGenerate(root);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ────────────────────────────────────────────────────────────────────
|
|
43
|
+
// Style-tag management
|
|
44
|
+
// ────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function ensureStyleTag() {
|
|
47
|
+
if (styleTag) return;
|
|
48
|
+
styleTag = document.getElementById(CONFIG.styleTagId);
|
|
49
|
+
if (!styleTag) {
|
|
50
|
+
styleTag = document.createElement("style");
|
|
51
|
+
styleTag.id = CONFIG.styleTagId;
|
|
52
|
+
styleTag.setAttribute("data-dnano", "true");
|
|
53
|
+
document.head.appendChild(styleTag);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Append a raw CSS rule string to the style tag.
|
|
59
|
+
*/
|
|
60
|
+
function injectRule(rule) {
|
|
61
|
+
styleTag.sheet
|
|
62
|
+
? styleTag.sheet.insertRule(rule, styleTag.sheet.cssRules.length)
|
|
63
|
+
: (styleTag.textContent += rule + "\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ────────────────────────────────────────────────────────────────────
|
|
67
|
+
// DOM scanning
|
|
68
|
+
// ────────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
function scanAndGenerate(root) {
|
|
71
|
+
const elements = root.querySelectorAll
|
|
72
|
+
? [root, ...root.querySelectorAll("*")]
|
|
73
|
+
: [root];
|
|
74
|
+
|
|
75
|
+
for (const el of elements) {
|
|
76
|
+
for (const cls of el.classList) {
|
|
77
|
+
if (!cls.startsWith(CONFIG.prefix)) continue;
|
|
78
|
+
if (processedClasses.has(cls)) continue;
|
|
79
|
+
|
|
80
|
+
const result = parseClass(cls);
|
|
81
|
+
if (!result) continue;
|
|
82
|
+
|
|
83
|
+
generateRule(cls, result);
|
|
84
|
+
processedClasses.add(cls);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ────────────────────────────────────────────────────────────────────
|
|
90
|
+
// CSS rule generation
|
|
91
|
+
// ────────────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Turn a parsed result into one or more CSS rule strings and inject them.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} cls - Original class name, e.g. "dn-hover-bg-red"
|
|
97
|
+
* @param {{ styles: Object, pseudo?: string, breakpoint?: string }} result
|
|
98
|
+
*/
|
|
99
|
+
function generateRule(cls, { styles, pseudo, breakpoint }) {
|
|
100
|
+
const selector = buildSelector(cls, pseudo);
|
|
101
|
+
const declarations = stylesToDeclarations(styles);
|
|
102
|
+
|
|
103
|
+
if (!declarations) return;
|
|
104
|
+
|
|
105
|
+
const rule = `${selector} { ${declarations} }`;
|
|
106
|
+
|
|
107
|
+
if (breakpoint) {
|
|
108
|
+
const bp = BREAKPOINTS[breakpoint];
|
|
109
|
+
if (bp) {
|
|
110
|
+
injectRule(`@media (min-width: ${bp}px) { ${rule} }`);
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
injectRule(rule);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Build the CSS selector for a class, with optional pseudo-state.
|
|
119
|
+
*
|
|
120
|
+
* Escapes special characters (colon, slash, dot) in class names for
|
|
121
|
+
* valid CSS selector strings.
|
|
122
|
+
*/
|
|
123
|
+
function buildSelector(cls, pseudo) {
|
|
124
|
+
const escaped = escapeCssClass(cls);
|
|
125
|
+
if (!pseudo) return `.${escaped}`;
|
|
126
|
+
|
|
127
|
+
const pseudoMap = {
|
|
128
|
+
hover: ":hover",
|
|
129
|
+
focus: ":focus",
|
|
130
|
+
active: ":active",
|
|
131
|
+
disabled: ":disabled",
|
|
132
|
+
placeholder: "::placeholder",
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const pseudoStr = pseudoMap[pseudo] || `:${pseudo}`;
|
|
136
|
+
return `.${escaped}${pseudoStr}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Escape characters that are special in CSS selectors.
|
|
141
|
+
*/
|
|
142
|
+
function escapeCssClass(cls) {
|
|
143
|
+
return cls.replace(/[./:[\]()#!,@*+~=^$|?{}\\]/g, (c) => `\\${c}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Convert a styles object { paddingTop: "20px", ... }
|
|
148
|
+
* into a declaration string "padding-top: 20px; ..."
|
|
149
|
+
*/
|
|
150
|
+
function stylesToDeclarations(styles) {
|
|
151
|
+
return Object.entries(styles)
|
|
152
|
+
.map(([prop, val]) => `${camelToKebab(prop)}: ${val}`)
|
|
153
|
+
.join("; ");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** paddingTop → padding-top */
|
|
157
|
+
function camelToKebab(str) {
|
|
158
|
+
return str.replace(/([A-Z])/g, (m) => `-${m.toLowerCase()}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ────────────────────────────────────────────────────────────────────
|
|
162
|
+
// MutationObserver — auto-process dynamically added nodes
|
|
163
|
+
// ────────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
function attachMutationObserver() {
|
|
166
|
+
const observer = new MutationObserver((mutations) => {
|
|
167
|
+
let needsScan = false;
|
|
168
|
+
for (const mutation of mutations) {
|
|
169
|
+
if (mutation.type === "childList" && mutation.addedNodes.length) {
|
|
170
|
+
needsScan = true;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
if (mutation.type === "attributes" && mutation.attributeName === "class") {
|
|
174
|
+
needsScan = true;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (needsScan) scanAndGenerate(document.documentElement);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
observer.observe(document.documentElement, {
|
|
182
|
+
childList: true,
|
|
183
|
+
subtree: true,
|
|
184
|
+
attributes: true,
|
|
185
|
+
attributeFilter: ["class"],
|
|
186
|
+
});
|
|
187
|
+
}
|
package/src/parser.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dnanoCSS — parser.js
|
|
3
|
+
* Parses a single "dn-*" class string into one or more CSS declarations.
|
|
4
|
+
*
|
|
5
|
+
* Returns: Array<{ property: string, value: string }>
|
|
6
|
+
* or null if the class is not recognised.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { NUMERIC_MAP, KEYWORD_MAP, buildColorRule } from "./utilities.js";
|
|
10
|
+
import { CONFIG } from "./constants.js";
|
|
11
|
+
|
|
12
|
+
// Color-prefix tokens that take a color name as their last segment.
|
|
13
|
+
const COLOR_PREFIXES = new Set(["bg", "text", "border", "outline", "shadow", "decoration"]);
|
|
14
|
+
|
|
15
|
+
// Responsive prefix tokens (sm, md, lg, xl, 2xl)
|
|
16
|
+
const BREAKPOINT_PREFIXES = new Set(["sm", "md", "lg", "xl", "2xl"]);
|
|
17
|
+
|
|
18
|
+
// Pseudo-state prefixes
|
|
19
|
+
const PSEUDO_PREFIXES = new Set(["hover", "focus", "active", "disabled", "placeholder"]);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Main parse function.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} cls - e.g. "dn-p-20", "dn-bg-primary", "dn-flex"
|
|
25
|
+
* @returns {{ styles: Object, pseudo?: string, breakpoint?: string } | null}
|
|
26
|
+
*/
|
|
27
|
+
export function parseClass(cls) {
|
|
28
|
+
if (!cls.startsWith(CONFIG.prefix)) return null;
|
|
29
|
+
|
|
30
|
+
// Strip the "dn-" prefix
|
|
31
|
+
let token = cls.slice(CONFIG.prefix.length); // e.g. "p-20" | "hover-bg-red" | "md-p-20"
|
|
32
|
+
|
|
33
|
+
let pseudo = null;
|
|
34
|
+
let breakpoint = null;
|
|
35
|
+
|
|
36
|
+
// ── Extract optional breakpoint prefix ──────────────────────────
|
|
37
|
+
const firstDash = token.indexOf("-");
|
|
38
|
+
if (firstDash !== -1) {
|
|
39
|
+
const maybeBreak = token.slice(0, firstDash);
|
|
40
|
+
if (BREAKPOINT_PREFIXES.has(maybeBreak)) {
|
|
41
|
+
breakpoint = maybeBreak;
|
|
42
|
+
token = token.slice(firstDash + 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Extract optional pseudo-state prefix ────────────────────────
|
|
47
|
+
const firstDash2 = token.indexOf("-");
|
|
48
|
+
if (firstDash2 !== -1) {
|
|
49
|
+
const maybePseudo = token.slice(0, firstDash2);
|
|
50
|
+
if (PSEUDO_PREFIXES.has(maybePseudo)) {
|
|
51
|
+
pseudo = maybePseudo;
|
|
52
|
+
token = token.slice(firstDash2 + 1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Try resolving the (possibly shortened) token ─────────────────
|
|
57
|
+
const styles = resolveToken(token);
|
|
58
|
+
if (!styles) return null;
|
|
59
|
+
|
|
60
|
+
return { styles, pseudo, breakpoint };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolves a token (with prefix stripped) into a styles object.
|
|
65
|
+
* Returns null if unrecognised.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} token e.g. "p-20" | "bg-primary" | "flex"
|
|
68
|
+
* @returns {Object|null}
|
|
69
|
+
*/
|
|
70
|
+
function resolveToken(token) {
|
|
71
|
+
// ── 1. Keyword / toggle utilities (exact match first) ──────────
|
|
72
|
+
if (KEYWORD_MAP[token]) {
|
|
73
|
+
return { ...KEYWORD_MAP[token] };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Need to split on dashes from here on ────────────────────────
|
|
77
|
+
// We must split carefully because tokens like "gap-x-4" or "border-t" exist.
|
|
78
|
+
// Strategy: try progressively longer "key" prefixes.
|
|
79
|
+
const parts = token.split("-");
|
|
80
|
+
|
|
81
|
+
// ── 2. Color utilities (dn-bg-{color}, dn-text-{color} …) ──────
|
|
82
|
+
if (parts.length >= 2 && COLOR_PREFIXES.has(parts[0])) {
|
|
83
|
+
// colorName is everything after the first segment
|
|
84
|
+
const colorName = parts.slice(1).join("-");
|
|
85
|
+
const rule = buildColorRule(parts[0], colorName);
|
|
86
|
+
if (rule) return rule;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── 3. Numeric utilities (dn-p-20, dn-fs-18, dn-rounded-10 …) ──
|
|
90
|
+
// Walk from longest key prefix to shortest to handle "gap-x", "min-w" etc.
|
|
91
|
+
for (let len = parts.length - 1; len >= 1; len--) {
|
|
92
|
+
const key = parts.slice(0, len).join("-");
|
|
93
|
+
const rawVal = parts.slice(len).join("-");
|
|
94
|
+
|
|
95
|
+
if (NUMERIC_MAP[key] !== undefined && rawVal !== "") {
|
|
96
|
+
const cssValue = buildNumericValue(rawVal);
|
|
97
|
+
const propOrArr = NUMERIC_MAP[key];
|
|
98
|
+
|
|
99
|
+
if (Array.isArray(propOrArr)) {
|
|
100
|
+
const styles = {};
|
|
101
|
+
propOrArr.forEach(p => { styles[p] = cssValue; });
|
|
102
|
+
return styles;
|
|
103
|
+
}
|
|
104
|
+
return { [propOrArr]: cssValue };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null; // unrecognised
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Convert a raw string value into a proper CSS value.
|
|
113
|
+
* "20" → "20px"
|
|
114
|
+
* "1.5" → "1.5px" (if non-zero decimal → keep as rem? No, keep simple.)
|
|
115
|
+
* "full" → "100%"
|
|
116
|
+
* "screen" → "100vw" (used rarely)
|
|
117
|
+
* "auto" → "auto"
|
|
118
|
+
* anything else → as-is
|
|
119
|
+
*/
|
|
120
|
+
function buildNumericValue(raw) {
|
|
121
|
+
if (raw === "auto") return "auto";
|
|
122
|
+
if (raw === "full") return "100%";
|
|
123
|
+
if (raw === "screen") return "100vw";
|
|
124
|
+
if (raw === "none") return "none";
|
|
125
|
+
if (raw === "px") return "1px";
|
|
126
|
+
|
|
127
|
+
// Pure number → add px
|
|
128
|
+
if (/^\d+(\.\d+)?$/.test(raw)) return `${raw}${CONFIG.unit}`;
|
|
129
|
+
|
|
130
|
+
// Fraction e.g. "1/2" → 50%
|
|
131
|
+
const fracMatch = raw.match(/^(\d+)\/(\d+)$/);
|
|
132
|
+
if (fracMatch) {
|
|
133
|
+
return `${((+fracMatch[1] / +fracMatch[2]) * 100).toFixed(4)}%`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return raw; // raw string (e.g. "bold", "center", hex colors)
|
|
137
|
+
}
|
package/src/utilities.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dnanoCSS — utilities.js
|
|
3
|
+
* Maps utility tokens → CSS property names and special handlers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { COLOR_PALETTE } from "./constants.js";
|
|
7
|
+
|
|
8
|
+
// ------------------------------------------------------------------
|
|
9
|
+
// 1. Numeric utilities (dn-{key}-{number})
|
|
10
|
+
// value is auto-converted to px unless unit is specified.
|
|
11
|
+
// ------------------------------------------------------------------
|
|
12
|
+
export const NUMERIC_MAP = {
|
|
13
|
+
// Spacing
|
|
14
|
+
p: "padding",
|
|
15
|
+
pt: "paddingTop",
|
|
16
|
+
pb: "paddingBottom",
|
|
17
|
+
pl: "paddingLeft",
|
|
18
|
+
pr: "paddingRight",
|
|
19
|
+
px: ["paddingLeft", "paddingRight"],
|
|
20
|
+
py: ["paddingTop", "paddingBottom"],
|
|
21
|
+
|
|
22
|
+
m: "margin",
|
|
23
|
+
mt: "marginTop",
|
|
24
|
+
mb: "marginBottom",
|
|
25
|
+
ml: "marginLeft",
|
|
26
|
+
mr: "marginRight",
|
|
27
|
+
mx: ["marginLeft", "marginRight"],
|
|
28
|
+
my: ["marginTop", "marginBottom"],
|
|
29
|
+
|
|
30
|
+
// Sizing
|
|
31
|
+
w: "width",
|
|
32
|
+
h: "height",
|
|
33
|
+
"min-w": "minWidth",
|
|
34
|
+
"min-h": "minHeight",
|
|
35
|
+
"max-w": "maxWidth",
|
|
36
|
+
"max-h": "maxHeight",
|
|
37
|
+
|
|
38
|
+
// Typography
|
|
39
|
+
fs: "fontSize",
|
|
40
|
+
lh: "lineHeight",
|
|
41
|
+
ls: "letterSpacing",
|
|
42
|
+
|
|
43
|
+
// Border
|
|
44
|
+
rounded: "borderRadius",
|
|
45
|
+
"border-w": "borderWidth",
|
|
46
|
+
|
|
47
|
+
// Flex / Grid
|
|
48
|
+
gap: "gap",
|
|
49
|
+
"gap-x":"columnGap",
|
|
50
|
+
"gap-y":"rowGap",
|
|
51
|
+
|
|
52
|
+
// Z-index / Opacity
|
|
53
|
+
z: "zIndex",
|
|
54
|
+
opacity: "opacity",
|
|
55
|
+
|
|
56
|
+
// Misc
|
|
57
|
+
top: "top",
|
|
58
|
+
right: "right",
|
|
59
|
+
bottom: "bottom",
|
|
60
|
+
left: "left",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ------------------------------------------------------------------
|
|
64
|
+
// 2. Keyword / toggle utilities (dn-{keyword})
|
|
65
|
+
// ------------------------------------------------------------------
|
|
66
|
+
export const KEYWORD_MAP = {
|
|
67
|
+
// Display
|
|
68
|
+
flex: { display: "flex" },
|
|
69
|
+
"inline-flex":{ display: "inline-flex" },
|
|
70
|
+
grid: { display: "grid" },
|
|
71
|
+
block: { display: "block" },
|
|
72
|
+
"inline-block":{ display: "inline-block" },
|
|
73
|
+
inline: { display: "inline" },
|
|
74
|
+
hidden: { display: "none" },
|
|
75
|
+
|
|
76
|
+
// Flex Direction
|
|
77
|
+
row: { flexDirection: "row" },
|
|
78
|
+
"row-reverse":{ flexDirection: "row-reverse" },
|
|
79
|
+
col: { flexDirection: "column" },
|
|
80
|
+
"col-reverse":{ flexDirection: "column-reverse" },
|
|
81
|
+
|
|
82
|
+
// Flex Wrap
|
|
83
|
+
wrap: { flexWrap: "wrap" },
|
|
84
|
+
nowrap: { flexWrap: "nowrap" },
|
|
85
|
+
|
|
86
|
+
// Justify Content
|
|
87
|
+
"justify-start": { justifyContent: "flex-start" },
|
|
88
|
+
"justify-end": { justifyContent: "flex-end" },
|
|
89
|
+
"justify-center": { justifyContent: "center" },
|
|
90
|
+
"justify-between": { justifyContent: "space-between" },
|
|
91
|
+
"justify-around": { justifyContent: "space-around" },
|
|
92
|
+
"justify-evenly": { justifyContent: "space-evenly" },
|
|
93
|
+
|
|
94
|
+
// Align Items
|
|
95
|
+
"items-start": { alignItems: "flex-start" },
|
|
96
|
+
"items-end": { alignItems: "flex-end" },
|
|
97
|
+
"items-center": { alignItems: "center" },
|
|
98
|
+
"items-baseline": { alignItems: "baseline" },
|
|
99
|
+
"items-stretch": { alignItems: "stretch" },
|
|
100
|
+
|
|
101
|
+
// Align Self
|
|
102
|
+
"self-start": { alignSelf: "flex-start" },
|
|
103
|
+
"self-end": { alignSelf: "flex-end" },
|
|
104
|
+
"self-center": { alignSelf: "center" },
|
|
105
|
+
"self-stretch": { alignSelf: "stretch" },
|
|
106
|
+
|
|
107
|
+
// Position
|
|
108
|
+
relative: { position: "relative" },
|
|
109
|
+
absolute: { position: "absolute" },
|
|
110
|
+
fixed: { position: "fixed" },
|
|
111
|
+
sticky: { position: "sticky" },
|
|
112
|
+
|
|
113
|
+
// Overflow
|
|
114
|
+
"overflow-hidden": { overflow: "hidden" },
|
|
115
|
+
"overflow-auto": { overflow: "auto" },
|
|
116
|
+
"overflow-scroll": { overflow: "scroll" },
|
|
117
|
+
"overflow-visible":{ overflow: "visible" },
|
|
118
|
+
|
|
119
|
+
// Text Alignment
|
|
120
|
+
"text-left": { textAlign: "left" },
|
|
121
|
+
"text-center": { textAlign: "center" },
|
|
122
|
+
"text-right": { textAlign: "right" },
|
|
123
|
+
"text-justify": { textAlign: "justify" },
|
|
124
|
+
|
|
125
|
+
// Font Weight
|
|
126
|
+
"fw-thin": { fontWeight: "100" },
|
|
127
|
+
"fw-light": { fontWeight: "300" },
|
|
128
|
+
"fw-normal": { fontWeight: "400" },
|
|
129
|
+
"fw-medium": { fontWeight: "500" },
|
|
130
|
+
"fw-semibold": { fontWeight: "600" },
|
|
131
|
+
"fw-bold": { fontWeight: "700" },
|
|
132
|
+
"fw-extrabold": { fontWeight: "800" },
|
|
133
|
+
"fw-black": { fontWeight: "900" },
|
|
134
|
+
|
|
135
|
+
// Font Style
|
|
136
|
+
italic: { fontStyle: "italic" },
|
|
137
|
+
"not-italic":{ fontStyle: "normal" },
|
|
138
|
+
|
|
139
|
+
// Text Decoration
|
|
140
|
+
underline: { textDecoration: "underline" },
|
|
141
|
+
"line-through":{ textDecoration: "line-through" },
|
|
142
|
+
"no-underline":{ textDecoration: "none" },
|
|
143
|
+
|
|
144
|
+
// Text Transform
|
|
145
|
+
uppercase: { textTransform: "uppercase" },
|
|
146
|
+
lowercase: { textTransform: "lowercase" },
|
|
147
|
+
capitalize: { textTransform: "capitalize" },
|
|
148
|
+
|
|
149
|
+
// Border
|
|
150
|
+
border: { border: "1px solid currentColor" },
|
|
151
|
+
"border-2": { border: "2px solid currentColor" },
|
|
152
|
+
"border-4": { border: "4px solid currentColor" },
|
|
153
|
+
"border-0": { border: "none" },
|
|
154
|
+
"border-t": { borderTop: "1px solid currentColor" },
|
|
155
|
+
"border-b": { borderBottom: "1px solid currentColor" },
|
|
156
|
+
"border-l": { borderLeft: "1px solid currentColor" },
|
|
157
|
+
"border-r": { borderRight: "1px solid currentColor" },
|
|
158
|
+
|
|
159
|
+
// Cursor
|
|
160
|
+
"cursor-pointer": { cursor: "pointer" },
|
|
161
|
+
"cursor-default": { cursor: "default" },
|
|
162
|
+
"cursor-not-allowed": { cursor: "not-allowed" },
|
|
163
|
+
|
|
164
|
+
// Misc
|
|
165
|
+
"w-full": { width: "100%" },
|
|
166
|
+
"h-full": { height: "100%" },
|
|
167
|
+
"w-screen": { width: "100vw" },
|
|
168
|
+
"h-screen": { height: "100vh" },
|
|
169
|
+
"m-auto": { margin: "auto" },
|
|
170
|
+
"mx-auto": { marginLeft: "auto", marginRight: "auto" },
|
|
171
|
+
"box-border":{ boxSizing: "border-box" },
|
|
172
|
+
"box-content":{ boxSizing: "content-box" },
|
|
173
|
+
"select-none":{ userSelect: "none" },
|
|
174
|
+
"pointer-events-none": { pointerEvents: "none" },
|
|
175
|
+
"truncate": { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
176
|
+
"sr-only": { position: "absolute", width: "1px", height: "1px", padding: "0", margin: "-1px", overflow: "hidden", clip: "rect(0,0,0,0)", whiteSpace: "nowrap", borderWidth: "0" },
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// ------------------------------------------------------------------
|
|
180
|
+
// 3. Color utilities (dn-bg-{color} dn-text-{color} dn-border-{color})
|
|
181
|
+
// ------------------------------------------------------------------
|
|
182
|
+
export function resolveColor(name) {
|
|
183
|
+
return COLOR_PALETTE[name] || name; // fallback: use raw value (e.g. hex)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function buildColorRule(prefix, colorName) {
|
|
187
|
+
const value = resolveColor(colorName);
|
|
188
|
+
if (!value) return null;
|
|
189
|
+
|
|
190
|
+
switch (prefix) {
|
|
191
|
+
case "bg": return { backgroundColor: value };
|
|
192
|
+
case "text": return { color: value };
|
|
193
|
+
case "border": return { borderColor: value };
|
|
194
|
+
case "outline": return { outlineColor: value };
|
|
195
|
+
case "shadow": return { boxShadow: `0 2px 8px ${value}` };
|
|
196
|
+
case "decoration": return { textDecorationColor: value };
|
|
197
|
+
default: return null;
|
|
198
|
+
}
|
|
199
|
+
}
|