Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .storybook/assets/example-layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Example layout for the Optics design system" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rolemodel/optics@2.4.0/dist/css/optics.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rolemodel/optics@2.5.0/dist/css/optics.min.css" />
<!-- <link rel="stylesheet" href="../src/optics.css" /> Enable this during development to see local changes -->
<script type="module" src="https://esm.sh/invokers-polyfill"></script>
<title>Optics Example Layout</title>
Expand Down
2 changes: 1 addition & 1 deletion .storybook/assets/login-layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Example layout for the Optics design system" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rolemodel/optics@2.4.0/dist/css/optics.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rolemodel/optics@2.5.0/dist/css/optics.min.css" />
<!-- <link rel="stylesheet" href="../src/optics.css" /> Enable this during development to see local changes -->
<title>Optics Login Layout</title>
<style>
Expand Down
2 changes: 1 addition & 1 deletion .storybook/assets/spinner-layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Example layout for the Optics design system" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rolemodel/optics@2.4.0/dist/css/optics.min.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@rolemodel/optics@2.5.0/dist/css/optics.min.css" />
<!-- <link rel="stylesheet" href="../src/optics.css" /> Enable this during development to see local changes -->
<title>Optics Spinner Layout</title>
<style>
Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,30 @@ All notable changes to this project will be documented in this file.

This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.5.0] - 2026-09-30

### Component Changes

- Add .icon--svg for inline SVG icon sets by @dallasbpeters in https://github.com/RoleModel/optics/pull/359
- `.icon--svg` is a modifier of `.icon` for icons drawn as inline SVG, such as Hugeicons. Size modifiers work as they do for icon fonts, weight modifiers set the stroke width, and `.icon--low-emphasis` lowers the opacity. Stroke widths can be customized with `--_op-icon-stroke-width-*`.

### Dependencies

- Combine Dependabot updates by @Jeremy-Walton in https://github.com/RoleModel/optics/pull/357
- Nine development dependencies were updated in the lockfile. None of these affect the shipped CSS.

### Documentation Changes

- Fix documentation site by @Jeremy-Walton in https://github.com/RoleModel/optics/pull/341
- Fixed the sidebar examples, the switch syntax example and light-dark compilation on the documentation site, and added browser support to the introduction page
- Serve AI-Friendly Markdown Docs (llms.txt) from the Docs Site by @zoopmaster in https://github.com/RoleModel/optics/pull/343
- The documentation is now also served as plain markdown (llms.txt), so AI coding assistants can read component markup, token values and customization patterns
- Claim Context7 entry to allow admin access by @theojluciano in https://github.com/RoleModel/optics/pull/344
- Backfill changelog by @Jeremy-Walton in https://github.com/RoleModel/optics/pull/358
- Added this changelog, with every release since v0.0.1-alpha

**Full Changelog**: https://github.com/RoleModel/optics/compare/v2.4.0...v2.5.0

## [2.4.0] - 2026-06-25

### Utility Changes
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@rolemodel/optics",
"version": "2.4.0",
"version": "2.5.0",
"packageManager": "yarn@4.8.1",
"description": "Optics is a css package that provides base styles and components that can be integrated and customized in a variety of projects.",
"main": "dist/css/optics.css",
Expand Down
30 changes: 30 additions & 0 deletions src/components/icon.css
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
--_op-icon-optical-size-large: 40;
--_op-icon-optical-size-x-large: 48;

/* SVG (.icon--svg) */
--_op-icon-stroke-width-light: 1;
--_op-icon-stroke-width-normal: 1.5;
--_op-icon-stroke-width-semi-bold: 2;
--_op-icon-stroke-width-bold: 2.5;
--_op-icon-svg-opacity-low-emphasis: 0.6;

/* Private API (component option defaults) */
--__op-icon-weight: var(--_op-icon-weight-normal);
--__op-icon-fill: var(--_op-icon-fill-outlined);
Expand Down Expand Up @@ -53,18 +60,22 @@
/* Weight Modifiers */
&.icon--weight-light {
--__op-icon-weight: var(--_op-icon-weight-light);
--__op-icon-stroke-width: var(--_op-icon-stroke-width-light);
}

&.icon--weight-normal {
--__op-icon-weight: var(--_op-icon-weight-normal);
--__op-icon-stroke-width: var(--_op-icon-stroke-width-normal);
}

&.icon--weight-semi-bold {
--__op-icon-weight: var(--_op-icon-weight-semi-bold);
--__op-icon-stroke-width: var(--_op-icon-stroke-width-semi-bold);
}

&.icon--weight-bold {
--__op-icon-weight: var(--_op-icon-weight-bold);
--__op-icon-stroke-width: var(--_op-icon-stroke-width-bold);
}

/* Emphasis */
Expand Down Expand Up @@ -100,6 +111,25 @@
--__op-icon-font-size: var(--_op-icon-font-size-x-large);
--__op-icon-optical-size: var(--_op-icon-optical-size-x-large);
}

/* SVG Modifier: an inline SVG instead of a font glyph, e.g. `icon icon--svg icon--large` */
&.icon--svg {
svg {
display: block;
block-size: 100%;
inline-size: 100%;
}

/* A rule on the <svg> root cannot beat a stroke-width attribute on a child, so target the attribute.
With no weight modifier the set keeps the widths it was drawn with. */
&:is(.icon--weight-light, .icon--weight-normal, .icon--weight-semi-bold, .icon--weight-bold) svg [stroke-width] {
stroke-width: var(--__op-icon-stroke-width);
}

&.icon--low-emphasis {
opacity: var(--_op-icon-svg-opacity-low-emphasis);
}
}
}

/* Material Icons class provided by the Google Fonts CDN */
Expand Down
29 changes: 29 additions & 0 deletions src/stories/Components/Icon/Icon.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,28 @@ Emphasis acts similarly to weight, but changes the thickness of the icon strokes

<Canvas of={IconStories.Emphasis} />

## SVG Icons

`.icon--svg` is a modifier of `.icon` for an icon drawn as inline SVG instead of a font glyph. Use it for SVG sets such as [Hugeicons](https://hugeicons.com), or for Lucide, Tabler, and Phosphor when you want only the icons your app uses. Optics does not ship the SVG files. Each app keeps its own, and the [rolemodel_rails icon generator](https://github.com/RoleModel/rolemodel_rails/tree/main/lib/generators/rolemodel/optics/icons) (`--custom`) renders them.

```html
<span class="icon icon--svg icon--large" aria-hidden="true">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none">
<path d="..." stroke="currentColor" stroke-width="1.5" />
</svg>
</span>
```

<Canvas of={IconStories.Svg} />

- **Color:** the SVG uses `currentColor`, so it follows the text color.
- **Size:** the size modifiers work as they do for fonts. The SVG fills the `.icon` box.
- **Weight:** the stroke keeps the width the set drew until a weight modifier is set. Then `.icon--weight-*` sets the stroke width of every stroked element.
- **Emphasis:** `.icon--low-emphasis` lowers the opacity. Normal and high look the same.
- **Fill:** SVG sets draw filled icons as a separate style (for example, Hugeicons `solid-rounded`), so the fill modifiers do not apply.

If the app uses only SVG icons, import `optics+no_icons.css` so that no icon font loads.

## Icon API

Styles are built on CSS variables scoped to the icon.
Expand Down Expand Up @@ -276,6 +298,13 @@ Here are the variables that can be customized:
--_op-icon-optical-size-medium
--_op-icon-optical-size-large
--_op-icon-optical-size-x-large

/* SVG icons (.icon--svg) */
--_op-icon-stroke-width-light
--_op-icon-stroke-width-normal
--_op-icon-stroke-width-semi-bold
--_op-icon-stroke-width-bold
--_op-icon-svg-opacity-low-emphasis
```
{/* prettier-ignore-end */}

Expand Down
37 changes: 37 additions & 0 deletions src/stories/Components/Icon/Icon.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,40 @@ export const Emphasis = {
emphasis: 'high',
},
}

// Simple stroke glyphs drawn for this story, in the shape SVG icon sets ship:
// a 24x24 viewBox, currentColor, and a stroke-width on each element.
const svgGlyphs = [
'<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.5" /><path d="M8 12.5l2.5 2.5L16 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />',
'<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />',
'<path d="M4 10.5L12 4l8 6.5V19a1 1 0 0 1-1 1h-4v-5h-6v5H5a1 1 0 0 1-1-1z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" />',
]

const svgIcon = (glyph, classes = '') =>
`<span class="icon icon--svg ${classes}" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none">${glyph}</svg></span>`

const svgRow = (label, classes) =>
`<div style="display: flex; gap: var(--op-space-medium); align-items: center;">
<code style="min-inline-size: 32ch;">${label}</code>
${svgGlyphs.map((glyph) => svgIcon(glyph, classes)).join('')}
</div>`

export const Svg = {
render: () => {
const wrapper = document.createElement('div')
wrapper.style.display = 'grid'
wrapper.style.gap = 'var(--op-space-medium)'
wrapper.innerHTML = [
['icon icon--svg', ''],
['icon--small', 'icon--small'],
['icon--large', 'icon--large'],
['icon--x-large', 'icon--x-large'],
['icon--large icon--weight-light', 'icon--large icon--weight-light'],
['icon--large icon--weight-bold', 'icon--large icon--weight-bold'],
['icon--large icon--low-emphasis', 'icon--large icon--low-emphasis'],
]
.map(([label, classes]) => svgRow(label, classes))
.join('')
return wrapper
},
}
Loading