<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mikhail Kokoulin</title>
    <description>The latest articles on DEV Community by Mikhail Kokoulin (@mikhail_kokoulin_d39d37a5).</description>
    <link>https://dev.to/mikhail_kokoulin_d39d37a5</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4093470%2Fcbe6d360-d31d-4b02-86db-cb58f4921927.jpg</url>
      <title>DEV Community: Mikhail Kokoulin</title>
      <link>https://dev.to/mikhail_kokoulin_d39d37a5</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mikhail_kokoulin_d39d37a5"/>
    <language>en</language>
    <item>
      <title>I open-sourced a UI kit — then went looking for everything I got wrong about it</title>
      <dc:creator>Mikhail Kokoulin</dc:creator>
      <pubDate>Tue, 25 Aug 2026 06:37:04 +0000</pubDate>
      <link>https://dev.to/mikhail_kokoulin_d39d37a5/i-open-sourced-a-ui-kit-then-went-looking-for-everything-i-got-wrong-about-it-4m72</link>
      <guid>https://dev.to/mikhail_kokoulin_d39d37a5/i-open-sourced-a-ui-kit-then-went-looking-for-everything-i-got-wrong-about-it-4m72</guid>
      <description>&lt;p&gt;There's no shortage of React UI kits on npm. Search for one right now, and you'll get hundreds of results, most with the same seven button variants and a Storybook someone abandoned halfway through. So when I open-sourced &lt;a href="https://github.com/mkokoulin/brightframe" rel="noopener noreferrer"&gt;&lt;code&gt;brightframe&lt;/code&gt;&lt;/a&gt; — pulled out of a real coworking site I built, &lt;a href="https://lancoworking.am" rel="noopener noreferrer"&gt;LAN&lt;/a&gt; — I didn't really want to write the usual "here's our 70 components, look how many there are" post. Component count isn't interesting. Anyone can list props and screenshot a button in five colors.&lt;/p&gt;

&lt;p&gt;What actually took time, and what I think is worth writing about, is the part that happens after the README makes a claim. "Tree-shakeable." "Server Components-safe." "Accessible." Those are three words I typed pretty confidently early on, and then, more recently, I sat down and tried to prove myself wrong on each one. This post is what that turned up.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Tree-shakeable per component" — okay, but how much, actually?
&lt;/h2&gt;

&lt;p&gt;Every component ships as its own entry point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;brightframe/tokens.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;brightframe/Btn.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Btn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;brightframe/Btn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Saying "unused components add nothing to your bundle" costs nothing. I added &lt;a href="https://github.com/ai/size-limit" rel="noopener noreferrer"&gt;&lt;code&gt;size-limit&lt;/code&gt;&lt;/a&gt; to CI so the claim has to keep being true, not just have been true once when I wrote the sentence:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entry&lt;/th&gt;
&lt;th&gt;Minified + brotli&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Whole kit (&lt;code&gt;import { ... } from "brightframe"&lt;/code&gt;, JS)&lt;/td&gt;
&lt;td&gt;40.13 kB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Whole kit (&lt;code&gt;brightframe/style.css&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;11.83 kB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One component (&lt;code&gt;brightframe/Btn&lt;/code&gt;, JS)&lt;/td&gt;
&lt;td&gt;641 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One component's styles (&lt;code&gt;brightframe/Btn.css&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;890 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;641 bytes vs. 40 kilobytes. That gap is the whole reason the per-component entry points exist, and now if a refactor accidentally makes &lt;code&gt;Btn&lt;/code&gt; drag in half the kit, the build just fails instead of me finding out from a bundle-size complaint six months later.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Server Components-safe" — this one had an actual bug in it
&lt;/h2&gt;

&lt;p&gt;RSC has no hook dispatcher at all. A component needs &lt;code&gt;"use client"&lt;/code&gt; if it does one of two things in its own source: calls a hook, or wires up a DOM event handler in its own JSX. I wrote a little script (&lt;code&gt;scripts/check-use-client.mjs&lt;/code&gt;) that walks every component and checks both, and runs in CI.&lt;/p&gt;

&lt;p&gt;First time I ran it, it found something I genuinely didn't expect. Every &lt;code&gt;RHF*&lt;/code&gt; field wrapper — the ones for react-hook-form — already had the directive. The parallel &lt;code&gt;Formik*&lt;/code&gt; family didn't have it on &lt;em&gt;any&lt;/em&gt; of them, despite every single one calling &lt;code&gt;useField()&lt;/code&gt; internally. Same author (me), same shape, same week probably, and one family was fine while the other would've thrown an unclear error the first time someone tried to render it inside a Server Component tree. I didn't catch this by reading files — I grepped the ~64 components that use hooks against the ~36 that already had the directive and diffed the two lists. Reading eight files carefully would not have found this; a dumb list comparison did.&lt;/p&gt;

&lt;p&gt;There's a smaller one I actually like more, because it breaks a mental shortcut. &lt;code&gt;Burger&lt;/code&gt; — the hamburger menu icon — holds no state of its own. &lt;code&gt;open&lt;/code&gt; and &lt;code&gt;setOpen&lt;/code&gt; come in as props. My gut said "no state, no hooks, should be server-safe." Wrong: its JSX still writes &lt;code&gt;onClick={() =&amp;gt; setOpen(!open)}&lt;/code&gt; directly, which is the &lt;em&gt;second&lt;/em&gt; rule, not the first. "Doesn't use &lt;code&gt;useState&lt;/code&gt;" and "server-safe" are not the same question, and it took a mechanical check to notice I'd been conflating them.&lt;/p&gt;

&lt;p&gt;Also, embarrassingly, the script only looked at &lt;code&gt;.tsx&lt;/code&gt; files for a while — until &lt;code&gt;useCombobox.ts&lt;/code&gt; (more on that hook below) shipped as its own publicly importable module with no directive and no &lt;code&gt;.tsx&lt;/code&gt; extension to be caught by the glob. Fixed the glob. This is the kind of gap you only find once you build something whose entire job is to catch you being wrong.&lt;/p&gt;

&lt;p&gt;And one thing I &lt;em&gt;didn't&lt;/em&gt; fix, on purpose: &lt;code&gt;Progress&lt;/code&gt; and &lt;code&gt;Skeleton&lt;/code&gt; spread &lt;code&gt;{...rest}&lt;/code&gt; onto their host element from a loosely typed props object, which technically means a consumer could pass an &lt;code&gt;onClick&lt;/code&gt; through and expect it to work. Both are already marked &lt;code&gt;"use client"&lt;/code&gt;. I left them that way instead of "correcting" them to match the strict rule, because the two failure modes aren't symmetric — an unnecessary directive costs you a small unused client boundary, a missing one silently breaks someone's click handler in production. When in doubt, I'd rather over-mark than under-mark. Wrote that down as an open, deliberately unresolved thing rather than pretending the mechanical rule settles it.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Accessible" — turns out that's a much bigger claim than a per-component test can back up
&lt;/h2&gt;

&lt;p&gt;Every component already has a &lt;code&gt;jest-axe&lt;/code&gt; unit test. For a long time I took that as "the kit is accessible," full stop. Then I added a second layer that actually renders every real Storybook story in a live Chromium browser (&lt;code&gt;@storybook/addon-vitest&lt;/code&gt; + &lt;code&gt;@storybook/addon-a11y&lt;/code&gt;) and ran it for the first time this week.&lt;/p&gt;

&lt;p&gt;29 real violations. Across 16 story files. Zero of them caught by the existing unit tests, because a unit test only ever renders one component in isolation — never two composed the way an actual page does.&lt;/p&gt;

&lt;p&gt;Some of what that turned up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Alert.module.css&lt;/code&gt; had two separate rules touching &lt;code&gt;.description&lt;/code&gt; — one setting &lt;code&gt;opacity: 90%&lt;/code&gt;, one setting &lt;code&gt;color&lt;/code&gt;. Neither line looks wrong on its own. Combined, against a tinted background, the description text on every single Alert variant dropped below the 4.5:1 contrast minimum. One merged rule fixed all four variants at once, and I felt a little dumb for not noticing two rules targeting the same class in the first place.&lt;/li&gt;
&lt;li&gt;The accent orange token measured 1.94–2.01:1 in a couple of real compositions — both as text on white and as white text on an accent fill. It's meant purely as a decorative accent, never a button fill, so nobody had ever actually checked it against a background where the contrast mattered. It just... sat there, unchecked, for however long.&lt;/li&gt;
&lt;li&gt;Two Storybook stories — meant to be &lt;em&gt;documentation&lt;/em&gt;, the thing people copy-paste from — showed passing a real &lt;code&gt;&amp;lt;Btn&amp;gt;&lt;/code&gt; into &lt;code&gt;DropdownMenu&lt;/code&gt;/&lt;code&gt;Popover&lt;/code&gt;'s &lt;code&gt;trigger&lt;/code&gt; prop. Both components already wrap &lt;code&gt;trigger&lt;/code&gt; in their own &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt; for the &lt;code&gt;aria-haspopup&lt;/code&gt; wiring. So the examples were literally teaching people to nest a button inside a button. Invalid HTML, real violation, and it was in the docs, which is worse than if it had just been a bug nobody saw.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HorizontalScroller&lt;/code&gt; had no way to reach its own scroll track with a keyboard. A scrollable region with nothing focusable inside it just isn't tabbable — obvious in hindsight, invisible until a real audit tool says so. Fixed it with &lt;code&gt;tabIndex={0}&lt;/code&gt; and &lt;code&gt;role="region"&lt;/code&gt;, and then immediately hit a second violation because two of them ended up on the same page with the same accessible name.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing I found and did &lt;em&gt;not&lt;/em&gt; fix — &lt;code&gt;BookingForm&lt;/code&gt;'s demo puts &lt;code&gt;GuestsCounter&lt;/code&gt;'s label straight onto &lt;code&gt;FormCard&lt;/code&gt;'s strong purple-tinted background, dropping its contrast from a fine 4.84:1 to 3.75:1. That's not a token I can nudge my way out of; it's a real design decision (a separate on-tint label color, or restricting that background to darker content). I marked it &lt;code&gt;todo&lt;/code&gt; in the a11y config rather than quietly ignore it or half-fix it with a hack. It's still broken. I just didn't pretend otherwise.&lt;/p&gt;

&lt;p&gt;And one finding I looked at, decided was probably noise, and left alone: a 4.47:1 reading on a token whose flat CSS math comes out to 4.84:1. My best guess is the checker is sampling actual anti-aliased pixel edges at a specific bold/17px size rather than the raw color value — a rendering artifact, not a real bug. That token is used in roughly 80 components. I wasn't going to darken it everywhere to chase a 0.37 gap I'm not even sure is real. Could be wrong about that. Wrote the reasoning down so whoever looks next doesn't have to start from zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Freezing 73 moving targets
&lt;/h2&gt;

&lt;p&gt;Nobody is going to click through 70-plus Storybook stories after every CSS change and eyeball whether something shifted. So every component now has a committed screenshot baseline per theme, diffed on every CI run through Vitest's browser-mode screenshot matcher.&lt;/p&gt;

&lt;p&gt;Two things bit me setting this up that I didn't see coming.&lt;/p&gt;

&lt;p&gt;Animations have to be frozen &lt;em&gt;per test&lt;/em&gt;, not once at the top of the file. A component mid-animation never settles into a stable frame, so the test just hangs and times out instead of failing with a useful message — which took me a minute to figure out, because "timeout" doesn't obviously mean "your spinner is still spinning." The fix is injecting &lt;code&gt;animation: none !important; transition: none !important&lt;/code&gt; in a &lt;code&gt;beforeEach&lt;/code&gt;, because Vitest's browser mode gives each test its own document, so you can't set it once globally and expect it to stick.&lt;/p&gt;

&lt;p&gt;Two components are just excluded outright — &lt;code&gt;Loader&lt;/code&gt;, which has no idle state at all (it's always animating, there's nothing to freeze into), and &lt;code&gt;MobileDatePicker&lt;/code&gt;, whose sheet uses &lt;code&gt;position: fixed&lt;/code&gt;, which contributes nothing to a bare, unstyled test container's layout box the way it would in a real page. I didn't try to force a fix for two components in an afternoon. I named the limitation, left it in the docs, and moved on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pulling one component's brain out of its body
&lt;/h2&gt;

&lt;p&gt;Most of &lt;code&gt;brightframe&lt;/code&gt; is deliberately not headless — a &lt;code&gt;&amp;lt;Card&amp;gt;&lt;/code&gt; that looks right without you writing a line of CSS is the actual pitch. But &lt;code&gt;Combobox&lt;/code&gt; now also ships its interaction logic on its own, as &lt;code&gt;useCombobox&lt;/code&gt;, with none of the styling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;combobox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCombobox&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;setValue&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;combobox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;containerRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;combobox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInputProps&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;combobox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;combobox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getListProps&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;combobox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filteredOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;combobox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOptionProps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The styled version is built on top of this hook now, not next to it, so there's exactly one copy of the open/filter/keyboard-nav logic instead of two that quietly drift apart over time. I started with &lt;code&gt;Combobox&lt;/code&gt; and not, say, &lt;code&gt;SelectField&lt;/code&gt; or &lt;code&gt;DropdownMenu&lt;/code&gt;, because it has strictly more logic than &lt;code&gt;SelectField&lt;/code&gt; needs (which means &lt;code&gt;SelectField&lt;/code&gt; could adopt this same hook later without losing anything), while &lt;code&gt;DropdownMenu&lt;/code&gt;'s keyboard handling is a genuinely different shape — item-list navigation, not text filtering — so it'd be its own extraction, not a copy-paste of this one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing the loop on where this all came from
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;brightframe&lt;/code&gt; was pulled out of &lt;code&gt;lan-site&lt;/code&gt;'s own internal component library. That's fine for me, but it's a one-way trip for anyone else stuck in the same spot with their own inline kit. So there's now &lt;code&gt;codemods/migrate-legacy-kit/&lt;/code&gt; — a small, config-driven codemod, dry-run by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun codemods/migrate-legacy-kit/cli.ts ../your-app/src/components &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--config&lt;/span&gt; your.config.ts &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--report&lt;/span&gt; dry-run-report.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rules are either &lt;code&gt;safe-rename&lt;/code&gt; (1:1, no behavior change, gets rewritten if you pass &lt;code&gt;--write&lt;/code&gt;) or &lt;code&gt;manual-review&lt;/code&gt; (flagged with a note, and there's no flag, no override, nothing that forces it through automatically). I tested that second guarantee specifically, because the moment a "just force it" escape hatch exists, someone eventually uses it on a rename that wasn't actually safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what was the point of all this
&lt;/h2&gt;

&lt;p&gt;None of this changes what &lt;code&gt;brightframe&lt;/code&gt; fundamentally is. Still a token-driven theme system, still mostly presentational components plus a handful of genuinely interactive ones, still no router or i18n dependency baked in. What changed is that the README's more interesting sentences — the bundle size, the RSC safety, the accessibility — aren't just sentences anymore. They're backed by something that runs on every push and fails loudly the moment they stop being true.&lt;/p&gt;

&lt;p&gt;If you're picking between UI kits — this one or someone else's — "how many components" is probably the wrong first question. I'd ask: if this claim turned out to be false tomorrow, would anything actually notice?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;brightframe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/mkokoulin/brightframe" rel="noopener noreferrer"&gt;https://github.com/mkokoulin/brightframe&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Storybook: &lt;a href="https://mkokoulin.github.io/brightframe/" rel="noopener noreferrer"&gt;https://mkokoulin.github.io/brightframe/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>a11y</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
