@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap');
.ux-box {
margin: 0;
font-family: Outfit, sans-serif;
}
.ux-box--padding {
padding: var(--ux-box--padding, 1rem);
}
.ux-box--gap {
gap: var(--ux-box--gap, .5rem);
}
.ux-box--priority-primary {
background: var(--ux-box--primary-bg, white);
color: var(--ux-box--primary-fg, black);
border-width: var(--ux-box--primary-bdw, 1px);
border-style: solid;
border-color: var(--ux-box--primary-bdc, Gainsboro);
border-radius: var(--ux-box--primary-bdr, .25rem);
box-shadow: var(--ux-box-primary-bs, 0 4px 14px rgba(160,160,160,.6));
}
.ux-box--priority-secondary {
background: var(--ux-box--secondary-bg, white);
color: var(--ux-box--secondary-fg, black);
border-width: var(--ux-box--secondary-bdw, 1px);
border-style: solid;
border-color: var(--ux-box--secondary-bdc, WhiteSmoke);
border-radius: var(--ux-box--secondary-bdr, .25rem);
box-shadow: var(--ux-box-secondary-bs, 0 4px 14px rgba(160,160,160,.1));
}
/* Some standard media element */
img {
display: block;
max-width: 400px;
width: 100%;
aspect-ratio: 4/3;
text-indent: 100%;
overflow: hidden;
}
/* Some standard text element */
:where(h2, p):empty {
max-width: 6em;
}
[data-standby="true"]:empty,
img[src="#"] {
/* These will need to be intents triggered by mode */
--skeleton-bg: #ededed;
--skeleton-shine: white;
min-height: 1lh;
min-width: 1em;
background-color: var(--skeleton-bg);
background-image: linear-gradient(
100deg,
transparent 40%,
color-mix(in srgb, var(--skeleton-shine), transparent 50%) 50%,
transparent 60%
);
background-size: 200% 100%;
background-position-x: 120%;
}
@keyframes loading {
to { background-position-x: -20% }
}
@media (prefers-reduced-motion: no-preference) {
[data-standby="true"]:empty,
img[src="#"] {
background-position-x: 180%;
animation: 2s loading ease-in-out infinite;
}
}
import React from "https://esm.sh/react";
import ReactDOM from "https://esm.sh/react-dom";
import classnames from "https://esm.sh/classnames";
const PRIORITIES = ['primary', 'secondary'];
function updateLogical(value) {
switch(value) {
case 'left':
case 'top':
return 'start';
case 'right':
case 'bottom':
return 'end';
default:
return value;
}
}
function createBox(TagName) {
return function(props) {
const {
distribute,
gap,
inset,
logical = true,
outset,
padding,
priority,
stack,
standby,
stretch,
style: userStyle = {},
wrap,
...rest
} = props;
let innerLayout = [/* inline, block */];
let outerLayout = [/* inline, block */];
if (typeof inset === 'string') {
innerLayout = [inset, inset];
} else if (inset && typeof inset === 'object') {
innerLayout = [inset.inline, inset.block];
}
if (typeof outset === 'string') {
outerLayout = [outset, outset];
} else if (outset && typeof outset === 'object') {
outerLayout = [outset.inline, outset.block];
}
if (logical) {
innerLayout.map(updateLogical);
outerLayout.map(updateLogical);
}
if (stack) {
innerLayout.reverse();
}
const style = {
justifyContent: innerLayout.at(0),
alignItems: innerLayout.at(1),
display: stretch ? 'flex' : 'inline-flex',
flex: stretch ? 1 : 'initial',
flexDirection: stack ? 'column' : 'row',
wrap: wrap ? 'wrap' : 'nowrap',
justifySelf: outerLayout.at(0),
alignSelf: outerLayout.at(1)
}
if (distribute) {
style.justifyContent = `space-${distribute}`;
}
const className = classnames('ux-box', {
'ux-box--gap': gap,
'ux-box--padding': padding,
[`ux-box--priority-${priority}`]: PRIORITIES.includes(priority)
});
return <TagName
{ ...rest }
data-standby={ standby }
style={ Object.assign(userStyle, style) }
className={ className }/>
}
}
const cache = new Map();
const box = new Proxy(createBox, {
get: (_, tagName) => {
if (!cache.has(tagName))
cache.set(tagName, createBox(tagName));
return cache.get(tagName);
},
});
function Card(props) {
const {
description,
image = '#',
loading,
title,
...rest
} = props;
return (
<box.div
padding
gap
stack
stretch
priority={ loading ? 'secondary' : 'primary' }>
<img src={ image }/>
<box.h2 standby={ loading }>{ title }</box.h2>
<box.p standby={ loading }>{ description }</box.p>
</box.div>
);
}
function App() {
return (
<box.div gap padding distribute='between'>
<Card
title='My Card'
description='Lorem ipsum'
image='https://baconmockup.com/400/300/'/>
<Card loading/>
</box.div>
)
}
const root = ReactDOM.createRoot(document.body);
root.render(<App/>);
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.