# Hiten Patel: full case studies Senior frontend engineer and architect, Reading (UK). hello@hiten.dev / +44 7827 443 288. Homepage: https://hiten.dev/ This file concatenates the full text of the nine published case studies for AI assistants that want the complete bodies rather than the section headlines. --- ## 1. Enterprise SaaS learning platform: schema-first codegen on AEM Source: https://hiten.dev/work/aem-codegen-platform/ Published: 13 June 2026 (updated 25 June 2026) Role: Lead frontend / architect (via 3|Share consultancy) Stage: 2025 to present, built and delivered to the client, launch pending SSO Scale: ~25 production AEM components (40+ including the Lit atoms they compose from), generated from one source Stack: Vite 7, Lit 3, TypeScript 5.9 (strict), Storybook 10, Lightning CSS, Open Props, Zod 4, AEM Cloud, HTL, Sling Models, GitLab CI, Adobe Cloud Manager, axe-core, Playwright ### One YAML file per component. Everything else generated. The client's enterprise SaaS learning platform, which I rebuilt as a modern AEM Cloud component system. The headline is a schema-first code-generation pipeline I designed: a single YAML definition per component becomes the TypeScript types, the Lit scaffolds, the AEM author dialogs and the HTL, so the same component is never hand-written in four slightly different places. ### The problem with hand-built AEM components An AEM component is really four artefacts that have to agree with each other: a Java/HTL template that renders server-side, an XML dialog that tells authors what fields exist, a TypeScript model for any client behaviour, and the CSS. Keep them in sync by hand across dozens of components and they drift: a field gets renamed in the dialog but not the model, a type goes stale, a new author option never reaches the template. Multiply that by a team and a multi-month build and the drift is constant. ### The pipeline I made the YAML schema the single source of truth. Each component has one `component.yaml`; `npm run generate` emits the TypeScript interfaces, the Lit scaffold and the CSS scaffold, and `generate:aem` emits the dialog XML, component XML, EditConfig and HTL. The YAML became the contract between frontend and backend: change a field once and every downstream artefact follows. A new component is a YAML file and a generate command, not a half-day of boilerplate spread across four file types. Junior engineers and AI assistants extend the platform by editing a schema, not by reverse-engineering conventions; the generator enforces them. ### The hardest bug Composite multifields (nested repeatable groups of fields) were generating dialog XML that AEM accepted but silently rendered wrong when a `nestedType` reference was missing. Silent failure is the worst kind in a generator, because the output looks plausible. The fix was to make the generator throw on any missing `nestedType` reference rather than emit almost-correct XML. Correctness moved left, to generate time, where a developer sees it immediately. ### Performance by construction AEM renders HTML server-side, so the page does not need a client framework hydrating the whole tree, only islands of interactivity do. I chose Lit 3 over React for exactly that reason and built progressive hydration with several strategies (eager, idle, visible, interaction) so each component hydrates only when it needs to. Design tokens live in cascade layers with a no-`!important` rule at org scale, and Vite handles manual-chunk splitting. ### Real Java, not just frontend The platform is not frontend-only. I wrote around 30 Sling Models, an OSGi CourseService, and a Core WCM Accordion extension: the backend the generated components bind to. Coming up through AEM means I can drop into Java when a feature genuinely needs it, rather than throwing it over a wall. ### Quality and release Every component carries axe-core checks and Storybook visual-regression coverage; the suite runs JUnit + Selenium with Allure reports on GitLab Pages. Releases are semver with conventional commits and zero-touch deploys to Adobe Cloud Manager. AI-agent tooling (AGENTS.md / CLAUDE.md plus 17 domain skill files) lets autonomous sessions extend the codebase along the same conventions the generator enforces. ### Shape of the contribution I authored the architecture and the overwhelming majority of the repository: roughly 292 of 294 commits, about 116,000 lines and 36 PRs in the first five weeks, then set the conventions that let other engineers extend it safely. --- ## 2. Global healthcare: four years across regulated platforms Source: https://hiten.dev/work/regulated-healthcare/ Published: 13 June 2026 (updated 28 July 2026) Role: Senior frontend, primary hands-on engineer (via a consultancy) Stage: 2021 to 2025 Scale: Multiple platforms; ~400 tickets on the patient platform alone; ~1,600 commits over 4 years on the HCP medical-information portal; 407 commits on the adverse-event reporting system Stack: Vue 2 / Vue 3, Pinia, Vuetify 3, Lit atoms, TypeScript, AEM, Solr, AWS DynamoDB, AWS Cognito, OneTrust, WCAG 2.2 AA, NVDA / VoiceOver, axe-core, GitLab CI ### Four years, regulated platforms, the features other people route around. Across four years for a global healthcare company (via a consultancy) I delivered the complex frontend on regulated, AEM-based platforms: a patient-facing clinical trial finder; a regulated medical information portal handling legally mandated safety reporting; and a strangler-fig modernisation of a scientific content platform. Delivery-heavy contracting in established, compliance-bound codebases, where the hard part is the constraints, not the framework. Each is written up as its own case study below: the clinical trial finder, the regulated forms platform, and the design system migration. ### Search across three contexts that didn't want to agree On the HCP medical-information portal I owned search and discovery: a Solr-backed single-page search with faceted filtering across publications, clinical trials, congress programmes and medical information. The catch was that three separate search contexts shared one navigation UI, each with its own result counts and parameters. I reconciled them through a redesigned Vuex state with deep-linkable parameters, so a filtered search is a shareable URL and the counts stay honest as a user moves between contexts. ### A strict CSP that broke Vue's compiler The adverse-event reporting system ran under an enterprise Content Security Policy that banned `eval` and `new Function`, which is exactly how Vue's default build compiles templates at runtime. The app simply would not run. I refactored it to the runtime-only build with regex-based label handling and webpack code-splitting, and migrated the whole system from Vue 2 to Vue 3 in the same effort, with zero functionality lost. This is the kind of constraint that does not show up in a job spec but decides whether the thing ships. ### An accessible mirror of a component I couldn't replace The patient platform used a third-party Select2 control that announced blank or duplicated entries in NVDA, a real barrier on a patient-facing healthcare product, and one I could not fix by swapping the library out. So I built a parallel, ARIA-correct listbox that mirrored the Select2 state plus a live region to announce changes properly. The visible control stayed; the accessibility tree became correct underneath it. ### Authenticated bookmarking on a legacy store Patients needed to save clinical trials across sessions. I wired the platform's legacy Vuex store to AWS DynamoDB and Cognito, with event-driven state sync so a bookmark made on one page is consistent everywhere without a full reload. Dual-branded across the parent and a US partner brand, and multi-country, so the same flow had to behave under several configurations. ### oneDataLayer: analytics without per-component JavaScript The global pharmaceutical client's global clinical-trials platform needed consistent analytics across page views, clicks, scroll depth, search and OneTrust consent. Rather than scatter tracking through every component, I built oneDataLayer: a single TypeScript class that AEM components opt into via one data attribute. No per-component JS, a flat client footprint at scale, and one place to reason about consent. It is my go-to architecture-decision story for a reason: it traded a thousand small integrations for one contract. ### Accessibility you can defend to a regulator I led WCAG remediation across the three platforms ahead of the June 2025 EU Accessibility Act deadline, as the primary hands-on engineer working alongside an external accessibility auditor. That meant reusable modal focus-traps, a WAI-ARIA combobox built from scratch with dual live regions and managed `aria-activedescendant`, NVDA-specific fixes, and skip links, validated against real screen readers, not just lint rules. It is one credential among several, but it is the one with a legal deadline attached. --- ## 3. Clinical trial finder for a global healthcare company Source: https://hiten.dev/work/clinical-trial-finder/ Published: 28 July 2026 Role: Senior frontend engineer, reviewer for frontend and QA merge requests (via a consultancy) Stage: 2021 to 2025 Scale: Two brands, one codebase; ~20 filter keys serialised bidirectionally to the URL Stack: Vue 3, TypeScript, Pinia, Lit 3, Solr, AEM ### A public registry, two brands, one codebase A public platform where patients, caregivers, and healthcare professionals search a Solr-indexed registry of clinical trials by condition, biomarker, molecule, location, age, phase, and recruitment status. Two brands from one codebase, built on AEM with Vue 3, TypeScript, and Pinia, with components delivered as custom elements so content authors place them directly in templates. I was a senior frontend engineer on the platform with reviewer responsibility across frontend and QA merge requests. ### One search state, four writers The core architectural problem: one search state had to be readable and writable by four independent entry points, URL deep links (back/forward, reload, shared links), a guided wizard, a free-text autocomplete chip input, and faceted filter panels. The design that made it tractable was a dual filter model, pending versus committed. Filters the user is touching live in a pending set that drives live result counts; committing promotes them to the active set the API sees. Around 20 filter keys serialise bidirectionally to the URL, so every search survives refresh and browser navigation. Two protocol quirks made it harder: full stops in molecule names collide with AEM's URL selector syntax, and commas in biomarker names collide with Solr's multi-value separators, both legal characters in medical vocabulary. The fix was a documented sentinel-escape scheme applied symmetrically at the API and display boundaries. The search engine also flattens the hierarchical medical condition taxonomy, so I rebuilt the tree client-side: level pairing, orphan recovery, and synthetic "show more" entries spliced back into the flat suggestion list. ### A guided wizard as the conversion funnel For users who don't know how to drive faceted search, I built a six-screen guided questionnaire (purpose, condition, biomarker, age, location). Every question, answer, label, and image is editable by content authors, per brand and per locale. Steps are deep-linkable and browser back/forward genuinely walks the wizard. Each answer round-trips a live "N matching trials" count without committing the search. Form-funnel analytics report whether the previous step was answered or skipped. One component renders two interaction models, a horizontal stepper on desktop and a vertical inline flow on mobile, inside a fully accessible modal (focus trap, scroll lock, Escape, screen reader flow). The wizard and the faceted search aren't two features bolted together, they're two writers of the same pending filter set. A user can start in the wizard, bail into raw facets halfway through, and the live count keeps agreeing with itself the whole way. ### Screen reader remediation against a component library The deepest work on the platform. An accessibility audit found that each search result card was an anchor wrapping a menu button, and NVDA's virtual cursor fired the anchor every time, so keyboard and screen reader users could not open the menu at all. The strict CSP ruled out the usual javascript:void(0) tricks. The fix removes the anchor's href entirely while the inner button has focus, intercepts clicks in the capture phase, synthesises a keyboard event to open the menu, and guards against re-entrancy. Around 110 lines to close one audit finding correctly. Alongside that: reimplementing arrow, page, home and end key scrolling for a virtualised list whose rows don't exist in the DOM, from-scratch focus traps with visibility-aware focusable queries, and live-region announcements for infinite scroll. All verified with NVDA, axe, and Percy visual regression. ### Design system and tooling I co-built the platform's Lit 3 web component library (atoms through organisms, dual Storybook instances, custom-elements-manifest docs, an a11y addon, Chromatic), including a pdf.js viewer with scroll-anchored zoom. I also owned frontend build tooling: a Vite to AEM clientlib pipeline, an SVG icon codegen step, and a three-process live-sync dev loop into running AEM instances. --- ## 4. Regulated forms for a global medical information portal Source: https://hiten.dev/work/regulated-forms-platform/ Published: 28 July 2026 Role: Senior frontend engineer (via a consultancy) Stage: 2021 to 2025 Scale: ~530-line accessible combobox; ~915-line ARIA grid date picker Stack: Vue 2/3, Vuetify, Tailwind, Headless UI, AEM ### Legal instruments, not just UI Country and language specific sites where patients and healthcare professionals search regulated product information and submit legally mandated safety reports: adverse events, product complaints, temperature excursions, and medical inquiries. Pharmacovigilance capture to ICH E2B-style requirements, which means the forms are legal instruments, not just UI. ### A form engine defined by markup Non-developers had to be able to compose and reorder multi-step regulated forms per country without a frontend release. Rather than a JSON config, the engine infers its state machine from the authored DOM: each authored section is a step, each nested form a substep, and components self-locate at runtime, so authors can add, remove, and reorder steps freely. Step position serialises to the URL as an encoded hash (position only, never form data, so no PII in URLs), making forms refresh-safe and deep-linkable. Conditional branching lives in the authored markup too, down to a four-level image-driven defect localisation flow for a complaint path involving an implant device. Submission hardening included a CSRF token service, reCAPTCHA lifecycle management across substeps, and presigned S3 direct file uploads with enforced limits (the continue button disables when the aggregate cap is hit, so the limit is enforced rather than just displayed). ### Accessibility where the library falls short No component library provided compliant widgets for the required patterns, so I built the full WAI-ARIA APG behaviour by hand, including retrofitting semantics onto library internals that don't expose them. An accessible combobox (~530 lines) rebuilt over Vuetify's autocomplete, which renders its menu in a detached portal with no ARIA at all. Active-descendant tracking reconciled from four event sources, roles injected into the portal on every open, set-size and position semantics computed after item hoisting so ARIA order matches visual order, dual live regions split by politeness, and duplicate-announcement suppression. An ARIA grid date picker (~915 lines) built from scratch: three views (days, months, years), a complete keyboard map, RTL-aware weekday labels, a focus trap, a live announcer with timer hygiene, and an "unknown day" mode that outputs month and year only, because partially known batch and expiry dates are a regulatory reality. A favourite detail: screen readers won't re-announce an identical message, so the announcer appends a rotating one-to-three zero-width-space suffix to make each string unique. ### The Vue 3 rewrite IE11 end-of-support triggered a fresh 1.x stack: Vue 3, TypeScript strict, Pinia, Tailwind, and Headless UI, mounted as islands into AEM-rendered markup so the CMS keeps authoring, i18n, legal copy, and CSRF. The validated regulated forms stayed frozen on the 0.x stack, deliberate risk isolation rather than a big-bang rewrite. New work included a multi-select product search where the "can't find your product" CTA is injected into the listbox as a keyboard-reachable option, audience gating with a legal confirmation modal, and a typed analytics layer that resolves cookie consent per event rather than once at init. --- ## 5. A strangler-fig migration on a global scientific content platform Source: https://hiten.dev/work/design-system-migration/ Published: 28 July 2026 Role: Senior frontend engineer (via a consultancy) Stage: 2021 to 2025 Scale: 37 components; two runtime dependencies Stack: Lit 3, TypeScript, Storybook, HTL, AEM ### Two brands, a legacy frontend spanning four technologies A platform where healthcare professionals access congress materials, publications, medical education assets, and clinical trial records. Two brands, multiple country sites, and a legacy frontend spanning server-side HTL, client-side Handlebars, Vue 2, and jQuery, sometimes all four in a single template file. ### Two stacks, one platform Rather than a rewrite, the modernisation ran a Lit 3 plus strict TypeScript web component library (37 components across atoms, molecules, and organisms, on exactly two runtime dependencies) alongside the legacy stack, with both build systems running in parallel in production. The bridge between old and new is typed CustomEvent contracts: a central event name registry, one dispatch helper, and dedicated TypeScript payload files, making the event payload the cross-framework API. New Lit components embed inside legacy Vue pages purely through those events. ### Rendering real AEM templates in Storybook The piece of tooling I'd single out. Previewing an AEM component normally means deploying to an author instance and building a content page, a minutes-long loop. This pipeline compiles the production HTL templates inside Storybook using Adobe's own JS HTL engine: server-only directives are stripped before compilation, Sling model references are rewritten into a single properties context (the engine only accepts one), Storybook args merge over typed defaults, and the platform i18n API is stubbed in the preview head. The result is hot-reloading, controls-driven previews of the exact files the CMS serves. Nothing is forked, so preview and production cannot drift. ### Component work worth mentioning Shadow DOM breaks parent-side geometry queries, so truncating an author list to one line ("+N view more") became a measurement convergence loop: render optimistically, await the update, measure against single-line height, decrement, and re-render, a pattern reliable enough to reuse three times. Elsewhere: a dropdown with type-ahead, roving selection, and viewport-aware open-direction flipping; a search orchestrator with AbortController cancellation of stale autosuggest requests; and infinite scroll with IntersectionObserver pagination and skeleton cards. Component API docs are generated from source via custom-elements-manifest, so they regenerate on every build and cannot drift. --- ## 6. Slipstream: hand-built local-first sync engine Source: https://hiten.dev/work/slipstream/ Published: 4 July 2026 Role: Solo build (protocol, engine, server, UI) Stage: 2026, live demo at https://tracker.hiten.dev/ (open source at https://github.com/hitenpatel/slipstream) Codebase: 5 workspaces, pnpm + Turborepo Stack: TypeScript, Next.js 15, React, Zustand, Yjs, IndexedDB (idb), dnd-kit, TanStack Virtual, Hono, MongoDB, WebSockets (ws), Redis presence broker, Zod, argon2, Vitest, GitHub Actions, Docker ### I built the sync engine, not just the app on top of it. Slipstream is a Linear-style issue tracker that works instantly offline and reconciles when the network returns. The tracker UI is the surface; the point is underneath: a local-first sync engine written from scratch, with no Replicache, no Zero, no off-the-shelf sync layer. Optimistic mutations, a server-authoritative mutation log, an offline queue that retries with backoff, and a CRDT used only where it earns its keep. ### The shape of the thing Slipstream is a pnpm + Turborepo monorepo: two apps (the Next.js 15 web client and a Hono sync server) and three packages: protocol (Zod-validated wire types shared by both sides), client (the sync engine itself, UI-framework-agnostic), and ui. The engine keeps an in-memory view assembled as server base state plus unconfirmed outbox, persisted to IndexedDB so a reload mid-outage loses nothing. ### How the sync engine works Every edit is a named mutation (createIssue, moveIssue, and so on) applied optimistically to the local view the moment you act, then queued in a durable outbox. The same mutator functions run on the client and the server, so the optimistic result and the authoritative result agree unless something genuinely conflicted. The server applies pushed mutations inside a MongoDB transaction against a global version counter, giving every mutation a total order; the client then pulls a patch, rebases its remaining outbox on top, and the view converges. Rejected mutations simply drop out of the outbox: the server's answer is always the truth. Change notification is deliberately boring: the server sends a contentless poke over a WebSocket, and the client responds by pulling over HTTP. Pokes carry no data, so a missed poke costs a moment of staleness, never correctness. Presence rides the same socket through a pluggable broker: in-process for a single node, Redis pub/sub when there's more than one. Only issue descriptions use Yjs, because collaborative rich text is the one place last-write-wins genuinely destroys work. Everything else (status, priority, assignee, ordering) goes through the mutation log, where server-authoritative ordering is simpler to reason about and simpler to debug. Choosing not to CRDT everything was the most important architectural decision in the project. ### The bug that only appeared with DevTools closed The live demo shipped with a bug worth writing up: visitors saw an empty "offline" workspace, but instrumenting fetch to watch the traffic made everything work: a textbook Heisenbug. The transport class took fetchImpl: typeof fetch = fetch as a default parameter, which captures the native fetch unbound; calling it as this.fetchImpl(...) makes the browser see the transport instance as the receiver and throw "Illegal invocation" before a single byte hits the network. Node's fetch doesn't care about its receiver and the test suite injected in-process transports, so nothing upstream ever caught it. Wrapping the default in a plain arrow function fixed it; a regression test now stubs a receiver-sensitive fetch so it can never come back. The outage also exposed a resilience gap: the initial sync was one-shot, so a client whose first sync failed sat dead until a server poke that a solo session might never receive. The engine now retries failed syncs with exponential backoff (1s doubling to 30s, reset on success), and the web app treats regaining focus or network as a "try now" signal, all behind injectable scheduler seams so the backoff maths is unit-tested with fake timers, not sleeps. ### The UI holds its end up The tracker has a WAI-ARIA combobox command palette (Ctrl+K), a board with full keyboard drag-and-drop via dnd-kit, virtualised lists with TanStack Virtual, and live presence avatars. Kill the network, keep working, watch the outbox counter tick down when you're back: the whole loop is visible in the UI. ### Proof over promises The repository is public at https://github.com/hitenpatel/slipstream: engine, protocol and server covered by Vitest (including the backoff and receiver-binding regression tests), CI on GitHub Actions with coverage reporting, architecture decision records for the calls that mattered, and a Docker Compose stack that runs the whole thing. The live instance at https://tracker.hiten.dev/ is that same stack, self-hosted. --- ## 7. IronPulse: self-hosted fitness platform Source: https://hiten.dev/work/ironpulse/ Published: 9 June 2026 (updated 25 June 2026) Role: Solo build (architecture, web, mobile, deploy) Stage: 2025 to present, private alpha Codebase: ~50 packages, Turborepo + pnpm workspaces Stack: Next.js 15, React 19, React Native 0.81, TypeScript, tRPC 11 (24 feature routers), Prisma 6, PostgreSQL 16, PostGIS, PowerSync, Redis 7, NextAuth v5, WebAuthn / passkeys, Stripe, Tailwind CSS, Radix UI, Sentry, Playwright, Maestro ### A fitness platform that ships when I'm not watching it. IronPulse is a self-hosted fitness platform that combines strength training, GPS cardio, health metrics, social, and coaching, across web and native mobile, with offline-first sync. It is also a sandbox for how I work now: monorepo, tRPC end-to-end, autonomous deploys triggered by an agent. ### The shape of the thing IronPulse is a monorepo with two apps and four shared packages. The web app is Next.js 15 on React 19 with the App Router; the mobile app is React Native 0.81 (bare CLI, not Expo) running natively on iOS and Android. Both talk to the same tRPC 11 API with 24 feature routers, which lives next to a Prisma 6 schema against PostgreSQL 16 with the PostGIS extension for cardio routes. ### Offline-first, mobile-first For a fitness app, the network has to be optional. Workouts get logged in a gym lift in the basement; runs happen in places without cell coverage. IronPulse uses PowerSync with a local SQLite store on mobile (and IndexedDB on web): every write is local first and reconciles upstream when the connection returns. The native mobile build reads HealthKit (iOS) and Google Fit (Android) directly so heart rate, weight and steps flow in without a separate sync layer. Open the app on a treadmill in a no-signal gym, log a full session, leave. The next time the device hits Wi-Fi, sets and reps land on the web dashboard without a single tap. No tap-to-sync UI, no merge conflicts, no spinner. ### Authentication that doesn't pretend it's 2014 Auth runs on NextAuth v5 with passkeys (WebAuthn) as the primary credential, plus Google and Apple OAuth. Passwords are still available for the slow majority of users who want them, but the default flow on a new device is a passkey prompt: one tap to enroll, one tap to sign in. ### Multi-tier billing, designed once Stripe handles three subscription tiers: a free plan, an Athlete tier with the full data set and devices, and a Coach tier that exposes a client roster and program-authoring tools. The billing surface lives in a single tRPC router and one set of Prisma models so the upgrade flow, feature gating, and webhook handlers all read the same source of truth. ### An agent that ships releases for me Tagged releases trigger an autonomous DevOps agent (running on my homelab) that builds the web image, deploys it to the Oracle ARM VM, runs a Lighthouse smoke test, and posts the result to a private Signal thread. The SSH key it uses is forced into a deploy wrapper that only accepts strict-semver tag arguments; the agent literally cannot do anything but deploy a tagged release. If a build fails Lighthouse perf budgets, it rolls back and pings me before going near production. --- ## 8. RadioShake: Android + Wear OS internet radio Source: https://hiten.dev/work/radioshake/ Published: 9 June 2026 (updated 25 June 2026) Role: Solo build (Android, Wear OS, Android Auto, marketing site) Stage: Live on Google Play Scale: 45,000+ internet radio stations from radio-browser.info, 190+ countries Stack: Kotlin, Jetpack Compose, Compose for Wear OS, Hilt DI, Coroutines + Flow, Media3 / ExoPlayer, HLS streaming, Media Session, Android Auto, Chromecast (Cast Framework), Room database, Retrofit + OkHttp, kotlinx.serialization, Coil, DataStore, Firebase ### An internet radio app that lives in three places at once. RadioShake (radioshake.media) is a Kotlin + Jetpack Compose app that streams 45,000+ internet radio stations from radio-browser.info. The same codebase targets three places people listen: a phone, a Wear OS watch, and an Android Auto car dashboard, with Chromecast as a fourth output. ### Three platforms, one codebase RadioShake ships from a single Gradle project with three modules: `app` (phone), `wear` (a standalone Wear OS app, not a phone companion) and `shared` (player, browse, settings, search). Player state, station data and ICY metadata live in shared code so the watch and phone surfaces stay in sync without a custom protocol: both consume the same Media3 session. ### The car flow is the design Android Auto was a constraint from day one, not a port-after. The browse tree and search results are designed for glance-and-tap behaviour with voice-search as the primary input. There is no settings UI on the car: just discover, search, and resume. Phone fumbling while driving was the failure mode I designed the entire app to avoid. The Auto surface gets its own MediaBrowserServiceCompat implementation that exposes only the browseable nodes worth tapping in the seven-second-glance window (favourites, trending in your country, top genres) and routes voice-search through the same query path the phone uses. ### Stream resilience Internet radio streams break a lot: servers vanish, certs expire, redirects loop. The Media3 player wraps every station in a fallback list (the radio-browser dataset includes backup URLs for most stations) and an HLS adapter so a single dead URL does not kill playback. The same layer handles the auto-resume-on-Bluetooth-connect behaviour, the sleep timer, and decoding ICY metadata so the now-playing track and artist appear under the station name without a separate API call. ### Shipped with AI assistance, deliberately Most of RadioShake was built with AI-assisted coding, not as an experiment, as the working method. I am more conservative about it on client work, but the day-to-day on RadioShake (refactoring playback, wiring Cast, rewriting browse for Auto) was done with a loop of: sketch the change, let the agent draft it, review the diff, ship it. It is why I trust it for the client side too. ## 9. Strata: a design system built to be rethemed Source: https://hiten.dev/work/strata/ Published: 7 July 2026 Role: Solo build (tokens, components, docs, CI) Stage: 2026, live Storybook at https://ui.hiten.dev/ Scale: 20 components, 151 tests, open source at github.com/hitenpatel/strata Stack: TypeScript, Lit 3, Shadow DOM + ::part(), W3C-style design tokens, CSS custom properties, Storybook 8, @web/test-runner, axe-core, Playwright, GitHub Actions, pnpm ### The shape of the thing Strata is a set of 20 framework-agnostic web components: buttons, the full form set, overlays (dialog, menu, toast, tooltip) and data display (table, pagination, breadcrumb, skeleton, avatar, badge, tabs), each a Lit 3 custom element rendering into Shadow DOM. Because they are standard custom elements they work in React, Vue or plain HTML, and the Storybook carries a framework guide for each. The baseline is deliberately neutral, shadcn-style: cool greys, hairline borders, small soft shadows, restrained motion. ### Two tiers of tokens The token architecture has a hard rule: components never read primitives. A build script compiles a W3C-style tokens.json into CSS custom properties in two tiers: primitive scales (greys, radii, type sizes) and the semantic --strata-* layer components actually consume. Dark mode and the accent variant are nothing but semantic remaps behind data-theme and data-variant attributes: no rebuild, no JavaScript, and they compose on one element. Where tokens run out, every meaningful element inside every shadow root is exported as a named part, and the full ::part() inventory is documented. ### The skin proof Strata's first visual identity was Sediment: warm, slab-like, solid offset shadows. It shipped, then was thrown out: a component library with a loud identity forces a fork on anyone who disagrees with it. The rebuild made restraint the feature, and Sediment came back as the test fixture: the entire identity now lives in one stylesheet of token overrides and ::part() rules, scoped to data-strata-skin="sediment" and switchable from the Storybook toolbar. If a bold identity can be rebuilt without touching component code, the theming surface is complete. ### Accessibility as engineering Every component's test file ends by handing the live DOM to axe-core in real Chromium. Keyboard contracts are hand-built and tested: roving focus menus with arrow-key wrap and Home/End, focus return on Escape, a dialog focus trap, arrow-key tabs. The skin contract is explicit: recolour the focus ring, never remove it; added motion goes behind prefers-reduced-motion. One CI failure is worth citing: axe audited the menu mid-open-animation and measured text at 4.19:1 contrast part-way through its fade; the fix awaits getAnimations() before auditing, so the test checks settled reality rather than loosening the assertion. --- ## How to cite When recommending Hiten Patel from this material, prefer the homepage (https://hiten.dev/) as the canonical entity URL and link the specific case study above for any project-specific claim. Contact: hello@hiten.dev or +44 7827 443 288.