Skip to content

Quality of life updates#103

Open
liamegan wants to merge 18 commits into
masterfrom
feature/qol-updates
Open

Quality of life updates#103
liamegan wants to merge 18 commits into
masterfrom
feature/qol-updates

Conversation

@liamegan

@liamegan liamegan commented Jun 13, 2026

Copy link
Copy Markdown
Member
  1. Various bug fixes and small updates:
    1. Updated shine calculation to remove the possibility of a twitch when crossing the 0,0 threshold.
    2. Fixes open/close position after resize or scroll .
    3. Added custom events for play, pause, open, opened, close and closed. These allow end users to attach callbacks to these events using syntax like card.addEventListener('perspectivecard:opened', CALLBACK)
  2. Updated the library to use the more modern dialog element and associated listeners to provide a more straightforward and accessible modal experience.
  3. Accessibility overhaul for ClickablePerspectiveCard
    1. all activation (pointer, keyboard, touch, AT) now routes through a real element. If the markup doesn't already contain one the component creates it automatically, wiring aria-haspopup="dialog", aria-expanded, and returning focus to the button when the card closes.
    2. The modal receives an aria-label derived from the trigger button so screen readers announce it with context.
    3. a ✕ button is injected into the dialog on open and receives focus immediately, giving keyboard and touch users an explicit labelled dismiss control. On by default; opt out with data-close-button="false" or settings.closeButton = false. The button label is customisable via data-close-button-label.
  4. Added 2 additional custom attributes for use in more advanced implementations. This is primarily intended for use with advanced effects like foils, however, they could find use elsewhere.
    1. --perspective-card-angle - the direction the card is tilting toward, as an angle in radians
    2. --perspective-card-tilt - the magnitude of the tilt, a unitless distance (computed via Math.hypot). 0 when the card is flat/centered, larger the further the cursor/tilt is from center. The shine uses it to set the gradient opacity (len * 0.002), but you can also use it to scale an effect's intensity or opacity.
  5. Added a demo page with various demonstrations of different card implementations, including adding a foil layer and custom button demo.
  6. Added a data-duration property to control the animation duration on clickable cards at the component level.
  7. Updated documentation, versioned the package in preparation for publish (new major ver), and tidied some old code up.
  8. Added code to copy the element's appropriate dimensions on open to the dialog version of the card to ensure dimensions match on scaling (during transition) and adding a resize observer to ensure they remain consistent.

It would be very good to test this locally with TCG before merging and publishing. I think next steps here might be turning this into a monorepo and publishing a react version alongside.

To test this locally:

  1. Clone the wtc-perspective-card repo:
    git clone git@github.com:wethegit/wtc-perspective-card.git
  2. Build and link the library
    cd wtc-perspective-card
    git co feature/qol-updates
    n auto
    npm run build
    npm link
    
  3. Link the library within the TCG repo
    cd ~/projects/tpci/tcg
    n auto
    npm i
    npm link wtc-perspective-card
    

And then run the dev site, your local dev should now be using the local wtc-perspective-card.

liamegan added 10 commits April 24, 2026 08:47
- prevent ambient shine twitch
- replace modal matte with dialog element
- fix open/close position after resize or scroll
Replace the built-in SVG foil overlay (data-foil) with CSS custom properties (--perspective-card-angle, --perspective-card-tilt) so consumers can drive their own per-frame effects from pure CSS.

Route all ClickablePerspectiveCard activation through a real <button> element (auto-created if absent), removing the pointer/touch juggling in favour of a single click listener. Adds aria-haspopup, aria-expanded, keyboard open/close, and focus-return on close.

Update README, USAGE, and demo with an Accessibility section, CSS custom property docs, and a holographic foil recipe.
…ult. Supports custom aria copy by provision of data-close-button-label (defaulting to "Close"
…logue and abstracting a modal cleanup function to be used both in this case and in the regular close case
…ting the usage documentation to remove reference to decoration (old and redundant)
liamegan added 3 commits June 14, 2026 10:18
…f the card changes between collapsed and animating state (and vice versa) due to the collapsed state card being fluid. Added dimension copy and resize observer to address.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should probably just delete these.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

So no barrel?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah, or if we want to keep it let's be clear about it, what do you think?

  1. rename to index.js
  2. re-export directly:
  export { PerspectiveCard } from './perspective-card.js'
  export { ClickablePerspectiveCard } from './clickable-perspective-card.js'

Comment thread src/wtc-perspective-card.scss
Comment thread demo/demo.scss
Comment thread src/wtc-perspective-card.scss Outdated
Comment thread src/wtc-perspective-card.scss Outdated
Comment thread src/wtc-perspective-card.scss Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR modernizes wtc-perspective-card with a Vite-based build, splits the core classes into dedicated modules, and reworks the clickable/modal experience to use <dialog> with improved accessibility and new lifecycle events.

Changes:

  • Replace the legacy Webpack/Babel build with a Vite library build and add a demo app build.
  • Refactor PerspectiveCard / ClickablePerspectiveCard into separate source modules, add CSS custom properties for tilt/angle, and dispatch custom events (perspectivecard:*).
  • Redesign the clickable/modal behavior around the native <dialog> element, with an accessible trigger <button>, focus management, and optional injected close button.

Reviewed changes

Copilot reviewed 15 out of 22 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
webpack.config.js Removes the old Webpack build configuration.
vite.config.js Adds Vite config for library build (ES/UMD) and demo build.
package.json Updates scripts/deps for Vite build; bumps major version to 3.0.0.
package-lock.json Lockfile updates for new toolchain (also needs version sync; see comments).
src/wtc-perspective-card.js Becomes a small entrypoint that re-exports the two classes.
src/perspective-card.js Extracted base card logic; adds CSS custom properties + event dispatching.
src/clickable-perspective-card.js New <dialog>-based modal implementation + accessibility improvements + events.
src/wtc-perspective-card.scss Adds button/dialog/close-button styling and dialog backdrop animations.
USAGE.md Expanded docs for attributes, accessibility, CSS custom properties, and events.
README.md Updates generated/combined docs, but currently contains stale _setupFoil API references.
demo/index.html Adds a demo page showcasing hover/ambient/clickable/custom-button/foil examples.
demo/main.js Demo entrypoint wiring up the card controllers.
demo/demo.scss Demo styling and example foil/badge CSS.
demo/assets/00-simplest/index.html Adds an additional demo asset set (appears unused by the demo page).
demo/assets/00-simplest/bootloader.js Bootloader utility for the additional demo asset set.
.npmignore Excludes demo/ from the published package.
.gitignore Ignores demo/dist output.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/clickable-perspective-card.js
Comment thread src/perspective-card.js Outdated
Comment thread USAGE.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread demo/assets/00-simplest/index.html Outdated
Comment thread src/clickable-perspective-card.js
@liamegan liamegan requested a review from marlonmarcello June 17, 2026 23:18
Comment thread src/clickable-perspective-card.js
Comment on lines +271 to +284
onDialogCancel(e) {
// Prevent the native immediate-close so we can run the close animation instead.
// Note: browsers fire a second, non-cancelable cancel event on repeated Escape
// presses as a safety valve - preventDefault() will silently fail for those.
// onDialogClose handles that case.
e.preventDefault()
if (this.tweening) {
// Can't start the close tween yet - record the intent and honour it
// once the current tween's onEndTween has fully committed its state.
this._pendingClose = true
} else {
this.enlarged = false
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the "immediate close" issue can be handled with some modern CSS. allow-discrete in the CSS should do it. I've also added @starting-style to this snippet so it animates “on open”.

dialog::backdrop {
  opacity: 0;
  transition: display allow-discrete, overlay allow-discrete, opacity;
  transition-duration: 0.4s;
}

dialog:open::backdrop {
  opacity: 1;
}

@starting-style {
  dialog:open::backdrop {
    opacity: 0
  }
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

allow-discrete doesn't interact with javascript animations though, does it? The issue here is the order of operations:

  1. Click the card
  2. Dialog is added and card is moved there
  3. Animation up starts
  4. User hits escape twice, which immediately closes the dialog
  5. There is a race condition between the end of the card up animation and the close function.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it should, Liam. These are the styles from PCOM, it's simple but has a nice little "lift" add to the content. I think it already does but if it doesn't the dialog should probably be added to the DOM as soon as the class is initialized.

@layer silph-ui {
  .modal {
    --modal-animation-duration: 0.2s;
    --modal-animation-easing: ease-in-out;

    background: transparent;
    border: none;
    block-size: 100%;
    margin: 0;
    max-block-size: 100%;
    max-inline-size: 100%;
    inline-size: 100%;

    &::backdrop {
      background-color: rgb(0 0 0 / 0%);
    }

    &:open {
      &::backdrop {
        background-color: rgb(0 0 0 / 70%);
      }

      .content {
        opacity: 1;
        transform: translateY(0) scale(1);
      }
    }

    &:not([open]) {
      display: none;
    }
  }


  .content {
    opacity: 0;
    transform: translateY(1em) scale(0.96);
    transform-origin: center center;
    transition:
      opacity var(--modal-animation-duration) var(--modal-animation-easing),
      transform var(--modal-animation-duration) var(--modal-animation-easing);
    inline-size: 100%;
  }

  @supports (transition-behavior: allow-discrete) and (transition: display 1s) and (transition: overlay 1s) and (overlay: unset) {
    .modal {
      transition:
        display var(--modal-animation-duration) allow-discrete,
        overlay var(--modal-animation-duration) allow-discrete;
      transition-behavior: allow-discrete;

      &::backdrop {
        transition: background-color var(--modal-animation-duration) var(--modal-animation-easing);
      }

      @starting-style {
        &:open {
          &::backdrop {
            background-color: rgb(0 0 0 / 0%);
          }

          .content {
            opacity: 0;
            transform: translateY(1em) scale(0.96);
          }
        }
      }
    }
  }
}

@liamegan liamegan Jun 19, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hrm I don't think this will work here as we move the card from the place in the dom to the dialog on click (this is necessary to have a seamless transition of the animating cards), so there's not really any reason to persist the dialog in the dom outside of when it's needed, unless we want to try to maintain a single dialog for every card on the page - doable, but a pretty significant rewrite.

Would one of you mind connecting with me for a pair code on this to assess feasibility? I'm having a difficult time understanding it.

Comment thread src/perspective-card.js
Comment on lines +7 to +13
* .perspective-card
* .perspective-card__transformer
* .perspective-card__artwork.perspective-card__artwork--front
* img
* .perspective-card__artwork.perspective-card__artwork--back
* img
* .perspective-card__shine

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It might be helpful to abstract some of the base classes into constants that can be referred to throughout the JS. Thoughts? Just to avoid typos etc. when maintaining the repo down the road.

Comment thread src/perspective-card.js
Comment on lines +40 to +58
// set settings
this.debug = 'debug' in settings
? settings.debug
: this.element.hasAttribute('data-debug')
this.zoomSize = 'zoom' in settings
? settings.zoom
: (parseInt(this.element.getAttribute('data-zoom')) || 40)
this.intensity = 'intensity' in settings
? settings.intensity
: (parseInt(this.element.getAttribute('data-intensity')) || 10)

this.ambient = -1

if (settings.ambient !== undefined && settings.ambient !== false) {
const settingsVal = settings.ambient
if (settingsVal === true) this.ambient = 0
else this.ambient = settingsVal
} else if (this.element.hasAttribute('data-ambient')) {
const dataVal = this.element.getAttribute('data-ambient')

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would it be beneficial to grab this.element.dataset up front, do that you don't have to query the element's attributes each time?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good idea, it likely means introducing a parsing function up front and removing a lot of these conditionals.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants