Skip to content
Merged
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
36 changes: 36 additions & 0 deletions docs/adr/0006-react-wrappers-via-output-target.md
Original file line number Diff line number Diff line change
@@ -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)
40 changes: 40 additions & 0 deletions docs/adr/0007-slots-for-content-props-for-config.md
Original file line number Diff line number Diff line change
@@ -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 `<slot>`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
- `<slot>` usage across 25 components (`libs/core/src/components/`)
- ADR-0009 (sanitizing consumer-supplied slotted/rich content)
47 changes: 47 additions & 0 deletions docs/adr/0008-cdn-and-npm-distribution.md
Original file line number Diff line number Diff line change
@@ -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 `<link>` and `<script>`. Both need the same components.

## Decision

Publish `@pine-ds/core` to **npm** and serve the same artifact over the **jsDelivr CDN**, and ship a **lazy loader** so components self-register on use. The package exposes multiple entry points (`main`, `module`, `unpkg`, `collection`, `loader`); CDN consumers drop in a pinned `<link>` + ESM `<script type="module">` + `nomodule` fallback:

```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@pine-ds/core@[VERSION]/dist/pine-core/pine-core.css" />
<script type="module" src="https://cdn.jsdelivr.net/npm/@pine-ds/core@[VERSION]/dist/pine-core/pine-core.esm.js"></script>
<script nomodule src="https://cdn.jsdelivr.net/npm/@pine-ds/core@[VERSION]/dist/pine-core/index.esm.js"></script>
```

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)
40 changes: 40 additions & 0 deletions docs/adr/0009-vetted-runtime-dependencies.md
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 5 additions & 1 deletion docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading