瀏覽代碼

feat: massively improve ripple button

Sv443 10 月之前
父節點
當前提交
d91c22ae4e

+ 5 - 3
.storybook/preview-head.html

@@ -1,9 +1,11 @@
 <script>
   // shim for GM functions
   var GM = GM || {
-    getResourceURL: function() {
-      return `https://picsum.photos/200`;
-    },
+    getResourceUrl: (path) => new Promise((res) => {
+      if(typeof path === "string")
+        return res(`https://raw.githubusercontent.com/Sv443/BetterYTM/develop/assets/${path}`);
+      return res(`https://picsum.photos/200`);
+    }),
     copyToClipboard: function() {
       console.log("GM.copyToClipboard shim called (discarded)");
     },

+ 1 - 0
changelog.md

@@ -27,6 +27,7 @@
       - From `watchFlexy` to `ytWatchFlexy`
       - From `watchMetadata` to `ytWatchMetadata`
   - Added Storybook for easier and faster development of components
+  - Improved ripple component design and functionality (refer to the contributing guide for more info)
 - **Plugin Interface Changes:**
   - Added new components:
     -  `createLongBtn()` to create a button with an icon and text (works either as normal or as a toggle button)  

+ 4 - 1
src/components/longButton.ts

@@ -84,7 +84,10 @@ export async function createLongBtn({
 
   const imgElem = document.createElement("src" in rest ? "img" : "div");
   imgElem.classList.add("bytm-generic-btn-img", iconPosition ?? "left");
-  imgElem.innerHTML = "src" in rest ? rest.src : await resourceToHTMLString(rest.resourceName as "_") ?? "";
+  if("src" in rest)
+    (imgElem as HTMLImageElement).src = rest.src;
+  else
+    imgElem.innerHTML = await resourceToHTMLString(rest.resourceName as "_") ?? "";
 
   const txtElem = document.createElement("span");
   txtElem.classList.add("bytm-generic-long-btn-txt", "bytm-no-select");

+ 5 - 9
src/components/ripple.css

@@ -18,11 +18,6 @@ html body .bytm-ripple.fast {
   --bytm-ripple-anim-duration: 0.35s;
 }
 
-html body .bytm-ripple,
-html body .bytm-ripple.normal {
-  --bytm-ripple-anim-duration: 0.55s;
-}
-
 html body .bytm-ripple.slow {
   --bytm-ripple-anim-duration: 0.75s;
 }
@@ -32,12 +27,13 @@ html body .bytm-ripple.slower {
 }
 
 .bytm-ripple-area {
+  --bytm-ripple-min-size: 110px;
   position: absolute;
-  background: rgba(255, 255, 255, 0.25);
+  background: rgba(255, 255, 255, 0.325);
   transform: translate(-50%, -50%);
   pointer-events: none;
   border-radius: 50%;
-  animation: bytm-scale-ripple var(--bytm-ripple-anim-duration) cubic-bezier(0.375, 0.330, 0.225, 0.930);
+  animation: bytm-scale-ripple var(--bytm-ripple-anim-duration, 0.55s) cubic-bezier(0.375, 0.330, 0.225, 0.930);
 }
 
 @keyframes bytm-scale-ripple {
@@ -48,8 +44,8 @@ html body .bytm-ripple.slower {
   }
   100% {
     /* Variable is added to .bytm-ripple by JS at runtime since there's no better way of getting the parent's width inside of here */
-    width: calc(var(--bytm-ripple-cont-width) * 2);
-    height: calc(var(--bytm-ripple-cont-width) * 2);
+    width: calc(max(var(--bytm-ripple-cont-width, var(--bytm-ripple-min-size)) * 2, var(--bytm-ripple-min-size)));
+    height: calc(max(var(--bytm-ripple-cont-width, var(--bytm-ripple-min-size)) * 2, var(--bytm-ripple-min-size)));
     opacity: 0;
   }
 }

+ 1 - 1
src/components/ripple.ts

@@ -34,7 +34,7 @@ export function createRipple<TElem extends HTMLElement>(rippleElement?: TElem, p
   const updateRippleWidth = () => 
     rippleEl.style.setProperty("--bytm-ripple-cont-width", rippleEl.clientWidth + "px");
 
-  rippleEl.addEventListener("click", (e) => {
+  rippleEl.addEventListener("mousedown", (e) => {
     updateRippleWidth();
 
     const x = (e as MouseEvent).clientX - rippleEl.getBoundingClientRect().left;

+ 1 - 1
src/features/behavior.ts

@@ -1,5 +1,5 @@
 import { clamp, interceptWindowEvent, pauseFor } from "@sv443-network/userutils";
-import { domLoaded, error, getDomain, getVideoTime, getWatchId, info, log, getVideoSelector, waitVideoElementReady, t, clearNode } from "../utils/index.js";
+import { domLoaded, error, getDomain, getVideoTime, getWatchId, info, log, getVideoSelector, waitVideoElementReady, clearNode } from "../utils/index.js";
 import { getFeatures } from "../config.js";
 import { addSelectorListener } from "../observers.js";
 import { initialParams } from "../constants.js";

+ 17 - 17
src/features/layout.css

@@ -28,21 +28,33 @@
   transition: background-color 0.2s ease;
 }
 
-.bytm-generic-btn:not(.long):hover {
+.bytm-generic-btn:hover {
   background-color: rgba(255, 255, 255, 0.2);
 }
 
 .bytm-generic-btn:active {
-  animation: flashBorder 0.4s ease 1;
+  animation: bytm-flash-border 0.4s ease 1;
+}
+
+@keyframes bytm-flash-border {
+  0% {
+    border: 1px solid initial;
+  }
+  25% {
+    border: 1px solid rgba(255, 255, 255, 0.5);
+  }
+  100% {
+    border: 1px solid initial;
+  }
 }
 
 .bytm-generic-btn:not(.bytm-toggle):active {
-  background-color: #5f5f5f;
+  background-color: rgba(255, 255, 255, 0.3);
 }
 
 .bytm-generic-btn.long {
-  --bytm-generic-btn-width: 136px;
-  padding: 0px;
+  --bytm-generic-btn-width: calc(min(136px, fit-content));
+  padding: 0px 14px;
   line-height: 36px;
 
   display: inline-flex;
@@ -71,18 +83,6 @@
   --bytm-generic-btn-height: 24px;
 }
 
-@keyframes flashBorder {
-  0% {
-    border: 1px solid initial;
-  }
-  20% {
-    border: 1px solid #808080;
-  }
-  100% {
-    border: 1px solid initial;
-  }
-}
-
 .bytm-generic-btn-img {
   display: inline-block;
   z-index: 1;

+ 3 - 3
src/stories/GenericBtns.stories.ts

@@ -8,7 +8,7 @@ import "../features/layout.css";
 //#region meta
 
 const meta = {
-  title: "Ripple",
+  title: "Generic Buttons",
   render,
   argTypes: {
     backgroundColor: { control: "color" },
@@ -42,17 +42,17 @@ function render(args: GenericBtnStoryArgs) {
   switch(args.type) {
   case "circular":
     createCircularBtn({
-      src: args.src,
+      resourceName: "icons/globe.svg" as "_",
       onClick: args.onClick,
       title: args.label,
     }).then((btnEl) => wrapperEl.appendChild(btnEl));
     break;
   case "long":
     createLongBtn({
+      resourceName: "icons/globe.svg" as "_",
       onClick: args.onClick,
       title: args.label,
       text: args.label,
-      src: args.src,
     }).then((btnEl) => wrapperEl.appendChild(btnEl));
     break;
   }

+ 69 - 50
src/stories/Ripple.stories.ts

@@ -1,7 +1,10 @@
 import type { StoryObj, Meta } from "@storybook/html";
 import { fn } from "@storybook/test";
 import { createRipple } from "../components/ripple.js";
+import { createCircularBtn } from "../components/circularButton.js";
+import { createLongBtn } from "../components/longButton.js";
 import "../components/ripple.css";
+import "../features/layout.css";
 
 //#region meta
 
@@ -24,11 +27,11 @@ const meta = {
 export default meta;
 
 type RippleProps = {
-  type: "button" | "area";
+  type: "area" | "circularRippleBtn" | "longRippleBtn";
   backgroundColor: string;
   color: string;
   label: string;
-  onClick: (e: MouseEvent) => void;
+  onClick: (e: MouseEvent | KeyboardEvent) => void;
   padding: string;
   fontSize: string;
   speed: "faster" | "fast" | "normal" | "slow" | "slower";
@@ -39,65 +42,65 @@ type Story = StoryObj<RippleProps>;
 //#region render
 
 function render(props: RippleProps) {
-  const rippleEl = createRipple(document.createElement("div"), { speed: props.speed });
+  let rippleElem: HTMLElement;
 
   switch(props.type) {
   case "area":
-    rippleEl.style.minWidth = "1000px";
-    rippleEl.style.minHeight = "600px";
-    rippleEl.style.position = "relative";
-    rippleEl.style.display = "inline-flex";
-    rippleEl.style.justifyContent = "center";
-    rippleEl.style.alignItems = "center";
-    rippleEl.style.overflow = "hidden";
-    rippleEl.style.borderRadius = "32px";
-    rippleEl.style.height = "24px";
-    rippleEl.style.cursor = "pointer";
-    rippleEl.tabIndex = 0;
-
-    rippleEl.style.backgroundColor = props.backgroundColor;
-    rippleEl.style.color = props.color;
-    rippleEl.style.fontSize = props.fontSize;
-    rippleEl.appendChild(document.createTextNode(props.label));
-
-    rippleEl.addEventListener("click", props.onClick);
+    rippleElem = createRipple(document.createElement("div"), { speed: props.speed });
+
+    rippleElem.style.minWidth = "1000px";
+    rippleElem.style.minHeight = "600px";
+    rippleElem.style.position = "relative";
+    rippleElem.style.display = "inline-flex";
+    rippleElem.style.justifyContent = "center";
+    rippleElem.style.alignItems = "center";
+    rippleElem.style.overflow = "hidden";
+    rippleElem.style.borderRadius = "50px";
+    rippleElem.style.height = "24px";
+    rippleElem.style.cursor = "pointer";
+    rippleElem.tabIndex = 0;
+
+    rippleElem.style.backgroundColor = props.backgroundColor;
+    rippleElem.style.color = props.color;
+    rippleElem.style.fontSize = props.fontSize;
+    rippleElem.appendChild(document.createTextNode(props.label));
+
+    rippleElem.addEventListener("click", props.onClick);
     break;
-  case "button":
-    rippleEl.tabIndex = 0;
-    rippleEl.style.height = "24px";
-    rippleEl.style.display = "inline-flex";
-    rippleEl.style.justifyContent = "center";
-    rippleEl.style.alignItems = "center";
-    rippleEl.style.borderRadius = "24px";
-    rippleEl.style.cursor = "pointer";
-
-    rippleEl.innerHTML = props.label;
-    rippleEl.style.backgroundColor = props.backgroundColor;
-    rippleEl.style.color = props.color;
-    rippleEl.style.fontSize = props.fontSize;
-    rippleEl.style.padding = props.padding;
-
-    rippleEl.addEventListener("click", props.onClick);
+  case "circularRippleBtn":
+    rippleElem = document.createElement("span");
+    createCircularBtn({
+      resourceName: "icons/image.svg" as "_",
+      onClick: props.onClick,
+      title: props.label,
+    }).then((btnEl) => {
+      const rippleBtnEl = createRipple(btnEl, { speed: props.speed });
+      rippleElem.appendChild(rippleBtnEl);
+    }).catch((err) => console.error("Error creating circular button:", err));
+
+    rippleElem.addEventListener("click", props.onClick);
+    break;
+  case "longRippleBtn":
+    rippleElem = document.createElement("span");
+    createLongBtn({
+      resourceName: "icons/image.svg" as "_",
+      onClick: props.onClick,
+      title: props.label,
+      text: props.label,
+    }).then((btnEl) => {
+      const rippleBtnEl = createRipple(btnEl, { speed: props.speed });
+      rippleElem.appendChild(rippleBtnEl);
+    }).catch((err) => console.error("Error creating long button:", err));
+
+    rippleElem.addEventListener("click", props.onClick);
     break;
   }
 
-  return rippleEl;
+  return rippleElem;
 }
 
 //#region stories
 
-export const Button: Story = {
-  args: {
-    type: "button",
-    backgroundColor: "#123489",
-    color: "white",
-    label: "Hello I am a button",
-    padding: "8px 36px",
-    fontSize: "16px",
-    speed: "normal",
-  },
-};
-
 export const Area: Story = {
   args: {
     type: "area",
@@ -108,3 +111,19 @@ export const Area: Story = {
     speed: "slow",
   },
 };
+
+export const CircularRippleBtn: Story = {
+  args: {
+    type: "circularRippleBtn",
+    label: "Button da circular",
+    speed: "normal",
+  },
+};
+
+export const LongRippleBtn: Story = {
+  args: {
+    type: "longRippleBtn",
+    label: "Button da long way",
+    speed: "normal",
+  },
+};