From fbbad0b748306eff61697af39a17ee9c9e91eff6 Mon Sep 17 00:00:00 2001 From: Quinton Jason Date: Mon, 13 Jul 2026 08:48:15 -0500 Subject: [PATCH] docs: add ADRs for React wrappers, slots-vs-props, distribution, and runtime deps --- .../0006-react-wrappers-via-output-target.md | 36 ++++++++++++++ ...0007-slots-for-content-props-for-config.md | 40 ++++++++++++++++ docs/adr/0008-cdn-and-npm-distribution.md | 47 +++++++++++++++++++ docs/adr/0009-vetted-runtime-dependencies.md | 40 ++++++++++++++++ docs/adr/README.md | 6 ++- 5 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 docs/adr/0006-react-wrappers-via-output-target.md create mode 100644 docs/adr/0007-slots-for-content-props-for-config.md create mode 100644 docs/adr/0008-cdn-and-npm-distribution.md create mode 100644 docs/adr/0009-vetted-runtime-dependencies.md diff --git a/docs/adr/0006-react-wrappers-via-output-target.md b/docs/adr/0006-react-wrappers-via-output-target.md new file mode 100644 index 000000000..7ab67c05b --- /dev/null +++ b/docs/adr/0006-react-wrappers-via-output-target.md @@ -0,0 +1,36 @@ +# 0006. Generate React wrappers via Stencil's output target + +- **Status:** Accepted (retrospective) +- **Date:** 2026-07-13 +- **Maintainers:** @Kajabi/dss-devs + +## Context + +Pine authors components as framework-agnostic Stencil web components (`pds-*`), but a large share of consumers are React apps. Consuming raw custom elements from React has real ergonomic gaps: events don't map to `on*` props, boolean/object props need manual marshalling, and there's no typed component surface. + +## Decision + +Generate typed **React wrappers** with `@stencil/react-output-target` (configured in `libs/core/stencil.config.ts`) and publish them as a separate package, **`@pine-ds/react`** (`libs/react/`), which depends on `@pine-ds/core` at a caret range (`^3.26.4`) and re-exports each component as an idiomatic React component with typed props and wired events. + +## Consequences + +**Positive** + +- React consumers get a typed, idiomatic API — events as props, proper types — instead of raw custom-element interop. +- The wrappers are generated from the source components, so they can't drift from the core API by hand. + +**Negative / accepted costs** + +- A second published package to version and release in lockstep with core; the caret dep on `@pine-ds/core` has to stay coherent across releases. +- Wrapper output is generated — a stale build produces stale wrappers, so the React package must be rebuilt when core changes. +- Adds a React-specific surface to maintain alongside the framework-agnostic core. + +## Alternatives considered + +- **Consume raw custom elements in React** — rejected: poor ergonomics (events, refs, typing) push friction onto every consumer. +- **Hand-write React wrappers** — rejected: unbounded maintenance and guaranteed drift from the core API. + +## References + +- `libs/core/stencil.config.ts` (`reactOutputTarget(...)`) +- `libs/react/` (`@pine-ds/react` package) diff --git a/docs/adr/0007-slots-for-content-props-for-config.md b/docs/adr/0007-slots-for-content-props-for-config.md new file mode 100644 index 000000000..4b8ab4a78 --- /dev/null +++ b/docs/adr/0007-slots-for-content-props-for-config.md @@ -0,0 +1,40 @@ +# 0007. Slots for content, props for configuration + +- **Status:** Accepted (retrospective) +- **Date:** 2026-07-13 +- **Maintainers:** @Kajabi/dss-devs + +## Context + +Every component needs two different kinds of input: **configuration** (variant, size, disabled, type) and **content** (labels, icons, arbitrary markup). Deciding case-by-case which goes where produces inconsistent APIs and, worse, caps flexibility when content is modeled as a fixed prop. + +## Decision + +**Props configure; slots carry content.** Behavior and appearance are props; anything that is or could be arbitrary markup is exposed as a named slot (`start`/`end`, `prefix`/`suffix`/`append`/`prepend`, etc.) rather than a string or icon-name prop. + +This is a pervasive pattern, not an aspiration — 25 components expose ``s. Where a content prop predated the convention, it's being deprecated toward a slot: `pds-button`'s `icon` prop is `@deprecated` in favor of the `start` slot. + +## Consequences + +**Positive** + +- Consumers can place *any* content — including other `pds-*` components — where a prop would have allowed only a string. +- A consistent mental model across the library: "config on the tag, content in the slots." +- New components inherit the convention instead of re-deciding it each time. + +**Negative / accepted costs** + +- Slotted content is the **consumer's** markup, so responsibility for its safety shifts to the component when it must read that markup as HTML — see ADR-0009 (DOMPurify sanitization in `pds-combobox`). +- For trivial cases a slot feels heavier than a prop would. +- Migrating legacy content props (like `icon`) to slots is a deprecation cycle, not a free change. + +## Alternatives considered + +- **Model content as props** (`icon="settings"`, `label="Save"`) — rejected: caps flexibility (no rich/composed content) and is exactly what drove the `icon`-prop deprecation. +- **Freeform single default slot everywhere** — rejected: named slots give components control over placement (start/end, prefix/suffix) that a single slot can't. + +## References + +- `pds-button` `icon` prop `@deprecated` → `start` slot +- `` usage across 25 components (`libs/core/src/components/`) +- ADR-0009 (sanitizing consumer-supplied slotted/rich content) diff --git a/docs/adr/0008-cdn-and-npm-distribution.md b/docs/adr/0008-cdn-and-npm-distribution.md new file mode 100644 index 000000000..4db06dfa5 --- /dev/null +++ b/docs/adr/0008-cdn-and-npm-distribution.md @@ -0,0 +1,47 @@ +# 0008. Dual distribution via npm and CDN with a lazy loader + +- **Status:** Accepted (retrospective) +- **Date:** 2026-07-13 +- **Maintainers:** @Kajabi/dss-devs + +## Context + +Pine's consumers split into two camps: bundler-based apps that `npm install`, and server-rendered / no-build surfaces (Rails views, marketing pages) that can only add a `` and ` + +``` + +This is the counterpart to the token side — see ds-tokens ADR-0004. + +## Consequences + +**Positive** + +- No-build surfaces get full components with three tags; bundler apps get tree-shakeable npm imports. +- The lazy loader registers components on demand and the `nomodule` bundle covers older browsers. +- One published artifact backs both channels. + +**Negative / accepted costs** + +- CDN versions are pinned by hand, and **`@pine-ds/core` and `@kajabi-ui/styles` CDN versions must be kept in sync** — the consuming ERB literally carries a comment saying so. A stale CDN pin won't surface as a dependency warning (as it wouldn't for tokens — see ds-tokens ADR-0004). +- Multiple entry points (`main`/`module`/`unpkg`/`collection`/`loader`) to keep coherent across releases. +- Runtime styling on CDN surfaces depends on jsDelivr availability. + +## Alternatives considered + +- **npm-only** — rejected: excludes the no-build server-rendered surfaces that are a large part of consumption. +- **A single eager bundle** — rejected: loses lazy registration and the `nomodule` fallback. + +## References + +- `README.md` (CDN link + module + nomodule snippet) +- `libs/core/package.json` (`main`, `module`, `unpkg`, `collection`, loader entry points) +- kajabi-products `app/views/shared/_pine_assets.html.erb` (pinned CDN tags; core/styles sync comment) +- ds-tokens ADR-0004 (CDN distribution for tokens) diff --git a/docs/adr/0009-vetted-runtime-dependencies.md b/docs/adr/0009-vetted-runtime-dependencies.md new file mode 100644 index 000000000..54b634497 --- /dev/null +++ b/docs/adr/0009-vetted-runtime-dependencies.md @@ -0,0 +1,40 @@ +# 0009. Use vetted third-party libraries for positioning and sanitization + +- **Status:** Accepted (retrospective) +- **Date:** 2026-07-13 +- **Maintainers:** @Kajabi/dss-devs + +## Context + +Two pieces of component behavior are genuinely hard to get right by hand: **overlay positioning** (keeping a popover/combobox anchored, flipped, and shifted within the viewport) and **sanitizing consumer-supplied HTML** that a component renders as `innerHTML`. Both are well-solved problems with mature libraries, and both are risky to reinvent — positioning for correctness, sanitization for security. + +## Decision + +Depend on small, well-maintained libraries for these instead of hand-rolling: + +- **`@floating-ui/dom`** for overlay positioning — used in `pds-combobox`, `pds-multiselect`, and the truncation tooltip util. +- **`DOMPurify`** for sanitizing consumer-supplied markup that gets read as HTML — used in `pds-combobox` (`sanitizeHtml` cleans an option's `innerHTML` before rendering the selected-option layout), configured to allow Pine components while stripping dangerous content, to prevent XSS. + +## Consequences + +**Positive** + +- Correct, battle-tested viewport-aware positioning without bespoke geometry code. +- A real XSS defense on the one place a component reads consumer markup as HTML — which is the safety cost of the slots/rich-content model (ADR-0007). + +**Negative / accepted costs** + +- These are **runtime** dependencies that ship in the bundle; their size counts against the `size-limit` budgets enforced in CI. +- **DOMPurify is load-bearing security** — removing or bypassing `sanitizeHtml` reintroduces XSS via option markup. Any refactor of `pds-combobox`'s option rendering must preserve it. +- The libraries must be kept current (security patches for DOMPurify especially). + +## Alternatives considered + +- **Hand-rolled positioning** — rejected: re-implements `@floating-ui/dom` (flip, shift, viewport collision) and is error-prone. +- **Manual escaping / trusting consumer markup** — rejected: manual escaping is fragile and an XSS liability; DOMPurify is the standard for this. + +## References + +- `libs/core/src/utils/truncation-tooltip.ts`, `pds-combobox`, `pds-multiselect` (`@floating-ui/dom`) +- `libs/core/src/components/pds-combobox/pds-combobox.tsx` (`sanitizeHtml` via `DOMPurify`) +- ADR-0007 (slots/rich content — the reason sanitization is needed) diff --git a/docs/adr/README.md b/docs/adr/README.md index a40a574a7..4af5370af 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -26,8 +26,12 @@ See [`0000-template.md`](./0000-template.md). | [0003](./0003-pine-custom-lint-plugins.md) | Custom Pine lint plugins enforce semantic-token usage | Accepted (retrospective) | | [0004](./0004-mcp-server-delivery.md) | Ship a Pine MCP server for AI tooling | Accepted (retrospective) | | [0005](./0005-dark-mode-via-semantic-tokens.md) | Roll out dark mode via semantic-token migration | Accepted (retrospective) | +| [0006](./0006-react-wrappers-via-output-target.md) | Generate React wrappers via Stencil's output target | Accepted (retrospective) | +| [0007](./0007-slots-for-content-props-for-config.md) | Slots for content, props for configuration | Accepted (retrospective) | +| [0008](./0008-cdn-and-npm-distribution.md) | Dual distribution via npm and CDN with a lazy loader | Accepted (retrospective) | +| [0009](./0009-vetted-runtime-dependencies.md) | Use vetted third-party libraries for positioning and sanitization | Accepted (retrospective) | -ADRs 0001–0005 were authored retrospectively to capture decisions already in the codebase. The **Maintainers** field on each names the team that owns the area today, not the original deciders. +ADRs 0001–0009 were authored retrospectively to capture decisions already in the codebase. The **Maintainers** field on each names the team that owns the area today, not the original deciders. ## When to write a new ADR