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 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
+ ![Version](https://img.shields.io/badge/version-0.0.1-blue)
4
+ ![License](https://img.shields.io/badge/license-MIT-green)
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
+ }
@@ -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
+ }
@@ -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
+ }