Skip to content

docs: Add 2nd-gen TypeScript style guide and Storybook docs automation#6067

Open
caseyisonit wants to merge 14 commits intomainfrom
caseyisonit/typescript-guide
Open

docs: Add 2nd-gen TypeScript style guide and Storybook docs automation#6067
caseyisonit wants to merge 14 commits intomainfrom
caseyisonit/typescript-guide

Conversation

@caseyisonit
Copy link
Contributor

@caseyisonit caseyisonit commented Mar 6, 2026

Description

Adds a comprehensive TypeScript style guide for 2nd-gen component development and automates publishing contributor docs to Storybook.

Motivation and context

Contributors (human and AI) need clear, consistent guidance for writing 2nd-gen components. This PR provides detailed conventions for file organization, class structure, decorators, types, and composition patterns—all based on the Badge component as the reference implementation.

Additionally, contributor docs are now automatically converted and displayed within Storybook, making them easier to discover and browse alongside component documentation.

Related issue(s)

  • fixes [SWC-1420]

Screenshots (if appropriate)

N/A - documentation changes

Author's checklist

  • I have read the CONTRIBUTING and PULL_REQUESTS documents.
  • I have reviewed at the Accessibility Practices for this feature, see: Aria Practices
  • I have added automated tests to cover my changes.
  • I have included a well-written changeset if my change needs to be published.
  • I have included updated documentation if my change required it.

Reviewer's checklist

  • Includes a Github Issue with appropriate flag or Jira ticket number without a link
  • Includes thoughtfully written changeset if changes suggested include patch, minor, or major features
  • Automated tests cover all use cases and follow best practices for writing
  • Validated on all supported browsers
  • All VRTs are approved before the author can update Golden Hash

Manual review test cases

  • Verify TypeScript guide renders correctly in Storybook

    1. Run yarn storybook in 2nd-gen/packages/swc
    2. Navigate to the Contributor Docs section
    3. Confirm the TypeScript style guide pages load and display properly
  • Verify navigation structure is accurate

    1. Open CONTRIBUTOR-DOCS/02_style-guide/02_typescript/README.md
    2. Confirm all 15 linked guides exist and have correct content

Device review

  • Did it pass in Desktop?
  • Did it pass in (emulated) Mobile?
  • Did it pass in (emulated) iPad?

Summary of changes:

Area Files Description
TypeScript style guide 15 new docs File organization, class structure, decorators, types, composition patterns
Storybook automation generate-contributor-docs.mjs Converts markdown docs to MDX for Storybook display
Minor fixes Badge, sized mixin Small updates aligned with documented patterns

@caseyisonit caseyisonit requested a review from a team as a code owner March 6, 2026 19:40
@changeset-bot
Copy link

changeset-bot bot commented Mar 6, 2026

⚠️ No Changeset found

Latest commit: f50f7b9

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor Author

@caseyisonit caseyisonit Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this correctly keeps 1st-types working but closer to 1st-gen. this is the pattern for correcting 1st-gen types while maintaining code quality in 2nd-gen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Properly reflects the types patterns we are wanting to enforce, the key update with this is NEVER using s2 in any type declarations so we avoid breaking changes in the future to remove that

Copy link
Contributor Author

@caseyisonit caseyisonit Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implements the as const pattern that will aid in IDE support for allowed values

@github-actions
Copy link
Contributor

github-actions bot commented Mar 6, 2026

📚 Branch Preview Links

🔍 First Generation Visual Regression Test Results

When a visual regression test fails (or has previously failed while working on this branch), its results can be found in the following URLs:

Deployed to Azure Blob Storage: pr-6067

If the changes are expected, update the current_golden_images_cache hash in the circleci config to accept the new images. Instructions are included in that file.
If the changes are unexpected, you can investigate the cause of the differences and update the code accordingly.

Copy link
Contributor Author

@caseyisonit caseyisonit Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

script to auto generate contributor docs to mdx files and work in storybook. CONTRIBUTOR-DOCS remains the source of truth. ties in to yarn start/storybook so it runs when you spin up storybook

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something we want to keep long-term? I feel like this is unnecessary overhead. Could we let Storybook render these files directly and respect the file structure to figure the hierarchy? that would allow us to get rid of the TOC generation, numeric prefix, etc.

],
],
'Contributor docs',
// GENERATED:CONTRIBUTOR-DOCS-SORT - Do not edit manually. Run `yarn generate:contributor-docs` to update.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

script generates a story sort based on the directory and file structure in CONTRIBUTOR-DOCS

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this! Is my understanding of this correct: if you run yarn storybook or yarn storybook:build, yarn generate:contributor-docs will run automatically? This comment is maybe just a tiny bit misleading in that you probably don't have to go out of your way to update this, and it just happens when you run storybook locally.

But, another thing I think about is:

What if someone makes a docs-only change to CONTRIBUTOR-DOCS, and never runs storybook, so they forget to update this?

Would an AI rule maybe be useful here to help remind? Or some other thing to combat this possible issue?

2. **Zero-width non-joiner**: Added `⁠` (zero-width non-joiner) between label text and required asterisk icon to prevent text wrapping issues in internationalized content
3. **Static color support**: Added support for `--staticBlack` and `--staticWhite` variants through the `staticColor` parameter

</details>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rogue closing tag that broke rendering

@caseyisonit caseyisonit added the Status:Ready for review PR ready for review or re-review. label Mar 6, 2026
@caseyisonit caseyisonit added Status:WIP PR is a work in progress or draft Status:Ready for review PR ready for review or re-review. and removed Status:Ready for review PR ready for review or re-review. Status:WIP PR is a work in progress or draft labels Mar 9, 2026
@caseyisonit caseyisonit added the 2nd gen These issues or PRs map to our 2nd generation work to modernizing infrastructure. label Mar 9, 2026
@rise-erpelding rise-erpelding self-requested a review March 9, 2026 18:09
Copy link
Collaborator

@rise-erpelding rise-erpelding left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving a few comments, this looks really nice and I do really like having the contributor docs in Storybook as well as GitHub!

/*
/**
* @todo The S1 types can be removed once we are no longer maintaining 1st-gen.
* @todo Rename STATUSLIGHT_ prefix to STATUS_LIGHT_ to align with type prefix
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a ticket to address this somewhere already? I guess we can roll it into something else if not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these will be addressed as part of the code quality work-streams. cc: @caseyisonit

}

/**
* Rewrite internal .md links to Storybook doc paths.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works beautifully for the vast majority of internal links but leaves out links to GitHub code files like [Badge](../../../2nd-gen/packages/core/components/badge/Badge.base.ts), would it be doable to parse these and link them to the appropriate GitHub files? Or use absolute links either way? Just something to think about, not necessarily a blocker for now but something we should address at some point.

Here are some links for specific examples of what I mean:

],
],
'Contributor docs',
// GENERATED:CONTRIBUTOR-DOCS-SORT - Do not edit manually. Run `yarn generate:contributor-docs` to update.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this! Is my understanding of this correct: if you run yarn storybook or yarn storybook:build, yarn generate:contributor-docs will run automatically? This comment is maybe just a tiny bit misleading in that you probably don't have to go out of your way to update this, and it just happens when you run storybook locally.

But, another thing I think about is:

What if someone makes a docs-only change to CONTRIBUTOR-DOCS, and never runs storybook, so they forget to update this?

Would an AI rule maybe be useful here to help remind? Or some other thing to combat this possible issue?

'Anti patterns',
'Property order quick reference',
],
'Linting tools',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also seeing some changes to this file after pulling down the latest changes to the branch and running storybook to regenerate these, do you see that on your end also?

Copy link
Contributor

@rubencarvalho rubencarvalho left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Left a couple of small comments. My larger concern is around the Storybook conversion script. Is there a world where we would just write the contributor docs directly in .mdx instead? Happy to defer this to a follow-up dedicated ticket if that makes more sense.

As a general recommendation, I also find these changes easier to review when they’re more scoped - for example, the Storybook generation could have been its own dedicated PR 👍

});
describe('Overlay - type="modal", v1', () => {
describe('handle multiple separate `contextmenu` events', async () => {
// @TODO: skipping this test suite because it's flaky in CI. Will review in the migration to Spectrum 2.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did you come to this conclusion? I haven't seen this flake nor does it show on our metrics?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also curious why this started flaking here - does not show in any of the recent PRs nor metrics 🕵️

* @slot - Text label of the badge.
* @slot icon - Optional icon that appears to the left of the label
*
* @todo review the mixin composition here. We currently have 3 levels of mixins on this class, but the mixin composition guide recommends a maximum of 2.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’d challenge the idea that a “maximum of 2” is a reasonable rule. There isn’t really a hard limit we need to follow... though fewer mixins generally makes things easier to reason about and avoids fragile super chains. Where did you find the guidance that 2 is the recommended max?

/*
/**
* @todo The S1 types can be removed once we are no longer maintaining 1st-gen.
* @todo Rename STATUSLIGHT_ prefix to STATUS_LIGHT_ to align with type prefix
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these will be addressed as part of the code quality work-streams. cc: @caseyisonit

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something we want to keep long-term? I feel like this is unnecessary overhead. Could we let Storybook render these files directly and respect the file structure to figure the hierarchy? that would allow us to get rid of the TOC generation, numeric prefix, etc.

: html`
<slot></slot>
`}
${when(this.variant === 'file', () => file(this.label))}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like a better use case of the choose directive (reference).


## Debug mode API

The `window.__swc` object provides debug utilities:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add a brief explainer of where/when does this get initialized?

| `ifDefined` | `lit/directives/if-defined.js` | Stories, templates | Skip attribute if undefined |
| `styleMap` | `lit/directives/style-map.js` | Storybook decorators | Dynamic inline styles |

### classMap
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if we should reference the official Lit documentation for these directives? It feels like we may be doing duplicate work here. Lit's examples show the directives signature more clearly too. E.g.:

classMap(classInfo: {[name: string]: string | boolean | number})

Copy link
Contributor

@nikkimk nikkimk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm loving the way this is taking shape. I have a few more suggestions and questions to address before it's good to go, so hit me up when ready for re-review.

],
],
'Contributor docs',
// GENERATED:CONTRIBUTOR-DOCS-SORT - Do not edit manually. Run `yarn generate:contributor-docs` to update.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love this

Comment on lines +84 to +96
**Renaming existing merged prefixes:** If a component's types file in core already uses a merged prefix (e.g., `STATUSLIGHT_`), rename the constants to the underscore-separated form (`STATUS_LIGHT_`) in core. To avoid a breaking change in 1st-gen, re-export the old name as a deprecated alias from the 1st-gen package:

```typescript
// 1st-gen/packages/status-light/src/StatusLight.ts
import { STATUS_LIGHT_VARIANTS_S1 } from '@spectrum-web-components/core/components/status-light';

/**
* @deprecated Use `STATUS_LIGHT_VARIANTS_S1` instead.
*/
export const STATUSLIGHT_VARIANTS_S1 = STATUS_LIGHT_VARIANTS_S1;
```

This keeps the canonical name correct in core while giving 1st-gen consumers a migration path.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this

Comment on lines +133 to +138
1. **Copyright header**
2. **Imports** (typically only `ElementSize` from core mixins)
3. **Shared constants** — sizes (e.g., `BADGE_VALID_SIZES`), canonical base arrays (e.g., `BADGE_VARIANTS_COLOR`, `BADGE_VARIANTS_SEMANTIC`), non-variant constants (e.g., `FIXED_VALUES`)
4. **S1-only constants** — subset arrays exclusive to 1st-gen, validated against canonical types via `satisfies` (e.g., `BADGE_VARIANTS_COLOR_S1`, `BADGE_VARIANTS_S1`)
5. **Canonical constants** — composed arrays that spread from shared bases (e.g., `BADGE_VARIANTS`)
6. **Types** — shared types (e.g., `BadgeSize`, `BadgeColorVariant`), then S1-only types (e.g., `BadgeVariantS1`), then canonical types (e.g., `BadgeVariant`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<3 the inclusion of examples

Comment on lines +332 to +359
### Dependency direction

The dependency always flows toward S1:

```text
Shared constants (base arrays + canonical types)
←── Canonical composed arrays spread from shared
←── S1 arrays validate against shared types via satisfies
```

This means:

- **Canonical base arrays and types live in SHARED** — `BADGE_VARIANTS_COLOR` and `BadgeColorVariant` are defined before S1-ONLY
- **S1 arrays validate against canonical types** — `BADGE_VARIANTS_COLOR_S1` uses `satisfies readonly BadgeColorVariant[]`
- **Canonical composed arrays never reference S1 arrays** — `BADGE_VARIANTS` does not spread `BADGE_VARIANTS_S1`
- **S1 arrays may spread from shared arrays** — `BADGE_VARIANTS_S1` spreads `BADGE_VARIANTS_SEMANTIC` (shared)
- **Deleting S1 content never breaks shared or canonical content**

### Removing S1

When 1st-gen is retired, the removal process for each types file is:

1. Delete the `S1-ONLY` section (constants and types)
2. Delete the `S1-ONLY` section separator
3. Remove the `@todo` comment about S1 removal
4. Optionally, if a shared constant was only shared for S1's benefit and is identical to the canonical array, inline it

No renames. No changes to canonical type names. No breaking changes for 2nd-gen consumers.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<3 this


```text
Picker → SizedMixin(ExpandableElement) → SpectrumElement
Combobox → Textfield → ManageHelpText(SizedMixin(Focusable))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Combobox → Textfield → ManageHelpText(SizedMixin(Focusable))
Combobox → Textfield → ManageHelpText(SizedMixin(Focusable)) (**Note**: This is very likely to change in 1st-gen in order to fix existing accessilbility issues with comobobox.)

Comment on lines +1268 to +1270
| Button / Button Group | Focusable, LikeAnchor, PendingState | Needs Focusable mixin |
| Action Button / Action Group | Focusable, LikeAnchor, Longpress | Needs Focusable + interaction |
| Link | Focusable, LikeAnchor | Needs Focusable + LikeAnchor |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Links (including buttons and action buttons) are moving to CSS-only typography solutions in 1st-gen. We should be CSS typography alternatives to all buttons in 1st gen.

Suggested change
| Button / Button Group | Focusable, LikeAnchor, PendingState | Needs Focusable mixin |
| Action Button / Action Group | Focusable, LikeAnchor, Longpress | Needs Focusable + interaction |
| Link | Focusable, LikeAnchor | Needs Focusable + LikeAnchor |
| Button / Button Group | Focusable, LikeAnchor, PendingState | Could be replaced by CSS-only typography solution |
| Action Button / Action Group | Focusable, LikeAnchor, Longpress | Could be replaced by CSS-only typography solution |

5. **Help text controller** — convert `ManageHelpText` mixin to controller pattern
6. **Platform utilities** — port `platform.ts` detection functions to `core/utils/`
7. **Focus utilities** — port `getActiveElement()`, `firstFocusableIn()`, focusable selectors to `core/utils/`
8. **Random ID utility** — port `randomID()` to `core/utils/` for generating unique IDs within shadow roots. Note: IDREFs don't cross shadow boundaries, so this is primarily useful for internal `aria-labelledby`/`aria-describedby` references within the same shadow root, or for non-ARIA purposes like CSS targeting.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:thumbs-up:


**Pattern**: Interactive/semantic components (Progress Circle, Divider) set `role` in `firstUpdated()` and manage dynamic ARIA attributes in `updated()`. Non-interactive display components (Badge, Status Light) don't set roles. Asset handles accessibility within its SVG templates.

**Note**: Divider sets `role="separator"` unconditionally, which is the correct pattern — components should be opinionated about their semantic role. Progress Circle's conditional check is unnecessary complexity that could be simplified to match Divider's approach.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!!! <3

5. **Custom getter/setter for `fixed`** in Badge base has a `@todo` noting it may behave identically to a standard Lit reactive property — unnecessary complexity.
6. **Section coverage gaps in tests**: Some components skip standard test categories despite implementing the underlying features.
7. **Duplicate JSDoc tags**: Asset SWC has duplicate `@example` annotations.
8. **Mixed import styles**: Combining package aliases with deep relative imports increases maintenance overhead.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
8. **Mixed import styles**: Combining package aliases with deep relative imports increases maintenance overhead.
8. **Mixed import styles**: Combining package aliases with deep relative imports increases maintenance overhead.
9. **Conditional role assignment**: Conditional ARIA roles in web components are an anti-pattern because each role carries distinct semantic meaning, interaction expectations, and structural rules that cannot be safely swapped at runtime. When a single component toggles between roles — say, switching between listbox and menu — it must also dynamically change which child roles are valid (option vs. menuitem), whether nesting is permitted, how keyboard navigation behaves, and what events assistive technologies expect. This complexity is inherently fragile: a missed edge case means the component silently violates the ARIA spec, producing an experience that appears accessible but fails in practice. A clearer approach is to split components along role boundaries. For example, sp-menu and sp-listbox should be separate components because a menu permits menuitem, menuitemcheckbox, and menuitemradio children and supports nested submenus, while a listbox permits only option children and has no concept of nesting. Merging these into one component obscures those constraints and forces the implementation to reconcile two incompatible interaction models. The same principle applies to swatch and swatch-group: a swatch that is purely informational (no role or role="img"), one that acts as a toggle button (role="button" with aria-pressed), one that acts as a checkbox (role="checkbox" with aria-checked), and one that acts as a radio button (role="radio" within a radiogroup) each have fundamentally different semantics, keyboard behaviors, and parent-container requirements. Collapsing all of these into a single component with a conditional role shifts the burden of correctness onto the consumer, who must know exactly which configuration produces a valid accessibility tree — the opposite of what a component library should do. Splitting components by role makes the choice explicit, enforces valid structure through the API itself, and ensures that each component can be tested and documented against a single, well-defined ARIA contract.

### Priority 5: Standardize accessibility patterns

1. **Add accessible name validation** to Badge base class (for icon-only variants). Status Light does not require accessible name validation — consumers are responsible for including text alongside the component.
2. **Use unconditional role assignment** everywhere. Components should be opinionated about their semantic role. Simplify Progress Circle to match Divider's pattern.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Progress Circle should always be a progressbar. Pending state should use an animated icon, since it does not communicate actual progress.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2nd gen These issues or PRs map to our 2nd generation work to modernizing infrastructure. Status:Ready for review PR ready for review or re-review.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants