From 2a798943082a2655a7eb3ad4318bccf5d4ee7296 Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Tue, 23 Jun 2026 12:48:12 -0400 Subject: [PATCH 01/14] Move stack split and cluster to advanced --- .storybook/preview.js | 2 +- src/stories/Utilities/{ => Advanced}/Cluster/Cluster.js | 2 +- src/stories/Utilities/{ => Advanced}/Cluster/Cluster.mdx | 2 +- .../Utilities/{ => Advanced}/Cluster/Cluster.stories.js | 2 +- src/stories/Utilities/{ => Advanced}/Split/Split.js | 2 +- src/stories/Utilities/{ => Advanced}/Split/Split.mdx | 2 +- .../Utilities/{ => Advanced}/Split/Split.stories.js | 2 +- src/stories/Utilities/{ => Advanced}/Stack/Stack.js | 2 +- src/stories/Utilities/{ => Advanced}/Stack/Stack.mdx | 2 +- .../Utilities/{ => Advanced}/Stack/Stack.stories.js | 2 +- src/stories/Utilities/Introduction.mdx | 9 ++++----- 11 files changed, 14 insertions(+), 15 deletions(-) rename src/stories/Utilities/{ => Advanced}/Cluster/Cluster.js (86%) rename src/stories/Utilities/{ => Advanced}/Cluster/Cluster.mdx (96%) rename src/stories/Utilities/{ => Advanced}/Cluster/Cluster.stories.js (96%) rename src/stories/Utilities/{ => Advanced}/Split/Split.js (86%) rename src/stories/Utilities/{ => Advanced}/Split/Split.mdx (96%) rename src/stories/Utilities/{ => Advanced}/Split/Split.stories.js (96%) rename src/stories/Utilities/{ => Advanced}/Stack/Stack.js (85%) rename src/stories/Utilities/{ => Advanced}/Stack/Stack.mdx (96%) rename src/stories/Utilities/{ => Advanced}/Stack/Stack.stories.js (96%) diff --git a/.storybook/preview.js b/.storybook/preview.js index c2b8a4b0..0b14f01a 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -23,7 +23,7 @@ const preview = { ['Base Reset', 'File Organization', 'Selective Imports', 'Tokens', 'Themes', 'Scale Overriding', 'Addons'], 'Tokens', 'Utilities', - ['Introduction'], + ['Introduction', 'Container', 'Item Width', 'Margin', 'Text Alignment', 'Visibility', 'Flex', 'Gap', 'Advanced'], 'Components', 'Recipes', ], diff --git a/src/stories/Utilities/Cluster/Cluster.js b/src/stories/Utilities/Advanced/Cluster/Cluster.js similarity index 86% rename from src/stories/Utilities/Cluster/Cluster.js rename to src/stories/Utilities/Advanced/Cluster/Cluster.js index 5a9e435a..9a5f680f 100644 --- a/src/stories/Utilities/Cluster/Cluster.js +++ b/src/stories/Utilities/Advanced/Cluster/Cluster.js @@ -1,4 +1,4 @@ -import { createChildren } from '../../helpers/utils' +import { createChildren } from '../../../helpers/utils' export const createCluster = ({ cluster = true, alignItems = '', gap = '' }) => { const wrapper = document.createElement('div') diff --git a/src/stories/Utilities/Cluster/Cluster.mdx b/src/stories/Utilities/Advanced/Cluster/Cluster.mdx similarity index 96% rename from src/stories/Utilities/Cluster/Cluster.mdx rename to src/stories/Utilities/Advanced/Cluster/Cluster.mdx index 0ea45cb6..d0eff28a 100644 --- a/src/stories/Utilities/Cluster/Cluster.mdx +++ b/src/stories/Utilities/Advanced/Cluster/Cluster.mdx @@ -1,6 +1,6 @@ import { Meta, Story, Canvas, Controls } from '@storybook/addon-docs/blocks' import * as ClusterStories from './Cluster.stories' -import { createSourceCodeLink } from '../../helpers/sourceCodeLink.js' +import { createSourceCodeLink } from '../../../helpers/sourceCodeLink.js' diff --git a/src/stories/Utilities/Cluster/Cluster.stories.js b/src/stories/Utilities/Advanced/Cluster/Cluster.stories.js similarity index 96% rename from src/stories/Utilities/Cluster/Cluster.stories.js rename to src/stories/Utilities/Advanced/Cluster/Cluster.stories.js index dbba4f28..5e112fc9 100644 --- a/src/stories/Utilities/Cluster/Cluster.stories.js +++ b/src/stories/Utilities/Advanced/Cluster/Cluster.stories.js @@ -1,7 +1,7 @@ import { createCluster } from './Cluster.js' export default { - title: 'Utilities/Cluster', + title: 'Utilities/Advanced/Cluster', render: ({ cluster, ...args }) => { return createCluster({ cluster, ...args }) }, diff --git a/src/stories/Utilities/Split/Split.js b/src/stories/Utilities/Advanced/Split/Split.js similarity index 86% rename from src/stories/Utilities/Split/Split.js rename to src/stories/Utilities/Advanced/Split/Split.js index eedb2741..33b4a21e 100644 --- a/src/stories/Utilities/Split/Split.js +++ b/src/stories/Utilities/Advanced/Split/Split.js @@ -1,4 +1,4 @@ -import { createChildren } from '../../helpers/utils' +import { createChildren } from '../../../helpers/utils' export const createSplit = ({ split = true, alignItems = '', gap = '' }) => { const wrapper = document.createElement('div') diff --git a/src/stories/Utilities/Split/Split.mdx b/src/stories/Utilities/Advanced/Split/Split.mdx similarity index 96% rename from src/stories/Utilities/Split/Split.mdx rename to src/stories/Utilities/Advanced/Split/Split.mdx index fd29fb50..9f5554fc 100644 --- a/src/stories/Utilities/Split/Split.mdx +++ b/src/stories/Utilities/Advanced/Split/Split.mdx @@ -1,6 +1,6 @@ import { Meta, Story, Canvas, Controls } from '@storybook/addon-docs/blocks' import * as SplitStories from './Split.stories' -import { createSourceCodeLink } from '../../helpers/sourceCodeLink.js' +import { createSourceCodeLink } from '../../../helpers/sourceCodeLink.js' diff --git a/src/stories/Utilities/Split/Split.stories.js b/src/stories/Utilities/Advanced/Split/Split.stories.js similarity index 96% rename from src/stories/Utilities/Split/Split.stories.js rename to src/stories/Utilities/Advanced/Split/Split.stories.js index fb90f668..baf4ef74 100644 --- a/src/stories/Utilities/Split/Split.stories.js +++ b/src/stories/Utilities/Advanced/Split/Split.stories.js @@ -1,7 +1,7 @@ import { createSplit } from './Split.js' export default { - title: 'Utilities/Split', + title: 'Utilities/Advanced/Split', render: ({ split, ...args }) => { return createSplit({ split, ...args }) }, diff --git a/src/stories/Utilities/Stack/Stack.js b/src/stories/Utilities/Advanced/Stack/Stack.js similarity index 85% rename from src/stories/Utilities/Stack/Stack.js rename to src/stories/Utilities/Advanced/Stack/Stack.js index 68a6e2c3..4f5d3fb2 100644 --- a/src/stories/Utilities/Stack/Stack.js +++ b/src/stories/Utilities/Advanced/Stack/Stack.js @@ -1,4 +1,4 @@ -import { createChildren } from '../../helpers/utils' +import { createChildren } from '../../../helpers/utils' export const createStack = ({ stack = true, alignItems = '', gap = '' }) => { const wrapper = document.createElement('div') diff --git a/src/stories/Utilities/Stack/Stack.mdx b/src/stories/Utilities/Advanced/Stack/Stack.mdx similarity index 96% rename from src/stories/Utilities/Stack/Stack.mdx rename to src/stories/Utilities/Advanced/Stack/Stack.mdx index ca014144..278f3d39 100644 --- a/src/stories/Utilities/Stack/Stack.mdx +++ b/src/stories/Utilities/Advanced/Stack/Stack.mdx @@ -1,6 +1,6 @@ import { Meta, Story, Canvas, Controls } from '@storybook/addon-docs/blocks' import * as StackStories from './Stack.stories' -import { createSourceCodeLink } from '../../helpers/sourceCodeLink.js' +import { createSourceCodeLink } from '../../../helpers/sourceCodeLink.js' diff --git a/src/stories/Utilities/Stack/Stack.stories.js b/src/stories/Utilities/Advanced/Stack/Stack.stories.js similarity index 96% rename from src/stories/Utilities/Stack/Stack.stories.js rename to src/stories/Utilities/Advanced/Stack/Stack.stories.js index 53aaa95e..c77de9da 100644 --- a/src/stories/Utilities/Stack/Stack.stories.js +++ b/src/stories/Utilities/Advanced/Stack/Stack.stories.js @@ -1,7 +1,7 @@ import { createStack } from './Stack.js' export default { - title: 'Utilities/Stack', + title: 'Utilities/Advanced/Stack', render: ({ stack, ...args }) => { return createStack({ stack, ...args }) }, diff --git a/src/stories/Utilities/Introduction.mdx b/src/stories/Utilities/Introduction.mdx index 6141a507..a792ea20 100644 --- a/src/stories/Utilities/Introduction.mdx +++ b/src/stories/Utilities/Introduction.mdx @@ -12,9 +12,8 @@ import { createAlert } from '../Components/Alert/Alert.js' }} > -Utility classes are simple CSS classes scoped to a simple style property like flex or gap. -They can be added together to style a portion of your HTML from scratch. -This is especially useful for quickly scaffolding out page layouts. +Utility classes are CSS classes scoped to a simple style property like flex or gap or a collection of related properties. +They can be combined together to quickly scaffold out page layouts or a portion of your HTML. Optics provides limited but high value utility classes. Utilities can often and easily lead to poorly written CSS, inconsistent styling, or unresponsive layouts. However, there are cases where utilities can provide great value. @@ -69,13 +68,13 @@ This will allow for more complex layout to be managed within a named CSS class a - If your layout needs to handle breakpoints or special responsive rules, utilities are going to get in the way of making this easy. You probably want to name the concept. - If a particular collection of utilities is getting used together often, it may point to a UI pattern in your design that should be named. -### Higher Order Utilities vs Components +### Advanced Utilities vs Components While flex utilities are great for moving quickly or for simple layouts, they can easily become cumbersome to manage and tend to turn into "Flex Spaghetti". In some of these cases, reaching for named components may make sense. However, there are times when a full component is more restrictive than you need and hurts productivity. Sometimes you need just a bit more structure -to your layout, but not a component. [Stack](?path=/docs/utilities-stack--docs), [Cluster](?path=/docs/utilities-cluster--docs), and [Split](?path=/docs/utilities-split--docs) +to your layout, but not a component. [Stack](?path=/docs/utilities-advanced-stack--docs), [Cluster](?path=/docs/utilities-advanced-cluster--docs), and [Split](?path=/docs/utilities-advanced-split--docs) provide a simple way to create readable, flexible layouts without the overhead of a full component. ## A word of warning From 28dbec8015b474f614fcd28cb22e2e5dae0637b1 Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Tue, 23 Jun 2026 13:07:50 -0400 Subject: [PATCH 02/14] Fix issue with selecting code snippets --- .storybook/preview.css | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.storybook/preview.css b/.storybook/preview.css index c8ca38e6..cbaf159a 100644 --- a/.storybook/preview.css +++ b/.storybook/preview.css @@ -23,20 +23,17 @@ } } +/* Set playground / preview background */ .sbdocs-preview .docs-story { background-color: var(--op-color-background); color: var(--op-color-on-background); } +/* Fix tooltip demo issue */ .sbdocs .sb-button-fix button { block-size: 0; } -/* Allow the "Show Code" button to appear above the footer in examples. */ -.docs-story div:last-child:has(.docblock-code-toggle) { - z-index: 1; -} - /* Transition Demos */ .transition-demo { display: inline-block; @@ -161,6 +158,7 @@ font-weight: initial; letter-spacing: initial; line-height: 19px; + user-select: initial; } } From afc311e7e4a5da13e2135aadd18e5d8ac1be38da Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Wed, 24 Jun 2026 21:24:26 -0400 Subject: [PATCH 03/14] Add margin resets to ensure utilities are correct even if used on elements with margin --- src/core/utilities.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/utilities.css b/src/core/utilities.css index 4664f617..abd1bdd6 100644 --- a/src/core/utilities.css +++ b/src/core/utilities.css @@ -424,6 +424,8 @@ .op-stack { display: flex; flex-direction: column; + margin-block: 0; + margin-inline: 0; } :where(.op-stack) { @@ -434,6 +436,8 @@ .op-cluster { display: flex; flex-wrap: wrap; + margin-block: 0; + margin-inline: 0; } :where(.op-cluster) { @@ -446,6 +450,8 @@ display: flex; flex-wrap: wrap; justify-content: space-between; + margin-block: 0; + margin-inline: 0; } :where(.op-split) { From d46a975f278abce62790383151e745771059386d Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Wed, 24 Jun 2026 21:25:17 -0400 Subject: [PATCH 04/14] Use nesting to keep the where close to the definition for utilities --- src/core/utilities.css | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/core/utilities.css b/src/core/utilities.css index abd1bdd6..36c9d3a5 100644 --- a/src/core/utilities.css +++ b/src/core/utilities.css @@ -426,10 +426,11 @@ flex-direction: column; margin-block: 0; margin-inline: 0; -} -:where(.op-stack) { - gap: var(--op-space-medium); + /* zero specificity. Let's utilities override */ + :where(&) { + gap: var(--op-space-medium); + } } /* Equivalent to .flex.flex-wrap.items-center.gap-md */ @@ -438,11 +439,12 @@ flex-wrap: wrap; margin-block: 0; margin-inline: 0; -} -:where(.op-cluster) { - align-items: center; - gap: var(--op-space-medium); + /* zero specificity. Let's utilities override */ + :where(&) { + align-items: center; + gap: var(--op-space-medium); + } } /* Equivalent to .flex.flex-wrap.items-center.justify-between.gap-md */ @@ -452,9 +454,10 @@ justify-content: space-between; margin-block: 0; margin-inline: 0; -} -:where(.op-split) { - align-items: center; - gap: var(--op-space-medium); + /* zero specificity. Let's utilities override */ + :where(&) { + align-items: center; + gap: var(--op-space-medium); + } } From 7005ebe73591fd1a1218e973fa6dc626b6dac7e0 Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Tue, 23 Jun 2026 13:30:59 -0400 Subject: [PATCH 05/14] Add flank utility --- src/core/utilities.css | 34 +++++++++++ src/stories/Utilities/Advanced/Flank/Flank.js | 40 +++++++++++++ .../Utilities/Advanced/Flank/Flank.mdx | 57 +++++++++++++++++++ .../Utilities/Advanced/Flank/Flank.stories.js | 54 ++++++++++++++++++ src/stories/Utilities/Introduction.mdx | 2 +- 5 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 src/stories/Utilities/Advanced/Flank/Flank.js create mode 100644 src/stories/Utilities/Advanced/Flank/Flank.mdx create mode 100644 src/stories/Utilities/Advanced/Flank/Flank.stories.js diff --git a/src/core/utilities.css b/src/core/utilities.css index 36c9d3a5..c8684997 100644 --- a/src/core/utilities.css +++ b/src/core/utilities.css @@ -461,3 +461,37 @@ gap: var(--op-space-medium); } } + +/* + side by side layout with a small area and a large area + similar to .flex with a .flex-grow-1 within it, but with a more semantic name and wrap handling. + + |[] [ ]| + |[ ] []| with end +*/ +.op-flank { + display: flex; + margin-block: 0; + margin-inline: 0; + + /* zero specificity. Let's utilities override */ + :where(&) { + flex-wrap: wrap; + align-items: center; + gap: var(--op-space-medium); + } + + /* Small area */ + &:not(.op-flank--end) > :first-child, + &.op-flank--end > :last-child { + flex-basis: auto; + flex-grow: 1; + } + + /* Large area */ + &:not(.op-flank--end) > :last-child, + &.op-flank--end > :first-child { + flex-basis: 0; + flex-grow: 999; + } +} diff --git a/src/stories/Utilities/Advanced/Flank/Flank.js b/src/stories/Utilities/Advanced/Flank/Flank.js new file mode 100644 index 00000000..27e5faab --- /dev/null +++ b/src/stories/Utilities/Advanced/Flank/Flank.js @@ -0,0 +1,40 @@ +import { createChildren } from '../../../helpers/utils' +import { createAvatar } from '../../../Components/Avatar/Avatar' +import { createTextPair } from '../../../Components/TextPair/TextPair' + +export const createFlank = ({ flank = true, end = false, gap = '', example = false }) => { + if (example) { + return createAvatarExample({ flank, end, gap }) + } + + const wrapper = document.createElement('div') + + wrapper.className = classlist(flank, end, gap) + + createChildren(wrapper, 2) + + return wrapper +} + +const createAvatarExample = ({ flank, end, gap }) => { + const avatar = createAvatar({ size: 'large', useLink: false, imageSource: 'https://avatars.githubusercontent.com/u/5957102?v=4' }).outerHTML + const textPairEl = createTextPair({ titleText: 'Jeremy Walton', subtitleText: 'RoleModel Software' }) + if (end) textPairEl.classList.add('text-right') + const textPair = textPairEl.outerHTML + + // Reverse order of elements if end is true + const [first, second] = end ? [textPair, avatar] : [avatar, textPair] + + return ` +
+
+ ${first} + ${second} +
+
+` +} + +const classlist = (flank, end, gap) => { + return [flank ? 'op-flank' : '', flank && end ? 'op-flank--end' : '', gap ? `gap-${gap}` : ''].filter(Boolean).join(' ') +} diff --git a/src/stories/Utilities/Advanced/Flank/Flank.mdx b/src/stories/Utilities/Advanced/Flank/Flank.mdx new file mode 100644 index 00000000..925d82a7 --- /dev/null +++ b/src/stories/Utilities/Advanced/Flank/Flank.mdx @@ -0,0 +1,57 @@ +import { Meta, Story, Canvas, Controls } from '@storybook/addon-docs/blocks' +import * as FlankStories from './Flank.stories' +import { createSourceCodeLink } from '../../../helpers/sourceCodeLink.js' + + + +# Flank + +
+ +The flank utility provides a simple way to create a horizontal row with an item flanked by another large item. It +works with all of the gap and other flex utilities. + +See [Utility Introduction](?path=/docs/utilities-introduction--docs#higher-order-utilities-vs-components) for more information. + +See [Utility Layout](?path=/docs/recipes-layout--docs#utility) for an example of how flanks +can be used to create more readable flex layouts. + +Note: This utility uses the `op` prefix to avoid potential naming conflicts with other CSS frameworks. +This is a pattern we hope to move towards for all utilities in the future. + +## Playground + + + + +## Without + +A normal `div` without the flank utility + + + +## Flank Property + +`.op-flank` Creates a flex row with an item flanked by another large item. + + + +## Flank End + +`.op-flank.op-flank--end` Flips which side of the container the item is flanked to. + + + +## Example Flank + +`.op-flank` with an avatar and text pair to demonstrate a common use case. + + + +`.op-flank.op-flank--end` to show an alternate example. + + diff --git a/src/stories/Utilities/Advanced/Flank/Flank.stories.js b/src/stories/Utilities/Advanced/Flank/Flank.stories.js new file mode 100644 index 00000000..5f2c1804 --- /dev/null +++ b/src/stories/Utilities/Advanced/Flank/Flank.stories.js @@ -0,0 +1,54 @@ +import { createFlank } from './Flank.js' + +export default { + title: 'Utilities/Advanced/Flank', + render: ({ flank, end, ...args }) => { + return createFlank({ flank, end, ...args }) + }, + argTypes: { + flank: { control: 'boolean' }, + end: { control: 'boolean' }, + gap: { + control: { type: 'select' }, + options: ['xxs', 'xs', 'sm', 'md', 'lg', 'xl'], + }, + example: { control: 'boolean' }, + }, + parameters: { + layout: 'fullscreen', + }, +} + +export const With = { + args: { + flank: true, + }, +} + +export const Without = { + args: { + flank: false, + }, +} + +export const FlankEnd = { + args: { + flank: true, + end: true + }, +} + +export const AvatarExample = { + args: { + flank: true, + example: true + }, +} + +export const AvatarExampleEnd = { + args: { + flank: true, + example: true, + end: true + }, +} diff --git a/src/stories/Utilities/Introduction.mdx b/src/stories/Utilities/Introduction.mdx index a792ea20..c4ea7a7d 100644 --- a/src/stories/Utilities/Introduction.mdx +++ b/src/stories/Utilities/Introduction.mdx @@ -74,7 +74,7 @@ While flex utilities are great for moving quickly or for simple layouts, they ca cumbersome to manage and tend to turn into "Flex Spaghetti". In some of these cases, reaching for named components may make sense. However, there are times when a full component is more restrictive than you need and hurts productivity. Sometimes you need just a bit more structure -to your layout, but not a component. [Stack](?path=/docs/utilities-advanced-stack--docs), [Cluster](?path=/docs/utilities-advanced-cluster--docs), and [Split](?path=/docs/utilities-advanced-split--docs) +to your layout, but not a component. [Stack](?path=/docs/utilities-advanced-stack--docs), [Cluster](?path=/docs/utilities-advanced-cluster--docs), [Split](?path=/docs/utilities-advanced-split--docs), and [Flank](?path=/docs/utilities-advanced-flank--docs) provide a simple way to create readable, flexible layouts without the overhead of a full component. ## A word of warning From 14c564b4b02300ac9dc62b2bea682c7d6f9e9fcd Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Tue, 23 Jun 2026 23:24:14 -0400 Subject: [PATCH 06/14] Add grid --- src/core/utilities.css | 39 ++++++++++++++ src/stories/Utilities/Advanced/Grid/Grid.js | 18 +++++++ src/stories/Utilities/Advanced/Grid/Grid.mdx | 39 ++++++++++++++ .../Utilities/Advanced/Grid/Grid.stories.js | 52 +++++++++++++++++++ src/stories/Utilities/Introduction.mdx | 2 +- 5 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 src/stories/Utilities/Advanced/Grid/Grid.js create mode 100644 src/stories/Utilities/Advanced/Grid/Grid.mdx create mode 100644 src/stories/Utilities/Advanced/Grid/Grid.stories.js diff --git a/src/core/utilities.css b/src/core/utilities.css index c8684997..0a0aac99 100644 --- a/src/core/utilities.css +++ b/src/core/utilities.css @@ -495,3 +495,42 @@ flex-grow: 999; } } + +/* Grid Layout */ +.op-grid { + --_op-grid-min-column-size: var(--op-space-4x-large); + --_op-grid-max-column-size: 0px; + + display: grid; + grid-template-columns: repeat( + auto-fit, + minmax(max(var(--_op-grid-min-column-size), var(--_op-grid-max-column-size)), 1fr) + ); + margin-block: 0; + margin-inline: 0; + + /* zero specificity. Let's utilities override */ + :where(&) { + gap: var(--op-space-medium); + } + + &.op-grid--2-column { + --_op-grid-max-column-size: calc(100% / 3); + } + + &.op-grid--3-column { + --_op-grid-max-column-size: calc(100% / 4); + } + + &.op-grid--4-column { + --_op-grid-max-column-size: calc(100% / 5); + } + + &.op-grid--5-column { + --_op-grid-max-column-size: calc(100% / 6); + } + + &.op-grid--6-column { + --_op-grid-max-column-size: calc(100% / 7); + } +} diff --git a/src/stories/Utilities/Advanced/Grid/Grid.js b/src/stories/Utilities/Advanced/Grid/Grid.js new file mode 100644 index 00000000..00442181 --- /dev/null +++ b/src/stories/Utilities/Advanced/Grid/Grid.js @@ -0,0 +1,18 @@ +import { createChildren } from '../../../helpers/utils' + +export const createGrid = ({ grid = true, gap = '', columns = 0 }) => { + const wrapper = document.createElement('div') + wrapper.style.height = '10rem' + + wrapper.className = [ + grid ? 'op-grid' : '', + gap ? `gap-${gap}` : '', + columns > 0 ? `op-grid--${columns}-column` : '', + ] + .filter(Boolean) + .join(' ') + + createChildren(wrapper, 10) + + return wrapper +} diff --git a/src/stories/Utilities/Advanced/Grid/Grid.mdx b/src/stories/Utilities/Advanced/Grid/Grid.mdx new file mode 100644 index 00000000..8c209538 --- /dev/null +++ b/src/stories/Utilities/Advanced/Grid/Grid.mdx @@ -0,0 +1,39 @@ +import { Meta, Story, Canvas, Controls } from '@storybook/addon-docs/blocks' +import * as GridStories from './Grid.stories.js' +import { createSourceCodeLink } from '../../../helpers/sourceCodeLink.js' + + + +# Grid + +
+ +The grid utility provides a simple way to create a dynamic grid layout. It is primarily intended for simple layouts with a minimum column size, or evenly divided columns from 2 to 6. + +See [Utility Introduction](?path=/docs/utilities-introduction--docs#higher-order-utilities-vs-components) for more information. + +Note: This utility uses the `op` prefix to avoid potential naming conflicts with other CSS frameworks. +This is a pattern we hope to move towards for all utilities in the future. + +## Playground + + + + +## Without + +A normal `div` without the grid utility + + + +## Two columns + + + +## Six columns + + diff --git a/src/stories/Utilities/Advanced/Grid/Grid.stories.js b/src/stories/Utilities/Advanced/Grid/Grid.stories.js new file mode 100644 index 00000000..5d227763 --- /dev/null +++ b/src/stories/Utilities/Advanced/Grid/Grid.stories.js @@ -0,0 +1,52 @@ +import { createGrid } from './Grid.js' + +export default { + title: 'Utilities/Advanced/Grid', + render: ({ grid, ...args }) => { + return createGrid({ grid, ...args }) + }, + argTypes: { + grid: { control: 'boolean' }, + gap: { + control: { type: 'select' }, + options: ['xxs', 'xs', 'sm', 'md', 'lg', 'xl'], + }, + columns: { + control: { + type: 'number', + min: 2, + max: 6, + step: 1, + }, + }, + }, + parameters: { + layout: 'fullscreen', + }, +} + +export const With = { + args: { + grid: true, + }, +} + +export const Without = { + args: { + grid: false, + }, +} + +export const TwoColumns = { + args: { + grid: true, + columns: 2, + }, +} + +export const SixColumns = { + args: { + grid: true, + columns: 6, + }, +} diff --git a/src/stories/Utilities/Introduction.mdx b/src/stories/Utilities/Introduction.mdx index c4ea7a7d..43fc88ad 100644 --- a/src/stories/Utilities/Introduction.mdx +++ b/src/stories/Utilities/Introduction.mdx @@ -74,7 +74,7 @@ While flex utilities are great for moving quickly or for simple layouts, they ca cumbersome to manage and tend to turn into "Flex Spaghetti". In some of these cases, reaching for named components may make sense. However, there are times when a full component is more restrictive than you need and hurts productivity. Sometimes you need just a bit more structure -to your layout, but not a component. [Stack](?path=/docs/utilities-advanced-stack--docs), [Cluster](?path=/docs/utilities-advanced-cluster--docs), [Split](?path=/docs/utilities-advanced-split--docs), and [Flank](?path=/docs/utilities-advanced-flank--docs) +to your layout, but not a component. [Stack](?path=/docs/utilities-advanced-stack--docs), [Cluster](?path=/docs/utilities-advanced-cluster--docs), [Split](?path=/docs/utilities-advanced-split--docs), [Flank](?path=/docs/utilities-advanced-flank--docs), and [Grid](?path=/docs/utilities-advanced-grid--docs) provide a simple way to create readable, flexible layouts without the overhead of a full component. ## A word of warning From a8524fba4a7bcb3fb44b1e8a2d7cf041021b1495 Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Wed, 24 Jun 2026 21:45:47 -0400 Subject: [PATCH 07/14] Add gap none utility --- src/core/utilities.css | 6 ++++++ src/stories/Utilities/Advanced/Cluster/Cluster.stories.js | 2 +- src/stories/Utilities/Advanced/Flank/Flank.stories.js | 2 +- src/stories/Utilities/Advanced/Grid/Grid.stories.js | 2 +- src/stories/Utilities/Advanced/Split/Split.stories.js | 2 +- src/stories/Utilities/Advanced/Stack/Stack.stories.js | 2 +- src/stories/Utilities/Gap/Gap.mdx | 6 ++++++ src/stories/Utilities/Gap/Gap.stories.js | 8 +++++++- 8 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/core/utilities.css b/src/core/utilities.css index 0a0aac99..60d7bd2e 100644 --- a/src/core/utilities.css +++ b/src/core/utilities.css @@ -94,6 +94,12 @@ } /* Gap Properties */ +.gap-none { + gap: 0; + + --op-gap: 0; +} + .gap-xxs { gap: var(--op-space-2x-small); diff --git a/src/stories/Utilities/Advanced/Cluster/Cluster.stories.js b/src/stories/Utilities/Advanced/Cluster/Cluster.stories.js index 5e112fc9..035090e3 100644 --- a/src/stories/Utilities/Advanced/Cluster/Cluster.stories.js +++ b/src/stories/Utilities/Advanced/Cluster/Cluster.stories.js @@ -13,7 +13,7 @@ export default { }, gap: { control: { type: 'select' }, - options: ['xxs', 'xs', 'sm', 'md', 'lg', 'xl'], + options: ['none', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'], }, }, parameters: { diff --git a/src/stories/Utilities/Advanced/Flank/Flank.stories.js b/src/stories/Utilities/Advanced/Flank/Flank.stories.js index 5f2c1804..a0e3572d 100644 --- a/src/stories/Utilities/Advanced/Flank/Flank.stories.js +++ b/src/stories/Utilities/Advanced/Flank/Flank.stories.js @@ -10,7 +10,7 @@ export default { end: { control: 'boolean' }, gap: { control: { type: 'select' }, - options: ['xxs', 'xs', 'sm', 'md', 'lg', 'xl'], + options: ['none', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'], }, example: { control: 'boolean' }, }, diff --git a/src/stories/Utilities/Advanced/Grid/Grid.stories.js b/src/stories/Utilities/Advanced/Grid/Grid.stories.js index 5d227763..5fde73be 100644 --- a/src/stories/Utilities/Advanced/Grid/Grid.stories.js +++ b/src/stories/Utilities/Advanced/Grid/Grid.stories.js @@ -9,7 +9,7 @@ export default { grid: { control: 'boolean' }, gap: { control: { type: 'select' }, - options: ['xxs', 'xs', 'sm', 'md', 'lg', 'xl'], + options: ['none', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'], }, columns: { control: { diff --git a/src/stories/Utilities/Advanced/Split/Split.stories.js b/src/stories/Utilities/Advanced/Split/Split.stories.js index baf4ef74..829e8261 100644 --- a/src/stories/Utilities/Advanced/Split/Split.stories.js +++ b/src/stories/Utilities/Advanced/Split/Split.stories.js @@ -13,7 +13,7 @@ export default { }, gap: { control: { type: 'select' }, - options: ['xxs', 'xs', 'sm', 'md', 'lg', 'xl'], + options: ['none', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'], }, }, parameters: { diff --git a/src/stories/Utilities/Advanced/Stack/Stack.stories.js b/src/stories/Utilities/Advanced/Stack/Stack.stories.js index c77de9da..67e6968b 100644 --- a/src/stories/Utilities/Advanced/Stack/Stack.stories.js +++ b/src/stories/Utilities/Advanced/Stack/Stack.stories.js @@ -13,7 +13,7 @@ export default { }, gap: { control: { type: 'select' }, - options: ['xxs', 'xs', 'sm', 'md', 'lg', 'xl'], + options: ['none', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'], }, }, parameters: { diff --git a/src/stories/Utilities/Gap/Gap.mdx b/src/stories/Utilities/Gap/Gap.mdx index 6b46dbff..6692b25b 100644 --- a/src/stories/Utilities/Gap/Gap.mdx +++ b/src/stories/Utilities/Gap/Gap.mdx @@ -78,3 +78,9 @@ When using a gap utility, `--op-gap` will be set to the same spacing value as ga `.gap-xl` This will use `--op-space-xl` for the item spacing. + +## None + +`.gap-none` This useful for removing a default gap. Especially useful for the [Advanced Utilities](?path=/docs/utilities-advanced-cluster--docs) which have a default gap of `--op-space-medium`. + + diff --git a/src/stories/Utilities/Gap/Gap.stories.js b/src/stories/Utilities/Gap/Gap.stories.js index 77cac679..769d8f6c 100644 --- a/src/stories/Utilities/Gap/Gap.stories.js +++ b/src/stories/Utilities/Gap/Gap.stories.js @@ -8,11 +8,17 @@ export default { argTypes: { size: { control: { type: 'select' }, - options: ['xxs', 'xs', 'sm', 'md', 'lg', 'xl'], + options: ['none', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'], }, }, } +export const None = { + args: { + size: 'none', + }, +} + export const ExtraExtraSmall = { args: { size: 'xxs', From be526331ba99c92570ea258b89354a99145d9d0f Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Wed, 24 Jun 2026 21:49:07 -0400 Subject: [PATCH 08/14] Allow gap to override text pair --- src/components/text_pair.css | 6 +++++- src/stories/Components/TextPair/TextPair.js | 7 ++++++- src/stories/Components/TextPair/TextPair.stories.js | 4 ++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/components/text_pair.css b/src/components/text_pair.css index a0fd963e..8b953447 100644 --- a/src/components/text_pair.css +++ b/src/components/text_pair.css @@ -10,7 +10,11 @@ display: flex; flex-direction: column; - gap: var(--op-space-x-small); + + /* zero specificity. Let's utilities override */ + :where(&) { + gap: var(--op-space-x-small); + } &.text-pair--inline { flex-direction: row; diff --git a/src/stories/Components/TextPair/TextPair.js b/src/stories/Components/TextPair/TextPair.js index 5ae68f5f..13d3b395 100644 --- a/src/stories/Components/TextPair/TextPair.js +++ b/src/stories/Components/TextPair/TextPair.js @@ -4,10 +4,15 @@ export const createTextPair = ({ titleSize = '', subtitleText = 'This is the Subtitle', subtitleSize = '', + gap = '', }) => { const element = document.createElement('div') - element.className = ['text-pair', inline ? 'text-pair--inline' : ''].filter(Boolean).join(' ') + element.className = [ + 'text-pair', + inline ? 'text-pair--inline' : '', + gap ? `gap-${gap}` : '' + ].filter(Boolean).join(' ') const title = document.createElement('span') title.className = ['text-pair__title', titleSize === '' ? '' : `text-pair__title--${titleSize}`] diff --git a/src/stories/Components/TextPair/TextPair.stories.js b/src/stories/Components/TextPair/TextPair.stories.js index bc5eff0a..70b1752d 100644 --- a/src/stories/Components/TextPair/TextPair.stories.js +++ b/src/stories/Components/TextPair/TextPair.stories.js @@ -17,6 +17,10 @@ export default { control: { type: 'select' }, options: ['small', 'medium', 'large'], }, + gap: { + control: { type: 'select' }, + options: ['none', 'xxs', 'xs', 'sm', 'md', 'lg', 'xl'], + }, }, } From b3127bdeb3c368adc2597d4f4638ed9937e44ff7 Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Wed, 24 Jun 2026 23:16:38 -0400 Subject: [PATCH 09/14] Add frame utility --- src/core/utilities.css | 59 ++++++++++++ src/stories/Utilities/Advanced/Frame/Frame.js | 18 ++++ .../Utilities/Advanced/Frame/Frame.mdx | 85 +++++++++++++++++ .../Utilities/Advanced/Frame/Frame.stories.js | 91 +++++++++++++++++++ 4 files changed, 253 insertions(+) create mode 100644 src/stories/Utilities/Advanced/Frame/Frame.js create mode 100644 src/stories/Utilities/Advanced/Frame/Frame.mdx create mode 100644 src/stories/Utilities/Advanced/Frame/Frame.stories.js diff --git a/src/core/utilities.css b/src/core/utilities.css index 60d7bd2e..131fcae1 100644 --- a/src/core/utilities.css +++ b/src/core/utilities.css @@ -540,3 +540,62 @@ --_op-grid-max-column-size: calc(100% / 7); } } + +/* Frame */ +@property --_op-frame-inline-size { + inherits: true; + initial-value: 100%; + syntax: ' | '; +} + +.op-frame { + display: flex; + overflow: hidden; + border-radius: inherit; + aspect-ratio: 1 / 1; + margin-block: 0; + margin-inline: 0; + max-inline-size: var(--_op-frame-inline-size); + + :where(&) { + align-items: center; + justify-content: center; + } + + > img, + > video { + block-size: 100%; + inline-size: 100%; + object-fit: cover; + } + + /* aspect ratio */ + + &.op-frame--square { + aspect-ratio: 1 / 1; + } + + &.op-frame--landscape { + aspect-ratio: 16 / 9; + } + + &.op-frame--portrait { + aspect-ratio: 9 / 16; + } + + &.op-frame--4-3 { + aspect-ratio: 4 / 3; + } + + &.op-frame--3-4 { + aspect-ratio: 3 / 4; + } + + &.op-frame--3-2 { + aspect-ratio: 3 / 2; + } + + &.op-frame--2-3 { + aspect-ratio: 2 / 3; + } +} diff --git a/src/stories/Utilities/Advanced/Frame/Frame.js b/src/stories/Utilities/Advanced/Frame/Frame.js new file mode 100644 index 00000000..377d31ff --- /dev/null +++ b/src/stories/Utilities/Advanced/Frame/Frame.js @@ -0,0 +1,18 @@ +const IMAGE_SOURCE = 'https://images.unsplash.com/photo-1517849845537-4d257902454a?w=800&q=80' + +export const createFrame = ({ frame = true, aspect = '', inlineSize = '' }) => { + const style = inlineSize ? ` style="--_op-frame-inline-size: ${inlineSize};"` : '' + + // Sizing is for demo only, don't write inline styles + return ` +
+
+ A dog looking at the camera +
+
+` +} + +const classlist = (frame, aspect) => { + return [frame ? 'op-frame' : '', frame && aspect ? `op-frame--${aspect}` : ''].filter(Boolean).join(' ') +} diff --git a/src/stories/Utilities/Advanced/Frame/Frame.mdx b/src/stories/Utilities/Advanced/Frame/Frame.mdx new file mode 100644 index 00000000..8f987f2e --- /dev/null +++ b/src/stories/Utilities/Advanced/Frame/Frame.mdx @@ -0,0 +1,85 @@ +import { Meta, Story, Canvas, Controls } from '@storybook/addon-docs/blocks' +import * as FrameStories from './Frame.stories.js' +import { createSourceCodeLink } from '../../../helpers/sourceCodeLink.js' + + + +# Frame + +
+ +The frame utility provides a simple way to constrain content to a consistent aspect ratio. It centers its content +and clips any overflow, so whatever it wraps fills a fixed shape regardless of its intrinsic dimensions. + +While it's a natural fit for media like images and videos, the frame isn't limited to them — its primary job is +enforcing the aspect ratio, so it works just as well for cards, map embeds, charts, or any element that should hold +a fixed shape. + +See [Utility Introduction](?path=/docs/utilities-introduction--docs#higher-order-utilities-vs-components) for more information. + +Note: This utility uses the `op` prefix to avoid potential naming conflicts with other CSS frameworks. +This is a pattern we hope to move towards for all utilities in the future. + +## Playground + + + + +## Without + +A normal `div` without the frame utility + + + +## Square + +`.op-frame.op-frame--square` Crops media to a 1:1 aspect ratio. This is also the default when no aspect modifier is applied. + + + +## Landscape + +`.op-frame.op-frame--landscape` Crops media to a 16:9 aspect ratio. + + + +## Portrait + +`.op-frame.op-frame--portrait` Crops media to a 9:16 aspect ratio. + + + +## 4:3 + +`.op-frame.op-frame--4-3` Crops media to a 4:3 aspect ratio. + + + +## 3:4 + +`.op-frame.op-frame--3-4` Crops media to a 3:4 (inverse of 4:3) aspect ratio. + + + +## 3:2 + +`.op-frame.op-frame--3-2` Crops media to a 3:2 aspect ratio. + + + +## 2:3 + +`.op-frame.op-frame--2-3` Crops media to a 2:3 (inverse of 3:2) aspect ratio. + + + +## Custom Inline Size + +The `--_op-frame-inline-size` custom property controls the frame's `max-inline-size` (defaults to `100%`). +Set it inline to constrain the frame to a smaller width while preserving its aspect ratio. + + diff --git a/src/stories/Utilities/Advanced/Frame/Frame.stories.js b/src/stories/Utilities/Advanced/Frame/Frame.stories.js new file mode 100644 index 00000000..06bb97a8 --- /dev/null +++ b/src/stories/Utilities/Advanced/Frame/Frame.stories.js @@ -0,0 +1,91 @@ +import { createFrame } from './Frame.js' + +export default { + title: 'Utilities/Advanced/Frame', + render: ({ frame, ...args }) => { + return createFrame({ frame, ...args }) + }, + argTypes: { + frame: { control: 'boolean' }, + aspect: { + control: { type: 'select' }, + options: ['square', 'landscape', 'portrait', '4-3', '3-4', '3-2', '2-3'], + }, + inlineSize: { + control: { type: 'text' }, + }, + }, + parameters: { + layout: 'fullscreen', + }, +} + +export const With = { + args: { + frame: true, + }, +} + +export const Without = { + args: { + frame: false, + }, +} + +export const Square = { + args: { + frame: true, + aspect: 'square', + }, +} + +export const Landscape = { + args: { + frame: true, + aspect: 'landscape', + }, +} + +export const Portrait = { + args: { + frame: true, + aspect: 'portrait', + }, +} + +export const FourThree = { + args: { + frame: true, + aspect: '4-3', + }, +} + +export const ThreeFour = { + args: { + frame: true, + aspect: '3-4', + }, +} + +export const ThreeTwo = { + args: { + frame: true, + aspect: '3-2', + }, +} + +export const TwoThree = { + args: { + frame: true, + aspect: '2-3', + }, +} + +export const CustomInlineSize = { + args: { + frame: true, + aspect: 'square', + inlineSize: '50%', + }, +} + From 38df0ec5126caa95103bcd8272407974f17cbe7d Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Wed, 24 Jun 2026 23:24:54 -0400 Subject: [PATCH 10/14] Add grid and frame example layout --- src/stories/Recipes/Layout/Layout.js | 71 +++++++++++++++++++ src/stories/Recipes/Layout/Layout.mdx | 8 +++ src/stories/Recipes/Layout/Layout.stories.js | 8 ++- .../Utilities/Advanced/Frame/Frame.mdx | 3 + 4 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/stories/Recipes/Layout/Layout.js b/src/stories/Recipes/Layout/Layout.js index bc0c5570..7b6af540 100644 --- a/src/stories/Recipes/Layout/Layout.js +++ b/src/stories/Recipes/Layout/Layout.js @@ -286,6 +286,73 @@ const createUtilityLayout = () => { ` } +const createCardGridLayout = () => { + return ` +
+ + + + +
+
+
+
+ Clint +
+
+
+ Clint + Dog · Male +
+
+
+ +
+
+
+ + photo + + Coming soon +
+
+
+
+ Daisy + Dog · Female +
+
+
+ +
+
+ June +
+
+
+ June + Dog · Female +
+
+
+ +
+
+ Wallace +
+
+
+ Wallace + Dog · Male +
+
+
+
+
+
+` +} + export const createLayout = ({ style = 'basic', rightSidebar = false }) => { if (style === 'basic') { return createBasicLayout() @@ -315,5 +382,9 @@ export const createLayout = ({ style = 'basic', rightSidebar = false }) => { return createUtilityLayout() } + if (style === 'card-grid') { + return createCardGridLayout() + } + return `
` } diff --git a/src/stories/Recipes/Layout/Layout.mdx b/src/stories/Recipes/Layout/Layout.mdx index d1be640c..22480157 100644 --- a/src/stories/Recipes/Layout/Layout.mdx +++ b/src/stories/Recipes/Layout/Layout.mdx @@ -262,3 +262,11 @@ A layout for a login page could look like the following: A layout explaining how the stack, split, and cluster utilities can be used to make flex layouts more readable. + +### Card Grid + +A responsive card grid built with the `op-grid` and `op-frame` utilities. The `op-frame` utility keeps each card's +media at a consistent aspect ratio regardless of the source image dimensions, and it isn't limited to media — the +"Coming soon" card frames a text placeholder to hold the same shape. + + diff --git a/src/stories/Recipes/Layout/Layout.stories.js b/src/stories/Recipes/Layout/Layout.stories.js index f5c8f100..1d62c93e 100644 --- a/src/stories/Recipes/Layout/Layout.stories.js +++ b/src/stories/Recipes/Layout/Layout.stories.js @@ -8,7 +8,7 @@ export default { argTypes: { style: { control: { type: 'select' }, - options: ['basic', 'sidebar', 'navbar', 'spinner', 'sidepanel', 'login', 'utility'], + options: ['basic', 'sidebar', 'navbar', 'spinner', 'sidepanel', 'login', 'utility', 'card-grid'], }, rightSidebar: { control: { type: 'boolean' }, @@ -68,3 +68,9 @@ export const Utility = { style: 'utility', }, } + +export const CardGrid = { + args: { + style: 'card-grid', + }, +} diff --git a/src/stories/Utilities/Advanced/Frame/Frame.mdx b/src/stories/Utilities/Advanced/Frame/Frame.mdx index 8f987f2e..90f09b48 100644 --- a/src/stories/Utilities/Advanced/Frame/Frame.mdx +++ b/src/stories/Utilities/Advanced/Frame/Frame.mdx @@ -21,6 +21,9 @@ a fixed shape. See [Utility Introduction](?path=/docs/utilities-introduction--docs#higher-order-utilities-vs-components) for more information. +See [Card Grid Layout](?path=/docs/recipes-layout--docs#card-grid) for an example of how frames +can be used to keep media at a consistent aspect ratio within a card grid. + Note: This utility uses the `op` prefix to avoid potential naming conflicts with other CSS frameworks. This is a pattern we hope to move towards for all utilities in the future. From b740c6e68cbcf5a616dd7626894a26e0ea048f2e Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Thu, 25 Jun 2026 08:56:47 -0400 Subject: [PATCH 11/14] Add flank scenario to layout example --- src/stories/Recipes/Layout/Layout.js | 98 ++++++++++++++------------- src/stories/Recipes/Layout/Layout.mdx | 2 +- 2 files changed, 52 insertions(+), 48 deletions(-) diff --git a/src/stories/Recipes/Layout/Layout.js b/src/stories/Recipes/Layout/Layout.js index 7b6af540..18205b2f 100644 --- a/src/stories/Recipes/Layout/Layout.js +++ b/src/stories/Recipes/Layout/Layout.js @@ -5,7 +5,7 @@ import { createSidePanel } from '../../Components/SidePanel/SidePanel.js' const createBasicLayout = () => { return ` -
+
@@ -20,7 +20,7 @@ const createBasicLayout = () => { const createSpinnerLayout = () => { const spinner = createSpinner({ size: 'large' }).outerHTML return ` -
+
${spinner}
` @@ -40,7 +40,7 @@ const createSidebarLayout = (rightSidebar) => { } return ` -
+
@@ -50,8 +50,8 @@ const createSidebarLayout = (rightSidebar) => {
Header
${Array.from(Array(16)) - .map((_item) => '

Some content

') - .join('\n')} + .map((_item) => '

Some content

') + .join('\n')}
@@ -62,7 +62,7 @@ const createSidebarLayout = (rightSidebar) => { const createNavbarLayout = () => { return ` -
+
@@ -84,39 +84,37 @@ const createSidePanelLayout = () => { overflow: auto; } -
+
- ${ - createSidebar({ - style: 'default', - size: 'drawer', - brand: true, - position: 'start', - activeLink: 'Home', - logout: false, - trailingDiv: false, - }).innerHTML - } + ${createSidebar({ + style: 'default', + size: 'drawer', + brand: true, + position: 'start', + activeLink: 'Home', + logout: false, + trailingDiv: false, + }).innerHTML + }
${createNavbar({ style: 'default' }).innerHTML}
${Array.from(Array(16)) - .map((_item) => '

Some content

') - .join('\n')} + .map((_item) => '

Some content

') + .join('\n')}
- ${ - createSidePanel({ - border: 'both', - sections: 20, - showDividers: true, - sectionPadding: 'all', - includeDemoWrapper: false, - }).outerHTML - } + ${createSidePanel({ + border: 'both', + sections: 20, + showDividers: true, + sectionPadding: 'all', + includeDemoWrapper: false, + }).outerHTML + }
@@ -159,7 +157,7 @@ const createLoginLayout = () => { } } -
+
@@ -195,22 +193,24 @@ const createLoginLayout = () => { const createUtilityLayout = () => { return ` -
+
-
+
-

Timeline with Icons

+

Timeline with Icons

-
- nature - - Buried by - Squirrel - +
+
+ Squirrel +
+
+ Squirrel + Animal +
Mar 31
@@ -243,16 +243,20 @@ const createUtilityLayout = () => {
- +

Timeline with Icons

-
- nature - - Buried by - squirrel - +
+
+ Squirrel +
+
+
+ Squirrel + Animal +
+
Mar 31
@@ -288,7 +292,7 @@ const createUtilityLayout = () => { const createCardGridLayout = () => { return ` -
+
diff --git a/src/stories/Recipes/Layout/Layout.mdx b/src/stories/Recipes/Layout/Layout.mdx index 22480157..66604309 100644 --- a/src/stories/Recipes/Layout/Layout.mdx +++ b/src/stories/Recipes/Layout/Layout.mdx @@ -259,7 +259,7 @@ A layout for a login page could look like the following: ### Utility -A layout explaining how the stack, split, and cluster utilities can be used to make flex layouts more readable. +A layout explaining how the stack, split, cluster, and flank utilities can be used to make flex layouts more readable. From 7043652ff0f55c8bdc516e2d6ef39fc4ac9e3466 Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Thu, 25 Jun 2026 13:45:52 -0400 Subject: [PATCH 12/14] Run linters --- .storybook/preview.js | 12 ++++- src/stories/Components/TextPair/TextPair.js | 8 ++-- src/stories/Recipes/Layout/Layout.js | 46 ++++++++++--------- src/stories/Utilities/Advanced/Flank/Flank.js | 10 +++- .../Utilities/Advanced/Flank/Flank.stories.js | 6 +-- .../Utilities/Advanced/Frame/Frame.stories.js | 1 - src/stories/Utilities/Advanced/Grid/Grid.js | 6 +-- 7 files changed, 50 insertions(+), 39 deletions(-) diff --git a/.storybook/preview.js b/.storybook/preview.js index 0b14f01a..9ddcd2d2 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -23,7 +23,17 @@ const preview = { ['Base Reset', 'File Organization', 'Selective Imports', 'Tokens', 'Themes', 'Scale Overriding', 'Addons'], 'Tokens', 'Utilities', - ['Introduction', 'Container', 'Item Width', 'Margin', 'Text Alignment', 'Visibility', 'Flex', 'Gap', 'Advanced'], + [ + 'Introduction', + 'Container', + 'Item Width', + 'Margin', + 'Text Alignment', + 'Visibility', + 'Flex', + 'Gap', + 'Advanced', + ], 'Components', 'Recipes', ], diff --git a/src/stories/Components/TextPair/TextPair.js b/src/stories/Components/TextPair/TextPair.js index 13d3b395..b5709bec 100644 --- a/src/stories/Components/TextPair/TextPair.js +++ b/src/stories/Components/TextPair/TextPair.js @@ -8,11 +8,9 @@ export const createTextPair = ({ }) => { const element = document.createElement('div') - element.className = [ - 'text-pair', - inline ? 'text-pair--inline' : '', - gap ? `gap-${gap}` : '' - ].filter(Boolean).join(' ') + element.className = ['text-pair', inline ? 'text-pair--inline' : '', gap ? `gap-${gap}` : ''] + .filter(Boolean) + .join(' ') const title = document.createElement('span') title.className = ['text-pair__title', titleSize === '' ? '' : `text-pair__title--${titleSize}`] diff --git a/src/stories/Recipes/Layout/Layout.js b/src/stories/Recipes/Layout/Layout.js index 18205b2f..0322caff 100644 --- a/src/stories/Recipes/Layout/Layout.js +++ b/src/stories/Recipes/Layout/Layout.js @@ -50,8 +50,8 @@ const createSidebarLayout = (rightSidebar) => {
Header
${Array.from(Array(16)) - .map((_item) => '

Some content

') - .join('\n')} + .map((_item) => '

Some content

') + .join('\n')}
@@ -89,32 +89,34 @@ const createSidePanelLayout = () => { - ${createSidebar({ - style: 'default', - size: 'drawer', - brand: true, - position: 'start', - activeLink: 'Home', - logout: false, - trailingDiv: false, - }).innerHTML - } + ${ + createSidebar({ + style: 'default', + size: 'drawer', + brand: true, + position: 'start', + activeLink: 'Home', + logout: false, + trailingDiv: false, + }).innerHTML + }
${createNavbar({ style: 'default' }).innerHTML}
${Array.from(Array(16)) - .map((_item) => '

Some content

') - .join('\n')} + .map((_item) => '

Some content

') + .join('\n')}
- ${createSidePanel({ - border: 'both', - sections: 20, - showDividers: true, - sectionPadding: 'all', - includeDemoWrapper: false, - }).outerHTML - } + ${ + createSidePanel({ + border: 'both', + sections: 20, + showDividers: true, + sectionPadding: 'all', + includeDemoWrapper: false, + }).outerHTML + }
diff --git a/src/stories/Utilities/Advanced/Flank/Flank.js b/src/stories/Utilities/Advanced/Flank/Flank.js index 27e5faab..a4a8fb12 100644 --- a/src/stories/Utilities/Advanced/Flank/Flank.js +++ b/src/stories/Utilities/Advanced/Flank/Flank.js @@ -17,7 +17,11 @@ export const createFlank = ({ flank = true, end = false, gap = '', example = fal } const createAvatarExample = ({ flank, end, gap }) => { - const avatar = createAvatar({ size: 'large', useLink: false, imageSource: 'https://avatars.githubusercontent.com/u/5957102?v=4' }).outerHTML + const avatar = createAvatar({ + size: 'large', + useLink: false, + imageSource: 'https://avatars.githubusercontent.com/u/5957102?v=4', + }).outerHTML const textPairEl = createTextPair({ titleText: 'Jeremy Walton', subtitleText: 'RoleModel Software' }) if (end) textPairEl.classList.add('text-right') const textPair = textPairEl.outerHTML @@ -36,5 +40,7 @@ const createAvatarExample = ({ flank, end, gap }) => { } const classlist = (flank, end, gap) => { - return [flank ? 'op-flank' : '', flank && end ? 'op-flank--end' : '', gap ? `gap-${gap}` : ''].filter(Boolean).join(' ') + return [flank ? 'op-flank' : '', flank && end ? 'op-flank--end' : '', gap ? `gap-${gap}` : ''] + .filter(Boolean) + .join(' ') } diff --git a/src/stories/Utilities/Advanced/Flank/Flank.stories.js b/src/stories/Utilities/Advanced/Flank/Flank.stories.js index a0e3572d..94a44b97 100644 --- a/src/stories/Utilities/Advanced/Flank/Flank.stories.js +++ b/src/stories/Utilities/Advanced/Flank/Flank.stories.js @@ -34,14 +34,14 @@ export const Without = { export const FlankEnd = { args: { flank: true, - end: true + end: true, }, } export const AvatarExample = { args: { flank: true, - example: true + example: true, }, } @@ -49,6 +49,6 @@ export const AvatarExampleEnd = { args: { flank: true, example: true, - end: true + end: true, }, } diff --git a/src/stories/Utilities/Advanced/Frame/Frame.stories.js b/src/stories/Utilities/Advanced/Frame/Frame.stories.js index 06bb97a8..f0a39a06 100644 --- a/src/stories/Utilities/Advanced/Frame/Frame.stories.js +++ b/src/stories/Utilities/Advanced/Frame/Frame.stories.js @@ -88,4 +88,3 @@ export const CustomInlineSize = { inlineSize: '50%', }, } - diff --git a/src/stories/Utilities/Advanced/Grid/Grid.js b/src/stories/Utilities/Advanced/Grid/Grid.js index 00442181..5daf48b7 100644 --- a/src/stories/Utilities/Advanced/Grid/Grid.js +++ b/src/stories/Utilities/Advanced/Grid/Grid.js @@ -4,11 +4,7 @@ export const createGrid = ({ grid = true, gap = '', columns = 0 }) => { const wrapper = document.createElement('div') wrapper.style.height = '10rem' - wrapper.className = [ - grid ? 'op-grid' : '', - gap ? `gap-${gap}` : '', - columns > 0 ? `op-grid--${columns}-column` : '', - ] + wrapper.className = [grid ? 'op-grid' : '', gap ? `gap-${gap}` : '', columns > 0 ? `op-grid--${columns}-column` : ''] .filter(Boolean) .join(' ') From 19b0f543465bcc7d4034910145a14085c1ec3f77 Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Thu, 25 Jun 2026 13:52:19 -0400 Subject: [PATCH 13/14] Bump version in preparation for release --- .storybook/assets/example-layout.html | 2 +- .storybook/assets/login-layout.html | 2 +- .storybook/assets/spinner-layout.html | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.storybook/assets/example-layout.html b/.storybook/assets/example-layout.html index aaef4d4c..e8533534 100644 --- a/.storybook/assets/example-layout.html +++ b/.storybook/assets/example-layout.html @@ -5,7 +5,7 @@ - + Optics Example Layout diff --git a/.storybook/assets/login-layout.html b/.storybook/assets/login-layout.html index ab66a38c..fcbe612d 100644 --- a/.storybook/assets/login-layout.html +++ b/.storybook/assets/login-layout.html @@ -5,7 +5,7 @@ - + Optics Login Layout