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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,4 @@ dist

# playwright
test-results/
playwright-report/
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,17 @@ Validation currently checks for:
- Playlist items that reference missing content
- Podcast episodes that reference unknown `showSlug` values

### Styling Conventions

The CSS refactor keeps global and shared concerns separate from one-off component rules:

- `src/styles/global.css` is the root entrypoint for base rules and shared stylesheet imports
- `src/styles/layout/` contains reusable page and content-shell layout primitives
- `src/styles/components/` contains shared card, metadata, and section-header primitives
- component-local `<style>` blocks should only keep rules that are unique to that component

If a style is repeated across multiple cards, layouts, metadata rows, or section headers, it belongs in the shared `src/styles/` layer rather than being copied into another Astro component.

### Content Aggregation

The ingestion scripts collect data from:
Expand Down Expand Up @@ -226,6 +237,8 @@ Refresh baselines locally with:
npm run test:visual:update
```

Run `npm run test:visual` after changing shared styling, layout structure, or reusable component rendering. If the changes are intentional and the new screenshots are correct, refresh the baselines with `npm run test:visual:update`.

## Deployment

The site is deployed via Netlify when code is pushed to the main branch. Content aggregation runs on scheduled GitHub Actions to keep the collection up to date.
Expand Down Expand Up @@ -267,6 +280,12 @@ When adding new pages or APIs:
- keep route files focused on shaping page props, not indexing collections
- run `npm run test:visual` when changing layout, styling, or component rendering

When changing CSS:

- prefer shared primitives in `src/styles/` for reused patterns
- keep one-off styles inside the component that owns them
- update or add an ADR when the styling change introduces a durable architectural convention

### Adding New Video Sources

To suggest new YouTube videos, channels, or playlists:
Expand Down
50 changes: 50 additions & 0 deletions docs/adr/0003-shared-css-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# ADR 0003: Organize shared CSS primitives while keeping unique styles local

## Status

Accepted

## Context

Styling had drifted toward repeated component-local `<style>` blocks. High-traffic UI surfaces such as cards, metadata rows, detail layouts, section headers, and taxonomy headers repeated the same CSS patterns with only small local differences.

This duplication made small UI changes slower and riskier because several components needed to stay visually aligned by convention rather than by shared primitives.

## Decision

We kept Astro component-local styles for one-off presentation details, but extracted repeated patterns into shared stylesheets under `src/styles/` and imported them through `src/styles/global.css`.

The shared layer now covers:

- `src/styles/layout/` for page-level content-shell, detail-page meta/prose layout, and taxonomy layout primitives
- `src/styles/components/cards.css` for shared card structure, shared list-card layout, and hover treatment
- `src/styles/components/meta.css` for metadata primitives, taxonomy links, and badges
- `src/styles/components/sections.css` for section and card header structure

Shared layout ownership is intentionally centralized:

- `BaseLayout` owns page shell behavior, including the desktop sidebar layout and its `1024px` collapse
- `ResponsiveGrid` owns collection layout behavior, including the `768px` single-column collapse for grids
- `content.css` owns the common detail-page content shell, title width/padding, and meta-to-prose collapse at `1024px`

This also means reusable components should not introduce competing layout shifts:

- cards now support only two structural families, `grid` and `list`
- the old mobile-only card rewrites were removed so cards no longer change structure independently of their container
- metadata layout is shared through `meta.css` and detail-page shell helpers instead of being redefined in each template

Component-local `<style>` blocks remain the place for styles that are unique to a single component, such as hero treatments, artwork presentation, line clamping, episode-table styling, or one page-only spacing rules.

## Consequences

Repeated CSS is reduced and shared UI patterns are easier to evolve consistently.

`src/styles/global.css` now acts as the root style entrypoint and should stay focused on global base rules plus shared imports, rather than absorbing more page-specific styling.

The responsive system is simpler and more predictable:

- `1024px` is the main page-structure breakpoint for sidebar and detail layouts
- `768px` is the collection-grid and mobile-navigation breakpoint
- `425px` remains a compact-density breakpoint rather than introducing new structural layout changes

This approach keeps the existing desktop compositions intact, but it introduces a clearer expectation: when a styling change affects reusable UI structure, visual regression tests should be run to verify the shared primitives did not introduce unintended diffs elsewhere.
18 changes: 18 additions & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Architecture Decision Records

This directory tracks durable architectural decisions for DesignSystems.media.

## Index

1. [ADR 0001: Centralize content loading, indexing, and validation](./0001-content-domain-and-validation.md)
2. [ADR 0002: Split smoke coverage from visual regression coverage](./0002-smoke-and-visual-regression-testing.md)
3. [ADR 0003: Organize shared CSS primitives while keeping unique styles local](./0003-shared-css-architecture.md)

## Format

Each ADR uses the same structure:

- `Status`
- `Context`
- `Decision`
- `Consequences`
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"test:unit": "node --experimental-strip-types --test src/tests/unit/content-domain.test.ts src/tests/unit/content-validation.test.mjs",
"test:smoke": "npm run build && npx playwright test --project=smoke",
"test:visual": "npm run build && npx playwright test --project=visual-desktop --project=visual-mobile",
"test:visual:report": "npm run build && npx playwright test --reporter=html --project=visual-desktop --project=visual-mobile",
"test:visual:update": "npm run build && npx playwright test --update-snapshots --project=visual-desktop --project=visual-mobile"
},
"dependencies": {
Expand Down
38 changes: 3 additions & 35 deletions src/components/CardHeader.astro
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,13 @@ interface Props {
const { title, link, linkLabel, padding } = Astro.props;
---

<div class:list={{ padding: padding }}>
<h3>{title}</h3>
<div class:list={["section-heading", { "section-heading--padding": padding }]}>
<h3 class="section-heading__title section-heading__title--small">{title}</h3>
{
link && (
<a href={link} title={`View more ${title}`}>
<a href={link} title={`View more ${title}`} class="section-heading__link">
{linkLabel}
</a>
)
}
</div>

<style>
div {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-block: 0rem 0;
margin-inline: 0rem;
align-items: baseline;
}
div.padding {
margin-inline: 1rem;
}
h3 {
padding: 0;
margin: 0;
font-size: 1rem;
font-weight: 500;
text-transform: uppercase;
}
a {
color: rgb(var(--accent));
text-decoration: none;
border-bottom: 1px dashed var(--accent-hover);
line-height: 1.3;
font-size: 0.8em;
}
a:hover {
color: var(--accent-hover);
border-bottom: 1px solid var(--accent-hover);
}
</style>
105 changes: 9 additions & 96 deletions src/components/PodcastCard.astro
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,14 @@ const isListLayout = layoutType === "list";

<div
class:list={[
"media-item",
"media-link", // Keep this for common styles
"media-card",
"media-card--hover",
{
"media-list-layout": isListLayout, // Apply this class when it's a list
"media-grid-layout": !isListLayout, // Apply this class when it's a grid (default)
"media-card--list": isListLayout,
},
]}
>
<a href={`/podcast/${slug}/`}>
<a href={`/podcast/${slug}/`} class="media-card__link media-card__image media-card__image--video">
<MediaImage
src={imageToUse}
alt={title}
Expand All @@ -54,10 +53,10 @@ const isListLayout = layoutType === "list";
loading={loading}
/>
</a>
<div class="media-content">
<a href={`/podcast/${slug}/`} class="title"><h3>{title}</h3></a>
<a href={`/show/${showSlug}/`} class="meta showTitle">{podcastTitle}</a>
<div class="meta">
<div class="media-card__content">
<a href={`/podcast/${slug}/`} class="media-card__title-link"><h3 class="media-card__title">{title}</h3></a>
<a href={`/show/${showSlug}/`} class="media-card__eyebrow">{podcastTitle}</a>
<div class="media-card__meta">
<span><FormattedDate date={publishedAt} /></span> {isListLayout &&
<span>•</span> <span>{formatDuration(duration)}</span>
}
Expand All @@ -67,94 +66,8 @@ const isListLayout = layoutType === "list";
</div>

<style>
/* Style for each media item */
.media-item {
margin: 0;
}
.media-link {
text-decoration: none;
display: flex;
flex-direction: column;
gap: 1rem;

img {
max-width: 100%;
height: auto;
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
}

.media-link:hover {
.media-image img {
/* outline: 1px solid var(--accent-hover); */
box-shadow:
0 0px 40px var(--accent-glow),
0 0px 10px var(--accent-hover);
outline: 1px solid var(--accent-hover);
transition: box-shadow 1s ease-out;
}
.title {
color: rgb(var(--accent));
}
}
a:hover h3 {
color: rgb(var(--accent));
}
.media-content a {
.media-card__content a {
text-decoration: none;
border-bottom: none;
}

a h3 {
font-size: 17px;
font-weight: 400;
margin: 2px 0;
}

a.showTitle {
font-size: 15px;
font-weight: 400;
color: var(--tx-2);
}
a.showTitle:hover {
text-decoration: underline;
}

.media-content {
display: flex;
flex-direction: column;
gap: 2px;
}
.media-content .meta{
font-size: 14px;
color: var(--tx-3);
display: flex;
flex-direction: row;
gap: 8px
}

/* Default "grid-like" layout (your current mobile layout) */
.media-grid-layout {
display: flex; /* Or grid, if you prefer a 2-column layout by default */
flex-direction: column;
gap: 1rem;
}

/* New list layout (similar to your @media rule) */
.media-list-layout {
display: grid;
grid-template-columns: minmax(80px, 100px) 1fr; /* The layout you want for list */
gap: 1rem; /* Adjust gap as needed for list */
align-items: start; /* Align items to top */
}

@media screen and (max-width: 768px) {
.media-grid-layout {
display: grid;
grid-template-columns: 1fr 4fr;
align-items: center;
}
}
</style>
37 changes: 3 additions & 34 deletions src/components/SectionHeader.astro
Original file line number Diff line number Diff line change
Expand Up @@ -10,44 +10,13 @@ interface Props {
const { title, link, linkLabel, padding,anchor } = Astro.props;
---

<div class:list={{ padding: padding }}>
<h2 id={anchor}>{title}</h2>
<div class:list={["section-heading", { "section-heading--padding": padding, "section-heading--loose": padding }]}>
<h2 id={anchor} class="section-heading__title section-heading__title--large">{title}</h2>
{
link && (
<a href={link} title={`View more ${title}`}>
<a href={link} title={`View more ${title}`} class="section-heading__link">
{linkLabel}
</a>
)
}
</div>

<style>
div {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-block: 0rem 0;
margin-inline: 0rem;
align-items: baseline;
}
div.padding {
margin-inline: 1rem;
margin-block: 1rem 0;
}
h2 {
padding: 0;
margin: 0;
font-size: 1.5rem;
}
a {
color: rgb(var(--accent));
text-decoration: none;
border-bottom: 1px dashed var(--accent-hover);
line-height: 1.1;
font-size: 0.8em;
}
a:hover {
color: var(--accent-hover);
border-bottom: 1px solid var(--accent-hover);
}
</style>
Loading