@dickpy/dsh-imagegen 1.5.9 → 1.5.10

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/lib/index.js CHANGED
@@ -41,7 +41,7 @@ function installSettingsSectionCompat(ctx, ns, schema, entry, hooks) {
41
41
  /** Settings namespace this plugin owns (host settings seam + bridge). */
42
42
  const IMAGEGEN_SETTINGS_NAMESPACE = "dsh-imagegen";
43
43
  /** Published package version shared by the host updater and the client UI. */
44
- const PLUGIN_VERSION = "1.5.9";
44
+ const PLUGIN_VERSION = "1.5.10";
45
45
  /** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
46
46
  const SETTINGS_API = {
47
47
  describe: "/api/dsh-imagegen/settings/describe",
package/package.json CHANGED
@@ -1,82 +1,82 @@
1
- {
2
- "name": "@dickpy/dsh-imagegen",
3
- "description": "AI image generation plugin for the dsh web GUI: text-to-image and image-to-image through configurable provider channels (gpt-image-2 / grok-imagine-image / nanobanana series / seedream-5.0-pro / dall-e-3, with native xAI Grok Imagine, Google Nano Banana and ByteDance Seedream request shaping), with per-channel model catalogs and a New Session / Image Generation tab entry opening a three-column studio beside the native conversation.",
4
- "version": "1.5.9",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "exports": {
8
- ".": "./lib/index.js",
9
- "./client": "./lib/client.js",
10
- "./package.json": "./package.json"
11
- },
12
- "dsh": {
13
- "bundle": {
14
- "patch": "./cordis.patch.yml"
15
- },
16
- "client": {
17
- "inject": [
18
- "@deepseek-ai/dsh-client-store",
19
- "@deepseek-ai/dsh-api-remotes",
20
- "@deepseek-ai/dsh-api-session-controller",
21
- "@deepseek-ai/dsh-client-connection",
22
- "@deepseek-ai/dsh-client-ui-conversation",
23
- "@deepseek-ai/dsh-client-ui-renderer",
24
- "@deepseek-ai/dsh-client-ui-settings",
25
- "@deepseek-ai/dsh-client-ui-tool"
26
- ],
27
- "platform": "web"
28
- }
29
- },
30
- "dependencies": {
31
- "lucide-react": "^1.41.0",
32
- "schemastery": "^3.18.0"
33
- },
34
- "devDependencies": {
35
- "@deepseek-ai/cordis": "^4.0.2",
36
- "@deepseek-ai/dsh-api-remotes": "0.1.2-alpha.2",
37
- "@deepseek-ai/dsh-api-session-controller": "0.1.2-alpha.2",
38
- "@deepseek-ai/dsh-attachment": "0.1.2-alpha.2",
39
- "@deepseek-ai/dsh-client-connection": "0.1.2-alpha.2",
40
- "@deepseek-ai/dsh-client-locale": "0.1.2-alpha.2",
41
- "@deepseek-ai/dsh-client-store": "0.1.2-alpha.2",
42
- "@deepseek-ai/dsh-client-ui-conversation": "0.1.2-alpha.2",
43
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-alpha.2",
44
- "@deepseek-ai/dsh-client-ui-renderer": "0.1.2-alpha.2",
45
- "@deepseek-ai/dsh-client-ui-settings": "0.1.2-alpha.2",
46
- "@deepseek-ai/dsh-client-ui-slots": "0.1.2-alpha.2",
47
- "@deepseek-ai/dsh-commands": "0.1.2-alpha.2",
48
- "@deepseek-ai/dsh-host-webserver": "0.1.2-alpha.2",
49
- "@deepseek-ai/dsh-llm": "0.1.2-alpha.2",
50
- "@deepseek-ai/dsh-settings": "0.1.2-alpha.2",
51
- "@deepseek-ai/dsh-system-prompt": "0.1.2-alpha.2",
52
- "@deepseek-ai/dsh-tools": "0.1.2-alpha.2",
53
- "@types/node": "^22.20.0",
54
- "@types/react": "~18.3.1",
55
- "@types/react-dom": "^18.3.5",
56
- "jsdom": "25.0.1",
57
- "lightningcss": "1.32.0",
58
- "react": "^18.3.1",
59
- "react-dom": "^18.3.1",
60
- "schemastery": "^3.18.0",
61
- "tsdown": "0.22.2",
62
- "typescript": "~5.7.2"
63
- },
64
- "files": [
65
- "lib",
66
- "src",
67
- "cordis.patch.yml",
68
- "docs",
69
- "README.md"
70
- ],
71
- "license": "Apache-2.0",
72
- "repository": {
73
- "type": "git",
74
- "url": "git+https://github.com/dickpy/dsh-imagegen.git"
75
- },
76
- "scripts": {
77
- "build": "tsdown",
78
- "watch": "tsdown --watch",
79
- "typecheck": "tsc --noEmit",
80
- "fetch-templates": "node scripts/fetch-templates.mjs"
81
- }
82
- }
1
+ {
2
+ "name": "@dickpy/dsh-imagegen",
3
+ "description": "AI image generation plugin for the dsh web GUI: text-to-image and image-to-image through configurable provider channels (gpt-image-2 / grok-imagine-image / nanobanana series / seedream-5.0-pro / dall-e-3, with native xAI Grok Imagine, Google Nano Banana and ByteDance Seedream request shaping), with per-channel model catalogs and a New Session / Image Generation tab entry opening a three-column studio beside the native conversation.",
4
+ "version": "1.5.10",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "dsh": {
13
+ "bundle": {
14
+ "patch": "./cordis.patch.yml"
15
+ },
16
+ "client": {
17
+ "inject": [
18
+ "@deepseek-ai/dsh-client-store",
19
+ "@deepseek-ai/dsh-api-remotes",
20
+ "@deepseek-ai/dsh-api-session-controller",
21
+ "@deepseek-ai/dsh-client-connection",
22
+ "@deepseek-ai/dsh-client-ui-conversation",
23
+ "@deepseek-ai/dsh-client-ui-renderer",
24
+ "@deepseek-ai/dsh-client-ui-settings",
25
+ "@deepseek-ai/dsh-client-ui-tool"
26
+ ],
27
+ "platform": "web"
28
+ }
29
+ },
30
+ "dependencies": {
31
+ "lucide-react": "^1.41.0",
32
+ "schemastery": "^3.18.0"
33
+ },
34
+ "devDependencies": {
35
+ "@deepseek-ai/cordis": "^4.0.2",
36
+ "@deepseek-ai/dsh-api-remotes": "0.1.2-alpha.2",
37
+ "@deepseek-ai/dsh-api-session-controller": "0.1.2-alpha.2",
38
+ "@deepseek-ai/dsh-attachment": "0.1.2-alpha.2",
39
+ "@deepseek-ai/dsh-client-connection": "0.1.2-alpha.2",
40
+ "@deepseek-ai/dsh-client-locale": "0.1.2-alpha.2",
41
+ "@deepseek-ai/dsh-client-store": "0.1.2-alpha.2",
42
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.2-alpha.2",
43
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-alpha.2",
44
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.2-alpha.2",
45
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.2-alpha.2",
46
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.2-alpha.2",
47
+ "@deepseek-ai/dsh-commands": "0.1.2-alpha.2",
48
+ "@deepseek-ai/dsh-host-webserver": "0.1.2-alpha.2",
49
+ "@deepseek-ai/dsh-llm": "0.1.2-alpha.2",
50
+ "@deepseek-ai/dsh-settings": "0.1.2-alpha.2",
51
+ "@deepseek-ai/dsh-system-prompt": "0.1.2-alpha.2",
52
+ "@deepseek-ai/dsh-tools": "0.1.2-alpha.2",
53
+ "@types/node": "^22.20.0",
54
+ "@types/react": "~18.3.1",
55
+ "@types/react-dom": "^18.3.5",
56
+ "jsdom": "25.0.1",
57
+ "lightningcss": "1.32.0",
58
+ "react": "^18.3.1",
59
+ "react-dom": "^18.3.1",
60
+ "schemastery": "^3.18.0",
61
+ "tsdown": "0.22.2",
62
+ "typescript": "~5.7.2"
63
+ },
64
+ "files": [
65
+ "lib",
66
+ "src",
67
+ "cordis.patch.yml",
68
+ "docs",
69
+ "README.md"
70
+ ],
71
+ "license": "Apache-2.0",
72
+ "repository": {
73
+ "type": "git",
74
+ "url": "git+https://github.com/dickpy/dsh-imagegen.git"
75
+ },
76
+ "scripts": {
77
+ "build": "tsdown",
78
+ "watch": "tsdown --watch",
79
+ "typecheck": "tsc --noEmit",
80
+ "fetch-templates": "node scripts/fetch-templates.mjs"
81
+ }
82
+ }
@@ -3,6 +3,7 @@
3
3
  * receives pointer events, so every listener observes the viewport element. */
4
4
 
5
5
  import { useEffect, useRef } from 'react'
6
+ import { detectLightSurface } from './surface-theme.ts'
6
7
  import css from './canvas-workspace.module.css'
7
8
 
8
9
  /** Interactive flowmap-style dot field ("fluid distortion"): the pointer's
@@ -632,25 +633,6 @@ const SHADER_QUAD_VERTEX = `
632
633
  }
633
634
  `
634
635
 
635
- /** Walk up from the grid layer to the first opaque surface and decide
636
- * light vs dark, so the shader backdrops can pick their lightMode ink. */
637
- function detectLightSurface(element: HTMLElement): boolean {
638
- let node: HTMLElement | null = element
639
- while (node !== null) {
640
- const color = getComputedStyle(node).backgroundColor
641
- const match = color.match(/rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:[\s,]+([\d.]+))?\s*\)/)
642
- if (match !== null) {
643
- const alpha = match[4] !== undefined ? Number(match[4]) : 1
644
- if (alpha >= 0.5) {
645
- const luminance = (0.2126 * Number(match[1]) + 0.7152 * Number(match[2]) + 0.0722 * Number(match[3])) / 255
646
- return luminance > 0.55
647
- }
648
- }
649
- node = node.parentElement
650
- }
651
- return false
652
- }
653
-
654
636
  /** Shared engine for the fullscreen-shader backdrops: compiles one program,
655
637
  * owns the DPR-sized canvas, the rAF loop, viewport resize and smoothed
656
638
  * pointer tracking. `frame` sets uniforms and calls draw(). WebGL2 is
@@ -351,6 +351,95 @@ export function CanvasWorkspace(props: CanvasWorkspaceProps): React.JSX.Element
351
351
  const [libraryOpen, setLibraryOpen] = useState(false)
352
352
  const [pickerTab, setPickerTab] = useState<'upload' | 'history' | 'gallery' | 'generate'>('upload')
353
353
  const backgroundFileRef = useRef<HTMLInputElement>(null)
354
+
355
+ /** reactbits.dev "Dock" port: each tile spring-scales by its distance to the
356
+ * pointer (width/height, so neighbours part like the macOS dock) and the
357
+ * panel breathes taller while hovered to make room for the labels. */
358
+ const dockOuterRef = useRef<HTMLDivElement>(null)
359
+ const dockRef = useRef<HTMLDivElement>(null)
360
+ useEffect(() => {
361
+ const outer = dockOuterRef.current
362
+ const dock = dockRef.current
363
+ if (outer === null || dock === null) return
364
+ if (typeof window.requestAnimationFrame !== 'function') return
365
+ const clockNow = (): number => (typeof performance !== 'undefined' ? performance.now() : Date.now())
366
+ const BASE = 34
367
+ const MAGNIFIED = 50
368
+ const DISTANCE = 150
369
+ const REST_HEIGHT = 42
370
+ const HOVER_HEIGHT = MAGNIFIED + MAGNIFIED / 2 + 4
371
+ const STIFFNESS = 170
372
+ const DAMPING = 16
373
+ const MASS = 0.5
374
+ const tiles = [...dock.querySelectorAll<HTMLElement>('[data-dock-item]')].map(el => ({ el, size: BASE, velocity: 0 }))
375
+ let outerSize = REST_HEIGHT
376
+ let outerVelocity = 0
377
+ let mouseX = Number.POSITIVE_INFINITY
378
+ let hovered = false
379
+ let raf = 0
380
+ let running = false
381
+ let last = clockNow()
382
+ const step = (now: number): void => {
383
+ const dt = Math.min(0.05, (now - last) / 1000)
384
+ last = now
385
+ let settled = true
386
+ for (const tile of tiles) {
387
+ let target = BASE
388
+ if (hovered) {
389
+ const rect = tile.el.getBoundingClientRect()
390
+ const distance = Math.abs(mouseX - (rect.left + rect.width / 2))
391
+ target = BASE + (MAGNIFIED - BASE) * Math.max(0, 1 - distance / DISTANCE)
392
+ }
393
+ tile.velocity += ((STIFFNESS * (target - tile.size) - DAMPING * tile.velocity) / MASS) * dt
394
+ tile.size += tile.velocity * dt
395
+ if (Math.abs(target - tile.size) > 0.15 || Math.abs(tile.velocity) > 2) settled = false
396
+ else {
397
+ tile.size = target
398
+ tile.velocity = 0
399
+ }
400
+ tile.el.style.width = `${tile.size.toFixed(2)}px`
401
+ tile.el.style.height = `${tile.size.toFixed(2)}px`
402
+ }
403
+ const outerTarget = hovered ? HOVER_HEIGHT : REST_HEIGHT
404
+ outerVelocity += ((STIFFNESS * (outerTarget - outerSize) - DAMPING * outerVelocity) / MASS) * dt
405
+ outerSize += outerVelocity * dt
406
+ if (Math.abs(outerTarget - outerSize) > 0.25 || Math.abs(outerVelocity) > 3) settled = false
407
+ else {
408
+ outerSize = outerTarget
409
+ outerVelocity = 0
410
+ }
411
+ outer.style.height = `${outerSize.toFixed(2)}px`
412
+ if (settled) {
413
+ running = false
414
+ return
415
+ }
416
+ raf = window.requestAnimationFrame(step)
417
+ }
418
+ const wake = (): void => {
419
+ if (running) return
420
+ running = true
421
+ last = clockNow()
422
+ raf = window.requestAnimationFrame(step)
423
+ }
424
+ const onPointerMove = (event: PointerEvent): void => {
425
+ mouseX = event.clientX
426
+ hovered = true
427
+ wake()
428
+ }
429
+ const onPointerLeave = (): void => {
430
+ hovered = false
431
+ mouseX = Number.POSITIVE_INFINITY
432
+ wake()
433
+ }
434
+ dock.addEventListener('pointermove', onPointerMove)
435
+ dock.addEventListener('pointerleave', onPointerLeave)
436
+ wake()
437
+ return () => {
438
+ window.cancelAnimationFrame(raf)
439
+ dock.removeEventListener('pointermove', onPointerMove)
440
+ dock.removeEventListener('pointerleave', onPointerLeave)
441
+ }
442
+ }, [])
354
443
  const imageFileRef = useRef<HTMLInputElement>(null)
355
444
  const [renamingTitle, setRenamingTitle] = useState(false)
356
445
  const [confirmDeleteProject, setConfirmDeleteProject] = useState(false)
@@ -1869,59 +1958,58 @@ export function CanvasWorkspace(props: CanvasWorkspaceProps): React.JSX.Element
1869
1958
  {emptyState}
1870
1959
  </div>
1871
1960
 
1872
- <div
1873
- className={`${css.dock} ${cursorClass}`}
1874
- data-canvas-no-zoom=""
1875
- onPointerMove={event => {
1876
- if (globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true) return
1877
- const dock = event.currentTarget
1878
- const cursorX = event.clientX - dock.getBoundingClientRect().left
1879
- // CSS-module class names are hashed in the DOM, so match by tag.
1880
- for (const button of dock.querySelectorAll<HTMLButtonElement>('button')) {
1881
- // offsetLeft is the layout position, unaffected by the scale transform,
1882
- // so the magnification wave does not feed back into itself.
1883
- const distance = Math.abs(cursorX - (button.offsetLeft + button.offsetWidth / 2))
1884
- const influence = Math.exp(-(distance * distance) / (2 * 48 * 48))
1885
- button.style.setProperty('--dock-scale', (1 + 0.24 * influence).toFixed(3))
1886
- button.style.setProperty('--dock-lift', `${(-8 * influence).toFixed(2)}px`)
1887
- }
1888
- }}
1889
- onPointerLeave={event => {
1890
- for (const button of event.currentTarget.querySelectorAll<HTMLButtonElement>('button')) {
1891
- button.style.setProperty('--dock-scale', '1')
1892
- button.style.setProperty('--dock-lift', '0px')
1893
- }
1894
- }}
1895
- >
1896
- <IconButton name="select" size={18} label={tt('canvas.toolSelect')} active={tool === 'select'} onClick={() => setTool('select')} />
1897
- <IconButton name="pan" size={18} label={tt('canvas.toolPan')} active={tool === 'pan'} onClick={() => setTool('pan')} />
1898
- <span className={css.dockDivider} />
1899
- <IconButton
1900
- name="image"
1901
- size={18}
1902
- label={tt('canvas.addImage')}
1903
- active={imageMenu !== null}
1904
- onClick={event => openDockMenu('image', event.currentTarget)}
1905
- onMouseEnter={event => openDockMenu('image', event.currentTarget)}
1906
- onMouseLeave={scheduleMenuClose}
1907
- />
1908
- <IconButton name="text" size={18} label={tt('canvas.addText')} onClick={() => placeNewNode(createTextNode())} />
1909
- <IconButton name="sparkle" size={18} label={tt('canvas.addConfigNode')} onClick={() => placeNewNode(createConfigNode())} />
1910
- <IconButton name="template" size={18} label={tt('canvas.templateLibrary')} active={libraryOpen} onClick={() => setLibraryOpen(previous => !previous)} />
1911
- <span className={css.dockDivider} />
1912
- <IconButton
1913
- name="background"
1914
- size={18}
1915
- label={tt('canvas.background')}
1916
- active={backgroundMenu !== null}
1917
- onClick={event => openDockMenu('background', event.currentTarget)}
1918
- onMouseEnter={event => openDockMenu('background', event.currentTarget)}
1919
- onMouseLeave={scheduleMenuClose}
1920
- />
1921
- <IconButton name="undo" size={18} label={tt('canvas.undo')} disabled={pastRef.current.length === 0} onClick={undo} />
1922
- <IconButton name="redo" size={18} label={tt('canvas.redo')} disabled={futureRef.current.length === 0} onClick={redo} />
1923
- <span className={css.dockDivider} />
1924
- <IconButton name="trash" size={18} label={tt('canvas.delete')} disabled={selectedIds.size === 0 && selectedConnectionId === null} onClick={deleteSelection} />
1961
+ <div className={css.dockOuter} data-canvas-no-zoom="" ref={dockOuterRef}>
1962
+ <div className={`${css.dock} ${cursorClass}`} ref={dockRef} data-canvas-no-zoom="" role="toolbar">
1963
+ <div className={css.dockItem} data-dock-item="" data-label={tt('canvas.toolSelect')}>
1964
+ <IconButton name="select" size={18} label={tt('canvas.toolSelect')} active={tool === 'select'} onClick={() => setTool('select')} />
1965
+ </div>
1966
+ <div className={css.dockItem} data-dock-item="" data-label={tt('canvas.toolPan')}>
1967
+ <IconButton name="pan" size={18} label={tt('canvas.toolPan')} active={tool === 'pan'} onClick={() => setTool('pan')} />
1968
+ </div>
1969
+ <span className={css.dockDivider} aria-hidden="true" />
1970
+ <div className={css.dockItem} data-dock-item="" data-label={tt('canvas.addImage')}>
1971
+ <IconButton
1972
+ name="image"
1973
+ size={18}
1974
+ label={tt('canvas.addImage')}
1975
+ active={imageMenu !== null}
1976
+ onClick={event => openDockMenu('image', event.currentTarget)}
1977
+ onMouseEnter={event => openDockMenu('image', event.currentTarget)}
1978
+ onMouseLeave={scheduleMenuClose}
1979
+ />
1980
+ </div>
1981
+ <div className={css.dockItem} data-dock-item="" data-label={tt('canvas.addText')}>
1982
+ <IconButton name="text" size={18} label={tt('canvas.addText')} onClick={() => placeNewNode(createTextNode())} />
1983
+ </div>
1984
+ <div className={css.dockItem} data-dock-item="" data-label={tt('canvas.addConfigNode')}>
1985
+ <IconButton name="sparkle" size={18} label={tt('canvas.addConfigNode')} onClick={() => placeNewNode(createConfigNode())} />
1986
+ </div>
1987
+ <div className={css.dockItem} data-dock-item="" data-label={tt('canvas.templateLibrary')}>
1988
+ <IconButton name="template" size={18} label={tt('canvas.templateLibrary')} active={libraryOpen} onClick={() => setLibraryOpen(previous => !previous)} />
1989
+ </div>
1990
+ <span className={css.dockDivider} aria-hidden="true" />
1991
+ <div className={css.dockItem} data-dock-item="" data-label={tt('canvas.background')}>
1992
+ <IconButton
1993
+ name="background"
1994
+ size={18}
1995
+ label={tt('canvas.background')}
1996
+ active={backgroundMenu !== null}
1997
+ onClick={event => openDockMenu('background', event.currentTarget)}
1998
+ onMouseEnter={event => openDockMenu('background', event.currentTarget)}
1999
+ onMouseLeave={scheduleMenuClose}
2000
+ />
2001
+ </div>
2002
+ <div className={css.dockItem} data-dock-item="" data-label={tt('canvas.undo')}>
2003
+ <IconButton name="undo" size={18} label={tt('canvas.undo')} disabled={pastRef.current.length === 0} onClick={undo} />
2004
+ </div>
2005
+ <div className={css.dockItem} data-dock-item="" data-label={tt('canvas.redo')}>
2006
+ <IconButton name="redo" size={18} label={tt('canvas.redo')} disabled={futureRef.current.length === 0} onClick={redo} />
2007
+ </div>
2008
+ <span className={css.dockDivider} aria-hidden="true" />
2009
+ <div className={css.dockItem} data-dock-item="" data-label={tt('canvas.delete')}>
2010
+ <IconButton name="trash" size={18} label={tt('canvas.delete')} disabled={selectedIds.size === 0 && selectedConnectionId === null} onClick={deleteSelection} />
2011
+ </div>
2012
+ </div>
1925
2013
  </div>
1926
2014
 
1927
2015
  {imageMenu !== null ? <div
@@ -0,0 +1,224 @@
1
+ /* reactbits.dev "GooeyNav" port, theme-following. The gooey merge uses an SVG
2
+ * alpha-contrast filter (blur + feColorMatrix) instead of the CSS
3
+ * blur+contrast trick, so colored particles survive on any surface: the pill
4
+ * and ink follow the host theme via [data-theme]. */
5
+
6
+ .container {
7
+ --gooey-time: 1500ms;
8
+ --gooey-color-1: #5227ff;
9
+ --gooey-color-2: #ff9ffc;
10
+ --gooey-color-3: #b19eef;
11
+ --gooey-color-4: #22d3ee;
12
+ /* light surface (default): dark ink pill, white text */
13
+ --gooey-pill: #17171c;
14
+ --gooey-pill-ink: #ffffff;
15
+ --gooey-ink: #5b5e6b;
16
+ position: relative;
17
+ font-size: 13px;
18
+ }
19
+
20
+ .container[data-theme='dark'] {
21
+ --gooey-pill: #ffffff;
22
+ --gooey-pill-ink: #17171c;
23
+ --gooey-ink: #ffffff;
24
+ }
25
+
26
+ .gooSvg {
27
+ position: absolute;
28
+ width: 0;
29
+ height: 0;
30
+ }
31
+
32
+ .container nav {
33
+ display: flex;
34
+ position: relative;
35
+ transform: translate3d(0, 0, 0.01px);
36
+ }
37
+
38
+ .container nav ul {
39
+ display: flex;
40
+ gap: 6px;
41
+ list-style: none;
42
+ padding: 0 4px;
43
+ margin: 0;
44
+ position: relative;
45
+ z-index: 3;
46
+ }
47
+
48
+ .container nav li {
49
+ border-radius: 100vw;
50
+ position: relative;
51
+ transition:
52
+ background-color 0.3s ease,
53
+ color 0.3s ease,
54
+ box-shadow 0.3s ease;
55
+ color: var(--gooey-ink);
56
+ }
57
+
58
+ .container nav li button {
59
+ display: inline-block;
60
+ padding: 5px 13px;
61
+ font: inherit;
62
+ color: inherit;
63
+ background: none;
64
+ border: none;
65
+ cursor: pointer;
66
+ white-space: nowrap;
67
+ }
68
+
69
+ .container nav li.active {
70
+ color: var(--gooey-pill-ink);
71
+ }
72
+
73
+ .container nav li::after {
74
+ content: '';
75
+ position: absolute;
76
+ inset: 0;
77
+ border-radius: 10px;
78
+ background: var(--gooey-pill);
79
+ opacity: 0;
80
+ transform: scale(0);
81
+ transition: all 0.3s ease;
82
+ z-index: -1;
83
+ }
84
+
85
+ .container nav li.active::after {
86
+ opacity: 1;
87
+ transform: scale(1);
88
+ }
89
+
90
+ .effect {
91
+ position: absolute;
92
+ left: 0;
93
+ top: 0;
94
+ width: 0;
95
+ height: 0;
96
+ opacity: 1;
97
+ pointer-events: none;
98
+ display: grid;
99
+ place-items: center;
100
+ z-index: 1;
101
+ }
102
+
103
+ .effectText {
104
+ color: var(--gooey-ink);
105
+ transition: color 0.3s ease;
106
+ font-size: 13px;
107
+ }
108
+
109
+ .effectText.active {
110
+ color: var(--gooey-pill-ink);
111
+ }
112
+
113
+ .effectFilter {
114
+ filter: url(#dsh-gooey-nav-goo);
115
+ }
116
+
117
+ .effectFilter::after {
118
+ content: '';
119
+ position: absolute;
120
+ inset: 0;
121
+ background: var(--gooey-pill);
122
+ transform: scale(0);
123
+ opacity: 0;
124
+ z-index: -1;
125
+ border-radius: 100vw;
126
+ }
127
+
128
+ .effectFilter.active::after {
129
+ animation: gooeyPill 0.3s ease both;
130
+ }
131
+
132
+ @keyframes gooeyPill {
133
+ to {
134
+ transform: scale(1);
135
+ opacity: 1;
136
+ }
137
+ }
138
+
139
+ .particle,
140
+ .point {
141
+ display: block;
142
+ opacity: 0;
143
+ width: 14px;
144
+ height: 14px;
145
+ border-radius: 100%;
146
+ transform-origin: center;
147
+ }
148
+
149
+ .particle {
150
+ --time: 1500ms;
151
+ position: absolute;
152
+ top: calc(50% - 7px);
153
+ left: calc(50% - 7px);
154
+ animation: gooeyParticle calc(var(--time)) ease 1 -350ms;
155
+ }
156
+
157
+ .point {
158
+ background: var(--color);
159
+ opacity: 1;
160
+ animation: gooeyPoint calc(var(--time)) ease 1 -350ms;
161
+ }
162
+
163
+ @keyframes gooeyParticle {
164
+ 0% {
165
+ transform: rotate(0deg) translate(calc(var(--start-x)), calc(var(--start-y)));
166
+ opacity: 1;
167
+ animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
168
+ }
169
+
170
+ 70% {
171
+ transform: rotate(calc(var(--rotate) * 0.5)) translate(calc(var(--end-x) * 1.2), calc(var(--end-y) * 1.2));
172
+ opacity: 1;
173
+ animation-timing-function: ease;
174
+ }
175
+
176
+ 85% {
177
+ transform: rotate(calc(var(--rotate) * 0.66)) translate(calc(var(--end-x)), calc(var(--end-y)));
178
+ opacity: 1;
179
+ }
180
+
181
+ 100% {
182
+ transform: rotate(calc(var(--rotate) * 1.2)) translate(calc(var(--end-x) * 0.5), calc(var(--end-y) * 0.5));
183
+ opacity: 1;
184
+ }
185
+ }
186
+
187
+ @keyframes gooeyPoint {
188
+ 0% {
189
+ transform: scale(0);
190
+ opacity: 0;
191
+ animation-timing-function: cubic-bezier(0.55, 0, 1, 0.45);
192
+ }
193
+
194
+ 25% {
195
+ transform: scale(calc(var(--scale) * 0.25));
196
+ }
197
+
198
+ 38% {
199
+ opacity: 1;
200
+ }
201
+
202
+ 65% {
203
+ transform: scale(var(--scale));
204
+ opacity: 1;
205
+ animation-timing-function: ease;
206
+ }
207
+
208
+ 85% {
209
+ transform: scale(var(--scale));
210
+ opacity: 1;
211
+ }
212
+
213
+ 100% {
214
+ transform: scale(0);
215
+ opacity: 0;
216
+ }
217
+ }
218
+
219
+ @media (prefers-reduced-motion: reduce) {
220
+ .particle,
221
+ .point {
222
+ animation: none;
223
+ }
224
+ }