diff --git a/.storybook/main.ts b/.storybook/main.ts index 9d92ed2e2e..8f3975ea78 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -66,6 +66,11 @@ const config: StorybookConfig = { { from: path.join(repoRoot, 'node_modules/@floating-ui/core/dist'), to: '/vendor/floating-ui-core' }, { from: path.join(repoRoot, 'node_modules/@floating-ui/dom/dist'), to: '/vendor/floating-ui-dom' }, { from: path.join(repoRoot, 'node_modules/virtual-select-plugin/dist'), to: '/vendor/virtual-select' }, + // PhotoSwipe 4.1.0 — the version the OutSystems platform ships for the Lightbox + // Image block. Not bundled by this library (the block is low-code only); OUI only + // restyles the overlay chrome. Serve the whole `dist` so `default-skin.css` can + // resolve its relative sprite URLs (default-skin.png/.svg, preloader.gif). + { from: path.join(repoRoot, 'node_modules/photoswipe/dist'), to: '/vendor/photoswipe' }, ], }; diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index fb4a455a29..93fc5e0a58 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -45,6 +45,15 @@ + + + + + diff --git a/package.json b/package.json index e58f2b6bb5..fb4fb0161d 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,8 @@ "gulp-typescript": "^6.0.0-alpha.1", "json-fixer": "^1.6.15", "nouislider": "^15.8.1", - "outsystems-design-tokens": "^1.4.0", + "outsystems-design-tokens": "^1.4.1", + "photoswipe": "^4.1.0", "postcss": "^8.4.38", "postcss-discard-comments": "^5.1.2", "postcss-discard-duplicates": "^5.1.0", diff --git a/src/scss/01-foundations/_icon-library-odc.scss b/src/scss/01-foundations/_icon-library-odc.scss index e0720e72f9..e9c303beb7 100644 --- a/src/scss/01-foundations/_icon-library-odc.scss +++ b/src/scss/01-foundations/_icon-library-odc.scss @@ -51,6 +51,7 @@ // Lightbox --osui-icon-arrow-next: '\f061'; --osui-icon-arrow-previous: '\f060'; + --osui-icon-zoom-in: '\f00e'; // fa-search-plus // Password Policy --osui-icon-circle-x: '\f057'; @@ -103,6 +104,7 @@ body.iconLibrary-phosphor { // Lightbox --osui-icon-arrow-next: '\e06c'; --osui-icon-arrow-previous: '\e058'; + --osui-icon-zoom-in: '\e310'; // ph-magnifying-glass-plus // Password Policy --osui-icon-circle-x: '\e4f8'; @@ -521,36 +523,69 @@ body.iconLibrary-phosphor { } // Lightbox -.pswp__button, -.pswp__button--arrow--left:before, -.pswp__button--arrow--right:before { +// Drop the vendor sprite so the glyphs below show through. On the button itself the +// `background` shorthand is fine, but NOT on the arrow pseudo-elements: default-skin.css +// paints both the sprite AND the arrows' translucent square in that one shorthand, so +// resetting it wholesale silently removes the square too (see the arrow rule below). +.pswp__button { background: none !important; } -.pswp__button.pswp__button--close:before { - color: variables.$token-primitives-base-white; - content: var(--osui-icon-clear); +// Top-bar buttons share one glyph box so they stay aligned with each other; only +// the `content` differs. `.pswp__button` is `position: relative` in photoswipe.css, +// which is what these absolute glyphs are positioned against. +.pswp__button.pswp__button--close:before, +.pswp__button.pswp__button--zoom:before { + color: variables.$token-icon-inverse; font-family: var(--osui-icon-font-family); - font-size: variables.$token-font-size-500; + font-size: variables.$token-font-size-700; position: absolute; - right: 25%; top: 50%; transform: translateY(-50%) translateX(-50%); } +.pswp__button.pswp__button--close:before { + content: var(--osui-icon-clear); +} + +.pswp__button.pswp__button--zoom:before { + content: var(--osui-icon-zoom-in); +} + +// The arrows' translucent square lives on the pseudo-element; the button itself is +// only a 70x100 hit area. Only the sprite is dropped here — `background-image`, not +// the shorthand, which would take the square with it. `!important` is required +// because `.pswp--svg …:before` swaps in the SVG sprite at higher specificity. +// +// Geometry is 32x32 inset 4px, replacing the vendor's 30x32 inset 6px. 32px is the +// design's icon container (an 18x18 glyph with 7px padding all round); the 4px inset is +// measured off the design frame and snapped to the ramp. Centred with +// `top: 50%`/`translateY` rather than the vendor's hardcoded `top: 35px`, so the +// square stays centred regardless of the button's height. .pswp__button--arrow--right:before, .pswp__button--arrow--left:before { - color: variables.$token-primitives-base-white; + align-items: center; + background-color: variables.$token-backdrop; + background-image: none !important; + color: variables.$token-icon-inverse; + display: flex; font-family: var(--osui-icon-font-family); - font-size: variables.$token-font-size-500; + font-size: variables.$token-font-size-450; + height: variables.$token-scale-800; + justify-content: center; + top: 50%; + transform: translateY(-50%); + width: variables.$token-scale-800; } .pswp__button--arrow--right:before { content: var(--osui-icon-arrow-next); + right: variables.$token-scale-100; } .pswp__button--arrow--left:before { content: var(--osui-icon-arrow-previous); + left: variables.$token-scale-100; } // Password Policy diff --git a/src/scss/04-patterns/03-interaction/_floating-actions.scss b/src/scss/04-patterns/03-interaction/_floating-actions.scss index 6862dd6c17..7dd6d1a557 100644 --- a/src/scss/04-patterns/03-interaction/_floating-actions.scss +++ b/src/scss/04-patterns/03-interaction/_floating-actions.scss @@ -15,12 +15,12 @@ &-actions-wrapper { // ─── Component CSS API ───────────────────────────────────────────── --osui-floating-actions-button-background: var(--color-primary); - --osui-floating-actions-button-background-hover: #{variables.$token-semantics-primary-800}; - --osui-floating-actions-button-background-press: #{variables.$token-bg-primary-base-press}; + --osui-floating-actions-button-background-hover: var(--color-primary-hover); +--osui-floating-actions-button-background-press: var(--color-primary-selected); --osui-floating-actions-button-color: var(--color-text-inverse); --osui-floating-actions-button-shadow: #{variables.$token-elevation-3}; --osui-floating-actions-item-background: var(--color-background-surface); - --osui-floating-actions-item-background-hover: #{variables.$token-primitives-neutral-100}; + --osui-floating-actions-item-background-hover: #{variables.$token-bg-neutral-subtlest-hover}; --osui-floating-actions-item-background-press: #{variables.$token-bg-neutral-subtlest-press}; --osui-floating-actions-item-color: var(--color-text); --osui-floating-actions-item-shadow: #{variables.$token-elevation-2}; @@ -124,7 +124,7 @@ color: var(--osui-floating-actions-item-color); cursor: pointer; display: flex; - font-size: variables.$token-font-size-400; + font-size: variables.$token-font-size-500; height: variables.$token-scale-1000; justify-content: center; margin-left: variables.$token-scale-400; @@ -155,7 +155,7 @@ color: var(--osui-floating-actions-button-color); cursor: pointer; display: flex; - font-size: variables.$token-font-size-550; + font-size: variables.$token-font-size-800; height: variables.$token-scale-1400; justify-content: center; pointer-events: auto; diff --git a/src/scss/04-patterns/03-interaction/_input-with-icon.scss b/src/scss/04-patterns/03-interaction/_input-with-icon.scss index b29dd8d1f8..43ec7bb655 100644 --- a/src/scss/04-patterns/03-interaction/_input-with-icon.scss +++ b/src/scss/04-patterns/03-interaction/_input-with-icon.scss @@ -8,7 +8,7 @@ /// .input-with-icon { // ─── Component CSS API ───────────────────────────────────────────── - --osui-input-with-icon-icon-color: var(--color-text-subtle); + --osui-input-with-icon-icon-color: variables.$token-icon-default; --osui-input-with-icon-icon-hover-color: var(--color-text-subtlest); // ─────────────────────────────────────────────────────────────────── @@ -32,10 +32,26 @@ } } + /* A trailing `.search-actions` slot is absolutely positioned over the input, so + reserve room for it or long values run underneath the icon. Matched with + `:has()` rather than a sibling combinator so it holds whichever side of + `.input-with-icon-input` the slot is authored on. Scoped away from + `.input-with-icon-right`, where `.search-actions` docks to the LEFT instead + and the padding pair is already set in that block. */ + &:not(.input-with-icon-right):has(.input-with-icon-content-icon.search-actions:not(:empty)) { + .input-with-icon-input input { + padding-right: variables.$token-scale-1000; + } + } + .input-with-icon { &-content-icon { + /* The slot is a fixed 40px box the glyph sits inside; without these the + glyph stretches full-height and hugs the slot's leading edge. */ + align-items: center; display: inline-flex; height: 100%; + justify-content: center; left: 0; position: absolute; width: variables.$token-scale-1000; diff --git a/src/scss/04-patterns/03-interaction/range-slider/_rangeslider.scss b/src/scss/04-patterns/03-interaction/range-slider/_rangeslider.scss index c9756627df..7cfdcc4f63 100644 --- a/src/scss/04-patterns/03-interaction/range-slider/_rangeslider.scss +++ b/src/scss/04-patterns/03-interaction/range-slider/_rangeslider.scss @@ -13,7 +13,10 @@ --osui-range-slider-handle-background: var(--color-background-surface); --osui-range-slider-handle-border-color: var(--color-primary); --osui-range-slider-handle-shadow: #{variables.$token-elevation-1}; - --osui-range-slider-disabled-track-color: #{variables.$token-bg-neutral-subtle-default}; + // Disabled: the track lightens from the enabled `--color-border` (#cecece), and the + // filled connect lightens further still so it recedes rather than standing out. + --osui-range-slider-disabled-track-color: var(--osui-range-slider-track-color); + --osui-range-slider-disabled-connect-color: #{variables.$token-bg-neutral-base-default}; --osui-range-slider-disabled-color: var(--color-text-disabled); // ─────────────────────────────────────────────────────────────────── @@ -249,20 +252,21 @@ --osui-range-slider-handle-background: var(--color-background-input-disabled); --osui-range-slider-disabled-color: var(--color-text-disabled); + // noUiSlider turns the provider element itself into `.noUi-target`, so the + // disabled track colour belongs HERE and not on a descendant — a nested + // `.noUi-target` rule matches nothing and silently leaves the track at its + // enabled colour. + background-color: var(--osui-range-slider-disabled-track-color); pointer-events: none; .noUi { - &-target { - background-color: var(--color-border); - } - &-connect { - background-color: var(--color-text-disabled); + background-color: var(--osui-range-slider-disabled-connect-color); } &-handle { - background-color: var(--osui-range-slider-handle-background); - border: variables.$token-border-size-025 solid var(--color-border-input); + --osui-range-slider-handle-border-color: #{variables.$token-border-input-default}; + box-shadow: none; &:before, @@ -276,9 +280,9 @@ } } - .noUi-tooltip { - color: var(--osui-range-slider-disabled-color); - } + // The floating value label deliberately keeps its default `--color-text` and is + // NOT dimmed: the current value stays readable while disabled. Only the scale + // (pips) recedes. Don't add a `.noUi-tooltip` colour override here. } } } @@ -293,8 +297,13 @@ border-color: var(--color-focus-outer); } + // Keep the handle's own border colour on focus — the focus affordance is the halo + // below, not a recolour. This rule comes after the `.noUi-active` / `:hover` + // primary border at equal specificity, so hardcoding a colour here silently wins + // over it and turns the pressed handle black. Routing through the CSS API var also + // means a disabled handle (which overrides it) stays grey when focused. .noUi-handle:focus { - border-color: var(--color-text); + border-color: var(--osui-range-slider-handle-border-color); box-shadow: 0 0 0 variables.$token-border-size-075 var(--color-focus-outer); } } diff --git a/stories/FloatingActions.stories.ts b/stories/FloatingActions.stories.ts index 567db3a7b4..764799bcb7 100644 --- a/stories/FloatingActions.stories.ts +++ b/stories/FloatingActions.stories.ts @@ -42,7 +42,7 @@ const ACTIONS: Array<[label: string, icon: string]> = [ function itemMarkup(label: string, icon: string, delay: number): string { return `
- ${label} + ${label}
diff --git a/stories/InputWithIcon.stories.ts b/stories/InputWithIcon.stories.ts index faa553880f..b0b8847a64 100644 --- a/stories/InputWithIcon.stories.ts +++ b/stories/InputWithIcon.stories.ts @@ -9,8 +9,16 @@ import { cls, extendedClassArgType } from './_helpers/lowcode'; * .input-with-icon root (flex, relative) * .input-with-icon-right icon docks right instead of left * .input-with-icon-content-icon absolute icon slot; when non-empty the - * sibling input gains matching padding + * ADJACENT sibling input gains matching padding + * .input-with-icon-content-icon + * .search-actions second slot, docked to the opposite edge * .input-with-icon-input > input the input itself + * + * Sizing / state come from the Input widget (src/scss/03-widgets/_inputs-and-textareas.scss): + * .form-control[data-input] 40px · font-size 350 (default) + * .form-control.input-large[data-input] 48px · font-size 400 (also the DEFAULT inside .phone/.tablet) + * .form-control.input-small[data-input] 32px · font-size 300 + * .not-valid error border; pair with span.validation-message */ interface InputWithIconArgs { @@ -53,3 +61,99 @@ export const Default: Story = {
`), }; + +/* ── Spec matrix ────────────────────────────────────────────────────────────── */ + +type Size = 'default' | 'large' | 'small'; +type State = 'default' | 'hover' | 'focus' | 'filled' | 'disabled' | 'error'; + +const SIZES: ReadonlyArray<{ key: Size; heading: string; cls: string }> = [ + { key: 'default', heading: 'Default / Desktop', cls: '' }, + { key: 'large', heading: 'Large / Phone', cls: 'input-large' }, + { key: 'small', heading: 'Small', cls: 'input-small' }, +]; + +const STATES: readonly State[] = ['default', 'hover', 'focus', 'filled', 'disabled', 'error']; + +/** One Label + Input-With-Icon (leading icon + trailing clear) + helper/error triplet. */ +function field(size: Size, state: State): string { + const sizeCls = SIZES.find((s) => s.key === size)!.cls; + const isError = state === 'error'; + const filled = state === 'focus' || state === 'filled' || state === 'disabled' || isError; + + return ` +
+ +
+
+ +
+
+ +
+
+ +
+
+ ${isError ? 'Error message' : '
Helper message
'} +
`; +} + +/** + * Every size × state combination from the Figma spec sheet, laid out as a matrix. + * + * `:hover` and `:focus` are re-declared against `[data-demo-state]` rather than + * triggered for real — a static grid can't hold six hovers and three focuses at + * once, and Chromatic snapshots can't hover at all. The demo rules deliberately + * read the SAME `--osui-input-*` CSS API vars the real `:hover` / `:focus` rules + * read, so they cannot drift from the component's actual values. + */ +export const SpecMatrix: Story = { + name: 'Spec matrix (size × state)', + parameters: { controls: { disable: true } }, + render: () => + renderStatic(` + +
+
+ ${SIZES.map((s) => `
${s.heading}
`).join('')} + ${STATES.map( + (state) => ` +
${state[0].toUpperCase()}${state.slice(1)}
+ ${SIZES.map((s) => field(s.key, state)).join('')}` + ).join('')} +
`), +}; diff --git a/stories/LightboxImage.stories.ts b/stories/LightboxImage.stories.ts index 779dab6a86..6c583f5b2d 100644 --- a/stories/LightboxImage.stories.ts +++ b/stories/LightboxImage.stories.ts @@ -1,40 +1,222 @@ import type { Meta, StoryObj } from '@storybook/html-vite'; -import { renderStatic } from './_helpers/osui'; +import { renderPattern, type Register } from './_helpers/osui'; import { cls, extendedClassArgType } from './_helpers/lowcode'; /** - * Lightbox Image — thumbnail markup only. At runtime the block opens the image - * with PhotoSwipe (`.pswp` overrides in - * src/scss/04-patterns/03-interaction/_lightbox-image.scss); the vendor lib is - * not bundled in this library nor loaded in Storybook, so the story shows the - * shipped thumbnail structure: `.lightbox-item > a > img`. + * Lightbox Image. + * + * There is no `LightboxImage` TypeScript pattern — the block is low-code only, and + * the overlay is rendered by **PhotoSwipe 4.1.0**, the version the OutSystems + * platform ships. It is a devDependency here purely so these stories can drive the + * real thing (served at /vendor/photoswipe, loaded in preview-head.html BEFORE the + * OUI theme so the OUI overrides win — that mirrors the platform's load order). + * + * What OUI owns is two slices of CSS: + * 1. thumbnail + Service Studio preview classes + * (src/scss/04-patterns/03-interaction/_lightbox-image.scss) — safe areas on + * `.pswp__top-bar`, RTL counter flip, focus ring on the thumbnail link + * 2. the overlay CHROME ICONS — `.pswp__button` sprites are stripped and replaced + * with icon-font glyphs (src/scss/01-foundations/_icon-library-odc.scss:521-552) + * + * (2) is what `Overlay chrome` exercises. */ -interface LightboxImageArgs { +/** + * Fixture images are generated SVG, not fetched. + * + * Two reasons, both learned the hard way: + * • **Size matters to the chrome.** PhotoSwipe only adds `pswp--zoom-allowed` (the + * class that reveals the zoom button, per default-skin.css) when the image is + * LARGER than its fit size. And the arrows' translucent square is only visible + * where it overlaps image content — over the black backdrop it is invisible by + * definition. So the fixture must be big and wide enough to reach the viewport's + * left/right edges, or the design's chrome cannot be evaluated at all. + * • **No network.** Remote images made Chromatic and headless runs non-deterministic. + */ +// Wider than 16:9 on purpose. PhotoSwipeUI_Default reserves `barsSize` (44px top and +// an auto bottom) out of the fit area, so a 16:9 image in a 16:9 viewport still ends up +// letterboxed left/right — which parks the arrows over the backdrop instead of the image +// and hides their square. ~2.1:1 fills the width at common viewport sizes, matching the +// design frame (black bars above/below, image edge to edge). +const W = 2560; +const H = 1200; +const COUNT = 4; + +/** A wide, deterministic photo stand-in. Mid-tone so the 25%-black arrow square reads. */ +function photo(i: number): string { + const hue = 190 + i * 34; + const svg = + `` + + `` + + `` + + `` + + `` + + `` + + `${i + 1}`; + return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`; +} + +const IMAGES = Array.from({ length: COUNT }, (_, i) => photo(i)); + +/** PhotoSwipe v4 requires this exact skeleton to be present in the DOM before init. */ +const PSWP_TEMPLATE = ` + `; + +interface PswpChrome { + counterEl: boolean; + zoomEl: boolean; + shareEl: boolean; + fullscreenEl: boolean; +} + +/** + * Construct + open a gallery on the `.pswp` element inside `root`. + * + * `history: false` is not optional — with the default PhotoSwipe writes `#&gid=…` to + * the URL, which fights Storybook's own routing and leaves the manager stuck on a + * stale story after the overlay closes. + */ +function openGallery(root: HTMLElement, index: number, chrome: PswpChrome, register: Register, captions = false): void { + const { PhotoSwipe, PhotoSwipeUI_Default } = window; + if (!PhotoSwipe || !PhotoSwipeUI_Default) { + throw new Error('PhotoSwipe globals missing — is /vendor/photoswipe loaded in preview-head.html?'); + } + + const pswpEl = root.querySelector('.pswp'); + if (!pswpEl) throw new Error('.pswp template not found in the story root'); + + // `title` is what populates `.pswp__caption__center`. The design has no caption, so + // the chrome story opts out; `Default` keeps it to exercise the caption rule OUI + // carries for the phone safe area (_lightbox-image.scss). + const items = IMAGES.map((src, i) => ({ src, w: W, h: H, ...(captions ? { title: `Image ${i + 1}` } : {}) })); + const gallery = new PhotoSwipe(pswpEl, PhotoSwipeUI_Default, items, { + index, + history: false, // see note above + bgOpacity: 1, + showHideOpacity: false, + closeOnScroll: false, + ...chrome, + }); + gallery.init(); + register(() => gallery.close()); +} + +const chromeArgTypes = { + counterEl: { + name: 'counterEl', + control: 'boolean', + description: 'PhotoSwipe UI option — show the "n / total" counter.', + }, + zoomEl: { name: 'zoomEl', control: 'boolean', description: 'PhotoSwipe UI option — show the zoom button.' }, + shareEl: { + name: 'shareEl', + control: 'boolean', + description: 'PhotoSwipe UI option — show the share button (not in the design).', + }, + fullscreenEl: { + name: 'fullscreenEl', + control: 'boolean', + description: 'PhotoSwipe UI option — show the fullscreen button (not in the design).', + }, +} as const; + +/* ── Thumbnail ──────────────────────────────────────────────────────────────── */ + +interface ThumbnailArgs extends PswpChrome { thumbnailWidth: number; extendedClass: string; } -const IMG = 'https://picsum.photos/seed/osui-lightbox/960/640'; +const meta: Meta = { title: 'Patterns/Interaction/LightboxImage' }; +export default meta; -const meta: Meta = { - title: 'Patterns/Interaction/LightboxImage', +/** + * The shipped thumbnail markup (`.lightbox-item > a > img`), wired to a real gallery + * so the whole thumbnail → overlay journey is exercised. Click any image to open. + */ +export const Default: StoryObj = { argTypes: { thumbnailWidth: { name: 'ThumbnailWidth (px)', control: { type: 'number', min: 80, max: 640 } }, extendedClass: extendedClassArgType, + ...chromeArgTypes, + }, + args: { + thumbnailWidth: 220, + extendedClass: '', + counterEl: true, + zoomEl: true, + shareEl: false, + fullscreenEl: false, }, - args: { thumbnailWidth: 240, extendedClass: '' }, + render: ({ thumbnailWidth, extendedClass, ...chrome }) => + renderPattern( + ` +
+ ${IMAGES.map( + (src, i) => ` +
+ + Lightbox thumbnail ${i + 1} + +
` + ).join('')} +
+ ${PSWP_TEMPLATE}`, + (root, register) => { + root.querySelectorAll('.lightbox-item a').forEach((link) => { + link.addEventListener('click', (ev) => { + ev.preventDefault(); + openGallery(root, Number(link.dataset.index ?? 0), chrome as PswpChrome, register, true); + }); + }); + } + ), }; -export default meta; -type Story = StoryObj; +/* ── Overlay chrome ─────────────────────────────────────────────────────────── */ -export const Default: Story = { - render: ({ thumbnailWidth, extendedClass }) => - renderStatic(` -
- - Lightbox thumbnail - -
`), +/** + * Opens straight onto image 3 of 4, so the overlay chrome the design specifies — + * counter, zoom, close, prev/next — is on screen without interaction (and therefore + * captured by Chromatic). Everything here is the real PhotoSwipe skin with the OUI + * icon overrides on top; nothing is faked. + */ +export const OverlayChrome: StoryObj = { + name: 'Overlay chrome', + argTypes: chromeArgTypes, + args: { counterEl: true, zoomEl: true, shareEl: false, fullscreenEl: false }, + render: (chrome) => renderPattern(PSWP_TEMPLATE, (root, register) => openGallery(root, 2, chrome, register)), }; diff --git a/stories/RangeSlider.stories.ts b/stories/RangeSlider.stories.ts index 840ce61fdd..57859ab658 100644 --- a/stories/RangeSlider.stories.ts +++ b/stories/RangeSlider.stories.ts @@ -71,3 +71,94 @@ export const Interval: Story = { args: { mode: 'interval' }, render: renderRangeSlider, }; + +/* ── Spec matrix ────────────────────────────────────────────────────────────── */ + +// The design frame: a 1–12 scale with end pips, floating value labels, single handle at +// 3 and interval 3–9. `TickMarksInterval: 11` puts pips at the two ends only. +const SPEC = { min: 1, max: 12, from: 3, to: 9, tickInterval: 11 }; + +const CELLS = [ + { row: 'Regular', col: 'Default', interval: false, disabled: false }, + { row: 'Regular', col: 'Disabled', interval: false, disabled: true }, + { row: 'Interval', col: 'Default', interval: true, disabled: false }, + { row: 'Interval', col: 'Disabled', interval: true, disabled: true }, +] as const; + +/** + * Every mode × state combination from the design frame, as a matrix. + * + * Each cell is a real pattern instance — four `Create`/`Initialize` pairs driving + * noUiSlider — rather than static markup, so tooltips and pips are produced by the + * provider exactly as they are at runtime. + */ +export const SpecMatrix: Story = { + name: 'Spec matrix (mode × state)', + parameters: { controls: { disable: true } }, + render: () => { + const ids = CELLS.map(() => uid('range-slider')); + const template = ` + +
+
+
Default
+
Disabled
+ ${['Regular', 'Interval'] + .map( + (row) => ` +
${row}
+ ${CELLS.filter((c) => c.row === row) + .map((c) => { + const id = ids[CELLS.indexOf(c)]; + return ` +
+
+
+
+
`; + }) + .join('')}` + ) + .join('')} +
`; + + return renderPattern(template, (_root, register) => { + const P = Patterns(); + CELLS.forEach((c, i) => { + const id = ids[i]; + P.RangeSliderAPI.Create( + id, + cfg({ + MinValue: SPEC.min, + MaxValue: SPEC.max, + Step: 1, + IsInterval: c.interval, + IsDisabled: c.disabled, + ShowFloatingLabel: true, + ShowTickMarks: true, + TickMarksInterval: SPEC.tickInterval, + StartingValueFrom: SPEC.from, + StartingValueTo: c.interval ? SPEC.to : 0, + Orientation: 'horizontal', + Size: '100%', + }), + c.interval ? 'interval' : 'single', + 'noUiSlider' + ); + P.RangeSliderAPI.Initialize(id); + register(() => P.RangeSliderAPI.Dispose?.(id)); + }); + }); + }, +}; diff --git a/stories/_helpers/osui.ts b/stories/_helpers/osui.ts index edcd28bb3c..322ebddfc1 100644 --- a/stories/_helpers/osui.ts +++ b/stories/_helpers/osui.ts @@ -23,6 +23,10 @@ declare global { Splide: any; noUiSlider: any; VirtualSelect: any; + // PhotoSwipe 4.1.0 — the version the platform ships for Lightbox Image. + // Core and default-UI are two separate globals; both are needed to construct a gallery. + PhotoSwipe: any; + PhotoSwipeUI_Default: any; } }