@aiquants/virtualscroll 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,2 +1,2 @@
1
1
  /*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
2
- @layer components{.aqvs-scroll-pane{display:flex;position:relative}.aqvs-scroll-pane-content{box-sizing:border-box;flex:1;height:100%;position:relative;overflow:hidden}.aqvs-tap-scroll-circle{touch-action:none;-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;transition-property:transform;transition-duration:.1s;transition-timing-function:cubic-bezier(0,0,.2,1);display:flex;position:relative}.aqvs-tap-scroll-circle-visual-outer{border-radius:9999px;position:absolute;inset:0}.aqvs-tap-scroll-circle-gradient{background:linear-gradient(to bottom right,#1d4ed899,#60a5fa8c,#bfdbfe66);border:1px solid #fff6;border-radius:9999px;position:absolute;inset:0;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-tap-scroll-circle-inner{border-radius:9999px;position:absolute;inset:18%}.aqvs-sample-visual-pupil{box-sizing:border-box;background-color:#ffffffd9;border:1px solid #ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-highlight{background-color:#fffc;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-rod{background-color:#ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-scrollbar{z-index:50;cursor:default;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;touch-action:none;background-color:#fff;position:relative}.aqvs-scrollbar-horizontal{flex-direction:row;align-items:stretch;display:flex}.aqvs-scrollbar-vertical{flex-direction:column;align-items:stretch;display:flex}.aqvs-scrollbar[data-visible=false],.aqvs-scrollbar-thumb-wrapper[data-visible=false]{opacity:0;pointer-events:none}.aqvs-scrollbar-tap-circle-wrapper{pointer-events:auto;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);position:absolute}.aqvs-scrollbar-arrow-button{color:#313131;letter-spacing:normal;background-color:#e0e0e0;border:0 solid;justify-content:center;align-items:center;padding:0;font-family:system-ui,sans-serif;font-size:.75rem;font-weight:400;line-height:1rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:flex}.aqvs-scrollbar-arrow-button:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scrollbar-arrow-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.aqvs-scrollbar-arrow-button:disabled{cursor:not-allowed;opacity:.5}.aqvs-scrollbar-arrow-button:enabled:hover{background-color:#d4d4d4}.aqvs-scrollbar-track{background-color:#f5f5f5;flex:1;position:relative}.aqvs-scrollbar-overlay{pointer-events:none;position:absolute;inset:0}.aqvs-scrollbar-thumb-wrapper{touch-action:none;position:absolute}.aqvs-scrollbar-thumb{transform-origin:50%;background-color:#7f7f7f;transition:transform 80ms ease-out;position:absolute}.aqvs-scrollbar-thumb[data-thumb-state=disabled]{transition:none;transform:none}.aqvs-scrollbar-thumb[data-thumb-state=hover]{background-color:#5f5f5f}.aqvs-scrollbar-thumb[data-thumb-state=dragging]{background-color:#4f4f4f;transition:transform 60ms ease-out}.aqvs-scrollbar-thumb-horizontal{inset:1.5px 0}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=hover]{top:-.5px;bottom:-.5px;transform:scaleY(1.06)}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=dragging]{top:-.5px;bottom:-.5px;transform:scaleY(1.12)}.aqvs-scrollbar-thumb-vertical{inset:0 1.5px}.aqvs-scrollbar-thumb-vertical[data-thumb-state=hover]{left:-.5px;right:-.5px;transform:scaleX(1.06)}.aqvs-scrollbar-thumb-vertical[data-thumb-state=dragging]{left:-.5px;right:-.5px;transform:scaleX(1.12)}.aqvs-scroll-to-edge-button{pointer-events:auto;cursor:pointer;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;color:#fff;text-transform:uppercase;letter-spacing:.05em;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background-color:#1f2937cc;border:0 solid;border-radius:9999px;padding:.25rem 3rem;font-family:inherit;font-size:10px;font-weight:500;line-height:1.5;transition-property:transform,background-color;transition-duration:.15s;transform:none;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-scroll-to-edge-button:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scroll-to-edge-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.aqvs-scroll-to-edge-button:hover{background-color:#374151}.aqvs-scroll-to-edge-button:active{transform:scale(.95)}.aqvs-scroll-to-edge-overlay{pointer-events:none;z-index:10;transition-property:opacity;transition-duration:.5s;position:absolute;inset:0}.aqvs-scroll-to-edge-overlay[data-visible=true]{opacity:1}.aqvs-scroll-to-edge-overlay[data-visible=false]{opacity:0}.aqvs-scroll-to-edge-overlay[data-visible=false] .aqvs-scroll-to-edge-button{pointer-events:none}.aqvs-scroll-to-edge-button-container{justify-content:center;display:flex;position:absolute;left:0;right:0}.aqvs-scroll-to-edge-button-container-top{top:.5rem}.aqvs-scroll-to-edge-button-container-bottom{bottom:.5rem}.aqvs-no-items-container{width:100%;position:absolute;top:0}.aqvs-no-items-text{text-align:center;color:#6b7280}.aqvs-item-container,.aqvs-bottom-inset,.aqvs-items-wrapper{width:100%;position:absolute}}
2
+ @layer components{.aqvs-scroll-pane{display:flex;position:relative}.aqvs-scroll-pane[data-self-measured=true]{height:100%}.aqvs-scroll-pane-content{box-sizing:border-box;flex:1;height:100%;position:relative;overflow:hidden}.aqvs-tap-scroll-circle{touch-action:none;-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;transition-property:transform;transition-duration:.1s;transition-timing-function:cubic-bezier(0,0,.2,1);display:flex;position:relative}.aqvs-tap-scroll-circle-visual-outer{border-radius:9999px;position:absolute;inset:0}.aqvs-tap-scroll-circle-gradient{background:linear-gradient(to bottom right,#1d4ed899,#60a5fa8c,#bfdbfe66);border:1px solid #fff6;border-radius:9999px;position:absolute;inset:0;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-tap-scroll-circle-inner{border-radius:9999px;position:absolute;inset:18%}.aqvs-sample-visual-pupil{box-sizing:border-box;background-color:#ffffffd9;border:1px solid #ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-highlight{background-color:#fffc;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-rod{background-color:#ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-scrollbar{z-index:50;cursor:default;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;touch-action:none;background-color:#fff;position:relative}.aqvs-scrollbar-horizontal{flex-direction:row;align-items:stretch;display:flex}.aqvs-scrollbar-vertical{flex-direction:column;align-items:stretch;display:flex}.aqvs-scrollbar[data-visible=false],.aqvs-scrollbar-thumb-wrapper[data-visible=false]{opacity:0;pointer-events:none}.aqvs-scrollbar-tap-circle-wrapper{pointer-events:auto;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);position:absolute}.aqvs-scrollbar-arrow-button{color:#313131;letter-spacing:normal;background-color:#e0e0e0;border:0 solid;justify-content:center;align-items:center;padding:0;font-family:system-ui,sans-serif;font-size:.75rem;font-weight:400;line-height:1rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:flex}.aqvs-scrollbar-arrow-button:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scrollbar-arrow-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.aqvs-scrollbar-arrow-button:disabled{cursor:not-allowed;opacity:.5}.aqvs-scrollbar-arrow-button:enabled:hover{background-color:#d4d4d4}.aqvs-scrollbar-track{background-color:#f5f5f5;flex:1;position:relative}.aqvs-scrollbar-overlay{pointer-events:none;position:absolute;inset:0}.aqvs-scrollbar-thumb-wrapper{touch-action:none;position:absolute}.aqvs-scrollbar-thumb{transform-origin:50%;background-color:#7f7f7f;transition:transform 80ms ease-out;position:absolute}.aqvs-scrollbar-thumb[data-thumb-state=disabled]{transition:none;transform:none}.aqvs-scrollbar-thumb[data-thumb-state=hover]{background-color:#5f5f5f}.aqvs-scrollbar-thumb[data-thumb-state=dragging]{background-color:#4f4f4f;transition:transform 60ms ease-out}.aqvs-scrollbar-thumb-horizontal{inset:1.5px 0}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=hover]{top:-.5px;bottom:-.5px;transform:scaleY(1.06)}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=dragging]{top:-.5px;bottom:-.5px;transform:scaleY(1.12)}.aqvs-scrollbar-thumb-vertical{inset:0 1.5px}.aqvs-scrollbar-thumb-vertical[data-thumb-state=hover]{left:-.5px;right:-.5px;transform:scaleX(1.06)}.aqvs-scrollbar-thumb-vertical[data-thumb-state=dragging]{left:-.5px;right:-.5px;transform:scaleX(1.12)}.aqvs-scroll-to-edge-button{pointer-events:auto;cursor:pointer;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;color:#fff;text-transform:uppercase;letter-spacing:.05em;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background-color:#1f2937cc;border:0 solid;border-radius:9999px;padding:.25rem 3rem;font-family:inherit;font-size:10px;font-weight:500;line-height:1.5;transition-property:transform,background-color;transition-duration:.15s;transform:none;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-scroll-to-edge-button:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scroll-to-edge-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.aqvs-scroll-to-edge-button:hover{background-color:#374151}.aqvs-scroll-to-edge-button:active{transform:scale(.95)}.aqvs-scroll-to-edge-overlay{pointer-events:none;z-index:10;transition-property:opacity;transition-duration:.5s;position:absolute;inset:0}.aqvs-scroll-to-edge-overlay[data-visible=true]{opacity:1}.aqvs-scroll-to-edge-overlay[data-visible=false]{opacity:0}.aqvs-scroll-to-edge-overlay[data-visible=false] .aqvs-scroll-to-edge-button{pointer-events:none}.aqvs-scroll-to-edge-button-container{justify-content:center;display:flex;position:absolute;left:0;right:0}.aqvs-scroll-to-edge-button-container-top{top:.5rem}.aqvs-scroll-to-edge-button-container-bottom{bottom:.5rem}.aqvs-no-items-container{width:100%;position:absolute;top:0}.aqvs-no-items-text{text-align:center;color:#6b7280}.aqvs-item-container,.aqvs-bottom-inset,.aqvs-items-wrapper{width:100%;position:absolute}}
@@ -1,3 +1,3 @@
1
1
  /*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
2
2
  /*! @aiquants/virtualscroll standalone CSS — 非 Tailwind ホスト専用。ホストの Tailwind ビルドと混在させないこと */
3
- @layer theme,base;@layer components{.aqvs-scroll-pane{display:flex;position:relative}.aqvs-scroll-pane-content{box-sizing:border-box;flex:1;height:100%;position:relative;overflow:hidden}.aqvs-tap-scroll-circle{touch-action:none;-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;transition-property:transform;transition-duration:.1s;transition-timing-function:cubic-bezier(0,0,.2,1);display:flex;position:relative}.aqvs-tap-scroll-circle-visual-outer{border-radius:9999px;position:absolute;inset:0}.aqvs-tap-scroll-circle-gradient{background:linear-gradient(to bottom right,#1d4ed899,#60a5fa8c,#bfdbfe66);border:1px solid #fff6;border-radius:9999px;position:absolute;inset:0;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-tap-scroll-circle-inner{border-radius:9999px;position:absolute;inset:18%}.aqvs-sample-visual-pupil{box-sizing:border-box;background-color:#ffffffd9;border:1px solid #ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-highlight{background-color:#fffc;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-rod{background-color:#ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-scrollbar{z-index:50;cursor:default;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;touch-action:none;background-color:#fff;position:relative}.aqvs-scrollbar-horizontal{flex-direction:row;align-items:stretch;display:flex}.aqvs-scrollbar-vertical{flex-direction:column;align-items:stretch;display:flex}.aqvs-scrollbar[data-visible=false],.aqvs-scrollbar-thumb-wrapper[data-visible=false]{opacity:0;pointer-events:none}.aqvs-scrollbar-tap-circle-wrapper{pointer-events:auto;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);position:absolute}.aqvs-scrollbar-arrow-button{color:#313131;letter-spacing:normal;background-color:#e0e0e0;border:0 solid;justify-content:center;align-items:center;padding:0;font-family:system-ui,sans-serif;font-size:.75rem;font-weight:400;line-height:1rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:flex}.aqvs-scrollbar-arrow-button:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scrollbar-arrow-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.aqvs-scrollbar-arrow-button:disabled{cursor:not-allowed;opacity:.5}.aqvs-scrollbar-arrow-button:enabled:hover{background-color:#d4d4d4}.aqvs-scrollbar-track{background-color:#f5f5f5;flex:1;position:relative}.aqvs-scrollbar-overlay{pointer-events:none;position:absolute;inset:0}.aqvs-scrollbar-thumb-wrapper{touch-action:none;position:absolute}.aqvs-scrollbar-thumb{transform-origin:50%;background-color:#7f7f7f;transition:transform 80ms ease-out;position:absolute}.aqvs-scrollbar-thumb[data-thumb-state=disabled]{transition:none;transform:none}.aqvs-scrollbar-thumb[data-thumb-state=hover]{background-color:#5f5f5f}.aqvs-scrollbar-thumb[data-thumb-state=dragging]{background-color:#4f4f4f;transition:transform 60ms ease-out}.aqvs-scrollbar-thumb-horizontal{inset:1.5px 0}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=hover]{top:-.5px;bottom:-.5px;transform:scaleY(1.06)}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=dragging]{top:-.5px;bottom:-.5px;transform:scaleY(1.12)}.aqvs-scrollbar-thumb-vertical{inset:0 1.5px}.aqvs-scrollbar-thumb-vertical[data-thumb-state=hover]{left:-.5px;right:-.5px;transform:scaleX(1.06)}.aqvs-scrollbar-thumb-vertical[data-thumb-state=dragging]{left:-.5px;right:-.5px;transform:scaleX(1.12)}.aqvs-scroll-to-edge-button{pointer-events:auto;cursor:pointer;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;color:#fff;text-transform:uppercase;letter-spacing:.05em;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background-color:#1f2937cc;border:0 solid;border-radius:9999px;padding:.25rem 3rem;font-family:inherit;font-size:10px;font-weight:500;line-height:1.5;transition-property:transform,background-color;transition-duration:.15s;transform:none;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-scroll-to-edge-button:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scroll-to-edge-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.aqvs-scroll-to-edge-button:hover{background-color:#374151}.aqvs-scroll-to-edge-button:active{transform:scale(.95)}.aqvs-scroll-to-edge-overlay{pointer-events:none;z-index:10;transition-property:opacity;transition-duration:.5s;position:absolute;inset:0}.aqvs-scroll-to-edge-overlay[data-visible=true]{opacity:1}.aqvs-scroll-to-edge-overlay[data-visible=false]{opacity:0}.aqvs-scroll-to-edge-overlay[data-visible=false] .aqvs-scroll-to-edge-button{pointer-events:none}.aqvs-scroll-to-edge-button-container{justify-content:center;display:flex;position:absolute;left:0;right:0}.aqvs-scroll-to-edge-button-container-top{top:.5rem}.aqvs-scroll-to-edge-button-container-bottom{bottom:.5rem}.aqvs-no-items-container{width:100%;position:absolute;top:0}.aqvs-no-items-text{text-align:center;color:#6b7280}.aqvs-item-container,.aqvs-bottom-inset,.aqvs-items-wrapper{width:100%;position:absolute}}@layer utilities;
3
+ @layer theme,base;@layer components{.aqvs-scroll-pane{display:flex;position:relative}.aqvs-scroll-pane[data-self-measured=true]{height:100%}.aqvs-scroll-pane-content{box-sizing:border-box;flex:1;height:100%;position:relative;overflow:hidden}.aqvs-tap-scroll-circle{touch-action:none;-webkit-user-select:none;user-select:none;justify-content:center;align-items:center;transition-property:transform;transition-duration:.1s;transition-timing-function:cubic-bezier(0,0,.2,1);display:flex;position:relative}.aqvs-tap-scroll-circle-visual-outer{border-radius:9999px;position:absolute;inset:0}.aqvs-tap-scroll-circle-gradient{background:linear-gradient(to bottom right,#1d4ed899,#60a5fa8c,#bfdbfe66);border:1px solid #fff6;border-radius:9999px;position:absolute;inset:0;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-tap-scroll-circle-inner{border-radius:9999px;position:absolute;inset:18%}.aqvs-sample-visual-pupil{box-sizing:border-box;background-color:#ffffffd9;border:1px solid #ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-highlight{background-color:#fffc;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-sample-visual-rod{background-color:#ffffff80;border-radius:9999px;position:absolute;top:50%;left:50%}.aqvs-scrollbar{z-index:50;cursor:default;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;touch-action:none;background-color:#fff;position:relative}.aqvs-scrollbar-horizontal{flex-direction:row;align-items:stretch;display:flex}.aqvs-scrollbar-vertical{flex-direction:column;align-items:stretch;display:flex}.aqvs-scrollbar[data-visible=false],.aqvs-scrollbar-thumb-wrapper[data-visible=false]{opacity:0;pointer-events:none}.aqvs-scrollbar-tap-circle-wrapper{pointer-events:auto;transition-property:opacity;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);position:absolute}.aqvs-scrollbar-arrow-button{color:#313131;letter-spacing:normal;background-color:#e0e0e0;border:0 solid;justify-content:center;align-items:center;padding:0;font-family:system-ui,sans-serif;font-size:.75rem;font-weight:400;line-height:1rem;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:flex}.aqvs-scrollbar-arrow-button:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scrollbar-arrow-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.aqvs-scrollbar-arrow-button:disabled{cursor:not-allowed;opacity:.5}.aqvs-scrollbar-arrow-button:enabled:hover{background-color:#d4d4d4}.aqvs-scrollbar-track{background-color:#f5f5f5;flex:1;position:relative}.aqvs-scrollbar-overlay{pointer-events:none;position:absolute;inset:0}.aqvs-scrollbar-thumb-wrapper{touch-action:none;position:absolute}.aqvs-scrollbar-thumb{transform-origin:50%;background-color:#7f7f7f;transition:transform 80ms ease-out;position:absolute}.aqvs-scrollbar-thumb[data-thumb-state=disabled]{transition:none;transform:none}.aqvs-scrollbar-thumb[data-thumb-state=hover]{background-color:#5f5f5f}.aqvs-scrollbar-thumb[data-thumb-state=dragging]{background-color:#4f4f4f;transition:transform 60ms ease-out}.aqvs-scrollbar-thumb-horizontal{inset:1.5px 0}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=hover]{top:-.5px;bottom:-.5px;transform:scaleY(1.06)}.aqvs-scrollbar-thumb-horizontal[data-thumb-state=dragging]{top:-.5px;bottom:-.5px;transform:scaleY(1.12)}.aqvs-scrollbar-thumb-vertical{inset:0 1.5px}.aqvs-scrollbar-thumb-vertical[data-thumb-state=hover]{left:-.5px;right:-.5px;transform:scaleX(1.06)}.aqvs-scrollbar-thumb-vertical[data-thumb-state=dragging]{left:-.5px;right:-.5px;transform:scaleX(1.12)}.aqvs-scroll-to-edge-button{pointer-events:auto;cursor:pointer;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent;color:#fff;text-transform:uppercase;letter-spacing:.05em;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background-color:#1f2937cc;border:0 solid;border-radius:9999px;padding:.25rem 3rem;font-family:inherit;font-size:10px;font-weight:500;line-height:1.5;transition-property:transform,background-color;transition-duration:.15s;transform:none;box-shadow:0 4px 6px -1px #0000001a,0 2px 4px -1px #0000000f}.aqvs-scroll-to-edge-button:focus{outline-offset:2px;outline:2px solid #0000}.aqvs-scroll-to-edge-button:focus-visible{outline-offset:1px;outline:2px solid #60a5fa}.aqvs-scroll-to-edge-button:hover{background-color:#374151}.aqvs-scroll-to-edge-button:active{transform:scale(.95)}.aqvs-scroll-to-edge-overlay{pointer-events:none;z-index:10;transition-property:opacity;transition-duration:.5s;position:absolute;inset:0}.aqvs-scroll-to-edge-overlay[data-visible=true]{opacity:1}.aqvs-scroll-to-edge-overlay[data-visible=false]{opacity:0}.aqvs-scroll-to-edge-overlay[data-visible=false] .aqvs-scroll-to-edge-button{pointer-events:none}.aqvs-scroll-to-edge-button-container{justify-content:center;display:flex;position:absolute;left:0;right:0}.aqvs-scroll-to-edge-button-container-top{top:.5rem}.aqvs-scroll-to-edge-button-container-bottom{bottom:.5rem}.aqvs-no-items-container{width:100%;position:absolute;top:0}.aqvs-no-items-text{text-align:center;color:#6b7280}.aqvs-item-container,.aqvs-bottom-inset,.aqvs-items-wrapper{width:100%;position:absolute}}@layer utilities;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiquants/virtualscroll",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "High-performance virtual scrolling component for React with variable item heights",
5
5
  "sideEffects": [
6
6
  "**/*.css"
@@ -1,4 +1,4 @@
1
- import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef } from "react"
1
+ import { forwardRef, useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react"
2
2
  import { twMerge } from "tailwind-merge"
3
3
  import { Logger } from "./logger.ts"
4
4
  import { ScrollBar, type ScrollBarTapCircleOptions, type ScrollBarThumbOverlayRenderProps, TAP_SCROLL_CANCEL_EVENT } from "./ScrollBar.tsx"
@@ -25,8 +25,32 @@ export type ScrollPaneProps = {
25
25
  children: (scrollPosition: number) => React.ReactNode
26
26
  /** The total size of the content. / コンテンツの総サイズ。 */
27
27
  contentSize: number
28
- /** The size of the visible area. / 表示領域のサイズ。 */
29
- viewportSize: number
28
+ /**
29
+ * Size of the visible band (px). Omit it to let the pane measure its own band.
30
+ * 可視帯の高さ (px)。省略するとペインが自分の帯を計測する。
31
+ *
32
+ * ❗ **省略が既定の使い方である。** この値は帯のクリップ窓・最大スクロール位置・つまみ写像・
33
+ * 描画枚数・スクロール可否のすべての基準であり、実際の帯と食い違うと 5 通りの壊れ方が
34
+ * **例外も警告も出さずに**発生する (はみ出し / 死に帯 / 末尾へ到達不能 / つまみが視野外へ /
35
+ * ホイールとドラッグの無効化)。自分で測れるものを消費側に渡させると、同じ正解を各画面で
36
+ * 書き直すことになり、実際に推測値や定数が代入される。
37
+ *
38
+ * 省略時は `.aqvs-scroll-pane-content` の実高さ (CSS の `height: 100%` が決める) を
39
+ * `ResizeObserver` で追跡する。**ホスト側がルート要素の高さを確定させること**が前提で
40
+ * (`h-full` / `flex-1` + `min-h-0` / 明示 px / `position: absolute` など)、高さが auto の
41
+ * ホストでは帯が 0 のままになる (開発ビルドで警告する)。
42
+ *
43
+ * 明示する正当なケースは「帯の高さを計測ではなく算出で知っている」場合のみである
44
+ * (例: ドロップダウンが件数 × 行高から高さを決める)。
45
+ */
46
+ viewportSize?: number
47
+ /**
48
+ * Notified when the self-measured viewport size changes (not called while `viewportSize` is given).
49
+ * 自己計測したビューポート高さが変化したときに通知する (`viewportSize` 指定時は呼ばれない)。
50
+ *
51
+ * 帯の数値を必要とする親 (`VirtualScroll` は描画枚数とアライン計算に使う) がこの口で受け取る。
52
+ */
53
+ onViewportSizeChange?: (viewportSize: number) => void
30
54
  /** The width of the scrollbar. / スクロールバーの幅。 */
31
55
  scrollBarWidth?: number
32
56
  /** Whether grabbing the scrollbar thumb is allowed. / スクロールバーのつまみ操作を許可するかどうか。 */
@@ -224,7 +248,8 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
224
248
  {
225
249
  children,
226
250
  contentSize,
227
- viewportSize,
251
+ viewportSize: viewportSizeProp,
252
+ onViewportSizeChange,
228
253
  scrollBarWidth = 12,
229
254
  enableThumbDrag = true,
230
255
  enableTrackClick = true,
@@ -256,6 +281,140 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
256
281
  const scrollPositionRef = useRef(Number.isFinite(initialScrollPosition) ? Math.max(0, initialScrollPosition) : 0)
257
282
  const scrollContainerRef = useRef<HTMLDivElement>(null)
258
283
  const contentAreaRef = useRef<HTMLDivElement>(null)
284
+
285
+ // viewportSize 未指定なら自分の帯を計測する。以降のすべての計算はこの解決済み値 (viewportSize) を使う。
286
+ // ❗ ScrollPane が持つ唯一の State であり、変化するのは**ホストの帯が変わったときだけ**である
287
+ // (スクロール中には一切変化しない)。ホットパスで setState しないという設計 (§1.1) は保たれる。
288
+ const isSelfMeasuringViewport = viewportSizeProp === undefined
289
+ const [measuredViewportSize, setMeasuredViewportSize] = useState(0)
290
+ const viewportSize = viewportSizeProp ?? measuredViewportSize
291
+ // 同一内容の警告を繰り返さないための最後の警告キー。
292
+ const lastViewportWarningRef = useRef<string | null>(null)
293
+
294
+ /**
295
+ * Tracks the real height of the content band so the pane can size itself.
296
+ * ペインが自分の帯の実高さを追跡し、自己サイズ決定できるようにする副作用。
297
+ *
298
+ * 計測対象は**コンテンツ要素**である。自己計測モードではこの要素へ高さのインラインスタイルを
299
+ * 書かず、CSS の `height: 100%` (= ルートの確定高さ) がレイアウトを所有するため、
300
+ * 「計測した数値を書き戻して自分のサイズを変える」フィードバックループが構造的に起こらない。
301
+ * ルート要素を測ると、消費側がルートへ付けた padding の分だけ帯を過大評価してしまう。
302
+ *
303
+ * ❗ **0 の計測は採用しない。** `display: none` のタブに置かれている等でレイアウトされて
304
+ * いないとき `offsetHeight` は 0 になる。0 を採ると `isScrollable` が偽になり、サイズ調整
305
+ * エフェクト (§9) が `scrollTo(0)` を発行して**タブ切替でスクロール位置が失われる**。
306
+ * 0 は「帯が 0」ではなく「まだ測れない」と解釈し、直前の値を保つ。
307
+ *
308
+ * 目的: 自己計測モードで帯の高さを State へ反映する。
309
+ * 依存関係: [contentSize, isSelfMeasuringViewport]
310
+ * クリーンアップ: ResizeObserver を切断する。
311
+ */
312
+ useLayoutEffect(() => {
313
+ if (!isSelfMeasuringViewport) {
314
+ return
315
+ }
316
+ const element = contentAreaRef.current
317
+ if (!element) {
318
+ return
319
+ }
320
+ // ResizeObserver の通知は layout の後・paint の前に届くため、ここでの読み取りは強制リフローにならない。
321
+ // transform: scale(...) の下でもレイアウト px を得るため rect ではなく offsetHeight を使う (整数丸め)。
322
+ const applyMeasuredSize = () => {
323
+ const measured = element.offsetHeight
324
+ if (measured > 0) {
325
+ setMeasuredViewportSize((previous) => (previous === measured ? previous : measured))
326
+ return
327
+ }
328
+ // offsetParent が null なら display: none 等で非表示 = 正常な未計測状態なので黙る。
329
+ // レイアウトされているのに 0 の場合はホストが高さを確定させていない (自己計測の前提破り)。
330
+ if (element.offsetParent !== null && contentSize > 0 && lastViewportWarningRef.current !== "zero") {
331
+ lastViewportWarningRef.current = "zero"
332
+ Logger.warn("[ScrollPane] The self-measured viewport is 0px. Give the scroll pane's host element a definite height (e.g. h-full, flex-1 + min-h-0, an explicit px height), or pass viewportSize explicitly.")
333
+ }
334
+ }
335
+ applyMeasuredSize()
336
+ if (typeof ResizeObserver !== "function") {
337
+ return
338
+ }
339
+ const observer = new ResizeObserver(applyMeasuredSize)
340
+ observer.observe(element)
341
+ return () => {
342
+ observer.disconnect()
343
+ }
344
+ }, [contentSize, isSelfMeasuringViewport])
345
+
346
+ /**
347
+ * Publishes the self-measured viewport size to the owner.
348
+ * 自己計測したビューポート高さを所有者へ通知する副作用。
349
+ *
350
+ * ❗ layout effect である。`VirtualScroll` は描画枚数とアライン計算にこの数値を使うため、
351
+ * paint 前に通知しないと初回フレームだけ 1 行しか描かれない見た目になる。
352
+ *
353
+ * 目的: 親が帯の数値を保持できるようにする。
354
+ * 依存関係: [isSelfMeasuringViewport, measuredViewportSize, onViewportSizeChange]
355
+ * クリーンアップ: 不要。
356
+ */
357
+ useLayoutEffect(() => {
358
+ if (!isSelfMeasuringViewport) {
359
+ return
360
+ }
361
+ onViewportSizeChange?.(measuredViewportSize)
362
+ }, [isSelfMeasuringViewport, measuredViewportSize, onViewportSizeChange])
363
+
364
+ /**
365
+ * Warns when an explicitly passed viewportSize disagrees with the host's real band.
366
+ * 明示された viewportSize がホストの実際の帯と食い違うときに警告する副作用。
367
+ *
368
+ * ❗ 食い違いは**例外も視覚的な破綻もなく**進行し、はみ出し / 死に帯 / 末尾へ到達不能 /
369
+ * つまみが視野外 / ホイールとドラッグの無効化を引き起こす。検知できるのはここだけである。
370
+ *
371
+ * 比較対象はルート要素の `clientHeight` である。コンテンツ要素は明示モードでは
372
+ * `height: viewportSize` を書かれているため測っても同義反復になる。ホストが高さを
373
+ * 確定させていない (auto) 場合はルートも `viewportSize` に一致するので警告は出ない
374
+ * — その場合は食い違い自体が存在しないため正しい。
375
+ *
376
+ * `Logger.warn` を無条件に使う (NODE_ENV で切らない): 単一ビルドを配布しており
377
+ * `process.env` の置換は消費側のバンドラ任せで信頼できないうえ、既存の誤用警告
378
+ * (`useLruCache` の無効容量、`scrollTo` の非有限値) と同じ扱いに揃えるため。
379
+ *
380
+ * 目的: 帯との食い違いを検知して通知する。
381
+ * 依存関係: [isSelfMeasuringViewport, viewportSize]
382
+ * クリーンアップ: ResizeObserver を切断する。
383
+ */
384
+ useLayoutEffect(() => {
385
+ if (isSelfMeasuringViewport) {
386
+ return
387
+ }
388
+ const element = scrollContainerRef.current
389
+ if (!element) {
390
+ return
391
+ }
392
+ const warnOnMismatch = () => {
393
+ const actual = element.clientHeight
394
+ // 0 は非表示 (display: none 等) の未計測状態なので比較しない。
395
+ if (actual <= 0 || Math.abs(actual - viewportSize) <= 1) {
396
+ return
397
+ }
398
+ const key = `${viewportSize}/${actual}`
399
+ if (lastViewportWarningRef.current === key) {
400
+ return
401
+ }
402
+ lastViewportWarningRef.current = key
403
+ Logger.warn(
404
+ `[ScrollPane] viewportSize=${viewportSize} but the host band is ${actual}px. The clip window, max scroll position, thumb mapping, rendered row count and scrollability all derive from viewportSize, so they are now wrong. Omit viewportSize to let the pane measure itself.`,
405
+ )
406
+ }
407
+ warnOnMismatch()
408
+ if (typeof ResizeObserver !== "function") {
409
+ return
410
+ }
411
+ const observer = new ResizeObserver(warnOnMismatch)
412
+ observer.observe(element)
413
+ return () => {
414
+ observer.disconnect()
415
+ }
416
+ }, [isSelfMeasuringViewport, viewportSize])
417
+
259
418
  // 横委譲コールバックは ref 経由で参照し、wheel リスナの再登録を避ける。
260
419
  const onWheelHorizontalRef = useRef(onWheelHorizontal)
261
420
  useEffect(() => {
@@ -1225,8 +1384,14 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
1225
1384
  [scrollTo, stopInertia],
1226
1385
  )
1227
1386
 
1387
+ // ❗ 自己計測モードではルートに `data-self-measured` を出し、パッケージ CSS の
1388
+ // `.aqvs-scroll-pane[data-self-measured="true"] { height: 100% }` でホストの帯まで伸ばす。
1389
+ // 明示モードでは content のインライン高さがルートを押し広げるためルート自身に高さは不要だが、
1390
+ // 高さを書かない自己計測モードでは「ルート auto → content の `height: 100%` が auto へ解決
1391
+ // → 帯 0」に潰れる (実ブラウザで実測)。インラインで書かず data 属性 + クラスにするのは、
1392
+ // 消費側が CSS で上書きできる状態を保つため (状態は data-* とパッケージ CSS で表現する方針)。
1228
1393
  return (
1229
- <div ref={scrollContainerRef} data-testid={testId} className={twMerge("aqvs-scroll-pane", className)} style={style}>
1394
+ <div ref={scrollContainerRef} data-testid={testId} data-self-measured={isSelfMeasuringViewport ? "true" : undefined} className={twMerge("aqvs-scroll-pane", className)} style={style}>
1230
1395
  <div
1231
1396
  ref={contentAreaRef}
1232
1397
  className={twMerge("aqvs-scroll-pane-content")}
@@ -1235,7 +1400,11 @@ export const ScrollPane = forwardRef<ScrollPaneHandle, ScrollPaneProps>(
1235
1400
  // コンテンツ領域には prop が無いため、パッケージ側で固定値を出す
1236
1401
  data-testid="aqvs-scroll-pane-content"
1237
1402
  style={{
1238
- height: viewportSize,
1403
+ // ❗ 自己計測モードでは高さを書かない。CSS の `height: 100%` (= ルートの確定高さ) が
1404
+ // レイアウトを所有することで、「計測した値を書き戻して自分のサイズを変える」
1405
+ // フィードバックループが構造的に起こらず、初回 paint の時点でも帯は実寸で正しい
1406
+ // (数値の反映だけが 1 コミット遅れる)。明示モードは従来どおり px を書く。
1407
+ height: isSelfMeasuringViewport ? undefined : viewportSize,
1239
1408
  paddingTop: resolvedInsets.top,
1240
1409
  paddingBottom: resolvedInsets.bottom,
1241
1410
  // スクロール不能時 (contentSize <= viewportSize) は touch-action を通常に戻し、
@@ -151,7 +151,21 @@ export type VirtualScrollProps<T> = {
151
151
  * さもないとキャッシュ済み高さが別のアイテムを指すインデックスに残る。
152
152
  */
153
153
  getItemHeight: (index: number) => number
154
- viewportSize: number
154
+ /**
155
+ * Size of the visible band (px). Omit it to let the component measure its own band.
156
+ * 可視帯の高さ (px)。省略するとコンポーネントが自分の帯を計測する。
157
+ *
158
+ * ❗ **省略が既定の使い方である。** この値は帯のクリップ窓・最大スクロール位置・つまみ写像・
159
+ * 描画枚数・スクロール可否のすべての基準であり、実際の帯と食い違うと 5 通りの壊れ方が
160
+ * **例外も警告も出さずに**発生する。自分で測れるものを消費側に渡させると、同じ正解を各画面で
161
+ * 書き直すことになり、実際に推測値や定数が代入される (詳細は `ScrollPaneProps.viewportSize`)。
162
+ *
163
+ * 省略時はホスト要素が高さを確定させていること (`h-full` / `flex-1` + `min-h-0` / 明示 px など)
164
+ * が前提で、高さが auto のホストでは帯が 0 のままになる (`Logger.warn` で警告する)。
165
+ * 明示する正当なケースは「帯の高さを計測ではなく算出で知っている」場合のみである
166
+ * (例: ドロップダウンが件数 × 行高から高さを決める)。
167
+ */
168
+ viewportSize?: number
155
169
  overscanCount?: number
156
170
  className?: string
157
171
  /** Test id emitted as data-testid on the scroll root (DOM hooks must use data-* attributes, never class selectors). / スクロールルートに data-testid として出力されるテスト ID (DOM フックはクラスセレクタでなく data-* 属性を使う)。 */
@@ -736,7 +750,7 @@ const VirtualScrollInner = <T,>(
736
750
  getItem,
737
751
  getItemKey,
738
752
  getItemHeight,
739
- viewportSize,
753
+ viewportSize: viewportSizeProp,
740
754
  overscanCount = 15,
741
755
  className,
742
756
  testId,
@@ -761,6 +775,16 @@ const VirtualScrollInner = <T,>(
761
775
 
762
776
  const { enablePointerDrag, pointerDragInputs, enableKeyboardNavigation = true, wheelSpeedMultiplier, inertiaOptions, clipItemHeight = false, resetOnGetItemHeightChange = false } = behaviorOptions ?? {}
763
777
 
778
+ // viewportSize 未指定のときは ScrollPane が自分の帯を計測し、その値をこのコールバックで通知する。
779
+ // 描画枚数 (computeRenderingRanges) とアライン計算 (scrollToIndex / ドリフト補正) はこの解決済み値を使う。
780
+ // 初回コミットは 0 だが ScrollPane が paint 前 (layout effect) に通知するためちらつきは無く、
781
+ // 帯が landed した時点でドリフト補正 effect (viewportSize を依存に持つ) がアンカー位置を再計算する。
782
+ const [measuredViewportSize, setMeasuredViewportSize] = useState(0)
783
+ const viewportSize = viewportSizeProp ?? measuredViewportSize
784
+ const handleViewportSizeChange = useCallback((nextViewportSize: number) => {
785
+ setMeasuredViewportSize((previous) => (previous === nextViewportSize ? previous : nextViewportSize))
786
+ }, [])
787
+
764
788
  // ❗ 内部ペイン ref の型は ScrollPaneHandle (ペイン座標)。VirtualScrollHandle と誤記すると
765
789
  // 2.0.0 以降は型ドキュメントが「論理座標を返す」と嘘をつく (構造的互換で型検査は通ってしまう)
766
790
  const scrollPaneRef = useRef<ScrollPaneHandle>(null)
@@ -2065,7 +2089,8 @@ const VirtualScrollInner = <T,>(
2065
2089
  <ScrollPane
2066
2090
  ref={scrollPaneRef}
2067
2091
  contentSize={totalContentHeight}
2068
- viewportSize={viewportSize}
2092
+ viewportSize={viewportSizeProp}
2093
+ onViewportSizeChange={handleViewportSizeChange}
2069
2094
  className={className}
2070
2095
  testId={testId}
2071
2096
  onScroll={handleScroll}
@@ -3,6 +3,13 @@
3
3
  display: flex;
4
4
  }
5
5
 
6
+ /* 自己計測モード (viewportSize 省略) のときだけルートをホストの帯まで伸ばす。
7
+ 明示モードでは content のインライン height がルートを押し広げるため不要で、
8
+ ここで無条件に伸ばすと既存消費者のルート要素の箱が変わってしまう。 */
9
+ .aqvs-scroll-pane[data-self-measured="true"] {
10
+ height: 100%;
11
+ }
12
+
6
13
  .aqvs-scroll-pane-content {
7
14
  box-sizing: border-box;
8
15
  position: relative;