Comparing version 0.1.2 to 0.1.3
85
index.js
import getConfig from "./lib/config.js"; | ||
import getParser from "./lib/parser.js"; | ||
import getParser, { hasVariant } from "./lib/parser.js"; | ||
import keyframes from "./lib/keyframes.js"; | ||
import { preflight, variables } from "./lib/styles.js"; | ||
import { preflight as preflightStyles, variables } from "./lib/styles.js"; | ||
import { theme } from "./lib/selector.js"; | ||
const onObserve = (process, filterClasses, appendStyle, appendStyleMedia) => mutations => { | ||
const onObserve = (process, filterClasses, append) => mutations => { | ||
let styles = [...new Set( | ||
@@ -26,3 +26,3 @@ mutations | ||
.map(i => { | ||
let classes = []; | ||
let c = []; | ||
const node = i.addedNodes[0] | ||
@@ -32,5 +32,5 @@ const all = node.getElementsByTagName('*'); | ||
for (var i = -1, l = all.length; ++i < l;) { | ||
classes.push((all[i].classList.value ||'').split(' ')); | ||
c.push((all[i].classList.value ||'').split(' ')); | ||
} | ||
return [...classes.flat(), ...(node.classList.value || '').split(' ')]; | ||
return [...c.flat(), ...(node.classList.value || '').split(' ')]; | ||
}) | ||
@@ -45,4 +45,3 @@ .flat() | ||
appendStyle(styles.filter(s => !s.includes('@media')).join('\n')); | ||
appendStyleMedia(styles.filter(s => s.includes('@media')).join('\n')); | ||
append(styles); | ||
} | ||
@@ -102,11 +101,25 @@ | ||
} | ||
document.head.appendChild(el); | ||
} | ||
export function init(userConfig, container = document.querySelector('body')) { | ||
export function init({ | ||
container = document.querySelector('body'), | ||
classes: userClasses = new Set(), | ||
config: userConfig = {}, | ||
preflight = true, | ||
} = {}) { | ||
if (window && window.$$headlong) return window.$$headlong; | ||
if (!(userClasses instanceof Set)) { | ||
throw new Error('Classes must be instance of Set'); | ||
} | ||
const classes = new Set(userClasses); | ||
const s = document.createElement('style'); | ||
s.setAttribute('type', 'text/css'); | ||
document.head.appendChild(s); | ||
const med = document.createElement('style'); | ||
med.setAttribute('type', 'text/css'); | ||
document.head.appendChild(med); | ||
@@ -121,5 +134,9 @@ function appendStyleMedia(css) { | ||
const classes = new Set(); | ||
const filterClasses = i => Boolean(i) && typeof i === "string" && !i.startsWith('svelte-'); | ||
function append(styles) { | ||
appendStyle(styles.filter(s => !s.includes('@media')).join('\n')); | ||
appendStyleMedia(styles.filter(s => s.includes('@media')).join('\n')); | ||
} | ||
const filterClasses = i => Boolean(i) && !classes.has(i) && typeof i === "string"; | ||
const configMerged = mergeUserConfig(getConfig(), userConfig); | ||
@@ -137,15 +154,49 @@ const parse = getParser(configMerged); | ||
const initialStyles = getInitialClasses(process, filterClasses); | ||
appendStyle(preflight + variables(configMerged) + initialStyles.filter(s => !s.includes('@media')).join('\n') + keyframes); | ||
appendStyleMedia(initialStyles.filter(s => s.includes('@media')).join('\n')); | ||
appendStyle( | ||
(preflight ? preflightStyles : "") | ||
+ variables(configMerged) | ||
+ keyframes | ||
); | ||
append(initialStyles); | ||
const classObserver = new MutationObserver(onObserve(process, filterClasses, appendStyle, appendStyleMedia)); | ||
const classObserver = new MutationObserver(onObserve(process, filterClasses, append)); | ||
observeClasses(classObserver, container); | ||
return { | ||
unsubscribe: () => classObserver.disconnect(), | ||
function output() { // TODO: what output options do we need? | ||
return { | ||
classes: new Set(classes), | ||
styles: `<style>${s.innerText}${med.innerText}</style>`.replace(/\n/, '') | ||
}; | ||
} | ||
window.$$headlong = { | ||
unsubscribe: () => { | ||
console.log('Headlong generated styles', output()); | ||
classObserver.disconnect(); | ||
window.$$headlong = null; | ||
return output(); | ||
}, | ||
parse, | ||
config: configMerged, | ||
output, | ||
apply: (selector, classList) => { | ||
const arr = classList.split(' '); | ||
const noVariantStyles = arr.filter(s => !hasVariant(s)).map(c => parse(c, true)).join(''); | ||
appendStyle(`${selector} { ${noVariantStyles} }`); | ||
// Facing the same problem with @apply variant:class like Tailwind 1.x. | ||
// Got to group all uninque variant groups, capture their styles separately, | ||
// then apply variant groups on selector argument (which can be tricky if we allow arbitrary selector) | ||
// so keeping it simple for now. | ||
// const variantStyles = arr.filter(hasVariant).map(c => parse(c).replace(c.split(":").pop(), selector)); | ||
// append(variantStyles); | ||
return `${selector} { ${noVariantStyles} }`; | ||
} | ||
}; | ||
return window.$$headlong; | ||
} | ||
export default init; |
44
index.md
@@ -9,3 +9,3 @@ # Headlong | ||
This library is not intended to replace the original Tailwind. Yet, there are environments where one cannot use PostCSS or maybe needs to interpolate _the living hell_ out of those class names, or play with configuration. | ||
This library is not intended to replace the original Tailwind. Yet, there are environments where one cannot use PostCSS or maybe needs to interpolate class names a lot, or play with configuration. | ||
@@ -22,3 +22,3 @@ Natural advantage of this approach is zero extra build time, _all_ classes are available by default, no need to enable responsive or whatever plugin. | ||
<div class="text-xs block my-8 font-mono p-2 bg-gray-100 dark:bg-gray-800 items-center shadow-lg"> | ||
<div class="text-xs block my-8 font-mono p-2 bg-gray-100 dark:bg-gray-800 justify-center shadow-lg"> | ||
@@ -46,5 +46,13 @@ { parsed = headlong.parse(className) } | ||
config, | ||
apply, // not quite there yet | ||
} = headlong(config, containerEl); | ||
// returns { styles, classes } of styles string and set of classes | ||
output, | ||
apply, | ||
} = headlong( | ||
config, | ||
{ | ||
container, // container element | ||
classes // list of classes to ignore | ||
}); | ||
// ... | ||
@@ -65,4 +73,4 @@ | ||
- [x] Min/max breakpoints, object, array notation breakpoints | ||
- [ ] `@apply` as a function | ||
- [ ] Combined selectors like ("sm:dark:hover:") | ||
- [x] `@apply` as a function (working apart from combined selectors) | ||
- [x] Combined selectors like ("sm:dark:hover:") | ||
- [ ] Negated values using css `calc` function relying on PostCSS plugin | ||
@@ -74,3 +82,3 @@ - [ ] Keyframes customization | ||
Please refer to Tailwind [documentation](https://tailwindcss.com/docs) for all available classes. Almost all of them work in headlong. | ||
Please refer to Tailwind [documentation](https://tailwindcss.com/docs) for all available classes. | ||
@@ -96,3 +104,3 @@ ### Placeholder color and opacity | ||
```html | ||
<div class="flex space-x-8 align-center items-center text-lg"> | ||
<div class="flex space-x-8 align-center justify-center text-lg"> | ||
<div class="w-1/5 h-16 py-4 text-xs bg-purple-300 text-cyan-200 text-center"> | ||
@@ -110,6 +118,6 @@ 1 | ||
<div class="flex space-x-8 align-center items-center text-lg"> | ||
<div class="w-1/5 h-8 py-4 text-xs bg-purple-300 text-cyan-200 text-center">1</div> | ||
<div class="w-1/5 h-8 py-4 text-xs bg-purple-300 text-cyan-200 text-center">2</div> | ||
<div class="w-1/5 h-8 py-4 text-xs bg-purple-300 text-cyan-200 text-center">3</div> | ||
<div class="flex space-x-8 align-center justify-center text-lg font-bold font-mono"> | ||
<div class="w-1/5 py-4 bg-purple-300 text-cyan-200 text-center">1</div> | ||
<div class="w-1/5 py-4 bg-purple-300 text-cyan-200 text-center">2</div> | ||
<div class="w-1/5 py-4 bg-purple-300 text-cyan-200 text-center">3</div> | ||
</div> | ||
@@ -165,3 +173,3 @@ | ||
<div | ||
class="rounded-t-xl overflow-hidden bg-gradient-to-r from-blue-50 to-light-blue-100 grid grid-cols-1 sm:grid-cols-4 gap-6 justify-items-center p-8" | ||
class="rounded-t-xl overflow-hidden bg-gradient-to-r from-blue-50 to-light-blue-100 grid grid-cols-1 sm:grid-cols-4 gap-6 justify-justify-center p-8" | ||
> | ||
@@ -181,3 +189,3 @@ <div | ||
<div class="rounded-t-xl overflow-hidden bg-gradient-to-r from-blue-50 to-light-blue-100 grid grid-cols-1 sm:grid-cols-4 gap-6 justify-items-center p-8"> | ||
<div class="rounded-t-xl overflow-hidden bg-gradient-to-r from-blue-50 to-light-blue-100 grid grid-cols-1 sm:grid-cols-4 gap-6 justify-justify-center p-8"> | ||
<div class="focus:outline-none text-sm w-24 py-3 rounded-md font-semibold text-white bg-blue-500 ring ring-blue-200 text-center hover:shadow"> | ||
@@ -193,3 +201,6 @@ ring | ||
{ headlong = init({}, document.getElementById('md')) } | ||
{ headlong = init({}, { | ||
container: document.getElementById('md'), | ||
preflight: false, | ||
}) } | ||
@@ -202,2 +213,5 @@ </div> | ||
} | ||
html { | ||
line-height: 1.5; | ||
} | ||
</style> |
@@ -14,2 +14,3 @@ import options from "./options.js"; | ||
disabled, | ||
checked, | ||
} from "./variants"; | ||
@@ -25,2 +26,3 @@ | ||
// TODO: user-defined variants | ||
const getVariants = config => ({ | ||
@@ -35,2 +37,3 @@ ...Object.fromEntries( | ||
disabled, | ||
checked, | ||
"group-hover": groupHover, | ||
@@ -40,18 +43,3 @@ "group-focus": groupFocus, | ||
// dark:sm:bg-red-500 should produce | ||
// @media screen(max-size: 640px) { | ||
// .mode-dark .dark\:sm\:bg-red-500 { | ||
// background-color: red; | ||
// } | ||
//} | ||
// dark:sm:hover:bg-red-500 should produce | ||
// @media screen(max-size: 640px) { | ||
// .mode-dark .dark\:sm\:hover\:bg-red-500:hover { | ||
// background-color: red; | ||
// } | ||
//} | ||
// so there is order | ||
function parseVariant(className, css, variants) { | ||
function parseVariant(className, css, variants, sanitize) { | ||
const vars = className | ||
@@ -62,25 +50,12 @@ .split(':') | ||
return vars.reduce((acc, cur) => variants[cur](sanitize(className), acc, cur), css); // broken | ||
} | ||
const selector = vars.reduce((acc, cur) => variants[cur](acc), sanitize('.' + className)); // TODO: mind the "." | ||
// escape | ||
const e = c => c.replace(':', '\\:'); | ||
if (selector.includes('##style##')) return selector.replace('##style##', css); | ||
// TODO: refactor out | ||
function sanitize(name) { | ||
name = name.replace('/', '\\/'); | ||
if (name.includes("placeholder-")) { | ||
name = e(name) + "::placeholder"; | ||
} else if (name.includes('space-') || /divide-(?!opacity)/.test(name)) { | ||
name = e(name) + " > * + *"; | ||
} else { | ||
name = e(name); | ||
} | ||
return name; | ||
return `${selector} { ${css} }`; | ||
} | ||
const hasVariant = s => /([^:]+:)+[^:]+(::)?[^:]+/.test(s); | ||
export const hasVariant = s => /([^:]+:)+[^:]+(::)?[^:]+/.test(s); | ||
// TODO: refactor for more flexible output | ||
export default function getParse(config) { | ||
@@ -90,10 +65,30 @@ const fns = getFns(config); | ||
return className => { | ||
function escape(s) { | ||
return s.replace(new RegExp(`(${Object.keys(variants).join('|')}):`, 'g'), '$1\\:'); | ||
} | ||
function sanitize(name) { | ||
name = name.replace('/', '\\/'); | ||
if (name.includes("placeholder-")) { | ||
name = escape(name) + "::placeholder"; | ||
} else if (name.includes('space-') || /divide-(?!opacity)/.test(name)) { | ||
name = escape(name) + " > * + *"; | ||
} else { | ||
name = escape(name); | ||
} | ||
return name; | ||
} | ||
return (className, onlyStyles = false) => { | ||
const classNameNoVariant = className.split(":").pop(); | ||
const css = options[classNameNoVariant] || fns.reduce((acc, cur) => acc || cur(classNameNoVariant), ""); | ||
let css = options[classNameNoVariant] || fns.reduce((acc, cur) => acc || cur(classNameNoVariant), ""); | ||
if (!css) return false; | ||
if (!css.wrap && !css.endsWith(";")) css += ";"; | ||
if (hasVariant(className)) { | ||
return parseVariant(className, css, variants); | ||
return parseVariant(className, css, variants, sanitize); | ||
} | ||
@@ -103,4 +98,6 @@ | ||
if (onlyStyles) return css; | ||
return `.${sanitize(className)} { ${css} }`; | ||
} | ||
} |
@@ -129,8 +129,11 @@ import { simpleMatch } from "./selector.js"; | ||
const contStyles = Object.keys(screens).reduce( | ||
(acc, size) => responsive(screens[size])("container", `max-width: ${screens[size]}; ${padded(size)}`), | ||
"", | ||
) + `.container { max-width: 100%; ${padded("DEFAULT")} }`; | ||
const contStyles = `.container { max-width: 100%; ${padded("DEFAULT")} }` | ||
+ Object.keys(screens).reduce( | ||
(acc, size) => acc + responsive(screens[size])(".container").replace('##style##', `max-width: ${screens[size]}; ${padded(size)}`), | ||
"", | ||
); | ||
console.log({ contStyles }); | ||
return name => name === 'container' ? { wrap: () => contStyles } : false; | ||
} |
@@ -21,18 +21,23 @@ // TODO: print and other media | ||
} | ||
export const responsive = rule => (className, style) => ` @media ${responsiveRuleString(rule)} { | ||
.${className} { ${style} } | ||
const stylePlaceholder = `{ ##style## }`; | ||
export const responsive = rule => (selector, css) => ` @media ${responsiveRuleString(rule)} { | ||
${selector} ${css ? `{ ${css} }` : stylePlaceholder} | ||
}`; | ||
export const dark = (className, style) => `.mode-dark .${className} { ${style} }`; | ||
// TODO: append dot to className before passing here | ||
export const dark = (selector) => `.mode-dark ${selector}`; | ||
export const hover = (className, style) => `.${className}:hover { ${style} }`; | ||
export const hover = (selector) => `${selector}:hover`; | ||
export const focus = (className, style) => `.${className}:focus { ${style} }`; | ||
export const focus = (selector) => `${selector}:focus`; | ||
export const active = (className, style) => `.${className}:active { ${style} }`; | ||
export const active = (selector) => `${selector}:active`; | ||
export const groupHover = (className, style) => `.group:hover .${className} { ${style} }`; | ||
export const checked = (selector) => `${selector}:checked`; | ||
export const groupFocus = (className, style) => `.group:focus .${className} { ${style} }`; | ||
export const groupHover = (selector) => `.group:hover ${selector}`; | ||
export const disabled = (className, style) => `.${className}[disabled] { ${style} }`; | ||
export const groupFocus = (selector) => `.group:focus ${selector}`; | ||
export const disabled = (selector) => `${selector}[disabled]`; |
{ | ||
"name": "headlong", | ||
"version": "0.1.2", | ||
"version": "0.1.3", | ||
"description": "Tailwind CSS on the fly", | ||
@@ -5,0 +5,0 @@ "scripts": { |
@@ -11,3 +11,3 @@ # Headlong | ||
This library is not intended to replace the original Tailwind. Yet, there are environments where one cannot use PostCSS or maybe needs to interpolate _the living hell_ out of those class names, or play with configuration. | ||
This library is not intended to replace the original Tailwind. Yet, there are environments where one cannot use PostCSS or maybe needs to interpolate class names a lot, or play with configuration. | ||
@@ -40,2 +40,12 @@ Natural advantage of this approach is zero extra build time, _all_ classes are available by default, no need to enable responsive or whatever plugin. | ||
## Changelog | ||
2021/2/20 | ||
- Disallow multiple instances of Headlong on the same page | ||
- Add "output" method | ||
- @apply for simple classes | ||
- Combined selectors | ||
- Fix container | ||
- Add :checked variant | ||
## Roadmap | ||
@@ -50,5 +60,5 @@ | ||
- [x] Min/max breakpoints, object, array notation breakpoints | ||
- [ ] `@apply` as a function | ||
- [ ] Combined selectors like ("sm:dark:hover:") | ||
- [x] `@apply` as a function (apart from combined variants just like in Tailwind 1.x) | ||
- [x] Combined variants like ("sm:dark:hover:") | ||
- [ ] Negated values using css `calc` function relying on PostCSS plugin | ||
- [ ] Keyframes customization |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
325579
3387
62