hybrids
Version:
A JavaScript framework for creating fully-featured web applications, components libraries, and single web components with unique declarative and functional architecture
321 lines (276 loc) • 8.43 kB
JavaScript
const NUMBER_REGEXP = /^\d+$/;
const rules = {
// base
block: (props, align) => ({
display: "block",
"text-align": align,
}),
inline: ({ display }) => ({
display: `inline${display ? `-${display}` : ""}`,
}),
contents: { display: "contents" },
hidden: { display: "none" },
// flexbox
...["row", "row-reverse", "column", "column-reverse"].reduce((acc, type) => {
acc[type] = (props, wrap = "nowrap") => ({
display: "flex",
"flex-flow": `${type} ${wrap}`,
});
return acc;
}, {}),
grow: (props, value = 1) => ({ "flex-grow": value }),
shrink: (props, value = 1) => ({ "flex-shrink": value }),
basis: (props, value) => ({ "flex-basis": dimension(value) }),
order: (props, value = 0) => ({ order: value }),
// grid
grid: (props, columns = "1", rows = "", autoFlow = "", dense = "") => ({
display: "grid",
...["columns", "rows"].reduce((acc, type) => {
const value = type === "columns" ? columns : rows;
acc[`grid-template-${type}`] =
value &&
value
.split("|")
.map((v) =>
v.match(NUMBER_REGEXP)
? `repeat(${v}, minmax(0, 1fr))`
: dimension(v),
)
.join(" ");
return acc;
}, {}),
"grid-auto-flow": `${autoFlow} ${dense && "dense"}`,
}),
area: (props, column = "", row = "") => ({
"grid-column": column.match(NUMBER_REGEXP) ? `span ${column}` : column,
"grid-row": row.match(NUMBER_REGEXP) ? `span ${row}` : row,
}),
gap: (props, column = 1, row = "") => ({
"column-gap": dimension(column),
"row-gap": dimension(row || column),
}),
// alignment
items: (props, v1 = "start", v2 = "") => ({
"place-items": `${v1} ${v2}`,
}),
content: (props, v1 = "start", v2 = "") => ({
"place-content": `${v1} ${v2}`,
}),
self: (props, v1 = "start", v2 = "") => ({
"place-self": `${v1} ${v2}`,
}),
center: { "place-items": "center", "place-content": "center" },
// size
size: (props, width, height = width) => ({
width: dimension(width),
height: dimension(height),
"box-sizing": "border-box",
}),
width: (props, base, min, max) => ({
width: dimension(base),
"min-width": dimension(min),
"max-width": dimension(max),
"box-sizing": "border-box",
}),
height: (props, base, min, max) => ({
height: dimension(base),
"min-height": dimension(min),
"max-height": dimension(max),
"box-sizing": "border-box",
}),
ratio: (props, v1) => ({ "aspect-ratio": v1 }),
overflow: (props, v1 = "hidden", v2 = "") => {
const type = v2 ? `-${v1}` : "";
const value = v2 ? v2 : v1;
return {
[`overflow${type}`]: value,
...(value === "scroll"
? {
"flex-grow": props["flex-grow"] || 1,
"flex-basis": 0,
"overscroll-behavior": "contain",
"--webkit-overflow-scrolling": "touch",
}
: {}),
};
},
margin: (props, v1 = "1", v2, v3, v4) => {
if (v1.match(/top|bottom|left|right/)) {
return {
[`margin-${v1}`]: dimension(v2 || "1"),
};
}
return {
margin: `${dimension(v1)} ${dimension(v2)} ${dimension(v3)} ${dimension(
v4,
)}`,
};
},
padding: (props, v1 = "1", v2, v3, v4) => {
if (v1.match(/top|bottom|left|right/)) {
return {
[`padding-${v1}`]: dimension(v2 || "1"),
};
}
return {
padding: `${dimension(v1)} ${dimension(v2)} ${dimension(v3)} ${dimension(
v4,
)}`,
};
},
// position types
absolute: { position: "absolute" },
relative: { position: "relative" },
fixed: { position: "fixed" },
sticky: { position: "sticky" },
static: { position: "static" },
// position values
inset: (props, value = 0) => {
const d = dimension(value);
return { top: d, right: d, bottom: d, left: d };
},
top: (props, value = 0) => ({ top: dimension(value) }),
bottom: (props, value = 0) => ({ bottom: dimension(value) }),
left: (props, value = 0) => ({ left: dimension(value) }),
right: (props, value = 0) => ({ right: dimension(value) }),
layer: (props, value = 1) => ({ "z-index": value }),
"": (props, _, ...args) => {
if (args.length < 2) {
throw new Error(
"Generic rule '::' requires at least two arguments, eg.: ::[property]:[name]",
);
}
return {
[args[args.length - 2]]: `var(--${args.join("-")})`,
};
},
view: (props, value) => ({ "view-transition-name": value }),
};
const dimensions = {
min: "min-content",
max: "max-content",
fit: "fit-content",
full: "100%",
};
const queries = {
print: "print",
portrait: "(orientation: portrait)",
landscape: "(orientation: landscape)",
hover: "(hover: hover)",
"any-hover": "(any-hover: hover)",
};
function dimension(value) {
value = dimensions[value] || value;
if (/^-?\d+(\.\d+)*$/.test(String(value))) {
return `${value * 8}px`;
}
return value || "";
}
const hasAdoptedStylesheets = !!(
globalThis.document && globalThis.document.adoptedStyleSheets
);
let globalSheet;
function getCSSStyleSheet() {
if (globalSheet) return globalSheet;
/* istanbul ignore else */
if (hasAdoptedStylesheets) {
globalSheet = new globalThis.CSSStyleSheet();
} else {
const el = globalThis.document.createElement("style");
el.appendChild(globalThis.document.createTextNode(""));
globalThis.document.head.appendChild(el);
globalSheet = el.sheet;
}
globalSheet.insertRule(":host([hidden]) { display: none; }");
return globalSheet;
}
const styleElements = new WeakMap();
let injectedTargets = new WeakSet();
export function inject(target) {
const root = target.getRootNode();
if (injectedTargets.has(root)) return;
const sheet = getCSSStyleSheet();
/* istanbul ignore else */
if (hasAdoptedStylesheets && root.adoptedStyleSheets) {
root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
} else {
if (root === globalThis.document) return;
let el = styleElements.get(root);
if (!el) {
el = globalThis.document.createElement("style");
root.appendChild(el);
styleElements.set(root, el);
}
let result = "";
for (let i = 0; i < sheet.cssRules.length; i++) {
result += sheet.cssRules[i].cssText;
}
el.textContent = result;
}
injectedTargets.add(root);
}
const classNames = new Map();
export function insertRule(node, query, tokens, hostMode) {
let className = classNames.get(node);
if (!className) {
className = `l-${Math.random().toString(36).substr(2, 5)}`;
classNames.set(node, className);
}
/* istanbul ignore next */
if (!hasAdoptedStylesheets) injectedTargets = new WeakSet();
const sheet = getCSSStyleSheet();
const [selectors, mediaQueries = ""] = query.split("@");
const cssRules = Object.entries(
tokens
.replace(/\s+/g, " ")
.trim()
.split(" ")
.reduce((acc, token) => {
const [id, ...args] = token.split(":");
const rule = rules[id];
if (!rule) {
throw TypeError(`Unsupported layout rule: '${id}'`);
}
return Object.assign(
acc,
typeof rule === "function"
? rule(acc, ...args.map((v) => (v.match(/--.*/) ? `var(${v})` : v)))
: rule,
);
}, {}),
).reduce(
(acc, [key, value]) =>
value !== undefined && value !== "" ? acc + `${key}: ${value};` : acc,
"",
);
const mediaSelector = mediaQueries
.split(":")
.map((query) =>
query
.split("|")
.map((q) => q && (queries[q] || `(min-width: ${q})`))
.join(" and "),
)
.join(", ");
if (hostMode) {
const shadowSelector = `:host(:where(.${className}-s${selectors}))`;
const contentSelector = `:where(.${className}-c${selectors})`;
[shadowSelector, contentSelector].forEach((selector) => {
sheet.insertRule(
mediaSelector
? `@media ${mediaSelector} { ${selector} { ${cssRules} } }`
: `${selector} { ${cssRules} }`,
sheet.cssRules.length - 1,
);
});
} else {
const selector = `.${className}${selectors}`;
sheet.insertRule(
mediaSelector
? `@media ${mediaSelector} { ${selector} { ${cssRules} } }`
: `${selector} { ${cssRules} }`,
sheet.cssRules.length - 1,
);
}
return className;
}