@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@18.2.0";
import ReactDOM from "https://esm.sh/react-dom@18.2.0";
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

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.