@nastechai-research/ui 0.13.2
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.
- package/CHANGELOG.md +5 -0
- package/README.md +21 -0
- package/dist/assets/filler-bg0.webp +0 -0
- package/dist/assets.d.ts +38 -0
- package/dist/fonts/Collapse-Bold.woff2 +0 -0
- package/dist/fonts/Collapse-BoldItalic.woff2 +0 -0
- package/dist/fonts/Collapse-Italic.woff2 +0 -0
- package/dist/fonts/Collapse-Light.woff2 +0 -0
- package/dist/fonts/Collapse-LightItalic.woff2 +0 -0
- package/dist/fonts/Collapse-Regular.woff2 +0 -0
- package/dist/fonts/Collapse-Thin.woff2 +0 -0
- package/dist/fonts/Collapse-ThinItalic.woff2 +0 -0
- package/dist/fonts/Mondwest-Regular.woff2 +0 -0
- package/dist/fonts/Neuebit-Bold.woff2 +0 -0
- package/dist/fonts/RulesCompressed-Medium.woff2 +0 -0
- package/dist/fonts/RulesCompressed-Regular.woff2 +0 -0
- package/dist/fonts/RulesExpanded-Bold.woff2 +0 -0
- package/dist/fonts/RulesExpanded-Regular.woff2 +0 -0
- package/dist/fonts.d.ts +6 -0
- package/dist/fonts.js +6 -0
- package/dist/hooks/use-below-breakpoint.d.ts +2 -0
- package/dist/hooks/use-below-breakpoint.js +17 -0
- package/dist/hooks/use-capped-frame.d.ts +2 -0
- package/dist/hooks/use-capped-frame.js +15 -0
- package/dist/hooks/use-confirm-delete.d.ts +10 -0
- package/dist/hooks/use-confirm-delete.js +35 -0
- package/dist/hooks/use-css-var-dims.d.ts +1 -0
- package/dist/hooks/use-css-var-dims.js +29 -0
- package/dist/hooks/use-gpu-tier.d.ts +34 -0
- package/dist/hooks/use-gpu-tier.js +111 -0
- package/dist/hooks/use-render-loop.d.ts +41 -0
- package/dist/hooks/use-render-loop.js +63 -0
- package/dist/hooks/use-smooth-controls.d.ts +16 -0
- package/dist/hooks/use-smooth-controls.js +217 -0
- package/dist/hooks/use-toast.d.ts +7 -0
- package/dist/hooks/use-toast.js +21 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +107 -0
- package/dist/ui/basic-page.d.ts +7 -0
- package/dist/ui/basic-page.js +18 -0
- package/dist/ui/build.css +4 -0
- package/dist/ui/components/animated-count.d.ts +10 -0
- package/dist/ui/components/animated-count.js +113 -0
- package/dist/ui/components/ascii.d.ts +10 -0
- package/dist/ui/components/ascii.js +79 -0
- package/dist/ui/components/badge.d.ts +6 -0
- package/dist/ui/components/badge.js +40 -0
- package/dist/ui/components/badges/nous-girl.d.ts +2 -0
- package/dist/ui/components/badges/nous-girl.js +83 -0
- package/dist/ui/components/blend-mode.d.ts +28 -0
- package/dist/ui/components/blend-mode.js +69 -0
- package/dist/ui/components/blink.d.ts +6 -0
- package/dist/ui/components/blink.js +17 -0
- package/dist/ui/components/bottom-sheet.d.ts +15 -0
- package/dist/ui/components/bottom-sheet.js +192 -0
- package/dist/ui/components/button.d.ts +14 -0
- package/dist/ui/components/button.js +147 -0
- package/dist/ui/components/card.d.ts +5 -0
- package/dist/ui/components/card.js +74 -0
- package/dist/ui/components/checkbox.d.ts +2 -0
- package/dist/ui/components/checkbox.js +27 -0
- package/dist/ui/components/command-block.d.ts +24 -0
- package/dist/ui/components/command-block.js +56 -0
- package/dist/ui/components/confirm-dialog.d.ts +13 -0
- package/dist/ui/components/confirm-dialog.js +113 -0
- package/dist/ui/components/cursor.d.ts +3 -0
- package/dist/ui/components/cursor.js +97 -0
- package/dist/ui/components/dialog.d.ts +15 -0
- package/dist/ui/components/dialog.js +171 -0
- package/dist/ui/components/dropdown-menu.d.ts +12 -0
- package/dist/ui/components/dropdown-menu.js +102 -0
- package/dist/ui/components/fit-text/fit-text.css +42 -0
- package/dist/ui/components/fit-text/index.d.ts +9 -0
- package/dist/ui/components/fit-text/index.js +25 -0
- package/dist/ui/components/graphs/bar-chart.d.ts +12 -0
- package/dist/ui/components/graphs/bar-chart.js +129 -0
- package/dist/ui/components/graphs/index.d.ts +3 -0
- package/dist/ui/components/graphs/index.js +4 -0
- package/dist/ui/components/graphs/line-chart.d.ts +14 -0
- package/dist/ui/components/graphs/line-chart.js +175 -0
- package/dist/ui/components/graphs/utils.d.ts +52 -0
- package/dist/ui/components/graphs/utils.js +162 -0
- package/dist/ui/components/grid/grid.css +79 -0
- package/dist/ui/components/grid/index.d.ts +2 -0
- package/dist/ui/components/grid/index.js +17 -0
- package/dist/ui/components/hover-bg.d.ts +1 -0
- package/dist/ui/components/hover-bg.js +14 -0
- package/dist/ui/components/icons/arrow.d.ts +6 -0
- package/dist/ui/components/icons/arrow.js +44 -0
- package/dist/ui/components/icons/check.d.ts +2 -0
- package/dist/ui/components/icons/check.js +13 -0
- package/dist/ui/components/icons/chevron.d.ts +6 -0
- package/dist/ui/components/icons/chevron.js +51 -0
- package/dist/ui/components/icons/discord.d.ts +2 -0
- package/dist/ui/components/icons/discord.js +15 -0
- package/dist/ui/components/icons/eye.d.ts +2 -0
- package/dist/ui/components/icons/eye.js +8 -0
- package/dist/ui/components/icons/gear.d.ts +6 -0
- package/dist/ui/components/icons/gear.js +30 -0
- package/dist/ui/components/icons/github.d.ts +2 -0
- package/dist/ui/components/icons/github.js +15 -0
- package/dist/ui/components/icons/hamburger.d.ts +6 -0
- package/dist/ui/components/icons/hamburger.js +56 -0
- package/dist/ui/components/icons/heart.d.ts +2 -0
- package/dist/ui/components/icons/heart.js +11 -0
- package/dist/ui/components/icons/index.d.ts +12 -0
- package/dist/ui/components/icons/index.js +13 -0
- package/dist/ui/components/icons/link.d.ts +2 -0
- package/dist/ui/components/icons/link.js +13 -0
- package/dist/ui/components/icons/minus.d.ts +2 -0
- package/dist/ui/components/icons/minus.js +13 -0
- package/dist/ui/components/icons/search.d.ts +2 -0
- package/dist/ui/components/icons/search.js +33 -0
- package/dist/ui/components/image-distortion.d.ts +21 -0
- package/dist/ui/components/image-distortion.js +398 -0
- package/dist/ui/components/input.d.ts +1 -0
- package/dist/ui/components/input.js +21 -0
- package/dist/ui/components/label.d.ts +1 -0
- package/dist/ui/components/label.js +18 -0
- package/dist/ui/components/leva-client.d.ts +1 -0
- package/dist/ui/components/leva-client.js +12 -0
- package/dist/ui/components/list-item.d.ts +6 -0
- package/dist/ui/components/list-item.js +27 -0
- package/dist/ui/components/overlays/blend-modes.d.ts +1 -0
- package/dist/ui/components/overlays/blend-modes.js +14 -0
- package/dist/ui/components/overlays/glitch.d.ts +6 -0
- package/dist/ui/components/overlays/glitch.js +209 -0
- package/dist/ui/components/overlays/greys.d.ts +6 -0
- package/dist/ui/components/overlays/greys.js +339 -0
- package/dist/ui/components/overlays/index.d.ts +14 -0
- package/dist/ui/components/overlays/index.js +34 -0
- package/dist/ui/components/overlays/lens-layers.d.ts +14 -0
- package/dist/ui/components/overlays/lens-layers.js +95 -0
- package/dist/ui/components/overlays/lens.d.ts +44 -0
- package/dist/ui/components/overlays/lens.js +60 -0
- package/dist/ui/components/overlays/noise.d.ts +6 -0
- package/dist/ui/components/overlays/noise.js +136 -0
- package/dist/ui/components/overlays/vignette.d.ts +6 -0
- package/dist/ui/components/overlays/vignette.js +47 -0
- package/dist/ui/components/poster.d.ts +62 -0
- package/dist/ui/components/poster.js +256 -0
- package/dist/ui/components/progress.d.ts +9 -0
- package/dist/ui/components/progress.js +53 -0
- package/dist/ui/components/scene-canvas.d.ts +23 -0
- package/dist/ui/components/scene-canvas.js +179 -0
- package/dist/ui/components/scramble.d.ts +9 -0
- package/dist/ui/components/scramble.js +63 -0
- package/dist/ui/components/segmented.d.ts +20 -0
- package/dist/ui/components/segmented.js +51 -0
- package/dist/ui/components/select.d.ts +18 -0
- package/dist/ui/components/select.js +215 -0
- package/dist/ui/components/selection-switcher.d.ts +1 -0
- package/dist/ui/components/selection-switcher.js +34 -0
- package/dist/ui/components/separator.d.ts +5 -0
- package/dist/ui/components/separator.js +22 -0
- package/dist/ui/components/shader.d.ts +7 -0
- package/dist/ui/components/shader.js +60 -0
- package/dist/ui/components/socials.d.ts +20 -0
- package/dist/ui/components/socials.js +21 -0
- package/dist/ui/components/spinner.d.ts +20 -0
- package/dist/ui/components/spinner.js +38 -0
- package/dist/ui/components/stats.d.ts +16 -0
- package/dist/ui/components/stats.js +36 -0
- package/dist/ui/components/switch.d.ts +7 -0
- package/dist/ui/components/switch.js +37 -0
- package/dist/ui/components/tabs.d.ts +14 -0
- package/dist/ui/components/tabs.js +44 -0
- package/dist/ui/components/terminal-demo.d.ts +32 -0
- package/dist/ui/components/terminal-demo.js +125 -0
- package/dist/ui/components/theme-toggle.d.ts +6 -0
- package/dist/ui/components/theme-toggle.js +66 -0
- package/dist/ui/components/tier-card.d.ts +53 -0
- package/dist/ui/components/tier-card.js +146 -0
- package/dist/ui/components/toast.d.ts +8 -0
- package/dist/ui/components/toast.js +39 -0
- package/dist/ui/components/tv.d.ts +3 -0
- package/dist/ui/components/tv.js +239 -0
- package/dist/ui/components/typography/h1.d.ts +11 -0
- package/dist/ui/components/typography/h1.js +18 -0
- package/dist/ui/components/typography/h2.d.ts +11 -0
- package/dist/ui/components/typography/h2.js +18 -0
- package/dist/ui/components/typography/index.d.ts +15 -0
- package/dist/ui/components/typography/index.js +41 -0
- package/dist/ui/components/typography/legend.d.ts +6 -0
- package/dist/ui/components/typography/legend.js +20 -0
- package/dist/ui/components/typography/small.d.ts +2 -0
- package/dist/ui/components/typography/small.js +9 -0
- package/dist/ui/components/watchlist.d.ts +11 -0
- package/dist/ui/components/watchlist.js +80 -0
- package/dist/ui/fonts.css +63 -0
- package/dist/ui/footer.d.ts +20 -0
- package/dist/ui/footer.js +65 -0
- package/dist/ui/globals.css +395 -0
- package/dist/ui/header.d.ts +41 -0
- package/dist/ui/header.js +270 -0
- package/dist/ui/layout-wrapper.d.ts +1 -0
- package/dist/ui/layout-wrapper.js +7 -0
- package/dist/utils/color.d.ts +4 -0
- package/dist/utils/color.js +14 -0
- package/dist/utils/index.d.ts +15 -0
- package/dist/utils/index.js +48 -0
- package/dist/utils/poly.d.ts +8 -0
- package/dist/utils/poly.js +3 -0
- package/package.json +120 -0
- package/src/assets/filler-bg0.webp +0 -0
- package/src/assets.d.ts +38 -0
- package/src/fonts/Collapse-Bold.woff2 +0 -0
- package/src/fonts/Collapse-BoldItalic.woff2 +0 -0
- package/src/fonts/Collapse-Italic.woff2 +0 -0
- package/src/fonts/Collapse-Light.woff2 +0 -0
- package/src/fonts/Collapse-LightItalic.woff2 +0 -0
- package/src/fonts/Collapse-Regular.woff2 +0 -0
- package/src/fonts/Collapse-Thin.woff2 +0 -0
- package/src/fonts/Collapse-ThinItalic.woff2 +0 -0
- package/src/fonts/Mondwest-Regular.woff2 +0 -0
- package/src/fonts/Neuebit-Bold.woff2 +0 -0
- package/src/fonts/RulesCompressed-Medium.woff2 +0 -0
- package/src/fonts/RulesCompressed-Regular.woff2 +0 -0
- package/src/fonts/RulesExpanded-Bold.woff2 +0 -0
- package/src/fonts/RulesExpanded-Regular.woff2 +0 -0
- package/src/fonts.ts +6 -0
- package/src/hooks/use-below-breakpoint.ts +21 -0
- package/src/hooks/use-capped-frame.ts +18 -0
- package/src/hooks/use-confirm-delete.ts +43 -0
- package/src/hooks/use-css-var-dims.ts +39 -0
- package/src/hooks/use-gpu-tier.ts +190 -0
- package/src/hooks/use-render-loop.ts +121 -0
- package/src/hooks/use-smooth-controls.ts +318 -0
- package/src/hooks/use-toast.ts +29 -0
- package/src/index.ts +130 -0
- package/src/ui/basic-page.tsx +34 -0
- package/src/ui/build.css +4 -0
- package/src/ui/components/animated-count.stories.tsx +67 -0
- package/src/ui/components/animated-count.tsx +168 -0
- package/src/ui/components/ascii.stories.tsx +30 -0
- package/src/ui/components/ascii.tsx +110 -0
- package/src/ui/components/badge.stories.tsx +31 -0
- package/src/ui/components/badge.tsx +60 -0
- package/src/ui/components/badges/nous-girl.tsx +52 -0
- package/src/ui/components/blend-mode.stories.tsx +33 -0
- package/src/ui/components/blend-mode.tsx +129 -0
- package/src/ui/components/blink.stories.tsx +32 -0
- package/src/ui/components/blink.tsx +21 -0
- package/src/ui/components/bottom-sheet.stories.tsx +43 -0
- package/src/ui/components/bottom-sheet.tsx +227 -0
- package/src/ui/components/button.stories.tsx +68 -0
- package/src/ui/components/button.tsx +170 -0
- package/src/ui/components/card.stories.tsx +63 -0
- package/src/ui/components/card.tsx +85 -0
- package/src/ui/components/checkbox.stories.tsx +113 -0
- package/src/ui/components/checkbox.tsx +36 -0
- package/src/ui/components/command-block.stories.tsx +52 -0
- package/src/ui/components/command-block.tsx +86 -0
- package/src/ui/components/confirm-dialog.stories.tsx +91 -0
- package/src/ui/components/confirm-dialog.tsx +130 -0
- package/src/ui/components/cursor.tsx +115 -0
- package/src/ui/components/dialog.stories.tsx +169 -0
- package/src/ui/components/dialog.tsx +177 -0
- package/src/ui/components/dropdown-menu.stories.tsx +52 -0
- package/src/ui/components/dropdown-menu.tsx +117 -0
- package/src/ui/components/fit-text/fit-text.css +42 -0
- package/src/ui/components/fit-text/index.stories.tsx +33 -0
- package/src/ui/components/fit-text/index.tsx +45 -0
- package/src/ui/components/forms.stories.tsx +173 -0
- package/src/ui/components/graphs/bar-chart.tsx +153 -0
- package/src/ui/components/graphs/index.stories.tsx +64 -0
- package/src/ui/components/graphs/index.tsx +4 -0
- package/src/ui/components/graphs/line-chart.tsx +213 -0
- package/src/ui/components/graphs/utils.tsx +265 -0
- package/src/ui/components/grid/grid.css +79 -0
- package/src/ui/components/grid/index.tsx +19 -0
- package/src/ui/components/hover-bg.stories.tsx +29 -0
- package/src/ui/components/hover-bg.tsx +15 -0
- package/src/ui/components/icons/arrow.tsx +42 -0
- package/src/ui/components/icons/check.tsx +14 -0
- package/src/ui/components/icons/chevron.tsx +45 -0
- package/src/ui/components/icons/discord.tsx +16 -0
- package/src/ui/components/icons/eye.tsx +12 -0
- package/src/ui/components/icons/gear.tsx +51 -0
- package/src/ui/components/icons/github.tsx +16 -0
- package/src/ui/components/icons/hamburger.tsx +52 -0
- package/src/ui/components/icons/heart.tsx +12 -0
- package/src/ui/components/icons/index.ts +12 -0
- package/src/ui/components/icons/link.tsx +14 -0
- package/src/ui/components/icons/minus.tsx +14 -0
- package/src/ui/components/icons/search.tsx +28 -0
- package/src/ui/components/image-distortion.stories.tsx +120 -0
- package/src/ui/components/image-distortion.tsx +499 -0
- package/src/ui/components/input.stories.tsx +39 -0
- package/src/ui/components/input.tsx +20 -0
- package/src/ui/components/label.stories.tsx +26 -0
- package/src/ui/components/label.tsx +16 -0
- package/src/ui/components/leva-client.tsx +14 -0
- package/src/ui/components/list-item.stories.tsx +83 -0
- package/src/ui/components/list-item.tsx +37 -0
- package/src/ui/components/overlays/blend-modes.ts +13 -0
- package/src/ui/components/overlays/glitch.tsx +243 -0
- package/src/ui/components/overlays/greys.tsx +386 -0
- package/src/ui/components/overlays/index.tsx +47 -0
- package/src/ui/components/overlays/lens-layers.tsx +121 -0
- package/src/ui/components/overlays/lens.ts +91 -0
- package/src/ui/components/overlays/noise.tsx +174 -0
- package/src/ui/components/overlays/vignette.tsx +60 -0
- package/src/ui/components/poster.stories.tsx +513 -0
- package/src/ui/components/poster.tsx +411 -0
- package/src/ui/components/progress.stories.tsx +48 -0
- package/src/ui/components/progress.tsx +56 -0
- package/src/ui/components/scene-canvas.tsx +254 -0
- package/src/ui/components/scramble.stories.tsx +49 -0
- package/src/ui/components/scramble.tsx +95 -0
- package/src/ui/components/segmented.stories.tsx +101 -0
- package/src/ui/components/segmented.tsx +81 -0
- package/src/ui/components/select.stories.tsx +88 -0
- package/src/ui/components/select.tsx +267 -0
- package/src/ui/components/selection-switcher.tsx +44 -0
- package/src/ui/components/separator.stories.tsx +33 -0
- package/src/ui/components/separator.tsx +24 -0
- package/src/ui/components/shader.tsx +83 -0
- package/src/ui/components/socials.tsx +42 -0
- package/src/ui/components/spinner.stories.tsx +101 -0
- package/src/ui/components/spinner.tsx +60 -0
- package/src/ui/components/stats.stories.tsx +24 -0
- package/src/ui/components/stats.tsx +53 -0
- package/src/ui/components/switch.stories.tsx +77 -0
- package/src/ui/components/switch.tsx +48 -0
- package/src/ui/components/tabs.stories.tsx +101 -0
- package/src/ui/components/tabs.tsx +66 -0
- package/src/ui/components/terminal-demo.stories.tsx +67 -0
- package/src/ui/components/terminal-demo.tsx +189 -0
- package/src/ui/components/theme-toggle.stories.tsx +47 -0
- package/src/ui/components/theme-toggle.tsx +66 -0
- package/src/ui/components/tier-card.stories.tsx +217 -0
- package/src/ui/components/tier-card.tsx +190 -0
- package/src/ui/components/toast.stories.tsx +55 -0
- package/src/ui/components/toast.tsx +49 -0
- package/src/ui/components/tv.stories.tsx +37 -0
- package/src/ui/components/tv.tsx +257 -0
- package/src/ui/components/typography/h1.tsx +18 -0
- package/src/ui/components/typography/h2.tsx +18 -0
- package/src/ui/components/typography/index.tsx +54 -0
- package/src/ui/components/typography/legend.tsx +24 -0
- package/src/ui/components/typography/small.tsx +11 -0
- package/src/ui/components/watchlist.stories.tsx +33 -0
- package/src/ui/components/watchlist.tsx +105 -0
- package/src/ui/fonts.css +63 -0
- package/src/ui/footer.tsx +111 -0
- package/src/ui/globals.css +395 -0
- package/src/ui/header.tsx +398 -0
- package/src/ui/layout-wrapper.tsx +11 -0
- package/src/utils/color.ts +21 -0
- package/src/utils/index.ts +62 -0
- package/src/utils/poly.ts +26 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/fonts.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** CSS variable names for the design language fonts. Set automatically by fonts.css. */
|
|
2
|
+
export const FONT_SANS = '--font-sans'
|
|
3
|
+
export const FONT_MONO = '--font-mono'
|
|
4
|
+
export const FONT_RULES_COMPRESSED = '--font-rules-compressed'
|
|
5
|
+
export const FONT_RULES_EXPANDED = '--font-rules-expanded'
|
|
6
|
+
export const FONT_MONDWEST = '--font-mondwest'
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
/** True when viewport width is strictly below `px`. */
|
|
6
|
+
export function useBelowBreakpoint(px: number) {
|
|
7
|
+
const query = `(max-width: ${px - 1}px)`
|
|
8
|
+
const [matches, setMatches] = useState(() =>
|
|
9
|
+
typeof window !== 'undefined' ? window.matchMedia(query).matches : false
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const mql = window.matchMedia(query)
|
|
14
|
+
const sync = () => setMatches(mql.matches)
|
|
15
|
+
sync()
|
|
16
|
+
mql.addEventListener('change', sync)
|
|
17
|
+
return () => mql.removeEventListener('change', sync)
|
|
18
|
+
}, [query])
|
|
19
|
+
|
|
20
|
+
return matches
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useFrame, useThree } from '@react-three/fiber'
|
|
4
|
+
import type { RenderCallback } from '@react-three/fiber'
|
|
5
|
+
import { useRef } from 'react'
|
|
6
|
+
|
|
7
|
+
export function useCappedFrame(cb: RenderCallback, max?: number) {
|
|
8
|
+
const last = useRef(performance.now())
|
|
9
|
+
const { size } = useThree()
|
|
10
|
+
const interval = 1e3 / (max ?? (size.width < 1024 ? 60 : 120))
|
|
11
|
+
|
|
12
|
+
useFrame((st, delta) => {
|
|
13
|
+
if (performance.now() - last.current > interval) {
|
|
14
|
+
last.current = performance.now()
|
|
15
|
+
cb(st, delta)
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
export function useConfirmDelete<TId>({
|
|
6
|
+
onDelete
|
|
7
|
+
}: {
|
|
8
|
+
onDelete: (id: TId) => Promise<void>
|
|
9
|
+
}) {
|
|
10
|
+
const [pendingId, setPendingId] = useState<TId | null>(null)
|
|
11
|
+
const [isDeleting, setIsDeleting] = useState(false)
|
|
12
|
+
|
|
13
|
+
const requestDelete = useCallback((id: TId) => {
|
|
14
|
+
setPendingId(id)
|
|
15
|
+
}, [])
|
|
16
|
+
|
|
17
|
+
const cancel = useCallback(() => {
|
|
18
|
+
if (!isDeleting) setPendingId(null)
|
|
19
|
+
}, [isDeleting])
|
|
20
|
+
|
|
21
|
+
const confirm = useCallback(async () => {
|
|
22
|
+
if (pendingId === null) return
|
|
23
|
+
const id = pendingId
|
|
24
|
+
setIsDeleting(true)
|
|
25
|
+
try {
|
|
26
|
+
await onDelete(id)
|
|
27
|
+
setPendingId(null)
|
|
28
|
+
} catch {
|
|
29
|
+
// Dialog stays open; caller can surface errors in onDelete
|
|
30
|
+
} finally {
|
|
31
|
+
setIsDeleting(false)
|
|
32
|
+
}
|
|
33
|
+
}, [pendingId, onDelete])
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
cancel,
|
|
37
|
+
confirm,
|
|
38
|
+
isDeleting,
|
|
39
|
+
isOpen: pendingId !== null,
|
|
40
|
+
pendingId,
|
|
41
|
+
requestDelete
|
|
42
|
+
} as const
|
|
43
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react'
|
|
4
|
+
|
|
5
|
+
export function useCssVarDims(
|
|
6
|
+
name: string,
|
|
7
|
+
ref: React.RefObject<HTMLElement | null>
|
|
8
|
+
) {
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (!ref.current) {
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const update = (width: number, height: number) => {
|
|
15
|
+
document.documentElement.style.setProperty(
|
|
16
|
+
`--${name}-width`,
|
|
17
|
+
`${width}px`
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
document.documentElement.style.setProperty(
|
|
21
|
+
`--${name}-height`,
|
|
22
|
+
`${height}px`
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { height, width } = ref.current.getBoundingClientRect()
|
|
27
|
+
update(width, height)
|
|
28
|
+
|
|
29
|
+
const ro = new ResizeObserver(entries => {
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
update(entry.contentRect.width, entry.contentRect.height)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
ro.observe(ref.current)
|
|
36
|
+
|
|
37
|
+
return () => ro.disconnect()
|
|
38
|
+
}, [name, ref])
|
|
39
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useStore } from '@nanostores/react'
|
|
4
|
+
import { atom } from 'nanostores'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tiers:
|
|
8
|
+
* 0 — no WebGL / software renderer / prefers-reduced-motion / WebGL ctx
|
|
9
|
+
* creation failed / detection not yet run
|
|
10
|
+
* 1 — low-end GPU (integrated, mobile, or failed perf benchmark)
|
|
11
|
+
* 2 — capable GPU (discrete / high-end integrated)
|
|
12
|
+
*
|
|
13
|
+
* The atom starts **pessimistic** (`0`) and the probe is scheduled to run
|
|
14
|
+
* *after the first paint* (see `scheduleDetection` below), never synchronously
|
|
15
|
+
* at module-evaluation time.
|
|
16
|
+
*
|
|
17
|
+
* Why pessimistic + deferred:
|
|
18
|
+
* - Every consumer gates its `<canvas>` / `THREE.WebGLRenderer` on `tier > 0`
|
|
19
|
+
* (or `=== 2`). Starting at `0` means a consumer reading `$gpuTier` during
|
|
20
|
+
* its first render never attempts to create a renderer before detection has
|
|
21
|
+
* run, so the `THREE.WebGLRenderer: Error creating WebGL context` crash on
|
|
22
|
+
* hardware where context creation fails cannot happen — the component
|
|
23
|
+
* renders its fallback and upgrades once detection resolves to a capable
|
|
24
|
+
* tier. (A previous version made the probe synchronous-at-module-load to
|
|
25
|
+
* dodge this crash; the pessimistic default removes the crash without that
|
|
26
|
+
* cost.)
|
|
27
|
+
* - Probing WebGL is *expensive* on software renderers: creating a context
|
|
28
|
+
* under SwiftShader / llvmpipe can block the main thread for hundreds of
|
|
29
|
+
* milliseconds. Running it synchronously at module load stalled first paint
|
|
30
|
+
* and produced a visible boot-time flash in apps that merely import this
|
|
31
|
+
* hook (e.g. the Hermes dashboard backdrop). Deferring past first paint
|
|
32
|
+
* keeps boot smooth; the tier just upgrades a frame or two later.
|
|
33
|
+
* - For SSR the server keeps the default `0` and the client's first render
|
|
34
|
+
* also reads `0`, so there is no hydration mismatch.
|
|
35
|
+
*/
|
|
36
|
+
export const $gpuTier = atom<GpuTier>(0)
|
|
37
|
+
|
|
38
|
+
const SOFTWARE_PATTERNS =
|
|
39
|
+
/swiftshader|llvmpipe|softpipe|software|microsoft basic/i
|
|
40
|
+
|
|
41
|
+
const LOW_END_PATTERNS =
|
|
42
|
+
/intel.*hd|intel.*uhd|intel.*iris|mali|adreno\s?[1-5]|powervr|apple gpu/i
|
|
43
|
+
|
|
44
|
+
let detected = false
|
|
45
|
+
|
|
46
|
+
function detectGpuTier() {
|
|
47
|
+
if (detected || typeof window === 'undefined') {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
detected = true
|
|
52
|
+
|
|
53
|
+
// The atom already holds 0; the early returns below simply leave it there.
|
|
54
|
+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let gl: null | WebGLRenderingContext = null
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const canvas = document.createElement('canvas')
|
|
62
|
+
gl = (canvas.getContext('webgl') ||
|
|
63
|
+
canvas.getContext('experimental-webgl')) as null | WebGLRenderingContext
|
|
64
|
+
} catch {
|
|
65
|
+
// Some sandboxed / hardened contexts throw on getContext rather than
|
|
66
|
+
// returning null (e.g. certain corporate browser policies). Treat as
|
|
67
|
+
// "no WebGL available".
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!gl) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const ext = gl.getExtension('WEBGL_debug_renderer_info')
|
|
76
|
+
const renderer = String(
|
|
77
|
+
ext
|
|
78
|
+
? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL)
|
|
79
|
+
: gl.getParameter(gl.RENDERER)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if (SOFTWARE_PATTERNS.test(renderer)) {
|
|
83
|
+
gl.getExtension('WEBGL_lose_context')?.loseContext()
|
|
84
|
+
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (LOW_END_PATTERNS.test(renderer)) {
|
|
89
|
+
$gpuTier.set(1)
|
|
90
|
+
gl.getExtension('WEBGL_lose_context')?.loseContext()
|
|
91
|
+
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
$gpuTier.set(2)
|
|
96
|
+
|
|
97
|
+
runBenchmark(gl)
|
|
98
|
+
.then(fps => $gpuTier.set(fps < 30 ? 1 : 2))
|
|
99
|
+
.catch(() => $gpuTier.set(1))
|
|
100
|
+
.finally(() => gl?.getExtension('WEBGL_lose_context')?.loseContext())
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Run detection once, *after* the first paint so the (potentially expensive)
|
|
105
|
+
* WebGL probe never blocks initial render. Prefer `requestIdleCallback` so the
|
|
106
|
+
* probe yields to rendering and input; fall back to a double
|
|
107
|
+
* `requestAnimationFrame` (guarantees at least one painted frame) and finally
|
|
108
|
+
* `setTimeout` where neither exists.
|
|
109
|
+
*/
|
|
110
|
+
function scheduleDetection() {
|
|
111
|
+
if (typeof window === 'undefined') {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (typeof window.requestIdleCallback === 'function') {
|
|
116
|
+
window.requestIdleCallback(() => detectGpuTier(), { timeout: 1000 })
|
|
117
|
+
} else if (typeof window.requestAnimationFrame === 'function') {
|
|
118
|
+
window.requestAnimationFrame(() =>
|
|
119
|
+
window.requestAnimationFrame(() => detectGpuTier())
|
|
120
|
+
)
|
|
121
|
+
} else {
|
|
122
|
+
setTimeout(() => detectGpuTier(), 0)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
scheduleDetection()
|
|
127
|
+
|
|
128
|
+
function runBenchmark(gl: WebGLRenderingContext): Promise<number> {
|
|
129
|
+
return new Promise(resolve => {
|
|
130
|
+
const vs = gl.createShader(gl.VERTEX_SHADER)!
|
|
131
|
+
const fs = gl.createShader(gl.FRAGMENT_SHADER)!
|
|
132
|
+
gl.shaderSource(
|
|
133
|
+
vs,
|
|
134
|
+
'attribute vec2 a;void main(){gl_Position=vec4(a,0,1);}'
|
|
135
|
+
)
|
|
136
|
+
gl.shaderSource(
|
|
137
|
+
fs,
|
|
138
|
+
'precision highp float;uniform float t;void main(){float v=0.;for(int i=0;i<64;i++)v+=sin(float(i)*t*.01);gl_FragColor=vec4(v*.001);}'
|
|
139
|
+
)
|
|
140
|
+
gl.compileShader(vs)
|
|
141
|
+
gl.compileShader(fs)
|
|
142
|
+
|
|
143
|
+
const prog = gl.createProgram()!
|
|
144
|
+
gl.attachShader(prog, vs)
|
|
145
|
+
gl.attachShader(prog, fs)
|
|
146
|
+
gl.linkProgram(prog)
|
|
147
|
+
gl.useProgram(prog)
|
|
148
|
+
|
|
149
|
+
const buf = gl.createBuffer()
|
|
150
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buf)
|
|
151
|
+
gl.bufferData(
|
|
152
|
+
gl.ARRAY_BUFFER,
|
|
153
|
+
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
|
|
154
|
+
gl.STATIC_DRAW
|
|
155
|
+
)
|
|
156
|
+
const a = gl.getAttribLocation(prog, 'a')
|
|
157
|
+
gl.enableVertexAttribArray(a)
|
|
158
|
+
gl.vertexAttribPointer(a, 2, gl.FLOAT, false, 0, 0)
|
|
159
|
+
|
|
160
|
+
const uT = gl.getUniformLocation(prog, 't')
|
|
161
|
+
let frames = 0
|
|
162
|
+
const start = performance.now()
|
|
163
|
+
|
|
164
|
+
const tick = () => {
|
|
165
|
+
gl.uniform1f(uT, frames)
|
|
166
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
|
|
167
|
+
gl.finish()
|
|
168
|
+
frames++
|
|
169
|
+
|
|
170
|
+
if (performance.now() - start < 200) {
|
|
171
|
+
requestAnimationFrame(tick)
|
|
172
|
+
} else {
|
|
173
|
+
const elapsed = performance.now() - start
|
|
174
|
+
gl.deleteProgram(prog)
|
|
175
|
+
gl.deleteShader(vs)
|
|
176
|
+
gl.deleteShader(fs)
|
|
177
|
+
gl.deleteBuffer(buf)
|
|
178
|
+
resolve((frames / elapsed) * 1000)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
requestAnimationFrame(tick)
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function useGpuTier() {
|
|
187
|
+
return useStore($gpuTier)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
type GpuTier = 0 | 1 | 2
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Visibility- and intersection-aware render-loop helper for the WebGL
|
|
5
|
+
* overlays.
|
|
6
|
+
*
|
|
7
|
+
* The overlays were previously running fragment shaders at 60fps for the
|
|
8
|
+
* entire lifetime of the page — including when the tab was hidden, the
|
|
9
|
+
* canvas had been scrolled out of view, or the user had been idle for
|
|
10
|
+
* hours. On retina laptops the compositor cost of mix-blend-mode on a
|
|
11
|
+
* full-viewport canvas plus continuous WebGL rasterisation is enough to
|
|
12
|
+
* keep the GPU hot indefinitely, which is what manifests as "fans go
|
|
13
|
+
* crazy after 2 hours of idle".
|
|
14
|
+
*
|
|
15
|
+
* `runRenderLoop` wraps a frame callback so that it:
|
|
16
|
+
*
|
|
17
|
+
* 1. Pauses entirely when `document.hidden` is true (background tab,
|
|
18
|
+
* minimised window, screen locked).
|
|
19
|
+
* 2. Pauses when the canvas's bounding rect is offscreen (we tell
|
|
20
|
+
* `IntersectionObserver` to look at the canvas itself).
|
|
21
|
+
* 3. Optionally caps the frame rate via a min-interval — the previous
|
|
22
|
+
* `gpuTier === 1 ? setTimeout(loop, 100) : raf` trick is preserved
|
|
23
|
+
* and extended so even tier-2 GPUs cap at e.g. 30fps for overlays
|
|
24
|
+
* that don't need 60.
|
|
25
|
+
*
|
|
26
|
+
* The callback receives the *delta* time in seconds since the last call
|
|
27
|
+
* (so `uTime` advances correctly across pauses without ever skipping
|
|
28
|
+
* forward by hours).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
interface RunRenderLoopOptions {
|
|
32
|
+
/** Element to observe with IntersectionObserver. When fully out of
|
|
33
|
+
* view, the loop pauses. Pass the canvas element itself. */
|
|
34
|
+
el: Element
|
|
35
|
+
/** Min ms between frames. 0 = no cap (uses requestAnimationFrame).
|
|
36
|
+
* Anything > 0 uses setTimeout-driven scheduling. */
|
|
37
|
+
minIntervalMs?: number
|
|
38
|
+
/** Frame callback. Receives the elapsed seconds since the previous
|
|
39
|
+
* *executed* frame (not since the previous scheduled frame), so
|
|
40
|
+
* uniforms keyed off this value will not jump after a long pause. */
|
|
41
|
+
onFrame: (deltaSeconds: number) => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function runRenderLoop({
|
|
45
|
+
el,
|
|
46
|
+
minIntervalMs = 0,
|
|
47
|
+
onFrame
|
|
48
|
+
}: RunRenderLoopOptions) {
|
|
49
|
+
let running = true
|
|
50
|
+
let visible = !document.hidden
|
|
51
|
+
let inView = true
|
|
52
|
+
let last = performance.now()
|
|
53
|
+
let raf = 0
|
|
54
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
55
|
+
|
|
56
|
+
const onVisibility = () => {
|
|
57
|
+
visible = !document.hidden
|
|
58
|
+
|
|
59
|
+
// When we come back from a hidden tab, reset the clock so the next
|
|
60
|
+
// frame's delta is ~one frame, not "hours since I was hidden".
|
|
61
|
+
if (visible) {
|
|
62
|
+
last = performance.now()
|
|
63
|
+
schedule()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const io = new IntersectionObserver(
|
|
68
|
+
entries => {
|
|
69
|
+
const wasInView = inView
|
|
70
|
+
inView = entries.some(e => e.isIntersecting)
|
|
71
|
+
|
|
72
|
+
if (!wasInView && inView) {
|
|
73
|
+
last = performance.now()
|
|
74
|
+
schedule()
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
{ threshold: 0 }
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
io.observe(el)
|
|
81
|
+
document.addEventListener('visibilitychange', onVisibility)
|
|
82
|
+
|
|
83
|
+
const tick = () => {
|
|
84
|
+
if (!running) return
|
|
85
|
+
|
|
86
|
+
if (!visible || !inView) {
|
|
87
|
+
// Don't reschedule — we'll be re-kicked by visibilitychange or IO.
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const now = performance.now()
|
|
92
|
+
const delta = (now - last) / 1000
|
|
93
|
+
last = now
|
|
94
|
+
|
|
95
|
+
onFrame(delta)
|
|
96
|
+
schedule()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function schedule() {
|
|
100
|
+
if (!running || !visible || !inView) return
|
|
101
|
+
|
|
102
|
+
if (minIntervalMs > 0) {
|
|
103
|
+
timer = setTimeout(tick, minIntervalMs)
|
|
104
|
+
} else {
|
|
105
|
+
raf = requestAnimationFrame(tick)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
schedule()
|
|
110
|
+
|
|
111
|
+
return () => {
|
|
112
|
+
running = false
|
|
113
|
+
io.disconnect()
|
|
114
|
+
document.removeEventListener('visibilitychange', onVisibility)
|
|
115
|
+
cancelAnimationFrame(raf)
|
|
116
|
+
|
|
117
|
+
if (timer !== undefined) {
|
|
118
|
+
clearTimeout(timer)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|