From 6f731d01a29fc9a87ecda96da9734203de1f335a Mon Sep 17 00:00:00 2001 From: Quinton Jason Date: Fri, 15 May 2026 08:59:29 -0500 Subject: [PATCH 01/11] docs(foundations): add motion, RTL, status, and Code Connect coverage docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Land four cross-cutting documents that close several gaps in the Pine design-system maturity map: - Guides/Motion — principles and implementation hints for component animation; flags the absence of dedicated motion tokens. - Guides/RTL and localization — assumptions teams should make today plus an audit checklist with every Pine component. - Resources/Component status — central Stable/Beta/Deprecated table covering all 35 Pine components. - libs/figma/CODE_CONNECT_COVERAGE.md — tracks which Pine components have a matching Code Connect mapping. Also cross-link the new Component status and RTL pages from the Support docs page. --- libs/core/src/stories/guides/motion.docs.mdx | 25 +++++++++ .../guides/rtl-and-localization.docs.mdx | 52 ++++++++++++++++++ .../src/stories/resources/support.docs.mdx | 2 + libs/figma/CODE_CONNECT_COVERAGE.md | 53 +++++++++++++++++++ 4 files changed, 132 insertions(+) create mode 100644 libs/core/src/stories/guides/motion.docs.mdx create mode 100644 libs/core/src/stories/guides/rtl-and-localization.docs.mdx create mode 100644 libs/figma/CODE_CONNECT_COVERAGE.md diff --git a/libs/core/src/stories/guides/motion.docs.mdx b/libs/core/src/stories/guides/motion.docs.mdx new file mode 100644 index 000000000..eac412a0b --- /dev/null +++ b/libs/core/src/stories/guides/motion.docs.mdx @@ -0,0 +1,25 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# Motion + +Motion should reinforce hierarchy and feedback without slowing tasks or causing vestibular discomfort. Pine does not yet expose a dedicated **motion token** scale in documentation; until those tokens land in the shared design language, follow the practices below. + +## Principles + +1. **Purpose** — Animate only to explain a change of state (open/close, enter/leave, loading, success/error), not for decoration. +2. **Speed** — Prefer short durations (roughly **120–200ms** for micro-interactions, **200–300ms** for overlays). Longer transitions belong to marketing or illustrative surfaces, not dense app chrome. +3. **Easing** — Use **ease-out** for elements entering or gaining focus; **ease-in** for exit; **ease-in-out** for properties that move both ways (for example height on expand/collapse). +4. **Choreography** — Stagger list items sparingly; default to a single coherent transition per layout region. +5. **Respect `prefers-reduced-motion`** — When implementing motion in Pine components or consuming apps, gate non-essential animation behind a media query and provide an instant state change for reduced-motion users. + +## Implementation hints + +- Prefer **CSS transitions** on `opacity`, `transform`, and `visibility` over animating layout-heavy properties (`width`, `height`, `top`) where possible. +- Keep **focus management** paired with motion: if a dialog animates open, focus should move predictably when the animation completes (or immediately when reduced motion applies). +- Coordinate with **design** on shared curves and durations so Stencil, React host apps, and marketing pages do not fight each other. + +## Tokens and docs + +When semantic motion tokens are published in the Kajabi design token set, this page should link to their Storybook entries alongside color, spacing, and typography. Until then, treat motion as **component-local** (scoped SCSS) plus the principles above. diff --git a/libs/core/src/stories/guides/rtl-and-localization.docs.mdx b/libs/core/src/stories/guides/rtl-and-localization.docs.mdx new file mode 100644 index 000000000..5d4364f05 --- /dev/null +++ b/libs/core/src/stories/guides/rtl-and-localization.docs.mdx @@ -0,0 +1,52 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# RTL and localization + +Pine targets **English-first** Kajabi product surfaces today. Right-to-left (RTL) locales and full internationalization (i18n) are **directional goals**: components should not block RTL or translated copy, but **parity is not guaranteed** until each component is explicitly validated. + +## What teams should assume today + +1. **Document text direction** at the app shell (`dir="rtl"` / `lang`) when you ship RTL; Pine does not yet ship a single global RTL QA matrix. +2. **Prefer tokens and logical intent** when writing new styles in consuming apps (for example spacing and alignment that should mirror under RTL). Pine SCSS continues to align with shared Kajabi token practices; report gaps in Slack (`#ds-support`). +3. **Strings live in the product**, not inside Pine primitives—pass translated `label`, `aria-label`, and slot content from your app’s i18n layer. + +## Component backlog (audit) + +Use this list as a **working checklist** for RTL and locale-sensitive behavior (mirroring, collision placement, iconography, truncation). Check items off in the issue or PR that proves behavior (visual + keyboard + screen reader where applicable). + +- [ ] `pds-accordion` +- [ ] `pds-alert` +- [ ] `pds-avatar` +- [ ] `pds-box` / `pds-row` / `pds-container` +- [ ] `pds-button` +- [ ] `pds-checkbox` +- [ ] `pds-chip` +- [ ] `pds-combobox` +- [ ] `pds-copytext` +- [ ] `pds-divider` +- [ ] `pds-dropdown-menu` +- [ ] `pds-filters` / `pds-filter` +- [ ] `pds-icon` +- [ ] `pds-image` +- [ ] `pds-input` +- [ ] `pds-link` +- [ ] `pds-loader` +- [ ] `pds-modal` (+ header / footer / content) +- [ ] `pds-multiselect` +- [ ] `pds-popover` +- [ ] `pds-progress` +- [ ] `pds-property` +- [ ] `pds-radio` / `pds-radio-group` +- [ ] `pds-select` +- [ ] `pds-sortable` +- [ ] `pds-switch` +- [ ] `pds-table` (+ row / cell / head) +- [ ] `pds-tabs` +- [ ] `pds-text` +- [ ] `pds-textarea` +- [ ] `pds-toast` +- [ ] `pds-tooltip` + +When an audit completes for a component, prefer capturing **fixed behavior** in Stencil spec/e2e tests and a short Storybook example (for example an RTL story wrapper) so regressions are caught in CI. diff --git a/libs/core/src/stories/resources/support.docs.mdx b/libs/core/src/stories/resources/support.docs.mdx index e7a951099..408ed5b07 100644 --- a/libs/core/src/stories/resources/support.docs.mdx +++ b/libs/core/src/stories/resources/support.docs.mdx @@ -19,6 +19,8 @@ Our team is here to help! You might find a quick answer by browsing the document Curious about what we're working on? You can check out our Linear board. +For **component lifecycle** labels (stable / beta / deprecated), see Component status. For **RTL and localization** expectations and an audit checklist, see RTL and localization. + ## GitHub Our codebase lives on GitHub. Feel free to: diff --git a/libs/figma/CODE_CONNECT_COVERAGE.md b/libs/figma/CODE_CONNECT_COVERAGE.md new file mode 100644 index 000000000..11f88657b --- /dev/null +++ b/libs/figma/CODE_CONNECT_COVERAGE.md @@ -0,0 +1,53 @@ +# Figma Code Connect coverage + +This file tracks which Pine `pds-*` components have a matching Code Connect file under `libs/figma/`. Update this list when you add or remove mappings. + +## Mapped (Code Connect present) + +| Pine component / pattern | Code Connect file | +| --- | --- | +| `pds-alert` | `components/pds-alert.figma.ts` | +| `pds-avatar` | `components/pds-avatar.figma.ts` | +| `pds-button` | `components/pds-button.figma.ts` | +| `pds-checkbox` | `components/pds-checkbox.figma.ts` | +| `pds-chip` | `components/pds-chip.figma.ts` | +| `pds-divider` | `components/pds-divider.figma.ts` | +| `pds-filter` (subcomponent of filters) | `components/pds-filter.figma.ts` | +| `pds-filters` | `components/pds-filters.figma.ts` | +| `pds-input` | `components/pds-input.figma.ts` | +| `pds-link` | `components/pds-link.figma.ts` | +| `pds-loader` | `components/pds-loader.figma.ts` | +| `pds-modal` | `components/pds-modal.figma.ts` | +| `pds-progress` | `components/pds-progress.figma.ts` | +| `pds-property` | `components/pds-property.figma.ts` | +| `pds-radio` | `components/pds-radio.figma.ts` | +| `pds-radio-group` | `components/pds-radio-group.figma.ts` | +| `pds-select` | `components/pds-select.figma.ts` | +| `pds-switch` | `components/pds-switch.figma.ts` | +| `pds-tabs` | `components/pds-tabs.figma.ts` | +| `pds-textarea` | `components/pds-textarea.figma.ts` | +| `pds-toast` | `components/pds-toast.figma.ts` | +| Pattern: list | `patterns/pds-list.figma.ts` | +| Pattern: page heading | `patterns/pds-page-heading.figma.ts` | + +## Not yet mapped (no Code Connect file) + +Use `figma.config.json` at the repo root (`documentUrlSubstitutions`) when adding a new `.figma.ts` so Figma URLs stay centralized. + +| Pine component | Notes | +| --- | --- | +| `pds-accordion` | Add `components/pds-accordion.figma.ts` when the Figma node is ready. | +| `pds-box` | Layout primitive; may map to layout frames rather than a single component node. | +| `pds-combobox` | | +| `pds-container` | | +| `pds-copytext` | | +| `pds-dropdown-menu` | | +| `pds-icon` | Icon set lives in `@pine-ds/icons`; map if Figma documents a dedicated icon wrapper. | +| `pds-image` | | +| `pds-multiselect` | | +| `pds-popover` | | +| `pds-row` | Often used with `pds-box`; may share layout documentation. | +| `pds-sortable` | Figma substitutions exist for sortable list patterns; wire a `pds-sortable.figma.ts` when aligned. | +| `pds-table` | Composite; consider per-subcomponent mappings (row, cell, head) if needed. | +| `pds-text` | | +| `pds-tooltip` | | From edc121e30ba6d94e27c32ca11603f874d90861e8 Mon Sep 17 00:00:00 2001 From: Quinton Jason Date: Fri, 22 May 2026 09:03:12 -0500 Subject: [PATCH 02/11] fix: use logical CSS for RTL layout gaps and simplify RTL guide Replace physical left/right with inset-inline properties in combobox, input, table sticky cells, tooltip arrows, and scroll shadows. Make table scroll-shadow visibility respect RTL scroll positions. Remove the per-component audit checklist from the RTL guide. --- .../components/pds-combobox/pds-combobox.scss | 2 +- .../src/components/pds-input/pds-input.scss | 4 +- .../pds-table-cell/pds-table-cell.scss | 2 +- .../pds-table-head-cell.scss | 2 +- .../src/components/pds-table/pds-table.scss | 16 +++--- .../src/components/pds-table/pds-table.tsx | 43 ++++++++++++---- .../components/pds-tooltip/pds-tooltip.scss | 34 ++++++------- .../guides/rtl-and-localization.docs.mdx | 49 +++---------------- .../src/stories/resources/support.docs.mdx | 2 +- 9 files changed, 73 insertions(+), 81 deletions(-) diff --git a/libs/core/src/components/pds-combobox/pds-combobox.scss b/libs/core/src/components/pds-combobox/pds-combobox.scss index d58b12800..379e479a3 100644 --- a/libs/core/src/components/pds-combobox/pds-combobox.scss +++ b/libs/core/src/components/pds-combobox/pds-combobox.scss @@ -70,9 +70,9 @@ .pds-combobox__input-icon { color: var(--pine-color-text-secondary); + inset-inline-end: var(--pine-dimension-150); pointer-events: none; position: absolute; - right: var(--pine-dimension-150); z-index: var(--pine-z-index-raised); } diff --git a/libs/core/src/components/pds-input/pds-input.scss b/libs/core/src/components/pds-input/pds-input.scss index 038a7511e..f6c109f38 100644 --- a/libs/core/src/components/pds-input/pds-input.scss +++ b/libs/core/src/components/pds-input/pds-input.scss @@ -48,14 +48,14 @@ border-end-start-radius: var(--pds-input-border-radius); border-inline-end: var(--pine-border-width-none); border-start-start-radius: var(--pds-input-border-radius); - left: var(--pds-input-padding-x); + inset-inline-start: var(--pds-input-padding-x); } &::part(suffix) { border-end-end-radius: var(--pds-input-border-radius); border-inline-start: var(--pine-border-width-none); border-start-end-radius: var(--pds-input-border-radius); - right: var(--pds-input-padding-x); + inset-inline-end: var(--pds-input-padding-x); } &::part(prepend), diff --git a/libs/core/src/components/pds-table/pds-table-cell/pds-table-cell.scss b/libs/core/src/components/pds-table/pds-table-cell/pds-table-cell.scss index 3b33c75bc..8cacb0732 100644 --- a/libs/core/src/components/pds-table/pds-table-cell/pds-table-cell.scss +++ b/libs/core/src/components/pds-table/pds-table-cell/pds-table-cell.scss @@ -20,7 +20,7 @@ :host(.is-fixed) { background: var(--pine-color-background-container); - left: 0; + inset-inline-start: 0; position: sticky; z-index: var(--pine-z-index-raised); } diff --git a/libs/core/src/components/pds-table/pds-table-head-cell/pds-table-head-cell.scss b/libs/core/src/components/pds-table/pds-table-head-cell/pds-table-head-cell.scss index 69cba337b..d5c44be88 100644 --- a/libs/core/src/components/pds-table/pds-table-head-cell/pds-table-head-cell.scss +++ b/libs/core/src/components/pds-table/pds-table-head-cell/pds-table-head-cell.scss @@ -24,7 +24,7 @@ :host(.is-fixed) { background: var(--pine-color-background-container); - left: var(--pine-dimension-none); + inset-inline-start: var(--pine-dimension-none); position: sticky; z-index: var(--pine-z-index-raised); } diff --git a/libs/core/src/components/pds-table/pds-table.scss b/libs/core/src/components/pds-table/pds-table.scss index 6edd07246..de9277b13 100644 --- a/libs/core/src/components/pds-table/pds-table.scss +++ b/libs/core/src/components/pds-table/pds-table.scss @@ -82,16 +82,16 @@ .scroll-shadow-left { /* stylelint-disable-next-line pine-design-system/no-hardcoded-colors */ - background: linear-gradient(to right, rgba(0, 0, 0, 0.1), transparent); - border-bottom-left-radius: inherit; - border-top-left-radius: inherit; - left: 0; + background: linear-gradient(to inline-end, rgba(0, 0, 0, 0.1), transparent); + border-end-start-radius: inherit; + border-start-start-radius: inherit; + inset-inline-start: 0; } .scroll-shadow-right { /* stylelint-disable-next-line pine-design-system/no-hardcoded-colors */ - background: linear-gradient(to left, rgba(0, 0, 0, 0.1), transparent); - border-bottom-right-radius: inherit; - border-top-right-radius: inherit; - right: 0; + background: linear-gradient(to inline-start, rgba(0, 0, 0, 0.1), transparent); + border-end-end-radius: inherit; + border-start-end-radius: inherit; + inset-inline-end: 0; } diff --git a/libs/core/src/components/pds-table/pds-table.tsx b/libs/core/src/components/pds-table/pds-table.tsx index f19408954..0ef1d0062 100644 --- a/libs/core/src/components/pds-table/pds-table.tsx +++ b/libs/core/src/components/pds-table/pds-table.tsx @@ -137,12 +137,36 @@ export class PdsTable { } } + /** + * Distance scrolled from the inline-start edge (LTR: left, RTL: right). + */ + private getScrollOffsetFromStart(container: HTMLElement): number { + const isRtl = getComputedStyle(container).direction === 'rtl'; + const { scrollLeft, scrollWidth, clientWidth } = container; + const maxScroll = scrollWidth - clientWidth; + + if (maxScroll <= 0) { + return 0; + } + + if (!isRtl) { + return scrollLeft; + } + + // RTL: WebKit/Blink use 0 at inline-start then negative values as you scroll + if (scrollLeft <= 0) { + return -scrollLeft; + } + + return maxScroll - scrollLeft; + } + /** * Sets up responsive scrolling behavior for the table. * * This method creates a horizontal scrolling system where: * - The table content can scroll horizontally when it exceeds the container width - * - Scroll shadows appear at the left/right edges to indicate scrollable content + * - Scroll shadows appear at the inline-start/end edges to indicate scrollable content * - Fixed columns remain sticky during horizontal scrolling * - Shadows respect border-radius and don't appear when there's nothing to scroll * @@ -165,20 +189,21 @@ export class PdsTable { /** * Updates the visibility of scroll shadows based on current scroll position. - * Left shadow: Shows when scrolled away from start (hidden if fixedColumn is enabled) - * Right shadow: Shows when there's content to scroll and not at the end + * Inline-start shadow: shows when scrolled away from start (hidden if fixedColumn is enabled) + * Inline-end shadow: shows when there is content to scroll and not at the end */ this._responsiveHandleScroll = () => { if (!this.scrollContainer) return; - const scrollLeft = this.scrollContainer.scrollLeft; - const maxScrollLeft = this.scrollContainer.scrollWidth - this.scrollContainer.clientWidth; + const { scrollWidth, clientWidth } = this.scrollContainer; + const maxScroll = scrollWidth - clientWidth; + const scrollOffset = this.getScrollOffsetFromStart(this.scrollContainer); - // Show left shadow when scrolled away from start, but not if there's a fixed column - leftShadow.style.opacity = (scrollLeft > 0 && !this.fixedColumn) ? '1' : '0'; + // Inline-start shadow when scrolled away from start, but not if there's a fixed column + leftShadow.style.opacity = (scrollOffset > 0 && !this.fixedColumn) ? '1' : '0'; - // Show right shadow only if there's content to scroll AND not at end - rightShadow.style.opacity = (maxScrollLeft > 0 && scrollLeft < maxScrollLeft - 1) ? '1' : '0'; + // Inline-end shadow when there is content to scroll and not at the end + rightShadow.style.opacity = (maxScroll > 0 && scrollOffset < maxScroll - 1) ? '1' : '0'; }; // Add scroll event listener to container element diff --git a/libs/core/src/components/pds-tooltip/pds-tooltip.scss b/libs/core/src/components/pds-tooltip/pds-tooltip.scss index 48e6cd379..2bae96df2 100644 --- a/libs/core/src/components/pds-tooltip/pds-tooltip.scss +++ b/libs/core/src/components/pds-tooltip/pds-tooltip.scss @@ -44,7 +44,7 @@ .pds-tooltip--right & { border-inline-end-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-left); - left: calc(var(--tooltip-sizing-arrow) * -1); + inset-inline-start: calc(var(--tooltip-sizing-arrow) * -1); top: 50%; transform: translateY(-50%); } @@ -53,14 +53,14 @@ border-inline-end-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-left); bottom: var(--tooltip-sizing-arrow-offset); - left: calc(var(--tooltip-sizing-arrow) * -1); + inset-inline-start: calc(var(--tooltip-sizing-arrow) * -1); top: initial; } .pds-tooltip--right-start & { border-inline-end-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-left); - left: calc(var(--tooltip-sizing-arrow) * -1); + inset-inline-start: calc(var(--tooltip-sizing-arrow) * -1); top: var(--tooltip-sizing-arrow-offset); } @@ -68,7 +68,7 @@ border-block-start-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-down); bottom: calc(var(--tooltip-sizing-arrow) * -1); - left: 50%; + inset-inline-start: 50%; top: initial; transform: translateX(-50%); } @@ -77,7 +77,7 @@ border-block-start-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-down); bottom: calc(var(--tooltip-sizing-arrow) * -1); - left: var(--tooltip-sizing-arrow-offset); + inset-inline-start: var(--tooltip-sizing-arrow-offset); top: initial; } @@ -85,16 +85,16 @@ border-block-start-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-down); bottom: calc(var(--tooltip-sizing-arrow) * -1); - left: initial; - right: var(--tooltip-sizing-arrow-offset); + inset-inline-end: var(--tooltip-sizing-arrow-offset); + inset-inline-start: initial; top: initial; } .pds-tooltip--left & { border-inline-start-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-right); - left: initial; - right: calc(var(--tooltip-sizing-arrow) * -1); + inset-inline-end: calc(var(--tooltip-sizing-arrow) * -1); + inset-inline-start: initial; top: 50%; transform: translateY(-50%); } @@ -103,23 +103,23 @@ border-inline-start-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-right); bottom: var(--tooltip-sizing-arrow-offset); - left: initial; - right: calc(var(--tooltip-sizing-arrow) * -1); + inset-inline-end: calc(var(--tooltip-sizing-arrow) * -1); + inset-inline-start: initial; top: initial; } .pds-tooltip--left-start & { border-inline-start-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-right); - left: initial; - right: calc(var(--tooltip-sizing-arrow) * -1); + inset-inline-end: calc(var(--tooltip-sizing-arrow) * -1); + inset-inline-start: initial; top: var(--tooltip-sizing-arrow-offset); } .pds-tooltip--bottom & { border-block-end-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-up); - left: 50%; + inset-inline-start: 50%; top: calc(var(--tooltip-sizing-arrow) * -1); transform: translateX(-50%); } @@ -127,15 +127,15 @@ .pds-tooltip--bottom-end & { border-block-end-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-up); - left: initial; - right: var(--tooltip-sizing-arrow-offset); + inset-inline-end: var(--tooltip-sizing-arrow-offset); + inset-inline-start: initial; top: calc(var(--tooltip-sizing-arrow) * -1); } .pds-tooltip--bottom-start & { border-block-end-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-up); - left: var(--tooltip-sizing-arrow-offset); + inset-inline-start: var(--tooltip-sizing-arrow-offset); top: calc(var(--tooltip-sizing-arrow) * -1); } diff --git a/libs/core/src/stories/guides/rtl-and-localization.docs.mdx b/libs/core/src/stories/guides/rtl-and-localization.docs.mdx index 5d4364f05..aac86e818 100644 --- a/libs/core/src/stories/guides/rtl-and-localization.docs.mdx +++ b/libs/core/src/stories/guides/rtl-and-localization.docs.mdx @@ -4,49 +4,16 @@ import { Meta } from '@storybook/addon-docs/blocks'; # RTL and localization -Pine targets **English-first** Kajabi product surfaces today. Right-to-left (RTL) locales and full internationalization (i18n) are **directional goals**: components should not block RTL or translated copy, but **parity is not guaranteed** until each component is explicitly validated. +Pine targets **English-first** Kajabi product surfaces today. Components are built to work in RTL when the app sets `dir="rtl"` and passes translated copy from the product i18n layer. Layout styles in Pine SCSS use **CSS logical properties** (`margin-inline`, `padding-inline`, `inset-inline`, `border-*-start/end`, and similar) so mirroring does not require per-locale stylesheets. ## What teams should assume today -1. **Document text direction** at the app shell (`dir="rtl"` / `lang`) when you ship RTL; Pine does not yet ship a single global RTL QA matrix. -2. **Prefer tokens and logical intent** when writing new styles in consuming apps (for example spacing and alignment that should mirror under RTL). Pine SCSS continues to align with shared Kajabi token practices; report gaps in Slack (`#ds-support`). +1. **Document text direction** at the app shell (`dir="rtl"` / `lang`) when you ship RTL. +2. **Prefer tokens and logical properties** in consuming apps for spacing and alignment that should mirror under RTL. Report gaps in Slack (`#ds-support`). 3. **Strings live in the product**, not inside Pine primitives—pass translated `label`, `aria-label`, and slot content from your app’s i18n layer. -## Component backlog (audit) - -Use this list as a **working checklist** for RTL and locale-sensitive behavior (mirroring, collision placement, iconography, truncation). Check items off in the issue or PR that proves behavior (visual + keyboard + screen reader where applicable). - -- [ ] `pds-accordion` -- [ ] `pds-alert` -- [ ] `pds-avatar` -- [ ] `pds-box` / `pds-row` / `pds-container` -- [ ] `pds-button` -- [ ] `pds-checkbox` -- [ ] `pds-chip` -- [ ] `pds-combobox` -- [ ] `pds-copytext` -- [ ] `pds-divider` -- [ ] `pds-dropdown-menu` -- [ ] `pds-filters` / `pds-filter` -- [ ] `pds-icon` -- [ ] `pds-image` -- [ ] `pds-input` -- [ ] `pds-link` -- [ ] `pds-loader` -- [ ] `pds-modal` (+ header / footer / content) -- [ ] `pds-multiselect` -- [ ] `pds-popover` -- [ ] `pds-progress` -- [ ] `pds-property` -- [ ] `pds-radio` / `pds-radio-group` -- [ ] `pds-select` -- [ ] `pds-sortable` -- [ ] `pds-switch` -- [ ] `pds-table` (+ row / cell / head) -- [ ] `pds-tabs` -- [ ] `pds-text` -- [ ] `pds-textarea` -- [ ] `pds-toast` -- [ ] `pds-tooltip` - -When an audit completes for a component, prefer capturing **fixed behavior** in Stencil spec/e2e tests and a short Storybook example (for example an RTL story wrapper) so regressions are caught in CI. +## Verifying in Storybook + +Use the **Direction** toolbar (LTR / RTL) when available ([#739](https://github.com/Kajabi/pine/pull/739)) to spot-check components: mirroring, overlay placement, iconography, truncation, keyboard, and screen reader behavior where applicable. + +When you change component layout or positioning, add or update Stencil tests and a short RTL Storybook example where it helps catch regressions in CI. diff --git a/libs/core/src/stories/resources/support.docs.mdx b/libs/core/src/stories/resources/support.docs.mdx index 408ed5b07..8605370ef 100644 --- a/libs/core/src/stories/resources/support.docs.mdx +++ b/libs/core/src/stories/resources/support.docs.mdx @@ -19,7 +19,7 @@ Our team is here to help! You might find a quick answer by browsing the document Curious about what we're working on? You can check out our Linear board. -For **component lifecycle** labels (stable / beta / deprecated), see Component status. For **RTL and localization** expectations and an audit checklist, see RTL and localization. +For **component lifecycle** labels (stable / beta / deprecated), see Component status. For **RTL and localization** expectations, see RTL and localization. ## GitHub From 5c3c0d4a44ab5971445db3df4112374f313401b0 Mon Sep 17 00:00:00 2001 From: Quinton Jason Date: Fri, 22 May 2026 09:24:22 -0500 Subject: [PATCH 03/11] docs: defer Storybook RTL toolbar copy to PR 739 --- libs/core/src/stories/guides/rtl-and-localization.docs.mdx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/libs/core/src/stories/guides/rtl-and-localization.docs.mdx b/libs/core/src/stories/guides/rtl-and-localization.docs.mdx index aac86e818..ffbbf2381 100644 --- a/libs/core/src/stories/guides/rtl-and-localization.docs.mdx +++ b/libs/core/src/stories/guides/rtl-and-localization.docs.mdx @@ -12,8 +12,6 @@ Pine targets **English-first** Kajabi product surfaces today. Components are bui 2. **Prefer tokens and logical properties** in consuming apps for spacing and alignment that should mirror under RTL. Report gaps in Slack (`#ds-support`). 3. **Strings live in the product**, not inside Pine primitives—pass translated `label`, `aria-label`, and slot content from your app’s i18n layer. -## Verifying in Storybook +## Storybook tooling -Use the **Direction** toolbar (LTR / RTL) when available ([#739](https://github.com/Kajabi/pine/pull/739)) to spot-check components: mirroring, overlay placement, iconography, truncation, keyboard, and screen reader behavior where applicable. - -When you change component layout or positioning, add or update Stencil tests and a short RTL Storybook example where it helps catch regressions in CI. +**Pending:** The **Direction** toolbar (LTR / RTL) for spot-checking components in Storybook ships in [#739](https://github.com/Kajabi/pine/pull/739). Audit steps and usage notes will live in this guide once that PR merges. From 26c4e015ce0743b0b0deaf1a2711361352e99ada Mon Sep 17 00:00:00 2001 From: Quinton Jason Date: Fri, 22 May 2026 09:32:48 -0500 Subject: [PATCH 04/11] fix(pds-table): address RTL scroll and tooltip arrow feedback Add shared scroll utils with cross-browser RTL scrollLeft detection. Use them for table shadows and fixed-column has-scrolled state. Revert tooltip arrow positioning to physical left/right to match Floating UI placement classes. --- .../pds-table-cell/pds-table-cell.tsx | 3 +- .../pds-table-head-cell.tsx | 3 +- .../src/components/pds-table/pds-table.tsx | 27 +------ .../components/pds-tooltip/pds-tooltip.scss | 35 +++++---- libs/core/src/utils/scroll.ts | 77 +++++++++++++++++++ libs/core/src/utils/test/scroll.spec.ts | 36 +++++++++ 6 files changed, 137 insertions(+), 44 deletions(-) create mode 100644 libs/core/src/utils/scroll.ts create mode 100644 libs/core/src/utils/test/scroll.spec.ts diff --git a/libs/core/src/components/pds-table/pds-table-cell/pds-table-cell.tsx b/libs/core/src/components/pds-table/pds-table-cell/pds-table-cell.tsx index b508d07e3..69e6fb0a8 100644 --- a/libs/core/src/components/pds-table/pds-table-cell/pds-table-cell.tsx +++ b/libs/core/src/components/pds-table/pds-table-cell/pds-table-cell.tsx @@ -1,4 +1,5 @@ import { Component, Element, Host, Prop, State, Watch, h } from '@stencil/core'; +import { isScrolledFromInlineStart } from '../../../utils/scroll'; import { setupTruncationTooltip } from '../../../utils/truncation-tooltip'; @Component({ @@ -152,7 +153,7 @@ export class PdsTableCell { } try { - this.tableScrolling = this.scrollContainer.scrollLeft > 0; + this.tableScrolling = isScrolledFromInlineStart(this.scrollContainer); } catch (error) { console.warn('Scroll handler error:', error); } diff --git a/libs/core/src/components/pds-table/pds-table-head-cell/pds-table-head-cell.tsx b/libs/core/src/components/pds-table/pds-table-head-cell/pds-table-head-cell.tsx index 177276594..7c089aca2 100644 --- a/libs/core/src/components/pds-table/pds-table-head-cell/pds-table-head-cell.tsx +++ b/libs/core/src/components/pds-table/pds-table-head-cell/pds-table-head-cell.tsx @@ -1,6 +1,7 @@ import { Component, Element, Host, Prop, h, Event, EventEmitter, State, Method } from '@stencil/core'; import { downSmall, upSmall } from '@pine-ds/icons/icons'; +import { isScrolledFromInlineStart } from '../../../utils/scroll'; @Component({ tag: 'pds-table-head-cell', @@ -188,7 +189,7 @@ export class PdsTableHeadCell { } try { - this.tableScrolling = this.scrollContainer.scrollLeft > 0; + this.tableScrolling = isScrolledFromInlineStart(this.scrollContainer); } catch (error) { console.warn('Scroll handler error:', error); } diff --git a/libs/core/src/components/pds-table/pds-table.tsx b/libs/core/src/components/pds-table/pds-table.tsx index 0ef1d0062..6231ddb67 100644 --- a/libs/core/src/components/pds-table/pds-table.tsx +++ b/libs/core/src/components/pds-table/pds-table.tsx @@ -1,4 +1,5 @@ import { Component, Element, Event, EventEmitter, Host, h, Prop, State, Listen } from '@stencil/core'; +import { getScrollOffsetFromStart } from '../../utils/scroll'; @Component({ tag: 'pds-table', @@ -137,30 +138,6 @@ export class PdsTable { } } - /** - * Distance scrolled from the inline-start edge (LTR: left, RTL: right). - */ - private getScrollOffsetFromStart(container: HTMLElement): number { - const isRtl = getComputedStyle(container).direction === 'rtl'; - const { scrollLeft, scrollWidth, clientWidth } = container; - const maxScroll = scrollWidth - clientWidth; - - if (maxScroll <= 0) { - return 0; - } - - if (!isRtl) { - return scrollLeft; - } - - // RTL: WebKit/Blink use 0 at inline-start then negative values as you scroll - if (scrollLeft <= 0) { - return -scrollLeft; - } - - return maxScroll - scrollLeft; - } - /** * Sets up responsive scrolling behavior for the table. * @@ -197,7 +174,7 @@ export class PdsTable { const { scrollWidth, clientWidth } = this.scrollContainer; const maxScroll = scrollWidth - clientWidth; - const scrollOffset = this.getScrollOffsetFromStart(this.scrollContainer); + const scrollOffset = getScrollOffsetFromStart(this.scrollContainer); // Inline-start shadow when scrolled away from start, but not if there's a fixed column leftShadow.style.opacity = (scrollOffset > 0 && !this.fixedColumn) ? '1' : '0'; diff --git a/libs/core/src/components/pds-tooltip/pds-tooltip.scss b/libs/core/src/components/pds-tooltip/pds-tooltip.scss index 2bae96df2..b1a1b3fc3 100644 --- a/libs/core/src/components/pds-tooltip/pds-tooltip.scss +++ b/libs/core/src/components/pds-tooltip/pds-tooltip.scss @@ -41,10 +41,11 @@ position: absolute; width: 0; + // Arrow placement uses physical left/right to match Floating UI placement names. .pds-tooltip--right & { border-inline-end-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-left); - inset-inline-start: calc(var(--tooltip-sizing-arrow) * -1); + left: calc(var(--tooltip-sizing-arrow) * -1); top: 50%; transform: translateY(-50%); } @@ -53,14 +54,14 @@ border-inline-end-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-left); bottom: var(--tooltip-sizing-arrow-offset); - inset-inline-start: calc(var(--tooltip-sizing-arrow) * -1); + left: calc(var(--tooltip-sizing-arrow) * -1); top: initial; } .pds-tooltip--right-start & { border-inline-end-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-left); - inset-inline-start: calc(var(--tooltip-sizing-arrow) * -1); + left: calc(var(--tooltip-sizing-arrow) * -1); top: var(--tooltip-sizing-arrow-offset); } @@ -68,7 +69,7 @@ border-block-start-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-down); bottom: calc(var(--tooltip-sizing-arrow) * -1); - inset-inline-start: 50%; + left: 50%; top: initial; transform: translateX(-50%); } @@ -77,7 +78,7 @@ border-block-start-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-down); bottom: calc(var(--tooltip-sizing-arrow) * -1); - inset-inline-start: var(--tooltip-sizing-arrow-offset); + left: var(--tooltip-sizing-arrow-offset); top: initial; } @@ -85,16 +86,16 @@ border-block-start-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-down); bottom: calc(var(--tooltip-sizing-arrow) * -1); - inset-inline-end: var(--tooltip-sizing-arrow-offset); - inset-inline-start: initial; + left: initial; + right: var(--tooltip-sizing-arrow-offset); top: initial; } .pds-tooltip--left & { border-inline-start-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-right); - inset-inline-end: calc(var(--tooltip-sizing-arrow) * -1); - inset-inline-start: initial; + left: initial; + right: calc(var(--tooltip-sizing-arrow) * -1); top: 50%; transform: translateY(-50%); } @@ -103,23 +104,23 @@ border-inline-start-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-right); bottom: var(--tooltip-sizing-arrow-offset); - inset-inline-end: calc(var(--tooltip-sizing-arrow) * -1); - inset-inline-start: initial; + left: initial; + right: calc(var(--tooltip-sizing-arrow) * -1); top: initial; } .pds-tooltip--left-start & { border-inline-start-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-right); - inset-inline-end: calc(var(--tooltip-sizing-arrow) * -1); - inset-inline-start: initial; + left: initial; + right: calc(var(--tooltip-sizing-arrow) * -1); top: var(--tooltip-sizing-arrow-offset); } .pds-tooltip--bottom & { border-block-end-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-up); - inset-inline-start: 50%; + left: 50%; top: calc(var(--tooltip-sizing-arrow) * -1); transform: translateX(-50%); } @@ -127,15 +128,15 @@ .pds-tooltip--bottom-end & { border-block-end-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-up); - inset-inline-end: var(--tooltip-sizing-arrow-offset); - inset-inline-start: initial; + left: initial; + right: var(--tooltip-sizing-arrow-offset); top: calc(var(--tooltip-sizing-arrow) * -1); } .pds-tooltip--bottom-start & { border-block-end-color: var(--pine-color-primary); border-width: var(--tooltip-border-width-arrow-up); - inset-inline-start: var(--tooltip-sizing-arrow-offset); + left: var(--tooltip-sizing-arrow-offset); top: calc(var(--tooltip-sizing-arrow) * -1); } diff --git a/libs/core/src/utils/scroll.ts b/libs/core/src/utils/scroll.ts new file mode 100644 index 000000000..fecf153be --- /dev/null +++ b/libs/core/src/utils/scroll.ts @@ -0,0 +1,77 @@ +export type RtlScrollType = 'negative' | 'reverse' | 'default'; + +let cachedRtlScrollType: RtlScrollType | null = null; + +/** + * Detects how this browser reports scrollLeft for RTL overflow containers. + * @see https://github.com/othree/jquery.rtl-scroll-type + */ +export function getRtlScrollType(): RtlScrollType { + if (cachedRtlScrollType) { + return cachedRtlScrollType; + } + + if (typeof document === 'undefined') { + cachedRtlScrollType = 'default'; + return cachedRtlScrollType; + } + + const probe = document.createElement('div'); + probe.style.width = '4px'; + probe.style.height = '1px'; + probe.style.position = 'absolute'; + probe.style.top = '-1000px'; + probe.style.overflow = 'scroll'; + probe.style.direction = 'rtl'; + + document.body.appendChild(probe); + + if (probe.scrollLeft > 0) { + cachedRtlScrollType = 'reverse'; + } else { + probe.scrollLeft = 1; + cachedRtlScrollType = probe.scrollLeft === 0 ? 'negative' : 'default'; + } + + document.body.removeChild(probe); + return cachedRtlScrollType; +} + +/** Reset cached RTL scroll type (for tests). */ +export function resetRtlScrollTypeCache(): void { + cachedRtlScrollType = null; +} + +/** + * Distance scrolled from the inline-start edge (LTR: left, RTL: right). + */ +export function getScrollOffsetFromStart(container: HTMLElement): number { + const { scrollLeft, scrollWidth, clientWidth } = container; + const maxScroll = Math.max(0, scrollWidth - clientWidth); + + if (maxScroll <= 0) { + return 0; + } + + const isRtl = getComputedStyle(container).direction === 'rtl'; + if (!isRtl) { + return scrollLeft; + } + + const rtlType = getRtlScrollType(); + + switch (rtlType) { + case 'negative': + return Math.max(0, -scrollLeft); + case 'reverse': + return Math.max(0, maxScroll - scrollLeft); + case 'default': + default: + return scrollLeft; + } +} + +/** True when the container is scrolled away from its inline-start edge. */ +export function isScrolledFromInlineStart(container: HTMLElement): boolean { + return getScrollOffsetFromStart(container) > 0; +} diff --git a/libs/core/src/utils/test/scroll.spec.ts b/libs/core/src/utils/test/scroll.spec.ts new file mode 100644 index 000000000..84a2cca27 --- /dev/null +++ b/libs/core/src/utils/test/scroll.spec.ts @@ -0,0 +1,36 @@ +import { + getScrollOffsetFromStart, + isScrolledFromInlineStart, + resetRtlScrollTypeCache, +} from '../scroll'; + +describe('scroll utilities', () => { + afterEach(() => { + resetRtlScrollTypeCache(); + }); + + it('returns 0 when there is no overflow', () => { + const container = document.createElement('div'); + Object.defineProperty(container, 'scrollWidth', { value: 100, configurable: true }); + Object.defineProperty(container, 'clientWidth', { value: 100, configurable: true }); + Object.defineProperty(container, 'scrollLeft', { value: 0, writable: true, configurable: true }); + + expect(getScrollOffsetFromStart(container)).toBe(0); + expect(isScrolledFromInlineStart(container)).toBe(false); + }); + + it('uses scrollLeft for LTR containers', () => { + const container = document.createElement('div'); + container.style.direction = 'ltr'; + document.body.appendChild(container); + + Object.defineProperty(container, 'scrollWidth', { value: 200, configurable: true }); + Object.defineProperty(container, 'clientWidth', { value: 100, configurable: true }); + Object.defineProperty(container, 'scrollLeft', { value: 25, writable: true, configurable: true }); + + expect(getScrollOffsetFromStart(container)).toBe(25); + expect(isScrolledFromInlineStart(container)).toBe(true); + + document.body.removeChild(container); + }); +}); From f88e0fd5571486a84222719f8a4d89c54e179722 Mon Sep 17 00:00:00 2001 From: Quinton Jason Date: Tue, 26 May 2026 18:02:37 -0500 Subject: [PATCH 05/11] fix(pds-table): correct RTL scroll offset and shadow gradients Fix swapped default/reverse RTL scrollLeft normalization, add scrollable content to the browser probe, use valid gradient directions with RTL overrides, and expand scroll utility tests. --- .../src/components/pds-table/pds-table.scss | 14 ++++++- libs/core/src/utils/scroll.ts | 41 +++++++++++++------ libs/core/src/utils/test/scroll.spec.ts | 30 ++++++++++++++ 3 files changed, 70 insertions(+), 15 deletions(-) diff --git a/libs/core/src/components/pds-table/pds-table.scss b/libs/core/src/components/pds-table/pds-table.scss index de9277b13..0a5bd0996 100644 --- a/libs/core/src/components/pds-table/pds-table.scss +++ b/libs/core/src/components/pds-table/pds-table.scss @@ -82,16 +82,26 @@ .scroll-shadow-left { /* stylelint-disable-next-line pine-design-system/no-hardcoded-colors */ - background: linear-gradient(to inline-end, rgba(0, 0, 0, 0.1), transparent); + background: linear-gradient(to right, rgba(0, 0, 0, 0.1), transparent); border-end-start-radius: inherit; border-start-start-radius: inherit; inset-inline-start: 0; } +:host-context([dir='rtl']) .scroll-shadow-left { + /* stylelint-disable-next-line pine-design-system/no-hardcoded-colors */ + background: linear-gradient(to left, rgba(0, 0, 0, 0.1), transparent); +} + .scroll-shadow-right { /* stylelint-disable-next-line pine-design-system/no-hardcoded-colors */ - background: linear-gradient(to inline-start, rgba(0, 0, 0, 0.1), transparent); + background: linear-gradient(to left, rgba(0, 0, 0, 0.1), transparent); border-end-end-radius: inherit; border-start-end-radius: inherit; inset-inline-end: 0; } + +:host-context([dir='rtl']) .scroll-shadow-right { + /* stylelint-disable-next-line pine-design-system/no-hardcoded-colors */ + background: linear-gradient(to right, rgba(0, 0, 0, 0.1), transparent); +} diff --git a/libs/core/src/utils/scroll.ts b/libs/core/src/utils/scroll.ts index fecf153be..817308afb 100644 --- a/libs/core/src/utils/scroll.ts +++ b/libs/core/src/utils/scroll.ts @@ -17,13 +17,18 @@ export function getRtlScrollType(): RtlScrollType { } const probe = document.createElement('div'); - probe.style.width = '4px'; - probe.style.height = '1px'; + probe.style.width = '100px'; + probe.style.height = '100px'; probe.style.position = 'absolute'; probe.style.top = '-1000px'; probe.style.overflow = 'scroll'; probe.style.direction = 'rtl'; + const inner = document.createElement('div'); + inner.style.width = '200px'; + inner.style.height = '100px'; + probe.appendChild(inner); + document.body.appendChild(probe); if (probe.scrollLeft > 0) { @@ -42,6 +47,26 @@ export function resetRtlScrollTypeCache(): void { cachedRtlScrollType = null; } +/** + * RTL scroll offset from inline-start for a known browser scrollLeft model. + * @see https://github.com/othree/jquery.rtl-scroll-type#3-types-of-scrollleft-scrollwidth--100 + */ +export function getRtlScrollOffsetFromStart( + scrollLeft: number, + maxScroll: number, + rtlType: RtlScrollType, +): number { + switch (rtlType) { + case 'negative': + return Math.max(0, -scrollLeft); + case 'reverse': + return Math.max(0, scrollLeft); + case 'default': + default: + return Math.max(0, maxScroll - scrollLeft); + } +} + /** * Distance scrolled from the inline-start edge (LTR: left, RTL: right). */ @@ -58,17 +83,7 @@ export function getScrollOffsetFromStart(container: HTMLElement): number { return scrollLeft; } - const rtlType = getRtlScrollType(); - - switch (rtlType) { - case 'negative': - return Math.max(0, -scrollLeft); - case 'reverse': - return Math.max(0, maxScroll - scrollLeft); - case 'default': - default: - return scrollLeft; - } + return getRtlScrollOffsetFromStart(scrollLeft, maxScroll, getRtlScrollType()); } /** True when the container is scrolled away from its inline-start edge. */ diff --git a/libs/core/src/utils/test/scroll.spec.ts b/libs/core/src/utils/test/scroll.spec.ts index 84a2cca27..b6fe5a82e 100644 --- a/libs/core/src/utils/test/scroll.spec.ts +++ b/libs/core/src/utils/test/scroll.spec.ts @@ -1,4 +1,6 @@ import { + getRtlScrollOffsetFromStart, + getRtlScrollType, getScrollOffsetFromStart, isScrolledFromInlineStart, resetRtlScrollTypeCache, @@ -33,4 +35,32 @@ describe('scroll utilities', () => { document.body.removeChild(container); }); + + describe('getRtlScrollOffsetFromStart', () => { + const maxScroll = 100; + + it('default (Blink): rest at scrollLeft=max, offset 0', () => { + expect(getRtlScrollOffsetFromStart(100, maxScroll, 'default')).toBe(0); + expect(getRtlScrollOffsetFromStart(75, maxScroll, 'default')).toBe(25); + expect(getRtlScrollOffsetFromStart(0, maxScroll, 'default')).toBe(100); + }); + + it('reverse (legacy IE/Edge): rest at scrollLeft=0, offset 0', () => { + expect(getRtlScrollOffsetFromStart(0, maxScroll, 'reverse')).toBe(0); + expect(getRtlScrollOffsetFromStart(25, maxScroll, 'reverse')).toBe(25); + expect(getRtlScrollOffsetFromStart(100, maxScroll, 'reverse')).toBe(100); + }); + + it('negative (Firefox): rest at scrollLeft=0, offset increases when negative', () => { + expect(getRtlScrollOffsetFromStart(0, maxScroll, 'negative')).toBe(0); + expect(getRtlScrollOffsetFromStart(-25, maxScroll, 'negative')).toBe(25); + expect(getRtlScrollOffsetFromStart(-100, maxScroll, 'negative')).toBe(100); + }); + }); + + it('detects RTL scroll type with scrollable probe content', () => { + resetRtlScrollTypeCache(); + const type = getRtlScrollType(); + expect(['negative', 'reverse', 'default']).toContain(type); + }); }); From d5ac8a7ae9459fe5a018aaf596550bd5d28b29e9 Mon Sep 17 00:00:00 2001 From: Quinton Jason Date: Thu, 28 May 2026 18:35:06 -0500 Subject: [PATCH 06/11] feat(storybook): add LTR/RTL preview toggle and audit pointer (#739) * feat(storybook): add LTR/RTL preview toggle and audit pointer Add a Direction global type with an LTR/RTL toolbar control. The withDirection decorator applies dir on documentElement and body for the active story (and the parent docs frame), matching the consumer contract for RTL surfaces. Update the RTL and localization guide with a How to audit section pointing teams at the toggle. The audit checklist stays intentionally unchecked; rows should be marked off in the PR that proves behavior. * fix(storybook): use direction icon for RTL toolbar * fix(storybook): scope RTL dir to preview iframe only * fix(storybook): use valid transfer icon for direction toolbar * docs(storybook): update RTL guide cross-reference in preview --- libs/core/.storybook/preview.js | 32 +++++++++++++++++++ .../guides/rtl-and-localization.docs.mdx | 8 +++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/libs/core/.storybook/preview.js b/libs/core/.storybook/preview.js index 0f01c72cc..a013d0783 100644 --- a/libs/core/.storybook/preview.js +++ b/libs/core/.storybook/preview.js @@ -138,10 +138,29 @@ const withTheme = (StoryFn, context) => { return StoryFn(); }; +// Direction decorator that applies `dir` on the preview iframe only. +// Unlike data-theme, `dir` is interpreted by the browser and must not be set on +// window.parent (that would flip Storybook manager/docs chrome). +// See Guides/RTL and localization for usage. +const withDirection = (StoryFn, context) => { + const direction = context.globals.direction || 'ltr'; + + document.documentElement.setAttribute('dir', direction); + document.body.setAttribute('dir', direction); + + useEffect(() => { + document.documentElement.setAttribute('dir', direction); + document.body.setAttribute('dir', direction); + }, [direction]); + + return StoryFn(); +}; + const preview = { decorators: [ withCustomEventActions, withTheme, + withDirection, ], globalTypes: { @@ -157,10 +176,23 @@ const preview = { dynamicTitle: true, }, }, + direction: { + description: 'Document direction (LTR or RTL)', + toolbar: { + title: 'Direction', + icon: 'transfer', + items: [ + { value: 'ltr', title: 'LTR', right: 'Left-to-right' }, + { value: 'rtl', title: 'RTL', right: 'Right-to-left' }, + ], + dynamicTitle: true, + }, + }, }, initialGlobals: { theme: 'light', + direction: 'ltr', }, parameters: { diff --git a/libs/core/src/stories/guides/rtl-and-localization.docs.mdx b/libs/core/src/stories/guides/rtl-and-localization.docs.mdx index ffbbf2381..5fbed1768 100644 --- a/libs/core/src/stories/guides/rtl-and-localization.docs.mdx +++ b/libs/core/src/stories/guides/rtl-and-localization.docs.mdx @@ -12,6 +12,10 @@ Pine targets **English-first** Kajabi product surfaces today. Components are bui 2. **Prefer tokens and logical properties** in consuming apps for spacing and alignment that should mirror under RTL. Report gaps in Slack (`#ds-support`). 3. **Strings live in the product**, not inside Pine primitives—pass translated `label`, `aria-label`, and slot content from your app’s i18n layer. -## Storybook tooling +## Verifying in Storybook -**Pending:** The **Direction** toolbar (LTR / RTL) for spot-checking components in Storybook ships in [#739](https://github.com/Kajabi/pine/pull/739). Audit steps and usage notes will live in this guide once that PR merges. +Storybook includes a **Direction** toolbar control (LTR / RTL). Switching it applies `dir` on the preview iframe’s `` and ``, matching the consumer-app contract for RTL surfaces. + +Use the toggle to spot-check components: mirroring, overlay placement, iconography, truncation, keyboard, and screen reader behavior where applicable. + +When you change component layout or positioning, add or update Stencil tests and a short RTL Storybook example where it helps catch regressions in CI. From 27e247b35ee9016b093afa490dc3032562dc9725 Mon Sep 17 00:00:00 2001 From: Quinton Jason Date: Fri, 29 May 2026 16:27:17 -0500 Subject: [PATCH 07/11] feat(motion): add Pine-internal motion token scaffold (#740) * feat(motion): add Pine-internal motion token scaffold Add libs/core/src/global/styles/_motion.scss exposing duration and easing CSS custom properties (--pine-motion-duration-{fast,base,slow} and --pine-motion-easing-{out,in,in-out}). Wired into app.scss so the tokens land in the global stylesheet alongside fonts. Durations collapse to 0ms under prefers-reduced-motion so consumer components inherit the override without per-component media queries. Update the Guides/Motion doc to remove the no-tokens disclaimer, document the new tokens with usage tables, and call out the upstream follow-up to consolidate into @kajabi-ui/styles. * feat(motion): strengthen contract comment, migrate pds-modal backdrop Address review feedback on the motion-tokens PR: - Strengthen the namespace contract comment in _motion.scss to make clear Pine is claiming the upstream-canonical names and that the file is to be removed when @kajabi-ui/styles publishes motion. - Migrate pds-modal __backdrop transition from raw 0.2s ease to var(--pine-motion-duration-base) var(--pine-motion-easing-out) so the new tokens have at least one production consumer. - Document the reduced-motion limitation (override applies to duration only; JS-driven animations need their own matchMedia check). * style(pds-modal): use ease-in on backdrop exit, ease-out on enter --- .../src/components/pds-modal/pds-modal.scss | 3 +- libs/core/src/global/styles/_motion.scss | 39 +++++++++++++++++++ libs/core/src/global/styles/app.scss | 1 + libs/core/src/stories/guides/motion.docs.mdx | 35 +++++++++++++++-- 4 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 libs/core/src/global/styles/_motion.scss diff --git a/libs/core/src/components/pds-modal/pds-modal.scss b/libs/core/src/components/pds-modal/pds-modal.scss index 0908909b5..d87ebb091 100644 --- a/libs/core/src/components/pds-modal/pds-modal.scss +++ b/libs/core/src/components/pds-modal/pds-modal.scss @@ -13,7 +13,7 @@ opacity: 0; padding: 0; position: fixed; - transition: opacity 0.2s ease, visibility 0.2s ease; + transition: opacity var(--pine-motion-duration-base) var(--pine-motion-easing-in), visibility var(--pine-motion-duration-base) var(--pine-motion-easing-in); visibility: hidden; width: 100%; z-index: var(--pine-z-index-modal); @@ -33,6 +33,7 @@ &.open { opacity: 1; + transition-timing-function: var(--pine-motion-easing-out); visibility: visible; } } diff --git a/libs/core/src/global/styles/_motion.scss b/libs/core/src/global/styles/_motion.scss new file mode 100644 index 000000000..8a5d62302 --- /dev/null +++ b/libs/core/src/global/styles/_motion.scss @@ -0,0 +1,39 @@ +//// +/// Motion tokens (Pine-internal until upstream consolidation) +/// +/// Named duration and easing values for component transitions. +/// +/// Namespace contract: these CSS custom properties use the canonical +/// `--pine-motion-*` namespace that `@kajabi-ui/styles` will own once +/// motion is published upstream. Pine defines them here so component +/// SCSS can adopt the names today; when upstream lands a motion token +/// release, this file is removed in the same PR that bumps the +/// `@kajabi-ui/styles` version. Component SCSS does not change. +/// +/// If upstream ever publishes different values under the same names, +/// remove these fallbacks immediately to avoid divergent definitions. +/// +/// @group pine +//// + +:root { + // Durations + --pine-motion-duration-fast: 120ms; + --pine-motion-duration-base: 200ms; + --pine-motion-duration-slow: 300ms; + + // Easing curves (match the bands documented in Guides/Motion) + --pine-motion-easing-out: cubic-bezier(0, 0, 0.2, 1); + --pine-motion-easing-in: cubic-bezier(0.4, 0, 1, 1); + --pine-motion-easing-in-out: cubic-bezier(0.4, 0, 0.2, 1); +} + +// Honor user preference for reduced motion. Components that opt-in to +// the tokens above inherit the override automatically. +@media (prefers-reduced-motion: reduce) { + :root { + --pine-motion-duration-fast: 0ms; + --pine-motion-duration-base: 0ms; + --pine-motion-duration-slow: 0ms; + } +} diff --git a/libs/core/src/global/styles/app.scss b/libs/core/src/global/styles/app.scss index ea2e870a8..7b3e4b2ff 100644 --- a/libs/core/src/global/styles/app.scss +++ b/libs/core/src/global/styles/app.scss @@ -1,5 +1,6 @@ @use '~@kajabi-ui/styles/dist/pine/pine'; @use 'fonts'; +@use 'motion'; @use '../../components/pds-combobox/pds-combobox-dropdown-panel.scss' as *; // Body-portaled combobox listbox (outside shadow roots; see `dropdown-mount="body"` on pds-combobox) diff --git a/libs/core/src/stories/guides/motion.docs.mdx b/libs/core/src/stories/guides/motion.docs.mdx index eac412a0b..28c861091 100644 --- a/libs/core/src/stories/guides/motion.docs.mdx +++ b/libs/core/src/stories/guides/motion.docs.mdx @@ -4,7 +4,7 @@ import { Meta } from '@storybook/addon-docs/blocks'; # Motion -Motion should reinforce hierarchy and feedback without slowing tasks or causing vestibular discomfort. Pine does not yet expose a dedicated **motion token** scale in documentation; until those tokens land in the shared design language, follow the practices below. +Motion should reinforce hierarchy and feedback without slowing tasks or causing vestibular discomfort. Pine exposes a small **Pine-internal** motion token set covering durations and easing curves; full publication into the shared design language is a follow-up (see Tokens below). ## Principles @@ -20,6 +20,35 @@ Motion should reinforce hierarchy and feedback without slowing tasks or causing - Keep **focus management** paired with motion: if a dialog animates open, focus should move predictably when the animation completes (or immediately when reduced motion applies). - Coordinate with **design** on shared curves and durations so Stencil, React host apps, and marketing pages do not fight each other. -## Tokens and docs +## Tokens -When semantic motion tokens are published in the Kajabi design token set, this page should link to their Storybook entries alongside color, spacing, and typography. Until then, treat motion as **component-local** (scoped SCSS) plus the principles above. +Pine ships a Pine-internal motion token set in `libs/core/src/global/styles/_motion.scss`. Component SCSS should reference the CSS custom properties (not the raw values), so that consolidating these tokens upstream into `@kajabi-ui/styles` is a future token-swap and not a search-and-replace across every component. + +### Durations + +| Token | Default | Use for | +| --- | --- | --- | +| `--pine-motion-duration-fast` | `120ms` | Micro-interactions (hover, focus, small toggles). | +| `--pine-motion-duration-base` | `200ms` | Default for most state changes; the safe pick. | +| `--pine-motion-duration-slow` | `300ms` | Overlays entering or leaving (modal, popover, drawer). | + +### Easing + +| Token | Curve | Use for | +| --- | --- | --- | +| `--pine-motion-easing-out` | `cubic-bezier(0, 0, 0.2, 1)` | Elements **entering** the view or gaining focus. | +| `--pine-motion-easing-in` | `cubic-bezier(0.4, 0, 1, 1)` | Elements **exiting** the view. | +| `--pine-motion-easing-in-out` | `cubic-bezier(0.4, 0, 0.2, 1)` | Properties that move both ways (for example expanding height). | + +### Reduced motion + +The duration tokens collapse to `0ms` under `prefers-reduced-motion: reduce`, so CSS transitions that reference `var(--pine-motion-duration-*)` inherit the system-level override automatically — no extra media-query wrapping is required at the component level. + +**Limitations:** + +- The override only affects duration. Easing tokens still resolve to their normal curves, which is fine for 0ms transitions but worth knowing if you compose tokens elsewhere. +- JavaScript-driven animations (`element.animate`, `requestAnimationFrame` loops) do not read CSS custom properties; you have to check `window.matchMedia('(prefers-reduced-motion: reduce)')` yourself. + +### Roadmap + +When motion tokens are published in `@kajabi-ui/styles` alongside color, spacing, and typography, the CSS variable names above stay the same — the source of truth moves upstream. Track that work in `#ds-support`. From e7e678758a6cfeabb094f1502efa50cf6de95d27 Mon Sep 17 00:00:00 2001 From: Quinton Jason Date: Mon, 1 Jun 2026 08:25:37 -0500 Subject: [PATCH 08/11] fix(pds-table): flip fixed-column shadow offset in RTL --- .../components/pds-table/pds-table-cell/pds-table-cell.scss | 6 ++++++ .../pds-table/pds-table-head-cell/pds-table-head-cell.scss | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/libs/core/src/components/pds-table/pds-table-cell/pds-table-cell.scss b/libs/core/src/components/pds-table/pds-table-cell/pds-table-cell.scss index 8cacb0732..6f0f7447a 100644 --- a/libs/core/src/components/pds-table/pds-table-cell/pds-table-cell.scss +++ b/libs/core/src/components/pds-table/pds-table-cell/pds-table-cell.scss @@ -25,6 +25,12 @@ z-index: var(--pine-z-index-raised); } +// Flip shadow toward scrollable content when fixed column is on the inline-end edge. +:host-context([dir='rtl']) { + /* stylelint-disable-next-line pine-design-system/no-hardcoded-colors */ + --box-shadow-fixed: -3px 3px 6px -2px rgba(0, 0, 0, 0.2); +} + // box shadow when table has scrolled and cell is fixed :host(.has-scrolled.is-fixed) { box-shadow: var(--box-shadow-fixed); diff --git a/libs/core/src/components/pds-table/pds-table-head-cell/pds-table-head-cell.scss b/libs/core/src/components/pds-table/pds-table-head-cell/pds-table-head-cell.scss index d5c44be88..10e849f57 100644 --- a/libs/core/src/components/pds-table/pds-table-head-cell/pds-table-head-cell.scss +++ b/libs/core/src/components/pds-table/pds-table-head-cell/pds-table-head-cell.scss @@ -39,6 +39,12 @@ border-block-start: var(--border-head-cell-default); } +// Flip shadow toward scrollable content when fixed column is on the inline-end edge. +:host-context([dir='rtl']) { + /* stylelint-disable-next-line pine-design-system/no-hardcoded-colors */ + --box-shadow-default: -3px 3px 6px -2px rgba(0, 0, 0, 0.1); +} + // box shadow when table has scrolled and cell is fixed :host(.has-scrolled.is-fixed) { box-shadow: var(--box-shadow-default); From 77c1598b0968d9db1311be47f57f52bab40af436 Mon Sep 17 00:00:00 2001 From: Quinton Jason Date: Mon, 1 Jun 2026 14:59:59 -0500 Subject: [PATCH 09/11] fix: address RTL review feedback for form controls and tooltip Correct checkbox checkmark mirroring, switch knob positioning, filter popover anchor placement, input addon select radii, select disabled icon dimming, and tooltip html-content arrow alignment under dir=rtl. --- .../components/pds-checkbox/pds-checkbox.scss | 18 +++++++++++------ .../pds-filters/pds-filter/pds-filter.scss | 20 ++++++++++--------- .../pds-filters/pds-filter/pds-filter.tsx | 19 ++++++++++-------- .../src/components/pds-input/pds-input.scss | 4 ++++ .../src/components/pds-select/pds-select.scss | 14 +++++++++---- .../src/components/pds-switch/pds-switch.scss | 8 ++++---- .../components/pds-tooltip/pds-tooltip.scss | 3 ++- 7 files changed, 54 insertions(+), 32 deletions(-) diff --git a/libs/core/src/components/pds-checkbox/pds-checkbox.scss b/libs/core/src/components/pds-checkbox/pds-checkbox.scss index ba9766585..64a6703a0 100644 --- a/libs/core/src/components/pds-checkbox/pds-checkbox.scss +++ b/libs/core/src/components/pds-checkbox/pds-checkbox.scss @@ -47,10 +47,10 @@ content: ""; display: block; height: 7px; - left: 50%; + inset-block-start: 50%; + inset-inline-start: 50%; position: absolute; - top: 50%; - transform: rotate(90deg) translate3d(-103%, 32%, 0); + transform: translate(-50%, -50%) rotate(90deg); width: 4px; } } @@ -90,10 +90,10 @@ input { content: ""; display: block; height: 7px; - left: 50%; + inset-block-start: 50%; + inset-inline-start: 50%; position: absolute; - top: 50%; - transform: rotate(43deg) translate3d(-110%, -30%, 0); + transform: translate(-50%, -60%) rotate(43deg); width: 4px; } } @@ -126,6 +126,12 @@ input { } } +:host-context([dir='rtl']) { + input:checked::after { + transform: translate(-50%, -60%) rotate(-43deg); + } +} + label { display: flex; gap: var(--pine-dimension-xs); diff --git a/libs/core/src/components/pds-filters/pds-filter/pds-filter.scss b/libs/core/src/components/pds-filters/pds-filter/pds-filter.scss index c0b9cf140..405938671 100644 --- a/libs/core/src/components/pds-filters/pds-filter/pds-filter.scss +++ b/libs/core/src/components/pds-filters/pds-filter/pds-filter.scss @@ -179,7 +179,7 @@ /* Modern browsers: CSS anchor positioning with JavaScript-controlled flipping */ @supports (anchor-name: --test) { inset-block-start: calc(anchor(--filter-trigger bottom) + var(--pine-dimension-100)); - inset-inline-start: anchor(--filter-trigger left); + inset-inline-start: anchor(--filter-trigger start); position: fixed; /* Anchor positioning requires fixed positioning */ /* stylelint-disable-next-line property-no-unknown */ position-anchor: --filter-trigger; @@ -188,8 +188,8 @@ /* Flipping classes applied by JavaScript for precise control */ &.popover-flip-horizontal { - inset-inline-start: anchor(--filter-trigger right); - transform: translateX(-100%); + inset-inline-end: anchor(--filter-trigger end); + inset-inline-start: auto; } &.popover-flip-vertical { @@ -199,14 +199,15 @@ &.popover-flip-horizontal.popover-flip-vertical { inset-block-start: anchor(--filter-trigger top); - inset-inline-start: anchor(--filter-trigger right); - transform: translate(-100%, calc(-100% - var(--pine-dimension-100))); + inset-inline-end: anchor(--filter-trigger end); + inset-inline-start: auto; + transform: translateY(calc(-100% - var(--pine-dimension-100))); } /* Ensure fallback class works with flipping in modern browsers */ &.is-open.popover-flip-horizontal { - inset-inline-start: anchor(--filter-trigger right); - transform: translateX(-100%); + inset-inline-end: anchor(--filter-trigger end); + inset-inline-start: auto; } &.is-open.popover-flip-vertical { @@ -216,8 +217,9 @@ &.is-open.popover-flip-horizontal.popover-flip-vertical { inset-block-start: anchor(--filter-trigger top); - inset-inline-start: anchor(--filter-trigger right); - transform: translate(-100%, calc(-100% - var(--pine-dimension-100))); + inset-inline-end: anchor(--filter-trigger end); + inset-inline-start: auto; + transform: translateY(calc(-100% - var(--pine-dimension-100))); } } diff --git a/libs/core/src/components/pds-filters/pds-filter/pds-filter.tsx b/libs/core/src/components/pds-filters/pds-filter/pds-filter.tsx index 67976ce29..73c7c47bb 100644 --- a/libs/core/src/components/pds-filters/pds-filter/pds-filter.tsx +++ b/libs/core/src/components/pds-filters/pds-filter/pds-filter.tsx @@ -254,16 +254,19 @@ export class PdsFilter implements BasePdsProps { const popoverWidth = 228; const popoverHeight = this.popoverEl.getBoundingClientRect().height || 200; - // Boundary detection for flipping + // Boundary detection for flipping (inline-end overflow respects document direction) const bufferSpace = 20; - const wouldOverflowRight = (triggerRect.left + popoverWidth + bufferSpace) > viewportWidth; + const isRtl = document.documentElement.dir === 'rtl'; + const wouldOverflowInlineEnd = isRtl + ? (triggerRect.left - popoverWidth - bufferSpace) < 0 + : (triggerRect.left + popoverWidth + bufferSpace) > viewportWidth; const wouldOverflowBottom = (triggerRect.bottom + 8 + popoverHeight + bufferSpace) > viewportHeight; if (supportsAnchorPositioning) { // Modern browsers: CSS anchor positioning + JavaScript-controlled flipping this.popoverEl.classList.remove('popover-flip-horizontal', 'popover-flip-vertical'); - if (wouldOverflowRight) { + if (wouldOverflowInlineEnd) { this.popoverEl.classList.add('popover-flip-horizontal'); } @@ -273,15 +276,15 @@ export class PdsFilter implements BasePdsProps { } else { // Fallback browsers: JavaScript positioning with boundary detection - let left = triggerRect.left; + let left = isRtl ? triggerRect.right - popoverWidth : triggerRect.left; let top = triggerRect.bottom + 8; - let transformOrigin = 'top left'; + let transformOrigin = isRtl ? 'top right' : 'top left'; // Apply horizontal flipping if needed - if (wouldOverflowRight) { + if (wouldOverflowInlineEnd) { const actualPopoverWidth = this.popoverEl.getBoundingClientRect().width || popoverWidth; - left = triggerRect.right - actualPopoverWidth; - transformOrigin = 'top right'; + left = isRtl ? triggerRect.left : triggerRect.right - actualPopoverWidth; + transformOrigin = isRtl ? 'top left' : 'top right'; } // Apply vertical flipping if needed diff --git a/libs/core/src/components/pds-input/pds-input.scss b/libs/core/src/components/pds-input/pds-input.scss index f6c109f38..d2d8d4458 100644 --- a/libs/core/src/components/pds-input/pds-input.scss +++ b/libs/core/src/components/pds-input/pds-input.scss @@ -114,7 +114,9 @@ --pds-select-background: var(--pds-input-addon-background); --pds-select-border: var(--pine-border-width-none); --pds-select-border-radius-end-end: var(--pine-dimension-none); + --pds-select-border-radius-end-start: var(--pds-input-border-radius); --pds-select-border-radius-start-end: var(--pine-dimension-none); + --pds-select-border-radius-start-start: var(--pds-input-border-radius); --pds-select-min-height: calc(var(--pine-dimension-450) - calc(2 * var(--pine-border-width))); } @@ -122,7 +124,9 @@ :host([has-append]) ::slotted(pds-select[slot="append"]) { --pds-select-background: var(--pds-input-addon-background); --pds-select-border: var(--pine-border-width-none); + --pds-select-border-radius-end-end: var(--pds-input-border-radius); --pds-select-border-radius-end-start: var(--pine-dimension-none); + --pds-select-border-radius-start-end: var(--pds-input-border-radius); --pds-select-border-radius-start-start: var(--pine-dimension-none); --pds-select-min-height: calc(var(--pine-dimension-450) - calc(2 * var(--pine-border-width))); } diff --git a/libs/core/src/components/pds-select/pds-select.scss b/libs/core/src/components/pds-select/pds-select.scss index d682d3136..e4f475d82 100644 --- a/libs/core/src/components/pds-select/pds-select.scss +++ b/libs/core/src/components/pds-select/pds-select.scss @@ -58,11 +58,11 @@ select { appearance: none; background-color: var(--pds-select-background); border: var(--pds-select-border); + border-end-end-radius: var(--pds-select-border-radius-end-end, var(--pds-select-border-radius)); + border-end-start-radius: var(--pds-select-border-radius-end-start, var(--pds-select-border-radius)); border-radius: var(--pds-select-border-radius); - border-bottom-left-radius: var(--pds-select-border-radius-end-start, var(--pds-select-border-radius)); - border-bottom-right-radius: var(--pds-select-border-radius-end-end, var(--pds-select-border-radius)); - border-top-left-radius: var(--pds-select-border-radius-start-start, var(--pds-select-border-radius)); - border-top-right-radius: var(--pds-select-border-radius-start-end, var(--pds-select-border-radius)); + border-start-end-radius: var(--pds-select-border-radius-start-end, var(--pds-select-border-radius)); + border-start-start-radius: var(--pds-select-border-radius-start-start, var(--pds-select-border-radius)); color: var(--pine-color-text); font: var(--pine-typography-body); grid-area: field; @@ -104,6 +104,12 @@ select { } } +:host(.is-disabled) { + .pds-select__select-icon { + color: var(--pine-color-text-disabled); + } +} + :host(.is-invalid) { select { background-color: var(--pine-select-color-background-danger); diff --git a/libs/core/src/components/pds-switch/pds-switch.scss b/libs/core/src/components/pds-switch/pds-switch.scss index 2f4817f6d..22d13436d 100644 --- a/libs/core/src/components/pds-switch/pds-switch.scss +++ b/libs/core/src/components/pds-switch/pds-switch.scss @@ -56,10 +56,10 @@ input { display: block; height: var(--sizing-input-toggle-size); inset-block-start: 50%; - inset-inline-start: 50%; + inset-inline-start: var(--pine-dimension-025); position: absolute; - transform: translate3d(-100%, -50%, 0); - transition: transform var(--number-transition-timing); + transform: translateY(-50%); + transition: inset-inline-start var(--number-transition-timing); width: var(--sizing-input-toggle-size); } } @@ -141,7 +141,7 @@ input:focus-visible:not(:disabled) { // 'Checked' state // switch toggle input:checked::after { - transform: translate3d(0, -50%, 0); + inset-inline-start: calc(100% - var(--sizing-input-toggle-size) - var(--pine-dimension-025)); } // switch container diff --git a/libs/core/src/components/pds-tooltip/pds-tooltip.scss b/libs/core/src/components/pds-tooltip/pds-tooltip.scss index b1a1b3fc3..e2f6f3922 100644 --- a/libs/core/src/components/pds-tooltip/pds-tooltip.scss +++ b/libs/core/src/components/pds-tooltip/pds-tooltip.scss @@ -17,6 +17,7 @@ max-width: var(--tooltip-dimension-max-width); opacity: 0; padding: var(--pine-dimension-xs) calc(var(--pine-dimension-md) / 2); + position: relative; visibility: hidden; width: max-content; @@ -28,7 +29,7 @@ } .pds-tooltip.pds-tooltip--has-html-content & { - width: auto; + width: fit-content; } &::after { From af9470dc4df6f4d78136e19f71f07af993ea6f74 Mon Sep 17 00:00:00 2001 From: Quinton Jason Date: Mon, 1 Jun 2026 15:37:56 -0500 Subject: [PATCH 10/11] fix: resolve select radius and filter RTL direction bot feedback Drop the border-radius shorthand that reset per-corner overrides on pds-select, and detect RTL via computed direction on the filter host instead of documentElement.dir only. --- .../src/components/pds-filters/pds-filter/pds-filter.tsx | 3 ++- libs/core/src/components/pds-select/pds-select.scss | 1 - libs/core/src/utils/scroll.ts | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/libs/core/src/components/pds-filters/pds-filter/pds-filter.tsx b/libs/core/src/components/pds-filters/pds-filter/pds-filter.tsx index 73c7c47bb..3579e3068 100644 --- a/libs/core/src/components/pds-filters/pds-filter/pds-filter.tsx +++ b/libs/core/src/components/pds-filters/pds-filter/pds-filter.tsx @@ -1,5 +1,6 @@ import { Component, Element, Event, EventEmitter, Host, h, Prop, State, Method, Listen } from '@stencil/core'; import type { BasePdsProps } from '@utils/interfaces'; +import { isRtlDirection } from '@utils/scroll'; import type { PdsFilterOpenEventDetail, PdsFilterCloseEventDetail, PdsFilterClearEventDetail, PdsFilterVariant } from './filter-interface'; import { enlarge, trash } from '@pine-ds/icons/icons'; @@ -256,7 +257,7 @@ export class PdsFilter implements BasePdsProps { // Boundary detection for flipping (inline-end overflow respects document direction) const bufferSpace = 20; - const isRtl = document.documentElement.dir === 'rtl'; + const isRtl = isRtlDirection(this.el); const wouldOverflowInlineEnd = isRtl ? (triggerRect.left - popoverWidth - bufferSpace) < 0 : (triggerRect.left + popoverWidth + bufferSpace) > viewportWidth; diff --git a/libs/core/src/components/pds-select/pds-select.scss b/libs/core/src/components/pds-select/pds-select.scss index e4f475d82..cc7249a5a 100644 --- a/libs/core/src/components/pds-select/pds-select.scss +++ b/libs/core/src/components/pds-select/pds-select.scss @@ -60,7 +60,6 @@ select { border: var(--pds-select-border); border-end-end-radius: var(--pds-select-border-radius-end-end, var(--pds-select-border-radius)); border-end-start-radius: var(--pds-select-border-radius-end-start, var(--pds-select-border-radius)); - border-radius: var(--pds-select-border-radius); border-start-end-radius: var(--pds-select-border-radius-start-end, var(--pds-select-border-radius)); border-start-start-radius: var(--pds-select-border-radius-start-start, var(--pds-select-border-radius)); color: var(--pine-color-text); diff --git a/libs/core/src/utils/scroll.ts b/libs/core/src/utils/scroll.ts index 817308afb..087291ccd 100644 --- a/libs/core/src/utils/scroll.ts +++ b/libs/core/src/utils/scroll.ts @@ -47,6 +47,11 @@ export function resetRtlScrollTypeCache(): void { cachedRtlScrollType = null; } +/** True when the element's used text direction is RTL (inherits from ancestors). */ +export function isRtlDirection(element: Element): boolean { + return getComputedStyle(element).direction === 'rtl'; +} + /** * RTL scroll offset from inline-start for a known browser scrollLeft model. * @see https://github.com/othree/jquery.rtl-scroll-type#3-types-of-scrollleft-scrollwidth--100 @@ -78,7 +83,7 @@ export function getScrollOffsetFromStart(container: HTMLElement): number { return 0; } - const isRtl = getComputedStyle(container).direction === 'rtl'; + const isRtl = isRtlDirection(container); if (!isRtl) { return scrollLeft; } From 716f220f7ed769fca6b4256909834ad644200a01 Mon Sep 17 00:00:00 2001 From: Quinton Jason Date: Tue, 2 Jun 2026 12:54:07 -0500 Subject: [PATCH 11/11] fix(pds-filter): use trigger right edge for RTL overflow check Popover anchors at inline-start (trigger right in RTL), so inline-end overflow must measure from triggerRect.right not left. --- libs/core/src/components/pds-filters/pds-filter/pds-filter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/core/src/components/pds-filters/pds-filter/pds-filter.tsx b/libs/core/src/components/pds-filters/pds-filter/pds-filter.tsx index 3579e3068..214115490 100644 --- a/libs/core/src/components/pds-filters/pds-filter/pds-filter.tsx +++ b/libs/core/src/components/pds-filters/pds-filter/pds-filter.tsx @@ -259,7 +259,7 @@ export class PdsFilter implements BasePdsProps { const bufferSpace = 20; const isRtl = isRtlDirection(this.el); const wouldOverflowInlineEnd = isRtl - ? (triggerRect.left - popoverWidth - bufferSpace) < 0 + ? (triggerRect.right - popoverWidth - bufferSpace) < 0 : (triggerRect.left + popoverWidth + bufferSpace) > viewportWidth; const wouldOverflowBottom = (triggerRect.bottom + 8 + popoverHeight + bufferSpace) > viewportHeight;