Skip to content

1351: Epic: Migrate the full component library to Single Directory Components (SDC) - #482

Open
dblanken-yale wants to merge 55 commits into
developfrom
1351-migrate-cl-to-sdc
Open

1351: Epic: Migrate the full component library to Single Directory Components (SDC)#482
dblanken-yale wants to merge 55 commits into
developfrom
1351-migrate-cl-to-sdc

Conversation

@dblanken-yale

@dblanken-yale dblanken-yale commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

1351: Epic: Migrate the full component library to Single Directory Components (SDC)

This is the consolidated atomic PR for epic #1351 — the full SDC migration work, delivered
incrementally across the per-wave PRs below (all stacked; this branch is their tip). Each per-wave
PR is kept for history but closed in favor of this one for review.

Description of work

Thin-wrapper SDCs live in atomic/components/<name>/ (schema *.component.yml + a Twig shim that
delegates to the canonical @atoms/@molecules/@organisms CLT template); Layout Builder block
templates are repointed at them; assets attach via libraryOverrides. 27 components converted.

Constituent atomic PRs (what each did):

Functional testing steps:

  • On the 1351-migrate-cl-to-sdc multidev, place/visit each converted block and confirm it renders identically to production
  • Confirm the accordion/tabs JS behaviors work; confirm no Layout Builder editor regressions
  • lando composer test:sdc — schema tests green (30 tests)

References yalesites-org/YaleSites-Internal#1351

Wave 0 pilot of the full SDC migration (epic #1351). Adds Single Directory
Components under atomic/components/ that delegate to the canonical
component-library templates (the thin-wrapper architecture), and repoints the
matching Layout Builder block templates at them.

- divider (props-only), callout (props + dial enum + slot), accordion
  (props + slots + JS via libraryOverrides): each a .component.yml schema and
  a .twig shim.
- Dial enums match the field formatter (list_key -> machine keys,
  list_default -> labels); block templates default NULL dial values (the
  adapter layer), matching pre-SDC behavior.
- Slots are forwarded with the block() capture + {% if false %} receiver
  pattern; `directory` is threaded so nested icon atoms resolve the SVG
  sprite path.
- Adds a PHP schema-validation Unit test using justinrainbow/json-schema.

Verified on Lando: all three render byte-identical to the pre-SDC baseline on
real pages, and the accordion JS attaches via libraryOverrides.
Wave 1 (atoms) of the SDC migration (epic #1351), applying the Wave 0 recipe to
the two most-reused typography atoms — the flagged Web Component candidates.

- heading (h1-h6, optional link + prefix icon) and text (rich-text wrapper):
  each a .component.yml schema + thin-wrapper .twig shim over the
  @atoms/typography templates. Both consume their content as a variable, so the
  slot is captured with block() and passed through (|trim for clean output).
- Extends the schema-validation Unit test with heading/text cases.

These atoms are authored from the Twig (no *-props.yml exists for typography).
Parents keep including the raw @atoms/ templates, so no page render path
changes — verified parents still render (200) and each SDC renders identical to
its direct include. Reuses the Wave-0-reviewed slot/shim pattern.

NOTE: this is a representative slice of Wave 1. The remaining atoms (controls,
images/icons, lists, tables, date-time, video, forms, and the utility
molecules) are staged for the same treatment; see the epic decisions log.
…copy-button)

Wave 1 (atoms) of the SDC migration (epic #1351). Converts the controls family
(the flagged Web Component candidates) per the Wave 0 recipe.

- cta (content is a variable slot), text-link (link__content block slot; maps
  link__style onto the source's single-underscore `link_style` var), and
  text-copy-button (block slot + pre_text; JS via libraryOverrides
  atomic/text-copy-button). Enum props authored from the Twig comment headers
  (controls share a story-level control-props.yml, so not codegen-seedable).
- button is SCSS-only (a CTA style variant, no template) so there is nothing to
  convert.
- Optional scalar props that consumers may omit are nullable ([string,"null"]);
  the source templates apply their own defaults.
- Extends the schema-validation Unit test (11 tests total).

Each renders identical to its direct @Atoms include; parents unchanged (200).
Wave 1 (atoms) of the SDC migration (epic #1351). Demonstrates the
scripts/sdc/generate-sdc.js --write flow end-to-end on a real component: the
generator seeded lists.component.yml (dropping the data-attribute sectionTheme
prop, deriving the ul/ol enum, detecting the list__content slot); refined by
hand to drop the story-only list__items array-prop in favor of the slot.

Renders identical to the direct @atoms/lists include; schema test added.
Wave 1 (atoms) of the SDC migration (epic #1351). A props-only wrapper over
@atoms/date-time/yds-date-time.twig (the date-formatting logic stays in the
component library). Date inputs typed to accept timestamps or strings; optional
end/all-day/override are nullable. Renders identical to the direct include;
schema test added (13 tests).
Wave 1 (atoms) of the SDC migration (epic #1351). link-skip (url prop +
content slot) and read-time (label prop; the minutes value is computed by the
component JS, attached via libraryOverrides atomic/read-time). Both render
identical to their direct includes; schema tests added (15 tests).
Wave 1 (atoms/primitives) of the SDC migration (epic #1351). theme enum prop +
width, with the taxonomy items forwarded as a slot into the template's
taxonomy_display__items block. Renders identical to the direct include;
schema test added (16 tests).
Wave 1 (atoms) of the SDC migration (epic #1351). Thin wrapper over
@atoms/images/image/yds-image.twig: the image markup (typically a Drupal
responsive-image render array) is forwarded as a slot into the image__image
block, with an optional figure caption. Renders identical to the direct
include for both the figure (captioned) and bare-<img> paths; schema test
added (17 tests).
Wave 1 of the SDC migration (epic #1351). basic-meta is a trivial wrapper over
the text atom; the meta content is forwarded as a slot. Renders identical to the
direct include; schema test added (18 tests). The other meta variants
(event/profile/publication/resource) render structured entity data and are
deferred as the data-driven long poles.
The SdcSchemaValidationTest helper json-encoded an empty prop array as a JSON
array ([]), failing the schema's type: object. Cast to object so an empty prop
set encodes as {}. Fixes the basic-meta test; all 18 SDC schema tests pass.
Wave 2 (static presentational molecules) of the SDC migration (epic #1351).
inline-message: theme dial + type/heading-level/link props, with heading and
content forwarded as variable-slots (content flows through the text atom, which
prints it with |raw). Block template repointed at the SDC; directory threaded
for the nested icon. Baseline-verified on /resource-test: renders
whitespace-normalized-identical to the pre-SDC output. Schema test added.
Wave 2 of the SDC migration (epic #1351). pull-quote: style dial + width/
alignment props (restored from the pre-SDC include context), with the quote and
attribution captured as Markup ({% set %}{{ block()|raw }}{% endset %}) to
preserve rich field markup in the auto-escaping blockquote/figcaption sinks; the
attribution is emptied when blank to keep the template's if-check falsy.
Baseline-verified normalized-identical on /welcome/centering-inclusivity.
…ints

The Wave 2 commits added the SDC components and tests but omitted the Layout
Builder block-template repoints (staging slip). Without these the block
templates still include the raw @molecules templates instead of the new SDCs.
The repoints were verified in the baseline diffs; committing them now so the
committed state actually renders via the SDCs.
Wave 2 of the SDC migration (epic #1351). wrapped-image: style + alignment
dials + width; content/caption forwarded as slots (they feed the text and image
atoms, which |raw internally) and the image markup injected into the
wrapped_image__image block. Block template repointed. Baseline-verified
full-page normalized-identical on /generic-homepage/generic-about-page.
Wave 2 of the SDC migration (epic #1351). wrapped-callout (wrapped_text_callout
block): theme + alignment dials + width; callout and body text forwarded as
slots that feed the text atom (|raw internally), |trim'd so empty slots stay
falsy. Block template repointed. Renders isolated-identical to the direct
include (no on-site content to page-test).
Wave 2 of the SDC migration (epic #1351). link-grid: theme dial (incl.
"inherit" for 50/50 layouts) + width/line-treatment; the overall heading and
four optional link columns are forwarded as slots. Each column slot both gates
the template's {% if link_grid__links_N %} (via a derived truthiness value) and
supplies the column content, proving the multi-conditional-slot pattern. SDC
slots named __col_* to avoid colliding with the template's link_grid__links_*
blocks. Block template repointed. Isolated-verified identical (no on-site
content), including correct gating of empty columns.
Wave 2 of the SDC migration (epic #1351). quick-links: variation/heading/theme/
width/alignment props; description (text-atom slot) plus optional image and
links columns forwarded as conditional slots (derived truthiness gates the
template's {% if %}, content injected into the matching CLT block). Block
template repointed; directory threaded. Baseline-verified region-identical on
/generic-homepage.
Wave 3 (interactive molecules) of the SDC migration (epic #1351). embed: a
width prop + the embedded content forwarded as a slot into the template's
embed__block block (captured as Markup). No component JS (it's an iframe
wrapper). Block template repointed. Isolated-verified identical.
Wave 3 of the SDC migration (epic #1351). video: width + alignment dial;
heading (Markup-captured for the heading atom), text (text-atom slot), and the
video/media markup (video__video block) forwarded as slots. Block template
repointed. Baseline-verified full-page normalized-identical on
/welcome/centering-inclusivity.
Wave 3 of the SDC migration (epic #1351). tabs is the first internal-state-
coupled component: the labels' aria-controls and the panels' ids share tabs__id.
Resolved WITHOUT the self-contained form by generating tabs__id in the field
template and threading it (+ base_class + key) explicitly to the label/content
partials, and passing it to the SDC. JS attaches via libraryOverrides
(atomic/tabs). Field template repointed. Baseline-verified region-identical
(ids normalized) on /welcome/text-heavy-content.
tabs__theme had a schema default but the field template doesn't pass it, so on
an uncached render it is NULL and fails type: string (the render cache masked
this during single-render verification). Nullable; the template applies its own
default ('one'). All component render-path pages now render 200 fresh+cached.
…masked NULL)

quick_links__background_color had a schema default but the block template
doesn't pass it (there is no field_style_color for quick_links), so on an
uncached render it is NULL and fails type: string. The render cache masked this
during single-render verification. Nullable; the template applies its own
default ('one').
…Wave 4)

Wave 4 (banners & spotlights) of the SDC migration (epic #1351). content-
spotlight renders the text-with-image molecule: the most prop-heavy conversion
so far (6 dial props + heading-level + 4 link props + 5 text slots + image
block). Dial props typed as nullable strings (the component is shared across
bundles whose dials differ; values are enforced Drupal-side). overline/
subheading/text feed the text atom (|raw), heading is Markup-captured, caption
feeds the image atom, image markup injected into the block. Content-spotlight
block template repointed. Baseline-verified region-identical on /generic-homepage
(200 fresh+cached).
Closes a coverage gap noted in the Waves 1-4 gate review: assert that omitting
the required date_time__start fails schema validation.
Wave 5 (organisms) of the SDC migration (epic #1351). tiles: 3 dials
(alignment/grid_count/vertical_alignment) + animation flag; tile items injected
into the tiles__items block. Block template repointed. Baseline-verified
region-identical on /tiles-example (200 fresh+cached).

Pre-existing flaw named: the block attached atomic/spotlights, but that library
does not exist and tiles has no behavior JS, so it was a dead reference — not
carried into the SDC. Flagged in the epic decisions log.
…ve 9)

Deliverable for #1364, part of epic #1351. Adds a Single Directory Components
section to the atomic contributing guide, since the SDC wrappers live here in
atomic/components/ (Drupal does not discover SDCs in node_modules/).

Documents the two-file thin-wrapper shape, that CSS/JS attach via libraryOverrides
pointing at existing atomic/* libraries, that the Layout Builder block template is
repointed to render the SDC, and the two rules that bite (assertion-gated prop
validation; no Twig-context inheritance -> thread directory). Points at the full
docs set in component-library-twig/docs/sdc/.
Deliverable for #1362 (global chrome), part of epic #1351. Establishes the SDC
pattern for global chrome, which does not fit the inline-block thin-wrapper used
by the 26 editor-placed components.

Investigation finding (documented in the epic log): no global-chrome component
(header, footer, nav, breadcrumbs, social-links, skip-link) has an inline-block
template — they render from region/menu/theme-hook templates with data from theme
settings and Drupal's menu system, all high blast (every page). The standalone
breadcrumb block is disabled; the live trail comes from a bespoke title-breadcrumb
combo. So chrome needs a different SDC pattern (an SDC invoked from the existing
template), and adopting it is a pending decision for the maintainer.

This commit delivers the achievable, zero-live-risk half: the breadcrumbs SDC
contract, proving the pattern without touching the live render path.
- atomic/components/breadcrumbs/breadcrumbs.component.yml — Canvas-forward schema:
  breadcrumbs__items (array of {title,url,is_active?}, required), modifiers,
  always_show; libraryOverrides -> atomic/breadcrumbs for the JS.
- atomic/components/breadcrumbs/breadcrumbs.twig — thin shim delegating to
  @organisms/menu/breadcrumbs/yds-breadcrumbs.twig; threads items + directory
  (SDC does not inherit Twig context).
- Schema-validation test (valid trail passes; missing required items array fails;
  item missing required title fails). Suite: 30 tests, 96 assertions green.

Verified by isolated render (renderInIsolation, twice on a fresh cr — 3 items,
wrapper present, no prop-validation error) since the live block is disabled.
NOT wired into the live render path (documented). Reviewed via a combined
code-review/simplify/security gate: schema matches the CLT contract, breadcrumb
titles are auto-escaped downstream (no XSS introduced), no must-fix findings.
Known wiring caveat: live Drupal items pass Url objects, not strings — the url
type needs revisiting when wired.
The breadcrumbs SDC (the Wave 7 chrome-pattern proof) existed but was not wired.
Point the live breadcrumb template (ys-breadcrumb-block.html.twig, the atomic
override that renders on every content page via the title-breadcrumb combo) at
atomic:breadcrumbs, passing the trail (`items`) explicitly as breadcrumbs__items
since the SDC does not inherit Twig context. The atomic/breadcrumbs library is
kept attached for the CSS.

Verified: breadcrumb trails render identically to dev on a 2-item page
(Home > Landing Page) and a 3-item deep page (Home > Something > Centering
Inclusivity); no Twig errors. This is the small, low-blast in-branch Wave 7 piece;
header/footer/nav remain a separate PR per the investigation.

References yalesites-org/YaleSites-Internal#1351
Convert the two galleries editor blocks. Both embed the same
@organisms/galleries/media-grid organism, so they share one atomic:media-grid
SDC: the gallery block passes the interactive variation (adding the lightbox
modal slot) and attaches atomic/gallery; the media-grid block passes the basic
variation. Captured slots (heading/items/modal) are passed in the embed's with{}
so they survive the isolated embed.

Wave 3 clarified: the hold is on ys_views_basic-driven blocks, not core Drupal
views. event-calendar stays deferred because it renders field_basic_params (the
ys_views_basic scaffold); gallery/media-grid have no ys_views_basic coupling.

Verified: test:sdc 44 -> 45; heading/items/modal slots capture in an isolated
embed render.

References yalesites-org/YaleSites-Internal#1351
The media-grid (non-gallery) block repoint passed width but not variation, so
media_grid__variation was NULL and failed the strict SDC enum
["basic","interactive"] — a 500 on every page with a media_grid block
(e.g. /kitchen-sink/deeper-kitchen-sink, /fancy-lab/research-areas). Pass
'basic' explicitly (SDC does not apply the schema default at render). The
gallery block already passes 'interactive'.

Caught by the menu-wide local-vs-dev parity crawl.

References yalesites-org/YaleSites-Internal#1351
@dblanken-yale

Copy link
Copy Markdown
Contributor Author

SDC Waves 1-5 + grand-hero (CLT refactor) landed on the matching 1351-migrate-cl-to-sdc branch. Full summary + what's left on the CLT PR: yalesites-org/component-library-twig#665. Other work: yalesites-org/yalesites-project#1330.

References yalesites-org/YaleSites-Internal#1351

dblanken-yale and others added 16 commits July 15, 2026 22:19
Convert the post_list, event_list, and directory Layout Builder block
adapters to render through a new atomic:view-embed SDC instead of calling
drupal_view() directly. The thin shim wraps only the stable drupal_view()
embed API (view_id, display_id, contextual argument); row markup stays in
the views' own row templates (ys_views_basic -> card-collection), so it is
unaffected by the Views Block Rework (#1161).

Drop dead per-adapter code (view__attibutes / card_collection__width /
parentNode) that was set but never printed; the real data-component-width
is emitted by the untouched views-view--*.html.twig row templates.

Add view-embed schema validation tests (schema suite 45 -> 47).

References yalesites-org/YaleSites-Internal#1351
…nt-wrapper SDC

Point the view and resource-view Layout Builder block adapters at the
atomic:component-wrapper SDC (slot `inner`) instead of embedding the raw
@organisms/component-wrapper/yds-component-wrapper.twig directly - the deferred
consumer-repoint for the create-only component-wrapper SDC (Wave 5).

Pass component_wrapper__alignment ('center') and component_wrapper__heading__level
('2') explicitly: the raw embed relied on the CLT template's internal defaults,
but the SDC validates props against its schema and would otherwise reject the
NULL values the block context supplies. The live outer wrapper div, the
field_heading_links cta-group, and the params field are preserved.

References yalesites-org/YaleSites-Internal#1351
…per SDC

Point the event-calendar Layout Builder block adapter at the
atomic:component-wrapper SDC (slot `inner`) instead of embedding the raw
@organisms/component-wrapper/yds-component-wrapper.twig directly - completing the
component-wrapper consumer repoints (view, resource-view, event-calendar).

Passes component_wrapper__alignment ('center') + component_wrapper__heading__level
('2') explicitly (the SDC validates props). The AJAX filter form, calendar JS,
and modal live inside the field_basic_params render array (the slot value), so
the thin wrapper passes them through unchanged. The live outer wrapper div,
field_heading_links cta-group, and field_basic_params are preserved.

References yalesites-org/YaleSites-Internal#1351
…-sdc

# Conflicts:
#	templates/block/layout-builder/block--inline-block--grand-hero.html.twig
#	templates/block/layout-builder/block--inline-block--image-banner.html.twig
1351: Wave 6 — repoint view + resource-view blocks to component-wrapper SDC
1351: Wave 6 — repoint event-calendar block to component-wrapper SDC
Wave 7 (global site chrome, #1362) — thin-wrapper SDCs for the non-menu chrome:

- New atomic:site-footer SDC (schema + shim) delegating to the CLT
  @organisms/site-footer template; slot footer__content forwards into the
  organism's footer__inner block.
- Repoint the two html.html.twig skip-link includes to the existing
  atomic:link-skip SDC (label moves from a variable to the content slot).
- Correct the breadcrumbs SDC docstrings, which still claimed the component
  was not wired; it is wired via ys-breadcrumb-block.html.twig (front-end path;
  the LB-preview branch still uses the raw CLT template).
Wave 7 (global site chrome, #1362).

The Drupal menu render tree carries \Drupal\Core\Url objects and menu-link
entities that cannot cross an SDC prop boundary. _atomic_flatten_menu_items()
(called from atomic_preprocess_menu) produces a plain, string-URL copy of the
tree; the new atomic:primary-nav SDC renders it with menu__contextual: true so
the shared CLT menu engine uses the string URL directly.

- New atomic:primary-nav SDC (schema + shim).
- menu--extras--main.html.twig renders through the SDC (included with context
  so directory flows and nested icon atoms resolve the versioned sprite path).
- Schema-validation cases for site-footer and primary-nav (test:sdc 47 -> 49).

Verified byte-identical to the pre-SDC render on a real page.
Wave 7 (global site chrome, #1362). Convert the utility_navigation block's menu
to atomic:utility-nav, reusing the menu-tree flatten from atomic_preprocess_menu.

- New atomic:utility-nav SDC (schema + shim delegating to _utility-nav--menu).
- menu--extras--utility-navigation.html.twig renders through the SDC (with
  context so directory + ys_node_access is_cas flags flow).
- Schema-validation case (test:sdc 49 -> 50).

Verified byte-identical to the pre-SDC render (both desktop + mobile) on a real page.
Wave 7 (global site chrome, #1362). Convert the book/collection 'in this section'
secondary navigation to atomic:in-this-section.

- New atomic_preprocess_book_tree flattens the book tree (reusing
  _atomic_flatten_menu_items; the book_tree hook is not covered by
  atomic_preprocess_menu but its items share the menu-item shape).
- New atomic:in-this-section SDC (schema + shim delegating to
  yds-site-in-this-section, which wraps secondary-nav) rendered with
  menu__contextual: true.
- book-tree.html.twig renders through the SDC (with context for directory).
- Schema-validation case (test:sdc 50 -> 51).

Verified via renderInIsolation (the SDC/wrapper/secondary-nav/menu chain renders
flattened string-URL items, incl. a nested child). NOTE: no book/collection
content exists on the local DB, so an on-page byte-diff must be done on the
multidev; the flatten is byte-safe by construction (the CLT menu engine reads
only the whitelisted item fields).
Convert the global site header (the region--header render path and the
@organisms/site-header organism) to an atomic:site-header thin-wrapper SDC,
following the Wave 7 chrome pattern. The utility drop-button menu tree is
flattened in atomic_preprocess_region__header (via _atomic_flatten_menu_items,
converting Url objects to string URLs) so it can cross the SDC prop boundary
and is rendered with menu__contextual: true. Because an SDC isolates its render
context, every value the organism and its nested utility-nav include previously
received through the non-only embed's context inheritance is threaded here as an
explicit prop or a markup-captured slot.

Rendered output is byte-identical to the pre-SDC header across the normal, mega,
focus (with and without background image), utility-search, utility-dropdown, and
site-name-as-image variants; one invisible, whitespace-only difference in the
aria-hidden focus background container is documented. Adds a site-header
schema-validation case so composer test:sdc covers it.

Part of the SDC migration epic (1351); split from the Wave 7 chrome work (1362).
1413: Wave 7: Site header SDC conversion
@dblanken-yale

Copy link
Copy Markdown
Contributor Author

This still needs approval @miketullo95 but am marking it as don't merge until we're ready for it.

…-sdc

# Conflicts:
#	templates/block/layout-builder/block--inline-block--inline-message.html.twig
#	templates/block/layout-builder/block--inline-block--link-grid.html.twig
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants