diff --git a/CLAUDE.md b/CLAUDE.md index 76b9c493..9d0be912 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -183,6 +183,17 @@ categories: [development, design] ### Page Motion (opt-in) Add `motion: true` to a page's front matter to load `src/js/motion.js`. On `page.njk` and `markdown.njk` layouts this fades in the page header and reveals top-level content sections (card grids stagger card by card) as they scroll into view. Pages on `base.njk` can mark elements directly with `data-reveal`, `data-reveal-group`, or `data-reveal-auto`. All motion is skipped for `prefers-reduced-motion`, and content stays visible without JS. The topographic background (`src/sass/_topo.scss`, `src/js/topo.js`) applies sitewide; regenerate its SVG with `node scripts/generate-topo.js [seed]`. +### Trail Progress (opt-in) +Pages on `base.njk` add a `camps` list to front matter to show the trail progress system: camp ticks (real anchor links), a pinned rail hiker on wide screens, and a progress track plus tick bar on small screens. Each `id` must match a section on the page, and the first camp is the trailhead. + +```yaml +camps: + - { id: top, label: Trailhead } + - { id: work, label: Work } +``` + +`src/_includes/trail-progress.njk` renders the markup, `src/js/trail.js` moves the hiker and marks the active camp (it keeps running under `prefers-reduced-motion`, stepping between camps), and `src/sass/_trail.scss` holds the styles. The home page also reads `meta.email`; the "Email me" button stays hidden until it is set. + ### Modify Site Navigation Edit `eleventyNavigation` in page front matter: - `key`: Unique identifier diff --git a/src/_data/meta.js b/src/_data/meta.js index b233ddb1..704d1f91 100644 --- a/src/_data/meta.js +++ b/src/_data/meta.js @@ -2,6 +2,7 @@ module.exports = { env: process.env.ELEVENTY_ENV, url: process.env.URL || "http://localhost:8080", siteName: "Adam Jolicoeur", + email: "", // contact address for the "Email me" button; the button stays hidden until this is set siteDescription: "Product designer building AI-native products, from interaction patterns to the systems that govern them.", siteURL: "https://www.adamjolicoeur.com", authorName: "Adam J. Jolicoeur", diff --git a/src/_includes/base.njk b/src/_includes/base.njk index 480173cf..12a85ae6 100644 --- a/src/_includes/base.njk +++ b/src/_includes/base.njk @@ -48,7 +48,7 @@ - + + {%- if camps %} + {% include 'trail-progress.njk' %} + {%- endif %}
{{ content | safe }}
@@ -116,5 +119,8 @@ {%- if motion %} {%- endif %} + {%- if camps %} + + {%- endif %} diff --git a/src/_includes/case-study.njk b/src/_includes/case-study.njk new file mode 100644 index 00000000..9ea4cb27 --- /dev/null +++ b/src/_includes/case-study.njk @@ -0,0 +1,40 @@ +--- +layout: base.njk +--- + +{#- Six-step case study. Front matter: eyebrow, headline, deck, heroImage, heroAlt, facts [{label,value}], results [{value,label}], confidential, camps. + The body holds the step sections:
. Camps drive the trail progress ticks. -#} +{%- if heroImage %} +
+
+ {{ heroAlt }} +
+
+{%- endif %} + +
+

{{ eyebrow }}

+

{{ headline }}

+

{{ deck }}

+ {%- if facts %} +
+ {%- for fact in facts %} +
{{ fact.label }}
{{ fact.value }}
+ {%- endfor %} +
+ {%- endif %} + {%- if results %} +
    + {%- for r in results %} +
  • {{ r.value }}{{ r.label }}
  • + {%- endfor %} +
+ {%- endif %} + {%- if confidential %}

Some details are omitted for confidentiality.

{%- endif %} +
+ + + {{ content | safe }} + + +{% include "case-study-nav.njk" %} diff --git a/src/_includes/trail-progress.njk b/src/_includes/trail-progress.njk new file mode 100644 index 00000000..e1cea138 --- /dev/null +++ b/src/_includes/trail-progress.njk @@ -0,0 +1,37 @@ +{#- Trail progress: camp ticks (real anchor links), a pinned rail hiker on desktop, and a progress track on phones. + Opted into with a `camps` list in front matter: [{ id: 'work', label: 'Work' }, ...]. The first camp is the trailhead. + js/trail.js adds the moving parts; without JS the ticks are plain links. -#} +{%- set lastCamp = camps.length - 1 -%} +{%- set hikerPath -%} + +{%- endset -%} + + +
+ + + {{ hikerPath | safe }} +
diff --git a/src/assets/img/archparser_dashboard1-thumb.webp b/src/assets/img/archparser_dashboard1-thumb.webp index 708d9ecc..95550a4f 100644 Binary files a/src/assets/img/archparser_dashboard1-thumb.webp and b/src/assets/img/archparser_dashboard1-thumb.webp differ diff --git a/src/assets/img/archparser_dashboard1.webp b/src/assets/img/archparser_dashboard1.webp index df115542..a66ceda2 100644 Binary files a/src/assets/img/archparser_dashboard1.webp and b/src/assets/img/archparser_dashboard1.webp differ diff --git a/src/assets/img/archparser_drawing-details-thumb.webp b/src/assets/img/archparser_drawing-details-thumb.webp index e6831817..7420513f 100644 Binary files a/src/assets/img/archparser_drawing-details-thumb.webp and b/src/assets/img/archparser_drawing-details-thumb.webp differ diff --git a/src/assets/img/archparser_drawing-details.webp b/src/assets/img/archparser_drawing-details.webp index 46c39fed..f130e003 100644 Binary files a/src/assets/img/archparser_drawing-details.webp and b/src/assets/img/archparser_drawing-details.webp differ diff --git a/src/assets/img/archparser_drawing-set-details-thumb.webp b/src/assets/img/archparser_drawing-set-details-thumb.webp index eef74981..209051ac 100644 Binary files a/src/assets/img/archparser_drawing-set-details-thumb.webp and b/src/assets/img/archparser_drawing-set-details-thumb.webp differ diff --git a/src/assets/img/archparser_drawing-set-details.webp b/src/assets/img/archparser_drawing-set-details.webp index 0845bcb5..de7d78e3 100644 Binary files a/src/assets/img/archparser_drawing-set-details.webp and b/src/assets/img/archparser_drawing-set-details.webp differ diff --git a/src/assets/img/archparser_template-creator-loaded-thumb.webp b/src/assets/img/archparser_template-creator-loaded-thumb.webp index 1c7e0501..7db0b53b 100644 Binary files a/src/assets/img/archparser_template-creator-loaded-thumb.webp and b/src/assets/img/archparser_template-creator-loaded-thumb.webp differ diff --git a/src/assets/img/archparser_template-creator-loaded.webp b/src/assets/img/archparser_template-creator-loaded.webp index 66a3abe2..78f9bede 100644 Binary files a/src/assets/img/archparser_template-creator-loaded.webp and b/src/assets/img/archparser_template-creator-loaded.webp differ diff --git a/src/assets/img/casim-architecture.svg b/src/assets/img/casim-architecture.svg new file mode 100644 index 00000000..8a48dd64 --- /dev/null +++ b/src/assets/img/casim-architecture.svg @@ -0,0 +1 @@ +Three clients connect to one MCP server, which gates access to four project data sourcesCLIENTSONE MCP SERVERPROJECT DATAClaude DesktopMCP InspectorCustom interfaceTypeScript MCP serverGated connectors and data scopeBudgetsWork ordersChange ordersSupplies \ No newline at end of file diff --git a/src/assets/img/casim-naming-convention.svg b/src/assets/img/casim-naming-convention.svg new file mode 100644 index 00000000..b4080f3e --- /dev/null +++ b/src/assets/img/casim-naming-convention.svg @@ -0,0 +1 @@ +Three different spellings of one supply map to a single conventionBEFOREAFTER, ONE CONVENTION2x4 StudSTUD, 2 X 4Stud 2in x 4inStud, 2x4Illustrative examples, not real data \ No newline at end of file diff --git a/src/index.md b/src/index.md index 74d6464e..3bf53547 100644 --- a/src/index.md +++ b/src/index.md @@ -1,16 +1,23 @@ --- layout: base.njk motion: true +camps: + - { id: top, label: Trailhead } + - { id: work, label: Work } + - { id: principles, label: How I work } + - { id: proof, label: Proof } + - { id: about, label: About } + - { id: summit, label: Summit } --- -
+
{% include 'hero-scene.njk' %}
-

Adam Jolicoeur

-

Product designer building AI-native products, from interaction patterns to the systems that govern them. 15+ years at AWS, Red Hat, and high-growth B2B.

-

Enterprise product designer. Systems thinker. Builder.

+

Adam Jolicoeur · Product designer

+

I design how people review, trust, and correct what AI does.

+

Product designer with 15 years in enterprise software at AWS and Red Hat, now designing AI assistants for enterprise teams.

- + Start the hike @@ -19,191 +26,115 @@ motion: true
-
-

About Me

-
-
-

Lead Product designer building AI-native products, from interaction patterns to the systems that govern them.

-

15+ years across AWS, Red Hat, and high-growth B2B, including an internal AI assistant, MCP architecture, and design systems used on 100+ construction projects.

-

- What makes me different: -

    -
  • I code production-quality prototypes: I build functional prototypes in HTML/CSS/React, which means my designs are validated with real data and ready for engineering handoff. This bridges the gap between design and development, accelerating shipping and reducing costly miscommunication.
  • -
  • I think like a PM: I've managed product roadmaps, prioritized features, and led cross-functional teams. This dual perspective helps me balance user needs with business objectives and collaborate effectively with product managers and stakeholders.
  • -
  • Former PatternFly core contributor: I was a core contributor to PatternFly (2016-2021), Red Hat's open-source design system with 20,000+ GitHub stars, used across OpenShift, Ansible, and 50+ products serving millions of developers worldwide.
  • -
-

-

- Currently: Lead Product Designer at Component Assembly Systems -
- Previously: Amazon Web Services, Red Hat, and high-growth B2B companies -

-
- -
-
-
-

Featured Work

-
-
- CASim dashboard screen -
-

CASim: Internal AI Assistant

-

Designed and architected an internal AI assistant, built on a custom MCP server, that makes years of inconsistent construction data answerable. Currently in UAT and beta testing.

-
- AIMCPEnterprise -
- View Case Study + +
+

Camp 1 · Selected work

+

Three projects, from AI to enterprise scale

+
+ -
- Weekly AI-powered work summary loading screen -
-

Weekly AI-Powered Work Summary

-

After archiving my work entries for the week, I found myself running a second, separate tool to generate a weekly summary. Now, I start the day, capture tasks, review, and archive, then summarize using AI.

-
- AIProductivity -
- View Case Study -
-
-
-
-
-
-

ArchParser

-

Architectural drawing analysis platform for OCR-based architectural drawing analysis management.

-
- DevelopmentClaude Code -
- View Case Study -
-
-

Task Management System

-

Lead designer for Task-it, Amazon's internal task management platform. Consolidated 3-4 disparate tracking tools into one unified system, reaching 500 daily active users across AWS within 18 months of launch.

-
- AWS500+ UsersEnterprise -
- View Case Study -
-
-

Component Library

-

Built comprehensive design system for construction management software spanning web and iOS applications. Created design tokens, component documentation, and prototypes that accelerated development cycles by 25%.

-
- LibraryDesign Tokens -
- View Case Study -
-
-

Application Lifecycle Management

-

Designed enterprise project management for OpenShift teams at Red Hat. Created seamless IDE integration and workflow tools for developers managing complex Kubernetes deployments.

-
- Red HatPlatforms -
- View Case Study -
+
+
+
-
-
-

What Colleagues Say

-
-
-

[Adam] is self-driven and keeps stakeholders (Project manager, Development manager, Engineers) aligned on UX solutions...design proposals are spot-on and considerate of [our] diverse set of users.

- - Product Owner - -
-
-

Adam is highly engaged across 6+ major features at any given time and is somehow never the blocker. Designers like this don't come around often.

- - Senior Developer Engineer, AWS - -
-
-

...What stood out to me while working with Adam was his positivity and confidence through every obstacle, pivot, and deadline. He is a true team player.

- - Front-End Engineer - -
+
-
-

Open Source Contributions

-
-

Beyond my professional work, I actively contribute to the open source community. My contributions span developer tools, design systems, and infrastructure projects that support the broader tech ecosystem.

-
-
-

- ๐Ÿš€ - Tools and Utilities -

-

- Developed and maintained several open source developer tools that enhance productivity and streamline workflows for both designers and software engineers. -

-
- Tooling - DX -
- Timetraked -
-
-

- ๐Ÿ”ง - PatternFly -

-

- Core contributor to Red Hat's professional design system (2016-2021). Designed accessible, enterprise-grade components for the open-source system now used across OpenShift, Ansible, and 50+ enterprise products (20,000+ GitHub stars). -

-
- HTML - CSS - JavaScript - A11y -
- -
-
-

- ๐ŸŽฎ - Apps and Games -

-

- Created several open source applications and games that showcase innovative use of web technologies, focusing on user engagement and interactive experiences. -

-
- Tooling - DX - Games - Apps -
- Game Tracker -
-
-

- ๐Ÿ“š - Documentation -

-

Regular contributor to open source documentation, helping make complex technical concepts more accessible to developers.

-
- Documentation - Community -
- -
-
-
- - View GitHub Profile + +
+

Camp 2 · How I work

+

Gate the connectors and data scope, not the creativity.

+
    +
  • Show the work

    People trust an answer when they can see how it was produced.

  • +
  • Make correcting cheap

    A wrong answer is survivable when fixing it takes seconds and the fix sticks.

  • +
  • Ask, don't hunt

    Users should state what they need, not learn where the data lives.

  • +
+
+ +
+

Camp 3 · Proof

+

Outcomes

+
+
+ +
+

Camp 4 · About and Brimfield Labs

+
+
+

I started by fixing technology, then wanted to build it.

+

I began on a help desk, moved into software engineering, and found UX design at a startup. That path is why I still ship working prototypes alongside the designs, now in Claude Code every day. I was a core contributor to PatternFly at Red Hat and designed Task-it at AWS. Outside of work I build local-first apps as Brimfield Labs, because I like software that keeps your data yours.

+ +

More about me

+ +
+

Summit · Contact

+

Building AI people can trust? Let’s talk.

+

+ {%- if meta.email != "" %} + Email me + {%- endif %} + Download resume + Back to trailhead ↑ +

+
diff --git a/src/js/motion.js b/src/js/motion.js index ba2879ce..48c103ee 100644 --- a/src/js/motion.js +++ b/src/js/motion.js @@ -71,6 +71,7 @@ hiker.classList.add('is-walking'); window.requestAnimationFrame(walk); }); + // Once the hiker reaches the summit he stays there; scrolling never resets the climb reveal.addEventListener('animationend', function () { walking = false; hiker.classList.remove('is-walking'); @@ -80,6 +81,106 @@ } } + // Home camp effects: quote words, count-up numbers, tile tilt, stacked cards + var frame = function (fn) { + var queued = false; + return function () { + if (queued) return; + queued = true; + window.requestAnimationFrame(function () { queued = false; fn(); }); + }; + }; + + // Quote words move from muted to full ink as the block crosses the viewport center + var quote = document.querySelector('.principle-quote'); + if (quote) { + var words = quote.textContent.trim().split(/\s+/); + quote.setAttribute('aria-label', words.join(' ')); + quote.textContent = ''; + var spans = words.map(function (word, i) { + var span = document.createElement('span'); + span.className = 'quote-word'; + span.setAttribute('aria-hidden', 'true'); + span.textContent = word + (i < words.length - 1 ? ' ' : ''); + quote.appendChild(span); + return span; + }); + var paintQuote = function () { + var rect = quote.getBoundingClientRect(); + var vh = window.innerHeight; + // 0 when the block enters near the bottom, 1 once it passes the center + var p = Math.min(Math.max((vh * 0.9 - rect.top) / (vh * 0.9 - vh * 0.45), 0), 1); + spans.forEach(function (span, i) { + span.classList.toggle('is-lit', p * spans.length > i); + }); + }; + window.addEventListener('scroll', frame(paintQuote), { passive: true }); + window.addEventListener('resize', frame(paintQuote)); + paintQuote(); + } + + // Each proof number counts up once when it first scrolls into view + var counters = Array.prototype.slice.call(document.querySelectorAll('.proof-number')); + var countUp = function (el) { + var match = el.textContent.trim().match(/^(\d+)(.*)$/); + if (!match) return; + var target = parseInt(match[1], 10); + var suffix = match[2]; + var start = null; + var step = function (now) { + if (start === null) start = now; + var t = Math.min((now - start) / 600, 1); + el.textContent = Math.round(target * (1 - Math.pow(1 - t, 3))) + suffix; + if (t < 1) window.requestAnimationFrame(step); + }; + el.textContent = '0' + suffix; + window.requestAnimationFrame(step); + }; + if (counters.length && 'IntersectionObserver' in window) { + var countObserver = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (!entry.isIntersecting) return; + countObserver.unobserve(entry.target); + countUp(entry.target); + }); + }, { threshold: 0.6 }); + counters.forEach(function (el) { countObserver.observe(el); }); + } + + // App tiles tilt up to 4 degrees toward the pointer (not for keyboard focus) + if (window.matchMedia('(hover: hover)').matches) { + document.querySelectorAll('.app-tile').forEach(function (tile) { + tile.addEventListener('pointermove', function (event) { + var rect = tile.getBoundingClientRect(); + var x = (event.clientX - rect.left) / rect.width - 0.5; + var y = (event.clientY - rect.top) / rect.height - 0.5; + tile.style.transform = 'perspective(600px) rotateX(' + (-y * 8).toFixed(2) + 'deg) rotateY(' + (x * 8).toFixed(2) + 'deg)'; + }); + tile.addEventListener('pointerleave', function () { tile.style.transform = ''; }); + }); + } + + // Pinned work cards scale and dim a little as the next card arrives + var cards = Array.prototype.slice.call(document.querySelectorAll('.stack-card')); + if (cards.length > 1) { + var paintStack = function () { + if (window.matchMedia('(width <= 900px)').matches) { + cards.forEach(function (card) { card.style.setProperty('--stack-p', '0'); }); + return; + } + cards.forEach(function (card, i) { + var next = cards[i + 1]; + if (!next) return; + var distance = next.getBoundingClientRect().top - card.getBoundingClientRect().top; + var p = 1 - Math.min(Math.max(distance / card.offsetHeight, 0), 1); + card.style.setProperty('--stack-p', p.toFixed(3)); + }); + }; + window.addEventListener('scroll', frame(paintStack), { passive: true }); + window.addEventListener('resize', frame(paintStack)); + paintStack(); + } + // Scroll reveals: single elements and staggered groups if (!('IntersectionObserver' in window)) return; diff --git a/src/js/trail.js b/src/js/trail.js new file mode 100644 index 00000000..39b00fe1 --- /dev/null +++ b/src/js/trail.js @@ -0,0 +1,91 @@ +/* +** Trail progress, opted into with a `camps` list in front matter. +** Page progress (0 to 1) moves the rail hiker on desktop and the track hiker on phones, +** and the camp ticks mark where you are. Under prefers-reduced-motion the hiker steps +** between camps instead of tracking every scroll frame. Without JS the ticks are plain links. +*/ +(function () { + var ticks = document.querySelector('.trail-ticks'); + if (!ticks) return; + + var root = document.documentElement; + var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); + var links = Array.prototype.slice.call(ticks.querySelectorAll('a[data-camp]')); + var camps = links + .map(function (link) { return document.getElementById(link.getAttribute('data-camp')); }) + .filter(Boolean); + if (camps.length < 2) return; + + var count = ticks.querySelector('[data-trail-count]'); + var last = links.length - 1; + var track = document.querySelector('[data-trail-track]'); + var footer = document.querySelector('footer'); + var targets = [ticks, document.querySelector('[data-trail-rail]'), track]; + var active = -1; + var progress = -1; + var ticking = false; + + root.classList.add('js-trail'); + + // Progress runs from the top to the moment the last camp reaches the active line + var pageProgress = function () { + var last = camps[camps.length - 1]; + var range = last.getBoundingClientRect().top + window.scrollY - window.innerHeight * 0.4; + return range > 0 ? Math.min(Math.max(window.scrollY / range, 0), 1) : 0; + }; + + // The active camp is the last one whose top has passed 40% of the viewport + var activeCamp = function () { + var line = window.innerHeight * 0.4; + var index = 0; + camps.forEach(function (camp, i) { + if (camp.getBoundingClientRect().top <= line) index = i; + }); + return index; + }; + + var setProgress = function (value) { + if (value === progress) return; + progress = value; + targets.forEach(function (el) { + if (el) el.style.setProperty('--trail-p', value.toFixed(4)); + }); + if (track) track.setAttribute('aria-valuenow', String(Math.round(value * 100))); + }; + + var update = function () { + ticking = false; + var index = activeCamp(); + var p = pageProgress(); + + // Reduced motion: the hiker steps to each camp, with no tracking between them + setProgress(reduceMotion.matches ? index / last : p); + + if (index !== active) { + active = index; + links.forEach(function (link, i) { + link.classList.toggle('is-past', i < index); + if (i === index) link.setAttribute('aria-current', 'location'); + else link.removeAttribute('aria-current'); + }); + if (count) count.textContent = 'Camp ' + index + ' / ' + last; + } + + // The rail fades in as the hero leaves; the flag plants at the summit + var footerShowing = footer && footer.getBoundingClientRect().top < window.innerHeight * 0.75; + root.classList.toggle('trail-rail-on', window.scrollY > window.innerHeight * 0.35 && !footerShowing); + root.classList.toggle('trail-at-summit', index === last || p >= 0.985); + }; + + var request = function () { + if (!ticking) { + ticking = true; + window.requestAnimationFrame(update); + } + }; + + window.addEventListener('scroll', request, { passive: true }); + window.addEventListener('resize', request); + if (reduceMotion.addEventListener) reduceMotion.addEventListener('change', request); + update(); +})(); diff --git a/src/pages/portfolio/alm.md b/src/pages/portfolio/alm.md index 833f5df6..97bf7eea 100644 --- a/src/pages/portfolio/alm.md +++ b/src/pages/portfolio/alm.md @@ -14,238 +14,112 @@ eleventyNavigation: key: Designs parent: Designs order: 2 +layout: case-study.njk +motion: true +eyebrow: 'Case study ยท Developer tools' +headline: 'Developers switched tools 20 or more times a day just to manage work items, so I designed tracking that lives inside the IDE.' +deck: 'Application Lifecycle Management (ALM) brought unified work item tracking into the OpenShift.io IDE.' +heroImage: /assets/img/ALM_planner.webp +heroAlt: 'The ALM planner design' +facts: + - { label: Role, value: 'Senior interaction designer, Red Hat' } + - { label: Timeline, value: '2017 to 2019' } + - { label: Worked with, value: '2 product managers, 3 engineers, 2 designers, and the PatternFly design system team' } + - { label: Status, value: 'Launched as a core feature of OpenShift.io in 2018' } +results: + - { value: '40%', label: 'Reduction in average work item management time through IDE integration' } + - { value: '2018', label: 'Launched as a core feature of OpenShift.io' } + - { value: '6 months', label: 'To adoption by multiple development teams' } +camps: + - { id: top, label: Trailhead } + - { id: context, label: Context } + - { id: research, label: Research } + - { id: decisions, label: Key decisions } + - { id: final, label: Final design } + - { id: impact, label: Impact } + - { id: learned, label: What I learned } --- - -
-

Project Overview

-
-

- Company:Red Hat, Inc. -

-

- Role:Senior Interaction Designer -

-

- Timeline:2017-2019 -

-

- Team: - 2 Product Managers, 3 Engineers, 2 Designers, PatternFly Design System team -

-

- THE CHALLENGE: - OpenShift developers were managing work items across 3+ disconnected tools (GitHub issues, Jira, internal tracking systems). This forced constant context-switching between their IDE, browser tabs, and project management tools, slowing development cycles, breaking focus, and creating visibility gaps for project managers. -

- The fragmentation was especially painful because developers had to: -

    -
  • Leave their IDE to check work item status
  • -
  • Manually link code commits to work items across multiple systems
  • -
  • Update work items in multiple places to keep teams synchronized
  • -
  • Context-switch an average of 20+ times per day just for task management
  • -
-

-

-

- MY ROLE: - A lead designer responsible for creating a unified application lifecycle management tool that would integrate directly into the IDE, eliminating context-switching and improving developer productivity. -

-

- THE SOLUTION: - I was part of a team that designed Application Lifecycle Management (ALM), a unified work item tracking system that integrated directly into the OpenShift.io IDE. The design focused on eliminating context-switching by bringing work item management into the developer's natural workflow, allowing them to create, update, and track work items without leaving their code editor. -

Key innovations included: -

    -
  • Seamless IDE integration with inline work item creation and updates
  • -
  • Automatic linking between code commits and work items
  • -
  • Real-time synchronization across team members
  • -
  • Flexible categorization supporting multiple team workflows
  • -
-

-

- THE IMPACT: -
-

    -
  • Integrated work item management directly into OpenShift developer workflow/li> -
  • Eliminated context-switching between IDE, browser, and project management tools
  • -
  • Created seamless connection between code commits and work items
  • -
  • Reduced average work item management time by 40% through IDE integration
  • -
  • Established design patterns adopted across other Red Hat developer tools
  • -
-

-
-
-
-

The Process

-

- Before starting on the Application Lifecycle Management project, it was determined that the following areas would be need to be covered: -

- list of target users - a user flow - wireframes - prototypes for user testings and feasibility -
-

+
+

Step 1 · Context

+

OpenShift developers managed work across 3 or more disconnected tools, and every switch broke their focus.

+

Work items lived in GitHub issues, Jira, and internal tracking systems. Developers moved between their IDE, browser tabs, and project management tools, which slowed development and left project managers with visibility gaps.

+
    +
  • Leave the IDE to check work item status
  • +
  • Manually link code commits to work items across several systems
  • +
  • Update work items in multiple places to keep teams in sync
  • +
  • Context-switch an average of 20 or more times a day for task management
  • +
+
Opportunity

How might developers create, update, and track work without leaving their code editor?

-
-

Target Users

-
-
-

- Through discussions with Stakeholders and Project Management, it was decided that the target users would be set in tiers: -

    -
  1. - General Users
      -
    • those who need to add and interact with issues, update project assets, and interact daily, a mix of designers and developers
    • -
    -
  2. -
  3. - Project Managers -
      -
    • organizers of work, set deadlines and release dates -
    • -
    -
  4. -
  5. - Administrators -
      -
    • control any integrations, add-ons, and access
    • -
    -
  6. -
-

-
-
-
-
-

Application Lifecycle Management flow

-
-
User flows
-
- ALM workflow -
-
+ +
+

Step 2 · Research

+

Three tiers of users, and four things to settle before any visual design.

+

With stakeholders and project management, we set the target users in tiers. Before starting, we agreed to cover a list of target users, a user flow, wireframes, and prototypes for user testing and feasibility.

+
+

General users

Add and interact with issues, update project assets, and work in the tool daily. A mix of designers and developers.

+

Project managers

Organize work and set deadlines and release dates.

+

Administrators

Control integrations, add-ons, and access.

+
+
The ALM user flow
User flows
-
-

Wireframes and Workflows

-

- For each section of the ALM application, workflows and wireframes were used to identify possible user paths, as well as to strategize initial designs before moving on to high fidelity mockups and prototypes. -

-
- -

From Work Items to Development

- -
-
-
Goals:
-
    -
  • Communicates to the user that direct access to developing the Work Item is available in a single click.
  • -
  • Implementation is unobtrusive and implies navigation.
  • -
  • Location will not accidentally be clicked, allowing for enough real estate for line lengths and other meta data. The location of the integration should also imply moving forward.
  • -
  • Ordered by recommendation, require users to fill in one area before moving on.
  • -
  • As a button, it needs to have a prominent place yet not take the place of a primary button (in style).
  • -
-
-

Steps to complete the process

-
-
-
- Step 1 of the ALM wireframes -
-
-
-
- Step 2 of the ALM wireframes -
-
-
-
- Step 3 of the ALM wireframes -
-
-
+ +
+

Step 3 · Key decisions

+

Moving from a work item to code had to take one click, feel unobtrusive, and never replace the primary button.

+

For each section of ALM, workflows and wireframes mapped the possible paths before high fidelity mockups. For the path from work items to development, the goals were:

+
    +
  • Tell users that direct access to developing the work item is one click away
  • +
  • Keep the implementation unobtrusive and imply navigation
  • +
  • Place it where it will not be clicked by accident, with room for line lengths and metadata, and imply moving forward
  • +
  • Order by recommendation and require users to fill in one area before moving on
  • +
  • As a button, give it a prominent place without taking over from a primary button
  • +
+
+
Step 1 of the ALM wireframes
Step 1
+
Step 2 of the ALM wireframes
Step 2
+
Step 3 of the ALM wireframes
Step 3
+
+

I annotated the wireframe with call-outs so anyone could follow my thinking without talking to me. That worked especially well with colleagues in other time zones.

+
Wireframe of the work item to coding experience
+

The flows and wireframes raised open questions:

+
    +
  • How do we keep linking between work items accurate?
  • +
  • If a work item has a file name and line number, is the commit SHA also used to identify the connection?
  • +
  • Can a user type the URL into a browser and reach the work item, or is it generated each time?
  • +
-
-

Wireframes

-

This wireframe was created with call outs in order to communicate my thoughts around the design to anyone who saw it, but could not talk with me. Specifically, this worked extremely well when working with colleagues in other time zones where our working ours did not match up.

-
-
- Wireframe of the work item to coding experience -
-
-
-

From these workflows and wireframes, it was determined that additional questions needed to be answered: -

    -
  1. How do we handle accuracy between the linking of work items?
  2. -
  3. If the work items has a fine name and line number, is the commit SHA also used to help identify the connection?
  4. -
  5. Are we able to type the URL in a browser and navigate to the work item? (Does it have a unique link or is it randomly generated each time?)
  6. -
-

-
-

Visual Design

-

After completing the wireframes and flows, a sample visual design was created. This design was created using the in-house design component library.

-
-
- ALM workflow -
-
+ +
+

Step 4 · Final design

+

A unified tracker inside the IDE, built with the in-house component library.

+

I created a visual design with the in-house component library after the flows and wireframes were complete. The key ideas were:

+
    +
  • Seamless IDE integration with inline work item creation and updates
  • +
  • Automatic linking between code commits and work items
  • +
  • Real-time synchronization across team members
  • +
  • Flexible categorization supporting multiple team workflows
  • +
+
The ALM planner visual design
-
-

Results & Impact

-

Adoption & Launch

-

-

    -
  • Launched as core feature of OpenShift.io in 2018
  • -
  • Adopted by multiple development teams within first 6 months
  • -
  • Integrated with GitHub, reducing average work item management time
  • -
  • Expanded to support multiple project management methodologies (Agile, Kanban, custom workflows)
  • -
-

-

User Feedback

-

-

    -
  • Developers consistently praised the IDE integration for eliminating context-switching
  • -
  • Post-launch surveys showed positive feedback
  • -
  • Reduced reported frustration with task management from "major pain point" to "barely noticeable"
  • -
-

-

Business Impact

-

-

    -
  • Improved cross-team visibility into development progress and project status
  • -
  • Reduced time spent on administrative task management, allowing developers to focus on coding
  • -
  • Design patterns and PatternFly components I created were adopted across other Red Hat developer tools
  • -
  • Demonstrated value of deeply understanding developer workflows before designing tools for them
  • -
-

+ +
+

Step 5 · Impact

+

Launched in 2018, adopted by several teams in six months, and reused across other Red Hat developer tools.

+
+

Adoption and launch

Launched as a core feature of OpenShift.io in 2018, integrated with GitHub, and extended to support Agile, Kanban, and custom workflows.

+

User feedback

Developers praised the IDE integration for removing context-switching. Reported frustration moved from “major pain point” to “barely noticeable”.

+

Business impact

Better visibility into progress and status, less time on administrative task management, and patterns and PatternFly components adopted across other Red Hat developer tools.

+
- -
-
-
-

What I Learned

-
-
-

- This project taught me invaluable lessons about designing for developers: -

    -
  1. Understand the workflow before designing the tool. By spending weeks observing OpenShift developers and understanding their daily routines, I identified friction points that weren't immediately obvious, like the cognitive cost of switching between tools and the importance of maintaining flow state.
  2. -
  3. The best developer tools are invisible. Developers want tools that integrate seamlessly into existing workflows rather than requiring them to adapt to new processes. ALM succeeded because it met developers where they already were, in their IDE, rather than forcing them to go elsewhere.
  4. -
  5. Developer experience is user experience. Too often, internal developer tools are treated as purely functional without considering the human experience. By applying rigorous UX research and design thinking to ALM, we created a tool that developers actually enjoyed using.
  6. -
  7. Working on ALM helped me identify gaps in PatternFly for developer tool use cases, leading to new components and patterns that benefited the entire Red Hat ecosystem.
  8. -
-

-

- The success of ALM reinforced that internal tools and developer experiences deserve the same level of design craft and user research as consumer products, and that investing in that craft pays dividends in productivity, satisfaction, and adoption. -

-
-
+ +
+

Step 6 · What I learned

+

Understand the workflow first, make the tool invisible, and treat developer experience as user experience.

+
+

Understand the workflow before designing the tool

Weeks observing OpenShift developers surfaced friction I would not have guessed, like the cost of switching tools and protecting flow state.

+

The best developer tools are invisible

ALM worked because it met developers in their IDE instead of asking them to go elsewhere.

+

Developer experience is user experience

Rigorous research and design made a tool developers enjoyed using.

+

PatternFly had gaps

Working on ALM exposed gaps for developer tool use cases and led to new components and patterns for the whole ecosystem.

+
diff --git a/src/pages/portfolio/archparser.md b/src/pages/portfolio/archparser.md index 8e984153..030ffcd4 100644 --- a/src/pages/portfolio/archparser.md +++ b/src/pages/portfolio/archparser.md @@ -3,7 +3,7 @@ title: 'ArchParser: Architectural Drawing Analysis Platform' portfolioOrder: 3 card: title: "ArchParser" - summary: "Architectural drawing analysis platform for OCR-based architectural drawing management." + summary: "The only designer, researcher, and developer on a tool that turns 500+ page drawing sets into structured metadata. Non-technical users build their own OCR templates, and every extraction shows a confidence score." image: "/assets/img/archparser_dashboard1.webp" imageAlt: "ArchParser dashboard" badges: ["Development", "Claude Code"] @@ -16,181 +16,114 @@ eleventyNavigation: key: Development parent: Development order: 1 -customCss: slides.css + +layout: case-study.njk +motion: true +eyebrow: 'Case study ยท OCR and document AI' +headline: 'Project managers hand-copied metadata from 500+ page drawing sets, so I built a tool that reads it for them.' +deck: 'ArchParser extracts drawing numbers, titles, dates, and revisions from architectural PDFs, even when every firm lays out its title block differently.' +heroImage: /assets/img/archparser_dashboard1.webp +heroAlt: 'ArchParser dashboard showing processed drawings across several jobs' +confidential: true +facts: + - { label: Role, value: 'Sole product designer, researcher, and full-stack developer' } + - { label: Timeline, value: '6 to 8 months, kickoff to demo-ready' } + - { label: Worked with, value: 'Solo, with no second designer, researcher, or developer. Project planners and managers at Component Assembly Systems, with Claude Code as a development partner' } + - { label: Status, value: 'Demo-ready, built to test and deploy' } +results: + - { value: '200+', label: 'Drawings processed in demonstrations' } + - { value: '4 to 5 hrs', label: 'Estimated savings per drawing set, an estimate from users and not a measured result' } + - { value: Ready, label: 'To test and deploy on a single source of information' } +camps: + - { id: top, label: Trailhead } + - { id: context, label: Context } + - { id: research, label: Research } + - { id: decisions, label: Key decisions } + - { id: final, label: Final design } + - { id: impact, label: Impact } + - { id: learned, label: What I learned } + --- -
-

Project Overview

-
-

- Role:Product Designer & Full-Stack Developer -

-

- Type:Personal Project (originally intended as a CAS tool) -

-

- Stack:TypeScript monorepo, NestJS, React, RadixUI, MySQL, Tesseract.js, built with Claude Code -

-

- Status:Demo-ready, paused while higher-priority CAS projects took precedence -

-
-

Construction project managers at Component Assembly Systems needed to pull metadata from hundreds of architectural drawing PDFs. Entering drawing numbers, revision dates, and project names from title blocks by hand was slow, error-prone, and impossible to sustain on 500+ page sets, and no existing tool handled custom title block layouts across different architectural firms. I interviewed project planners and managers about their workflows, then designed and built ArchParser to automate the extraction.

-

ArchParser is a TypeScript monorepo with a NestJS backend for OCR processing and a React frontend for managing drawings. Demonstrations have processed over 200 drawings, and users of the current manual process estimated it would save roughly half a day (4 to 5 hours) of manual work per drawing set. That figure is an estimate, not a measured result.

-
    -
  • Processes 500+ page PDF drawing sets with memory-optimized batch processing
  • -
  • A visual template creator lets non-technical users configure extraction, so projects don't wait on a developer
  • -
  • Each extraction shows a confidence score out of 100, so users can proceed with high-confidence results and review the rest
  • -
  • Revision tracking and real-time progress updates support long-running jobs on large drawing sets
  • -
-
-
-

Key Design Decisions

-

The technical work is summarized further down, but the choices that shaped how people actually use ArchParser were design decisions. Four mattered most.

-
-
-

1. Let users build templates, not developers

-

Every OCR template tells the system where to look for a drawing number, title, date, and revision. The alternative was to have developers configure each template. I rejected it for two reasons. Title block layouts vary with file types, architectural firms, and project size, so the number of templates was never going to be small or stable. Projects also move fast, and waiting on a developer for every new layout would have slowed the very projects the tool was meant to help. The visual template creator lets a project manager draw the regions on a sample page and export the template themselves.

-
-
-
-
-

2. Design for confidence and review, not blind trust

-

OCR is never perfect on architectural drawings, so I designed the error handling around confidence and the ability to review. Each extraction shows a confidence score out of 100. In practice, when confidence was around 75, users generally proceeded with what the system provided, and they still reviewed a random sample of files to check it. Anything wrong could be corrected inline. The goal was to make a wrong result easy to spot and cheap to fix, and to let users decide how much to trust the output.

- -
-
-
-
-

3. Consistency over novelty, even with an AI partner

-

Claude Code suggested changes to button placement and some layouts. I rejected them because they differed from what was already present in the system, and changing familiar patterns would have hurt usability for people who already knew the interface. Claude Code sped up implementation, but judgment about the user's existing context stayed with me.

-
-
-
-
-

4. AI search as an optional add-on

-

I designed the semantic search to be optional, for the point when the company adopts an AI platform. It supports Claude, OpenAI, or a local Ollama model, so a team can choose based on budget and privacy needs. The core product works fully without it.

-
-
-
-
-

The Template Creator

-

The template creator is a standalone HTML and JavaScript tool built with PDF.js that works offline with no backend dependency. Users upload a sample PDF, draw rectangles over the title block, label each region (drawing number, title, date, revision), and export a template file for ArchParser.

-
-
-
Initial Load
-
-
- Image of the template creator's empty canvas -
Empty canvas, ready for PDF
-
-
-
-
-
PDF Loaded
-
-
- Image of the template creator with a loaded PDF -
Document details loaded and ready
-
-
-
-
-
-
-

Visual Progressions

-

- The visual design progressed quickly, but one idea held from the beginning: get users into their jobs as quickly and easily as possible. -

-
-
-
-
-
- -
-
- -
-
- -
-
- -
-
-
-
-
+
+

Step 1 · Context

+

Entering drawing numbers and revision dates by hand was slow and error-prone on 500+ page sets, and no tool handled custom title blocks.

+
Opportunity

How might project managers get drawing metadata without typing it, however each firm lays out its title block?

+

What I did alone

+

ArchParser had one person on it, so every layer of the product went through me, from the first interview to the last test.

+
+
Research

I interviewed project planners and managers about how they pulled metadata from drawing sets, then turned what I heard into requirements, including support for title block layouts that vary by architectural firm.

+
Design

I designed the template creator, the confidence and review model, the dashboard, and the drawing review screens, and kept them consistent with patterns users already knew.

+
Build

I built the React and Vite frontend with RadixUI, the NestJS backend, and the OCR pipeline, with Claude Code as a development partner. The backend has 14 test suites with 53 tests, and the frontend and backend share TypeScript types.

+
-
-

Working with Claude Code

-

I used Claude Code as a development partner across architecture, implementation, and debugging. We weighed trade-offs such as absolute versus relative paths and synchronous versus batch processing before writing code, and it was most useful on TypeORM migrations, NestJS module structure, and tracking down memory and path-resolution bugs. It also helped draft the CLAUDE.md and README files that explain why decisions were made. Design judgment stayed with me, as in decision 3 above.

+
+

Step 2 · Research

+

I interviewed planners and managers before designing anything. Two findings shaped the product.

+
+

Layouts never stand still

Title blocks vary by firm, file type, and project size, and projects can’t wait on a developer for each one.

+

Trust is a dial

At confidence around 75 out of 100, users generally proceeded, and still reviewed a random sample.

+
+

Users of the manual process estimated about 4 to 5 hours saved per drawing set.

+

I interviewed 2 project managers and 3 planners about how they pull drawing metadata today.

-
-

Technical Architecture

-

ArchParser is a monorepo with three workspaces: a NestJS backend, a React and Vite frontend built with RadixUI, and shared TypeScript types that catch API contract drift at compile time. The backend handles PDF-to-PNG conversion and Tesseract.js extraction, configurable OCR templates, a WebSocket gateway for live progress, and the optional LLM service. TypeORM and MySQL store jobs, drawing sets, drawings, templates, and knowledge chunks, with self-referencing drawing sets for revision tracking. The frontend includes a filterable dashboard, a PDF upload workflow, drawing review pages with inline editing, and a mock server so the interface can be developed without the backend. The backend has 14 test suites with 53 tests.

-
-
-
Set Details
-
- Image of the drawing set details page -
-
-
-
Drawing List
-
- Image of the drawing list page -
-
-
-
Drawing Details
-
- Image of the drawing details page -
-
-
+
+

Step 3 · Key decisions

+

Four decisions shaped how people use ArchParser, and one idea held throughout: get users into their jobs quickly.

+
+

Users build templates, not developers

A visual creator replaced developer-configured templates, so projects never wait on a developer. Because I was designing and building it, I could ship it as a standalone offline tool instead of writing a spec for someone else to interpret.

+
Template creator
+
+
+

Confidence, not blind trust

Each extraction shows a score out of 100 and can be fixed inline. Since I owned the whole stack, I could carry that decision from the OCR output through to the inline correction in the review screens.

+
Drawing details with inline editing
+
+
+

Consistency over novelty

I declined AI-suggested layouts that broke patterns users already knew.

+
ArchParser dashboard
+
+
+

AI search is optional

It supports Claude, OpenAI, or a local model, and the core product works fully without it.

+
+
+
Early ArchParser desktop design
Early
+
Mid-progress ArchParser desktop design
Mid
+
Later ArchParser desktop design
Later
+
+

Real-world tests forced two fixes

+

500+ page PDFs ran out of memory, so I moved to adaptive batches. Records lost their links after restarts, so I centralized paths and migrated 373 records.

+
+
What I assumed

Claude Code’s suggested button placements and layouts might improve the interface.

+
What I found

They differed from patterns already in the system, and changing familiar patterns would hurt people who knew it.

+
What I did instead

I kept the existing patterns. Claude Code sped up implementation, and with no second designer to review the call, judgment stayed with me.

+
-
-

Key Iterations

-

Real-world testing forced two architectural changes. First, 500+ page PDFs exhausted memory because the first version converted the whole document to images at once. I switched to single-page conversion with batch sizes that adapt to document size, plus sequential OCR processing and memory monitoring, and large sets now process without crashes.

-

Second, processed drawings lost their link to database records after restarts, caused by inconsistent path resolution, a naming mismatch on deletion, and absolute paths stored in the database. I centralized path handling, standardized folder names, stored relative paths, and migrated 373 existing records, which also made the system portable across environments. The dashboard was refined too, with independent pagination and sorting, date and status filters, and a Kanban-style monitor view, after high-volume tables proved hard to navigate.

+
+

Step 4 · Final design

+

Users draw regions on a sample PDF, label them, and export a template. The tool works offline.

+
ArchParser template creator with a PDF loaded
The template creator with a sample PDF loaded.
+
+
Drawing set details page
Set details
+
Drawing list page
Drawing list
+
Drawing details page with inline editing
Drawing details
+
- -
-
-
-

What I Learned

-
-
-
    -
  1. Test with realistic data from the start. The first version worked on small PDFs and failed on real 500+ page sets. Building with realistic data earlier would have surfaced the memory problem sooner.
  2. -
  3. Empowering users beat clever engineering. The visual template creator did more for adoption than any backend optimization, because it removed a bottleneck.
  4. -
  5. An AI partner speeds implementation, not judgment. Claude Code accelerated architecture work, debugging, and boilerplate, but the decisions about consistency and what users already know were mine to make.
  6. -
-
-
+
+

Step 5 · Impact

+

Demo-ready, and built to cut half a day from each drawing set.

+
    +
  • 200+Drawings processed in demonstrations.
  • +
  • 4 to 5 hrsEstimated savings per drawing set. This is an estimate from users, not a measured result.
  • +
  • ReadyTo test and deploy on a single source of information.
  • +
-
- +
+

Step 6 · What I learned

+

Test with real data, empower users, and keep judgment human.

+
+

Test with realistic data from the start

Small PDFs worked. Real 500+ page sets failed.

+

Empowering users beat clever engineering

The template creator did more for adoption than any optimization.

+

An AI partner speeds implementation, not judgment

Decisions about what users already know stayed with me.

+

Owning every layer shortens the loop and removes the reviewer

When real-world testing exposed the memory and path problems, I could change the design, frontend, and backend in one pass instead of handing a ticket across a team. The tradeoff was that nobody else was checking my work, which is why I leaned on tests and shared types.

+
diff --git a/src/pages/portfolio/casim.md b/src/pages/portfolio/casim.md index 50a8f7ff..70f46538 100644 --- a/src/pages/portfolio/casim.md +++ b/src/pages/portfolio/casim.md @@ -3,7 +3,7 @@ title: 'CASim: Making Years of Inconsistent Construction Data Answerable' portfolioOrder: 1 card: title: "CASim: Internal AI Assistant" - summary: "Designed and architected an internal AI assistant, built on a custom MCP server, that makes years of inconsistent construction data answerable. Currently in UAT and beta testing." + summary: "The only designer, researcher, and front-end developer on an internal AI assistant, built on a custom MCP server, that makes years of inconsistent construction data answerable. Currently in UAT and beta testing." image: "/assets/img/casim-dashboard.webp" badges: ["AI", "MCP", "Enterprise"] group: "Professional" @@ -15,82 +15,104 @@ eleventyNavigation: key: CASim parent: Portfolio order: 1 +layout: case-study.njk +motion: true +eyebrow: 'Case study ยท AI and agent design' +headline: 'Nobody could answer how a projectโ€™s budget compared to what it actually cost, so I designed an assistant that could.' +deck: 'CASim is an internal AI assistant that makes years of inconsistent construction data answerable.' +heroImage: /assets/img/casim-dashboard.webp +heroAlt: 'CASim agents dashboard showing 19 agents across 15 domains' +confidential: true +facts: + - { label: Role, value: 'Sole designer, researcher, and front-end developer, plus creative lead and architect: MCP server, agents, prompts, and tooling' } + - { label: Timeline, value: '9 months, kickoff to UAT' } + - { label: Worked with, value: 'Solo on design, research, and front end. Executive team on data defaults, development team on data cleanup after the naming decisions, IT leadership on the access model' } + - { label: Status, value: 'In UAT and beta testing' } +results: + - { value: Answerable, label: 'Budget against final financials, including work orders, change orders, and supply markup' } + - { value: Source fixed, label: 'Chose to clean the source data over a permanent terminology file' } + - { value: UAT, label: 'Now in beta testing, so the adoption story is still being written' } +camps: + - { id: top, label: Trailhead } + - { id: context, label: Context } + - { id: research, label: Research } + - { id: decisions, label: Key decisions } + - { id: final, label: Final design } + - { id: impact, label: Impact } + - { id: learned, label: What I learned } + --- -
-

Project Overview

-
-

Role:Creative Lead & Architect (design, MCP server, agents, prompts, and tooling)

-

Type:Internal Tool

-

Stack:TypeScript MCP server, Claude Desktop, MCP Inspector, custom web interface

-

Status:In Progress ยท Currently in UAT and beta testing

-
+
+

Step 1 · Context

+

One question was close to impossible to answer: how did a project’s budget compare to its final financials?

+

Every work order, change order, and supply markup existed in our system. Nothing connected them, so answering took manual assembly across screens, and most people didn’t try.

+
Opportunity

How might anyone get a trustworthy answer about a project’s financials without knowing where the data lives?

+

What I did alone

+

I was the only designer, researcher, and front-end developer on CASim, and I also architected the MCP server, agents, prompts, and skills behind it. One person carried the work from the first question to the interface people use every day.

+
+
Research

I interviewed stakeholders about the question that was nearly impossible to answer, audited years of data to find where names, dimensions, and identifiers had drifted, and tested tool behavior in MCP Inspector and Claude Desktop before judging how it felt in the interface.

+
Design

I designed how the assistant reasons about project data and the access model that decides what it can reach.

+
Build

I built the MCP server and the front end of the custom interface. The development team’s part was updating the source data, after the naming decisions were made.

+
- -
-
-

- One question was close to impossible to answer at our company: how did a project's budget compare to its final financials once every work order, change order, and supply markup was counted? All of that information existed in our system. Nothing connected it, so answering took manual assembly across screens, and most people didn't try. -

- - -

- I assumed the hard part would be the AI. It was the data. Over the years, data entry had drifted from consistent inputs to a loose, sometimes conflicting set of values. Construction supplies were the clearest example: names slipped, dimensions were written several ways, and multiple identifiers meant the same product with no link between them. An assistant that sits on top of that data inherits every inconsistency, and it answers confidently regardless. -

-
+
+

Step 2 · Research

+

I assumed the hard part would be the AI. It was the data, drifted over 20+ years.

+
+

Names slipped

The same supply appeared under different names.

+

Dimensions varied

Sizes were written several ways.

+

No shared identifier

Several identifiers meant one product, with no link.

+
+

An assistant on top of inconsistent data inherits every inconsistency, and it answers confidently regardless.

+

I worked with 6 project managers to learn how they rebuilt financials by hand, and audited years of data to find where names, dimensions, and identifiers had drifted.

- -
-

The decision

-
-

We had two realistic paths. The first was to build a terminology file, a definitions layer that mapped every variant name and dimension to a canonical meaning so the assistant could translate on the fly. The second was to work with the executive team to decide what the defaults should be, then have the development team update the data and database to match. -

-

- The terminology file was the faster, lower-risk option, and it was tempting for that reason. I rejected it because it would have hardened the mess into a permanent dependency. Every new variant would need a new mapping, someone would have to own that file indefinitely, and the assistant's answers would only be as good as the last person who updated it. We chose the harder path of returning to a single consistent naming convention and fixing the source. -

-
+
+

Step 3 · Key decisions

+

I rejected the faster fix, a terminology file, because it would harden the mess into a permanent dependency.

+
+
+

Fix the source, not the symptom

+

A definitions layer would translate every variant on the fly, but someone owns it forever. Executives set the defaults and developers updated the data instead.

+
+
Three different spellings of one supply map to a single convention
+
+
+
+

Gate the connectors, not the creativity

+

A working server let IT leadership see exactly what the assistant could and couldn’t reach.

+
+
+

The cleanup drew fair pushback about what might be lost. Early results answered it. I also backtracked more than once to cover gaps the first architecture missed. The research behind that call was mine, so explaining it and answering for what the cleanup might miss was my job too.

+
+
What I assumed

A terminology file mapping every variant to one meaning was the faster, lower-risk path.

+
What I found

Every new variant would need a new mapping, and answers would only be as good as the last update.

+
What I did instead

Executives set the defaults and developers corrected the data at the source.

+
- -
-

What I built

-
-

I was the creative lead and architect for CASim, and I built the core pieces myself. That includes the MCP server, the agents, prompts, and skills that shape how the assistant reasons about project data, and the tooling that connects it all. The server runs against three clients: Claude Desktop for daily use, MCP Inspector for testing tool behavior directly, and a custom interface for the people who will use CASim day to day. Keeping those three in play meant I could check whether a tool behaved correctly in isolation before judging how it felt in the interface. -

- -

- I also designed the access model around a simple principle I later presented to IT leadership: gate the connectors and data scope, not the creativity. A working server made the argument concrete, because leadership could see exactly what the assistant could reach and what it couldn't. -

-
- +
+

Step 4 · Final design

+

One server, three clients: Claude Desktop for daily use, MCP Inspector for testing, and a custom interface for everyone else.

+

With design, research, and the front end all in my hands, a bad answer could be traced to the tool, the prompt, or the data without waiting on a handoff.

+
CASim agents dashboard
Agents grouped by domain, with search and usage totals.
+
Three clients connect to one MCP server, which gates access to four project data sources
One server behind three clients, so a tool can be checked in isolation before judging how it feels in the interface.
- -
-

Outcome

-
-

- The decision was not universally popular. There was real pushback about how the data was being handled and about what information might be missed, forgotten, or lost in the cleanup. That concern was fair, and it shaped how carefully we approached it. -

-

- The initial results answered it. They showed there was real insight sitting in the system that nobody could reach, and that the cleanup path was the right one. The question that had been nearly impossible, budget against end-of-project financials including work orders, change orders, and supply markup, became answerable. CASim is now in UAT and beta testing, so the adoption story is still being written. -

-
+
+

Step 5 · Impact

+

The question that was nearly impossible became answerable.

+
    +
  • AnswerableBudget against final financials, including work orders, change orders, and supply markup.
  • +
  • UATNow in beta testing, so the adoption story is still being written.
  • +
- -
-
-
-

Reflections

-
-
-

- Now that the project is in beta and proceeding through UAT, I am able to reflect on what I'd do differently and, potentially, what I would change moving forward in how I approach AI products. -

-

- Looking back on the beginnings of the project, I would have researched and tested more before diving into creation. There were many times where I had to stop, research, and backtrack in order to cover either a gap that I had in the system, or a concern that wasn't addressed in the original architecture. While this did not delay the project, it did cause unnecessary churn that could have been avoided by taking a little extra time with research. -

-

- Additionally, the pushback on data inconsistencies was unexpected although it should not have been. With a system that is 20+ years old, there are bound to be drifts in how data is stored and what has been entered by the thousands of users of the years. -

-
-
+
+

Step 6 · What I learned

+

Research first, expect pushback, and fix the source.

+
+

Research before building

Extra time up front would have avoided the backtracking.

+

Expect the data to push back

In a 20+ year system, drift is certain.

+

Fix the source, not the symptom

The harder path made the answers trustworthy.

+

Working alone sharpened the cost

Nobody else was positioned to catch the gaps in my research, so the checks had to live in how I worked. That is why testing each tool in isolation in MCP Inspector mattered so much.

+
+

CASim is in UAT and beta testing. I will update this page as adoption results come in.

diff --git a/src/pages/portfolio/color-picker.md b/src/pages/portfolio/color-picker.md index 60fa5c1c..025ca340 100644 --- a/src/pages/portfolio/color-picker.md +++ b/src/pages/portfolio/color-picker.md @@ -1,5 +1,4 @@ --- -layout: markdown.njk title: 'Color Picker' portfolioOrder: 7 card: @@ -15,91 +14,54 @@ eleventyNavigation: key: CaseStudies parent: CaseStudies order: 3 -containers: true +layout: case-study.njk +motion: true +eyebrow: 'Case study ยท Accessibility and components' +headline: 'A color picker is not accessible by nature, so I paired WCAG-compliant colors with patterns and icons.' +deck: 'An accessible color picker for the design system, so color is never the only way to tell labels apart.' +heroImage: /assets/img/colorPalette.webp +heroAlt: 'The color palette for the accessible color picker' +facts: + - { label: Role, value: 'Designer' } + - { label: Stack, value: 'Figma, CSS, React library' } + - { label: Status, value: 'Launched' } +results: + - { value: '44', label: 'Color combinations meeting WCAG AA' } + - { value: '18', label: 'Of those also meeting WCAG AAA' } + - { value: 'Launched', label: 'To production, with follow-up design and testing planned' } +camps: + - { id: top, label: Trailhead } + - { id: context, label: Context } + - { id: decisions, label: Key decisions } + - { id: final, label: Final design } + - { id: impact, label: Impact } --- - -:::section - -## Project Overview - -> An accessible color picker for the design system that pairs WCAG-compliant colors with patterns and icons, so color is never the only way to tell labels apart. - -:::card - -**ROLE**: Designer - -**STACK**: Figma, CSS, React library - -**STATUS**: Launched - -::: - -::::section - -## Introduction - -A color picker is not accessible by nature, so the goal of this component design was to provide a solution that not only allowed users to select colors for labels, but also provide alternatives such as patterns and icons. - -
-
- Image of the color palette -
-
- -:::: - -::::section - -## Getting Started - -To start, a basic set of colors were chosen from the available color palette (as defined in the design system). Each color was verified against a light background (`#ffffff`) and a dark background (`#010101`), as well as against multiple layers. - -> In total, forty-four (44) combinations met **WCAG AA** compliance, with eighteen (18) meeting **WCAG AAA** compliance. - -
-
- Image of the color palette -
-
- -:::: - -::::section - -## Next Steps - -After the colors had been established, I worked with the PM and Dev team to determine the first components to test these with. - -> It was determined that **Labels** would be the best place to start, as they were the most ubiquitous across the system. - -
-
- Image showing where labels are used -
-
- -:::: - -::::section - -## Comparisons and Testing - -Once the basic levels had been configured, I needed to compare the current label coloring scheme to what the new component provided. This required working with the development team to create a prototype environment that allowed the component to be used in real-time without surfacing the in-progress work to the production users. - -
-
- Image comparing before and after labels -
-
- -:::: - -:::section - -## Findings - -Once the prototype was up and running, I was able to test out the interactions, visuals, and test the new component with users. - -The color picker launched to production. The findings from this round also fed follow-up work: another round of design and testing. - -:::: +
+

Step 1 · Context

+

One of the most up-voted feature requests had a built-in problem: color alone cannot carry meaning.

+

A color picker is not accessible by nature. The goal of this component was to let users choose colors for labels, and to offer alternatives such as patterns and icons.

+
Opportunity

How might label colors stay distinguishable for everyone, whatever they can see?

+
+ +
+

Step 2 · Key decisions

+

I verified every color against light and dark backgrounds and started with the most ubiquitous component, labels.

+

I chose a base set of colors from the design system palette. Each was checked against a light background (#ffffff), a dark background (#010101), and multiple layers.

+

In total, 44 combinations met WCAG AA, with 18 meeting WCAG AAA.

+
The verified color palette
+

With the PM and development team, we agreed labels were the best place to start, since they are the most common across the system.

+
Where labels are used in the product
+
+ +
+

Step 3 · Final design

+

A prototype environment let us test the new labels in real time without exposing unfinished work to production users.

+

To compare the current label scheme with the new component, I worked with the development team on a prototype environment. I tested the interactions and visuals there, and tested the component with users.

+
Before and after comparison of labels
+
+ +
+

Step 4 · Impact

+

The color picker launched to production, and the findings fed another round of design and testing.

+

Measured results are not available for this page yet. What I can say is that the component shipped, and the findings from this round became follow-up work.

+
diff --git a/src/pages/portfolio/component-library.md b/src/pages/portfolio/component-library.md index 26a6f2e9..b321478d 100644 --- a/src/pages/portfolio/component-library.md +++ b/src/pages/portfolio/component-library.md @@ -16,139 +16,76 @@ eleventyNavigation: key: ComponentLibrary parent: Designs order: 4 +layout: case-study.njk +motion: true +eyebrow: 'Case study ยท Design systems' +headline: 'Web and iOS shared no visual language, so I built the first unified design system for construction management software.' +deck: 'A library of design tokens, components, and Storybook documentation for Component Assembly Systems, built for web and iOS.' +heroImage: /assets/img/Storybook-GettingStarted.webp +heroAlt: 'Getting Started screen on Storybook' +facts: + - { label: Role, value: 'Lead product designer' } + - { label: Timeline, value: '2024 to present' } + - { label: Platform, value: 'Web and iOS' } + - { label: Status, value: 'In active use, documented in Storybook' } +results: + - { value: 'First', label: 'Unified design system across web and iOS' } + - { value: '25%', label: 'Faster development cycles, from the project summary' } + - { value: 'Storybook', label: 'Documentation for design and components' } +camps: + - { id: top, label: Trailhead } + - { id: context, label: Context } + - { id: decisions, label: Design tokens } + - { id: final, label: Final design } + - { id: impact, label: Impact } --- - -
-

Project Overview

-
-

- Built comprehensive design system for Component Assembly Systems' construction management platform, spanning web and iOS applications. -

-

Created unified visual language, reusable components, and documentation that accelerated development cycles.

-

- Role:Lead Product Designer -

-

- Timeline:2024-Present -

-

- Platform:Web & iOS -

- +
+

Step 1 · Context

+

Construction management software across web and iOS needed one visual language and one set of reusable parts.

+

I built the design system for Component Assembly Systems’ construction management platform. It spans web and iOS, with a unified visual language, reusable components, and documentation that speeds up development.

-
-

Design Tokens

-
-
-
Color Palette
-
-
- Component Library light color palette -
-
-
-
-
Typography
-
-
- Component Library typography -
-
-
-
-
-
-
Spacing & Sizing
-
-
-        
-  --cas-spacer-xxs: 0.25rem; /* 4px */
-  --cas-spacer-xs: 0.5rem;   /* 8px */
-  --cas-spacer-sm: 0.75rem;  /* 12px */
-  --cas-spacer: 1rem;        /* 16px */
-  --cas-spacer-md: 1rem;     /* 16px */
-  --cas-spacer-lg: 1.25rem;  /* 20px */
-  --cas-spacer-xl: 1.5rem;   /* 24px */
-  --cas-spacer-xxl: 2rem;    /* 32px */
-  --cas-spacer-xxxl: 3rem;   /* 48px */
-        
-        
-
-
-
-
Iconography
-
- Component Library icon examples -
-
-
+
+

Step 2 · Design tokens

+

Tokens came first, so every component inherits color, type, spacing, and icons from one source.

+
+
Component library light color palette
Color palette
+
Component library typography
Typography
+
Component library icon examples
Iconography
+

Spacing and sizing

+
--cas-spacer-xxs: 0.25rem; /* 4px */
+--cas-spacer-xs: 0.5rem;   /* 8px */
+--cas-spacer-sm: 0.75rem;  /* 12px */
+--cas-spacer: 1rem;        /* 16px */
+--cas-spacer-md: 1rem;     /* 16px */
+--cas-spacer-lg: 1.25rem;  /* 20px */
+--cas-spacer-xl: 1.5rem;   /* 24px */
+--cas-spacer-xxl: 2rem;    /* 32px */
+--cas-spacer-xxxl: 3rem;   /* 48px */
+
-
-

Components

-
-
-
Buttons
-
- Component Library color palette -
-
-
-
Cards
-
- Component Library typography -
-
-
-
-
-
Data Tables
-
- Component data table example -
-
-
-
Forms, Inputs, Dropdowns
-
-
- Component Library typography -
-
- Component Library typography -
-
-
-
-
-
-

Documentation

-

All design and component documentation was created using Storybook. -

-
-
- Component Library typography -
- -
+
+

Step 3 · Final design

+

Buttons, cards, tables, and form controls, specified so they are ready for production.

+
+
Button examples
Buttons
+
Card examples
Cards
+
Data table component example
Data tables
+
Dropdown examples
Dropdowns
+
Text input examples
Text inputs
+

All design and component documentation lives in Storybook.

+
Storybook Getting Started page
+

View Storybook

- -
-
-
-

Impact

-
-
-

-

    -
  • Established first unified design system across web and iOS
  • -
  • Reduced design-to-development handoff time by providing production-ready component specifications
  • -
  • Created foundation for rapid feature development and consistent user experience
  • -
  • Documented design tokens and usage guidelines for engineering team
  • -
-

-
-
+ +
+

Step 4 · Impact

+

The first unified design system gave engineering a foundation for faster, more consistent features.

+
    +
  • Established the first unified design system across web and iOS
  • +
  • Reduced design-to-development handoff time with production-ready component specifications
  • +
  • Created a foundation for rapid feature development and a consistent user experience
  • +
  • Documented design tokens and usage guidelines for the engineering team
  • +
diff --git a/src/pages/portfolio/soccertracker.md b/src/pages/portfolio/soccertracker.md index 99ab6042..d9fa84dd 100644 --- a/src/pages/portfolio/soccertracker.md +++ b/src/pages/portfolio/soccertracker.md @@ -1,5 +1,4 @@ --- -layout: markdown.njk title: 'Soccer Game Tracker' portfolioOrder: 8 card: @@ -15,205 +14,70 @@ eleventyNavigation: key: CaseStudies parent: CaseStudies order: 2 -containers: true +layout: case-study.njk +motion: true +eyebrow: 'Case study ยท Native iOS app' +headline: 'Every sideline app wanted my data and an account, so I built a native one that keeps everything on the phone.' +deck: 'A native iOS app for coaches to track games: fast to set up, minimal during play, and with data the coach controls.' +facts: + - { label: Role, value: 'Designer and developer' } + - { label: Stack, value: 'SwiftUI' } + - { label: Status, value: 'Work in progress, in active use this season' } +results: + - { value: 'Native', label: 'Swift with local storage and no network dependency' } + - { value: 'Solo', label: 'Design and implementation by one person' } + - { value: 'In use', label: 'Tested on a real sideline, in real game conditions' } +camps: + - { id: top, label: Trailhead } + - { id: context, label: Context } + - { id: decisions, label: Key decisions } + - { id: final, label: Final design } + - { id: impact, label: Impact } --- - -:::section - - ## Project Overview - - > A native iOS app, built by a coach, who needed fast and reliable game tracking without surrendering data to a third-party platform, designed for the sideline, built in Swift. - - :::card - - **ROLE**: Designer & Developer - - **STACK**: SwiftUI - - **STATUS**: Work in progress - -::: - -::::section - -## Introduction - - Coaching a youth soccer team generates a surprising amount of data. Scores, substitutions, player minutes, goals, assists, saves, cards. The problem isn't that the data is hard to capture; it's that every existing option forces a tradeoff I wasn't willing to make. Pen and paper is fast but produces nothing useful afterward. Third-party apps are convenient but own your data, require accounts, and are built for organizations, not individual coaches. I wanted something purpose-built for how I actually work on a sideline: fast to set up, minimal interaction during play, and with data I control entirely. - -
-
-
-
- New Game setup screen -
set the roster: starters and substitutes
-
-
-
-
-
-
- New Game setup screen -
add game info: team size, location, date & time
-
-
-
-
-:::: - -::::section -## The harder right choice - - I built SoccerGameTracker as a native iOS app in Swift, solo, from scratch. The easier path would have been a PWA or React Native wrapper; I've shipped both, and they're faster to build. I chose Swift because a sideline app has a specific performance contract: launch instantly, respond without lag when a goal gets scored, never drop data if the phone gets pocketed. Native with local storage and no network dependency satisfies all three. No sync to worry about, no account to authenticate, no API that might be down at kickoff. - - > every decision went through one person: me - - **Constraint clarified scope**. Without a backend, multi-device sync and shared rosters aren't on the table, and that's fine. Knowing what the app isn't made it easier to build what it is. I handled both the design and implementation entirely, which meant every decision, from interaction model to data structure, went through one person. That's mostly an advantage when you need to move fast and stay opinionated. -:::: - -::::section - - ## Designing for the sideline - - > The core design problem is attention. - - During a live game, a coach is watching players, calling substitutions, talking to parents. The app gets a glance and a thumb tap, not focused interaction. Every decision on the Live Game screen follows from that. - -
-
Pregame
-
- The pre-game setup flow handles the overhead up front. -
New Game collects opposition, date, location, and half duration, everything you'd put on a physical scoresheet, plus roster selection from a pre-built player list with jersey number and position. -
The Game vs. Scrimmage toggle is a small but deliberate decision: scrimmages have different stakes and different lineup dynamics, and I wanted the history to reflect that distinction clearly. -
-
- -
-
Scoreboard & Quick Actions
-
- The scoreboard and half timer sit at the top, readable in a second. The Quick Actions section, Team Goal and Opponent Goal, are the two largest tap targets in the app, sized and colored for immediate recognition in any lighting. Correction actions (Remove Team Goal, Subtract Goal) are present but visually subordinate: same area, smaller, lighter. Reachable when you need them, harder to hit by accident. -
-
- -
-
Players & Activity
-
- Below the quick actions is where the app earns its depth. Each player in the active roster is tappable, opening a detail view where you can log individual stats in real time: goals, assists, saves, shots, yellow cards, red cards. Every action is timestamped and tied to the game half, building a full action log that the post-game summary renders as a timeline. That's the data that makes post-game review actually useful: not just who won, but who contributed, when, and how often. -
-
- -
-
-
- Live Game screen showing scoreboard, timer, Quick Actions, and player list -
Live Game screen showing scoreboard, timer, Quick Actions, and player list
-
-
-
-
- - ### Substitutions - - :::card - Substitutions were a meaningful product decision. Mid-game sub support, with a distinct "substituted out" state separate from bench status, reflects how substitutions actually work: a player who comes off isn't the same as a player who never started. Getting that distinction right matters for per-player stats to be accurate. - ::: - -
-
-
-
- View a player's stats and position before substituting them on or off -
View a player's stats and position before substituting them on or off
-
-
-
-
-
-
- Substitutes cannot accidentally have statistics assigned -
Substitutes cannot accidentally have statistics assigned
-
-
-
-
-
-
- View who subbed in and out of the game by half -
View who subbed in and out of the game by half
-
-
-
-
-:::: - -::::section - - ## After the game - - > As a coach, being able to view historical information is a key part in planning practices, setting lineups, and determining strategy. - - ### History - -
-
-
-
- View historical game data, zooming in to see each game's details -
View historical game data, with key info front and center
-
-
-
-
-
-
- View historical game data, zooming in to see each game's details -
Zooming into any game shows player-by-player stats
-
-
-
-
-:::: - -::::section - ## Where it stands - - :::card - The app is in active use this season, tested against the real constraint it was built for: a sideline, in real game conditions. Feedback from that use has been indispensable, with the per-player stats used between games to inform lineup and substitution planning. That's the outcome I was building toward. - - What real use has surfaced is mostly sequencing: the transition from pre-game setup into the live view needs less friction, and the post-game summary screen is still being refined. **Those are the next iterations.** - ::: - - > An App Store release is planned once the final rough edges are resolved. The app does one thing, does it reliably, and doesn't ask for anything it doesn't need. -:::: +
+

Step 1 · Context

+

Pen and paper is fast but leaves nothing useful, and third-party apps own your data.

+

Coaching a youth soccer team generates a surprising amount of data: scores, substitutions, player minutes, goals, assists, saves, and cards. Capturing it is not the hard part. Every existing option forced a tradeoff I was not willing to make. Third-party apps are convenient, but they require accounts and are built for organizations, not individual coaches.

+
Opportunity

How might a coach track a whole game with one thumb, and keep the data?

+
+
New game roster screen
Set the roster: starters and substitutes
+
New game setup screen
Add game info: team size, location, date, and time
+
+
+ +
+

Step 2 · Key decisions

+

I chose Swift and local storage because a sideline app has a performance contract: launch instantly, respond without lag, never drop data.

+

A PWA or React Native wrapper would have been faster to build, and I have shipped both. Native with local storage satisfies all three requirements, with no sync, no account to authenticate, and no API that might be down at kickoff.

+

Every decision went through one person: me.

+

The constraint clarified scope. Without a backend, multi-device sync and shared rosters are off the table, and knowing what the app is not made it easier to build what it is.

+

The core design problem is attention. During a live game a coach is watching players, calling substitutions, and talking to parents, so the app gets a glance and a thumb tap.

+
+

Pregame

Setup handles the overhead up front: opposition, date, location, half duration, and roster selection. A Game or Scrimmage toggle keeps the history honest.

+

Scoreboard and quick actions

The two largest tap targets are Team Goal and Opponent Goal. Correction actions are present but smaller and lighter, so they are harder to hit by accident.

+

Players and activity

Tapping a player logs goals, assists, saves, shots, and cards. Each action is timestamped and tied to the half, so the post-game summary reads as a timeline.

+
+
+ +
+

Step 3 · Final design

+

A live game screen built for a glance, and substitutions that match how they really work.

+
Live Game screen showing scoreboard, timer, Quick Actions, and player list
Live Game screen showing scoreboard, timer, Quick Actions, and player list
+

Mid-game substitutions have a distinct “substituted out” state, separate from bench status, because a player who comes off is not the same as a player who never started. That distinction keeps per-player stats accurate.

+
+
View a player's stats and position before substituting
View stats and position before substituting
+
Substitutes cannot accidentally have statistics assigned
Substitutes cannot accidentally have stats assigned
+
Who subbed in and out by half
See who subbed in and out by half
+
+
+ +
+

Step 4 · Impact

+

In use this season, with per-player stats informing lineups and substitution planning between games.

+

As a coach, historical information shapes how I plan practices, set lineups, and choose strategy.

+
+
Historical game data
History, with key info front and center
+
Player-by-player stats for a game
Zoom into any game for player-by-player stats
+
+

Real use has mostly surfaced sequencing. The move from pregame setup into the live view needs less friction, and the post-game summary is still being refined. Those are the next iterations, and an App Store release is planned once the rough edges are resolved.

+
diff --git a/src/pages/portfolio/task-it.md b/src/pages/portfolio/task-it.md index ae9af379..3ab2e6a7 100644 --- a/src/pages/portfolio/task-it.md +++ b/src/pages/portfolio/task-it.md @@ -16,263 +16,105 @@ eleventyNavigation: key: Designs parent: Designs order: 1 +layout: case-study.njk +motion: true +eyebrow: 'Case study ยท Enterprise product design' +headline: 'AWS teams tracked work in 3 or 4 different tools, so I designed one that 500 people chose to use every day.' +deck: 'Task-it is an internal task management platform that consolidated fragmented tracking tools into one system with role-agnostic patterns.' +heroImage: /assets/img/task-it_product.webp +heroAlt: 'Task-it product illustration' +confidential: true +facts: + - { label: Role, value: 'Senior UX designer and lead designer, Amazon Web Services' } + - { label: Timeline, value: '2021 to 2023' } + - { label: Worked with, value: '3 product managers, 8 engineers, and a UX researcher' } + - { label: Status, value: 'Launched, with patterns adopted in other AWS internal tools' } +results: + - { value: '500', label: 'Daily active users across 15+ AWS teams within 18 months of launch' } + - { value: '3 to 4 โ†’ 1', label: 'Separate tracking systems consolidated into one platform' } + - { value: Organic, label: 'Growth came from word of mouth, not mandates from leadership' } +camps: + - { id: top, label: Trailhead } + - { id: context, label: Context } + - { id: research, label: Research } + - { id: decisions, label: Key decisions } + - { id: final, label: Final design } + - { id: impact, label: Impact } + - { id: learned, label: What I learned } + --- -
-

Project Overview

-
-

- Company:Amazon Web Services -

-

- Role:Senior UX Designer (Lead Designer) -

-

- Timeline:2021-2023 -

-

- Team: - Collaborated with 3 PMs, 8 engineers, UX researcher -

-

- THE CHALLENGE: - AWS teams were using 3-4 different task tracking tools (Jira, internal systems, spreadsheets), leading to fragmented workflows, poor cross-team visibility, and significant time wasted in context-switching. Product managers struggled to get a unified view of work across teams, and individual contributors were frustrated by having to update multiple systems. -

-

- MY ROLE: - I designed Task-it, a unified task management platform that consolidated these disparate tools into a single, intuitive system with role-agnostic patterns that could serve diverse AWS teams. -

-

- THE SOLUTION: - I designed Task-it, a unified task management platform that consolidated these disparate tools into a single, intuitive system. The design focused on role-agnostic patterns that could serve diverse AWS teams, from product managers tracking roadmaps to engineers managing technical debt to support teams triaging customer issues. -

- THE IMPACT: -
-

    -
  • 500 daily active users across 15+ AWS teams within 18 months of launch
  • -
  • Consolidated 3-4 separate tracking systems into one unified platform
  • -
  • Reduced context-switching and improved cross-team collaboration
  • -
  • Drove feature roadmap through continuous user research and usability testing
  • -
  • Created scalable UX patterns now being adopted in other AWS internal tools
  • -
-

-
-
-
-

The Process

-

Before any design work (wireframes, mockups, prototypes) could begin, extensive user research and testing was needed.

-
-

Research methods with example questions: -

    -
  1. User interviews -
      -
    • Ask users what their role is on their product team.
    • -
    • What product(s) do they use to manage their work?
    • -
    • If they do not use the existing internal product, why not?
    • -
    -
  2. -
  3. Competitive analysis -
      -
    • What products are used internally, besides the previous internal-built system?
    • -
    • Why are they used? What do they provide that the current internal system does not?
    • -
    -
  4. -
  5. On-hand testing -
      -
    • As a user, I needed to work within the system we were working to replace so that I can find the issues that currently exist.
    • -
    • Findings were compared against user interviews. Those interviews were also used as a basis for various testing methods.
    • -
    -
  6. -
  7. Observe users -
      -
    • Watch how users use the products: where do they always visit? What tasks are they always performing?
    • -
    -
  8. -
-

-
-
-

Once a baseline had been established, the first round of user flow diagrams and analysis took place. Each step in the various user flows had two goals: -

    -
  1. Follow user expectations -
      -
    • Users expected to be able to perform basic tasks, such as creating work items, viewing work items, and organizing their work in a single system.
    • -
    -
  2. -
  3. Simplify existing processes -
      -
    • Users would not accept a more complicated process. Interviews taught me that anything more complicated than what they had would be a deal-breaker for switching to, no matter the directives from management.
    • -
    -
  4. -
-

-
-
-
-

Target Audience

-

For the initial release (scoped internally as a "beta"), the target audience consisted of Individual Contributors (IC), with a mix of high-interaction and low-interaction users. These were categorized as 1. Contributors, 2. Owners, and 3. Viewers.

-
-
    -
  1. Contributor -
      -
    • Those who actively added items to a team's task queue.
    • -
    -
  2. -
  3. Owner -
      -
    • Individuals who were, at a high-level, responsible for the delivery and success daily and long-term tasks and goals. While not ICs, Owners had a direct hand in what tasks teams took on.
    • -
    -
  4. -
  5. Viewer -
      -
    • Those who stayed on the "outside" of the day-to-day operations, but could influence the work taking place. Viewers were not considered to be a top-level user, but the entire process had to take into account their work. If things became harder for them to see/find, then they were likely to become a blocker to adoption.
    • -
    -
  6. -
-
-

After discussions with Project Management (PM), it was determined that the target audience would be broken down into tiers: Tier 1 and Tier 2. -

-
-
    -
  1. - Tier 1 consisted of those who used the existing solutions multiple times per day and could assist with driving adoption. These were the Contributors. -
  2. -
  3. - Tier 2 consisted of those who interactive with existing solutions on a weekly basis, and then only to monitor the work being done by those in Tier 1. -
  4. -
-
+
+

Step 1 · Context

+

Work lived in Jira, internal systems, and spreadsheets, and nobody could see across teams.

+

AWS teams used 3 or 4 different task tracking tools. That meant fragmented workflows, poor cross-team visibility, and real time lost to context-switching. Product managers struggled to get a unified view of work across teams, and individual contributors were frustrated by updating multiple systems.

+
Opportunity

How might one system serve product managers tracking roadmaps, engineers managing technical debt, and support teams triaging customer issues?

-
-

User flows, Wireframes, and Mockups

-

For the initial Task Details view, user flows and wireframes were used to identify possible errors. The findings from this work would go on to determine the direction of the final mockups.

-
-
User flows
-
-
- Image of the user flow -
-
-
-
-
User states
-
-
- Image of the various user flow states -
-
-
+
+

Step 2 · Research

+

Users would not accept a more complicated process, so research came before any wireframe.

+
+

User interviews

Their role, the products they use to manage work, and why some avoided the existing internal product.

+

Competitive analysis

What else teams used internally, and what it offered that the internal system did not.

+

Hands-on testing

I worked inside the system we were replacing to find its issues, then compared them against the interviews.

+

Observation

Where users always go, and which tasks they perform every day.

+
+

Anything more complicated than what they had would be a deal-breaker, no matter the directives from management.

+

Who it was for

+

The beta targeted individual contributors, with a mix of high-interaction and low-interaction users in three groups. After discussions with product management, we split them into two tiers.

+
+
Contributors

Actively added items to a team’s task queue. Tier 1: they used existing tools many times a day and could drive adoption.

+
Owners

Responsible for delivery and success of daily and long-term goals. Not ICs, but they shaped which tasks teams took on.

+
Viewers

Stayed outside day-to-day operations but influenced the work. Tier 2: weekly check-ins, and a likely blocker to adoption if things got harder to find.

+
-
-

Initial findings

-

Based off of these user flows, I determined that the success and failures paths needed to be looked at further. Depending on the user, these paths had the potential to break experiences and decrease the usability and delight in the product.

-

Wireframes

-

When creating wireframes, I start by taking an existing components (whether already in the application or from the component library in use) and lay out my page. From there, I begin to add some details (using the redacted script font) and basic headings. This is then reviewed with PM, Development, and, if available, fellow UX designers.

-

Mockups

-

After reviews have been completed on the wireframes, mockups are created. In my process, I utilize mockups to get the full look/feel of the page without interactions. Once completed, these are used for user testing as well as final review with the development team.

-
-
-
Wireframe
-
- Image of the task details wireframe -
-
-
-
Mockup
-
- Image of the first version of the task details mockup -
-
-
+
+

Step 3 · Key decisions

+

Every step in every flow had two goals: follow what users expect, and make the process simpler.

+

Users expected to create work items, view them, and organize their work in one system. Anything beyond that had to earn its place. I used user flows and states to find where success and failure paths could break the experience before any mockup existed.

+
+
The Task-it user flow
User flows
+
The states in the Task-it user flow
User states
+
+

Let users set the priority

+

More attributes were planned, so the Task Details page had to adapt. I ran a card-sorting survey that included existing and future attributes, with one prompt: which attributes are required for you to get your work done most efficiently? Users set the order, not the team.

+
The card-sorting survey used for attribute priority
The card-sorting survey.
-
-

Testing and Revisions

-

As part of the iterative process, I consistently looked at user feedback and heat-mapping to determine if there were any changes needed. From the beginning, it was known that additional attributes would be made available to users and that the Task Details page would have to be able to adapt accordingly.

-

As part of the testing process, I created a card-sorting survey for users, where I included all of the existing attributes, as well as some future-planned attributes, and asked users to order them by priority. Priority was determined by the user, with the only prompt being "what attribute(s) are required for you to get your work done in the most efficient manner?".

-
-
User Survey
-
- Image of the user testing process -
-
-

Revisions

-

- After analyzing the results of the card-sorting user testing, additional mockups were created to task with the updated attribute priorities. -

-
-
-
Revision 1
-
- Image of the first version of the task details mockup -
-
-
-
Revision 2
-
- Image of the second version of the task details mockup -
-
-
-
Revision 3
-
- Image of the third version of the task details mockup -
-
-
+
+

Step 4 · Final design

+

Wireframes, then mockups, then three revisions driven by what users ranked.

+

I start wireframes from existing components, add redacted-script placeholder text and basic headings, and review with product management, development, and fellow designers. Mockups give the full look and feel for user testing and final development review.

+
+
The task details wireframe
Wireframe
+
The first task details mockup
Mockup
+
+

After the card sort, I revised the page to match the updated attribute priorities.

+
+
Task details revision 1
Revision 1
+
Task details revision 2
Revision 2
+
Task details revision 3
Revision 3
+
-
-

Results & Impact

-

Adoption & Growth

-

-

    -
  • Launched to pilot teams in Q2 2022, reaching general availability by Q4 2022
  • -
  • Grew from 50 pilot users to 500 daily active users within 18 months
  • -
  • Expanded across multiple AWS teams spanning product, engineering, and support organizations
  • -
  • Achieved organic growth through word-of-mouth recommendations from satisfied users
  • -
-

-

User Feedback

-

-

    -
  • Post-launch usability testing showed a high satisfaction rate with overall experience
  • -
  • Users consistently praised the unified view: "Finally, one place to see all my work"
  • -
  • Engineers appreciated the flexible categorization: "Task-it adapts to how I work, not the other way around"
  • -
  • Product managers valued cross-team visibility: "I can finally see what's blocked across all my teams"
  • -
-

-

Business Impact

-

-

    -
  • Consolidated 3-4 legacy tools, reducing tool sprawl and maintenance overhead
  • -
  • Improved cross-team visibility into project status, blockers, and dependencies
  • -
  • Reduced onboarding time for new AWS employees (one tool to learn instead of multiple)
  • -
  • Established design patterns now being adopted in other AWS internal applications
  • -
  • Demonstrated value of user-centered design for internal tools
  • -
-

+
+

Step 5 · Impact

+

From 50 pilot users to 500 daily active users in 18 months, mostly by word of mouth.

+
    +
  • 500Daily active users across 15+ AWS teams within 18 months of launch.
  • +
  • 3 to 4 โ†’ 1Legacy tools consolidated, reducing tool sprawl and maintenance overhead.
  • +
  • AdoptedDesign patterns now used in other AWS internal applications.
  • +
+
+

Adoption

Pilot teams in Q2 2022, general availability by Q4 2022, and growth across product, engineering, and support organizations.

+

User feedback

Post-launch usability testing showed high satisfaction. “Finally, one place to see all my work.” “Task-it adapts to how I work, not the other way around.”

+

Business impact

Better visibility into status, blockers, and dependencies, and a shorter onboarding path with one tool to learn instead of several.

+
- -
-
-
-

What I Learned

-
-
-

- This project reinforced three critical lessons about designing internal tools: -

    -
  1. Internal tools deserve the same design rigor as customer-facing products. By investing in proper user research, prototyping, and testing, we created a tool that users actually wanted to use, driving organic adoption without mandates from leadership.
  2. -
  3. Flexibility beats rigid workflows. Rather than forcing all teams into a single process, we designed role-agnostic patterns that could adapt to different team structures and workflows. This flexibility was key to achieving broad adoption across diverse AWS organizations.
  4. -
  5. Continuous research is essential. By conducting regular usability tests and gathering feedback throughout development, we caught issues early and built features that users actually needed, not just what we assumed they wanted.
  6. -
-

-

- The success of Task-it demonstrated that internal tools can be delightful and that investing in UX research pays dividends in adoption, productivity, and user satisfaction. -

-
-
+
+

Step 6 · What I learned

+

Give internal tools the same rigor as customer-facing products, stay flexible, and keep researching.

+
+

Internal tools deserve design rigor

Research, prototyping, and testing built a tool people wanted to use, with adoption driven by users and not by mandate.

+

Flexibility beats rigid workflows

Role-agnostic patterns that adapted to different team structures were the key to broad adoption.

+

Continuous research is essential

Regular usability tests caught issues early and kept us building what users needed, not what we assumed.

+
diff --git a/src/pages/portfolio/timetracker.md b/src/pages/portfolio/timetracker.md index 2a439c7b..eb848975 100644 --- a/src/pages/portfolio/timetracker.md +++ b/src/pages/portfolio/timetracker.md @@ -1,5 +1,4 @@ --- -layout: markdown.njk title: 'Weekly AI-Powered Work Summary' portfolioOrder: 2 card: @@ -17,257 +16,89 @@ eleventyNavigation: key: Development parent: Development order: 2 -containers: true +layout: case-study.njk +motion: true +eyebrow: 'Case study ยท AI product design' +headline: 'I skipped my weekly summary because the tooling made it feel like a project, so I built AI summaries into the tool that already knows my week.' +deck: 'A Weekly Report feature for Timetraked, my time tracking PWA, plus a separate generator that turns a summary into a full report.' +heroImage: /assets/img/timetracker-weekly-report.webp +heroAlt: 'Weekly Report screen showing the time period to summarize and tone selection' +facts: + - { label: Role, value: 'Lead product designer and product manager' } + - { label: Type, value: 'Feature extension, personal productivity tool' } + - { label: Stack, value: 'React, TypeScript, Tailwind CSS, Google Gemini API (Timetraked); OpenAI GPT 5.5, Docker (Weekly Report Generator)' } + - { label: Status, value: 'Shipped and in daily use. The Weekly Report Generator runs in Docker and is currently private, with plans to make it public.' } +results: + - { value: 'Under 1 min', label: 'To produce a weekly summary, available wherever Timetraked is' } + - { value: '30 min โ†’ 1', label: 'Minutes per full weekly report, from about 30 by hand' } + - { value: 'Daily', label: 'Timetraked has been in daily use since early 2025' } +camps: + - { id: top, label: Trailhead } + - { id: context, label: Context } + - { id: decisions, label: Key decisions } + - { id: final, label: Final design } + - { id: impact, label: Impact } --- - -:::section - -## Project Overview - - :::card - - **Role:** Lead Product Designer & Product Manager - - **Type:** Feature extension, personal productivity tool - - **Stack:** React, TypeScript, Tailwind CSS, Google Gemini API (Timetraked); OpenAI GPT 5.5, Docker (Weekly Report Generator) - - **Status:** Shipped, in daily use. The Weekly Report Generator runs in Docker and is currently private, with plans to make it public. - -::: - -::::section - -## About Timetraked - - Timetraked (formerly TimeTracker Pro) is a Progressive Web App (PWA) I designed, built, and use every day to track how I spend my work hours. It runs in the browser, installs as a native-like application on desktop and mobile, and stores data locally by default, with no account required. Through the day I create tasks, assign them to projects and categories, and archive each completed day. Over time, it builds a personal record of where my time actually goes. - - I built it because every time tracking tool that I tried was either too complex for my personal use, or too simple to be useful. Timetraked is purpose-built for the way I work: start the day, capture tasks as they happen, and review and archive at the end. It has been in daily use since early 2025 and has grown into a small, but complete, product, with export formats, project management, offline support, and invoicing. The Weekly Report feature is the most recent addition, and the one that most clearly shows how the product has evolved from a personal utility into a considered system. - -
-
- Start screen for Timetraked -
-
-:::: - -::::section -## The Weekly Report Feature - - :::card - After archiving each week, I found myself running a second, separate tool to generate a weekly summary. That tool only worked with PDFs directly exported from the time tracker, only ran on a local development server, and produced output that always needed manual cleanup before it was shareable. It was a workaround, not a product. - - The Weekly Report feature replaces that entire workflow. It lives inside Timetraked as a dedicated route, directly reading the same data the rest of the app uses, and generates a coherent weekly narrative using an AI model (Gemini). The output can be tuned for different audiences, such as a team's standup, a client update, or a personal retrospective, and is editable before copying to a separate document. No export step, no separate server, no manual cleanup. - ::: - -
-
-
- Weekly Report Screen -
review the time period to summarize and select a tone before generation
-
-
-
- - > The right tool for summarizing my work week is the tool that already knows my work week. -:::: - -::::section -## The Problem - - :::card - The original two-tool setup had three specific friction points: - - 1. No real integration - 2. Availability - 3. Output always requiring editing - - - The generator only accepted PDFs from the time tracker, creating a dependency without any actual data sharing. The export was the only bridge between them. - - - The generator ran on a local Vite server, which meant it was only accessible from my machine and only in a single context. Any time I needed a summary on a different device, I was out of luck. - - - Because the generator had no understanding of who I was writing for, every summary came out generic and required manual work to make it usable. The generator sounded too similar from week-to-week, diminishing the quality of the reports. - ::: - -> The deeper problem was that the tooling made a simple, recurring task feel like a project in itself. I needed a paragraph that I could drop into a standup or send to a client. _What I had was a process_. -:::: - -::::section -## Extend, Not Replace - - :::card - The obvious move might have been to build a better standalone summary tool, but I chose not to, and the reasoning matters. - - A standalone tool would still need an export step, a file upload, or an API connection to access my time data. It would have to be general enough to work with any input, which means losing all the context that makes a summary actually useful: what categories mean, which projects are billable, and what a typical week looks like, specifically for me. - - Extending Timetraked meant the summary feature could read directly from the same local data store the rest of the app uses. No synchronization problem. No format negotiation. The data is already structured exactly the way the feature needs it to be: tasks with descriptions, projects, categories, and duration, grouped by day. - - It also meant the feature could be opinionated in ways that a generic tool cannot. It knows to exclude break-time and lunch categories automatically. It understands the project and client structure. It has enough context to group a week of task descriptions into a meaningful narrative rather than a list. -:::: - -::::section -## Two Tools, One Deliberate Boundary - - :::card - The short weekly summary and the full weekly report do different jobs, so I split them. The summary lives inside Timetraked because it reads the daily time entries straight from the same database, which means no export step and no syncing. The Weekly Report Generator is a separate tool. You paste in the summary from Timetraked, and the AI writes a full report from a template built into the system. - - The hardest question was whether to fold the generator into Timetraked too. I kept it separate so it could work with any summary, not only ones tied to one tool's data format, and so its use cases could grow over time. Timetraked is where the data lives, but the generator has no dependency on it. - ::: - - :::card - The cost is maintainability. A separate project means another repository, another set of dependencies, and another thing I have to fix when it breaks. It also adds a copy-and-paste step between the two tools, which is the price of the boundary. I accepted both to keep the generator reusable. - - The template is built in for now, and making it easy to customize for different outputs is the next item on the list. The generator runs in a Docker container today and is currently private, with plans to make it public. - ::: -:::: - -::::section -## Design Decisions - - > The goal was to make the AI feel like a step in a workflow, not a magic trick. - - ### Two-panel Layout - - At wider viewports, the report page uses a two-column layout: controls on the left, output on the right. This came from recognizing that the interaction has two distinct modes, configuring the request and reviewing the result, and that collapsing them into a single scrolling column would blur that separation. - - The left column handles week navigation, the data breakdown, and tone selection. It is the setup side. The right column is purely reactive: it shows whatever state the output is in: **idle**, **generating**, **error**, or a **draft summary ready** to edit and copy. Nothing in the right column requires user input; it only responds to decisions made on the left. - - This separation also meant each column could be designed for its specific job. The left column is dense and interactive. The right column has more breathing room because reading and editing require it. - -
-
-
-
- Report generation preview -
select what to include in the report and the desired tone
-
-
-
-
-
-
- Client Summary -
the report summary, ready to copy into any document
-
-
-
-
- - ### The generation flow as a design experience - - The most deliberate UX decision was treating the AI generation as a visible, legible transition rather than a black-box button press. When generation is in progress, the right panel names the week being summarized and shows staggered skeleton lines that suggest an output is forthcoming. - - The idle state, before anything has been generated, uses a dashed border container with a short explanation of what will appear there. This keeps the two-column layout from reading as broken or incomplete on first load, and gives the right panel a purpose even before it has content. - - Error states are structured and recoverable: a header that names the failure, a plain-language description, and a "Try again" action inline. The error is informative without being alarming. - -
-
-
- Report generation preview -
feedback showing the generation of the report
-
-
-
- -:::: - -::::section - ## Tone as a product decision - - The tone selector (**Standup**, **Client**, **Retrospective**) reflects a real choice I make every week about who I'm writing for. These are not cosmetic variations. Each tone maps to a different set of system prompt instructions sent to the model, producing meaningfully different outputs. - -
-
-
Standup
-
-
- Standup Summary -
terse, first-person, and focused on what moved and what is in progress
-
-
-
-
-
Client
-
-
- Client Summary -
outcome-oriented, professional, and suitable for sharing progress on deliverables
-
-
-
-
-
Retrospective
-
-
- Retrospective Summary -
more reflective, noting themes and shift across the week rather than just outputs
-
-
-
-
- - > Having tone as an explicit control also solves a subtle UX problem: when you change the tone, the existing summary resets so you never mistake a standup summary for a client-ready output. The control communicates its own consequences. - - ### Editable output by default - - :::card-shadow - The generated summary appears in an editable text area, rather than a read-only display. This frames the AI's role correctly: it drafts, the human approves. The copy button and a brief 'changes are not saved' note completes the mental model: this is a staging area, not a final product. - - A "regenerate" button sits alongside the copy button, so it is easy to get a different draft without scrolling back to the controls. The two actions, regenerate and copy, represent the two things a person actually wants to do with a draft: try again or take it. - ::: - -
- - ### AI prompt architecture - - :::card-shadow - The prompt that drives the summary is split into two parts. The system instruction establishes the persona, length constraints (3 to 5 sentences), voice (first-person, no filler phrases), and the specific tone instructions for whichever mode is selected. The user message contains the serialized week data: a lean, human-readable representation of each workday with task titles, descriptions, project names, and durations. - - Before sending, the serialization step strips timestamps, IDs, and non-work categories. What reaches the model is only what a human would include if they were writing the summary prompt by hand. This matters because model output quality is directly proportional to input clarity: sending structured noise produces structured noise back. - - The prompt architecture is designed to evolve. As I use the feature and notice patterns in what I edit or regenerate, the system instructions can be tuned to match how I actually write and what different audiences respond to. The tone categories themselves may expand: a sprint summary or executive update tone would be natural additions as the use cases grow. - ::: -:::: - -
- -::::section - :::card - ### Outcome - - Every week I use the standup tone on Monday morning to orient myself before the week's work begins, with the retrospective tone on Friday to close out the week. Before this feature existed, I skipped that ritual more often than not due to the fact that the tooling made it feel like work. *Now it takes less than a minute*. The summary is available wherever Timetraked is available, and the tone control means the output is usually usable with one or two edits rather than a full rewrite. - - The Weekly Report Generator extends that routine. I use it every week, and it takes less than a minute to produce a full report that used to take me about 30 minutes to write by hand. - - From a portfolio standpoint the project demonstrates a specific combination of skills: identifying friction in a real workflow, making a considered architectural decision about where a feature belongs, designing the interactions states that make AI output feel trustworthy, and shipping something that has been in daily use since it launched. -:::: +
+

Step 1 · Context

+

Summarizing my week took a second tool, a PDF export, and manual cleanup, so I skipped it.

+

Timetraked (formerly TimeTracker Pro) is a Progressive Web App I designed, built, and use every day. It installs like a native app, stores data locally by default, and needs no account. Through the day I create tasks, assign them to projects and categories, and archive each completed day.

+

I built it because every time tracker I tried was either too complex for personal use or too simple to be useful. It has grown into a small, complete product with export formats, project management, offline support, and invoicing.

+
Start screen for Timetraked
+

After archiving each week, I ran a second tool to generate a summary. It only accepted PDFs exported from the time tracker, only ran on a local development server, and produced output that always needed manual cleanup.

+
+

No real integration

The generator only accepted PDFs from the time tracker. The export was the only bridge between them.

+

Availability

It ran on a local Vite server, so it was only reachable from my machine in a single context.

+

Output needing edits

It had no idea who I was writing for, so every summary came out generic and sounded the same week to week.

+
+
Opportunity

The right tool for summarizing my work week is the tool that already knows my work week.

+
+ +
+

Step 2 · Key decisions

+

I extended the app instead of building a better standalone tool, and kept the full report generator separate on purpose.

+

Extend, not replace

+

A standalone tool would still need an export step, a file upload, or an API connection to reach my time data. It would have to work with any input, which means losing the context that makes a summary useful: what categories mean, which projects are billable, and what a typical week looks like for me.

+

Extending Timetraked meant the feature reads from the same local data store as the rest of the app, with no synchronization problem and no format negotiation. It also let the feature be opinionated. It excludes break and lunch categories, understands the project and client structure, and groups a week of task descriptions into a narrative instead of a list.

+

Two tools, one deliberate boundary

+

The short weekly summary and the full weekly report do different jobs. The summary lives inside Timetraked because it reads daily entries from the same database. The Weekly Report Generator is separate: you paste in the summary and the AI writes a full report from a template built into the system.

+

The hardest question was whether to fold the generator in too. I kept it separate so it works with any summary, not only ones tied to one tool’s data format, and so its use cases can grow.

+
+

The cost

Another repository, another set of dependencies, and a copy-and-paste step between the tools. I accepted all three to keep the generator reusable.

+

What is next

The template is built in for now. Making it easy to customize for different outputs is the next item, and the generator is private for now, with plans to make it public.

+
+
+ +
+

Step 3 · Final design

+

The AI is a visible step in a workflow, not a magic trick: configure on the left, review and edit on the right.

+

Two-panel layout

+

At wider viewports, controls sit on the left and output on the right. The interaction has two modes, configuring the request and reviewing the result, and one scrolling column would blur them. The left column handles week navigation, the data breakdown, and tone selection. The right column only reacts, showing one of four states: idle, generating, error, or a draft ready to edit and copy.

+
+
Report generation preview
Select what to include in the report and the desired tone
+
The generated report summary
The report summary, ready to copy into any document
+
+

Generation as a legible transition

+

While generating, the right panel names the week being summarized and shows staggered skeleton lines. The idle state uses a dashed container with a short explanation, so the layout never reads as broken on first load. Errors are structured and recoverable: a header naming the failure, a plain-language description, and an inline “Try again”.

+
Report generation loading state
Feedback showing the generation of the report
+

Tone as a product decision

+

The tone selector reflects a real choice I make every week about who I am writing for. Each tone maps to different system prompt instructions and produces meaningfully different output. Changing the tone resets the existing summary, so you never mistake a standup draft for a client-ready one.

+
+
Standup summary
Standup: terse, first-person, focused on what moved and what is in progress
+
Client summary
Client: outcome-oriented, professional, suitable for sharing progress
+
Retrospective summary
Retrospective: reflective, noting themes and shifts across the week
+
+

Editable output by default

+

The summary appears in an editable text area, which frames the AI correctly: it drafts, the human approves. A copy button and a short “changes are not saved” note complete the model, and a regenerate button sits beside copy for the two things people want to do with a draft: try again or take it.

+

Prompt architecture

+

The system instruction sets the persona, length (3 to 5 sentences), voice (first-person, no filler), and the instructions for the selected tone. The user message carries the serialized week: task titles, descriptions, project names, and durations. Before sending, serialization strips timestamps, IDs, and non-work categories, so the model sees only what a person would write into a prompt by hand. Sending structured noise returns structured noise.

+

The prompt is designed to evolve as I notice what I edit or regenerate, and the tone categories can expand, for example to a sprint summary or an executive update.

+
+ +
+

Step 4 · Impact

+

A ritual I used to skip now takes under a minute.

+

I use the standup tone on Monday morning to orient the week and the retrospective tone on Friday to close it out. Before this feature, I skipped that ritual more often than not because the tooling made it feel like work. The summary is available wherever Timetraked is, and the output usually needs one or two edits instead of a rewrite.

+

The Weekly Report Generator extends the routine. I use it every week, and it produces a full report in under a minute that used to take about 30 minutes to write by hand.

+

The project shows a specific mix of skills: finding friction in a real workflow, deciding where a feature belongs architecturally, designing the interaction states that make AI output feel trustworthy, and shipping something that has been in daily use since launch.

+
diff --git a/src/sass/_case-study.scss b/src/sass/_case-study.scss new file mode 100644 index 00000000..9c5367a9 --- /dev/null +++ b/src/sass/_case-study.scss @@ -0,0 +1,299 @@ +/* Six-step case study template. Steps are camps on the trail. */ +.cs-hero { + max-width: none; + padding-top: var(--space-xl); + padding-bottom: var(--space-md); + background: var(--earth-dune, #e8dcc8); + + figure { + max-width: 1200px; + margin: 0 auto; + } + img { + width: 100%; + height: auto; + aspect-ratio: 16 / 8; + object-fit: cover; + object-position: top; + border: 1px solid var(--earth-brown); + border-radius: var(--radius-lg); + box-shadow: 8px 8px 0 0 rgb(45 31 18 / 15%); + } +} + +.cs-eyebrow, +.cs-kicker, +.cs-facts dt, +.cs-label { + font-family: var(--font-family-mono); + font-size: var(--font-size-xs); + letter-spacing: 0.12em; + text-transform: uppercase; +} +.cs-eyebrow, +.cs-kicker { + color: var(--accent-coral-dark); +} +.cs-headline { + max-width: 980px; + margin: var(--space-sm) 0; + font-family: var(--font-family-serif); + font-size: clamp(2rem, 4.6vw, 3.25rem); + letter-spacing: -0.02em; + line-height: 1.1; + text-shadow: none; +} +.cs-deck { + max-width: 760px; + font-size: 1.25rem; + line-height: 1.5; + color: var(--earth-stone, #6b5d52); +} +.cs-facts { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: var(--space-lg); + margin: var(--space-lg) 0; + + div { + padding-top: var(--space-sm); + border-top: 2px solid var(--earth-dark); + } + dt { + color: var(--earth-stone, #6b5d52); + } + dd { + margin: var(--space-2xs) 0 0; + line-height: 1.5; + } +} +.cs-results { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--space-md); + margin: 0 0 var(--space-md) !important; + padding: 0 !important; + list-style: none; + + li { + display: flex; + flex-direction: column; + gap: var(--space-2xs); + padding: var(--space-md); + background: var(--earth-cream); + border: 1px solid var(--earth-brown); + border-radius: var(--radius-xl); + box-shadow: 8px 8px 0 0 rgb(45 31 18 / 15%); + } +} +.cs-result-value { + font-size: clamp(1.75rem, 3.4vw, 2.25rem); + font-weight: 700; + line-height: 1; + letter-spacing: -0.02em; + color: var(--accent-coral-dark); +} +.cs-note { + font-size: var(--font-size-sm, 0.875rem); + font-style: italic; + color: var(--earth-stone, #6b5d52); +} + +/* Steps */ +.cs-step { + display: flow-root; + max-width: 1200px; + + &.cs-band { + max-width: none; + padding-right: max(var(--space-lg), calc((100% - 1200px) / 2 + var(--space-lg))); + padding-left: max(var(--space-lg), calc((100% - 1200px) / 2 + var(--space-lg))); + background: var(--earth-dune, #e8dcc8); + } + h2 { + max-width: 900px; + margin: var(--space-xs) 0 var(--space-md); + font-family: var(--font-family-serif); + font-size: clamp(1.625rem, 3.2vw, 2.25rem); + line-height: 1.2; + letter-spacing: -0.01em; + } + h2::after { + content: none; + } + > p { + max-width: 720px; + line-height: 1.55; + } + h3 { + margin: 0 0 var(--space-xs); + font-family: var(--font-family-sans); + font-size: 1.0625rem; + line-height: 1.3; + } +} +.cs-kicker { + display: flex; + gap: var(--space-sm); + align-items: center; + + &::before { + content: ''; + width: 32px; + height: 1px; + background: var(--accent-coral-dark); + } +} +.cs-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: var(--space-md); + margin: var(--space-md) 0; +} +.cs-card { + padding: var(--space-md); + background: var(--earth-cream); + border: 1px solid var(--earth-brown); + border-radius: var(--radius-xl); + box-shadow: 8px 8px 0 0 rgb(45 31 18 / 15%); + + p { + margin: 0; + color: var(--earth-stone, #6b5d52); + } +} +.cs-quote { + max-width: 860px; + background: none; + border: 0; + box-shadow: none; + margin: var(--space-lg) auto; + padding: var(--space-sm) 0; + text-align: center; + + &::before, + &::after { + content: ''; + display: block; + width: 56px; + height: 2px; + margin: var(--space-sm) auto; + background: var(--accent-coral-dark); + } + p { + margin: 0; + font-family: var(--font-family-serif); + font-size: clamp(1.375rem, 2.8vw, 1.75rem); + font-style: italic; + font-weight: 500; + line-height: 1.35; + color: var(--accent-coral-dark); + } +} +.cs-label { + display: block; + margin-bottom: var(--space-xs); + color: var(--accent-coral-dark); +} +.cs-figure { + margin: var(--space-md) 0; + + img { + width: 100%; + height: auto; + border: 1px solid var(--earth-brown); + border-radius: var(--radius-md); + background: var(--earth-cream); + } + figcaption { + margin-top: var(--space-xs); + font-size: var(--font-size-sm, 0.875rem); + color: var(--earth-stone, #6b5d52); + } +} +.cs-figures { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: var(--space-md); + margin: var(--space-md) 0; + + .cs-figure { + margin: 0; + } + img { + aspect-ratio: 3 / 2; + object-fit: cover; + object-position: top; + } +} +.cs-decision { + display: grid; + grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); + gap: var(--space-lg); + align-items: start; + margin: var(--space-lg) 0; + + .cs-figure { + margin: 0; + } +} +.cs-assumption { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--space-md); + margin: var(--space-lg) 0; + + div { + padding-top: var(--space-sm); + border-top: 2px solid var(--earth-dark); + } + p { + margin: 0; + } +} + +@media (width <= 900px) { + .cs-facts, + .cs-results, + .cs-assumption { + grid-template-columns: 1fr 1fr; + } + .cs-decision { + grid-template-columns: 1fr; + } +} +@media (width <= 600px) { + .cs-facts, + .cs-results, + .cs-assumption { + grid-template-columns: 1fr; + } +} + +.cs-decision.cs-single { + grid-template-columns: 1fr; +} + +/* Phone screenshots keep their own proportions */ +.cs-figures.cs-phones { + grid-template-columns: repeat(auto-fit, minmax(200px, 280px)); + justify-content: center; + + img { + aspect-ratio: auto; + max-height: 640px; + object-fit: contain; + } +} +.cs-list { + max-width: 720px; + margin: var(--space-sm) 0 var(--space-md) !important; + padding-left: var(--space-lg) !important; + line-height: 1.55; +} + +.cs-card pre { + margin: var(--space-xs) 0 0; + overflow-x: auto; + font-size: var(--font-size-sm, 0.875rem); +} diff --git a/src/sass/_home.scss b/src/sass/_home.scss new file mode 100644 index 00000000..485922b0 --- /dev/null +++ b/src/sass/_home.scss @@ -0,0 +1,325 @@ +/* Home page camps: work stack, principles, proof, about, summit */ +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + border: 0; +} + +.hero-kicker { + font-family: var(--font-family-mono); + font-size: var(--font-size-xs); + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--accent-coral-dark); +} +.hero-statement { + margin: var(--space-sm) 0 var(--space-md); + font-family: var(--font-family-serif); + font-size: clamp(2.25rem, 5.5vw, 4rem); + letter-spacing: -0.01em; + line-height: 1.1; + text-shadow: none; +} + +.camp { + display: flow-root; +} +.camp-band { + max-width: none; + padding-right: max(var(--space-lg), calc((100% - 1200px) / 2 + var(--space-lg))); + padding-left: max(var(--space-lg), calc((100% - 1200px) / 2 + var(--space-lg))); + background: var(--earth-dune, #e8dcc8); +} +.camp-kicker, +.camp-label { + font-family: var(--font-family-mono); + font-size: var(--font-size-xs); + letter-spacing: 0.12em; + text-transform: uppercase; +} +.camp-kicker { + margin-bottom: var(--space-xs); + color: var(--accent-coral-dark); +} +.camp-label { + color: var(--earth-stone, #6b5d52); +} + +/* Selected work: cards pin and stack as the next one arrives */ +.stack { + display: grid; + gap: var(--space-xl); + margin-top: var(--space-xl); +} +.stack-card { + position: sticky; + top: calc(6rem + var(--i, 0) * 1rem); + display: grid; + grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); + gap: var(--space-lg); + align-items: center; + padding: var(--space-lg); + background: var(--earth-cream); + border: 1px solid var(--earth-brown); + border-radius: var(--radius-xl); + box-shadow: 8px 8px 0 0 rgb(45 31 18 / 15%); + + img { + width: 100%; + height: auto; + aspect-ratio: 3 / 2; + object-fit: cover; + object-position: top; + border: 1px solid var(--earth-brown); + border-radius: var(--radius-md); + } + h3 { + margin: var(--space-xs) 0; + font-family: var(--font-family-sans); + font-size: clamp(1.25rem, 2.4vw, 1.75rem); + line-height: 1.2; + } + h3 a { + color: var(--earth-dark); + border-bottom: 0; + + /* The whole card is the target */ + &::after { + content: ''; + position: absolute; + inset: 0; + } + &:hover, + &:focus-visible { + text-decoration: underline; + text-decoration-color: var(--accent-coral); + text-underline-offset: 4px; + } + } +} +.chips { + display: flex; + flex-wrap: wrap; + gap: var(--space-xs); + margin: var(--space-sm) 0 0 !important; + padding: 0 !important; + list-style: none; + + li { + padding: 2px var(--space-sm); + font-size: var(--font-size-sm, 0.875rem); + background: var(--earth-dune, #e8dcc8); + border-radius: var(--radius-pill); + } +} + +.more-work { + margin-top: var(--space-3xl); +} +.more-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: var(--space-md); + margin: var(--space-sm) 0 0 !important; + padding: 0 !important; + list-style: none; +} +.more-tile { + display: flex; + flex-direction: column; + gap: var(--space-xs); + height: 100%; + padding: var(--space-md); + color: var(--earth-dark); + background: var(--earth-cream); + border: 1px solid var(--earth-brown); + border-bottom-width: 1px; + border-radius: var(--radius-lg); + box-shadow: 4px 4px 0 0 rgb(45 31 18 / 15%); + + strong { + font-weight: 600; + } + &:hover strong, + &:focus-visible strong { + text-decoration: underline; + text-decoration-color: var(--accent-coral); + text-underline-offset: 4px; + } +} +.more-tile-soon { + border-style: dashed; + box-shadow: none; +} + +/* Principles */ +.principle-quote { + max-width: 900px; + margin-bottom: var(--space-xl); + font-size: clamp(2rem, 5vw, 3.5rem); + line-height: 1.15; +} +.principles { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--space-lg); + margin-top: var(--space-xl) !important; + margin-bottom: 0 !important; + padding: 0 !important; + list-style: none; + + li { + padding-top: var(--space-sm); + border-top: 2px solid var(--earth-dark); + } + h3 { + margin-bottom: var(--space-xs); + font-family: var(--font-family-sans); + font-size: 1.125rem; + } +} + +/* Proof */ +.proof-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--space-lg); + margin: var(--space-lg) 0 0 !important; + padding: 0 !important; + list-style: none; +} +.proof-card { + display: flex; + flex-direction: column; + gap: var(--space-xs); + height: 100%; + padding: var(--space-lg); + color: var(--earth-dark); + background: var(--earth-cream); + border: 1px solid var(--earth-brown); + border-radius: var(--radius-xl); + box-shadow: 8px 8px 0 0 rgb(45 31 18 / 15%); + + &:hover, + &:focus-visible { + box-shadow: 12px 12px 0 0 rgb(45 31 18 / 25%); + } +} +.proof-number { + font-family: var(--font-family-serif); + font-size: clamp(3rem, 7vw, 4.5rem); + font-weight: 700; + line-height: 1; + color: var(--accent-coral-dark); +} +.proof-link { + margin-top: auto; + padding-top: var(--space-sm); + font-weight: 600; +} + +/* About */ +.about-camp { + max-width: 900px; +} +.app-tiles { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--space-md); + margin: var(--space-lg) 0 !important; + padding: 0 !important; + list-style: none; +} +.app-tile { + display: flex; + flex-direction: column; + gap: var(--space-2xs); + height: 100%; + padding: var(--space-md); + color: var(--earth-dark); + background: var(--earth-cream); + border: 1px solid var(--earth-brown); + border-radius: var(--radius-lg); + box-shadow: 4px 4px 0 0 rgb(45 31 18 / 15%); + + span { + font-weight: 400; + } + &:hover, + &:focus-visible { + box-shadow: 8px 8px 0 0 rgb(45 31 18 / 25%); + } +} + +/* Summit */ +.summit { + min-height: 70vh; + display: flex; + flex-direction: column; + justify-content: center; +} +.summit-actions { + display: flex; + flex-wrap: wrap; + gap: var(--space-md); + align-items: center; + margin-top: var(--space-lg); +} +.summit-top { + min-height: 44px; + display: inline-flex; + align-items: center; + font-weight: 600; +} + +/* Cards pin only where there is room; elsewhere they stack in normal flow */ +@media (width <= 900px) { + .stack-card { + position: static; + grid-template-columns: 1fr; + } + .more-grid, + .proof-grid, + .principles, + .app-tiles { + grid-template-columns: 1fr 1fr; + } +} +@media (width <= 600px) { + .more-grid, + .proof-grid, + .principles, + .app-tiles { + grid-template-columns: 1fr; + } +} +@media (prefers-reduced-motion: reduce) { + .stack-card { + position: static; + } +} + +/* Motion: styled only when motion.js runs and reduced motion is not requested */ +@media (prefers-reduced-motion: no-preference) { + .quote-word { + color: var(--earth-stone-light, #968a80); + transition: color 200ms ease; + } + .quote-word.is-lit { + color: var(--earth-dark); + } + .stack-card { + transform-origin: top center; + scale: calc(1 - 0.04 * var(--stack-p, 0)); + filter: brightness(calc(1 - 0.06 * var(--stack-p, 0))); + } + .app-tile { + transition: transform 150ms ease, box-shadow 150ms ease; + } +} diff --git a/src/sass/_landing.scss b/src/sass/_landing.scss index 094eeb7f..c3e3d87b 100644 --- a/src/sass/_landing.scss +++ b/src/sass/_landing.scss @@ -261,6 +261,20 @@ } } +/* Phones: the scene gets a fixed height and the hero reserves that space below + the text, so the tagline never runs over the summit, flag, or sun */ +@media (width <= 600px) { + .hero-trail { + --scene-height: clamp(200px, 30svh, 300px); + + padding-top: clamp(2rem, 6svh, 4rem); + padding-bottom: calc(var(--scene-height) + var(--space-md, 1rem)); + } + .hero-scene { + height: var(--scene-height); + } +} + @keyframes hero-ridge-rise { from { opacity: 0; diff --git a/src/sass/_trail.scss b/src/sass/_trail.scss new file mode 100644 index 00000000..da2c3968 --- /dev/null +++ b/src/sass/_trail.scss @@ -0,0 +1,283 @@ +/* Trail progress: camp ticks, rail hiker (desktop), progress track (phones) */ +.trail-ticks, +.trail-rail, +.trail-track { + --trail-p: 0; +} + +.trail-ticks { + position: fixed; + top: var(--space-lg); + right: var(--space-lg); + z-index: 999; + display: flex; + align-items: center; + gap: var(--space-sm); + padding: var(--space-xs) var(--space-sm); + background: var(--earth-cream); + border: 1px solid var(--earth-brown); + border-radius: var(--radius-pill); + box-shadow: 3px 3px 0 var(--earth-brown); + + ol { + display: flex; + list-style: none; + margin: 0 !important; + padding: 0 !important; + } + + a, + a:hover, + a:focus { + border-bottom: 0; + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + text-decoration: none; + } +} +.trail-tick { + display: block; + box-sizing: border-box; + width: 10px; + height: 10px; + border: 2px solid var(--earth-sage); + border-radius: 50%; +} +.trail-ticks a.is-past .trail-tick, +.trail-ticks a[aria-current='location'] .trail-tick { + background: var(--accent-coral-dark); + border-color: var(--accent-coral-dark); +} +.trail-ticks a[aria-current='location'] .trail-tick { + box-shadow: 0 0 0 3px var(--earth-cream), 0 0 0 4px var(--accent-coral-dark); +} +.trail-count { + font-family: var(--font-family-mono); + font-size: var(--font-size-xs); + color: var(--earth-brown); + white-space: nowrap; +} + +/* Rail and track only exist once JS has set them up */ +.trail-rail, +.trail-track { + display: none; +} +.js-trail .trail-rail { + position: fixed; + top: 96px; + bottom: 32px; + left: 24px; + z-index: 998; + display: block; + width: 24px; + opacity: 0; + pointer-events: none; +} +.js-trail.trail-rail-on .trail-rail { + opacity: 1; +} +.trail-rail-line, +.trail-rail-fill { + position: absolute; + top: 0; + bottom: 0; + left: 10px; + width: 4px; + border-radius: 2px; +} +.trail-rail-line { + background: var(--earth-sand); +} +.trail-rail-fill { + background: var(--accent-coral-dark); + transform: scaleY(var(--trail-p)); + transform-origin: top; +} +.trail-rail-hiker { + position: absolute; + top: 0; + left: 1px; + color: var(--earth-dark); + + /* Travel the rail's height minus the hiker's own height */ + transform: translateY(calc(var(--trail-p) * (100vh - 128px - 28px))); +} +.trail-hiker { + display: block; +} +.trail-hiker-pack { + fill: var(--accent-coral); +} +.trail-rail-flag { + position: absolute; + bottom: 0; + left: 12px; + width: 2px; + height: 36px; + background: var(--earth-dark); + transform: scaleY(0); + transform-origin: bottom; + + &::after { + content: ''; + position: absolute; + top: 0; + left: 2px; + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + border-left: 16px solid var(--accent-coral); + } +} +.trail-at-summit .trail-rail-flag { + transform: scaleY(1); +} + +@media (width <= 1099px) { + /* Camp ticks become a bottom bar */ + .trail-ticks { + inset: auto 0 0; + justify-content: space-between; + padding: 0 var(--space-xs) 0 var(--space-md); + background: var(--earth-dark); + border: 0; + border-radius: 0; + box-shadow: none; + min-height: 44px; + + a { + height: 44px; + } + } + .trail-tick { + border-color: var(--earth-mist, #b4a89c); + } + .trail-ticks a.is-past .trail-tick, + .trail-ticks a[aria-current='location'] .trail-tick { + background: var(--accent-coral); + border-color: var(--accent-coral); + } + .trail-ticks a[aria-current='location'] .trail-tick { + box-shadow: 0 0 0 3px var(--earth-dark), 0 0 0 4px var(--accent-coral); + } + .trail-count { + order: -1; + color: var(--earth-cream); + } + body.has-trail { + padding-bottom: 44px; + } + + /* Keep the scroll-to-top button clear of the bar */ + /* stylelint-disable-next-line selector-id-pattern */ + body.has-trail #goTopBtn { + bottom: 60px !important; + } +} + +@media (width <= 1199px) { + /* No gutter for the rail: a strip above the bottom edge carries the hiker */ + .js-trail .trail-rail { + display: none; + } + .js-trail .trail-track { + position: fixed; + right: 0; + bottom: 0; + left: 0; + z-index: 998; + display: block; + height: 34px; + background: var(--earth-cream); + border-top: 1px solid var(--earth-brown); + pointer-events: none; + } + .trail-track-line, + .trail-track-fill { + position: absolute; + right: var(--space-md); + bottom: 5px; + left: var(--space-md); + height: 4px; + border-radius: 2px; + } + .trail-track-line { + background: var(--earth-sand); + } + .trail-track-fill { + right: auto; + width: calc((100% - 2rem) * var(--trail-p)); + background: var(--accent-coral-dark); + } + .trail-track-hiker { + position: absolute; + top: 1px; + left: var(--space-md); + color: var(--earth-dark); + transform: translateX(calc((100vw - 2rem) * var(--trail-p) - 11px)); + } + body.has-trail { + padding-bottom: 34px; + } + + /* stylelint-disable-next-line selector-id-pattern */ + body.has-trail #goTopBtn { + bottom: 50px !important; + } +} + +@media (width <= 1099px) { + /* Camp ticks become a bar under the track */ + .trail-ticks { + inset: auto 0 0; + justify-content: space-between; + padding: 0 var(--space-xs) 0 var(--space-md); + background: var(--earth-dark); + border: 0; + border-radius: 0; + box-shadow: none; + min-height: 44px; + + a { + height: 44px; + } + } + .trail-tick { + border-color: var(--earth-mist, #b4a89c); + } + .trail-ticks a.is-past .trail-tick, + .trail-ticks a[aria-current='location'] .trail-tick { + background: var(--accent-coral); + border-color: var(--accent-coral); + } + .trail-ticks a[aria-current='location'] .trail-tick { + box-shadow: 0 0 0 3px var(--earth-dark), 0 0 0 4px var(--accent-coral); + } + .trail-count { + order: -1; + color: var(--earth-cream); + } + .js-trail .trail-track { + bottom: 44px; + } + body.has-trail { + padding-bottom: 78px; + } + + /* stylelint-disable-next-line selector-id-pattern */ + body.has-trail #goTopBtn { + bottom: 94px !important; + } +} + +@media (prefers-reduced-motion: no-preference) { + .js-trail .trail-rail { + transition: opacity 300ms ease; + } + .trail-rail-flag { + transition: transform 0.5s cubic-bezier(0.3, 1.6, 0.5, 1); + } +} diff --git a/src/sass/style.scss b/src/sass/style.scss index f39ad9a2..598ec536 100644 --- a/src/sass/style.scss +++ b/src/sass/style.scss @@ -177,6 +177,9 @@ figcaption { @import 'cards'; @import 'gallery'; @import 'landing'; +@import 'trail'; +@import 'home'; +@import 'case-study'; @import 'topo'; // @import 'containers'; diff --git a/src/sw.js b/src/sw.js index a7c46a98..7bee1b0e 100755 --- a/src/sw.js +++ b/src/sw.js @@ -5,6 +5,11 @@ importScripts( // This is your Service Worker, you can put any of your custom Service Worker // code in this file, above the `precacheAndRoute` line. +// Take over as soon as a new service worker installs. Without this, a deploy is +// served stale from the old precache until every tab of the site is closed. +self.skipWaiting(); +workbox.core.clientsClaim(); + // When widget is installed/pinned, push initial state. self.addEventListener('widgetinstall', (event) => { event.waitUntil(updateWidget(event));