diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css index 357724d9..615faa67 100644 --- a/app/assets/stylesheets/application.tailwind.css +++ b/app/assets/stylesheets/application.tailwind.css @@ -4,11 +4,11 @@ /* Reset rules, default styles applied to plain HTML */ @layer base { - details > summary::-webkit-details-marker { + details>summary::-webkit-details-marker { @apply hidden; } - details > summary { + details>summary { @apply list-none; } } @@ -38,7 +38,7 @@ @apply w-full cursor-pointer rounded-lg bg-black p-3 text-center text-white hover:bg-gray-700; } - input:checked + label + .toggle-switch-dot { + input:checked+label+.toggle-switch-dot { transform: translateX(100%); } @@ -90,6 +90,10 @@ @apply font-bold; } } + + .tooltip { + @apply hidden absolute; + } } /* Small, single purpose classes that should take precedence over other styles */ diff --git a/app/javascript/controllers/tooltip_controller.js b/app/javascript/controllers/tooltip_controller.js new file mode 100644 index 00000000..cad419a1 --- /dev/null +++ b/app/javascript/controllers/tooltip_controller.js @@ -0,0 +1,74 @@ +import { Controller } from '@hotwired/stimulus' +import { + computePosition, + flip, + shift, + offset, + arrow +} from '@floating-ui/dom'; + +export default class extends Controller { + static targets = ["arrow", "tooltip"]; + static values = { + placement: { type: String, default: "top" }, + offset: { type: Number, default: 10 }, + crossAxis: { type: Number, default: 0 }, + alignmentAxis: { type: Number, default: null }, + }; + + connect() { + this.element.addEventListener("mouseenter", this.showTooltip); + this.element.addEventListener("mouseleave", this.hideTooltip); + this.element.addEventListener("focus", this.showTooltip); + this.element.addEventListener("blur", this.hideTooltip); + }; + + showTooltip = () => { + this.tooltipTarget.style.display = 'block'; + this.#update(); + }; + + hideTooltip = () => { + this.tooltipTarget.style.display = ''; + }; + + disconnect() { + this.element.removeEventListener("mouseenter", this.showTooltip); + this.element.removeEventListener("mouseleave", this.hideTooltip); + this.element.removeEventListener("focus", this.showTooltip); + this.element.removeEventListener("blur", this.hideTooltip); + }; + + #update() { + computePosition(this.element, this.tooltipTarget, { + placement: this.placementValue, + middleware: [ + offset({ mainAxis: this.offsetValue, crossAxis: this.crossAxisValue, alignmentAxis: this.alignmentAxisValue }), + flip(), + shift({ padding: 5 }), + arrow({ element: this.arrowTarget }), + ], + }).then(({ x, y, placement, middlewareData }) => { + Object.assign(this.tooltipTarget.style, { + left: `${x}px`, + top: `${y}px`, + }); + + const { x: arrowX, y: arrowY } = middlewareData.arrow; + const staticSide = { + top: 'bottom', + right: 'left', + bottom: 'top', + left: 'right', + }[placement.split('-')[0]]; + + Object.assign(this.arrowTarget.style, { + left: arrowX != null ? `${arrowX}px` : '', + top: arrowY != null ? `${arrowY}px` : '', + right: '', + bottom: '', + [staticSide]: '-4px', + }); + }); + }; +} diff --git a/app/views/accounts/_tooltip.html.erb b/app/views/accounts/_tooltip.html.erb new file mode 100644 index 00000000..6a985793 --- /dev/null +++ b/app/views/accounts/_tooltip.html.erb @@ -0,0 +1,26 @@ +<%# locals: (account:) -%> +
+ <%= lucide_icon("info", class: "w-4 h-4 shrink-0 text-gray-500") %> + +
+
diff --git a/app/views/accounts/show.html.erb b/app/views/accounts/show.html.erb index 3d3de088..c794140d 100644 --- a/app/views/accounts/show.html.erb +++ b/app/views/accounts/show.html.erb @@ -52,7 +52,12 @@
- <%= tag.p t(".total_value"), class: "text-sm font-medium text-gray-500" %> +
+
+ <%= tag.p t(".total_value"), class: "text-sm font-medium text-gray-500" %> +
+ <%= render "tooltip", account: @account if @account.investment? %> +
<%= tag.p format_money(@account.value, precision: 0), class: "text-gray-900 text-3xl font-medium" %>
<% if @series.trend.direction.flat? %> diff --git a/config/importmap.rb b/config/importmap.rb index 7ec2b7f7..96aaed73 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -46,3 +46,7 @@ pin "d3-zoom" # @3.0.0 pin "delaunator" # @5.0.1 pin "internmap" # @2.0.3 pin "robust-predicates" # @3.0.2 +pin "@floating-ui/dom", to: "@floating-ui--dom.js" # @1.6.9 +pin "@floating-ui/core", to: "@floating-ui--core.js" # @1.6.6 +pin "@floating-ui/utils", to: "@floating-ui--utils.js" # @0.2.6 +pin "@floating-ui/utils/dom", to: "@floating-ui--utils--dom.js" # @0.2.6 diff --git a/config/locales/views/accounts/en.yml b/config/locales/views/accounts/en.yml index ef9ea9d3..45bc7fce 100644 --- a/config/locales/views/accounts/en.yml +++ b/config/locales/views/accounts/en.yml @@ -71,5 +71,10 @@ en: new: New account sync_all: success: Successfully queued accounts for syncing. + tooltip: + cash: Cash + holdings: Holdings + total_value_tooltip: The total value is the sum of cash balance and your holdings + value, minus margin loans. update: success: Account updated diff --git a/test/system/tooltips_test.rb b/test/system/tooltips_test.rb new file mode 100644 index 00000000..e9de040c --- /dev/null +++ b/test/system/tooltips_test.rb @@ -0,0 +1,26 @@ +require "application_system_test_case" + +class TooltipsTest < ApplicationSystemTestCase + include ActionView::Helpers::NumberHelper + include ApplicationHelper + + setup do + sign_in @user = users(:family_admin) + @account = accounts(:investment) + end + + test "can see account information tooltip" do + visit account_path(@account) + find('[data-controller="tooltip"]').hover + assert find("#tooltip", visible: true) + within "#tooltip" do + assert_text I18n.t("accounts.tooltip.total_value_tooltip") + assert_text I18n.t("accounts.tooltip.holdings") + assert_text format_money(@account.investment.holdings_value, precision: 0) + assert_text I18n.t("accounts.tooltip.cash") + assert_text format_money(@account.balance_money, precision: 0) + end + find("body").click + assert find("#tooltip", visible: false) + end +end diff --git a/vendor/javascript/@floating-ui--core.js b/vendor/javascript/@floating-ui--core.js new file mode 100644 index 00000000..68a5cf39 --- /dev/null +++ b/vendor/javascript/@floating-ui--core.js @@ -0,0 +1,2 @@ +import{getSideAxis as t,getAlignmentAxis as e,getAxisLength as n,getSide as o,getAlignment as s,evaluate as c,getPaddingObject as i,rectToClientRect as r,min as l,clamp as a,getOppositeAlignmentPlacement as f,placements as m,getAlignmentSides as u,getOppositePlacement as d,getExpandedPlacements as g,getOppositeAxisPlacements as p,sides as y,max as h,getOppositeAxis as w}from"@floating-ui/utils";export{rectToClientRect}from"@floating-ui/utils";function computeCoordsFromPlacement(c,i,r){let{reference:l,floating:a}=c;const f=t(i);const m=e(i);const u=n(m);const d=o(i);const g=f==="y";const p=l.x+l.width/2-a.width/2;const y=l.y+l.height/2-a.height/2;const h=l[u]/2-a[u]/2;let w;switch(d){case"top":w={x:p,y:l.y-a.height};break;case"bottom":w={x:p,y:l.y+l.height};break;case"right":w={x:l.x+l.width,y:y};break;case"left":w={x:l.x-a.width,y:y};break;default:w={x:l.x,y:l.y}}switch(s(i)){case"start":w[m]-=h*(r&&g?-1:1);break;case"end":w[m]+=h*(r&&g?-1:1);break}return w}const computePosition=async(t,e,n)=>{const{placement:o="bottom",strategy:s="absolute",middleware:c=[],platform:i}=n;const r=c.filter(Boolean);const l=await(i.isRTL==null?void 0:i.isRTL(e));let a=await i.getElementRects({reference:t,floating:e,strategy:s});let{x:f,y:m}=computeCoordsFromPlacement(a,o,l);let u=o;let d={};let g=0;for(let n=0;n({name:"arrow",options:t,async fn(o){const{x:r,y:f,placement:m,rects:u,platform:d,elements:g,middlewareData:p}=o;const{element:y,padding:h=0}=c(t,o)||{};if(y==null)return{};const w=i(h);const v={x:r,y:f};const x=e(m);const b=n(x);const R=await d.getDimensions(y);const A=x==="y";const O=A?"top":"left";const P=A?"bottom":"right";const C=A?"clientHeight":"clientWidth";const T=u.reference[b]+u.reference[x]-v[x]-u.floating[b];const L=v[x]-u.reference[x];const B=await(d.getOffsetParent==null?void 0:d.getOffsetParent(y));let D=B?B[C]:0;D&&await(d.isElement==null?void 0:d.isElement(B))||(D=g.floating[C]||u.floating[b]);const E=T/2-L/2;const k=D/2-R[b]/2-1;const S=l(w[O],k);const F=l(w[P],k);const H=S;const V=D-R[b]-F;const W=D/2-R[b]/2+E;const j=a(H,W,V);const z=!p.arrow&&s(m)!=null&&W!==j&&u.reference[b]/2-(Ws(e)===t)),...n.filter((e=>s(e)!==t))]:n.filter((t=>o(t)===t));return c.filter((n=>!t||(s(n)===t||!!e&&f(n)!==n)))}const autoPlacement=function(t){t===void 0&&(t={});return{name:"autoPlacement",options:t,async fn(e){var n,i,r;const{rects:l,middlewareData:a,placement:f,platform:d,elements:g}=e;const{crossAxis:p=false,alignment:y,allowedPlacements:h=m,autoAlignment:w=true,...v}=c(t,e);const x=y!==void 0||h===m?getPlacementList(y||null,w,h):h;const b=await detectOverflow(e,v);const R=((n=a.autoPlacement)==null?void 0:n.index)||0;const A=x[R];if(A==null)return{};const O=u(A,l,await(d.isRTL==null?void 0:d.isRTL(g.floating)));if(f!==A)return{reset:{placement:x[0]}};const P=[b[o(A)],b[O[0]],b[O[1]]];const C=[...((i=a.autoPlacement)==null?void 0:i.overflows)||[],{placement:A,overflows:P}];const T=x[R+1];if(T)return{data:{index:R+1,overflows:C},reset:{placement:T}};const L=C.map((t=>{const e=s(t.placement);return[t.placement,e&&p?t.overflows.slice(0,2).reduce(((t,e)=>t+e),0):t.overflows[0],t.overflows]})).sort(((t,e)=>t[1]-e[1]));const B=L.filter((t=>t[2].slice(0,s(t[0])?2:3).every((t=>t<=0))));const D=((r=B[0])==null?void 0:r[0])||L[0][0];return D!==f?{data:{index:R+1,overflows:C},reset:{placement:D}}:{}}}};const flip=function(e){e===void 0&&(e={});return{name:"flip",options:e,async fn(n){var s,i;const{placement:r,middlewareData:l,rects:a,initialPlacement:f,platform:m,elements:y}=n;const{mainAxis:h=true,crossAxis:w=true,fallbackPlacements:v,fallbackStrategy:x="bestFit",fallbackAxisSideDirection:b="none",flipAlignment:R=true,...A}=c(e,n);if((s=l.arrow)!=null&&s.alignmentOffset)return{};const O=o(r);const P=t(f);const C=o(f)===f;const T=await(m.isRTL==null?void 0:m.isRTL(y.floating));const L=v||(C||!R?[d(f)]:g(f));const B=b!=="none";!v&&B&&L.push(...p(f,R,b,T));const D=[f,...L];const E=await detectOverflow(n,A);const k=[];let S=((i=l.flip)==null?void 0:i.overflows)||[];h&&k.push(E[O]);if(w){const t=u(r,a,T);k.push(E[t[0]],E[t[1]])}S=[...S,{placement:r,overflows:k}];if(!k.every((t=>t<=0))){var F,H;const e=(((F=l.flip)==null?void 0:F.index)||0)+1;const n=D[e];if(n)return{data:{index:e,overflows:S},reset:{placement:n}};let o=(H=S.filter((t=>t.overflows[0]<=0)).sort(((t,e)=>t.overflows[1]-e.overflows[1]))[0])==null?void 0:H.placement;if(!o)switch(x){case"bestFit":{var V;const e=(V=S.filter((e=>{if(B){const n=t(e.placement);return n===P||n==="y"}return true})).map((t=>[t.placement,t.overflows.filter((t=>t>0)).reduce(((t,e)=>t+e),0)])).sort(((t,e)=>t[1]-e[1]))[0])==null?void 0:V[0];e&&(o=e);break}case"initialPlacement":o=f;break}if(r!==o)return{reset:{placement:o}}}return{}}}};function getSideOffsets(t,e){return{top:t.top-e.height,right:t.right-e.width,bottom:t.bottom-e.height,left:t.left-e.width}}function isAnySideFullyClipped(t){return y.some((e=>t[e]>=0))}const hide=function(t){t===void 0&&(t={});return{name:"hide",options:t,async fn(e){const{rects:n}=e;const{strategy:o="referenceHidden",...s}=c(t,e);switch(o){case"referenceHidden":{const t=await detectOverflow(e,{...s,elementContext:"reference"});const o=getSideOffsets(t,n.reference);return{data:{referenceHiddenOffsets:o,referenceHidden:isAnySideFullyClipped(o)}}}case"escaped":{const t=await detectOverflow(e,{...s,altBoundary:true});const o=getSideOffsets(t,n.floating);return{data:{escapedOffsets:o,escaped:isAnySideFullyClipped(o)}}}default:return{}}}}};function getBoundingRect(t){const e=l(...t.map((t=>t.left)));const n=l(...t.map((t=>t.top)));const o=h(...t.map((t=>t.right)));const s=h(...t.map((t=>t.bottom)));return{x:e,y:n,width:o-e,height:s-n}}function getRectsByLine(t){const e=t.slice().sort(((t,e)=>t.y-e.y));const n=[];let o=null;for(let t=0;to.height/2?n.push([s]):n[n.length-1].push(s);o=s}return n.map((t=>r(getBoundingRect(t))))}const inline=function(e){e===void 0&&(e={});return{name:"inline",options:e,async fn(n){const{placement:s,elements:a,rects:f,platform:m,strategy:u}=n;const{padding:d=2,x:g,y:p}=c(e,n);const y=Array.from(await(m.getClientRects==null?void 0:m.getClientRects(a.reference))||[]);const w=getRectsByLine(y);const v=r(getBoundingRect(y));const x=i(d);function getBoundingClientRect(){if(w.length===2&&w[0].left>w[1].right&&g!=null&&p!=null)return w.find((t=>g>t.left-x.left&&gt.top-x.top&&p=2){if(t(s)==="y"){const t=w[0];const e=w[w.length-1];const n=o(s)==="top";const c=t.top;const i=e.bottom;const r=n?t.left:e.left;const l=n?t.right:e.right;const a=l-r;const f=i-c;return{top:c,bottom:i,left:r,right:l,width:a,height:f,x:r,y:c}}const e=o(s)==="left";const n=h(...w.map((t=>t.right)));const c=l(...w.map((t=>t.left)));const i=w.filter((t=>e?t.left===c:t.right===n));const r=i[0].top;const a=i[i.length-1].bottom;const f=c;const m=n;const u=m-f;const d=a-r;return{top:r,bottom:a,left:f,right:m,width:u,height:d,x:f,y:r}}return v}const b=await m.getElementRects({reference:{getBoundingClientRect:getBoundingClientRect},floating:a.floating,strategy:u});return f.reference.x!==b.reference.x||f.reference.y!==b.reference.y||f.reference.width!==b.reference.width||f.reference.height!==b.reference.height?{reset:{rects:b}}:{}}}};async function convertValueToCoords(e,n){const{placement:i,platform:r,elements:l}=e;const a=await(r.isRTL==null?void 0:r.isRTL(l.floating));const f=o(i);const m=s(i);const u=t(i)==="y";const d=["left","top"].includes(f)?-1:1;const g=a&&u?-1:1;const p=c(n,e);let{mainAxis:y,crossAxis:h,alignmentAxis:w}=typeof p==="number"?{mainAxis:p,crossAxis:0,alignmentAxis:null}:{mainAxis:0,crossAxis:0,alignmentAxis:null,...p};m&&typeof w==="number"&&(h=m==="end"?w*-1:w);return u?{x:h*g,y:y*d}:{x:y*d,y:h*g}}const offset=function(t){t===void 0&&(t=0);return{name:"offset",options:t,async fn(e){var n,o;const{x:s,y:c,placement:i,middlewareData:r}=e;const l=await convertValueToCoords(e,t);return i===((n=r.offset)==null?void 0:n.placement)&&(o=r.arrow)!=null&&o.alignmentOffset?{}:{x:s+l.x,y:c+l.y,data:{...l,placement:i}}}}};const shift=function(e){e===void 0&&(e={});return{name:"shift",options:e,async fn(n){const{x:s,y:i,placement:r}=n;const{mainAxis:l=true,crossAxis:f=false,limiter:m={fn:t=>{let{x:e,y:n}=t;return{x:e,y:n}}},...u}=c(e,n);const d={x:s,y:i};const g=await detectOverflow(n,u);const p=t(o(r));const y=w(p);let h=d[y];let v=d[p];if(l){const t=y==="y"?"top":"left";const e=y==="y"?"bottom":"right";const n=h+g[t];const o=h-g[e];h=a(n,h,o)}if(f){const t=p==="y"?"top":"left";const e=p==="y"?"bottom":"right";const n=v+g[t];const o=v-g[e];v=a(n,v,o)}const x=m.fn({...n,[y]:h,[p]:v});return{...x,data:{x:x.x-s,y:x.y-i}}}}};const limitShift=function(e){e===void 0&&(e={});return{options:e,fn(n){const{x:s,y:i,placement:r,rects:l,middlewareData:a}=n;const{offset:f=0,mainAxis:m=true,crossAxis:u=true}=c(e,n);const d={x:s,y:i};const g=t(r);const p=w(g);let y=d[p];let h=d[g];const v=c(f,n);const x=typeof v==="number"?{mainAxis:v,crossAxis:0}:{mainAxis:0,crossAxis:0,...v};if(m){const t=p==="y"?"height":"width";const e=l.reference[p]-l.floating[t]+x.mainAxis;const n=l.reference[p]+l.reference[t]-x.mainAxis;yn&&(y=n)}if(u){var b,R;const t=p==="y"?"width":"height";const e=["top","left"].includes(o(r));const n=l.reference[g]-l.floating[t]+(e&&((b=a.offset)==null?void 0:b[g])||0)+(e?0:x.crossAxis);const s=l.reference[g]+l.reference[t]+(e?0:((R=a.offset)==null?void 0:R[g])||0)-(e?x.crossAxis:0);hs&&(h=s)}return{[p]:y,[g]:h}}}};const size=function(e){e===void 0&&(e={});return{name:"size",options:e,async fn(n){const{placement:i,rects:r,platform:a,elements:f}=n;const{apply:m=(()=>{}),...u}=c(e,n);const d=await detectOverflow(n,u);const g=o(i);const p=s(i);const y=t(i)==="y";const{width:w,height:v}=r.floating;let x;let b;if(g==="top"||g==="bottom"){x=g;b=p===(await(a.isRTL==null?void 0:a.isRTL(f.floating))?"start":"end")?"left":"right"}else{b=g;x=p==="end"?"top":"bottom"}const R=v-d.top-d.bottom;const A=w-d.left-d.right;const O=l(v-d[x],R);const P=l(w-d[b],A);const C=!n.middlewareData.shift;let T=O;let L=P;y?L=p||C?l(P,A):A:T=p||C?l(O,R):R;if(C&&!p){const t=h(d.left,0);const e=h(d.right,0);const n=h(d.top,0);const o=h(d.bottom,0);y?L=w-2*(t!==0||e!==0?t+e:h(d.left,d.right)):T=v-2*(n!==0||o!==0?n+o:h(d.top,d.bottom))}await m({...n,availableWidth:L,availableHeight:T});const B=await a.getDimensions(f.floating);return w!==B.width||v!==B.height?{reset:{rects:true}}:{}}}};export{arrow,autoPlacement,computePosition,detectOverflow,flip,hide,inline,limitShift,offset,shift,size}; + diff --git a/vendor/javascript/@floating-ui--dom.js b/vendor/javascript/@floating-ui--dom.js new file mode 100644 index 00000000..aa6d33d0 --- /dev/null +++ b/vendor/javascript/@floating-ui--dom.js @@ -0,0 +1,10 @@ +import{rectToClientRect as t,detectOverflow as e,offset as n,autoPlacement as i,shift as o,flip as s,size as c,hide as r,arrow as l,inline as f,limitShift as a,computePosition as u}from"@floating-ui/core";import{round as g,createCoords as h,max as d,min as p,floor as m}from"@floating-ui/utils";import{getComputedStyle as w,isHTMLElement as x,isElement as R,getWindow as v,isWebKit as y,getFrameElement as C,getDocumentElement as O,isTopLayer as b,getNodeName as T,isOverflowElement as P,getNodeScroll as L,getParentNode as B,isLastTraversableNode as S,getOverflowAncestors as A,isContainingBlock as F,isTableElement as E,getContainingBlock as V}from"@floating-ui/utils/dom";export{getOverflowAncestors}from"@floating-ui/utils/dom";function getCssDimensions(t){const e=w(t);let n=parseFloat(e.width)||0;let i=parseFloat(e.height)||0;const o=x(t);const s=o?t.offsetWidth:n;const c=o?t.offsetHeight:i;const r=g(n)!==s||g(i)!==c;if(r){n=s;i=c}return{width:n,height:i,$:r}}function unwrapElement(t){return R(t)?t:t.contextElement}function getScale(t){const e=unwrapElement(t);if(!x(e))return h(1);const n=e.getBoundingClientRect();const{width:i,height:o,$:s}=getCssDimensions(e);let c=(s?g(n.width):n.width)/i;let r=(s?g(n.height):n.height)/o;c&&Number.isFinite(c)||(c=1);r&&Number.isFinite(r)||(r=1);return{x:c,y:r}}const W=h(0);function getVisualOffsets(t){const e=v(t);return y()&&e.visualViewport?{x:e.visualViewport.offsetLeft,y:e.visualViewport.offsetTop}:W}function shouldAddVisualOffsets(t,e,n){e===void 0&&(e=false);return!(!n||e&&n!==v(t))&&e}function getBoundingClientRect(e,n,i,o){n===void 0&&(n=false);i===void 0&&(i=false);const s=e.getBoundingClientRect();const c=unwrapElement(e);let r=h(1);n&&(o?R(o)&&(r=getScale(o)):r=getScale(e));const l=shouldAddVisualOffsets(c,i,o)?getVisualOffsets(c):h(0);let f=(s.left+l.x)/r.x;let a=(s.top+l.y)/r.y;let u=s.width/r.x;let g=s.height/r.y;if(c){const t=v(c);const e=o&&R(o)?v(o):o;let n=t;let i=C(n);while(i&&o&&e!==n){const t=getScale(i);const e=i.getBoundingClientRect();const o=w(i);const s=e.left+(i.clientLeft+parseFloat(o.paddingLeft))*t.x;const c=e.top+(i.clientTop+parseFloat(o.paddingTop))*t.y;f*=t.x;a*=t.y;u*=t.x;g*=t.y;f+=s;a+=c;n=v(i);i=C(n)}}return t({width:u,height:g,x:f,y:a})}function convertOffsetParentRelativeRectToViewportRelativeRect(t){let{elements:e,rect:n,offsetParent:i,strategy:o}=t;const s=o==="fixed";const c=O(i);const r=!!e&&b(e.floating);if(i===c||r&&s)return n;let l={scrollLeft:0,scrollTop:0};let f=h(1);const a=h(0);const u=x(i);if(u||!u&&!s){(T(i)!=="body"||P(c))&&(l=L(i));if(x(i)){const t=getBoundingClientRect(i);f=getScale(i);a.x=t.x+i.clientLeft;a.y=t.y+i.clientTop}}return{width:n.width*f.x,height:n.height*f.y,x:n.x*f.x-l.scrollLeft*f.x+a.x,y:n.y*f.y-l.scrollTop*f.y+a.y}}function getClientRects(t){return Array.from(t.getClientRects())}function getWindowScrollBarX(t){return getBoundingClientRect(O(t)).left+L(t).scrollLeft}function getDocumentRect(t){const e=O(t);const n=L(t);const i=t.ownerDocument.body;const o=d(e.scrollWidth,e.clientWidth,i.scrollWidth,i.clientWidth);const s=d(e.scrollHeight,e.clientHeight,i.scrollHeight,i.clientHeight);let c=-n.scrollLeft+getWindowScrollBarX(t);const r=-n.scrollTop;w(i).direction==="rtl"&&(c+=d(e.clientWidth,i.clientWidth)-o);return{width:o,height:s,x:c,y:r}}function getViewportRect(t,e){const n=v(t);const i=O(t);const o=n.visualViewport;let s=i.clientWidth;let c=i.clientHeight;let r=0;let l=0;if(o){s=o.width;c=o.height;const t=y();if(!t||t&&e==="fixed"){r=o.offsetLeft;l=o.offsetTop}}return{width:s,height:c,x:r,y:l}}function getInnerBoundingClientRect(t,e){const n=getBoundingClientRect(t,true,e==="fixed");const i=n.top+t.clientTop;const o=n.left+t.clientLeft;const s=x(t)?getScale(t):h(1);const c=t.clientWidth*s.x;const r=t.clientHeight*s.y;const l=o*s.x;const f=i*s.y;return{width:c,height:r,x:l,y:f}}function getClientRectFromClippingAncestor(e,n,i){let o;if(n==="viewport")o=getViewportRect(e,i);else if(n==="document")o=getDocumentRect(O(e));else if(R(n))o=getInnerBoundingClientRect(n,i);else{const t=getVisualOffsets(e);o={...n,x:n.x-t.x,y:n.y-t.y}}return t(o)}function hasFixedPositionAncestor(t,e){const n=B(t);return!(n===e||!R(n)||S(n))&&(w(n).position==="fixed"||hasFixedPositionAncestor(n,e))}function getClippingElementAncestors(t,e){const n=e.get(t);if(n)return n;let i=A(t,[],false).filter((t=>R(t)&&T(t)!=="body"));let o=null;const s=w(t).position==="fixed";let c=s?B(t):t;while(R(c)&&!S(c)){const e=w(c);const n=F(c);n||e.position!=="fixed"||(o=null);const r=s?!n&&!o:!n&&e.position==="static"&&!!o&&["absolute","fixed"].includes(o.position)||P(c)&&!n&&hasFixedPositionAncestor(t,c);r?i=i.filter((t=>t!==c)):o=e;c=B(c)}e.set(t,i);return i}function getClippingRect(t){let{element:e,boundary:n,rootBoundary:i,strategy:o}=t;const s=n==="clippingAncestors"?b(e)?[]:getClippingElementAncestors(e,this._c):[].concat(n);const c=[...s,i];const r=c[0];const l=c.reduce(((t,n)=>{const i=getClientRectFromClippingAncestor(e,n,o);t.top=d(i.top,t.top);t.right=p(i.right,t.right);t.bottom=p(i.bottom,t.bottom);t.left=d(i.left,t.left);return t}),getClientRectFromClippingAncestor(e,r,o));return{width:l.right-l.left,height:l.bottom-l.top,x:l.left,y:l.top}}function getDimensions(t){const{width:e,height:n}=getCssDimensions(t);return{width:e,height:n}}function getRectRelativeToOffsetParent(t,e,n){const i=x(e);const o=O(e);const s=n==="fixed";const c=getBoundingClientRect(t,true,s,e);let r={scrollLeft:0,scrollTop:0};const l=h(0);if(i||!i&&!s){(T(e)!=="body"||P(o))&&(r=L(e));if(i){const t=getBoundingClientRect(e,true,s,e);l.x=t.x+e.clientLeft;l.y=t.y+e.clientTop}else o&&(l.x=getWindowScrollBarX(o))}const f=c.left+r.scrollLeft-l.x;const a=c.top+r.scrollTop-l.y;return{x:f,y:a,width:c.width,height:c.height}}function isStaticPositioned(t){return w(t).position==="static"}function getTrueOffsetParent(t,e){return x(t)&&w(t).position!=="fixed"?e?e(t):t.offsetParent:null}function getOffsetParent(t,e){const n=v(t);if(b(t))return n;if(!x(t)){let e=B(t);while(e&&!S(e)){if(R(e)&&!isStaticPositioned(e))return e;e=B(e)}return n}let i=getTrueOffsetParent(t,e);while(i&&E(i)&&isStaticPositioned(i))i=getTrueOffsetParent(i,e);return i&&S(i)&&isStaticPositioned(i)&&!F(i)?n:i||V(t)||n}const getElementRects=async function(t){const e=this.getOffsetParent||getOffsetParent;const n=this.getDimensions;const i=await n(t.floating);return{reference:getRectRelativeToOffsetParent(t.reference,await e(t.floating),t.strategy),floating:{x:0,y:0,width:i.width,height:i.height}}};function isRTL(t){return w(t).direction==="rtl"}const D={convertOffsetParentRelativeRectToViewportRelativeRect:convertOffsetParentRelativeRectToViewportRelativeRect,getDocumentElement:O,getClippingRect:getClippingRect,getOffsetParent:getOffsetParent,getElementRects:getElementRects,getClientRects:getClientRects,getDimensions:getDimensions,getScale:getScale,isElement:R,isRTL:isRTL};function observeMove(t,e){let n=null;let i;const o=O(t);function cleanup(){var t;clearTimeout(i);(t=n)==null||t.disconnect();n=null}function refresh(s,c){s===void 0&&(s=false);c===void 0&&(c=1);cleanup();const{left:r,top:l,width:f,height:a}=t.getBoundingClientRect();s||e();if(!f||!a)return;const u=m(l);const g=m(o.clientWidth-(r+f));const h=m(o.clientHeight-(l+a));const w=m(r);const x=-u+"px "+-g+"px "+-h+"px "+-w+"px";const R={rootMargin:x,threshold:d(0,p(1,c))||1};let v=true;function handleObserve(t){const e=t[0].intersectionRatio;if(e!==c){if(!v)return refresh();e?refresh(false,e):i=setTimeout((()=>{refresh(false,1e-7)}),1e3)}v=false}try{n=new IntersectionObserver(handleObserve,{...R,root:o.ownerDocument})}catch(t){n=new IntersectionObserver(handleObserve,R)}n.observe(t)}refresh(true);return cleanup} +/** + * Automatically updates the position of the floating element when necessary. + * Should only be called when the floating element is mounted on the DOM or + * visible on the screen. + * @returns cleanup function that should be invoked when the floating element is + * removed from the DOM or hidden from the screen. + * @see https://floating-ui.com/docs/autoUpdate + */function autoUpdate(t,e,n,i){i===void 0&&(i={});const{ancestorScroll:o=true,ancestorResize:s=true,elementResize:c=typeof ResizeObserver==="function",layoutShift:r=typeof IntersectionObserver==="function",animationFrame:l=false}=i;const f=unwrapElement(t);const a=o||s?[...f?A(f):[],...A(e)]:[];a.forEach((t=>{o&&t.addEventListener("scroll",n,{passive:true});s&&t.addEventListener("resize",n)}));const u=f&&r?observeMove(f,n):null;let g=-1;let h=null;if(c){h=new ResizeObserver((t=>{let[i]=t;if(i&&i.target===f&&h){h.unobserve(e);cancelAnimationFrame(g);g=requestAnimationFrame((()=>{var t;(t=h)==null||t.observe(e)}))}n()}));f&&!l&&h.observe(f);h.observe(e)}let d;let p=l?getBoundingClientRect(t):null;l&&frameLoop();function frameLoop(){const e=getBoundingClientRect(t);!p||e.x===p.x&&e.y===p.y&&e.width===p.width&&e.height===p.height||n();p=e;d=requestAnimationFrame(frameLoop)}n();return()=>{var t;a.forEach((t=>{o&&t.removeEventListener("scroll",n);s&&t.removeEventListener("resize",n)}));u==null||u();(t=h)==null||t.disconnect();h=null;l&&cancelAnimationFrame(d)}}const H=e;const z=n;const I=i;const M=o;const X=s;const q=c;const N=r;const U=l;const $=f;const _=a;const computePosition=(t,e,n)=>{const i=new Map;const o={platform:D,...n};const s={...o.platform,_c:i};return u(t,e,{...o,platform:s})};export{U as arrow,I as autoPlacement,autoUpdate,computePosition,H as detectOverflow,X as flip,N as hide,$ as inline,_ as limitShift,z as offset,D as platform,M as shift,q as size}; + diff --git a/vendor/javascript/@floating-ui--utils--dom.js b/vendor/javascript/@floating-ui--utils--dom.js new file mode 100644 index 00000000..098e5ac3 --- /dev/null +++ b/vendor/javascript/@floating-ui--utils--dom.js @@ -0,0 +1,2 @@ +function getNodeName(e){return isNode(e)?(e.nodeName||"").toLowerCase():"#document"}function getWindow(e){var t;return(e==null||(t=e.ownerDocument)==null?void 0:t.defaultView)||window}function getDocumentElement(e){var t;return(t=(isNode(e)?e.ownerDocument:e.document)||window.document)==null?void 0:t.documentElement}function isNode(e){return e instanceof Node||e instanceof getWindow(e).Node}function isElement(e){return e instanceof Element||e instanceof getWindow(e).Element}function isHTMLElement(e){return e instanceof HTMLElement||e instanceof getWindow(e).HTMLElement}function isShadowRoot(e){return typeof ShadowRoot!=="undefined"&&(e instanceof ShadowRoot||e instanceof getWindow(e).ShadowRoot)}function isOverflowElement(e){const{overflow:t,overflowX:n,overflowY:o,display:r}=getComputedStyle(e);return/auto|scroll|overlay|hidden|clip/.test(t+o+n)&&!["inline","contents"].includes(r)}function isTableElement(e){return["table","td","th"].includes(getNodeName(e))}function isTopLayer(e){return[":popover-open",":modal"].some((t=>{try{return e.matches(t)}catch(e){return false}}))}function isContainingBlock(e){const t=isWebKit();const n=isElement(e)?getComputedStyle(e):e;return n.transform!=="none"||n.perspective!=="none"||!!n.containerType&&n.containerType!=="normal"||!t&&!!n.backdropFilter&&n.backdropFilter!=="none"||!t&&!!n.filter&&n.filter!=="none"||["transform","perspective","filter"].some((e=>(n.willChange||"").includes(e)))||["paint","layout","strict","content"].some((e=>(n.contain||"").includes(e)))}function getContainingBlock(e){let t=getParentNode(e);while(isHTMLElement(t)&&!isLastTraversableNode(t)){if(isContainingBlock(t))return t;if(isTopLayer(t))return null;t=getParentNode(t)}return null}function isWebKit(){return!(typeof CSS==="undefined"||!CSS.supports)&&CSS.supports("-webkit-backdrop-filter","none")}function isLastTraversableNode(e){return["html","body","#document"].includes(getNodeName(e))}function getComputedStyle(e){return getWindow(e).getComputedStyle(e)}function getNodeScroll(e){return isElement(e)?{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}:{scrollLeft:e.scrollX,scrollTop:e.scrollY}}function getParentNode(e){if(getNodeName(e)==="html")return e;const t=e.assignedSlot||e.parentNode||isShadowRoot(e)&&e.host||getDocumentElement(e);return isShadowRoot(t)?t.host:t}function getNearestOverflowAncestor(e){const t=getParentNode(e);return isLastTraversableNode(t)?e.ownerDocument?e.ownerDocument.body:e.body:isHTMLElement(t)&&isOverflowElement(t)?t:getNearestOverflowAncestor(t)}function getOverflowAncestors(e,t,n){var o;t===void 0&&(t=[]);n===void 0&&(n=true);const r=getNearestOverflowAncestor(e);const i=r===((o=e.ownerDocument)==null?void 0:o.body);const l=getWindow(r);if(i){const e=getFrameElement(l);return t.concat(l,l.visualViewport||[],isOverflowElement(r)?r:[],e&&n?getOverflowAncestors(e):[])}return t.concat(r,getOverflowAncestors(r,[],n))}function getFrameElement(e){return Object.getPrototypeOf(e.parent)?e.frameElement:null}export{getComputedStyle,getContainingBlock,getDocumentElement,getFrameElement,getNearestOverflowAncestor,getNodeName,getNodeScroll,getOverflowAncestors,getParentNode,getWindow,isContainingBlock,isElement,isHTMLElement,isLastTraversableNode,isNode,isOverflowElement,isShadowRoot,isTableElement,isTopLayer,isWebKit}; + diff --git a/vendor/javascript/@floating-ui--utils.js b/vendor/javascript/@floating-ui--utils.js new file mode 100644 index 00000000..b457af7b --- /dev/null +++ b/vendor/javascript/@floating-ui--utils.js @@ -0,0 +1,2 @@ +const t=["top","right","bottom","left"];const e=["start","end"];const n=t.reduce(((t,n)=>t.concat(n,n+"-"+e[0],n+"-"+e[1])),[]);const i=Math.min;const o=Math.max;const g=Math.round;const c=Math.floor;const createCoords=t=>({x:t,y:t});const s={left:"right",right:"left",bottom:"top",top:"bottom"};const r={start:"end",end:"start"};function clamp(t,e,n){return o(t,i(e,n))}function evaluate(t,e){return typeof t==="function"?t(e):t}function getSide(t){return t.split("-")[0]}function getAlignment(t){return t.split("-")[1]}function getOppositeAxis(t){return t==="x"?"y":"x"}function getAxisLength(t){return t==="y"?"height":"width"}function getSideAxis(t){return["top","bottom"].includes(getSide(t))?"y":"x"}function getAlignmentAxis(t){return getOppositeAxis(getSideAxis(t))}function getAlignmentSides(t,e,n){n===void 0&&(n=false);const i=getAlignment(t);const o=getAlignmentAxis(t);const g=getAxisLength(o);let c=o==="x"?i===(n?"end":"start")?"right":"left":i==="start"?"bottom":"top";e.reference[g]>e.floating[g]&&(c=getOppositePlacement(c));return[c,getOppositePlacement(c)]}function getExpandedPlacements(t){const e=getOppositePlacement(t);return[getOppositeAlignmentPlacement(t),e,getOppositeAlignmentPlacement(e)]}function getOppositeAlignmentPlacement(t){return t.replace(/start|end/g,(t=>r[t]))}function getSideList(t,e,n){const i=["left","right"];const o=["right","left"];const g=["top","bottom"];const c=["bottom","top"];switch(t){case"top":case"bottom":return n?e?o:i:e?i:o;case"left":case"right":return e?g:c;default:return[]}}function getOppositeAxisPlacements(t,e,n,i){const o=getAlignment(t);let g=getSideList(getSide(t),n==="start",i);if(o){g=g.map((t=>t+"-"+o));e&&(g=g.concat(g.map(getOppositeAlignmentPlacement)))}return g}function getOppositePlacement(t){return t.replace(/left|right|bottom|top/g,(t=>s[t]))}function expandPaddingObject(t){return{top:0,right:0,bottom:0,left:0,...t}}function getPaddingObject(t){return typeof t!=="number"?expandPaddingObject(t):{top:t,right:t,bottom:t,left:t}}function rectToClientRect(t){const{x:e,y:n,width:i,height:o}=t;return{width:i,height:o,top:n,left:e,right:e+i,bottom:n+o,x:e,y:n}}export{e as alignments,clamp,createCoords,evaluate,expandPaddingObject,c as floor,getAlignment,getAlignmentAxis,getAlignmentSides,getAxisLength,getExpandedPlacements,getOppositeAlignmentPlacement,getOppositeAxis,getOppositeAxisPlacements,getOppositePlacement,getPaddingObject,getSide,getSideAxis,o as max,i as min,n as placements,rectToClientRect,g as round,t as sides}; +