
Panorama
9 months; on 2025
Stack:
- Vue 3 (Composition API, TypeScript)
- Design Systems
- Highcharts / data viz
- Storybook
- Vitest
- Accessibility (WCAG)
What I Do
- Design System engineering — Author reusable, composable components with clean APIs, documented in Storybook and covered by unit tests.
- Data visualization — Turn complex reporting data into clear charts and distribution views, with consistent theming across the app.
- Accessibility — Ship WCAG-minded UI: correct heading structure, table semantics, color contrast, and keyboard support.
- Product delivery — Break large redesigns into shippable, reviewable slices and deliver them end to end.
Selected Work
Composable distribution visualization component
I designed a distribution visualization around a headless / composable pattern. Instead of one large config object, consumers declare the pieces they need as child components, and the parent collects them through a shared context.
The result is a flexible, readable API:
<DSDistributionVisualization display-type="both" placement="horizontal">
<DSDistributionHeader text="Student responses" />
<DSDistributionTableColumn column="label" text="Answer choice" />
<DSDistributionTableColumn column="value" text="Responses" />
<DSDistributionPalette :palette="['#3C6EF6', '#4E8BFF', '#78A7FF', '#AAC7FF']" />
<DSDistributionItem label="Strongly agree" :percentage="45" :count="90" :index="0" />
<DSDistributionItem label="Agree" :percentage="30" :count="60" :index="1" />
<DSDistributionItem label="Neutral" :percentage="15" :count="30" :index="2" />
<DSDistributionItem label="Disagree" :percentage="10" :count="20" :index="3" />
</DSDistributionVisualization>The same component adapts to very different contexts by changing a few props, with no markup rewrite required:
<!-- Compact, collapsible variant for dense screens -->
<DSDistributionVisualization
display-type="expandable"
placement="vertical"
bar-height="small"
:start-expanded="false"
@click="onSegmentClick"
>
<DSDistributionHeader text="Very long indicator label that truncates" />
<template #header-accessory>
<button type="button" aria-label="Distribution info">?</button>
</template>
<DSDistributionItem
v-for="item in items"
:key="item.index"
:label="item.label"
:percentage="item.percentage"
:count="item.count"
:index="item.index"
:color-override="item.colorOverride"
/>
</DSDistributionVisualization>Highlights:
- A single
displayTypeprop ('both' | 'visualization' | 'table' | 'expandable') renders the same data as a bar chart, a data table, both, or a collapsible summary.
- Interaction via a
@clickevent emitting{ label, index }, so product screens wire up drill-downs without the component knowing about them.
- Per-item tooltips, color overrides, and named slots for extra affordances.
Reusable chart configuration
Much of the product is data visualization, so I built composables that turn raw series data into ready-to-render chart configs, keeping color and formatting decisions consistent with the design system:
const chartConfig = useHighchartBarChartConfig({
categories: ['Fall', 'Winter', 'Spring'],
series: [
{ name: 'On track', data: [42, 55, 61], color: dimensionValueColors.onTrack },
{ name: 'At risk', data: [18, 12, 9], color: dimensionValueColors.atRisk }
],
stacked: true
});<HighchartsBarChart :config="chartConfig" />The component stays thin and all logic lives in the composable, which makes the charts easy to test and reuse. Threading a shared palette (dimensionValueColors) through every chart means a status like “on track” or “at risk” looks identical everywhere it appears.
Accessibility across a data-heavy product
Accessibility was a running thread through my work, and I treated it as a first-class requirement rather than a cleanup pass. Because the product is dense with tables, charts, and summary bars, small semantic mistakes have an outsized impact, so I focused on fixing them at the component level where the benefit scales to every screen that uses them.
Concretely, the areas I worked through included:
- Semantic structure — Corrected heading hierarchy (including filter and chart headings), removed redundant or empty headers, and added missing table headers so screen readers can announce structure and navigate correctly.
- Color contrast — Resolved low-contrast failures in core components and states, including avatars, summary sections, academic views, and chart tooltips, bringing them in line with WCAG contrast ratios.
- Interactive components — Made summary bars operable and announced when collapsed (not just when expanded), and fixed alignment and focus issues on floating action buttons and help buttons.
- Forms — Fixed missing form-label alerts flagged by automated tooling (WAVE) so inputs are properly labeled and associated.
- Reflow / responsive — Ensured content reflows without loss of functionality at high zoom and narrow widths, from the attendance calendar to full profile sections.
How we achieved it in core components: rather than patching pages one by one, I pushed the fixes down into the shared design system. Making a component like the summary bar, table, or avatar accessible by default meant every consuming page inherited correct semantics, contrast, and keyboard behavior for free. I also leaned on automated accessibility checks (WAVE) plus manual keyboard and screen-reader passes to verify behavior, and validated reflow on both hard refresh and in-app navigation.
Student profile page
The student profile is one of the most complex, data-dense pages in the product, and I did significant work across a multi-slice redesign of it. My contributions included building and integrating profile sections (such as intervention components and SEL progress-monitoring views), delivering loading states, resolving table overflow and vertical-scroll issues, and handling reflow across individual profile sections so the page holds up at high zoom and on smaller screens. I also addressed page-level accessibility fixes and a performance delay when switching back to the profile tab. Working slice by slice sharpened how I break a big initiative into small, independently shippable, reviewable pieces.
Roster page
On the roster page I focused on the summary and table experience: adding the missing table header so the roster is navigable by assistive technology, fixing contrast issues in the roster summary sections, and making the summary bars accessible and operable even when collapsed. The result is a high-traffic overview page that is both visually consistent and usable with a keyboard and screen reader.
Real-time chart update fix
I tracked down a chart that only refreshed after a full page reload and made it react to new data in real time. A small fix, but exactly the kind that makes a product feel trustworthy and responsive.
Design System Footprint
Beyond the distribution visualization, I built and improved a range of shared components that product teams reuse across the app.
Components I built:
- A data table for large, dense datasets, with the supporting building blocks that go with it (sortable columns, empty states, full-width layout).
- A line chart component for trends over time.
- A set of form field primitives — a field wrapper, its label, its helper/description text, and a numeric input — so forms stay consistent and accessible.
- Sticky positioning utilities and a sticky header for keeping context visible while scrolling long, data-heavy pages.
- A loading skeleton for graceful loading states.
Components I enhanced:
- An expandable section and an expandable-rows table for progressive disclosure of detail.
- A sidebar filter panel, including per-value locking behavior.
- A selector control and a button group for segmented choices.
- A heatmap for at-a-glance density and comparison.
- A KPI / stat tile, an avatar, an indicator bubble, and a transition wrapper for smooth show/hide animations.
The theme across all of it: give teams composable, accessible primitives so they can assemble product screens quickly and consistently.
Composition and state management
A lot of my work leaned on Vue’s Composition API and a clear separation between kinds of state. I extracted logic into composables so components stayed thin and the behavior was reusable and testable on its own. Examples from my work include composables that back chart configuration, sticky/layout offsets and target sizing, section navigation, form handling, and read-only data views:
// Component stays declarative; the composable owns the logic.
const { config } = useDsLineChart();
const { offsets } = useLayoutOffsets();
const { activeSection, jumpTo } = useJumpToSection();For sharing data across components I followed a “right tool for each data type” approach rather than reaching for one global store:
- Server data via TanStack Query (cached, deduped, auto-refetched) fetching students, interventions, and reporting data through query composables like
useInterventionDetailsQuery.
- UI state with local
ref/reactivewhen component-scoped, and a shared store (Pinia) only when genuinely cross-component.
- Route/session data read through the router and context composables instead of prop-drilling or global variables.
The composable-first, headless pattern also carried into the design system: my distribution visualization uses a provide/inject context so child components (DSDistributionItem, DSDistributionHeader, and friends) register themselves with the parent through a shared reactive store, keeping the public API simple while the state lives in one place.
Application components (product-level)
Beyond the design system, I built many feature components inside the product app itself, composing the shared primitives into real workflows:
- Progress-monitoring and intervention charts — Chart sections and per-goal progress views, plus the info modals that explain how to read them, for tracking student outcomes over time.
- Social-emotional learning (SEL) experience — A set of components for browsing and reviewing SEL data: topic cards and headers, category and choice headers, read-only choice views, respondent and benchmark selectors (with their info modals), and improvement suggestions.
- Attendance and behavior views — Absences-and-tardies and incident-detail views, plus tables for incident data from different perspectives.
- Chart wrappers — App-level bar and line chart components (including Highcharts-backed ones) that adapt the reporting data to the shared chart primitives.
- Navigation and page chrome — A sticky header for the student profile with its action controls, plus “jump to section” navigation for moving quickly around long, section-heavy pages.
What I Bring
- Component APIs that are flexible without being confusing.
- A bias toward reuse and leverage: fix it once, in the right layer.
- Accessibility treated as a requirement, not an afterthought.
- Comfort shipping large work incrementally and reviewably.







