feat(credits): solde IA unique (option M), packs Stripe et polish billing
Un pot de crédits global (BASIC 100 / PRO 1 000 / BUSINESS 4 000) remplace les seaux par fonction côté débit. Packs one-shot S/M/L, admin sans seaux legacy, usage multi-features, hints de coût, anti-flash thème et hydratation admin. Inclut aussi le pipeline slides renforcé et la page publique polishée.
This commit is contained in:
77
memento-note/components/publish/reading-progress.tsx
Normal file
77
memento-note/components/publish/reading-progress.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
/**
|
||||
* Thin reading progress bar. Tracks the nearest scrollable ancestor
|
||||
* (public layout uses a fixed-height overflow container, not window).
|
||||
*/
|
||||
export function ReadingProgress({ accent = '#A47148' }: { accent?: string }) {
|
||||
const [progress, setProgress] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
const findScrollRoot = (): HTMLElement | Window => {
|
||||
let el: HTMLElement | null = document.querySelector('[data-pub-root]') as HTMLElement | null
|
||||
while (el) {
|
||||
const { overflowY } = getComputedStyle(el)
|
||||
if (overflowY === 'auto' || overflowY === 'scroll') return el
|
||||
el = el.parentElement
|
||||
}
|
||||
return window
|
||||
}
|
||||
|
||||
const root = findScrollRoot()
|
||||
|
||||
const update = () => {
|
||||
let scrollTop: number
|
||||
let scrollHeight: number
|
||||
let clientHeight: number
|
||||
if (root === window) {
|
||||
scrollTop = window.scrollY
|
||||
scrollHeight = document.documentElement.scrollHeight
|
||||
clientHeight = window.innerHeight
|
||||
} else {
|
||||
const node = root as HTMLElement
|
||||
scrollTop = node.scrollTop
|
||||
scrollHeight = node.scrollHeight
|
||||
clientHeight = node.clientHeight
|
||||
}
|
||||
const max = Math.max(1, scrollHeight - clientHeight)
|
||||
setProgress(Math.min(100, Math.max(0, (scrollTop / max) * 100)))
|
||||
}
|
||||
|
||||
update()
|
||||
root.addEventListener('scroll', update, { passive: true })
|
||||
window.addEventListener('resize', update, { passive: true })
|
||||
return () => {
|
||||
root.removeEventListener('scroll', update)
|
||||
window.removeEventListener('resize', update)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 3,
|
||||
zIndex: 100,
|
||||
background: 'transparent',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: '100%',
|
||||
width: `${progress}%`,
|
||||
background: accent,
|
||||
boxShadow: `0 0 12px ${accent}66`,
|
||||
transition: 'width 80ms linear',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user