Merge branch 'library-hub' into comfy_vibe

This commit is contained in:
orkhanart
2025-11-29 12:55:13 -08:00
68 changed files with 3577 additions and 708 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 450 125.2">
<!-- Generator: Adobe Illustrator 29.5.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 141) -->
<defs>
<style>
.st0 {
fill: #1b2dce;
}
.st1 {
fill: #fff;
}
.st2 {
opacity: .6;
}
.st3 {
display: none;
}
</style>
</defs>
<g id="watermark" class="st3">
<g>
<g id="watermark1" data-name="watermark" class="st2">
<g>
<g>
<g>
<polygon class="st1" points="188.8 273.8 183.2 286.6 180.3 286.6 174.7 273.8 177.9 273.8 181.8 283 185.8 273.8 188.8 273.8"/>
<path class="st1" d="M198.5,282.3h-7.5c.3,1.2,1.3,2,2.8,2s1.8-.3,2.5-1l1.5,1.7c-.9,1-2.3,1.6-4.1,1.6-3.4,0-5.6-2.1-5.6-5.1s2.3-5.1,5.3-5.1,5.1,1.9,5.1,5.1c0,.2,0,.5,0,.8M191,280.7h4.9c-.2-1.2-1.1-2.1-2.4-2.1-1.3,0-2.2.8-2.4,2.1"/>
<path class="st1" d="M199.7,281.5c0-3,2.3-5.1,5.5-5.1s3.7.9,4.4,2.5l-2.2,1.2c-.5-.9-1.3-1.4-2.2-1.4-1.4,0-2.6,1-2.6,2.7s1.1,2.7,2.6,2.7,1.7-.4,2.2-1.4l2.2,1.2c-.7,1.6-2.3,2.5-4.4,2.5-3.2,0-5.5-2.1-5.5-5.1"/>
<path class="st1" d="M217.8,286c-.6.4-1.4.6-2.3.6-2.3,0-3.7-1.2-3.7-3.5v-4.1h-1.5v-2.2h1.5v-2.4h2.9v2.4h2.5v2.2h-2.5v4c0,.8.5,1.3,1.2,1.3s.8-.1,1.2-.4l.8,2Z"/>
</g>
<path class="st1" d="M237.5,276.6v2.6c-.2,0-.4,0-.6,0-1.6,0-2.6.9-2.6,2.8v4.7h-2.9v-9.9h2.7v1.3c.7-1,1.9-1.5,3.4-1.5"/>
<path class="st1" d="M238.3,285.1l1-2.2c1.1.8,2.7,1.3,4.2,1.3s2.5-.6,2.5-1.4c0-2.4-7.5-.8-7.5-5.5s1.8-4,5.4-4,3.3.4,4.5,1.1l-.9,2.3c-1.2-.7-2.4-1-3.6-1-1.8,0-2.4.7-2.4,1.5,0,2.4,7.5.7,7.5,5.5s-1.8,4-5.5,4c-2,0-4.1-.6-5.2-1.5"/>
<path class="st1" d="M260.4,282.3h-7.5c.3,1.2,1.3,2,2.8,2s1.8-.3,2.5-1l1.5,1.7c-.9,1-2.3,1.6-4.1,1.6-3.4,0-5.6-2.1-5.6-5.1s2.3-5.1,5.3-5.1,5.1,1.9,5.1,5.1c0,.2,0,.5,0,.8M252.9,280.7h4.9c-.2-1.2-1.1-2.1-2.4-2.1-1.3,0-2.2.8-2.4,2.1"/>
<path class="st1" d="M272,282.3h-7.5c.3,1.2,1.3,2,2.8,2s1.8-.3,2.5-1l1.5,1.7c-.9,1-2.3,1.6-4.1,1.6-3.4,0-5.6-2.1-5.6-5.1s2.3-5.1,5.3-5.1,5.1,1.9,5.1,5.1c0,.2,0,.5,0,.8M264.5,280.7h4.9c-.2-1.2-1.1-2.1-2.4-2.1-1.3,0-2.2.8-2.4,2.1"/>
<polygon class="st1" points="278.2 282.8 276.8 284.1 276.8 286.6 274 286.6 274 273 276.8 273 276.8 280.7 281 276.8 284.4 276.8 280.3 280.9 284.8 286.6 281.3 286.6 278.2 282.8"/>
<path class="st1" d="M231.6,288h0l-2.9-2.9c-.4.5-1,.9-1.5,1.3,0,0-.2.1-.3.2l3,3c.5.5,1.2.5,1.7,0,.5-.5.5-1.2,0-1.7"/>
<path class="st1" d="M225,275.9c.2-.2.5-.3.8-.3s0,0,.1,0c-.2-.2-.5-.2-.8-.2s-.7.2-1,.5c0,0-.1,0-.2,0,0,0,0-.2,0-.3,0-.2-.1-.4-.3-.5,0,.1.1.3.1.4s0,.2,0,.3c-2.8.2-5,2.5-5,5.4s2.4,5.4,5.4,5.4,5.4-2.4,5.4-5.4c0-2.7-2-5-4.6-5.4M223,277.3c.4,0,.8.1,1.2.3.4-.2.8-.4,1.3-.4.7,0,1.3.3,1.8.8-.4-.4-1-.6-1.6-.6s-.9.1-1.2.3c0,0-.2,0-.2.1,0,0-.1,0-.2-.1-.4-.2-.8-.3-1.3-.3s-.6,0-.9.2c.3-.2.7-.3,1.1-.3M226.8,281.7c.1.3.2.6.2,1,0,1.5-1.2,2.8-2.8,2.8s-2.8-1.2-2.8-2.8,0-.7.2-1c-.6-.4-1-1.1-1-1.8,0-1.2,1-2.2,2.2-2.2s1.1.2,1.5.6c.4-.4.9-.6,1.5-.6,1.2,0,2.2,1,2.2,2.2s-.4,1.5-1,1.8"/>
<path class="st1" d="M226.7,279.7c0,.5-.4.9-.9.9s-.9-.4-.9-.9.4-.9.9-.9.2,0,.3,0c0,0,0,.1,0,.2,0,.3.2.5.5.5s0,0,.1,0c0,0,0,.1,0,.2"/>
<path class="st1" d="M223.3,279.7c0,.4-.3.7-.7.7s-.7-.3-.7-.7.3-.7.7-.7.1,0,.2,0c0,0,0,.1,0,.2,0,.2.2.4.4.4s0,0,.1,0c0,0,0,0,0,.1"/>
<g>
<g>
<rect class="st1" x="217.9" y="280.4" width=".6" height=".6"/>
<rect class="st1" x="229.8" y="280.4" width=".6" height=".6"/>
<path class="st1" d="M230.2,280.7h-.1c0-3.2-2.6-5.9-5.9-5.9s-5.9,2.6-5.9,5.9h-.1c0-1.6.6-3.1,1.8-4.3s2.7-1.8,4.3-1.8,3.1.6,4.3,1.8c1.1,1.1,1.8,2.7,1.8,4.3"/>
</g>
<rect class="st1" x="223.9" y="274.4" width=".6" height=".6"/>
<rect class="st1" x="219.9" y="274.7" width="8.4" height=".1"/>
<path class="st1" d="M220.2,274.8c0,.1,0,.2-.2.2s-.2,0-.2-.2,0-.2.2-.2c.1,0,.2,0,.2.2"/>
<path class="st1" d="M228.6,274.8c0,.1,0,.2-.2.2s-.2,0-.2-.2,0-.2.2-.2c.1,0,.2,0,.2.2"/>
</g>
</g>
<path class="st1" d="M289.4,286c0,1.6-2.4,1.6-2.4,0,0-1.6,2.4-1.6,2.4,0Z"/>
<g>
<g>
<path class="st1" d="M136.7,279.2l1.8,6.1,1.9-6.1h2.2l-2.8,8.1h-2.3l-.8-2.4-.7-2.7-.7,2.7-.8,2.4h-2.3l-2.8-8.1h2.2l1.9,6.1,1.8-6.1h1.8Z"/>
<path class="st1" d="M151.7,279.2l1.8,6.1,1.9-6.1h2.2l-2.8,8.1h-2.3l-.8-2.4-.7-2.7-.7,2.7-.8,2.4h-2.3l-2.8-8.1h2.2l1.9,6.1,1.8-6.1h1.8Z"/>
<path class="st1" d="M166.7,279.2l1.8,6.1,1.9-6.1h2.2l-2.8,8.1h-2.3l-.8-2.4-.7-2.7-.7,2.7-.8,2.4h-2.3l-2.8-8.1h2.2l1.9,6.1,1.8-6.1h1.8Z"/>
</g>
<path class="st1" d="M175.9,286.1c0,1.6-2.4,1.6-2.4,0,0-1.6,2.4-1.6,2.4,0Z"/>
<path class="st1" d="M297.7,286c-.9.9-1.9,1.3-3.1,1.3-2.3,0-4.3-1.4-4.3-4.3s2-4.3,4.3-4.3,2,.3,2.9,1.2l-1.3,1.3c-.5-.4-1.1-.6-1.6-.6-1.3,0-2.3,1-2.3,2.4s1,2.4,2.3,2.4,1.3-.2,1.8-.7l1.3,1.3Z"/>
<path class="st1" d="M306.8,283.1c0,2.3-1.6,4.2-4.2,4.2s-4.2-1.9-4.2-4.2,1.6-4.2,4.2-4.2c2.6,0,4.2,1.9,4.2,4.2ZM300.4,283.1c0,1.2.7,2.4,2.2,2.4s2.2-1.1,2.2-2.4-.9-2.4-2.2-2.4c-1.4,0-2.2,1.2-2.2,2.4Z"/>
<path class="st1" d="M313.6,287.3v-4.3c0-1.1-.6-2-1.7-2s-1.7,1-1.7,2v4.3h-2v-8.1h1.9v1c.6-.8,1.5-1.1,2.3-1.1s1.9.4,2.4,1.5c.7-1.1,1.7-1.5,2.7-1.5,2.3,0,3.4,1.4,3.4,3.8v4.4h-2v-4.4c0-1.1-.4-2-1.5-2s-1.8.9-1.8,2v4.3h-2Z"/>
</g>
</g>
</g>
<g class="st2">
<g>
<path class="st1" d="M321,243.9c0-1.2-1-2.1-2.2-2.1-.9,0-.8.1-.8-.9,0-1.1,0-2.2,0-3.3,0-.4-.1-.7-.4-1-2.3-2.7-4.7-5.3-7-8-.1-.1-.3-.2-.4-.4h-18.6c-.8.3-1.1.9-1.1,1.7,0,3.7,0,7.5,0,11.2s0,.6-.5.6c-1.5-.1-2.5,1-2.5,2.5,0,3.5,0,7,0,10.4,0,1.6.9,2.4,2.5,2.4q.6,0,.6.6c0,2.2,0,4.4,0,6.7,0,1.2.6,1.8,1.8,1.8,8,0,16,0,24,0,1.2,0,1.8-.6,1.8-1.7,0-2.3,0-4.5,0-6.8,0-.5,0-.5.5-.5.3,0,.5,0,.8,0,1-.2,1.7-1,1.7-2.1,0-3.7,0-7.4,0-11.1ZM292.5,229.8h16c.5,0,.6,0,.6.6,0,2.2,0,4.5,0,6.7,0,.8.2,1.1,1.1,1.1,1.9,0,3.8,0,5.7,0,.5,0,.5,0,.5.5,0,.9,0,1.8,0,2.7,0,.5,0,.5-.5.5-3.9,0-7.8,0-11.7,0h-11.6c-.5,0-.6,0-.6-.6,0-3.6,0-7.2,0-10.9,0-.6,0-.6.6-.6ZM315.8,264.2c-3.9,0-7.8,0-11.6,0h-11.6c-.6,0-.6,0-.6-.6,0-2,0-4,0-6,0-.5,0-.5.5-.5,7.8,0,15.6,0,23.4,0,.5,0,.5,0,.5.5,0,2,0,4,0,6,0,.6,0,.6-.6.6Z"/>
<g>
<rect class="st1" x="302.4" y="229.7" width="1.8" height="1.1"/>
<rect class="st1" x="300.7" y="230.7" width="1.8" height="1.1"/>
<rect class="st1" x="302.4" y="231.8" width="1.8" height="1.1"/>
<rect class="st1" x="300.7" y="232.8" width="1.8" height="1.1"/>
<rect class="st1" x="302.4" y="233.9" width="1.8" height="1.1"/>
<rect class="st1" x="300.7" y="234.9" width="1.8" height="1.1"/>
<rect class="st1" x="302.4" y="236" width="1.8" height="1.1"/>
<rect class="st1" x="300.7" y="237" width="1.8" height="1.1"/>
<rect class="st1" x="302.4" y="238.1" width="1.8" height="1.1"/>
<rect class="st1" x="300.7" y="239.1" width="1.8" height="1.1"/>
<rect class="st1" x="302.4" y="240.2" width="1.8" height="1.1"/>
<rect class="st1" x="300.7" y="241.3" width="1.8" height="1.1"/>
</g>
</g>
<path class="st1" d="M162.6,243.9c0-1.2-1-2.1-2.2-2.1-.9,0-.8.1-.8-.9,0-1.1,0-2.2,0-3.3,0-.4-.1-.7-.4-1-2.3-2.7-4.7-5.3-7-8-.1-.1-.3-.2-.4-.4h-18.6c-.8.3-1.1.9-1.1,1.7,0,3.7,0,7.5,0,11.2s0,.6-.6.6c-1.5-.1-2.5,1-2.5,2.5,0,3.5,0,7,0,10.4,0,1.6.9,2.4,2.4,2.4q.6,0,.6.6c0,2.2,0,4.4,0,6.7,0,1.2.6,1.8,1.8,1.8,8,0,16,0,24,0,1.2,0,1.8-.6,1.8-1.7,0-2.3,0-4.5,0-6.8,0-.5,0-.5.5-.5.3,0,.5,0,.8,0,1-.2,1.7-1,1.7-2.1,0-3.7,0-7.4,0-11.1ZM134.2,229.8h16c.5,0,.6,0,.6.6,0,2.2,0,4.5,0,6.7,0,.8.2,1.1,1.1,1.1,1.9,0,3.8,0,5.7,0,.5,0,.5,0,.5.5,0,.9,0,1.8,0,2.7,0,.5,0,.5-.5.5-3.9,0-7.8,0-11.7,0h-11.6c-.6,0-.6,0-.6-.6,0-3.6,0-7.2,0-10.9,0-.6,0-.6.6-.6ZM157.5,264.2c-3.9,0-7.8,0-11.6,0h-11.6c-.6,0-.6,0-.6-.6,0-2,0-4,0-6,0-.5,0-.5.5-.5,7.8,0,15.6,0,23.4,0,.5,0,.5,0,.5.5,0,2,0,4,0,6,0,.6,0,.6-.6.6Z"/>
<g>
<path class="st1" d="M134.8,253.3v-7.9h2.8c.4,0,.7,0,1.1.2s.6.3.9.5c.2.2.4.5.6.8.1.3.2.6.2,1s0,.7-.2,1-.3.6-.6.8c-.2.2-.5.4-.9.5s-.7.2-1.1.2h-1.3v2.9h-1.5ZM137.6,249c.4,0,.7-.1.9-.3.2-.2.3-.5.3-.8s0-.3,0-.4-.1-.2-.2-.3c0,0-.2-.2-.4-.2-.1,0-.3,0-.5,0h-1.3v2.2h1.3Z"/>
<path class="st1" d="M141.3,245.4h1.7l3.2,5.3h0v-1.5c0,0,0-3.8,0-3.8h1.5v7.9h-1.6l-3.4-5.6h0v1.5c0,0,0,4.1,0,4.1h-1.5v-7.9h0Z"/>
<path class="st1" d="M152.9,248.9h3.9c0,0,0,.2,0,.3,0,.1,0,.2,0,.4,0,.5,0,1-.2,1.4s-.4.8-.7,1.2c-.4.4-.8.7-1.3.9-.5.2-1.1.3-1.7.3s-1.1-.1-1.6-.3c-.5-.2-.9-.5-1.3-.9-.4-.4-.7-.8-.9-1.3s-.3-1.1-.3-1.6.1-1.1.3-1.6.5-.9.9-1.3c.4-.4.8-.7,1.3-.9s1-.3,1.6-.3,1.2.1,1.7.3.9.5,1.3.9l-1,1c-.3-.3-.5-.5-.9-.6-.3-.1-.7-.2-1.1-.2s-.7,0-1,.2c-.3.1-.6.3-.9.5-.2.2-.4.5-.6.9-.1.3-.2.7-.2,1.1s0,.8.2,1.1c.1.3.3.6.6.9.2.2.5.4.9.5.3.1.7.2,1,.2s.8,0,1.1-.2c.3-.1.5-.3.7-.5.1-.1.3-.3.4-.5.1-.2.2-.4.2-.7h-2.5v-1.3h0Z"/>
</g>
<path class="st1" d="M199,243.9c0-1.2-1-2.1-2.2-2.1-.9,0-.8.1-.8-.9,0-1.1,0-2.2,0-3.3,0-.4-.1-.7-.4-1-2.3-2.7-4.7-5.3-7-8-.1-.1-.3-.2-.4-.4h-18.6c-.8.3-1.1.9-1.1,1.7,0,3.7,0,7.5,0,11.2s0,.6-.6.6c-1.5-.1-2.5,1-2.5,2.5,0,3.5,0,7,0,10.4,0,1.6.9,2.4,2.4,2.4q.6,0,.6.6c0,2.2,0,4.4,0,6.7,0,1.2.6,1.8,1.8,1.8,8,0,16,0,24,0,1.2,0,1.8-.6,1.8-1.7,0-2.3,0-4.5,0-6.8,0-.5,0-.5.5-.5.3,0,.5,0,.8,0,1-.2,1.7-1,1.7-2.1,0-3.7,0-7.4,0-11.1ZM170.6,229.8h16c.5,0,.6,0,.6.6,0,2.2,0,4.5,0,6.7,0,.8.2,1.1,1.1,1.1,1.9,0,3.8,0,5.7,0,.5,0,.5,0,.5.5,0,.9,0,1.8,0,2.7,0,.5,0,.5-.5.5-3.9,0-7.8,0-11.7,0h-11.6c-.6,0-.6,0-.6-.6,0-3.6,0-7.2,0-10.9,0-.6,0-.6.6-.6ZM193.9,264.2c-3.9,0-7.8,0-11.6,0h-11.6c-.6,0-.6,0-.6-.6,0-2,0-4,0-6,0-.5,0-.5.5-.5,7.8,0,15.6,0,23.4,0,.5,0,.5,0,.5.5,0,2,0,4,0,6,0,.6,0,.6-.6.6Z"/>
<g>
<path class="st1" d="M180.1,245.4h1.7l3,7.9h-1.6l-.7-1.9h-3l-.7,1.9h-1.6l3-7.9ZM182,250l-.7-2-.3-1h0l-.3,1-.7,2h2.1Z"/>
<path class="st1" d="M186.3,247.1c-.1,0-.2,0-.4,0s-.2-.1-.3-.2c0,0-.2-.2-.2-.3,0-.1,0-.2,0-.4s0-.3,0-.4c0-.1.1-.2.2-.3,0,0,.2-.2.3-.2.1,0,.2,0,.4,0,.3,0,.5,0,.7.3.2.2.3.4.3.7s0,.5-.3.7c-.2.2-.4.3-.7.3ZM185.6,253.3v-5.4h1.4v5.4h-1.4Z"/>
</g>
<path class="st1" d="M236,243.9c0-1.2-1-2.1-2.2-2.1-.9,0-.8.1-.8-.9,0-1.1,0-2.2,0-3.3,0-.4-.1-.7-.4-1-2.3-2.7-4.7-5.3-7-8-.1-.1-.3-.2-.4-.4h-18.6c-.8.3-1.1.9-1.1,1.7,0,3.7,0,7.5,0,11.2s0,.6-.6.6c-1.5-.1-2.5,1-2.5,2.5,0,3.5,0,7,0,10.4,0,1.6.9,2.4,2.4,2.4q.6,0,.6.6c0,2.2,0,4.4,0,6.7,0,1.2.6,1.8,1.8,1.8,8,0,16,0,24,0,1.2,0,1.8-.6,1.8-1.7,0-2.3,0-4.5,0-6.8,0-.5,0-.5.5-.5.3,0,.5,0,.8,0,1-.2,1.7-1,1.7-2.1,0-3.7,0-7.4,0-11.1ZM207.6,229.8h16c.5,0,.6,0,.6.6,0,2.2,0,4.5,0,6.7,0,.8.2,1.1,1.1,1.1,1.9,0,3.8,0,5.7,0,.5,0,.5,0,.5.5,0,.9,0,1.8,0,2.7,0,.5,0,.5-.5.5-3.9,0-7.8,0-11.7,0h-11.6c-.6,0-.6,0-.6-.6,0-3.6,0-7.2,0-10.9,0-.6,0-.6.6-.6ZM230.9,264.2c-3.9,0-7.8,0-11.6,0h-11.6c-.6,0-.6,0-.6-.6,0-2,0-4,0-6,0-.5,0-.5.5-.5,7.8,0,15.6,0,23.4,0,.5,0,.5,0,.5.5,0,2,0,4,0,6,0,.6,0,.6-.6.6Z"/>
<g>
<path class="st1" d="M211.4,253.5c-.3,0-.7,0-1-.1-.3,0-.6-.2-.8-.4-.3-.2-.5-.4-.7-.7-.2-.3-.3-.6-.5-1l1.4-.6c.1.4.3.7.5,1,.3.3.6.4,1,.4s.3,0,.4,0c.1,0,.3,0,.4-.2s.2-.2.3-.3c0-.1,0-.3,0-.4s0-.3,0-.4c0-.1-.1-.2-.3-.3-.1-.1-.3-.2-.5-.3-.2,0-.4-.2-.7-.3l-.5-.2c-.2,0-.4-.2-.6-.3-.2-.1-.4-.3-.6-.5s-.3-.4-.4-.6c-.1-.2-.2-.5-.2-.8s0-.6.2-.9c.1-.3.3-.5.5-.7.2-.2.5-.4.8-.5s.7-.2,1-.2.7,0,1,.2.5.2.7.4c.2.2.4.3.5.5.1.2.2.4.3.6l-1.3.6c0-.2-.2-.4-.4-.6-.2-.2-.5-.3-.8-.3s-.6,0-.8.2-.3.3-.3.6.1.4.3.6c.2.2.5.3,1,.5l.5.2c.3.1.6.2.9.4.3.1.5.3.7.5.2.2.3.4.4.7.1.3.1.5.1.9s0,.8-.2,1.1c-.2.3-.4.5-.6.7-.3.2-.5.3-.9.4-.3,0-.6.1-1,.1Z"/>
<path class="st1" d="M214.5,245.4h1.6l1.6,4.8.3,1h0l.3-1,1.7-4.8h1.6l-2.9,7.9h-1.6l-2.8-7.9Z"/>
<path class="st1" d="M226.1,248.9h3.9c0,0,0,.2,0,.3s0,.2,0,.4c0,.5,0,1-.2,1.4-.2.4-.4.8-.7,1.2-.4.4-.8.7-1.3.9-.5.2-1.1.3-1.7.3s-1.1-.1-1.6-.3-.9-.5-1.3-.9c-.4-.4-.7-.8-.9-1.3-.2-.5-.3-1.1-.3-1.6s.1-1.1.3-1.6.5-.9.9-1.3c.4-.4.8-.7,1.3-.9.5-.2,1-.3,1.6-.3s1.2.1,1.7.3.9.5,1.3.9l-1,1c-.3-.3-.5-.5-.9-.6-.3-.1-.7-.2-1.1-.2s-.7,0-1,.2c-.3.1-.6.3-.8.5-.2.2-.4.5-.6.9-.1.3-.2.7-.2,1.1s0,.8.2,1.1c.1.3.3.6.6.9.2.2.5.4.9.5.3.1.7.2,1,.2s.8,0,1.1-.2.5-.3.7-.5c.1-.1.3-.3.4-.5.1-.2.2-.4.2-.7h-2.5v-1.3h0Z"/>
</g>
<path class="st1" d="M272.3,243.9c0-1.2-1-2.1-2.2-2.1-.9,0-.8.1-.8-.9,0-1.1,0-2.2,0-3.3,0-.4-.1-.7-.4-1-2.3-2.7-4.7-5.3-7-8-.1-.1-.3-.2-.4-.4h-18.6c-.8.3-1.1.9-1.1,1.7,0,3.7,0,7.5,0,11.2s0,.6-.5.6c-1.5-.1-2.5,1-2.5,2.5,0,3.5,0,7,0,10.4,0,1.6.9,2.4,2.5,2.4q.6,0,.6.6c0,2.2,0,4.4,0,6.7,0,1.2.6,1.8,1.8,1.8,8,0,16,0,24,0,1.2,0,1.8-.6,1.8-1.7,0-2.3,0-4.5,0-6.8,0-.5,0-.5.5-.5.3,0,.5,0,.8,0,1-.2,1.7-1,1.7-2.1,0-3.7,0-7.4,0-11.1ZM243.8,229.8h16c.5,0,.6,0,.6.6,0,2.2,0,4.5,0,6.7,0,.8.2,1.1,1.1,1.1,1.9,0,3.8,0,5.7,0,.5,0,.5,0,.5.5,0,.9,0,1.8,0,2.7,0,.5,0,.5-.5.5-3.9,0-7.8,0-11.7,0h-11.6c-.5,0-.6,0-.6-.6,0-3.6,0-7.2,0-10.9,0-.6,0-.6.6-.6ZM267.1,264.2c-3.9,0-7.8,0-11.6,0h-11.6c-.6,0-.6,0-.6-.6,0-2,0-4,0-6,0-.5,0-.5.5-.5,7.8,0,15.6,0,23.4,0,.5,0,.5,0,.5.5,0,2,0,4,0,6,0,.6,0,.6-.6.6Z"/>
<g>
<path class="st1" d="M248,246.8v1.8h3.2v1.4h-3.2v1.8h3.5v1.4h-5v-7.9h5v1.4h-3.5Z"/>
<path class="st1" d="M252.9,253.3v-7.9h2.8c.4,0,.7,0,1.1.2s.6.3.9.5.4.5.6.8c.1.3.2.6.2,1s0,.7-.2,1c-.1.3-.3.6-.6.8s-.5.4-.9.5-.7.2-1.1.2h-1.3v2.9h-1.5ZM255.7,249c.4,0,.7-.1.9-.3.2-.2.3-.5.3-.8s0-.3,0-.4-.1-.2-.2-.3c0,0-.2-.2-.4-.2-.1,0-.3,0-.5,0h-1.3v2.2h1.3Z"/>
<path class="st1" d="M261.8,253.5c-.3,0-.7,0-1-.1-.3,0-.6-.2-.8-.4-.3-.2-.5-.4-.7-.7-.2-.3-.3-.6-.5-1l1.4-.6c.1.4.3.7.5,1,.3.3.6.4,1,.4s.3,0,.4,0c.1,0,.3,0,.4-.2.1,0,.2-.2.3-.3,0-.1.1-.3.1-.4s0-.3,0-.4c0-.1-.1-.2-.3-.3-.1-.1-.3-.2-.5-.3-.2,0-.4-.2-.7-.3l-.5-.2c-.2,0-.4-.2-.6-.3-.2-.1-.4-.3-.6-.5s-.3-.4-.4-.6c-.1-.2-.2-.5-.2-.8s0-.6.2-.9c.1-.3.3-.5.5-.7.2-.2.5-.4.8-.5.3-.1.7-.2,1-.2s.7,0,1,.2.5.2.7.4c.2.2.4.3.5.5.1.2.2.4.3.6l-1.3.6c0-.2-.2-.4-.4-.6-.2-.2-.5-.3-.8-.3s-.6,0-.8.2c-.2.2-.3.3-.3.6s.1.4.3.6.5.3,1,.5l.5.2c.3.1.6.2.9.4.3.1.5.3.7.5.2.2.3.4.4.7,0,.3.1.5.1.9s0,.8-.2,1.1c-.2.3-.4.5-.6.7-.3.2-.5.3-.9.4-.3,0-.6.1-1,.1Z"/>
</g>
<g>
<path class="st1" d="M296.4,251.8l4-5h-3.8v-1.4h5.6v1.5l-4,5h4v1.4h-5.8v-1.5h0Z"/>
<path class="st1" d="M303.4,245.4h1.5v7.9h-1.5v-7.9Z"/>
<path class="st1" d="M306.5,253.3v-7.9h2.8c.4,0,.7,0,1.1.2s.6.3.9.5.4.5.6.8c.1.3.2.6.2,1s0,.7-.2,1c-.1.3-.3.6-.6.8s-.5.4-.9.5-.7.2-1.1.2h-1.3v2.9h-1.5ZM309.3,249c.4,0,.7-.1.9-.3.2-.2.3-.5.3-.8s0-.3,0-.4-.1-.2-.2-.3c0,0-.2-.2-.4-.2-.1,0-.3,0-.5,0h-1.3v2.2h1.3Z"/>
</g>
</g>
<g class="st2">
<path class="st1" d="M275.1,245.9h9.1v2.2h-9.1v-2.2ZM275.1,249.7h9.1v2.2h-9.1v-2.2Z"/>
</g>
</g>
<g class="st2">
<path class="st1" d="M131.8,291.5h1.6c1.3,0,2.1.8,2.1,2s-.9,2-2.1,2h-1.6v-3.9ZM133.3,295.1c1,0,1.7-.7,1.7-1.6s-.7-1.6-1.7-1.6h-1.2v3.2h1.2Z"/>
<path class="st1" d="M140.1,293.5c0-1.1.9-2,2.1-2s2.1.9,2.1,2-.9,2-2.1,2-2.1-.9-2.1-2ZM143.9,293.5c0-.9-.7-1.6-1.7-1.6s-1.7.7-1.7,1.6.7,1.6,1.7,1.6c.9,0,1.7-.7,1.7-1.6Z"/>
<path class="st1" d="M154.6,291.5l-1.3,3.9h-.4l-1.2-3.4-1.2,3.4h-.4l-1.3-3.9h.4l1.1,3.4,1.2-3.4h.4l1.2,3.4,1.2-3.4h.4Z"/>
<path class="st1" d="M162.7,291.5v3.9h-.3l-2.5-3.2v3.2h-.4v-3.9h.3l2.5,3.2v-3.2h.4Z"/>
<path class="st1" d="M168.1,291.5h.4v3.6h2.2v.4h-2.6v-3.9h0Z"/>
<path class="st1" d="M175.1,293.5c0-1.1.9-2,2.1-2s2.1.9,2.1,2-.9,2-2.1,2-2.1-.9-2.1-2ZM178.8,293.5c0-.9-.7-1.6-1.7-1.6s-1.7.7-1.7,1.6.7,1.6,1.7,1.6c.9,0,1.7-.7,1.7-1.6Z"/>
<path class="st1" d="M186.6,294.4h-2.2l-.5,1.1h-.4l1.8-3.9h.4l1.8,3.9h-.4l-.5-1.1ZM186.5,294l-.9-2.1-.9,2.1h1.9Z"/>
<path class="st1" d="M192.3,291.5h1.6c1.3,0,2.1.8,2.1,2s-.9,2-2.1,2h-1.6v-3.9ZM193.9,295.1c1,0,1.7-.7,1.7-1.6s-.7-1.6-1.7-1.6h-1.2v3.2h1.2Z"/>
<path class="st1" d="M209,294.4h-2.2l-.5,1.1h-.4l1.8-3.9h.4l1.8,3.9h-.4l-.5-1.1ZM208.9,294l-.9-2.1-.9,2.1h1.9Z"/>
<path class="st1" d="M214.7,291.5h.4v3.6h2.2v.4h-2.6v-3.9h0Z"/>
<path class="st1" d="M222.1,291.5h.4v3.6h2.2v.4h-2.6v-3.9h0Z"/>
<path class="st1" d="M235.4,291.8v1.6h2v.4h-2v1.7h-.4v-3.9h2.7v.4h-2.3Z"/>
<path class="st1" d="M242.7,291.5h.4v3.9h-.4v-3.9Z"/>
<path class="st1" d="M248.5,291.5h.4v3.6h2.2v.4h-2.6v-3.9h0Z"/>
<path class="st1" d="M258.6,295.1v.4h-2.8v-3.9h2.7v.4h-2.3v1.4h2v.4h-2v1.5h2.4Z"/>
<path class="st1" d="M269.7,291.8v1.6h2v.4h-2v1.7h-.4v-3.9h2.7v.4h-2.3Z"/>
<path class="st1" d="M276.5,293.5c0-1.1.9-2,2.1-2s2.1.9,2.1,2-.9,2-2.1,2-2.1-.9-2.1-2ZM280.2,293.5c0-.9-.7-1.6-1.7-1.6s-1.7.7-1.7,1.6.7,1.6,1.7,1.6c.9,0,1.7-.7,1.7-1.6Z"/>
<path class="st1" d="M288.3,295.4l-.9-1.3c-.1,0-.2,0-.3,0h-1.1v1.3h-.4v-3.9h1.5c1,0,1.6.5,1.6,1.4s-.3,1.1-.9,1.2l1,1.4h-.5ZM288.3,292.8c0-.6-.4-1-1.2-1h-1v2h1c.8,0,1.2-.4,1.2-1Z"/>
<path class="st1" d="M297.9,291.5v3.9h-.4v-3.2l-1.6,2.7h-.2l-1.6-2.6v3.1h-.4v-3.9h.3l1.7,2.9,1.7-2.9h.3Z"/>
<path class="st1" d="M305.7,294.4h-2.2l-.5,1.1h-.4l1.8-3.9h.4l1.8,3.9h-.4l-.5-1.1ZM305.6,294l-.9-2.1-.9,2.1h1.9Z"/>
<path class="st1" d="M312,291.8h-1.4v-.4h3.2v.4h-1.4v3.6h-.4v-3.6h0Z"/>
<path class="st1" d="M318.1,295l.2-.3c.3.3.8.5,1.3.5.7,0,1.1-.3,1.1-.7,0-1.1-2.4-.4-2.4-1.9s.5-1.1,1.5-1.1.9.1,1.2.4v.3c-.5-.2-.9-.3-1.2-.3-.7,0-1,.3-1,.7,0,1.1,2.4.4,2.4,1.9s-.5,1.1-1.5,1.1c-.6,0-1.2-.2-1.5-.5Z"/>
</g>
</g>
<path class="st0" d="M376.7,125.2c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l4.5-15.5c1.5-5.1,6.9-9.3,12-9.3h10c1.2,0,2.2-.8,2.6-1.9l1.7-6c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1h-21.9c-.1,0-.2,0-.4,0h-7.4c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l.9-3c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1h-2.8c-1.2,0-2.2.8-2.6,1.9l-1.3,4.5c-1.5,5.1-6.9,9.3-12,9.3h-10c-1.2,0-2.2.8-2.6,1.9l-2.8,9.8s0,.1,0,.2l-4,14s0,.1,0,.2l-2.9,10.1c-1.5,5.1-6.9,9.3-12,9.3h-15.5c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l13-45.2s0-.1,0-.2l.9-3.2c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1h-2.8c-1.2,0-2.2.8-2.6,1.9l-5.7,19.6s0,.1,0,.2l-2.1,7.4c-1.5,5.1-6.9,9.3-12,9.3h-15.5c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l9.4-32.8c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1h-2.8c-1.2,0-2.2.8-2.6,1.9l-4,14s0,.1,0,.2l-5.8,20.1c-1.5,5.1-6.9,9.3-12,9.3h-15.5c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l4.5-15.5s0-.1,0-.2l4.9-17.1c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1h-2.8c-1.2,0-2.2.8-2.6,1.9l-.4,1.4s0,.1,0,.2l-9.1,31.4s0,.1,0,.2l-.3,1.1c-1.5,5.1-6.9,9.3-12,9.3h-15.5c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l.9-3c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1h-2.8c-1.2,0-2.2.8-2.6,1.9l-1.3,4.5c-1.5,5.1-6.9,9.3-12,9.3h-10.1c-1.2,0-2.2.8-2.6,1.9l-3.3,11.5c-1.5,5.1-6.9,9.3-12,9.3h-15.5c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l2.9-10c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1h-8.3c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l.9-3c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1h-2.8c-1.2,0-2.2.8-2.6,1.9l-1.3,4.5c-1.5,5.1-6.9,9.3-12,9.3h-21.3s-15.6,0-15.6,0c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l2.9-10.1c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1H7c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l7.7-26.9s0-.1,0-.2l2.5-8.7c0,0,0-.1,0-.2l.7-2.3c1.5-5.1,6.9-9.3,12-9.3h10c1.2,0,2.2-.8,2.6-1.9l3.3-11.5C40.7,4.2,46,0,51.2,0h21.3S88,0,88,0c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-4.5,15.5c-1.5,5.1-6.9,9.3-12,9.3h-21.3s-9.9,0-9.9,0c-1.2,0-2.2.8-2.6,1.9l-4.4,15.3s0,.1,0,.2l-3.8,13.3c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1s14.1,0,14.1,0h15.5c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-.9,3.1c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1h2.8c1.2,0,2.2-.8,2.6-1.9l1.7-5.8s6.2-21.5,6.2-21.5c1.5-5.1,6.9-9.3,12-9.3h9.9c1.2,0,2.2-.8,2.6-1.9l3.3-11.5c1.5-5.1,6.9-9.3,12-9.3h15.5c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-2.9,10c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1h8.4c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-7.4,25.9c-.2.8,0,1.7.4,2.3s1.3,1.1,2.1,1.1h2.8c1.2,0,2.2-.8,2.6-1.9l3.5-12.2s0-.1,0-.2l9.1-31.4s0-.1,0-.2l1.8-6.1c1.5-5.1,6.9-9.3,12-9.3h36.8c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-2.9,10.1c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1h2.9c1.2,0,2.2-.8,2.6-1.9l3.3-11.5c1.5-5.1,6.9-9.3,12-9.3h15.5c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-2.9,10c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1h8.3c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-.9,3.1c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1h2.8c1.2,0,2.2-.8,2.6-1.9l7.9-27.3c1.5-5.1,6.9-9.3,12-9.3h10c1.2,0,2.2-.8,2.6-1.9l3.3-11.5c1.5-5.1,6.9-9.3,12-9.3h15.5c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-4.5,15.5c-1.5,5.1-6.9,9.3-12,9.3h-10c-1.2,0-2.2.8-2.6,1.9l-1.7,6c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1h8.4c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-.9,3.1c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1h2.8c1.2,0,2.2-.8,2.6-1.9l7.9-27.3c1.5-5.1,6.9-9.3,12-9.3h15.5c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-7,24.4s0,.1,0,.2l-.8,2.6c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1h2.8c1.2,0,2.2-.8,2.6-1.9l8.3-28.6c1.5-5.1,6.9-9.3,12-9.3h15.5c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-17.6,61.1c-1.5,5.1-6.9,9.3-12,9.3h-10c-1.2,0-2.2.8-2.6,1.9l-3.3,11.5c-1.5,5.1-6.9,9.3-12,9.3h-15.5ZM125.5,57c-1.2,0-2.2.8-2.6,1.9l-8.3,28.8c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1h2.8c1.2,0,2.2-.8,2.6-1.9l8.3-28.8c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1h-2.8Z"/>
</svg>

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 450 125.2">
<!-- Generator: Adobe Illustrator 29.5.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 141) -->
<defs>
<style>
.st0 {
fill: #f3ff56;
}
.st1 {
fill: #fff;
}
.st2 {
opacity: .6;
}
.st3 {
display: none;
}
</style>
</defs>
<g id="watermark" class="st3">
<g>
<g id="watermark1" data-name="watermark" class="st2">
<g>
<g>
<g>
<polygon class="st1" points="188.8 273.8 183.2 286.6 180.3 286.6 174.7 273.8 177.9 273.8 181.8 283 185.8 273.8 188.8 273.8"/>
<path class="st1" d="M198.5,282.3h-7.5c.3,1.2,1.3,2,2.8,2s1.8-.3,2.5-1l1.5,1.7c-.9,1-2.3,1.6-4.1,1.6-3.4,0-5.6-2.1-5.6-5.1s2.3-5.1,5.3-5.1,5.1,1.9,5.1,5.1c0,.2,0,.5,0,.8M191,280.7h4.9c-.2-1.2-1.1-2.1-2.4-2.1-1.3,0-2.2.8-2.4,2.1"/>
<path class="st1" d="M199.7,281.5c0-3,2.3-5.1,5.5-5.1s3.7.9,4.4,2.5l-2.2,1.2c-.5-.9-1.3-1.4-2.2-1.4-1.4,0-2.6,1-2.6,2.7s1.1,2.7,2.6,2.7,1.7-.4,2.2-1.4l2.2,1.2c-.7,1.6-2.3,2.5-4.4,2.5-3.2,0-5.5-2.1-5.5-5.1"/>
<path class="st1" d="M217.8,286c-.6.4-1.4.6-2.3.6-2.3,0-3.7-1.2-3.7-3.5v-4.1h-1.5v-2.2h1.5v-2.4h2.9v2.4h2.5v2.2h-2.5v4c0,.8.5,1.3,1.2,1.3s.8-.1,1.2-.4l.8,2Z"/>
</g>
<path class="st1" d="M237.5,276.6v2.6c-.2,0-.4,0-.6,0-1.6,0-2.6.9-2.6,2.8v4.7h-2.9v-9.9h2.7v1.3c.7-1,1.9-1.5,3.4-1.5"/>
<path class="st1" d="M238.3,285.1l1-2.2c1.1.8,2.7,1.3,4.2,1.3s2.5-.6,2.5-1.4c0-2.4-7.5-.8-7.5-5.5s1.8-4,5.4-4,3.3.4,4.5,1.1l-.9,2.3c-1.2-.7-2.4-1-3.6-1-1.8,0-2.4.7-2.4,1.5,0,2.4,7.5.7,7.5,5.5s-1.8,4-5.5,4c-2,0-4.1-.6-5.2-1.5"/>
<path class="st1" d="M260.4,282.3h-7.5c.3,1.2,1.3,2,2.8,2s1.8-.3,2.5-1l1.5,1.7c-.9,1-2.3,1.6-4.1,1.6-3.4,0-5.6-2.1-5.6-5.1s2.3-5.1,5.3-5.1,5.1,1.9,5.1,5.1c0,.2,0,.5,0,.8M252.9,280.7h4.9c-.2-1.2-1.1-2.1-2.4-2.1-1.3,0-2.2.8-2.4,2.1"/>
<path class="st1" d="M272,282.3h-7.5c.3,1.2,1.3,2,2.8,2s1.8-.3,2.5-1l1.5,1.7c-.9,1-2.3,1.6-4.1,1.6-3.4,0-5.6-2.1-5.6-5.1s2.3-5.1,5.3-5.1,5.1,1.9,5.1,5.1c0,.2,0,.5,0,.8M264.5,280.7h4.9c-.2-1.2-1.1-2.1-2.4-2.1-1.3,0-2.2.8-2.4,2.1"/>
<polygon class="st1" points="278.2 282.8 276.8 284.1 276.8 286.6 274 286.6 274 273 276.8 273 276.8 280.7 281 276.8 284.4 276.8 280.3 280.9 284.8 286.6 281.3 286.6 278.2 282.8"/>
<path class="st1" d="M231.6,288h0l-2.9-2.9c-.4.5-1,.9-1.5,1.3,0,0-.2.1-.3.2l3,3c.5.5,1.2.5,1.7,0,.5-.5.5-1.2,0-1.7"/>
<path class="st1" d="M225,275.9c.2-.2.5-.3.8-.3s0,0,.1,0c-.2-.2-.5-.2-.8-.2s-.7.2-1,.5c0,0-.1,0-.2,0,0,0,0-.2,0-.3,0-.2-.1-.4-.3-.5,0,.1.1.3.1.4s0,.2,0,.3c-2.8.2-5,2.5-5,5.4s2.4,5.4,5.4,5.4,5.4-2.4,5.4-5.4c0-2.7-2-5-4.6-5.4M223,277.3c.4,0,.8.1,1.2.3.4-.2.8-.4,1.3-.4.7,0,1.3.3,1.8.8-.4-.4-1-.6-1.6-.6s-.9.1-1.2.3c0,0-.2,0-.2.1,0,0-.1,0-.2-.1-.4-.2-.8-.3-1.3-.3s-.6,0-.9.2c.3-.2.7-.3,1.1-.3M226.8,281.7c.1.3.2.6.2,1,0,1.5-1.2,2.8-2.8,2.8s-2.8-1.2-2.8-2.8,0-.7.2-1c-.6-.4-1-1.1-1-1.8,0-1.2,1-2.2,2.2-2.2s1.1.2,1.5.6c.4-.4.9-.6,1.5-.6,1.2,0,2.2,1,2.2,2.2s-.4,1.5-1,1.8"/>
<path class="st1" d="M226.7,279.7c0,.5-.4.9-.9.9s-.9-.4-.9-.9.4-.9.9-.9.2,0,.3,0c0,0,0,.1,0,.2,0,.3.2.5.5.5s0,0,.1,0c0,0,0,.1,0,.2"/>
<path class="st1" d="M223.3,279.7c0,.4-.3.7-.7.7s-.7-.3-.7-.7.3-.7.7-.7.1,0,.2,0c0,0,0,.1,0,.2,0,.2.2.4.4.4s0,0,.1,0c0,0,0,0,0,.1"/>
<g>
<g>
<rect class="st1" x="217.9" y="280.4" width=".6" height=".6"/>
<rect class="st1" x="229.8" y="280.4" width=".6" height=".6"/>
<path class="st1" d="M230.2,280.7h-.1c0-3.2-2.6-5.9-5.9-5.9s-5.9,2.6-5.9,5.9h-.1c0-1.6.6-3.1,1.8-4.3s2.7-1.8,4.3-1.8,3.1.6,4.3,1.8c1.1,1.1,1.8,2.7,1.8,4.3"/>
</g>
<rect class="st1" x="223.9" y="274.4" width=".6" height=".6"/>
<rect class="st1" x="219.9" y="274.7" width="8.4" height=".1"/>
<path class="st1" d="M220.2,274.8c0,.1,0,.2-.2.2s-.2,0-.2-.2,0-.2.2-.2c.1,0,.2,0,.2.2"/>
<path class="st1" d="M228.6,274.8c0,.1,0,.2-.2.2s-.2,0-.2-.2,0-.2.2-.2c.1,0,.2,0,.2.2"/>
</g>
</g>
<path class="st1" d="M289.4,286c0,1.6-2.4,1.6-2.4,0,0-1.6,2.4-1.6,2.4,0Z"/>
<g>
<g>
<path class="st1" d="M136.7,279.2l1.8,6.1,1.9-6.1h2.2l-2.8,8.1h-2.3l-.8-2.4-.7-2.7-.7,2.7-.8,2.4h-2.3l-2.8-8.1h2.2l1.9,6.1,1.8-6.1h1.8Z"/>
<path class="st1" d="M151.7,279.2l1.8,6.1,1.9-6.1h2.2l-2.8,8.1h-2.3l-.8-2.4-.7-2.7-.7,2.7-.8,2.4h-2.3l-2.8-8.1h2.2l1.9,6.1,1.8-6.1h1.8Z"/>
<path class="st1" d="M166.7,279.2l1.8,6.1,1.9-6.1h2.2l-2.8,8.1h-2.3l-.8-2.4-.7-2.7-.7,2.7-.8,2.4h-2.3l-2.8-8.1h2.2l1.9,6.1,1.8-6.1h1.8Z"/>
</g>
<path class="st1" d="M175.9,286.1c0,1.6-2.4,1.6-2.4,0,0-1.6,2.4-1.6,2.4,0Z"/>
<path class="st1" d="M297.7,286c-.9.9-1.9,1.3-3.1,1.3-2.3,0-4.3-1.4-4.3-4.3s2-4.3,4.3-4.3,2,.3,2.9,1.2l-1.3,1.3c-.5-.4-1.1-.6-1.6-.6-1.3,0-2.3,1-2.3,2.4s1,2.4,2.3,2.4,1.3-.2,1.8-.7l1.3,1.3Z"/>
<path class="st1" d="M306.8,283.1c0,2.3-1.6,4.2-4.2,4.2s-4.2-1.9-4.2-4.2,1.6-4.2,4.2-4.2c2.6,0,4.2,1.9,4.2,4.2ZM300.4,283.1c0,1.2.7,2.4,2.2,2.4s2.2-1.1,2.2-2.4-.9-2.4-2.2-2.4c-1.4,0-2.2,1.2-2.2,2.4Z"/>
<path class="st1" d="M313.6,287.3v-4.3c0-1.1-.6-2-1.7-2s-1.7,1-1.7,2v4.3h-2v-8.1h1.9v1c.6-.8,1.5-1.1,2.3-1.1s1.9.4,2.4,1.5c.7-1.1,1.7-1.5,2.7-1.5,2.3,0,3.4,1.4,3.4,3.8v4.4h-2v-4.4c0-1.1-.4-2-1.5-2s-1.8.9-1.8,2v4.3h-2Z"/>
</g>
</g>
</g>
<g class="st2">
<g>
<path class="st1" d="M321,243.9c0-1.2-1-2.1-2.2-2.1-.9,0-.8.1-.8-.9,0-1.1,0-2.2,0-3.3,0-.4-.1-.7-.4-1-2.3-2.7-4.7-5.3-7-8-.1-.1-.3-.2-.4-.4h-18.6c-.8.3-1.1.9-1.1,1.7,0,3.7,0,7.5,0,11.2s0,.6-.5.6c-1.5-.1-2.5,1-2.5,2.5,0,3.5,0,7,0,10.4,0,1.6.9,2.4,2.5,2.4q.6,0,.6.6c0,2.2,0,4.4,0,6.7,0,1.2.6,1.8,1.8,1.8,8,0,16,0,24,0,1.2,0,1.8-.6,1.8-1.7,0-2.3,0-4.5,0-6.8,0-.5,0-.5.5-.5.3,0,.5,0,.8,0,1-.2,1.7-1,1.7-2.1,0-3.7,0-7.4,0-11.1ZM292.5,229.8h16c.5,0,.6,0,.6.6,0,2.2,0,4.5,0,6.7,0,.8.2,1.1,1.1,1.1,1.9,0,3.8,0,5.7,0,.5,0,.5,0,.5.5,0,.9,0,1.8,0,2.7,0,.5,0,.5-.5.5-3.9,0-7.8,0-11.7,0h-11.6c-.5,0-.6,0-.6-.6,0-3.6,0-7.2,0-10.9,0-.6,0-.6.6-.6ZM315.8,264.2c-3.9,0-7.8,0-11.6,0h-11.6c-.6,0-.6,0-.6-.6,0-2,0-4,0-6,0-.5,0-.5.5-.5,7.8,0,15.6,0,23.4,0,.5,0,.5,0,.5.5,0,2,0,4,0,6,0,.6,0,.6-.6.6Z"/>
<g>
<rect class="st1" x="302.4" y="229.7" width="1.8" height="1.1"/>
<rect class="st1" x="300.7" y="230.7" width="1.8" height="1.1"/>
<rect class="st1" x="302.4" y="231.8" width="1.8" height="1.1"/>
<rect class="st1" x="300.7" y="232.8" width="1.8" height="1.1"/>
<rect class="st1" x="302.4" y="233.9" width="1.8" height="1.1"/>
<rect class="st1" x="300.7" y="234.9" width="1.8" height="1.1"/>
<rect class="st1" x="302.4" y="236" width="1.8" height="1.1"/>
<rect class="st1" x="300.7" y="237" width="1.8" height="1.1"/>
<rect class="st1" x="302.4" y="238.1" width="1.8" height="1.1"/>
<rect class="st1" x="300.7" y="239.1" width="1.8" height="1.1"/>
<rect class="st1" x="302.4" y="240.2" width="1.8" height="1.1"/>
<rect class="st1" x="300.7" y="241.3" width="1.8" height="1.1"/>
</g>
</g>
<path class="st1" d="M162.6,243.9c0-1.2-1-2.1-2.2-2.1-.9,0-.8.1-.8-.9,0-1.1,0-2.2,0-3.3,0-.4-.1-.7-.4-1-2.3-2.7-4.7-5.3-7-8-.1-.1-.3-.2-.4-.4h-18.6c-.8.3-1.1.9-1.1,1.7,0,3.7,0,7.5,0,11.2s0,.6-.6.6c-1.5-.1-2.5,1-2.5,2.5,0,3.5,0,7,0,10.4,0,1.6.9,2.4,2.4,2.4q.6,0,.6.6c0,2.2,0,4.4,0,6.7,0,1.2.6,1.8,1.8,1.8,8,0,16,0,24,0,1.2,0,1.8-.6,1.8-1.7,0-2.3,0-4.5,0-6.8,0-.5,0-.5.5-.5.3,0,.5,0,.8,0,1-.2,1.7-1,1.7-2.1,0-3.7,0-7.4,0-11.1ZM134.2,229.8h16c.5,0,.6,0,.6.6,0,2.2,0,4.5,0,6.7,0,.8.2,1.1,1.1,1.1,1.9,0,3.8,0,5.7,0,.5,0,.5,0,.5.5,0,.9,0,1.8,0,2.7,0,.5,0,.5-.5.5-3.9,0-7.8,0-11.7,0h-11.6c-.6,0-.6,0-.6-.6,0-3.6,0-7.2,0-10.9,0-.6,0-.6.6-.6ZM157.5,264.2c-3.9,0-7.8,0-11.6,0h-11.6c-.6,0-.6,0-.6-.6,0-2,0-4,0-6,0-.5,0-.5.5-.5,7.8,0,15.6,0,23.4,0,.5,0,.5,0,.5.5,0,2,0,4,0,6,0,.6,0,.6-.6.6Z"/>
<g>
<path class="st1" d="M134.8,253.3v-7.9h2.8c.4,0,.7,0,1.1.2s.6.3.9.5c.2.2.4.5.6.8.1.3.2.6.2,1s0,.7-.2,1-.3.6-.6.8c-.2.2-.5.4-.9.5s-.7.2-1.1.2h-1.3v2.9h-1.5ZM137.6,249c.4,0,.7-.1.9-.3.2-.2.3-.5.3-.8s0-.3,0-.4-.1-.2-.2-.3c0,0-.2-.2-.4-.2-.1,0-.3,0-.5,0h-1.3v2.2h1.3Z"/>
<path class="st1" d="M141.3,245.4h1.7l3.2,5.3h0v-1.5c0,0,0-3.8,0-3.8h1.5v7.9h-1.6l-3.4-5.6h0v1.5c0,0,0,4.1,0,4.1h-1.5v-7.9h0Z"/>
<path class="st1" d="M152.9,248.9h3.9c0,0,0,.2,0,.3,0,.1,0,.2,0,.4,0,.5,0,1-.2,1.4s-.4.8-.7,1.2c-.4.4-.8.7-1.3.9-.5.2-1.1.3-1.7.3s-1.1-.1-1.6-.3c-.5-.2-.9-.5-1.3-.9-.4-.4-.7-.8-.9-1.3s-.3-1.1-.3-1.6.1-1.1.3-1.6.5-.9.9-1.3c.4-.4.8-.7,1.3-.9s1-.3,1.6-.3,1.2.1,1.7.3.9.5,1.3.9l-1,1c-.3-.3-.5-.5-.9-.6-.3-.1-.7-.2-1.1-.2s-.7,0-1,.2c-.3.1-.6.3-.9.5-.2.2-.4.5-.6.9-.1.3-.2.7-.2,1.1s0,.8.2,1.1c.1.3.3.6.6.9.2.2.5.4.9.5.3.1.7.2,1,.2s.8,0,1.1-.2c.3-.1.5-.3.7-.5.1-.1.3-.3.4-.5.1-.2.2-.4.2-.7h-2.5v-1.3h0Z"/>
</g>
<path class="st1" d="M199,243.9c0-1.2-1-2.1-2.2-2.1-.9,0-.8.1-.8-.9,0-1.1,0-2.2,0-3.3,0-.4-.1-.7-.4-1-2.3-2.7-4.7-5.3-7-8-.1-.1-.3-.2-.4-.4h-18.6c-.8.3-1.1.9-1.1,1.7,0,3.7,0,7.5,0,11.2s0,.6-.6.6c-1.5-.1-2.5,1-2.5,2.5,0,3.5,0,7,0,10.4,0,1.6.9,2.4,2.4,2.4q.6,0,.6.6c0,2.2,0,4.4,0,6.7,0,1.2.6,1.8,1.8,1.8,8,0,16,0,24,0,1.2,0,1.8-.6,1.8-1.7,0-2.3,0-4.5,0-6.8,0-.5,0-.5.5-.5.3,0,.5,0,.8,0,1-.2,1.7-1,1.7-2.1,0-3.7,0-7.4,0-11.1ZM170.6,229.8h16c.5,0,.6,0,.6.6,0,2.2,0,4.5,0,6.7,0,.8.2,1.1,1.1,1.1,1.9,0,3.8,0,5.7,0,.5,0,.5,0,.5.5,0,.9,0,1.8,0,2.7,0,.5,0,.5-.5.5-3.9,0-7.8,0-11.7,0h-11.6c-.6,0-.6,0-.6-.6,0-3.6,0-7.2,0-10.9,0-.6,0-.6.6-.6ZM193.9,264.2c-3.9,0-7.8,0-11.6,0h-11.6c-.6,0-.6,0-.6-.6,0-2,0-4,0-6,0-.5,0-.5.5-.5,7.8,0,15.6,0,23.4,0,.5,0,.5,0,.5.5,0,2,0,4,0,6,0,.6,0,.6-.6.6Z"/>
<g>
<path class="st1" d="M180.1,245.4h1.7l3,7.9h-1.6l-.7-1.9h-3l-.7,1.9h-1.6l3-7.9ZM182,250l-.7-2-.3-1h0l-.3,1-.7,2h2.1Z"/>
<path class="st1" d="M186.3,247.1c-.1,0-.2,0-.4,0s-.2-.1-.3-.2c0,0-.2-.2-.2-.3,0-.1,0-.2,0-.4s0-.3,0-.4c0-.1.1-.2.2-.3,0,0,.2-.2.3-.2.1,0,.2,0,.4,0,.3,0,.5,0,.7.3.2.2.3.4.3.7s0,.5-.3.7c-.2.2-.4.3-.7.3ZM185.6,253.3v-5.4h1.4v5.4h-1.4Z"/>
</g>
<path class="st1" d="M236,243.9c0-1.2-1-2.1-2.2-2.1-.9,0-.8.1-.8-.9,0-1.1,0-2.2,0-3.3,0-.4-.1-.7-.4-1-2.3-2.7-4.7-5.3-7-8-.1-.1-.3-.2-.4-.4h-18.6c-.8.3-1.1.9-1.1,1.7,0,3.7,0,7.5,0,11.2s0,.6-.6.6c-1.5-.1-2.5,1-2.5,2.5,0,3.5,0,7,0,10.4,0,1.6.9,2.4,2.4,2.4q.6,0,.6.6c0,2.2,0,4.4,0,6.7,0,1.2.6,1.8,1.8,1.8,8,0,16,0,24,0,1.2,0,1.8-.6,1.8-1.7,0-2.3,0-4.5,0-6.8,0-.5,0-.5.5-.5.3,0,.5,0,.8,0,1-.2,1.7-1,1.7-2.1,0-3.7,0-7.4,0-11.1ZM207.6,229.8h16c.5,0,.6,0,.6.6,0,2.2,0,4.5,0,6.7,0,.8.2,1.1,1.1,1.1,1.9,0,3.8,0,5.7,0,.5,0,.5,0,.5.5,0,.9,0,1.8,0,2.7,0,.5,0,.5-.5.5-3.9,0-7.8,0-11.7,0h-11.6c-.6,0-.6,0-.6-.6,0-3.6,0-7.2,0-10.9,0-.6,0-.6.6-.6ZM230.9,264.2c-3.9,0-7.8,0-11.6,0h-11.6c-.6,0-.6,0-.6-.6,0-2,0-4,0-6,0-.5,0-.5.5-.5,7.8,0,15.6,0,23.4,0,.5,0,.5,0,.5.5,0,2,0,4,0,6,0,.6,0,.6-.6.6Z"/>
<g>
<path class="st1" d="M211.4,253.5c-.3,0-.7,0-1-.1-.3,0-.6-.2-.8-.4-.3-.2-.5-.4-.7-.7-.2-.3-.3-.6-.5-1l1.4-.6c.1.4.3.7.5,1,.3.3.6.4,1,.4s.3,0,.4,0c.1,0,.3,0,.4-.2s.2-.2.3-.3c0-.1,0-.3,0-.4s0-.3,0-.4c0-.1-.1-.2-.3-.3-.1-.1-.3-.2-.5-.3-.2,0-.4-.2-.7-.3l-.5-.2c-.2,0-.4-.2-.6-.3-.2-.1-.4-.3-.6-.5s-.3-.4-.4-.6c-.1-.2-.2-.5-.2-.8s0-.6.2-.9c.1-.3.3-.5.5-.7.2-.2.5-.4.8-.5s.7-.2,1-.2.7,0,1,.2.5.2.7.4c.2.2.4.3.5.5.1.2.2.4.3.6l-1.3.6c0-.2-.2-.4-.4-.6-.2-.2-.5-.3-.8-.3s-.6,0-.8.2-.3.3-.3.6.1.4.3.6c.2.2.5.3,1,.5l.5.2c.3.1.6.2.9.4.3.1.5.3.7.5.2.2.3.4.4.7.1.3.1.5.1.9s0,.8-.2,1.1c-.2.3-.4.5-.6.7-.3.2-.5.3-.9.4-.3,0-.6.1-1,.1Z"/>
<path class="st1" d="M214.5,245.4h1.6l1.6,4.8.3,1h0l.3-1,1.7-4.8h1.6l-2.9,7.9h-1.6l-2.8-7.9Z"/>
<path class="st1" d="M226.1,248.9h3.9c0,0,0,.2,0,.3s0,.2,0,.4c0,.5,0,1-.2,1.4-.2.4-.4.8-.7,1.2-.4.4-.8.7-1.3.9-.5.2-1.1.3-1.7.3s-1.1-.1-1.6-.3-.9-.5-1.3-.9c-.4-.4-.7-.8-.9-1.3-.2-.5-.3-1.1-.3-1.6s.1-1.1.3-1.6.5-.9.9-1.3c.4-.4.8-.7,1.3-.9.5-.2,1-.3,1.6-.3s1.2.1,1.7.3.9.5,1.3.9l-1,1c-.3-.3-.5-.5-.9-.6-.3-.1-.7-.2-1.1-.2s-.7,0-1,.2c-.3.1-.6.3-.8.5-.2.2-.4.5-.6.9-.1.3-.2.7-.2,1.1s0,.8.2,1.1c.1.3.3.6.6.9.2.2.5.4.9.5.3.1.7.2,1,.2s.8,0,1.1-.2.5-.3.7-.5c.1-.1.3-.3.4-.5.1-.2.2-.4.2-.7h-2.5v-1.3h0Z"/>
</g>
<path class="st1" d="M272.3,243.9c0-1.2-1-2.1-2.2-2.1-.9,0-.8.1-.8-.9,0-1.1,0-2.2,0-3.3,0-.4-.1-.7-.4-1-2.3-2.7-4.7-5.3-7-8-.1-.1-.3-.2-.4-.4h-18.6c-.8.3-1.1.9-1.1,1.7,0,3.7,0,7.5,0,11.2s0,.6-.5.6c-1.5-.1-2.5,1-2.5,2.5,0,3.5,0,7,0,10.4,0,1.6.9,2.4,2.5,2.4q.6,0,.6.6c0,2.2,0,4.4,0,6.7,0,1.2.6,1.8,1.8,1.8,8,0,16,0,24,0,1.2,0,1.8-.6,1.8-1.7,0-2.3,0-4.5,0-6.8,0-.5,0-.5.5-.5.3,0,.5,0,.8,0,1-.2,1.7-1,1.7-2.1,0-3.7,0-7.4,0-11.1ZM243.8,229.8h16c.5,0,.6,0,.6.6,0,2.2,0,4.5,0,6.7,0,.8.2,1.1,1.1,1.1,1.9,0,3.8,0,5.7,0,.5,0,.5,0,.5.5,0,.9,0,1.8,0,2.7,0,.5,0,.5-.5.5-3.9,0-7.8,0-11.7,0h-11.6c-.5,0-.6,0-.6-.6,0-3.6,0-7.2,0-10.9,0-.6,0-.6.6-.6ZM267.1,264.2c-3.9,0-7.8,0-11.6,0h-11.6c-.6,0-.6,0-.6-.6,0-2,0-4,0-6,0-.5,0-.5.5-.5,7.8,0,15.6,0,23.4,0,.5,0,.5,0,.5.5,0,2,0,4,0,6,0,.6,0,.6-.6.6Z"/>
<g>
<path class="st1" d="M248,246.8v1.8h3.2v1.4h-3.2v1.8h3.5v1.4h-5v-7.9h5v1.4h-3.5Z"/>
<path class="st1" d="M252.9,253.3v-7.9h2.8c.4,0,.7,0,1.1.2s.6.3.9.5.4.5.6.8c.1.3.2.6.2,1s0,.7-.2,1c-.1.3-.3.6-.6.8s-.5.4-.9.5-.7.2-1.1.2h-1.3v2.9h-1.5ZM255.7,249c.4,0,.7-.1.9-.3.2-.2.3-.5.3-.8s0-.3,0-.4-.1-.2-.2-.3c0,0-.2-.2-.4-.2-.1,0-.3,0-.5,0h-1.3v2.2h1.3Z"/>
<path class="st1" d="M261.8,253.5c-.3,0-.7,0-1-.1-.3,0-.6-.2-.8-.4-.3-.2-.5-.4-.7-.7-.2-.3-.3-.6-.5-1l1.4-.6c.1.4.3.7.5,1,.3.3.6.4,1,.4s.3,0,.4,0c.1,0,.3,0,.4-.2.1,0,.2-.2.3-.3,0-.1.1-.3.1-.4s0-.3,0-.4c0-.1-.1-.2-.3-.3-.1-.1-.3-.2-.5-.3-.2,0-.4-.2-.7-.3l-.5-.2c-.2,0-.4-.2-.6-.3-.2-.1-.4-.3-.6-.5s-.3-.4-.4-.6c-.1-.2-.2-.5-.2-.8s0-.6.2-.9c.1-.3.3-.5.5-.7.2-.2.5-.4.8-.5.3-.1.7-.2,1-.2s.7,0,1,.2.5.2.7.4c.2.2.4.3.5.5.1.2.2.4.3.6l-1.3.6c0-.2-.2-.4-.4-.6-.2-.2-.5-.3-.8-.3s-.6,0-.8.2c-.2.2-.3.3-.3.6s.1.4.3.6.5.3,1,.5l.5.2c.3.1.6.2.9.4.3.1.5.3.7.5.2.2.3.4.4.7,0,.3.1.5.1.9s0,.8-.2,1.1c-.2.3-.4.5-.6.7-.3.2-.5.3-.9.4-.3,0-.6.1-1,.1Z"/>
</g>
<g>
<path class="st1" d="M296.4,251.8l4-5h-3.8v-1.4h5.6v1.5l-4,5h4v1.4h-5.8v-1.5h0Z"/>
<path class="st1" d="M303.4,245.4h1.5v7.9h-1.5v-7.9Z"/>
<path class="st1" d="M306.5,253.3v-7.9h2.8c.4,0,.7,0,1.1.2s.6.3.9.5.4.5.6.8c.1.3.2.6.2,1s0,.7-.2,1c-.1.3-.3.6-.6.8s-.5.4-.9.5-.7.2-1.1.2h-1.3v2.9h-1.5ZM309.3,249c.4,0,.7-.1.9-.3.2-.2.3-.5.3-.8s0-.3,0-.4-.1-.2-.2-.3c0,0-.2-.2-.4-.2-.1,0-.3,0-.5,0h-1.3v2.2h1.3Z"/>
</g>
</g>
<g class="st2">
<path class="st1" d="M275.1,245.9h9.1v2.2h-9.1v-2.2ZM275.1,249.7h9.1v2.2h-9.1v-2.2Z"/>
</g>
</g>
<g class="st2">
<path class="st1" d="M131.8,291.5h1.6c1.3,0,2.1.8,2.1,2s-.9,2-2.1,2h-1.6v-3.9ZM133.3,295.1c1,0,1.7-.7,1.7-1.6s-.7-1.6-1.7-1.6h-1.2v3.2h1.2Z"/>
<path class="st1" d="M140.1,293.5c0-1.1.9-2,2.1-2s2.1.9,2.1,2-.9,2-2.1,2-2.1-.9-2.1-2ZM143.9,293.5c0-.9-.7-1.6-1.7-1.6s-1.7.7-1.7,1.6.7,1.6,1.7,1.6c.9,0,1.7-.7,1.7-1.6Z"/>
<path class="st1" d="M154.6,291.5l-1.3,3.9h-.4l-1.2-3.4-1.2,3.4h-.4l-1.3-3.9h.4l1.1,3.4,1.2-3.4h.4l1.2,3.4,1.2-3.4h.4Z"/>
<path class="st1" d="M162.7,291.5v3.9h-.3l-2.5-3.2v3.2h-.4v-3.9h.3l2.5,3.2v-3.2h.4Z"/>
<path class="st1" d="M168.1,291.5h.4v3.6h2.2v.4h-2.6v-3.9h0Z"/>
<path class="st1" d="M175.1,293.5c0-1.1.9-2,2.1-2s2.1.9,2.1,2-.9,2-2.1,2-2.1-.9-2.1-2ZM178.8,293.5c0-.9-.7-1.6-1.7-1.6s-1.7.7-1.7,1.6.7,1.6,1.7,1.6c.9,0,1.7-.7,1.7-1.6Z"/>
<path class="st1" d="M186.6,294.4h-2.2l-.5,1.1h-.4l1.8-3.9h.4l1.8,3.9h-.4l-.5-1.1ZM186.5,294l-.9-2.1-.9,2.1h1.9Z"/>
<path class="st1" d="M192.3,291.5h1.6c1.3,0,2.1.8,2.1,2s-.9,2-2.1,2h-1.6v-3.9ZM193.9,295.1c1,0,1.7-.7,1.7-1.6s-.7-1.6-1.7-1.6h-1.2v3.2h1.2Z"/>
<path class="st1" d="M209,294.4h-2.2l-.5,1.1h-.4l1.8-3.9h.4l1.8,3.9h-.4l-.5-1.1ZM208.9,294l-.9-2.1-.9,2.1h1.9Z"/>
<path class="st1" d="M214.7,291.5h.4v3.6h2.2v.4h-2.6v-3.9h0Z"/>
<path class="st1" d="M222.1,291.5h.4v3.6h2.2v.4h-2.6v-3.9h0Z"/>
<path class="st1" d="M235.4,291.8v1.6h2v.4h-2v1.7h-.4v-3.9h2.7v.4h-2.3Z"/>
<path class="st1" d="M242.7,291.5h.4v3.9h-.4v-3.9Z"/>
<path class="st1" d="M248.5,291.5h.4v3.6h2.2v.4h-2.6v-3.9h0Z"/>
<path class="st1" d="M258.6,295.1v.4h-2.8v-3.9h2.7v.4h-2.3v1.4h2v.4h-2v1.5h2.4Z"/>
<path class="st1" d="M269.7,291.8v1.6h2v.4h-2v1.7h-.4v-3.9h2.7v.4h-2.3Z"/>
<path class="st1" d="M276.5,293.5c0-1.1.9-2,2.1-2s2.1.9,2.1,2-.9,2-2.1,2-2.1-.9-2.1-2ZM280.2,293.5c0-.9-.7-1.6-1.7-1.6s-1.7.7-1.7,1.6.7,1.6,1.7,1.6c.9,0,1.7-.7,1.7-1.6Z"/>
<path class="st1" d="M288.3,295.4l-.9-1.3c-.1,0-.2,0-.3,0h-1.1v1.3h-.4v-3.9h1.5c1,0,1.6.5,1.6,1.4s-.3,1.1-.9,1.2l1,1.4h-.5ZM288.3,292.8c0-.6-.4-1-1.2-1h-1v2h1c.8,0,1.2-.4,1.2-1Z"/>
<path class="st1" d="M297.9,291.5v3.9h-.4v-3.2l-1.6,2.7h-.2l-1.6-2.6v3.1h-.4v-3.9h.3l1.7,2.9,1.7-2.9h.3Z"/>
<path class="st1" d="M305.7,294.4h-2.2l-.5,1.1h-.4l1.8-3.9h.4l1.8,3.9h-.4l-.5-1.1ZM305.6,294l-.9-2.1-.9,2.1h1.9Z"/>
<path class="st1" d="M312,291.8h-1.4v-.4h3.2v.4h-1.4v3.6h-.4v-3.6h0Z"/>
<path class="st1" d="M318.1,295l.2-.3c.3.3.8.5,1.3.5.7,0,1.1-.3,1.1-.7,0-1.1-2.4-.4-2.4-1.9s.5-1.1,1.5-1.1.9.1,1.2.4v.3c-.5-.2-.9-.3-1.2-.3-.7,0-1,.3-1,.7,0,1.1,2.4.4,2.4,1.9s-.5,1.1-1.5,1.1c-.6,0-1.2-.2-1.5-.5Z"/>
</g>
</g>
<path class="st0" d="M376.7,125.2c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l4.5-15.5c1.5-5.1,6.9-9.3,12-9.3h10c1.2,0,2.2-.8,2.6-1.9l1.7-6c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1h-21.9c-.1,0-.2,0-.4,0h-7.4c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l.9-3c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1h-2.8c-1.2,0-2.2.8-2.6,1.9l-1.3,4.5c-1.5,5.1-6.9,9.3-12,9.3h-10c-1.2,0-2.2.8-2.6,1.9l-2.8,9.8s0,.1,0,.2l-4,14s0,.1,0,.2l-2.9,10.1c-1.5,5.1-6.9,9.3-12,9.3h-15.5c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l13-45.2s0-.1,0-.2l.9-3.2c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1h-2.8c-1.2,0-2.2.8-2.6,1.9l-5.7,19.6s0,.1,0,.2l-2.1,7.4c-1.5,5.1-6.9,9.3-12,9.3h-15.5c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l9.4-32.8c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1h-2.8c-1.2,0-2.2.8-2.6,1.9l-4,14s0,.1,0,.2l-5.8,20.1c-1.5,5.1-6.9,9.3-12,9.3h-15.5c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l4.5-15.5s0-.1,0-.2l4.9-17.1c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1h-2.8c-1.2,0-2.2.8-2.6,1.9l-.4,1.4s0,.1,0,.2l-9.1,31.4s0,.1,0,.2l-.3,1.1c-1.5,5.1-6.9,9.3-12,9.3h-15.5c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l.9-3c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1h-2.8c-1.2,0-2.2.8-2.6,1.9l-1.3,4.5c-1.5,5.1-6.9,9.3-12,9.3h-10.1c-1.2,0-2.2.8-2.6,1.9l-3.3,11.5c-1.5,5.1-6.9,9.3-12,9.3h-15.5c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l2.9-10c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1h-8.3c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l.9-3c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1h-2.8c-1.2,0-2.2.8-2.6,1.9l-1.3,4.5c-1.5,5.1-6.9,9.3-12,9.3h-21.3s-15.6,0-15.6,0c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l2.9-10.1c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1H7c-2.4,0-4.4-.9-5.7-2.6-1.3-1.8-1.7-4.2-.9-6.7l7.7-26.9s0-.1,0-.2l2.5-8.7c0,0,0-.1,0-.2l.7-2.3c1.5-5.1,6.9-9.3,12-9.3h10c1.2,0,2.2-.8,2.6-1.9l3.3-11.5C40.7,4.2,46,0,51.2,0h21.3S88,0,88,0c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-4.5,15.5c-1.5,5.1-6.9,9.3-12,9.3h-21.3s-9.9,0-9.9,0c-1.2,0-2.2.8-2.6,1.9l-4.4,15.3s0,.1,0,.2l-3.8,13.3c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1s14.1,0,14.1,0h15.5c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-.9,3.1c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1h2.8c1.2,0,2.2-.8,2.6-1.9l1.7-5.8s6.2-21.5,6.2-21.5c1.5-5.1,6.9-9.3,12-9.3h9.9c1.2,0,2.2-.8,2.6-1.9l3.3-11.5c1.5-5.1,6.9-9.3,12-9.3h15.5c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-2.9,10c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1h8.4c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-7.4,25.9c-.2.8,0,1.7.4,2.3s1.3,1.1,2.1,1.1h2.8c1.2,0,2.2-.8,2.6-1.9l3.5-12.2s0-.1,0-.2l9.1-31.4s0-.1,0-.2l1.8-6.1c1.5-5.1,6.9-9.3,12-9.3h36.8c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-2.9,10.1c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1h2.9c1.2,0,2.2-.8,2.6-1.9l3.3-11.5c1.5-5.1,6.9-9.3,12-9.3h15.5c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-2.9,10c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1h8.3c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-.9,3.1c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1h2.8c1.2,0,2.2-.8,2.6-1.9l7.9-27.3c1.5-5.1,6.9-9.3,12-9.3h10c1.2,0,2.2-.8,2.6-1.9l3.3-11.5c1.5-5.1,6.9-9.3,12-9.3h15.5c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-4.5,15.5c-1.5,5.1-6.9,9.3-12,9.3h-10c-1.2,0-2.2.8-2.6,1.9l-1.7,6c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1h8.4c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-.9,3.1c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1h2.8c1.2,0,2.2-.8,2.6-1.9l7.9-27.3c1.5-5.1,6.9-9.3,12-9.3h15.5c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-7,24.4s0,.1,0,.2l-.8,2.6c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1h2.8c1.2,0,2.2-.8,2.6-1.9l8.3-28.6c1.5-5.1,6.9-9.3,12-9.3h15.5c2.4,0,4.4.9,5.7,2.6,1.3,1.8,1.7,4.2.9,6.7l-17.6,61.1c-1.5,5.1-6.9,9.3-12,9.3h-10c-1.2,0-2.2.8-2.6,1.9l-3.3,11.5c-1.5,5.1-6.9,9.3-12,9.3h-15.5ZM125.5,57c-1.2,0-2.2.8-2.6,1.9l-8.3,28.8c-.2.8,0,1.7.4,2.3.5.7,1.3,1.1,2.1,1.1h2.8c1.2,0,2.2-.8,2.6-1.9l8.3-28.8c.2-.8,0-1.7-.4-2.3-.5-.7-1.3-1.1-2.1-1.1h-2.8Z"/>
</svg>

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -313,6 +313,10 @@ body {
.p-tooltip {
--p-tooltip-padding: 0.25rem 0.5rem;
--p-tooltip-border-radius: 4px;
--p-tooltip-background: #000000;
--p-tooltip-color: #fafafa;
--p-tooltip-show-delay: 100ms;
--p-tooltip-hide-delay: 0ms;
}
.p-tooltip .p-tooltip-text {
@@ -323,17 +327,25 @@ body {
letter-spacing: 0.05em;
padding: 0.25rem 0.5rem;
white-space: nowrap;
border: 1px solid #52525b;
border-radius: 4px;
}
/* Dark mode tooltip - for canvas/editor */
.dark .p-tooltip,
.dark-theme .p-tooltip {
--p-tooltip-background: #27272a;
--p-tooltip-color: #e4e4e7;
}
/* Light mode tooltip - for workspace */
.p-tooltip {
--p-tooltip-background: #18181b;
--p-tooltip-background: #000000;
--p-tooltip-color: #fafafa;
}
/* ===================== PrimeVue Popover Overrides ===================== */
@layer primevue {
.p-popover {
--p-popover-background: #18181b;
--p-popover-border-color: #3f3f46;
--p-popover-border-radius: 0.5rem;
--p-popover-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
--p-popover-padding: 0;
--p-popover-content-padding: 0;
}
}

View File

@@ -7,16 +7,23 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AssetsSidebar: typeof import('./components/v2/canvas/AssetsSidebar.vue')['default']
AssetsTab: typeof import('./components/v2/workspace/AssetsTab.vue')['default']
CanvasBottomBar: typeof import('./components/v2/canvas/CanvasBottomBar.vue')['default']
CanvasLeftSidebar: typeof import('./components/v2/canvas/CanvasLeftSidebar.vue')['default']
CanvasLogoMenu: typeof import('./components/v2/canvas/CanvasLogoMenu.vue')['default']
CanvasRightToolbar: typeof import('./components/v2/canvas/CanvasRightToolbar.vue')['default']
CanvasRunControls: typeof import('./components/v2/canvas/CanvasRunControls.vue')['default']
CanvasShareDialog: typeof import('./components/v2/canvas/CanvasShareDialog.vue')['default']
CanvasTabBar: typeof import('./components/v2/canvas/CanvasTabBar.vue')['default']
CanvasTabs: typeof import('./components/v2/canvas/CanvasTabs.vue')['default']
CreateProjectDialog: typeof import('./components/v2/workspace/CreateProjectDialog.vue')['default']
FlowNode: typeof import('./components/v2/nodes/FlowNode.vue')['default']
FlowNodeMinimized: typeof import('./components/v2/nodes/FlowNodeMinimized.vue')['default']
FlowNodeTerminal: typeof import('./components/v2/nodes/terminal/FlowNodeTerminal.vue')['default']
GlassNode: typeof import('./components/experimental/nodes/GlassNode.vue')['default']
LibraryBrandKitSection: typeof import('./components/v1/sidebar/LibraryBrandKitSection.vue')['default']
LibraryGridCard: typeof import('./components/common/sidebar/LibraryGridCard.vue')['default']
LibraryModelsSection: typeof import('./components/v1/sidebar/LibraryModelsSection.vue')['default']
LibraryNodesSection: typeof import('./components/v1/sidebar/LibraryNodesSection.vue')['default']
LibrarySidebar: typeof import('./components/v2/canvas/LibrarySidebar.vue')['default']
@@ -32,12 +39,17 @@ declare module 'vue' {
LinearTemplateSelector: typeof import('./components/linear/LinearTemplateSelector.vue')['default']
LinearWorkflowSidebar: typeof import('./components/linear/LinearWorkflowSidebar.vue')['default']
LinearWorkspace: typeof import('./components/linear/LinearWorkspace.vue')['default']
MinimalNode: typeof import('./components/experimental/nodes/MinimalNode.vue')['default']
ModelsTab: typeof import('./components/v2/workspace/ModelsTab.vue')['default']
NodeHeader: typeof import('./components/v2/nodes/NodeHeader.vue')['default']
NodeHeaderTerminal: typeof import('./components/v2/nodes/terminal/NodeHeaderTerminal.vue')['default']
NodePropertiesPanel: typeof import('./components/v2/canvas/NodePropertiesPanel.vue')['default']
NodeSlots: typeof import('./components/v2/nodes/NodeSlots.vue')['default']
NodeSlotsTerminal: typeof import('./components/v2/nodes/terminal/NodeSlotsTerminal.vue')['default']
NodeWidgets: typeof import('./components/v2/nodes/NodeWidgets.vue')['default']
NodeWidgetsTerminal: typeof import('./components/v2/nodes/terminal/NodeWidgetsTerminal.vue')['default']
PackagesTab: typeof import('./components/v2/workspace/PackagesTab.vue')['default']
PillNode: typeof import('./components/experimental/nodes/PillNode.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SidebarGridCard: typeof import('./components/common/sidebar/SidebarGridCard.vue')['default']
@@ -46,6 +58,8 @@ declare module 'vue' {
SidebarTreeItem: typeof import('./components/common/sidebar/SidebarTreeItem.vue')['default']
SidebarViewToggle: typeof import('./components/common/sidebar/SidebarViewToggle.vue')['default']
SlotDot: typeof import('./components/v2/nodes/SlotDot.vue')['default']
TemplatesSidebar: typeof import('./components/v2/canvas/TemplatesSidebar.vue')['default']
TerminalNode: typeof import('./components/experimental/nodes/TerminalNode.vue')['default']
V1SidebarAssetsTab: typeof import('./components/v1/sidebar/V1SidebarAssetsTab.vue')['default']
V1SidebarIconBar: typeof import('./components/v1/sidebar/V1SidebarIconBar.vue')['default']
V1SidebarModelsTab: typeof import('./components/v1/sidebar/V1SidebarModelsTab.vue')['default']
@@ -61,7 +75,9 @@ declare module 'vue' {
WidgetText: typeof import('./components/v2/nodes/widgets/WidgetText.vue')['default']
WidgetToggle: typeof import('./components/v2/nodes/widgets/WidgetToggle.vue')['default']
WorkflowsTab: typeof import('./components/v2/workspace/WorkflowsTab.vue')['default']
WorkspaceCard: typeof import('./components/v2/workspace/WorkspaceCard.vue')['default']
WorkspaceEmptyState: typeof import('./components/v2/workspace/WorkspaceEmptyState.vue')['default']
WorkspaceFilterSelect: typeof import('./components/v2/workspace/WorkspaceFilterSelect.vue')['default']
WorkspaceLayout: typeof import('./components/v2/layout/WorkspaceLayout.vue')['default']
WorkspaceSearchInput: typeof import('./components/v2/workspace/WorkspaceSearchInput.vue')['default']
WorkspaceSidebar: typeof import('./components/v2/layout/WorkspaceSidebar.vue')['default']

View File

@@ -0,0 +1,99 @@
<script setup lang="ts">
interface Props {
title: string
subtitle?: string
thumbnail?: string
icon?: string
iconClass?: string
badge?: string
badgeClass?: string
starred?: boolean
draggable?: boolean
}
const props = withDefaults(defineProps<Props>(), {
subtitle: undefined,
thumbnail: undefined,
icon: undefined,
iconClass: 'text-zinc-400',
badge: undefined,
badgeClass: 'bg-zinc-700 text-zinc-400',
starred: false,
draggable: true,
})
const emit = defineEmits<{
click: []
}>()
</script>
<template>
<div
class="group cursor-pointer overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900/80 transition-all hover:border-zinc-600 hover:bg-zinc-800/80"
:draggable="props.draggable"
@click="emit('click')"
>
<!-- Thumbnail -->
<div class="relative aspect-[4/3] overflow-hidden bg-zinc-800">
<img
v-if="props.thumbnail"
:src="props.thumbnail"
:alt="props.title"
class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
/>
<div
v-else
class="flex h-full w-full items-center justify-center"
>
<i :class="[props.icon || 'pi pi-file', 'text-2xl text-zinc-600']" />
</div>
<!-- Gradient overlay -->
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
<!-- Starred indicator -->
<div
v-if="props.starred"
class="absolute left-1.5 top-1.5 flex h-5 w-5 items-center justify-center rounded bg-amber-500/20 backdrop-blur-sm"
>
<i class="pi pi-star-fill text-[10px] text-amber-400" />
</div>
<!-- Badge (top-right) -->
<div
v-if="props.badge"
class="absolute right-1.5 top-1.5"
>
<span :class="['rounded px-1.5 py-0.5 text-[9px] font-medium backdrop-blur-sm', props.badgeClass]">
{{ props.badge }}
</span>
</div>
<!-- Icon badge (bottom-left) -->
<div
v-if="props.icon"
class="absolute bottom-1.5 left-1.5 flex h-6 w-6 items-center justify-center rounded bg-black/40 backdrop-blur-sm"
>
<i :class="[props.icon, 'text-xs', props.iconClass]" />
</div>
<!-- Add button (bottom-right, on hover) -->
<button
class="absolute bottom-1.5 right-1.5 flex h-6 w-6 items-center justify-center rounded bg-white/90 text-zinc-800 opacity-0 transition-all hover:bg-white group-hover:opacity-100"
@click.stop
>
<i class="pi pi-plus text-xs" />
</button>
</div>
<!-- Content -->
<div class="p-2">
<div class="truncate text-xs font-medium text-zinc-200 group-hover:text-white">
{{ props.title }}
</div>
<div v-if="props.subtitle" class="mt-0.5 truncate text-[10px] text-zinc-500">
{{ props.subtitle }}
</div>
</div>
</div>
</template>

View File

@@ -1,5 +1,6 @@
export { default as SidebarTreeCategory } from './SidebarTreeCategory.vue'
export { default as SidebarTreeItem } from './SidebarTreeItem.vue'
export { default as SidebarGridCard } from './SidebarGridCard.vue'
export { default as LibraryGridCard } from './LibraryGridCard.vue'
export { default as SidebarViewToggle } from './SidebarViewToggle.vue'
export { default as SidebarSearchBox } from './SidebarSearchBox.vue'

View File

@@ -21,6 +21,16 @@ function getAssetIcon(type: BrandAsset['type']): string {
default: return 'pi pi-file'
}
}
function getAssetTypeLabel(type: BrandAsset['type']): string {
switch (type) {
case 'logo': return 'Logo'
case 'font': return 'Font'
case 'template': return 'Template'
case 'guideline': return 'Guide'
default: return type
}
}
</script>
<template>
@@ -38,30 +48,12 @@ function getAssetIcon(type: BrandAsset['type']): string {
<i class="pi pi-palette text-xs text-amber-400" />
<span class="flex-1 text-xs font-medium text-zinc-300">Brand Kit</span>
<span class="rounded bg-zinc-800 px-1.5 py-0.5 text-[10px] text-zinc-500">
{{ assets.length }}
{{ assets.filter(a => a.type !== 'color').length }}
</span>
</button>
<!-- Items -->
<div v-if="expanded" class="ml-4 space-y-0.5 border-l border-zinc-800 pl-2">
<!-- Colors Row -->
<div class="px-2 py-1.5">
<div class="mb-1 text-[10px] font-medium uppercase tracking-wider text-zinc-500">Colors</div>
<div class="flex gap-2">
<div
v-for="asset in assets.filter(a => a.type === 'color')"
:key="asset.id"
v-tooltip.top="{ value: asset.name, showDelay: 50 }"
class="group relative cursor-pointer"
>
<div
class="h-6 w-6 rounded border border-zinc-700 transition-transform group-hover:scale-110"
:style="{ backgroundColor: asset.value }"
/>
</div>
</div>
</div>
<!-- Other Assets -->
<div
v-for="asset in assets.filter(a => a.type !== 'color')"
:key="asset.id"
@@ -77,29 +69,49 @@ function getAssetIcon(type: BrandAsset['type']): string {
<!-- Grid View -->
<template v-else>
<div class="mb-1.5 flex items-center gap-2 px-1">
<i class="pi pi-palette text-xs text-amber-400" />
<span class="text-[10px] font-medium uppercase tracking-wider text-zinc-500">Brand Kit</span>
</div>
<div class="grid grid-cols-3 gap-1.5">
<div
v-for="asset in assets.filter(a => a.type === 'color')"
:key="asset.id"
v-tooltip.top="{ value: asset.name, showDelay: 50 }"
class="group cursor-pointer rounded-lg border border-zinc-800 bg-zinc-900 p-2 transition-all hover:border-zinc-700"
>
<div class="mb-1.5 h-8 w-full rounded" :style="{ backgroundColor: asset.value }" />
<div class="truncate text-[10px] text-zinc-500">{{ asset.name }}</div>
<div class="mb-2 flex items-center justify-between px-1">
<div class="flex items-center gap-2">
<i class="pi pi-palette text-xs text-amber-400" />
<span class="text-[10px] font-medium uppercase tracking-wider text-zinc-500">Brand Kit</span>
</div>
<span class="rounded bg-zinc-800 px-1.5 py-0.5 text-[9px] text-zinc-500">
{{ assets.filter(a => a.type !== 'color').length }}
</span>
</div>
<!-- Assets Grid -->
<div class="grid grid-cols-2 gap-2">
<div
v-for="asset in assets.filter(a => a.type !== 'color')"
:key="asset.id"
class="group cursor-pointer rounded-lg border border-zinc-800 bg-zinc-900 p-2 transition-all hover:border-zinc-700"
class="group cursor-pointer overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900/80 transition-all hover:border-zinc-600 hover:bg-zinc-800/80"
>
<div class="mb-1.5 flex h-8 items-center justify-center rounded bg-zinc-800">
<i :class="[getAssetIcon(asset.type), 'text-base text-zinc-600']" />
<!-- Icon Thumbnail -->
<div class="relative flex aspect-[4/3] items-center justify-center bg-zinc-800">
<i :class="[getAssetIcon(asset.type), 'text-3xl text-zinc-600 transition-colors group-hover:text-amber-400']" />
<!-- Type Badge -->
<div class="absolute right-1.5 top-1.5">
<span class="rounded bg-amber-500/30 px-1.5 py-0.5 text-[9px] font-medium text-amber-300 backdrop-blur-sm">
{{ getAssetTypeLabel(asset.type) }}
</span>
</div>
<!-- Add button -->
<button
class="absolute bottom-1.5 right-1.5 flex h-6 w-6 items-center justify-center rounded bg-white/90 text-zinc-800 opacity-0 transition-all hover:bg-white group-hover:opacity-100"
@click.stop
>
<i class="pi pi-plus text-xs" />
</button>
</div>
<!-- Content -->
<div class="p-2">
<div class="truncate text-xs font-medium text-zinc-200 group-hover:text-white">
{{ asset.name }}
</div>
<div v-if="asset.description" class="mt-0.5 truncate text-[10px] text-zinc-500">
{{ asset.description }}
</div>
</div>
<div class="truncate text-[10px] text-zinc-400">{{ asset.name }}</div>
</div>
</div>
</template>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { TeamModel } from '@/data/sidebarMockData'
import { LibraryGridCard } from '@/components/common/sidebar'
defineProps<{
models: TeamModel[]
@@ -20,6 +21,16 @@ function getModelTypeLabel(type: TeamModel['type']): string {
default: return type
}
}
function getModelBadgeClass(type: TeamModel['type']): string {
switch (type) {
case 'checkpoint': return 'bg-purple-500/30 text-purple-300'
case 'lora': return 'bg-green-500/30 text-green-300'
case 'embedding': return 'bg-amber-500/30 text-amber-300'
case 'controlnet': return 'bg-cyan-500/30 text-cyan-300'
default: return 'bg-zinc-700 text-zinc-400'
}
}
</script>
<template>
@@ -69,30 +80,27 @@ function getModelTypeLabel(type: TeamModel['type']): string {
<!-- Grid View -->
<template v-else>
<div class="mb-1.5 flex items-center gap-2 px-1">
<i class="pi pi-box text-xs text-green-400" />
<span class="text-[10px] font-medium uppercase tracking-wider text-zinc-500">Team Models</span>
<div class="mb-2 flex items-center justify-between px-1">
<div class="flex items-center gap-2">
<i class="pi pi-box text-xs text-green-400" />
<span class="text-[10px] font-medium uppercase tracking-wider text-zinc-500">Models</span>
</div>
<span class="rounded bg-zinc-800 px-1.5 py-0.5 text-[9px] text-zinc-500">
{{ models.length }}
</span>
</div>
<div class="grid grid-cols-2 gap-1.5">
<div
<div class="grid grid-cols-2 gap-2">
<LibraryGridCard
v-for="model in models"
:key="model.id"
class="group cursor-pointer rounded-lg border border-zinc-800 bg-zinc-900 p-2 transition-all hover:border-zinc-700 hover:bg-zinc-800/50"
draggable="true"
>
<div class="mb-1 flex items-center justify-between">
<span class="rounded bg-zinc-800 px-1 py-0.5 text-[9px] text-zinc-500">
{{ getModelTypeLabel(model.type) }}
</span>
<span class="text-[9px] text-zinc-600">{{ model.size }}</span>
</div>
<div class="truncate text-xs text-zinc-400 group-hover:text-zinc-200">
{{ model.name }}
</div>
<div class="mt-0.5 truncate text-[10px] text-zinc-600">
{{ model.downloads }} downloads
</div>
</div>
:title="model.name"
:subtitle="`${model.size} · ${model.downloads} downloads`"
:thumbnail="model.thumbnail"
icon="pi pi-box"
icon-class="text-green-400"
:badge="getModelTypeLabel(model.type)"
:badge-class="getModelBadgeClass(model.type)"
/>
</div>
</template>
</template>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { NodePack } from '@/data/sidebarMockData'
import { LibraryGridCard } from '@/components/common/sidebar'
defineProps<{
packs: NodePack[]
@@ -68,36 +69,27 @@ const emit = defineEmits<{
<!-- Grid View -->
<template v-else>
<div class="mb-1.5 flex items-center gap-2 px-1">
<i class="pi pi-code text-xs text-purple-400" />
<span class="text-[10px] font-medium uppercase tracking-wider text-zinc-500">Custom Nodes</span>
<div class="mb-2 flex items-center justify-between px-1">
<div class="flex items-center gap-2">
<i class="pi pi-code text-xs text-purple-400" />
<span class="text-[10px] font-medium uppercase tracking-wider text-zinc-500">Nodepacks</span>
</div>
<span class="rounded bg-zinc-800 px-1.5 py-0.5 text-[9px] text-zinc-500">
{{ packs.length }}
</span>
</div>
<div class="grid grid-cols-2 gap-1.5">
<div
<div class="grid grid-cols-2 gap-2">
<LibraryGridCard
v-for="pack in packs"
:key="pack.id"
class="group cursor-pointer rounded-lg border border-zinc-800 bg-zinc-900 p-2 transition-all hover:border-zinc-700 hover:bg-zinc-800/50"
>
<div class="mb-1 flex items-center justify-between">
<span class="rounded bg-zinc-800 px-1 py-0.5 text-[9px] text-zinc-500">
v{{ pack.version }}
</span>
<span
:class="[
'rounded px-1 py-0.5 text-[9px]',
pack.installed ? 'bg-green-500/20 text-green-400' : 'bg-zinc-800 text-zinc-500'
]"
>
{{ pack.installed ? 'Installed' : pack.nodes + ' nodes' }}
</span>
</div>
<div class="truncate text-xs text-zinc-400 group-hover:text-zinc-200">
{{ pack.name }}
</div>
<div class="mt-0.5 truncate text-[10px] text-zinc-600">
{{ pack.author }}
</div>
</div>
:title="pack.name"
:subtitle="`${pack.nodes} nodes · v${pack.version}`"
:thumbnail="pack.thumbnail"
icon="pi pi-code"
icon-class="text-purple-400"
:badge="pack.installed ? 'Installed' : 'Available'"
:badge-class="pack.installed ? 'bg-green-500/30 text-green-300' : 'bg-zinc-700 text-zinc-400'"
/>
</div>
</template>
</template>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import type { SharedWorkflow } from '@/data/sidebarMockData'
import { LibraryGridCard } from '@/components/common/sidebar'
defineProps<{
workflows: SharedWorkflow[]
@@ -57,31 +58,28 @@ const emit = defineEmits<{
<!-- Grid View -->
<template v-else>
<div class="mb-1.5 flex items-center gap-2 px-1">
<i class="pi pi-sitemap text-xs text-blue-400" />
<span class="text-[10px] font-medium uppercase tracking-wider text-zinc-500">Shared Workflows</span>
<div class="mb-2 flex items-center justify-between px-1">
<div class="flex items-center gap-2">
<i class="pi pi-sitemap text-xs text-blue-400" />
<span class="text-[10px] font-medium uppercase tracking-wider text-zinc-500">Workflows</span>
</div>
<span class="rounded bg-zinc-800 px-1.5 py-0.5 text-[9px] text-zinc-500">
{{ workflows.length }}
</span>
</div>
<div class="grid grid-cols-2 gap-1.5">
<div
<div class="grid grid-cols-2 gap-2">
<LibraryGridCard
v-for="workflow in workflows"
:key="workflow.id"
class="group cursor-pointer rounded-lg border border-zinc-800 bg-zinc-900 p-2 transition-all hover:border-zinc-700 hover:bg-zinc-800/50"
draggable="true"
>
<div class="mb-1 flex items-center justify-between">
<i v-if="workflow.starred" class="pi pi-star-fill text-[10px] text-amber-400" />
<i v-else class="pi pi-sitemap text-[10px] text-zinc-600" />
<span class="rounded bg-zinc-800 px-1 py-0.5 text-[9px] text-zinc-600">
{{ workflow.nodes }}
</span>
</div>
<div class="truncate text-xs text-zinc-400 group-hover:text-zinc-200">
{{ workflow.name }}
</div>
<div class="mt-0.5 truncate text-[10px] text-zinc-600">
{{ workflow.updatedAt }}
</div>
</div>
:title="workflow.name"
:subtitle="`${workflow.nodes} nodes · ${workflow.updatedAt}`"
:thumbnail="workflow.thumbnail"
icon="pi pi-sitemap"
icon-class="text-blue-400"
:badge="workflow.category"
badge-class="bg-blue-500/30 text-blue-300"
:starred="workflow.starred"
/>
</div>
</template>
</template>

View File

@@ -11,7 +11,7 @@ function handleTabClick(tabId: Exclude<SidebarTabId, null>): void {
</script>
<template>
<nav class="flex w-12 flex-col items-center border-r border-zinc-800 bg-zinc-900 py-2">
<nav class="flex w-12 flex-col items-center border-r border-zinc-800 bg-black py-2">
<!-- Tab buttons -->
<div class="flex flex-col gap-1">
<button

View File

@@ -6,9 +6,9 @@ import { SidebarSearchBox, SidebarViewToggle } from '@/components/common/sidebar
import V1SidebarNodesTab from './V1SidebarNodesTab.vue'
import V1SidebarModelsTab from './V1SidebarModelsTab.vue'
import V1SidebarWorkflowsTab from './V1SidebarWorkflowsTab.vue'
import V1SidebarAssetsTab from './V1SidebarAssetsTab.vue'
import V1SidebarTemplatesTab from './V1SidebarTemplatesTab.vue'
import LibrarySidebar from '@/components/v2/canvas/LibrarySidebar.vue'
import AssetsSidebar from '@/components/v2/canvas/AssetsSidebar.vue'
import TemplatesSidebar from '@/components/v2/canvas/TemplatesSidebar.vue'
const uiStore = useUiStore()
@@ -83,7 +83,7 @@ function setFilter(value: string): void {
<template>
<aside
class="border-r border-zinc-800 bg-zinc-900/95 transition-all duration-200"
class="border-r border-zinc-800 bg-black/95 transition-all duration-200"
:class="sidebarPanelExpanded ? 'w-80' : 'w-0 overflow-hidden'"
>
<!-- Library Tab - Full custom layout -->
@@ -92,6 +92,18 @@ function setFilter(value: string): void {
@close="uiStore.closeSidebarPanel()"
/>
<!-- Assets Tab - Full custom layout -->
<AssetsSidebar
v-else-if="sidebarPanelExpanded && activeSidebarTab === 'assets'"
@close="uiStore.closeSidebarPanel()"
/>
<!-- Templates Tab - Full custom layout -->
<TemplatesSidebar
v-else-if="sidebarPanelExpanded && activeSidebarTab === 'templates'"
@close="uiStore.closeSidebarPanel()"
/>
<!-- Other Tabs - Standard layout -->
<div v-else-if="sidebarPanelExpanded" class="flex h-full w-80 flex-col">
<!-- Panel Header -->
@@ -136,7 +148,7 @@ function setFilter(value: string): void {
</button>
<div
v-if="showFilterMenu"
class="absolute left-0 top-full z-50 mt-1 min-w-[120px] rounded-lg border border-zinc-700 bg-zinc-900 py-1 shadow-xl"
class="absolute left-0 top-full z-50 mt-1 min-w-[120px] rounded-lg border border-zinc-700 bg-black py-1 shadow-xl"
>
<button
v-for="option in filterOptions"
@@ -162,7 +174,7 @@ function setFilter(value: string): void {
</button>
<div
v-if="showSortMenu"
class="absolute right-0 top-full z-50 mt-1 min-w-[120px] rounded-lg border border-zinc-700 bg-zinc-900 py-1 shadow-xl"
class="absolute right-0 top-full z-50 mt-1 min-w-[120px] rounded-lg border border-zinc-700 bg-black py-1 shadow-xl"
>
<button
v-for="option in sortOptions"
@@ -184,8 +196,6 @@ function setFilter(value: string): void {
<V1SidebarNodesTab v-if="activeSidebarTab === 'nodes'" :view-mode="viewMode" />
<V1SidebarModelsTab v-else-if="activeSidebarTab === 'models'" :view-mode="viewMode" />
<V1SidebarWorkflowsTab v-else-if="activeSidebarTab === 'workflows'" :view-mode="viewMode" />
<V1SidebarAssetsTab v-else-if="activeSidebarTab === 'assets'" :view-mode="viewMode" />
<V1SidebarTemplatesTab v-else-if="activeSidebarTab === 'templates'" :view-mode="viewMode" />
</div>
</div>
</aside>

View File

@@ -0,0 +1,314 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import Button from 'primevue/button'
import { SidebarSearchBox, SidebarViewToggle, LibraryGridCard } from '@/components/common/sidebar'
interface AssetItem {
id: string
name: string
type: 'image' | 'video' | 'audio' | 'mask' | '3d'
size: string
dimensions?: string
thumbnail?: string
icon: string
iconClass: string
badge?: string
badgeClass?: string
updatedAt: string
}
const emit = defineEmits<{
close: []
}>()
const searchQuery = ref('')
const viewMode = ref<'list' | 'grid'>('grid')
const sortBy = ref('recent')
const showFilterMenu = ref(false)
const showSortMenu = ref(false)
const activeFilters = ref<Set<string>>(new Set())
const sortOptions = [
{ label: 'Recent', value: 'recent' },
{ label: 'Name', value: 'name' },
{ label: 'Size', value: 'size' },
]
const filterOptions = [
{ label: 'Images', value: 'image', icon: 'pi pi-image', color: 'text-blue-400' },
{ label: 'Videos', value: 'video', icon: 'pi pi-video', color: 'text-purple-400' },
{ label: 'Audio', value: 'audio', icon: 'pi pi-volume-up', color: 'text-green-400' },
{ label: 'Masks', value: 'mask', icon: 'pi pi-circle', color: 'text-amber-400' },
{ label: '3D', value: '3d', icon: 'pi pi-box', color: 'text-cyan-400' },
]
function setSort(value: string): void {
sortBy.value = value
showSortMenu.value = false
}
function toggleFilter(value: string): void {
const newFilters = new Set(activeFilters.value)
if (newFilters.has(value)) {
newFilters.delete(value)
} else {
newFilters.add(value)
}
activeFilters.value = newFilters
}
function clearFilters(): void {
activeFilters.value = new Set()
}
const filterLabel = computed(() => {
if (activeFilters.value.size === 0) return 'All'
if (activeFilters.value.size === 1) {
const value = [...activeFilters.value][0]
return filterOptions.find(o => o.value === value)?.label || 'All'
}
return `${activeFilters.value.size} selected`
})
// Mock assets data
const allAssets = computed<AssetItem[]>(() => [
{ id: '1', name: 'reference_portrait.png', type: 'image', size: '2.4 MB', dimensions: '1024x1024', thumbnail: '/assets/card_images/workflow_01.webp', icon: 'pi pi-image', iconClass: 'text-blue-400', badge: 'PNG', badgeClass: 'bg-blue-500/30 text-blue-300', updatedAt: '2 hours ago' },
{ id: '2', name: 'depth_map_01.png', type: 'mask', size: '512 KB', dimensions: '512x512', thumbnail: '/assets/card_images/2690a78c-c210-4a52-8c37-3cb5bc4d9e71.webp', icon: 'pi pi-circle', iconClass: 'text-amber-400', badge: 'Mask', badgeClass: 'bg-amber-500/30 text-amber-300', updatedAt: '1 day ago' },
{ id: '3', name: 'hero_background.jpg', type: 'image', size: '3.8 MB', dimensions: '1920x1080', thumbnail: '/assets/card_images/bacb46ea-7e63-4f19-a253-daf41461e98f.webp', icon: 'pi pi-image', iconClass: 'text-blue-400', badge: 'JPG', badgeClass: 'bg-blue-500/30 text-blue-300', updatedAt: '3 days ago' },
{ id: '4', name: 'animation_loop.mp4', type: 'video', size: '12.5 MB', dimensions: '1080x1920', thumbnail: '/assets/card_images/228616f4-12ad-426d-84fb-f20e488ba7ee.webp', icon: 'pi pi-video', iconClass: 'text-purple-400', badge: 'MP4', badgeClass: 'bg-purple-500/30 text-purple-300', updatedAt: '1 week ago' },
{ id: '5', name: 'controlnet_pose.png', type: 'mask', size: '890 KB', dimensions: '768x1024', thumbnail: '/assets/card_images/683255d3-1d10-43d9-a6ff-ef142061e88a.webp', icon: 'pi pi-circle', iconClass: 'text-amber-400', badge: 'Pose', badgeClass: 'bg-amber-500/30 text-amber-300', updatedAt: '2 days ago' },
{ id: '6', name: 'product_shot.png', type: 'image', size: '1.8 MB', dimensions: '2048x2048', thumbnail: '/assets/card_images/91f1f589-ddb4-4c4f-b3a7-ba30fc271987.webp', icon: 'pi pi-image', iconClass: 'text-blue-400', badge: 'PNG', badgeClass: 'bg-blue-500/30 text-blue-300', updatedAt: '5 days ago' },
{ id: '7', name: 'ambient_audio.wav', type: 'audio', size: '4.2 MB', icon: 'pi pi-volume-up', iconClass: 'text-green-400', badge: 'WAV', badgeClass: 'bg-green-500/30 text-green-300', updatedAt: '1 week ago' },
{ id: '8', name: 'canny_edges.png', type: 'mask', size: '320 KB', dimensions: '1024x1024', thumbnail: '/assets/card_images/28e9f7ea-ef00-48e8-849d-8752a34939c7.webp', icon: 'pi pi-circle', iconClass: 'text-amber-400', badge: 'Canny', badgeClass: 'bg-amber-500/30 text-amber-300', updatedAt: '4 days ago' },
{ id: '9', name: 'style_reference.webp', type: 'image', size: '680 KB', dimensions: '512x768', thumbnail: '/assets/card_images/comfyui_workflow.jpg', icon: 'pi pi-image', iconClass: 'text-blue-400', badge: 'WEBP', badgeClass: 'bg-blue-500/30 text-blue-300', updatedAt: '6 days ago' },
{ id: '10', name: 'promo_video.mp4', type: 'video', size: '28.4 MB', dimensions: '1920x1080', thumbnail: '/assets/card_images/dda28581-37c8-44da-8822-57d1ccc2118c_2130x1658.png', icon: 'pi pi-video', iconClass: 'text-purple-400', badge: 'MP4', badgeClass: 'bg-purple-500/30 text-purple-300', updatedAt: '2 weeks ago' },
])
// Filter and search
const filteredAssets = computed(() => {
let items = allAssets.value
// Apply type filters (multi-select)
if (activeFilters.value.size > 0) {
items = items.filter(i => activeFilters.value.has(i.type))
}
// Apply search
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
items = items.filter(i => i.name.toLowerCase().includes(query))
}
// Apply sort
if (sortBy.value === 'name') {
items = [...items].sort((a, b) => a.name.localeCompare(b.name))
} else if (sortBy.value === 'size') {
items = [...items].sort((a, b) => {
const sizeA = parseFloat(a.size)
const sizeB = parseFloat(b.size)
return sizeB - sizeA
})
}
return items
})
</script>
<template>
<div class="flex h-full w-80 flex-col">
<!-- Panel Header -->
<div class="flex items-center justify-between border-b border-zinc-800 px-3 py-2">
<span class="text-xs font-semibold uppercase tracking-wide text-zinc-400">
ASSETS
</span>
<div class="flex items-center gap-1">
<Button
icon="pi pi-window-maximize"
text
severity="secondary"
size="small"
class="!h-6 !w-6"
v-tooltip.top="'Expand'"
/>
<Button
icon="pi pi-times"
text
severity="secondary"
size="small"
class="!h-6 !w-6"
@click="emit('close')"
/>
</div>
</div>
<!-- Search & Controls -->
<div class="border-b border-zinc-800 p-2">
<SidebarSearchBox
v-model="searchQuery"
placeholder="Search assets..."
:show-action="true"
action-tooltip="Upload Asset"
action-icon="pi pi-upload"
/>
<!-- View Controls -->
<div class="mt-2 flex items-center justify-between">
<SidebarViewToggle v-model="viewMode" />
<!-- Filter & Sort -->
<div class="flex items-center gap-1">
<!-- Filter Dropdown -->
<div class="relative">
<button
:class="[
'flex h-6 items-center gap-1 rounded px-2 text-[10px] transition-colors',
activeFilters.size > 0
? 'bg-blue-600/20 text-blue-400 hover:bg-blue-600/30'
: 'bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200'
]"
@click="showFilterMenu = !showFilterMenu"
>
<i class="pi pi-filter text-[10px]" />
<span>{{ filterLabel }}</span>
<i class="pi pi-chevron-down text-[8px]" />
</button>
<div
v-if="showFilterMenu"
class="absolute left-0 top-full z-50 mt-1 w-36 rounded-lg border border-zinc-700 bg-black py-1 shadow-xl"
>
<!-- Clear all -->
<button
v-if="activeFilters.size > 0"
class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs text-zinc-500 transition-colors hover:bg-zinc-800 hover:text-zinc-300"
@click="clearFilters"
>
<i class="pi pi-times text-[10px]" />
Clear all
</button>
<div v-if="activeFilters.size > 0" class="mx-2 my-1 h-px bg-zinc-800" />
<!-- Filter options -->
<button
v-for="option in filterOptions"
:key="option.value"
class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs transition-colors hover:bg-zinc-800"
@click="toggleFilter(option.value)"
>
<div
:class="[
'flex h-3.5 w-3.5 items-center justify-center rounded border transition-colors',
activeFilters.has(option.value)
? 'border-blue-500 bg-blue-500'
: 'border-zinc-600 bg-transparent'
]"
>
<i v-if="activeFilters.has(option.value)" class="pi pi-check text-[8px] text-white" />
</div>
<i :class="[option.icon, 'text-[10px]', option.color]" />
<span :class="activeFilters.has(option.value) ? 'text-zinc-200' : 'text-zinc-400'">
{{ option.label }}
</span>
</button>
</div>
</div>
<!-- Sort Dropdown -->
<div class="relative">
<button
class="flex h-6 items-center gap-1 rounded bg-zinc-800 px-2 text-[10px] text-zinc-400 transition-colors hover:bg-zinc-700 hover:text-zinc-200"
@click="showSortMenu = !showSortMenu"
>
<i class="pi pi-sort-alt text-[10px]" />
<span>{{ sortOptions.find(o => o.value === sortBy)?.label }}</span>
<i class="pi pi-chevron-down text-[8px]" />
</button>
<div
v-if="showSortMenu"
class="absolute right-0 top-full z-50 mt-1 min-w-[100px] rounded-lg border border-zinc-700 bg-black py-1 shadow-xl"
>
<button
v-for="option in sortOptions"
:key="option.value"
class="flex w-full items-center px-3 py-1.5 text-left text-xs transition-colors"
:class="sortBy === option.value ? 'bg-zinc-800 text-zinc-200' : 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200'"
@click="setSort(option.value)"
>
{{ option.label }}
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Content -->
<div class="flex-1 overflow-y-auto p-2">
<!-- Empty State -->
<div
v-if="filteredAssets.length === 0"
class="flex flex-col items-center justify-center py-8 text-center"
>
<i class="pi pi-images mb-2 text-2xl text-zinc-600" />
<p class="text-xs text-zinc-500">No assets found</p>
</div>
<!-- List View -->
<div v-else-if="viewMode === 'list'" class="select-none space-y-0.5">
<div
v-for="asset in filteredAssets"
:key="asset.id"
class="group flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 transition-colors hover:bg-zinc-800"
draggable="true"
>
<i :class="[asset.icon, 'text-xs', asset.iconClass]" />
<div class="min-w-0 flex-1">
<div class="flex items-center gap-1.5">
<span class="truncate text-xs text-zinc-300 group-hover:text-zinc-100">{{ asset.name }}</span>
</div>
<div class="flex items-center gap-2 text-[10px] text-zinc-600">
<span v-if="asset.badge" :class="['rounded px-1 py-0.5 text-[9px]', asset.badgeClass]">
{{ asset.badge }}
</span>
<span>{{ asset.size }}</span>
<span v-if="asset.dimensions">{{ asset.dimensions }}</span>
</div>
</div>
<i class="pi pi-plus text-[10px] text-zinc-600 opacity-0 transition-opacity group-hover:opacity-100" />
</div>
</div>
<!-- Grid View -->
<div v-else class="grid grid-cols-1 gap-2">
<LibraryGridCard
v-for="asset in filteredAssets"
:key="asset.id"
:title="asset.name"
:subtitle="`${asset.size}${asset.dimensions ? ' · ' + asset.dimensions : ''}`"
:thumbnail="asset.thumbnail"
:icon="asset.icon"
:icon-class="asset.iconClass"
:badge="asset.badge"
:badge-class="asset.badgeClass"
/>
</div>
</div>
</div>
</template>
<style scoped>
div::-webkit-scrollbar {
width: 4px;
}
div::-webkit-scrollbar-track {
background: transparent;
}
div::-webkit-scrollbar-thumb {
background: #3f3f46;
border-radius: 2px;
}
div::-webkit-scrollbar-thumb:hover {
background: #52525b;
}
</style>

View File

@@ -146,7 +146,7 @@ const mockRecents = [
<!-- Expandable Panel (above tabs) -->
<div
v-if="bottomPanelExpanded"
class="bottom-panel flex overflow-hidden rounded-xl border border-zinc-800 bg-zinc-900/95 shadow-2xl backdrop-blur transition-all duration-300"
class="bottom-panel flex overflow-hidden rounded-xl border border-zinc-800 bg-black/95 shadow-2xl backdrop-blur transition-all duration-300"
:style="{
width: isExtended ? 'calc(100vw - 100px)' : '720px',
maxWidth: isExtended ? '1400px' : '720px',
@@ -157,7 +157,7 @@ const mockRecents = [
<!-- Left Sidebar -->
<div
v-if="showSidebar"
class="flex w-48 shrink-0 flex-col border-r border-zinc-800 bg-zinc-900/50"
class="flex w-48 shrink-0 flex-col border-r border-zinc-800 bg-black/50"
>
<div class="p-3">
<div class="text-[10px] font-semibold uppercase tracking-wider text-zinc-500">Categories</div>
@@ -431,7 +431,7 @@ const mockRecents = [
</div>
<!-- Bottom Tab Bar -->
<div class="flex items-center gap-1 rounded-lg border border-zinc-800 bg-zinc-900/90 px-2 py-1.5 backdrop-blur">
<div class="flex items-center gap-1 rounded-lg border border-zinc-800 bg-black/90 px-2 py-1.5 backdrop-blur">
<!-- Tab buttons -->
<button
v-for="tab in BOTTOM_BAR_TABS"
@@ -447,6 +447,23 @@ const mockRecents = [
>
<i :class="[tab.icon, 'text-base']" />
</button>
<!-- Divider -->
<div class="mx-1 h-5 w-px bg-zinc-700" />
<!-- Settings & Shortcuts -->
<button
v-tooltip.top="{ value: 'Keyboard Shortcuts', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-400 transition-colors hover:bg-zinc-800 hover:text-zinc-200"
>
<i class="pi pi-bolt text-base" />
</button>
<button
v-tooltip.top="{ value: 'Settings', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-400 transition-colors hover:bg-zinc-800 hover:text-zinc-200"
>
<i class="pi pi-cog text-base" />
</button>
</div>
</div>
</template>

View File

@@ -0,0 +1,146 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
interface Props {
orientation?: 'vertical' | 'horizontal'
}
const props = withDefaults(defineProps<Props>(), {
orientation: 'vertical'
})
const emit = defineEmits<{
fitView: []
zoomIn: []
zoomOut: []
}>()
// Tool mode: 'select' or 'pan'
const toolMode = ref<'select' | 'pan'>('select')
// Zoom level (percentage)
const zoomLevel = ref(75)
// Toggle states
const showMinimap = ref(false)
const showLinks = ref(true)
const isVertical = computed(() => props.orientation === 'vertical')
const tooltipPos = computed(() => isVertical.value ? 'left' : 'top')
function setToolMode(mode: 'select' | 'pan'): void {
toolMode.value = mode
}
function handleFitView(): void {
emit('fitView')
}
function handleZoomIn(): void {
zoomLevel.value = Math.min(400, zoomLevel.value + 25)
emit('zoomIn')
}
function handleZoomOut(): void {
zoomLevel.value = Math.max(10, zoomLevel.value - 25)
emit('zoomOut')
}
function toggleMinimap(): void {
showMinimap.value = !showMinimap.value
}
function toggleLinks(): void {
showLinks.value = !showLinks.value
}
</script>
<template>
<div
class="absolute z-10"
:class="isVertical ? 'right-4 top-1/2 -translate-y-1/2' : 'bottom-4 right-4'"
>
<div
class="flex items-center gap-1 rounded-lg border border-zinc-800 bg-black/90 p-1.5 backdrop-blur"
:class="isVertical ? 'flex-col' : 'flex-row'"
>
<!-- Select / Pan Toggle -->
<button
v-tooltip:[tooltipPos]="{ value: 'Select', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center rounded-md transition-colors"
:class="toolMode === 'select' ? 'bg-zinc-700 text-zinc-100' : 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200'"
@click="setToolMode('select')"
>
<i class="pi pi-arrow-up-left text-base" />
</button>
<button
v-tooltip:[tooltipPos]="{ value: 'Pan', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center rounded-md transition-colors"
:class="toolMode === 'pan' ? 'bg-zinc-700 text-zinc-100' : 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200'"
@click="setToolMode('pan')"
>
<i class="pi pi-arrows-alt text-base" />
</button>
<!-- Divider -->
<div :class="isVertical ? 'my-1 h-px w-5 bg-zinc-700' : 'mx-1 h-5 w-px bg-zinc-700'" />
<!-- Fit to Screen -->
<button
v-tooltip:[tooltipPos]="{ value: 'Fit to Screen', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-400 transition-colors hover:bg-zinc-800 hover:text-zinc-200"
@click="handleFitView"
>
<i class="pi pi-expand text-base" />
</button>
<!-- Divider -->
<div :class="isVertical ? 'my-1 h-px w-5 bg-zinc-700' : 'mx-1 h-5 w-px bg-zinc-700'" />
<!-- Zoom Controls -->
<button
v-tooltip:[tooltipPos]="{ value: 'Zoom In', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-400 transition-colors hover:bg-zinc-800 hover:text-zinc-200"
@click="handleZoomIn"
>
<i class="pi pi-plus text-sm" />
</button>
<div
v-tooltip:[tooltipPos]="{ value: 'Zoom Level', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center text-[10px] font-medium text-zinc-400"
>
{{ zoomLevel }}%
</div>
<button
v-tooltip:[tooltipPos]="{ value: 'Zoom Out', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-400 transition-colors hover:bg-zinc-800 hover:text-zinc-200"
@click="handleZoomOut"
>
<i class="pi pi-minus text-sm" />
</button>
<!-- Divider -->
<div :class="isVertical ? 'my-1 h-px w-5 bg-zinc-700' : 'mx-1 h-5 w-px bg-zinc-700'" />
<!-- Minimap Toggle -->
<button
v-tooltip:[tooltipPos]="{ value: showMinimap ? 'Hide Minimap' : 'Show Minimap', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center rounded-md transition-colors"
:class="showMinimap ? 'bg-zinc-700 text-zinc-100' : 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200'"
@click="toggleMinimap"
>
<i class="pi pi-map text-base" />
</button>
<!-- Links Toggle -->
<button
v-tooltip:[tooltipPos]="{ value: showLinks ? 'Hide Links' : 'Show Links', showDelay: 300 }"
class="flex h-8 w-8 items-center justify-center rounded-md transition-colors"
:class="showLinks ? 'bg-zinc-700 text-zinc-100' : 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200'"
@click="toggleLinks"
>
<i class="pi pi-link text-base" />
</button>
</div>
</div>
</template>

View File

@@ -0,0 +1,115 @@
<script setup lang="ts">
import { ref } from 'vue'
import Popover from 'primevue/popover'
const runMenu = ref<InstanceType<typeof Popover> | null>(null)
const isRunning = ref(false)
const queueCount = ref(0)
function toggleRunMenu(event: Event): void {
runMenu.value?.toggle(event)
}
function runWorkflow(): void {
isRunning.value = true
setTimeout(() => {
isRunning.value = false
}, 2000)
}
function runOnChange(): void {
// Toggle run on change mode
}
function addToQueue(): void {
queueCount.value++
}
function clearQueue(): void {
queueCount.value = 0
}
</script>
<template>
<div class="absolute right-4 top-4 z-10 flex items-center gap-2">
<!-- Queue indicator -->
<div
v-if="queueCount > 0"
class="flex items-center gap-1.5 rounded-md bg-amber-500/20 px-2.5 py-1.5 text-xs font-medium text-amber-400"
>
<i class="pi pi-list text-[10px]" />
<span>{{ queueCount }} in queue</span>
<button
class="ml-1 rounded p-0.5 transition-colors hover:bg-amber-500/20"
@click="clearQueue"
>
<i class="pi pi-times text-[10px]" />
</button>
</div>
<!-- Add to Queue -->
<button
v-tooltip.bottom="{ value: 'Add to Queue', showDelay: 50 }"
class="flex h-8 items-center gap-1.5 rounded-md bg-zinc-800/80 px-3 text-sm text-zinc-300 shadow-sm backdrop-blur transition-colors hover:bg-zinc-700 hover:text-white"
@click="addToQueue"
>
<i class="pi pi-plus text-xs" />
<span>Queue</span>
</button>
<!-- Run Button with Dropdown -->
<div class="relative flex">
<button
class="flex h-8 items-center gap-1.5 rounded-l-md bg-blue-600 px-3 text-sm font-medium text-white shadow-sm transition-colors hover:bg-blue-500"
:disabled="isRunning"
@click="runWorkflow"
>
<i :class="['text-xs', isRunning ? 'pi pi-spin pi-spinner' : 'pi pi-play']" />
<span>{{ isRunning ? 'Running...' : 'Run' }}</span>
</button>
<button
class="flex h-8 items-center rounded-r-md border-l border-blue-500 bg-blue-600 px-1.5 text-white shadow-sm transition-colors hover:bg-blue-500"
@click="toggleRunMenu"
>
<i class="pi pi-chevron-down text-[10px]" />
</button>
<!-- Run Menu Popover -->
<Popover ref="runMenu" append-to="self">
<div class="flex w-48 flex-col py-1">
<button
class="flex items-center gap-2 px-3 py-2 text-sm text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
@click="runWorkflow(); runMenu?.hide()"
>
<i class="pi pi-play text-xs text-blue-500" />
<span>Run Workflow</span>
<span class="ml-auto text-xs text-zinc-400"></span>
</button>
<button
class="flex items-center gap-2 px-3 py-2 text-sm text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
@click="runOnChange(); runMenu?.hide()"
>
<i class="pi pi-sync text-xs text-green-500" />
<span>Run on Change</span>
</button>
<div class="my-1 h-px bg-zinc-200 dark:bg-zinc-700" />
<button
class="flex items-center gap-2 px-3 py-2 text-sm text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
@click="addToQueue(); runMenu?.hide()"
>
<i class="pi pi-plus text-xs text-amber-500" />
<span>Add to Queue</span>
<span class="ml-auto text-xs text-zinc-400"></span>
</button>
<button
class="flex items-center gap-2 px-3 py-2 text-sm text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
@click="clearQueue(); runMenu?.hide()"
>
<i class="pi pi-trash text-xs text-red-500" />
<span>Clear Queue</span>
</button>
</div>
</Popover>
</div>
</div>
</template>

View File

@@ -0,0 +1,298 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import Dialog from 'primevue/dialog'
import InputText from 'primevue/inputtext'
import Select from 'primevue/select'
interface SharedUser {
id: string
name: string
email: string
avatar?: string
initials: string
role: 'owner' | 'editor' | 'viewer'
color: string
}
const props = defineProps<{
visible: boolean
workflowName?: string
}>()
const emit = defineEmits<{
'update:visible': [value: boolean]
}>()
const dialogVisible = computed({
get: () => props.visible,
set: (value) => emit('update:visible', value)
})
const inviteEmail = ref('')
const inviteRole = ref<'editor' | 'viewer'>('editor')
const linkCopied = ref(false)
const linkAccess = ref<'restricted' | 'anyone'>('restricted')
const linkPermission = ref<'viewer' | 'editor'>('viewer')
const roleOptions = [
{ label: 'Can edit', value: 'editor' },
{ label: 'Can view', value: 'viewer' }
]
const linkAccessOptions = [
{ label: 'Restricted', value: 'restricted', description: 'Only people with access can open' },
{ label: 'Anyone with the link', value: 'anyone', description: 'Anyone on the internet with the link can view' }
]
const sharedUsers = ref<SharedUser[]>([
{ id: '1', name: 'John Doe', email: 'john@example.com', initials: 'JD', role: 'owner', color: 'bg-blue-600' },
{ id: '2', name: 'Sarah Wilson', email: 'sarah@example.com', initials: 'SW', role: 'editor', color: 'bg-purple-600' },
{ id: '3', name: 'Mike Chen', email: 'mike@example.com', initials: 'MC', role: 'viewer', color: 'bg-green-600' }
])
const shareLink = computed(() => {
return `https://comfy.app/share/${props.workflowName?.toLowerCase().replace(/\s+/g, '-') || 'workflow'}`
})
function copyLink(): void {
navigator.clipboard.writeText(shareLink.value)
linkCopied.value = true
setTimeout(() => {
linkCopied.value = false
}, 2000)
}
function inviteUser(): void {
if (!inviteEmail.value) return
const newUser: SharedUser = {
id: Date.now().toString(),
name: inviteEmail.value.split('@')[0] || 'User',
email: inviteEmail.value,
initials: inviteEmail.value.substring(0, 2).toUpperCase(),
role: inviteRole.value,
color: ['bg-pink-600', 'bg-amber-600', 'bg-cyan-600', 'bg-indigo-600'][Math.floor(Math.random() * 4)] || 'bg-zinc-600'
}
sharedUsers.value.push(newUser)
inviteEmail.value = ''
}
function updateUserRole(userId: string, role: 'editor' | 'viewer'): void {
const user = sharedUsers.value.find(u => u.id === userId)
if (user && user.role !== 'owner') {
user.role = role
}
}
function removeUser(userId: string): void {
const index = sharedUsers.value.findIndex(u => u.id === userId)
if (index > -1 && sharedUsers.value[index]?.role !== 'owner') {
sharedUsers.value.splice(index, 1)
}
}
function getRoleLabel(role: string): string {
switch (role) {
case 'owner': return 'Owner'
case 'editor': return 'Can edit'
case 'viewer': return 'Can view'
default: return role
}
}
</script>
<template>
<Dialog
v-model:visible="dialogVisible"
modal
:draggable="false"
:closable="true"
:pt="{
root: { class: 'bg-white dark:bg-zinc-900 rounded-xl shadow-2xl border border-zinc-200 dark:border-zinc-800 w-[480px]' },
header: { class: 'p-0' },
content: { class: 'p-0' },
mask: { class: 'backdrop-blur-sm bg-black/50' }
}"
>
<template #header>
<div class="flex w-full items-center justify-between border-b border-zinc-200 px-5 py-4 dark:border-zinc-800">
<div>
<h2 class="text-base font-semibold text-zinc-900 dark:text-zinc-100">Share "{{ workflowName || 'Workflow' }}"</h2>
<p class="mt-0.5 text-sm text-zinc-500 dark:text-zinc-400">Invite others to collaborate</p>
</div>
</div>
</template>
<div class="p-5">
<!-- Invite Section -->
<div class="mb-5">
<label class="mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-300">
Invite people
</label>
<div class="flex gap-2">
<div class="relative flex-1">
<i class="pi pi-envelope absolute left-3 top-1/2 -translate-y-1/2 text-sm text-zinc-400" />
<InputText
v-model="inviteEmail"
placeholder="Enter email address"
class="w-full rounded-lg border border-zinc-300 bg-white py-2.5 pl-9 pr-3 text-sm text-zinc-900 placeholder-zinc-400 outline-none transition-colors focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-500"
@keyup.enter="inviteUser"
/>
</div>
<Select
v-model="inviteRole"
:options="roleOptions"
option-label="label"
option-value="value"
class="w-32"
:pt="{
root: { class: 'border border-zinc-300 dark:border-zinc-700 rounded-lg' },
label: { class: 'text-sm py-2.5 px-3' }
}"
/>
<button
class="rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-blue-500 disabled:opacity-50"
:disabled="!inviteEmail"
@click="inviteUser"
>
Invite
</button>
</div>
</div>
<!-- People with access -->
<div class="mb-5">
<label class="mb-2 block text-sm font-medium text-zinc-700 dark:text-zinc-300">
People with access
</label>
<div class="max-h-48 space-y-1 overflow-y-auto rounded-lg border border-zinc-200 dark:border-zinc-800">
<div
v-for="user in sharedUsers"
:key="user.id"
class="group flex items-center gap-3 px-3 py-2.5 transition-colors hover:bg-zinc-50 dark:hover:bg-zinc-800/50"
>
<!-- Avatar -->
<div
:class="[
'flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full text-xs font-semibold text-white',
user.color
]"
>
{{ user.initials }}
</div>
<!-- User info -->
<div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-zinc-900 dark:text-zinc-100">
{{ user.name }}
<span v-if="user.role === 'owner'" class="ml-1 text-xs text-zinc-400">(you)</span>
</p>
<p class="truncate text-xs text-zinc-500 dark:text-zinc-400">{{ user.email }}</p>
</div>
<!-- Role selector / Remove -->
<div class="flex items-center gap-1">
<Select
v-if="user.role !== 'owner'"
:model-value="user.role"
:options="roleOptions"
option-label="label"
option-value="value"
class="w-28"
:pt="{
root: { class: 'border-0 bg-transparent' },
label: { class: 'text-xs py-1 px-2 text-zinc-500 dark:text-zinc-400' },
trigger: { class: 'w-4' }
}"
@update:model-value="updateUserRole(user.id, $event)"
/>
<span v-else class="px-2 text-xs text-zinc-400">{{ getRoleLabel(user.role) }}</span>
<button
v-if="user.role !== 'owner'"
class="rounded p-1 text-zinc-400 opacity-0 transition-all hover:bg-zinc-200 hover:text-zinc-600 group-hover:opacity-100 dark:hover:bg-zinc-700 dark:hover:text-zinc-300"
@click="removeUser(user.id)"
>
<i class="pi pi-times text-xs" />
</button>
</div>
</div>
</div>
</div>
<!-- Link sharing section -->
<div class="rounded-lg border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-800/50">
<div class="mb-3 flex items-start gap-3">
<div class="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-zinc-200 dark:bg-zinc-700">
<i class="pi pi-link text-zinc-600 dark:text-zinc-400" />
</div>
<div class="flex-1">
<div class="flex items-center gap-2">
<Select
v-model="linkAccess"
:options="linkAccessOptions"
option-label="label"
option-value="value"
class="flex-1"
:pt="{
root: { class: 'border-0 bg-transparent' },
label: { class: 'text-sm font-medium py-0 px-0 text-zinc-900 dark:text-zinc-100' },
trigger: { class: 'w-4' }
}"
/>
</div>
<p class="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">
{{ linkAccess === 'restricted' ? 'Only people with access can open' : 'Anyone on the internet with the link' }}
</p>
</div>
<Select
v-if="linkAccess === 'anyone'"
v-model="linkPermission"
:options="roleOptions"
option-label="label"
option-value="value"
class="w-28"
:pt="{
root: { class: 'border border-zinc-300 dark:border-zinc-700 rounded-lg' },
label: { class: 'text-xs py-1.5 px-2' }
}"
/>
</div>
<!-- Copy link -->
<div class="flex gap-2">
<div class="flex-1 truncate rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-600 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-400">
{{ shareLink }}
</div>
<button
:class="[
'flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
linkCopied
? 'bg-green-600 text-white'
: 'bg-zinc-900 text-white hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200'
]"
@click="copyLink"
>
<i :class="['text-xs', linkCopied ? 'pi pi-check' : 'pi pi-copy']" />
{{ linkCopied ? 'Copied!' : 'Copy link' }}
</button>
</div>
</div>
</div>
<!-- Footer -->
<div class="flex items-center justify-between border-t border-zinc-200 px-5 py-4 dark:border-zinc-800">
<button class="flex items-center gap-1.5 text-sm text-zinc-500 transition-colors hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200">
<i class="pi pi-cog text-xs" />
Advanced settings
</button>
<button
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-500"
@click="dialogVisible = false"
>
Done
</button>
</div>
</Dialog>
</template>

View File

@@ -1,10 +1,12 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import CanvasLogoMenu from './CanvasLogoMenu.vue'
import CanvasTabs, { type CanvasTab } from './CanvasTabs.vue'
import CanvasShareDialog from './CanvasShareDialog.vue'
const router = useRouter()
const showShareDialog = ref(false)
const tabs = ref<CanvasTab[]>([
{ id: 'workflow-1', name: 'Main Workflow', isActive: true },
@@ -51,6 +53,10 @@ function createNewTab(): void {
})
selectTab(newId)
}
const activeWorkflowName = computed(() => {
return tabs.value.find(t => t.id === activeTabId.value)?.name || 'Workflow'
})
</script>
<template>
@@ -97,15 +103,16 @@ function createNewTab(): void {
<button
v-tooltip.bottom="{ value: 'Share', showDelay: 50 }"
class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-500 transition-colors hover:bg-zinc-800 hover:text-zinc-100"
@click="showShareDialog = true"
>
<i class="pi pi-share-alt text-sm" />
</button>
<button
v-tooltip.bottom="{ value: 'Run Workflow', showDelay: 50 }"
class="flex h-8 w-8 items-center justify-center rounded-md bg-blue-600 text-white transition-colors hover:bg-blue-500"
>
<i class="pi pi-play text-sm" />
</button>
</div>
<!-- Share Dialog -->
<CanvasShareDialog
v-model:visible="showShareDialog"
:workflow-name="activeWorkflowName"
/>
</div>
</template>

View File

@@ -1,13 +1,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import Button from 'primevue/button'
import Avatar from 'primevue/avatar'
import AvatarGroup from 'primevue/avatargroup'
import { SidebarSearchBox, SidebarViewToggle } from '@/components/common/sidebar'
import LibraryBrandKitSection from '@/components/v1/sidebar/LibraryBrandKitSection.vue'
import LibraryWorkflowsSection from '@/components/v1/sidebar/LibraryWorkflowsSection.vue'
import LibraryModelsSection from '@/components/v1/sidebar/LibraryModelsSection.vue'
import LibraryNodesSection from '@/components/v1/sidebar/LibraryNodesSection.vue'
import { SidebarSearchBox, SidebarViewToggle, LibraryGridCard } from '@/components/common/sidebar'
import {
TEAM_MEMBERS_DATA,
BRAND_ASSETS_DATA,
@@ -16,7 +10,22 @@ import {
NODE_PACKS_DATA,
} from '@/data/sidebarMockData'
const props = defineProps<{
interface LibraryItem {
id: string
name: string
description?: string
type: 'workflow' | 'model' | 'nodepack' | 'brand'
subtype?: string
thumbnail?: string
icon: string
iconClass: string
badge?: string
badgeClass?: string
starred?: boolean
meta?: string
}
defineProps<{
teamName?: string
teamLogo?: string
}>()
@@ -26,63 +35,168 @@ const emit = defineEmits<{
}>()
const searchQuery = ref('')
const viewMode = ref<'list' | 'grid'>('list')
const viewMode = ref<'list' | 'grid'>('grid')
const sortBy = ref('name')
const showFilterMenu = ref(false)
const showSortMenu = ref(false)
const activeFilter = ref('All')
const activeFilters = ref<Set<string>>(new Set())
const sortOptions = [
{ label: 'Name', value: 'name' },
{ label: 'Recently Added', value: 'recent' },
{ label: 'Author', value: 'author' },
]
const filterOptions = ['All', 'Brand Kit', 'Workflows', 'Models', 'Nodes']
const filterOptions = [
{ label: 'Workflows', value: 'workflow', icon: 'pi pi-sitemap', color: 'text-blue-400' },
{ label: 'Models', value: 'model', icon: 'pi pi-box', color: 'text-green-400' },
{ label: 'Nodepacks', value: 'nodepack', icon: 'pi pi-code', color: 'text-purple-400' },
{ label: 'Brand Kit', value: 'brand', icon: 'pi pi-palette', color: 'text-amber-400' },
]
function setSort(value: string): void {
sortBy.value = value
showSortMenu.value = false
}
function setFilter(value: string): void {
activeFilter.value = value
showFilterMenu.value = false
function toggleFilter(value: string): void {
const newFilters = new Set(activeFilters.value)
if (newFilters.has(value)) {
newFilters.delete(value)
} else {
newFilters.add(value)
}
activeFilters.value = newFilters
}
// Current team info
const currentTeam = computed(() => ({
name: props.teamName || 'Netflix',
logo: props.teamLogo,
plan: 'Enterprise',
members: 24,
}))
function clearFilters(): void {
activeFilters.value = new Set()
}
// Collapsible sections
const sections = ref({
brand: true,
workflows: true,
models: false,
nodes: false,
const filterLabel = computed(() => {
if (activeFilters.value.size === 0) return 'All'
if (activeFilters.value.size === 1) {
const value = [...activeFilters.value][0]
return filterOptions.find(o => o.value === value)?.label || 'All'
}
return `${activeFilters.value.size} selected`
})
function toggleSection(sectionId: keyof typeof sections.value): void {
sections.value[sectionId] = !sections.value[sectionId]
}
// Combine all items into a unified list
const allItems = computed<LibraryItem[]>(() => {
const items: LibraryItem[] = []
// Data
const teamMembers = TEAM_MEMBERS_DATA
const brandAssets = BRAND_ASSETS_DATA
const sharedWorkflows = computed(() => createSharedWorkflowsData(teamMembers))
const teamModels = computed(() => createTeamModelsData(teamMembers))
const nodePacks = NODE_PACKS_DATA
// Add workflows
const workflows = createSharedWorkflowsData(TEAM_MEMBERS_DATA)
workflows.forEach(w => {
items.push({
id: `workflow-${w.id}`,
name: w.name,
description: w.description,
type: 'workflow',
thumbnail: w.thumbnail,
icon: 'pi pi-sitemap',
iconClass: 'text-blue-400',
badge: `${w.nodes} nodes`,
badgeClass: 'bg-blue-500/30 text-blue-300',
starred: w.starred,
meta: w.updatedAt,
})
})
const filteredWorkflows = computed(() => {
if (!searchQuery.value) return sharedWorkflows.value
const query = searchQuery.value.toLowerCase()
return sharedWorkflows.value.filter(
w => w.name.toLowerCase().includes(query) || w.description.toLowerCase().includes(query)
)
// Add models
const models = createTeamModelsData(TEAM_MEMBERS_DATA)
models.forEach(m => {
const typeLabels: Record<string, string> = {
checkpoint: 'Checkpoint',
lora: 'LoRA',
embedding: 'Embedding',
controlnet: 'ControlNet',
}
const typeColors: Record<string, string> = {
checkpoint: 'bg-purple-500/30 text-purple-300',
lora: 'bg-green-500/30 text-green-300',
embedding: 'bg-amber-500/30 text-amber-300',
controlnet: 'bg-cyan-500/30 text-cyan-300',
}
items.push({
id: `model-${m.id}`,
name: m.name,
description: m.description,
type: 'model',
subtype: m.type,
thumbnail: m.thumbnail,
icon: 'pi pi-box',
iconClass: 'text-green-400',
badge: typeLabels[m.type] || m.type,
badgeClass: typeColors[m.type] || 'bg-zinc-700 text-zinc-400',
meta: m.size,
})
})
// Add nodepacks
NODE_PACKS_DATA.forEach(p => {
items.push({
id: `nodepack-${p.id}`,
name: p.name,
description: p.description,
type: 'nodepack',
thumbnail: p.thumbnail,
icon: 'pi pi-code',
iconClass: 'text-purple-400',
badge: p.installed ? 'Installed' : `${p.nodes} nodes`,
badgeClass: p.installed ? 'bg-green-500/30 text-green-300' : 'bg-zinc-700 text-zinc-400',
meta: `v${p.version}`,
})
})
// Add brand assets (excluding colors)
BRAND_ASSETS_DATA.filter(a => a.type !== 'color').forEach(a => {
const typeLabels: Record<string, string> = {
logo: 'Logo',
font: 'Font',
template: 'Template',
guideline: 'Guide',
}
items.push({
id: `brand-${a.id}`,
name: a.name,
description: a.description,
type: 'brand',
subtype: a.type,
icon: a.type === 'logo' ? 'pi pi-image' : a.type === 'font' ? 'pi pi-align-left' : a.type === 'template' ? 'pi pi-clone' : 'pi pi-book',
iconClass: 'text-amber-400',
badge: typeLabels[a.type] || a.type,
badgeClass: 'bg-amber-500/30 text-amber-300',
})
})
return items
})
// Filter and search
const filteredItems = computed(() => {
let items = allItems.value
// Apply type filters (multi-select)
if (activeFilters.value.size > 0) {
items = items.filter(i => activeFilters.value.has(i.type))
}
// Apply search
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
items = items.filter(i =>
i.name.toLowerCase().includes(query) ||
i.description?.toLowerCase().includes(query)
)
}
// Apply sort
if (sortBy.value === 'name') {
items = [...items].sort((a, b) => a.name.localeCompare(b.name))
}
return items
})
</script>
@@ -93,14 +207,24 @@ const filteredWorkflows = computed(() => {
<span class="text-xs font-semibold uppercase tracking-wide text-zinc-400">
TEAM LIBRARY
</span>
<Button
icon="pi pi-times"
text
severity="secondary"
size="small"
class="!h-6 !w-6"
@click="emit('close')"
/>
<div class="flex items-center gap-1">
<Button
icon="pi pi-window-maximize"
text
severity="secondary"
size="small"
class="!h-6 !w-6"
v-tooltip.top="'Expand'"
/>
<Button
icon="pi pi-times"
text
severity="secondary"
size="small"
class="!h-6 !w-6"
@click="emit('close')"
/>
</div>
</div>
<!-- Search & Controls -->
@@ -122,25 +246,53 @@ const filteredWorkflows = computed(() => {
<!-- Filter Dropdown -->
<div class="relative">
<button
class="flex h-6 items-center gap-1 rounded bg-zinc-800 px-2 text-[10px] text-zinc-400 transition-colors hover:bg-zinc-700 hover:text-zinc-200"
:class="[
'flex h-6 items-center gap-1 rounded px-2 text-[10px] transition-colors',
activeFilters.size > 0
? 'bg-blue-600/20 text-blue-400 hover:bg-blue-600/30'
: 'bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200'
]"
@click="showFilterMenu = !showFilterMenu"
>
<i class="pi pi-filter text-[10px]" />
<span>{{ activeFilter }}</span>
<span>{{ filterLabel }}</span>
<i class="pi pi-chevron-down text-[8px]" />
</button>
<div
v-if="showFilterMenu"
class="absolute left-0 top-full z-50 mt-1 min-w-[120px] rounded-lg border border-zinc-700 bg-zinc-900 py-1 shadow-xl"
class="absolute left-0 top-full z-50 mt-1 w-40 rounded-lg border border-zinc-700 bg-black py-1 shadow-xl"
>
<!-- Clear all -->
<button
v-if="activeFilters.size > 0"
class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs text-zinc-500 transition-colors hover:bg-zinc-800 hover:text-zinc-300"
@click="clearFilters"
>
<i class="pi pi-times text-[10px]" />
Clear all
</button>
<div v-if="activeFilters.size > 0" class="mx-2 my-1 h-px bg-zinc-800" />
<!-- Filter options -->
<button
v-for="option in filterOptions"
:key="option"
class="flex w-full items-center px-3 py-1.5 text-left text-xs transition-colors"
:class="activeFilter === option ? 'bg-zinc-800 text-zinc-200' : 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200'"
@click="setFilter(option)"
:key="option.value"
class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs transition-colors hover:bg-zinc-800"
@click="toggleFilter(option.value)"
>
{{ option }}
<div
:class="[
'flex h-3.5 w-3.5 items-center justify-center rounded border transition-colors',
activeFilters.has(option.value)
? 'border-blue-500 bg-blue-500'
: 'border-zinc-600 bg-transparent'
]"
>
<i v-if="activeFilters.has(option.value)" class="pi pi-check text-[8px] text-white" />
</div>
<i :class="[option.icon, 'text-[10px]', option.color]" />
<span :class="activeFilters.has(option.value) ? 'text-zinc-200' : 'text-zinc-400'">
{{ option.label }}
</span>
</button>
</div>
</div>
@@ -157,7 +309,7 @@ const filteredWorkflows = computed(() => {
</button>
<div
v-if="showSortMenu"
class="absolute right-0 top-full z-50 mt-1 min-w-[120px] rounded-lg border border-zinc-700 bg-zinc-900 py-1 shadow-xl"
class="absolute right-0 top-full z-50 mt-1 min-w-[120px] rounded-lg border border-zinc-700 bg-black py-1 shadow-xl"
>
<button
v-for="option in sortOptions"
@@ -176,75 +328,54 @@ const filteredWorkflows = computed(() => {
<!-- Content -->
<div class="flex-1 overflow-y-auto p-2">
<!-- Team Header Card -->
<div class="mb-3 rounded-lg border border-zinc-800 bg-zinc-900 p-2.5">
<div class="flex items-center gap-3">
<div
class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-lg text-lg font-bold"
:style="{ backgroundColor: '#E50914' }"
>
<span class="text-white">N</span>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<span class="truncate text-sm font-semibold text-zinc-100">{{ currentTeam.name }}</span>
<span class="rounded bg-blue-500/20 px-1.5 py-0.5 text-[10px] font-medium text-blue-400">
{{ currentTeam.plan }}
</span>
</div>
<div class="mt-0.5 flex items-center gap-2">
<AvatarGroup class="!gap-0">
<Avatar
v-for="member in teamMembers.slice(0, 3)"
:key="member.name"
:label="member.initials"
shape="circle"
size="small"
class="!h-5 !w-5 !border !border-zinc-900 !bg-zinc-700 !text-[9px] !text-zinc-300"
/>
</AvatarGroup>
<span class="text-[10px] text-zinc-500">
{{ currentTeam.members }} members
</span>
</div>
</div>
</div>
<!-- Empty State -->
<div
v-if="filteredItems.length === 0"
class="flex flex-col items-center justify-center py-8 text-center"
>
<i class="pi pi-inbox mb-2 text-2xl text-zinc-600" />
<p class="text-xs text-zinc-500">No items found</p>
</div>
<!-- List View -->
<div v-if="viewMode === 'list'" class="select-none space-y-0.5">
<LibraryBrandKitSection
:assets="brandAssets"
:view-mode="viewMode"
:expanded="sections.brand"
@toggle="toggleSection('brand')"
/>
<LibraryWorkflowsSection
:workflows="filteredWorkflows"
:view-mode="viewMode"
:expanded="sections.workflows"
@toggle="toggleSection('workflows')"
/>
<LibraryModelsSection
:models="teamModels"
:view-mode="viewMode"
:expanded="sections.models"
@toggle="toggleSection('models')"
/>
<LibraryNodesSection
:packs="nodePacks"
:view-mode="viewMode"
:expanded="sections.nodes"
@toggle="toggleSection('nodes')"
/>
<div v-else-if="viewMode === 'list'" class="select-none space-y-0.5">
<div
v-for="item in filteredItems"
:key="item.id"
class="group flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 transition-colors hover:bg-zinc-800"
draggable="true"
>
<i :class="[item.icon, 'text-xs', item.iconClass]" />
<div class="min-w-0 flex-1">
<div class="flex items-center gap-1.5">
<span class="truncate text-xs text-zinc-300 group-hover:text-zinc-100">{{ item.name }}</span>
<i v-if="item.starred" class="pi pi-star-fill text-[8px] text-amber-400" />
</div>
<div class="flex items-center gap-2 text-[10px] text-zinc-600">
<span v-if="item.badge" :class="['rounded px-1 py-0.5 text-[9px]', item.badgeClass]">
{{ item.badge }}
</span>
<span v-if="item.meta">{{ item.meta }}</span>
</div>
</div>
<i class="pi pi-plus text-[10px] text-zinc-600 opacity-0 transition-opacity group-hover:opacity-100" />
</div>
</div>
<!-- Grid View -->
<div v-else class="space-y-3">
<LibraryBrandKitSection :assets="brandAssets" :view-mode="viewMode" :expanded="true" />
<LibraryWorkflowsSection :workflows="filteredWorkflows" :view-mode="viewMode" :expanded="true" />
<LibraryModelsSection :models="teamModels" :view-mode="viewMode" :expanded="true" />
<LibraryNodesSection :packs="nodePacks" :view-mode="viewMode" :expanded="true" />
<div v-else class="grid grid-cols-1 gap-2">
<LibraryGridCard
v-for="item in filteredItems"
:key="item.id"
:title="item.name"
:subtitle="item.meta"
:thumbnail="item.thumbnail"
:icon="item.icon"
:icon-class="item.iconClass"
:badge="item.badge"
:badge-class="item.badgeClass"
:starred="item.starred"
/>
</div>
</div>
</div>

View File

@@ -0,0 +1,326 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import Button from 'primevue/button'
import { SidebarSearchBox, SidebarViewToggle, LibraryGridCard } from '@/components/common/sidebar'
import { TEMPLATE_CATEGORIES_DATA } from '@/data/sidebarMockData'
interface TemplateItem {
id: string
name: string
description: string
category: string
categoryIcon: string
nodes: number
thumbnail?: string
badge?: string
badgeClass?: string
}
const emit = defineEmits<{
close: []
}>()
const searchQuery = ref('')
const viewMode = ref<'list' | 'grid'>('grid')
const sortBy = ref('name')
const showFilterMenu = ref(false)
const showSortMenu = ref(false)
const activeFilters = ref<Set<string>>(new Set())
const sortOptions = [
{ label: 'Name', value: 'name' },
{ label: 'Node Count', value: 'nodes' },
]
const filterOptions = [
{ label: 'Official', value: 'official', icon: 'pi pi-verified', color: 'text-blue-400' },
{ label: 'SDXL', value: 'sdxl', icon: 'pi pi-star', color: 'text-purple-400' },
{ label: 'ControlNet', value: 'controlnet', icon: 'pi pi-sliders-v', color: 'text-amber-400' },
{ label: 'Video', value: 'video', icon: 'pi pi-video', color: 'text-green-400' },
{ label: 'Community', value: 'community', icon: 'pi pi-users', color: 'text-cyan-400' },
]
function setSort(value: string): void {
sortBy.value = value
showSortMenu.value = false
}
function toggleFilter(value: string): void {
const newFilters = new Set(activeFilters.value)
if (newFilters.has(value)) {
newFilters.delete(value)
} else {
newFilters.add(value)
}
activeFilters.value = newFilters
}
function clearFilters(): void {
activeFilters.value = new Set()
}
const filterLabel = computed(() => {
if (activeFilters.value.size === 0) return 'All'
if (activeFilters.value.size === 1) {
const value = [...activeFilters.value][0]
return filterOptions.find(o => o.value === value)?.label || 'All'
}
return `${activeFilters.value.size} selected`
})
// Combine all templates into a flat list
const allTemplates = computed<TemplateItem[]>(() => {
const items: TemplateItem[] = []
const categoryColors: Record<string, { badge: string; badgeClass: string }> = {
official: { badge: 'Official', badgeClass: 'bg-blue-500/30 text-blue-300' },
sdxl: { badge: 'SDXL', badgeClass: 'bg-purple-500/30 text-purple-300' },
controlnet: { badge: 'ControlNet', badgeClass: 'bg-amber-500/30 text-amber-300' },
video: { badge: 'Video', badgeClass: 'bg-green-500/30 text-green-300' },
community: { badge: 'Community', badgeClass: 'bg-cyan-500/30 text-cyan-300' },
}
TEMPLATE_CATEGORIES_DATA.forEach(category => {
category.templates.forEach(template => {
const colors = categoryColors[category.id] || { badge: category.label, badgeClass: 'bg-zinc-700 text-zinc-400' }
items.push({
id: `${category.id}-${template.name}`,
name: template.display,
description: template.description,
category: category.id,
categoryIcon: category.icon,
nodes: template.nodes,
badge: colors.badge,
badgeClass: colors.badgeClass,
})
})
})
return items
})
// Filter and search
const filteredTemplates = computed(() => {
let items = allTemplates.value
// Apply category filters (multi-select)
if (activeFilters.value.size > 0) {
items = items.filter(i => activeFilters.value.has(i.category))
}
// Apply search
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
items = items.filter(i =>
i.name.toLowerCase().includes(query) ||
i.description.toLowerCase().includes(query)
)
}
// Apply sort
if (sortBy.value === 'name') {
items = [...items].sort((a, b) => a.name.localeCompare(b.name))
} else if (sortBy.value === 'nodes') {
items = [...items].sort((a, b) => b.nodes - a.nodes)
}
return items
})
</script>
<template>
<div class="flex h-full w-80 flex-col">
<!-- Panel Header -->
<div class="flex items-center justify-between border-b border-zinc-800 px-3 py-2">
<span class="text-xs font-semibold uppercase tracking-wide text-zinc-400">
TEMPLATES
</span>
<div class="flex items-center gap-1">
<Button
icon="pi pi-window-maximize"
text
severity="secondary"
size="small"
class="!h-6 !w-6"
v-tooltip.top="'Expand'"
/>
<Button
icon="pi pi-times"
text
severity="secondary"
size="small"
class="!h-6 !w-6"
@click="emit('close')"
/>
</div>
</div>
<!-- Search & Controls -->
<div class="border-b border-zinc-800 p-2">
<SidebarSearchBox
v-model="searchQuery"
placeholder="Search templates..."
:show-action="true"
action-tooltip="Browse Templates"
action-icon="pi pi-external-link"
/>
<!-- View Controls -->
<div class="mt-2 flex items-center justify-between">
<SidebarViewToggle v-model="viewMode" />
<!-- Filter & Sort -->
<div class="flex items-center gap-1">
<!-- Filter Dropdown -->
<div class="relative">
<button
:class="[
'flex h-6 items-center gap-1 rounded px-2 text-[10px] transition-colors',
activeFilters.size > 0
? 'bg-blue-600/20 text-blue-400 hover:bg-blue-600/30'
: 'bg-zinc-800 text-zinc-400 hover:bg-zinc-700 hover:text-zinc-200'
]"
@click="showFilterMenu = !showFilterMenu"
>
<i class="pi pi-filter text-[10px]" />
<span>{{ filterLabel }}</span>
<i class="pi pi-chevron-down text-[8px]" />
</button>
<div
v-if="showFilterMenu"
class="absolute left-0 top-full z-50 mt-1 w-36 rounded-lg border border-zinc-700 bg-black py-1 shadow-xl"
>
<!-- Clear all -->
<button
v-if="activeFilters.size > 0"
class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs text-zinc-500 transition-colors hover:bg-zinc-800 hover:text-zinc-300"
@click="clearFilters"
>
<i class="pi pi-times text-[10px]" />
Clear all
</button>
<div v-if="activeFilters.size > 0" class="mx-2 my-1 h-px bg-zinc-800" />
<!-- Filter options -->
<button
v-for="option in filterOptions"
:key="option.value"
class="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs transition-colors hover:bg-zinc-800"
@click="toggleFilter(option.value)"
>
<div
:class="[
'flex h-3.5 w-3.5 items-center justify-center rounded border transition-colors',
activeFilters.has(option.value)
? 'border-blue-500 bg-blue-500'
: 'border-zinc-600 bg-transparent'
]"
>
<i v-if="activeFilters.has(option.value)" class="pi pi-check text-[8px] text-white" />
</div>
<i :class="[option.icon, 'text-[10px]', option.color]" />
<span :class="activeFilters.has(option.value) ? 'text-zinc-200' : 'text-zinc-400'">
{{ option.label }}
</span>
</button>
</div>
</div>
<!-- Sort Dropdown -->
<div class="relative">
<button
class="flex h-6 items-center gap-1 rounded bg-zinc-800 px-2 text-[10px] text-zinc-400 transition-colors hover:bg-zinc-700 hover:text-zinc-200"
@click="showSortMenu = !showSortMenu"
>
<i class="pi pi-sort-alt text-[10px]" />
<span>{{ sortOptions.find(o => o.value === sortBy)?.label }}</span>
<i class="pi pi-chevron-down text-[8px]" />
</button>
<div
v-if="showSortMenu"
class="absolute right-0 top-full z-50 mt-1 min-w-[100px] rounded-lg border border-zinc-700 bg-black py-1 shadow-xl"
>
<button
v-for="option in sortOptions"
:key="option.value"
class="flex w-full items-center px-3 py-1.5 text-left text-xs transition-colors"
:class="sortBy === option.value ? 'bg-zinc-800 text-zinc-200' : 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-200'"
@click="setSort(option.value)"
>
{{ option.label }}
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Content -->
<div class="flex-1 overflow-y-auto p-2">
<!-- Empty State -->
<div
v-if="filteredTemplates.length === 0"
class="flex flex-col items-center justify-center py-8 text-center"
>
<i class="pi pi-copy mb-2 text-2xl text-zinc-600" />
<p class="text-xs text-zinc-500">No templates found</p>
</div>
<!-- List View -->
<div v-else-if="viewMode === 'list'" class="select-none space-y-0.5">
<div
v-for="template in filteredTemplates"
:key="template.id"
class="group flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 transition-colors hover:bg-zinc-800"
draggable="true"
>
<i :class="[template.categoryIcon, 'text-xs text-zinc-500']" />
<div class="min-w-0 flex-1">
<div class="flex items-center gap-1.5">
<span class="truncate text-xs text-zinc-300 group-hover:text-zinc-100">{{ template.name }}</span>
</div>
<div class="flex items-center gap-2 text-[10px] text-zinc-600">
<span v-if="template.badge" :class="['rounded px-1 py-0.5 text-[9px]', template.badgeClass]">
{{ template.badge }}
</span>
<span>{{ template.nodes }} nodes</span>
</div>
</div>
<i class="pi pi-plus text-[10px] text-zinc-600 opacity-0 transition-opacity group-hover:opacity-100" />
</div>
</div>
<!-- Grid View -->
<div v-else class="grid grid-cols-1 gap-2">
<LibraryGridCard
v-for="template in filteredTemplates"
:key="template.id"
:title="template.name"
:subtitle="`${template.nodes} nodes · ${template.description}`"
:icon="template.categoryIcon"
icon-class="text-zinc-400"
:badge="template.badge"
:badge-class="template.badgeClass"
/>
</div>
</div>
</div>
</template>
<style scoped>
div::-webkit-scrollbar {
width: 4px;
}
div::-webkit-scrollbar-track {
background: transparent;
}
div::-webkit-scrollbar-thumb {
background: #3f3f46;
border-radius: 2px;
}
div::-webkit-scrollbar-thumb:hover {
background: #52525b;
}
</style>

View File

@@ -1,9 +1,23 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import Tooltip from 'primevue/tooltip'
import Popover from 'primevue/popover'
import { useUiStore } from '@/stores/uiStore'
const vTooltip = Tooltip
const uiStore = useUiStore()
const accountMenu = ref<InstanceType<typeof Popover> | null>(null)
const logoMenu = ref<InstanceType<typeof Popover> | null>(null)
function toggleAccountMenu(event: Event): void {
accountMenu.value?.toggle(event)
}
function toggleLogoMenu(event: Event): void {
logoMenu.value?.toggle(event)
}
interface MenuItem {
label: string
@@ -38,17 +52,33 @@ const userMenuGroups = computed<MenuGroup[]>(() => [
items: [
{ label: 'Dashboard', icon: 'pi pi-home', route: `/${props.workspaceId}` },
{ label: 'Recents', icon: 'pi pi-clock', route: `/${props.workspaceId}/recents` },
{ label: 'Projects', icon: 'pi pi-folder', route: `/${props.workspaceId}/projects` },
{ label: 'Canvases', icon: 'pi pi-objects-column', route: `/${props.workspaceId}/canvases` }
{ label: 'Library Hub', icon: 'pi pi-database', route: `/${props.workspaceId}/library` }
]
},
{
label: 'Library',
label: 'My Library',
items: [
{ label: 'Projects', icon: 'pi pi-folder', route: `/${props.workspaceId}/projects` },
{ label: 'Canvases', icon: 'pi pi-objects-column', route: `/${props.workspaceId}/canvases` },
{ label: 'Templates', icon: 'pi pi-th-large', route: `/${props.workspaceId}/templates` },
{ label: 'Workflows', icon: 'pi pi-sitemap', route: `/${props.workspaceId}/workflows` },
{ label: 'Assets', icon: 'pi pi-images', route: `/${props.workspaceId}/assets` },
{ label: 'Models', icon: 'pi pi-box', route: `/${props.workspaceId}/models` },
{ label: 'Trash', icon: 'pi pi-trash', route: `/${props.workspaceId}/trash` }
{ label: 'Models', icon: 'pi pi-box', route: `/${props.workspaceId}/models` }
]
},
{
label: 'Shared Projects',
items: [
{ label: 'Image Generation', icon: 'pi pi-folder', route: `/${props.workspaceId}/img-gen` },
{ label: 'Video Processing', icon: 'pi pi-folder', route: `/${props.workspaceId}/video-proc` },
{ label: 'Audio Enhancement', icon: 'pi pi-folder', route: `/${props.workspaceId}/audio-enh` }
]
},
{
label: 'Starred',
items: [
{ label: 'Main Workflow', icon: 'pi pi-star-fill', route: `/${props.workspaceId}/img-gen/main-workflow` },
{ label: 'Upscale 4x', icon: 'pi pi-star-fill', route: `/${props.workspaceId}/upscale/upscale-4x` }
]
}
])
@@ -65,28 +95,43 @@ const teamMenuGroups = computed<MenuGroup[]>(() => [
items: [
{ label: 'Dashboard', icon: 'pi pi-home', route: `/${props.workspaceId}` },
{ label: 'Recents', icon: 'pi pi-clock', route: `/${props.workspaceId}/recents` },
{ label: 'Projects', icon: 'pi pi-folder', route: `/${props.workspaceId}/projects` },
{ label: 'Canvases', icon: 'pi pi-objects-column', route: `/${props.workspaceId}/canvases` }
{ label: 'Library Hub', icon: 'pi pi-database', route: `/${props.workspaceId}/library` }
]
},
{
label: 'Library',
label: 'My Library',
items: [
{ label: 'Projects', icon: 'pi pi-folder', route: `/${props.workspaceId}/projects` },
{ label: 'Canvases', icon: 'pi pi-objects-column', route: `/${props.workspaceId}/canvases` },
{ label: 'Templates', icon: 'pi pi-th-large', route: `/${props.workspaceId}/templates` },
{ label: 'Workflows', icon: 'pi pi-sitemap', route: `/${props.workspaceId}/workflows` },
{ label: 'Assets', icon: 'pi pi-images', route: `/${props.workspaceId}/assets` },
{ label: 'Models', icon: 'pi pi-box', route: `/${props.workspaceId}/models` },
{ label: 'Trash', icon: 'pi pi-trash', route: `/${props.workspaceId}/trash` }
{ label: 'Models', icon: 'pi pi-box', route: `/${props.workspaceId}/models` }
]
},
{
label: 'Team',
label: 'Shared Projects',
items: [
{ label: 'Members', icon: 'pi pi-users', route: `/${props.workspaceId}/members`, badge: 8 },
{ label: 'Activity', icon: 'pi pi-history', route: `/${props.workspaceId}/activity` }
{ label: 'Image Generation', icon: 'pi pi-folder', route: `/${props.workspaceId}/img-gen` },
{ label: 'Video Processing', icon: 'pi pi-folder', route: `/${props.workspaceId}/video-proc` },
{ label: 'Audio Enhancement', icon: 'pi pi-folder', route: `/${props.workspaceId}/audio-enh` }
]
},
{
label: 'Starred',
items: [
{ label: 'Main Workflow', icon: 'pi pi-star-fill', route: `/${props.workspaceId}/img-gen/main-workflow` },
{ label: 'Upscale 4x', icon: 'pi pi-star-fill', route: `/${props.workspaceId}/upscale/upscale-4x` }
]
}
])
const trashItem = computed<MenuItem>(() => ({
label: 'Trash',
icon: 'pi pi-trash',
route: `/${props.workspaceId}/trash`
}))
const menuGroups = computed(() => (isTeam.value ? teamMenuGroups.value : userMenuGroups.value))
function isActive(itemRoute?: string): boolean {
@@ -104,26 +149,124 @@ function signOut(): void {
<template>
<aside
class="flex h-full w-60 flex-col border-r border-zinc-200 bg-zinc-50/50 dark:border-zinc-800 dark:bg-zinc-950"
class="flex h-full w-[300px] flex-col border-r border-zinc-200 bg-zinc-50/50 dark:border-zinc-800 dark:bg-zinc-950"
>
<!-- Header -->
<div class="flex h-14 items-center gap-3 border-b border-zinc-200 px-4 dark:border-zinc-800">
<div
<!-- Logo -->
<div class="flex h-14 items-center justify-between px-3">
<button
class="flex items-center gap-1.5 rounded-md px-2 py-1.5 transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800"
@click="toggleLogoMenu"
>
<img src="/comfy-logo-yellow.svg" alt="ComfyUI" class="h-6" />
<i class="pi pi-chevron-down text-[10px] text-zinc-400" />
</button>
<!-- Settings -->
<RouterLink
v-tooltip.bottom="'Settings'"
:to="`/${workspaceId}/settings`"
:class="[
'flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md text-sm font-semibold',
isTeam ? 'bg-blue-600 text-white' : 'bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900'
'flex h-8 w-8 items-center justify-center rounded-md transition-colors',
isActive(`/${workspaceId}/settings`)
? 'bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900'
: 'text-zinc-500 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200'
]"
>
{{ workspaceId.charAt(0).toUpperCase() }}
</div>
<div class="flex-1 overflow-hidden">
<p class="truncate text-sm font-medium text-zinc-900 dark:text-zinc-100">
{{ workspaceId }}
</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">
{{ isTeam ? 'Team' : 'Personal' }}
</p>
</div>
<i class="pi pi-cog text-base" />
</RouterLink>
<!-- Logo Menu Popover -->
<Popover ref="logoMenu" append-to="self">
<div class="flex w-60 flex-col p-1">
<!-- File Section -->
<div class="px-3 pb-1 pt-1.5 text-[11px] font-medium uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
File
</div>
<button class="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-[13px] text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-800">
<i class="pi pi-file w-4 text-sm text-zinc-400" />
<span class="flex-1">New Workflow</span>
<span class="text-[11px] text-zinc-400">Ctrl+N</span>
</button>
<button class="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-[13px] text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-800">
<i class="pi pi-folder-open w-4 text-sm text-zinc-400" />
<span class="flex-1">Open...</span>
<span class="text-[11px] text-zinc-400">Ctrl+O</span>
</button>
<button class="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-[13px] text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-800">
<i class="pi pi-save w-4 text-sm text-zinc-400" />
<span class="flex-1">Save</span>
<span class="text-[11px] text-zinc-400">Ctrl+S</span>
</button>
<button class="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-[13px] text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-800">
<i class="pi pi-download w-4 text-sm text-zinc-400" />
<span>Export...</span>
</button>
<div class="mx-2 my-1 h-px bg-zinc-200 dark:bg-zinc-700" />
<!-- Workspace Section -->
<div class="px-3 pb-1 pt-1.5 text-[11px] font-medium uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
Workspace
</div>
<RouterLink
:to="`/${workspaceId}`"
class="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-[13px] text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-800"
@click="logoMenu?.hide()"
>
<i class="pi pi-home w-4 text-sm text-zinc-400" />
<span>Dashboard</span>
</RouterLink>
<RouterLink
:to="`/${workspaceId}/projects`"
class="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-[13px] text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-800"
@click="logoMenu?.hide()"
>
<i class="pi pi-folder w-4 text-sm text-zinc-400" />
<span>Projects</span>
</RouterLink>
<div class="mx-2 my-1 h-px bg-zinc-200 dark:bg-zinc-700" />
<!-- Account Section -->
<div class="px-3 pb-1 pt-1.5 text-[11px] font-medium uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
Account
</div>
<RouterLink
:to="`/${workspaceId}/settings`"
class="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-[13px] text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-800"
@click="logoMenu?.hide()"
>
<i class="pi pi-cog w-4 text-sm text-zinc-400" />
<span>Settings</span>
</RouterLink>
<button
class="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-[13px] text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-800"
@click="uiStore.toggleInterfaceVersion()"
>
<i class="pi pi-sparkles w-4 text-sm text-zinc-400" />
<span class="flex-1">Experimental UI</span>
<div
class="h-5 w-9 rounded-full p-0.5 transition-colors"
:class="uiStore.interfaceVersion === 'v2' ? 'bg-blue-500' : 'bg-zinc-300 dark:bg-zinc-600'"
>
<div
class="h-4 w-4 rounded-full bg-white transition-transform"
:class="uiStore.interfaceVersion === 'v2' ? 'translate-x-4' : 'translate-x-0'"
/>
</div>
</button>
<div class="mx-2 my-1 h-px bg-zinc-200 dark:bg-zinc-700" />
<button
class="flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-[13px] text-red-500 transition-colors hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-500/10"
@click="signOut(); logoMenu?.hide()"
>
<i class="pi pi-sign-out w-4 text-sm" />
<span>Sign out</span>
</button>
</div>
</Popover>
</div>
<!-- Menu Groups -->
@@ -165,27 +308,139 @@ function signOut(): void {
</template>
</nav>
<!-- Footer -->
<div class="flex items-center justify-end gap-1 border-t border-zinc-200 px-3 py-2 dark:border-zinc-800">
<!-- Trash -->
<div class="px-3 pb-2">
<RouterLink
v-tooltip.top="'Settings'"
:to="`/${workspaceId}/settings`"
:to="trashItem.route ?? '#'"
:class="[
'flex h-8 w-8 items-center justify-center rounded-md transition-colors',
isActive(`/${workspaceId}/settings`)
? 'bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900'
: 'text-zinc-500 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200'
'flex items-center gap-3 rounded-md px-2 py-1.5 text-sm transition-colors',
isActive(trashItem.route)
? 'bg-zinc-900 font-medium text-white dark:bg-zinc-100 dark:text-zinc-900'
: 'text-zinc-600 hover:bg-zinc-100 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-100'
]"
>
<i class="pi pi-cog text-base" />
<i :class="[trashItem.icon, 'text-base']" />
<span>{{ trashItem.label }}</span>
</RouterLink>
</div>
<!-- Footer -->
<div class="border-t border-zinc-200 px-3 py-3 dark:border-zinc-800">
<!-- Account Dropdown -->
<button
v-tooltip.top="'Sign out'"
class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-500 transition-colors hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
@click="signOut"
class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 transition-colors hover:bg-zinc-100 dark:hover:bg-zinc-800"
@click="toggleAccountMenu"
>
<i class="pi pi-sign-out text-base" />
<div
:class="[
'flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full text-xs font-semibold',
isTeam ? 'bg-blue-600 text-white' : 'bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900'
]"
>
J
</div>
<div class="flex-1 overflow-hidden text-left">
<p class="truncate text-sm font-medium text-zinc-900 dark:text-zinc-100">
John Doe
</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">
{{ isTeam ? 'Netflix' : 'Personal Workspace' }}
</p>
</div>
<i class="pi pi-chevron-down text-xs text-zinc-400" />
</button>
<!-- Account Popover -->
<Popover ref="accountMenu" append-to="self">
<div class="flex w-64 flex-col p-2">
<!-- Personal -->
<p class="px-2 py-1 text-[11px] font-medium uppercase tracking-wide text-zinc-400 dark:text-zinc-500">
Personal
</p>
<RouterLink
to="/user"
class="flex items-center gap-2.5 rounded-md px-2 py-2 text-sm text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
@click="accountMenu?.hide()"
>
<div class="flex h-7 w-7 items-center justify-center rounded-full bg-zinc-900 text-xs font-semibold text-white dark:bg-zinc-100 dark:text-zinc-900">
U
</div>
<div class="flex-1">
<p class="font-medium">My Workspace</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">Owner</p>
</div>
</RouterLink>
<div class="my-2 h-px bg-zinc-200 dark:bg-zinc-700" />
<!-- Team Workspaces -->
<p class="px-2 py-1 text-[11px] font-medium uppercase tracking-wide text-zinc-400 dark:text-zinc-500">
Acme Studio
</p>
<RouterLink
to="/team"
class="flex items-center gap-2.5 rounded-md px-2 py-2 text-sm text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
@click="accountMenu?.hide()"
>
<div class="flex h-7 w-7 items-center justify-center rounded-full bg-violet-600 text-xs font-semibold text-white">
<i class="pi pi-sitemap text-xs" />
</div>
<div class="flex-1">
<p class="font-medium">Workflow Builder</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">Full access</p>
</div>
</RouterLink>
<RouterLink
to="/team-artist"
class="flex items-center gap-2.5 rounded-md px-2 py-2 text-sm text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
@click="accountMenu?.hide()"
>
<div class="flex h-7 w-7 items-center justify-center rounded-full bg-pink-600 text-xs font-semibold text-white">
<i class="pi pi-palette text-xs" />
</div>
<div class="flex-1">
<p class="font-medium">Visual Artist</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">Edit assets & canvases</p>
</div>
</RouterLink>
<RouterLink
to="/team-motion"
class="flex items-center gap-2.5 rounded-md px-2 py-2 text-sm text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
@click="accountMenu?.hide()"
>
<div class="flex h-7 w-7 items-center justify-center rounded-full bg-orange-600 text-xs font-semibold text-white">
<i class="pi pi-video text-xs" />
</div>
<div class="flex-1">
<p class="font-medium">Motion Designer</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">Edit video workflows</p>
</div>
</RouterLink>
<RouterLink
to="/team-pm"
class="flex items-center gap-2.5 rounded-md px-2 py-2 text-sm text-zinc-700 transition-colors hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
@click="accountMenu?.hide()"
>
<div class="flex h-7 w-7 items-center justify-center rounded-full bg-emerald-600 text-xs font-semibold text-white">
<i class="pi pi-chart-bar text-xs" />
</div>
<div class="flex-1">
<p class="font-medium">Project Manager</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">View only</p>
</div>
</RouterLink>
<div class="my-2 h-px bg-zinc-200 dark:bg-zinc-700" />
<button
class="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-red-500 transition-colors hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-500/10"
@click="signOut(); accountMenu?.hide()"
>
<i class="pi pi-sign-out text-sm" />
<span>Sign out</span>
</button>
</div>
</Popover>
</div>
</aside>
</template>

View File

@@ -56,10 +56,8 @@ const outlineClass = computed(() => {
return 'outline outline-2 outline-blue-500/50'
})
const headerStyle = computed(() => {
const color = props.data.headerColor || props.data.definition.headerColor
return color ? { backgroundColor: color } : {}
})
// Header color is no longer used in the new compact design
const headerStyle = computed(() => ({}))
const bodyStyle = computed(() => {
const color = props.data.bodyColor || props.data.definition.bodyColor
@@ -95,9 +93,9 @@ const hasInputs = computed(() => props.data.definition.inputs.length > 0)
const hasOutputs = computed(() => props.data.definition.outputs.length > 0)
// Handle positioning constants
const HEADER_HEIGHT = 36
const SLOT_HEIGHT = 24
const PROGRESS_BAR_HEIGHT = 4
const HEADER_HEIGHT = 28
const SLOT_HEIGHT = 22
const PROGRESS_BAR_HEIGHT = 3
const handleTopOffset = computed(() => {
const hasProgressBar = isExecuting.value && props.data.progress !== undefined
@@ -131,13 +129,12 @@ function getHandleTop(index: number): string {
<div
v-else
:class="[
'flow-node relative min-w-[225px] rounded-lg',
'flow-node relative min-w-[280px] rounded-lg',
'border transition-all duration-150',
'bg-zinc-900',
borderClass,
outlineClass,
{
'ring-4 ring-blue-500/30': selected && !hasError && !isExecuting,
'ring-2 ring-blue-500/30': selected && !hasError && !isExecuting,
}
]"
:style="[bodyStyle, { opacity: nodeOpacity }]"
@@ -154,14 +151,13 @@ function getHandleTop(index: number): string {
:pinned="data.flags.pinned"
:badges="data.badges"
:state="data.state"
:style="headerStyle"
@collapse="handleCollapse"
@update:title="handleTitleUpdate"
/>
<div
v-if="isExecuting && data.progress !== undefined"
class="relative h-1 mx-4"
class="relative h-[3px] mx-2"
:class="isCollapsed ? 'absolute bottom-0 left-0 right-0 mx-0' : ''"
>
<div class="absolute inset-0 bg-blue-500/30 rounded-full" />
@@ -175,19 +171,19 @@ function getHandleTop(index: number): string {
<div class="flex items-center justify-between px-2 py-1">
<div
v-if="hasInputs"
class="h-3 w-3 rounded-full bg-zinc-600 border-2 border-zinc-800"
class="h-2.5 w-2.5 rounded-full bg-zinc-600 border-2 border-zinc-800"
/>
<div v-else class="w-3" />
<div v-else class="w-2.5" />
<div
v-if="hasOutputs"
class="h-3 w-3 rounded-full bg-zinc-600 border-2 border-zinc-800"
class="h-2.5 w-2.5 rounded-full bg-zinc-600 border-2 border-zinc-800"
/>
<div v-else class="w-3" />
<div v-else class="w-2.5" />
</div>
</template>
<template v-else>
<div class="flex flex-col gap-1 pb-2">
<div class="flex flex-col pb-2">
<NodeSlots
:inputs="data.definition.inputs"
:outputs="data.definition.outputs"
@@ -200,12 +196,15 @@ function getHandleTop(index: number): string {
@update:value="handleWidgetUpdate"
/>
<div v-if="data.previewUrl" class="px-4 pt-2">
<div v-if="data.previewUrl" class="px-2 pt-2">
<img
:src="data.previewUrl"
alt="Preview"
class="w-full rounded-lg object-cover max-h-40"
class="w-full rounded object-cover"
/>
<div v-if="data.previewSize" class="text-center text-[10px] text-zinc-500 mt-1">
{{ data.previewSize }}
</div>
</div>
</div>
</template>
@@ -255,13 +254,13 @@ function getHandleTop(index: number): string {
<style scoped>
.flow-node {
--node-body-bg: #18181b;
--node-body-bg: #1a1a1e;
background-color: var(--node-body-bg);
}
.vue-flow-handle {
width: 16px !important;
height: 16px !important;
width: 14px !important;
height: 14px !important;
background: transparent !important;
border: none !important;
opacity: 0 !important;

View File

@@ -57,11 +57,10 @@ const hasOutputs = computed(() => props.outputs.length > 0)
:class="[
'flow-node-minimized relative rounded-lg',
'border transition-all duration-150',
'bg-zinc-900',
borderClass,
outlineClass,
{
'ring-4 ring-blue-500/30': selected && !hasError && !isExecuting,
'ring-2 ring-blue-500/30': selected && !hasError && !isExecuting,
}
]"
:style="[bodyStyle, { opacity: nodeOpacity }]"
@@ -73,45 +72,39 @@ const hasOutputs = computed(() => props.outputs.length > 0)
/>
<!-- Compact header -->
<div
:class="[
'node-header-minimized py-1.5 px-2 text-xs',
'bg-zinc-800 text-zinc-100 rounded-t-lg',
]"
:style="headerStyle"
>
<div class="flex items-center justify-between gap-2 min-w-0">
<div class="flex items-center gap-1.5 min-w-0 flex-1">
<div class="node-header-minimized py-1 px-2 text-zinc-100 rounded-t-lg">
<div class="flex items-center justify-between gap-1.5 min-w-0">
<div class="flex items-center gap-1 min-w-0 flex-1">
<button
class="flex h-4 w-4 shrink-0 items-center justify-center rounded text-zinc-400 transition-colors hover:bg-zinc-700 hover:text-zinc-200"
class="flex h-4 w-4 shrink-0 items-center justify-center text-zinc-500 transition-colors hover:text-zinc-300"
@click.stop="emit('expand')"
>
<i class="pi pi-chevron-down -rotate-90 text-[10px]" />
</button>
<span class="truncate font-medium text-[11px]">{{ title }}</span>
<span class="truncate font-medium text-xs">{{ title }}</span>
</div>
<div class="flex shrink-0 items-center gap-1">
<div v-if="isExecuting || hasError" class="flex shrink-0 items-center gap-1">
<i
v-if="isExecuting"
class="pi pi-spin pi-spinner text-[10px] text-blue-400"
class="pi pi-spin pi-spinner text-[9px] text-blue-400"
/>
<i
v-if="hasError"
class="pi pi-exclamation-triangle text-[10px] text-red-400"
class="pi pi-exclamation-triangle text-[9px] text-red-400"
/>
</div>
</div>
</div>
<!-- Compact slots row -->
<div class="flex items-center justify-between px-1 py-1 rounded-b-lg">
<div class="flex items-center justify-between px-1 py-0.5 rounded-b-lg">
<!-- Input dots -->
<div class="flex items-center gap-0.5">
<div
v-for="(input, index) in visibleInputs"
:key="`input-${index}`"
class="h-2 w-2 rounded-full border border-zinc-900"
class="h-2 w-2 rounded-full"
:style="{ backgroundColor: getSlotColor(input.type) }"
:title="input.label || input.name"
/>
@@ -123,7 +116,7 @@ const hasOutputs = computed(() => props.outputs.length > 0)
<div
v-for="(output, index) in visibleOutputs"
:key="`output-${index}`"
class="h-2 w-2 rounded-full border border-zinc-900"
class="h-2 w-2 rounded-full"
:style="{ backgroundColor: getSlotColor(output.type) }"
:title="output.label || output.name"
/>
@@ -153,15 +146,15 @@ const hasOutputs = computed(() => props.outputs.length > 0)
<style scoped>
.flow-node-minimized {
--node-body-bg: #18181b;
--node-body-bg: #1a1a1e;
background-color: var(--node-body-bg);
min-width: 100px;
max-width: 160px;
min-width: 90px;
max-width: 150px;
}
.vue-flow-handle {
width: 16px !important;
height: 16px !important;
width: 14px !important;
height: 14px !important;
background: transparent !important;
border: none !important;
opacity: 0 !important;

View File

@@ -87,68 +87,66 @@ function getBadgeClasses(variant?: string): string {
<template>
<div
:class="[
'node-header py-2 pl-2 pr-3 text-sm',
'bg-zinc-800 text-zinc-100',
'node-header px-2 py-1.5',
'text-zinc-100',
collapsed ? 'rounded-lg' : 'rounded-t-lg',
]"
@dblclick="handleDoubleClick"
>
<div class="flex items-center justify-between gap-2 min-w-0">
<div class="flex items-center gap-2 min-w-0 flex-1">
<button
class="flex h-5 w-5 shrink-0 items-center justify-center rounded text-zinc-400 transition-colors hover:bg-zinc-700 hover:text-zinc-200"
@click="handleCollapseClick"
@dblclick.stop
>
<i
:class="[
'pi pi-chevron-down text-xs transition-transform duration-200',
collapsed && '-rotate-90',
]"
/>
</button>
<div class="flex items-center gap-1.5 min-w-0">
<button
class="flex h-4 w-4 shrink-0 items-center justify-center text-zinc-500 transition-colors hover:text-zinc-300"
@click="handleCollapseClick"
@dblclick.stop
>
<i
:class="[
'pi pi-chevron-down text-[10px] transition-transform duration-200',
collapsed && '-rotate-90',
]"
/>
</button>
<div class="flex min-w-0 flex-1 items-center">
<input
v-if="isEditing"
v-model="editValue"
type="text"
class="w-full min-w-0 truncate bg-transparent text-sm font-semibold text-zinc-100 outline-none ring-1 ring-blue-500 rounded px-1"
autofocus
@blur="handleTitleBlur"
@keydown="handleTitleKeydown"
/>
<span v-else class="truncate text-sm font-semibold">
{{ title }}
</span>
</div>
<div class="flex min-w-0 flex-1 items-center">
<input
v-if="isEditing"
v-model="editValue"
type="text"
class="w-full min-w-0 truncate bg-transparent text-xs font-medium text-zinc-100 outline-none ring-1 ring-blue-500 rounded px-1"
autofocus
@blur="handleTitleBlur"
@keydown="handleTitleKeydown"
/>
<span v-else class="truncate text-xs font-medium">
{{ title }}
</span>
</div>
<div class="flex shrink-0 items-center gap-1.5">
<div v-if="badges?.length || statusBadge || pinned" class="flex shrink-0 items-center gap-1">
<span
v-for="badge in badges"
:key="badge.text"
:class="[
'inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium',
'inline-flex items-center gap-0.5 rounded px-1 py-0.5 text-[9px] font-medium',
getBadgeClasses(badge.variant),
]"
>
<i v-if="badge.icon" :class="['pi', badge.icon, 'text-[9px]']" />
<i v-if="badge.icon" :class="['pi', badge.icon, 'text-[8px]']" />
{{ badge.text }}
</span>
<span
v-if="statusBadge"
:class="[
'inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium',
'inline-flex items-center gap-0.5 rounded px-1 py-0.5 text-[9px] font-medium',
getBadgeClasses(statusBadge.variant),
]"
>
<i v-if="statusBadge.icon" :class="['pi', statusBadge.icon, 'text-[9px]']" />
<i v-if="statusBadge.icon" :class="['pi', statusBadge.icon, 'text-[8px]']" />
{{ statusBadge.text }}
</span>
<i v-if="pinned" class="pi pi-thumbtack text-xs text-zinc-500" />
<i v-if="pinned" class="pi pi-thumbtack text-[10px] text-zinc-500" />
</div>
</div>
</div>

View File

@@ -22,13 +22,13 @@ const visibleOutputs = props.outputs.filter(s => !s.hidden)
<div
v-for="(input, index) in visibleInputs"
:key="`input-${index}`"
class="flex items-center gap-2 h-6 pr-4 group"
class="flex items-center gap-1.5 h-[22px] pr-3 group"
>
<SlotDot
:color="getSlotColor(input.type)"
side="left"
/>
<span class="text-xs text-zinc-400 truncate">
<span class="text-[11px] text-zinc-400 truncate">
{{ input.label || input.name }}
</span>
</div>
@@ -41,9 +41,9 @@ const visibleOutputs = props.outputs.filter(s => !s.hidden)
<div
v-for="(output, index) in visibleOutputs"
:key="`output-${index}`"
class="flex items-center gap-2 h-6 pl-4 group"
class="flex items-center gap-1.5 h-[22px] pl-3 group"
>
<span class="text-xs text-zinc-400 truncate">
<span class="text-[11px] text-zinc-400 truncate uppercase tracking-wide">
{{ output.label || output.name }}
</span>
<SlotDot

View File

@@ -28,65 +28,67 @@ function getWidgetValue(widget: WidgetDefinition): unknown {
</script>
<template>
<div class="node-widgets px-3 pt-1 pb-1 flex flex-col gap-2">
<div class="node-widgets px-2 flex flex-col gap-1">
<div
v-for="widget in widgets"
:key="widget.name"
class="widget-row"
class="widget-row flex items-center gap-3 min-h-[28px]"
>
<label class="widget-label text-[10px] text-zinc-500 mb-0.5 block">
<label class="widget-label text-[11px] text-zinc-400 shrink-0 min-w-[70px]">
{{ widget.label || widget.name }}
</label>
<WidgetSlider
v-if="widget.type === 'slider'"
:widget="widget as WidgetDefinition<number>"
:model-value="getWidgetValue(widget) as number"
@update:model-value="(v) => handleUpdate(widget.name, v)"
/>
<div class="flex-1 min-w-0">
<WidgetSlider
v-if="widget.type === 'slider'"
:widget="widget as WidgetDefinition<number>"
:model-value="getWidgetValue(widget) as number"
@update:model-value="(v) => handleUpdate(widget.name, v)"
/>
<WidgetNumber
v-else-if="widget.type === 'number'"
:widget="widget as WidgetDefinition<number>"
:model-value="getWidgetValue(widget) as number"
@update:model-value="(v) => handleUpdate(widget.name, v)"
/>
<WidgetNumber
v-else-if="widget.type === 'number'"
:widget="widget as WidgetDefinition<number>"
:model-value="getWidgetValue(widget) as number"
@update:model-value="(v) => handleUpdate(widget.name, v)"
/>
<WidgetText
v-else-if="widget.type === 'text'"
:widget="widget as WidgetDefinition<string>"
:model-value="getWidgetValue(widget) as string"
@update:model-value="(v) => handleUpdate(widget.name, v)"
/>
<WidgetText
v-else-if="widget.type === 'text'"
:widget="widget as WidgetDefinition<string>"
:model-value="getWidgetValue(widget) as string"
@update:model-value="(v) => handleUpdate(widget.name, v)"
/>
<WidgetText
v-else-if="widget.type === 'textarea'"
:widget="widget as WidgetDefinition<string>"
:model-value="getWidgetValue(widget) as string"
:multiline="true"
@update:model-value="(v) => handleUpdate(widget.name, v)"
/>
<WidgetText
v-else-if="widget.type === 'textarea'"
:widget="widget as WidgetDefinition<string>"
:model-value="getWidgetValue(widget) as string"
:multiline="true"
@update:model-value="(v) => handleUpdate(widget.name, v)"
/>
<WidgetSelect
v-else-if="widget.type === 'select'"
:widget="widget as WidgetDefinition<string | number>"
:model-value="getWidgetValue(widget) as string | number"
@update:model-value="(v) => handleUpdate(widget.name, v)"
/>
<WidgetSelect
v-else-if="widget.type === 'select'"
:widget="widget as WidgetDefinition<string | number>"
:model-value="getWidgetValue(widget) as string | number"
@update:model-value="(v) => handleUpdate(widget.name, v)"
/>
<WidgetToggle
v-else-if="widget.type === 'toggle'"
:widget="widget as WidgetDefinition<boolean>"
:model-value="getWidgetValue(widget) as boolean"
@update:model-value="(v) => handleUpdate(widget.name, v)"
/>
<WidgetToggle
v-else-if="widget.type === 'toggle'"
:widget="widget as WidgetDefinition<boolean>"
:model-value="getWidgetValue(widget) as boolean"
@update:model-value="(v) => handleUpdate(widget.name, v)"
/>
<WidgetColor
v-else-if="widget.type === 'color'"
:widget="widget as WidgetDefinition<string>"
:model-value="getWidgetValue(widget) as string"
@update:model-value="(v) => handleUpdate(widget.name, v)"
/>
<WidgetColor
v-else-if="widget.type === 'color'"
:widget="widget as WidgetDefinition<string>"
:model-value="getWidgetValue(widget) as string"
@update:model-value="(v) => handleUpdate(widget.name, v)"
/>
</div>
</div>
</div>
</template>

View File

@@ -14,32 +14,27 @@ withDefaults(defineProps<Props>(), {
<div
:class="[
'slot-dot relative flex items-center justify-center',
side === 'left' ? '-ml-1.5' : '-mr-1.5',
side === 'left' ? '-ml-1' : '-mr-1',
]"
>
<!-- Outer ring on hover -->
<div
class="absolute h-5 w-5 rounded-full opacity-0 transition-opacity duration-150 group-hover:opacity-100"
class="absolute h-4 w-4 rounded-full opacity-0 transition-opacity duration-150 group-hover:opacity-100"
:style="{ backgroundColor: `${color}30` }"
/>
<!-- Main dot -->
<div
:class="[
'relative h-3 w-3 rounded-full border-2 border-zinc-900 transition-all duration-150',
'relative h-2.5 w-2.5 rounded-full border-[1.5px] transition-all duration-150',
'cursor-crosshair',
]"
:style="{
backgroundColor: color,
boxShadow: connected ? `0 0 6px ${color}` : undefined,
borderColor: '#1a1a1e',
boxShadow: connected ? `0 0 4px ${color}` : undefined,
}"
>
<!-- Inner highlight -->
<div
class="absolute inset-0.5 rounded-full opacity-40"
:style="{ background: `linear-gradient(135deg, white 0%, transparent 50%)` }"
/>
</div>
/>
</div>
</template>

View File

@@ -49,16 +49,16 @@ function handleInput(event: Event): void {
.widget-color {
display: flex;
align-items: center;
gap: 8px;
padding: 2px 0;
gap: 6px;
height: 24px;
}
.color-preview {
width: 32px;
height: 20px;
border-radius: 4px;
width: 24px;
height: 18px;
border-radius: 3px;
overflow: hidden;
border: 1px solid #3f3f46;
border: none;
}
.color-input {
@@ -76,21 +76,22 @@ function handleInput(event: Event): void {
.color-input::-webkit-color-swatch {
border: none;
border-radius: 3px;
}
.color-input::-moz-color-swatch {
border: none;
border-radius: 3px;
}
.color-input:disabled {
opacity: 0.5;
opacity: 0.4;
cursor: not-allowed;
}
.color-value {
font-size: 11px;
font-size: 10px;
font-family: monospace;
color: #a1a1aa;
min-width: 60px;
color: #71717a;
}
</style>

View File

@@ -105,41 +105,40 @@ function decrement(): void {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 26px;
background: #3f3f46;
border: 1px solid #3f3f46;
color: #a1a1aa;
width: 26px;
height: 24px;
background: #2a2a2e;
border: none;
color: #71717a;
cursor: pointer;
transition: all 0.15s;
}
.number-btn:first-child {
border-radius: 6px 0 0 6px;
border-radius: 4px 0 0 4px;
}
.number-btn:last-child {
border-radius: 0 6px 6px 0;
border-radius: 0 4px 4px 0;
}
.number-btn:hover:not(:disabled) {
background: #52525b;
color: #fafafa;
background: #3f3f46;
color: #a1a1aa;
}
.number-btn:disabled {
opacity: 0.5;
opacity: 0.4;
cursor: not-allowed;
}
.number-input {
flex: 1;
background: #27272a;
border: 1px solid #3f3f46;
border-left: none;
border-right: none;
color: #fafafa;
padding: 4px 8px;
height: 24px;
background: #2a2a2e;
border: none;
color: #e4e4e7;
padding: 0 8px;
font-size: 11px;
text-align: center;
outline: none;
@@ -154,12 +153,11 @@ function decrement(): void {
}
.number-input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
background: #323238;
}
.number-input:disabled {
opacity: 0.5;
opacity: 0.4;
cursor: not-allowed;
}
</style>

View File

@@ -48,32 +48,36 @@ function handleChange(event: Event): void {
.custom-select {
width: 100%;
background: #27272a;
border: 1px solid #3f3f46;
border-radius: 6px;
color: #fafafa;
padding: 6px 28px 6px 10px;
height: 24px;
background: #2a2a2e;
border: none;
border-radius: 4px;
color: #e4e4e7;
padding: 0 24px 0 10px;
font-size: 11px;
outline: none;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23a1a1aa' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%2371717a' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px center;
}
.custom-select:hover {
background: #323238;
}
.custom-select:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
background: #323238;
}
.custom-select:disabled {
opacity: 0.5;
opacity: 0.4;
cursor: not-allowed;
}
.custom-select option {
background: #27272a;
color: #fafafa;
background: #2a2a2e;
color: #e4e4e7;
}
</style>

View File

@@ -94,13 +94,13 @@ function handleNumberBlur(event: Event): void {
.widget-slider {
display: flex;
align-items: center;
gap: 8px;
padding: 2px 0;
gap: 6px;
height: 24px;
}
.slider-container {
flex: 1;
height: 20px;
height: 24px;
display: flex;
align-items: center;
}
@@ -109,7 +109,7 @@ function handleNumberBlur(event: Event): void {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 4px;
height: 3px;
background: linear-gradient(
to right,
#3b82f6 0%,
@@ -117,30 +117,30 @@ function handleNumberBlur(event: Event): void {
#3f3f46 var(--fill-percent),
#3f3f46 100%
);
border-radius: 4px;
border-radius: 2px;
outline: none;
cursor: pointer;
}
.custom-slider:disabled {
opacity: 0.5;
opacity: 0.4;
cursor: not-allowed;
}
.custom-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
background: #fafafa;
border: 2px solid #3b82f6;
width: 12px;
height: 12px;
background: #e4e4e7;
border: none;
border-radius: 50%;
cursor: grab;
transition: background-color 0.15s, transform 0.15s;
}
.custom-slider::-webkit-slider-thumb:hover {
background: #3b82f6;
background: #fafafa;
transform: scale(1.1);
}
@@ -150,17 +150,17 @@ function handleNumberBlur(event: Event): void {
}
.custom-slider::-moz-range-thumb {
width: 14px;
height: 14px;
background: #fafafa;
border: 2px solid #3b82f6;
width: 12px;
height: 12px;
background: #e4e4e7;
border: none;
border-radius: 50%;
cursor: grab;
transition: background-color 0.15s, transform 0.15s;
}
.custom-slider::-moz-range-thumb:hover {
background: #3b82f6;
background: #fafafa;
transform: scale(1.1);
}
@@ -174,12 +174,13 @@ function handleNumberBlur(event: Event): void {
}
.number-input {
width: 56px;
background: #27272a;
border: 1px solid #3f3f46;
border-radius: 6px;
color: #fafafa;
padding: 4px 6px;
width: 50px;
height: 24px;
background: #2a2a2e;
border: none;
border-radius: 4px;
color: #e4e4e7;
padding: 0 6px;
font-size: 11px;
text-align: center;
outline: none;
@@ -193,12 +194,11 @@ function handleNumberBlur(event: Event): void {
}
.number-input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
background: #323238;
}
.number-input:disabled {
opacity: 0.5;
opacity: 0.4;
cursor: not-allowed;
}
</style>

View File

@@ -53,33 +53,48 @@ function handleInput(event: Event): void {
width: 100%;
}
.custom-input,
.custom-input {
width: 100%;
height: 24px;
background: #2a2a2e;
border: none;
border-radius: 4px;
color: #e4e4e7;
padding: 0 10px;
font-size: 11px;
outline: none;
}
.custom-textarea {
width: 100%;
background: #27272a;
border: 1px solid #3f3f46;
border-radius: 6px;
color: #fafafa;
background: #2a2a2e;
border: none;
border-radius: 4px;
color: #e4e4e7;
padding: 6px 10px;
font-size: 11px;
outline: none;
resize: none;
}
.custom-input:hover,
.custom-textarea:hover {
background: #323238;
}
.custom-input:focus,
.custom-textarea:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
background: #323238;
}
.custom-input:disabled,
.custom-textarea:disabled {
opacity: 0.5;
opacity: 0.4;
cursor: not-allowed;
}
.custom-input::placeholder,
.custom-textarea::placeholder {
color: #71717a;
color: #52525b;
}
</style>

View File

@@ -43,6 +43,7 @@ function toggle(): void {
.widget-toggle {
display: flex;
align-items: center;
height: 24px;
}
.toggle-button {
@@ -54,16 +55,16 @@ function toggle(): void {
}
.toggle-button:disabled {
opacity: 0.5;
opacity: 0.4;
cursor: not-allowed;
}
.toggle-track {
display: block;
width: 36px;
height: 20px;
width: 32px;
height: 16px;
background: #3f3f46;
border-radius: 10px;
border-radius: 8px;
position: relative;
transition: background-color 0.2s;
}
@@ -76,12 +77,11 @@ function toggle(): void {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background: #fafafa;
width: 12px;
height: 12px;
background: #e4e4e7;
border-radius: 50%;
transition: transform 0.2s;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.toggle-button.active .toggle-thumb {

View File

@@ -34,7 +34,7 @@ function handleNodeLeave(): void {
<template>
<!-- Level 1: Category Icon Bar -->
<nav class="flex w-12 flex-col items-center border-r border-zinc-800 bg-zinc-900 py-2">
<nav class="flex w-12 flex-col items-center border-r border-zinc-800 bg-black py-2">
<div class="flex flex-1 flex-col gap-0.5 overflow-y-auto scrollbar-hide">
<button
v-for="category in NODE_CATEGORIES"
@@ -60,17 +60,17 @@ function handleNodeLeave(): void {
<div class="mt-auto flex flex-col gap-1 pt-2">
<button
v-tooltip.right="{ value: 'Settings', showDelay: 50 }"
v-tooltip.right="{ value: 'Help', showDelay: 50 }"
class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-500 transition-colors hover:bg-zinc-800 hover:text-zinc-200"
>
<i class="pi pi-cog text-xs" />
<i class="pi pi-question-circle text-xs" />
</button>
</div>
</nav>
<!-- Level 2: Subcategory Panel -->
<aside
class="border-r border-zinc-800 bg-zinc-900/98 transition-all duration-200 ease-out"
class="border-r border-zinc-800 bg-black/98 transition-all duration-200 ease-out"
:class="nodePanelExpanded ? 'w-72' : 'w-0 overflow-hidden'"
>
<div v-if="nodePanelExpanded && activeNodeCategoryData" class="flex h-full w-72 flex-col">
@@ -156,7 +156,7 @@ function handleNodeLeave(): void {
<Transition name="fade">
<div
v-if="hoveredNode && nodePanelExpanded"
class="pointer-events-none fixed z-50 ml-2 w-64 rounded-lg border border-zinc-700 bg-zinc-900 p-3 shadow-xl"
class="pointer-events-none fixed z-50 ml-2 w-64 rounded-lg border border-zinc-700 bg-black p-3 shadow-xl"
:style="{ top: `${previewPosition.top}px`, left: 'calc(48px + 288px + 8px)' }"
>
<div class="mb-2 flex items-center gap-2">

View File

@@ -0,0 +1,99 @@
<script setup lang="ts">
interface Props {
thumbnail: string
title: string
description?: string
icon?: string
badge?: string
badgeClass?: string
stats?: Array<{ icon: string; value: string | number }>
updatedAt?: string
actionLabel?: string
actionIcon?: string
}
withDefaults(defineProps<Props>(), {
description: undefined,
icon: undefined,
badge: undefined,
badgeClass: 'bg-zinc-500/20 text-zinc-400',
stats: () => [],
updatedAt: undefined,
actionLabel: undefined,
actionIcon: undefined
})
const emit = defineEmits<{
click: []
menu: [event: MouseEvent]
}>()
</script>
<template>
<div
class="group cursor-pointer overflow-hidden rounded-lg border border-zinc-200 bg-white text-left transition-all hover:border-zinc-300 hover:shadow-sm dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700"
@click="emit('click')"
>
<div class="relative aspect-square overflow-hidden">
<img
:src="thumbnail"
:alt="title"
class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
/>
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
<!-- Type icon badge (bottom-left) -->
<div
v-if="icon"
class="absolute bottom-2 left-2 flex h-8 w-8 items-center justify-center rounded-md bg-black/30 backdrop-blur-sm"
>
<i :class="[icon, 'text-sm text-white/90']" />
</div>
<!-- Menu button (top-right) -->
<button
class="absolute right-2 top-2 rounded p-1 text-white/70 opacity-0 transition-opacity hover:bg-black/20 hover:text-white group-hover:opacity-100"
@click.stop="emit('menu', $event)"
>
<i class="pi pi-ellipsis-h text-sm" />
</button>
</div>
<div class="p-3">
<div class="flex items-start justify-between gap-2">
<div class="min-w-0 flex-1">
<h3 class="truncate font-medium text-zinc-900 dark:text-zinc-100">{{ title }}</h3>
<p v-if="description" class="mt-0.5 line-clamp-1 text-sm text-zinc-500 dark:text-zinc-400">
{{ description }}
</p>
</div>
<!-- Action button -->
<span
v-if="actionLabel"
class="inline-flex flex-shrink-0 items-center gap-1 rounded-md bg-blue-50 px-2 py-1 text-xs font-medium text-blue-600 transition-colors group-hover:bg-blue-600 group-hover:text-white dark:bg-blue-950 dark:text-blue-400 dark:group-hover:bg-blue-600 dark:group-hover:text-white"
>
<i v-if="actionIcon" :class="[actionIcon, 'text-[10px]']" />
{{ actionLabel }}
</span>
</div>
<div v-if="badge || stats.length > 0 || updatedAt" class="mt-2 flex items-center gap-2 text-xs text-zinc-400 dark:text-zinc-500">
<!-- Badge -->
<span v-if="badge" :class="['rounded px-1.5 py-0.5 text-[10px] font-medium', badgeClass]">
{{ badge }}
</span>
<!-- Stats -->
<template v-if="stats.length > 0">
<span v-for="(stat, idx) in stats" :key="idx" class="flex items-center gap-1">
<i v-if="stat.icon" :class="[stat.icon, 'text-[10px]']" />
{{ stat.value }}
</span>
</template>
<!-- Updated time -->
<span v-if="updatedAt" class="ml-auto">{{ updatedAt }}</span>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
interface FilterOption {
value: string
label: string
}
defineProps<{
modelValue: string
options: FilterOption[]
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
</script>
<template>
<div class="relative">
<select
:value="modelValue"
class="appearance-none rounded-md border border-zinc-200 bg-white py-2 pl-3 pr-8 text-sm text-zinc-700 outline-none transition-colors focus:border-zinc-400 focus:ring-1 focus:ring-zinc-400 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300 dark:focus:border-zinc-500 dark:focus:ring-zinc-500"
@change="emit('update:modelValue', ($event.target as HTMLSelectElement).value)"
>
<option v-for="option in options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
<i class="pi pi-filter pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-xs text-zinc-400" />
</div>
</template>

View File

@@ -3,4 +3,6 @@ export { default as WorkspaceEmptyState } from './WorkspaceEmptyState.vue'
export { default as WorkspaceViewToggle } from './WorkspaceViewToggle.vue'
export { default as WorkspaceSearchInput } from './WorkspaceSearchInput.vue'
export { default as WorkspaceSortSelect } from './WorkspaceSortSelect.vue'
export { default as WorkspaceFilterSelect } from './WorkspaceFilterSelect.vue'
export { default as WorkspaceCard } from './WorkspaceCard.vue'
export { default as CreateProjectDialog } from './CreateProjectDialog.vue'

View File

@@ -244,6 +244,7 @@ export interface SharedWorkflow {
nodes: number
category: string
starred: boolean
thumbnail?: string
}
export interface TeamModel {
@@ -254,6 +255,7 @@ export interface TeamModel {
size: string
author: TeamMember
downloads: number
thumbnail?: string
}
export interface NodePack {
@@ -264,6 +266,7 @@ export interface NodePack {
nodes: number
author: string
installed: boolean
thumbnail?: string
}
export const TEMPLATE_CATEGORIES_DATA: TemplateCategory[] = [
@@ -354,6 +357,7 @@ export function createSharedWorkflowsData(members: TeamMember[]): SharedWorkflow
nodes: 12,
category: 'Production',
starred: true,
thumbnail: '/assets/card_images/workflow_01.webp',
},
{
id: '2',
@@ -364,6 +368,7 @@ export function createSharedWorkflowsData(members: TeamMember[]): SharedWorkflow
nodes: 18,
category: 'Marketing',
starred: true,
thumbnail: '/assets/card_images/2690a78c-c210-4a52-8c37-3cb5bc4d9e71.webp',
},
{
id: '3',
@@ -374,6 +379,7 @@ export function createSharedWorkflowsData(members: TeamMember[]): SharedWorkflow
nodes: 24,
category: 'Production',
starred: false,
thumbnail: '/assets/card_images/bacb46ea-7e63-4f19-a253-daf41461e98f.webp',
},
{
id: '4',
@@ -384,6 +390,7 @@ export function createSharedWorkflowsData(members: TeamMember[]): SharedWorkflow
nodes: 8,
category: 'Marketing',
starred: false,
thumbnail: '/assets/card_images/228616f4-12ad-426d-84fb-f20e488ba7ee.webp',
},
]
}
@@ -398,6 +405,7 @@ export function createTeamModelsData(members: TeamMember[]): TeamModel[] {
size: '144 MB',
author: members[0]!,
downloads: 156,
thumbnail: '/assets/card_images/683255d3-1d10-43d9-a6ff-ef142061e88a.webp',
},
{
id: '2',
@@ -407,6 +415,7 @@ export function createTeamModelsData(members: TeamMember[]): TeamModel[] {
size: '6.94 GB',
author: members[1]!,
downloads: 89,
thumbnail: '/assets/card_images/91f1f589-ddb4-4c4f-b3a7-ba30fc271987.webp',
},
{
id: '3',
@@ -416,6 +425,7 @@ export function createTeamModelsData(members: TeamMember[]): TeamModel[] {
size: '72 MB',
author: members[2]!,
downloads: 234,
thumbnail: '/assets/card_images/28e9f7ea-ef00-48e8-849d-8752a34939c7.webp',
},
{
id: '4',
@@ -425,6 +435,7 @@ export function createTeamModelsData(members: TeamMember[]): TeamModel[] {
size: '24 KB',
author: members[0]!,
downloads: 312,
thumbnail: '/assets/card_images/comfyui_workflow.jpg',
},
]
}
@@ -438,6 +449,7 @@ export const NODE_PACKS_DATA: NodePack[] = [
nodes: 8,
author: 'Netflix Creative Tech',
installed: true,
thumbnail: '/assets/card_images/can-you-rate-my-comfyui-workflow-v0-o9clchhji39c1.webp',
},
{
id: '2',
@@ -447,6 +459,7 @@ export const NODE_PACKS_DATA: NodePack[] = [
nodes: 4,
author: 'Netflix Creative Tech',
installed: true,
thumbnail: '/assets/card_images/dda28581-37c8-44da-8822-57d1ccc2118c_2130x1658.png',
},
{
id: '3',
@@ -456,5 +469,6 @@ export const NODE_PACKS_DATA: NodePack[] = [
nodes: 6,
author: 'Netflix Creative Tech',
installed: false,
thumbnail: '/assets/card_images/workflow_01.webp',
},
]

View File

@@ -47,6 +47,11 @@ const ComfyPreset = definePreset(Aura, {
},
select: {
borderRadius: '8px'
},
popover: {
borderRadius: '8px',
shadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
padding: '0'
}
}
})
@@ -79,7 +84,24 @@ app.use(PrimeVue, {
app.use(ToastService)
app.use(ConfirmationService)
// PrimeVue directives
app.directive('tooltip', Tooltip)
// PrimeVue directives with custom defaults
app.directive('tooltip', {
...Tooltip,
getSSRProps() {
return {}
},
mounted(el, binding) {
// Set fast show delay (100ms) as default
const value = binding.value
if (typeof value === 'string') {
binding.value = { value, showDelay: 100, hideDelay: 0 }
} else if (typeof value === 'object' && value !== null) {
binding.value = { showDelay: 100, hideDelay: 0, ...value }
}
Tooltip.mounted(el, binding)
},
updated: Tooltip.updated,
unmounted: Tooltip.unmounted
})
app.mount('#app')

View File

@@ -54,6 +54,16 @@ const v2Routes: RouteRecordRaw[] = [
name: 'workspace-recents',
component: () => import('./views/v2/workspace/RecentsView.vue')
},
{
path: 'templates',
name: 'workspace-templates',
component: () => import('./views/v2/workspace/TemplatesView.vue')
},
{
path: 'library',
name: 'workspace-library',
component: () => import('./views/v2/workspace/LibraryView.vue')
},
{
path: 'trash',
name: 'workspace-trash',

View File

@@ -8,6 +8,8 @@ import '@vue-flow/core/dist/theme-default.css'
import CanvasTabBar from '@/components/v2/canvas/CanvasTabBar.vue'
import CanvasLeftSidebar from '@/components/v2/canvas/CanvasLeftSidebar.vue'
import CanvasBottomBar from '@/components/v2/canvas/CanvasBottomBar.vue'
import CanvasRightToolbar from '@/components/v2/canvas/CanvasRightToolbar.vue'
import CanvasRunControls from '@/components/v2/canvas/CanvasRunControls.vue'
import NodePropertiesPanel from '@/components/v2/canvas/NodePropertiesPanel.vue'
import { FlowNode } from '@/components/v2/nodes'
import { useWorkspaceStore } from '@/stores/workspaceStore'
@@ -36,7 +38,7 @@ onMounted(() => {
})
// Vue Flow
const { onNodeClick, onPaneClick, fitView } = useVueFlow()
const { onNodeClick, onPaneClick, fitView, zoomIn, zoomOut } = useVueFlow()
// Center the workflow on mount with 50% zoom
onMounted(() => {
@@ -109,6 +111,14 @@ function closePropertiesPanel(): void {
<!-- Interface 2.0: Floating bottom bar -->
<CanvasBottomBar v-if="isInterface2" />
<!-- Right toolbar: vertical for v2, horizontal for v1 -->
<CanvasRightToolbar
:orientation="isInterface2 ? 'vertical' : 'horizontal'"
@fit-view="fitView({ padding: 0.3 })"
@zoom-in="zoomIn()"
@zoom-out="zoomOut()"
/>
<!-- Workflow name -->
<div class="absolute left-4 top-4 z-10">
<span
@@ -117,6 +127,9 @@ function closePropertiesPanel(): void {
{{ props.canvasId }}
</span>
</div>
<!-- Run Controls (top-right) -->
<CanvasRunControls />
</main>
<!-- Right sidebar - Node Properties -->

View File

@@ -1,13 +1,14 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import { WorkspaceCard } from '@/components/v2/workspace'
const route = useRoute()
const workspaceId = computed(() => route.params.workspaceId as string)
// View mode
type ViewMode = 'grid' | 'list'
const viewMode = ref<ViewMode>('list')
const viewMode = ref<ViewMode>('grid')
// Filter type
type AssetType = 'all' | 'image' | 'video' | 'audio'
@@ -26,11 +27,11 @@ const sortOptions: { value: SortOption; label: string }[] = [
// Mock assets data
const assets = ref([
{ id: 'asset-1', name: 'input-image.png', type: 'image', size: '2.4 MB', sizeBytes: 2516582, dimensions: '1024x1024', updatedAt: '2 hours ago', updatedTimestamp: Date.now() - 2 * 60 * 60 * 1000 },
{ id: 'asset-2', name: 'reference.jpg', type: 'image', size: '1.8 MB', sizeBytes: 1887437, dimensions: '768x768', updatedAt: '1 day ago', updatedTimestamp: Date.now() - 24 * 60 * 60 * 1000 },
{ id: 'asset-3', name: 'mask.png', type: 'image', size: '0.5 MB', sizeBytes: 524288, dimensions: '512x512', updatedAt: '2 days ago', updatedTimestamp: Date.now() - 2 * 24 * 60 * 60 * 1000 },
{ id: 'asset-4', name: 'output-video.mp4', type: 'video', size: '24.5 MB', sizeBytes: 25690112, dimensions: '1920x1080', updatedAt: '3 days ago', updatedTimestamp: Date.now() - 3 * 24 * 60 * 60 * 1000 },
{ id: 'asset-5', name: 'background.wav', type: 'audio', size: '8.2 MB', sizeBytes: 8598323, dimensions: '3:24', updatedAt: '1 week ago', updatedTimestamp: Date.now() - 7 * 24 * 60 * 60 * 1000 }
{ id: 'asset-1', name: 'input-image.png', type: 'image', size: '2.4 MB', sizeBytes: 2516582, dimensions: '1024x1024', updatedAt: '2 hours ago', updatedTimestamp: Date.now() - 2 * 60 * 60 * 1000, thumbnail: '/thumbnails/asset-1.jpg' },
{ id: 'asset-2', name: 'reference.jpg', type: 'image', size: '1.8 MB', sizeBytes: 1887437, dimensions: '768x768', updatedAt: '1 day ago', updatedTimestamp: Date.now() - 24 * 60 * 60 * 1000, thumbnail: '/thumbnails/asset-2.jpg' },
{ id: 'asset-3', name: 'mask.png', type: 'image', size: '0.5 MB', sizeBytes: 524288, dimensions: '512x512', updatedAt: '2 days ago', updatedTimestamp: Date.now() - 2 * 24 * 60 * 60 * 1000, thumbnail: '/assets/card_images/workflow_01.webp' },
{ id: 'asset-4', name: 'output-video.mp4', type: 'video', size: '24.5 MB', sizeBytes: 25690112, dimensions: '1920x1080', updatedAt: '3 days ago', updatedTimestamp: Date.now() - 3 * 24 * 60 * 60 * 1000, thumbnail: '/assets/card_images/2690a78c-c210-4a52-8c37-3cb5bc4d9e71.webp' },
{ id: 'asset-5', name: 'background.wav', type: 'audio', size: '8.2 MB', sizeBytes: 8598323, dimensions: '3:24', updatedAt: '1 week ago', updatedTimestamp: Date.now() - 7 * 24 * 60 * 60 * 1000, thumbnail: '/assets/card_images/bacb46ea-7e63-4f19-a253-daf41461e98f.webp' }
])
// Search, filter and sort
@@ -201,30 +202,17 @@ function getAssetIcon(type: string): string {
v-else-if="viewMode === 'grid'"
class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5"
>
<div
<WorkspaceCard
v-for="asset in filteredAssets"
:key="asset.id"
class="group aspect-square cursor-pointer rounded-lg border border-zinc-200 bg-white p-4 text-left transition-all hover:border-zinc-300 hover:shadow-sm dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700"
>
<div class="flex h-full flex-col">
<div class="flex items-start justify-between">
<div class="flex h-10 w-10 items-center justify-center rounded-md bg-zinc-100 dark:bg-zinc-800">
<i :class="[getAssetIcon(asset.type), 'text-zinc-500 dark:text-zinc-400']" />
</div>
<button
class="rounded p-1 text-zinc-400 opacity-0 transition-opacity hover:bg-zinc-100 hover:text-zinc-600 group-hover:opacity-100 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
@click.stop
>
<i class="pi pi-ellipsis-h text-sm" />
</button>
</div>
<div class="mt-auto">
<h3 class="truncate font-medium text-zinc-900 dark:text-zinc-100">{{ asset.name }}</h3>
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">{{ asset.dimensions }}</p>
<p class="mt-1 text-xs text-zinc-400 dark:text-zinc-500">{{ asset.size }}</p>
</div>
</div>
</div>
:thumbnail="asset.thumbnail"
:title="asset.name"
:icon="getAssetIcon(asset.type)"
:stats="[
{ icon: '', value: asset.dimensions },
{ icon: '', value: asset.size }
]"
/>
</div>
<!-- List View -->

View File

@@ -105,6 +105,7 @@ const emptyStateDescription = computed(() =>
<div class="p-6">
<WorkspaceViewHeader
title="Canvases"
:subtitle="`${canvases.length} canvases`"
action-label="New Canvas"
@action="createCanvas"
/>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { WorkspaceCard } from '@/components/v2/workspace'
const route = useRoute()
const router = useRouter()
@@ -22,12 +23,12 @@ const recentActivity = [
]
const starterTemplates = [
{ id: 'txt2img', name: 'Text to Image', description: 'Generate images from text prompts', icon: 'pi pi-image', gradient: 'from-violet-500 to-purple-600' },
{ id: 'img2img', name: 'Image to Image', description: 'Transform existing images', icon: 'pi pi-images', gradient: 'from-blue-500 to-cyan-500' },
{ id: 'upscale', name: 'Upscale', description: '4x image upscaling workflow', icon: 'pi pi-expand', gradient: 'from-emerald-500 to-teal-600' },
{ id: 'inpaint', name: 'Inpainting', description: 'Edit parts of an image', icon: 'pi pi-pencil', gradient: 'from-orange-500 to-amber-500' },
{ id: 'controlnet', name: 'ControlNet', description: 'Guided image generation', icon: 'pi pi-sliders-h', gradient: 'from-pink-500 to-rose-600' },
{ id: 'video', name: 'Video Generation', description: 'Create videos from prompts', icon: 'pi pi-video', gradient: 'from-indigo-500 to-blue-600' }
{ id: 'txt2img', name: 'Text to Image', description: 'Generate images from text prompts', icon: 'pi pi-image', thumbnail: '/assets/card_images/workflow_01.webp' },
{ id: 'img2img', name: 'Image to Image', description: 'Transform existing images', icon: 'pi pi-images', thumbnail: '/assets/card_images/2690a78c-c210-4a52-8c37-3cb5bc4d9e71.webp' },
{ id: 'upscale', name: 'Upscale', description: '4x image upscaling workflow', icon: 'pi pi-expand', thumbnail: '/assets/card_images/bacb46ea-7e63-4f19-a253-daf41461e98f.webp' },
{ id: 'inpaint', name: 'Inpainting', description: 'Edit parts of an image', icon: 'pi pi-pencil', thumbnail: '/assets/card_images/228616f4-12ad-426d-84fb-f20e488ba7ee.webp' },
{ id: 'controlnet', name: 'ControlNet', description: 'Guided image generation', icon: 'pi pi-sliders-h', thumbnail: '/assets/card_images/683255d3-1d10-43d9-a6ff-ef142061e88a.webp' },
{ id: 'video', name: 'Video Generation', description: 'Create videos from prompts', icon: 'pi pi-video', thumbnail: '/assets/card_images/91f1f589-ddb4-4c4f-b3a7-ba30fc271987.webp' }
]
</script>
@@ -89,33 +90,31 @@ const starterTemplates = [
<div class="mb-8">
<h2 class="mb-3 text-sm font-medium text-zinc-900 dark:text-zinc-100">Start from a template</h2>
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
<button
<WorkspaceCard
v-for="template in starterTemplates"
:key="template.id"
class="group overflow-hidden rounded-lg border border-zinc-200 bg-white text-left transition-all hover:border-zinc-300 hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700"
:thumbnail="template.thumbnail"
:title="template.name"
:description="template.description"
:icon="template.icon"
action-label="Run"
action-icon="pi pi-play"
@click="router.push(`/${workspaceId}/default/${template.id}`)"
/>
</div>
<!-- View All Templates CTA -->
<div class="mt-6 flex items-center gap-4 pt-2">
<button
class="inline-flex items-center gap-2 rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700"
@click="router.push(`/${workspaceId}/templates`)"
>
<div
:class="[
'flex aspect-square items-center justify-center bg-gradient-to-br',
template.gradient
]"
>
<i :class="[template.icon, 'text-3xl text-white/90']" />
</div>
<div class="flex items-end justify-between gap-2 p-3">
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ template.name }}</p>
<p class="mt-0.5 line-clamp-1 text-xs text-zinc-500 dark:text-zinc-400">{{ template.description }}</p>
</div>
<span
class="inline-flex flex-shrink-0 items-center gap-1 rounded-md bg-blue-50 px-2 py-1 text-xs font-medium text-blue-600 transition-colors group-hover:bg-blue-600 group-hover:text-white dark:bg-blue-950 dark:text-blue-400 dark:group-hover:bg-blue-600 dark:group-hover:text-white"
>
<i class="pi pi-play text-[10px]" />
Run
</span>
</div>
View Templates
<i class="pi pi-arrow-right text-xs" />
</button>
<span class="text-sm text-zinc-400 dark:text-zinc-500">
<span class="font-semibold text-zinc-600 dark:text-zinc-300">803+</span> workflows, models, nodes by Comfy & community
</span>
<div class="h-px flex-1 bg-zinc-200 dark:bg-zinc-800" />
</div>
</div>

View File

@@ -0,0 +1,409 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import Popover from 'primevue/popover'
import {
WorkspaceSearchInput,
WorkspaceViewToggle,
WorkspaceSortSelect,
WorkspaceFilterSelect,
WorkspaceCard,
} from '@/components/v2/workspace'
const route = useRoute()
const workspaceId = computed(() => route.params.workspaceId as string)
// Library/Brand switcher
interface Library {
id: string
name: string
icon: string
color: string
itemCount: number
}
const libraries = ref<Library[]>([
{ id: 'netflix', name: 'Netflix', icon: 'pi pi-play', color: 'bg-red-600', itemCount: 248 },
{ id: 'adobe', name: 'Adobe Creative', icon: 'pi pi-palette', color: 'bg-rose-600', itemCount: 156 },
{ id: 'personal', name: 'My Library', icon: 'pi pi-user', color: 'bg-zinc-600', itemCount: 89 },
{ id: 'community', name: 'Community Hub', icon: 'pi pi-users', color: 'bg-violet-600', itemCount: 1240 },
])
const currentLibrary = ref<Library>(libraries.value[0])
const libraryMenu = ref<InstanceType<typeof Popover> | null>(null)
function toggleLibraryMenu(event: Event): void {
libraryMenu.value?.toggle(event)
}
function selectLibrary(library: Library): void {
currentLibrary.value = library
libraryMenu.value?.hide()
}
// Category tabs
type CategoryId = 'all' | 'workflows' | 'models' | 'nodepacks' | 'assets' | 'brand-kit'
interface Category {
id: CategoryId
label: string
icon: string
count: number
}
const categories = ref<Category[]>([
{ id: 'all', label: 'All', icon: 'pi pi-th-large', count: 248 },
{ id: 'workflows', label: 'Workflows', icon: 'pi pi-sitemap', count: 64 },
{ id: 'models', label: 'Models', icon: 'pi pi-box', count: 38 },
{ id: 'nodepacks', label: 'Nodepacks', icon: 'pi pi-th-large', count: 24 },
{ id: 'assets', label: 'Assets', icon: 'pi pi-images', count: 89 },
{ id: 'brand-kit', label: 'Brand Kit', icon: 'pi pi-palette', count: 33 },
])
const activeCategory = ref<CategoryId>('all')
// View mode & filters
type ViewMode = 'grid' | 'list'
const viewMode = ref<ViewMode>('grid')
const searchQuery = ref('')
const sortBy = ref('recent')
const filterBy = ref('all')
const sortOptions = [
{ value: 'recent', label: 'Recently Added' },
{ value: 'name', label: 'Name' },
{ value: 'popular', label: 'Most Used' },
{ value: 'updated', label: 'Last Updated' },
]
const filterOptions = [
{ value: 'all', label: 'All Items' },
{ value: 'shared', label: 'Shared with me' },
{ value: 'owned', label: 'Created by me' },
{ value: 'favorited', label: 'Favorited' },
]
// Mock library items
interface LibraryItem {
id: string
name: string
description: string
type: CategoryId
thumbnail: string
icon: string
author: string
updatedAt: string
updatedTimestamp: number
uses: number
isShared: boolean
isFavorited: boolean
}
const items = ref<LibraryItem[]>([
{ id: '1', name: 'SDXL Base Pipeline', description: 'Standard text-to-image workflow with SDXL', type: 'workflows', thumbnail: '/assets/card_images/workflow_01.webp', icon: 'pi pi-sitemap', author: 'Netflix Design', updatedAt: '2 hours ago', updatedTimestamp: Date.now() - 2 * 60 * 60 * 1000, uses: 1250, isShared: true, isFavorited: true },
{ id: '2', name: 'ControlNet Canny', description: 'Edge-guided image generation', type: 'workflows', thumbnail: '/assets/card_images/comfyui_workflow.jpg', icon: 'pi pi-sitemap', author: 'Team', updatedAt: '1 day ago', updatedTimestamp: Date.now() - 24 * 60 * 60 * 1000, uses: 890, isShared: true, isFavorited: false },
{ id: '3', name: 'SDXL Lightning v1.0', description: '4-step fast generation checkpoint', type: 'models', thumbnail: '/assets/card_images/2690a78c-c210-4a52-8c37-3cb5bc4d9e71.webp', icon: 'pi pi-box', author: 'ByteDance', updatedAt: '3 days ago', updatedTimestamp: Date.now() - 3 * 24 * 60 * 60 * 1000, uses: 3420, isShared: false, isFavorited: true },
{ id: '4', name: 'Flux.1 Dev', description: 'High quality diffusion model', type: 'models', thumbnail: '/assets/card_images/bacb46ea-7e63-4f19-a253-daf41461e98f.webp', icon: 'pi pi-box', author: 'Black Forest', updatedAt: '1 week ago', updatedTimestamp: Date.now() - 7 * 24 * 60 * 60 * 1000, uses: 5670, isShared: false, isFavorited: false },
{ id: '5', name: 'ComfyUI Manager', description: 'Install and manage custom nodes', type: 'nodepacks', thumbnail: '/assets/card_images/228616f4-12ad-426d-84fb-f20e488ba7ee.webp', icon: 'pi pi-th-large', author: 'Comfy Org', updatedAt: '2 weeks ago', updatedTimestamp: Date.now() - 14 * 24 * 60 * 60 * 1000, uses: 12400, isShared: false, isFavorited: true },
{ id: '6', name: 'Impact Pack', description: 'Advanced sampling and detailing', type: 'nodepacks', thumbnail: '/assets/card_images/683255d3-1d10-43d9-a6ff-ef142061e88a.webp', icon: 'pi pi-th-large', author: 'Dr.Lt.Data', updatedAt: '5 days ago', updatedTimestamp: Date.now() - 5 * 24 * 60 * 60 * 1000, uses: 8900, isShared: false, isFavorited: false },
{ id: '7', name: 'Brand Logo Pack', description: 'Netflix brand logos in various formats', type: 'brand-kit', thumbnail: '/assets/card_images/91f1f589-ddb4-4c4f-b3a7-ba30fc271987.webp', icon: 'pi pi-palette', author: 'Brand Team', updatedAt: '1 month ago', updatedTimestamp: Date.now() - 30 * 24 * 60 * 60 * 1000, uses: 450, isShared: true, isFavorited: false },
{ id: '8', name: 'Color Guidelines', description: 'Official brand color palette', type: 'brand-kit', thumbnail: '/assets/card_images/28e9f7ea-ef00-48e8-849d-8752a34939c7.webp', icon: 'pi pi-palette', author: 'Brand Team', updatedAt: '2 months ago', updatedTimestamp: Date.now() - 60 * 24 * 60 * 60 * 1000, uses: 890, isShared: true, isFavorited: true },
{ id: '9', name: 'Hero Images Q4', description: 'Generated hero images for campaigns', type: 'assets', thumbnail: '/assets/card_images/dda28581-37c8-44da-8822-57d1ccc2118c_2130x1658.png', icon: 'pi pi-images', author: 'Creative Team', updatedAt: '3 days ago', updatedTimestamp: Date.now() - 3 * 24 * 60 * 60 * 1000, uses: 67, isShared: true, isFavorited: false },
{ id: '10', name: 'Social Media Assets', description: 'Generated social media graphics', type: 'assets', thumbnail: '/assets/card_images/can-you-rate-my-comfyui-workflow-v0-o9clchhji39c1.webp', icon: 'pi pi-images', author: 'Marketing', updatedAt: '1 week ago', updatedTimestamp: Date.now() - 7 * 24 * 60 * 60 * 1000, uses: 234, isShared: true, isFavorited: false },
{ id: '11', name: 'Video Upscale 4K', description: 'AI-powered video upscaling workflow', type: 'workflows', thumbnail: '/assets/card_images/workflow_01.webp', icon: 'pi pi-sitemap', author: 'Video Team', updatedAt: '4 days ago', updatedTimestamp: Date.now() - 4 * 24 * 60 * 60 * 1000, uses: 567, isShared: true, isFavorited: false },
{ id: '12', name: 'Typography Set', description: 'Brand approved fonts and styles', type: 'brand-kit', thumbnail: '/assets/card_images/2690a78c-c210-4a52-8c37-3cb5bc4d9e71.webp', icon: 'pi pi-palette', author: 'Brand Team', updatedAt: '2 weeks ago', updatedTimestamp: Date.now() - 14 * 24 * 60 * 60 * 1000, uses: 320, isShared: true, isFavorited: false },
])
// Filtered and sorted items
const filteredItems = computed(() => {
let result = [...items.value]
// Filter by category
if (activeCategory.value !== 'all') {
result = result.filter(item => item.type === activeCategory.value)
}
// Filter by filter option
if (filterBy.value === 'shared') {
result = result.filter(item => item.isShared)
} else if (filterBy.value === 'favorited') {
result = result.filter(item => item.isFavorited)
}
// Filter by search
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
result = result.filter(item =>
item.name.toLowerCase().includes(query) ||
item.description.toLowerCase().includes(query) ||
item.author.toLowerCase().includes(query)
)
}
// Sort
result.sort((a, b) => {
switch (sortBy.value) {
case 'name':
return a.name.localeCompare(b.name)
case 'popular':
return b.uses - a.uses
case 'updated':
case 'recent':
default:
return b.updatedTimestamp - a.updatedTimestamp
}
})
return result
})
// Helpers
function getTypeColor(type: CategoryId): string {
const colors: Record<string, string> = {
workflows: 'bg-blue-500/20 text-blue-400',
models: 'bg-purple-500/20 text-purple-400',
nodepacks: 'bg-green-500/20 text-green-400',
assets: 'bg-amber-500/20 text-amber-400',
'brand-kit': 'bg-pink-500/20 text-pink-400',
}
return colors[type] || 'bg-zinc-500/20 text-zinc-400'
}
function getTypeLabel(type: CategoryId): string {
const labels: Record<string, string> = {
workflows: 'Workflow',
models: 'Model',
nodepacks: 'Nodepack',
assets: 'Asset',
'brand-kit': 'Brand Kit',
}
return labels[type] || type
}
function formatUses(uses: number): string {
if (uses >= 1000) {
return `${(uses / 1000).toFixed(1)}k`
}
return uses.toString()
}
</script>
<template>
<div class="p-6">
<!-- Header with Library Switcher -->
<div class="mb-6 flex items-center justify-between">
<div class="flex items-center gap-4">
<!-- Library Switcher -->
<button
class="flex items-center gap-3 rounded-lg border border-zinc-200 bg-white px-4 py-2.5 transition-colors hover:border-zinc-300 hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:hover:border-zinc-600 dark:hover:bg-zinc-700"
@click="toggleLibraryMenu"
>
<div :class="['flex h-8 w-8 items-center justify-center rounded-md text-white', currentLibrary.color]">
<i :class="[currentLibrary.icon, 'text-sm']" />
</div>
<div class="text-left">
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ currentLibrary.name }}</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ currentLibrary.itemCount }} items</p>
</div>
<i class="pi pi-chevron-down text-xs text-zinc-400" />
</button>
<!-- Library Menu -->
<Popover ref="libraryMenu" append-to="self">
<div class="w-72 p-2">
<p class="px-2 py-1.5 text-xs font-medium uppercase tracking-wide text-zinc-500 dark:text-zinc-400">
Switch Library
</p>
<div class="mt-1 flex flex-col gap-0.5">
<button
v-for="lib in libraries"
:key="lib.id"
:class="[
'flex items-center gap-3 rounded-md px-2 py-2 text-left transition-colors',
currentLibrary.id === lib.id
? 'bg-zinc-100 dark:bg-zinc-700'
: 'hover:bg-zinc-50 dark:hover:bg-zinc-800'
]"
@click="selectLibrary(lib)"
>
<div :class="['flex h-8 w-8 items-center justify-center rounded-md text-white', lib.color]">
<i :class="[lib.icon, 'text-sm']" />
</div>
<div class="flex-1">
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ lib.name }}</p>
<p class="text-xs text-zinc-500 dark:text-zinc-400">{{ lib.itemCount }} items</p>
</div>
<i v-if="currentLibrary.id === lib.id" class="pi pi-check text-sm text-blue-500" />
</button>
</div>
</div>
</Popover>
<div>
<h1 class="text-2xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100">
Library Hub
</h1>
<p class="text-sm text-zinc-500 dark:text-zinc-400">
Shared workflows, models, nodepacks, and brand assets
</p>
</div>
</div>
<div class="flex items-center gap-2">
<RouterLink
:to="`/${workspaceId}/create`"
class="inline-flex items-center gap-2 rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700"
>
<i class="pi pi-bolt text-xs" />
Linear
</RouterLink>
<RouterLink
:to="`/${workspaceId}/canvas`"
class="inline-flex items-center gap-2 rounded-md border border-zinc-200 bg-white px-3 py-2 text-sm font-medium text-zinc-700 transition-colors hover:bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300 dark:hover:bg-zinc-700"
>
<i class="pi pi-share-alt text-xs" />
Node
</RouterLink>
<button
class="inline-flex items-center gap-2 rounded-md bg-zinc-900 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
>
<i class="pi pi-plus text-xs" />
Add to Library
</button>
</div>
</div>
<!-- Category Tabs -->
<div class="mb-6 flex items-center gap-1 rounded-lg border border-zinc-200 bg-zinc-50 p-1 dark:border-zinc-700 dark:bg-zinc-800/50">
<button
v-for="cat in categories"
:key="cat.id"
:class="[
'flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-colors',
activeCategory === cat.id
? 'bg-white text-zinc-900 shadow-sm dark:bg-zinc-700 dark:text-zinc-100'
: 'text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200'
]"
@click="activeCategory = cat.id"
>
<i :class="[cat.icon, 'text-sm']" />
{{ cat.label }}
<span
:class="[
'rounded-full px-1.5 py-0.5 text-xs',
activeCategory === cat.id
? 'bg-zinc-100 text-zinc-600 dark:bg-zinc-600 dark:text-zinc-200'
: 'bg-zinc-200/50 text-zinc-500 dark:bg-zinc-700 dark:text-zinc-400'
]"
>
{{ cat.count }}
</span>
</button>
</div>
<!-- Search & Filters -->
<div class="mb-4 flex items-center gap-3">
<WorkspaceSearchInput
v-model="searchQuery"
placeholder="Search library..."
/>
<WorkspaceViewToggle v-model="viewMode" />
<WorkspaceSortSelect v-model="sortBy" :options="sortOptions" />
<WorkspaceFilterSelect v-model="filterBy" :options="filterOptions" />
</div>
<!-- Empty State -->
<div
v-if="filteredItems.length === 0"
class="flex flex-col items-center justify-center rounded-lg border border-dashed border-zinc-300 py-16 dark:border-zinc-700"
>
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-zinc-100 dark:bg-zinc-800">
<i class="pi pi-inbox text-xl text-zinc-400" />
</div>
<h3 class="mt-4 text-sm font-medium text-zinc-900 dark:text-zinc-100">No items found</h3>
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">
{{ searchQuery || filterBy !== 'all' ? 'Try different filters' : 'Add items to get started' }}
</p>
</div>
<!-- Grid View -->
<div
v-else-if="viewMode === 'grid'"
class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5"
>
<WorkspaceCard
v-for="item in filteredItems"
:key="item.id"
:thumbnail="item.thumbnail"
:title="item.name"
:description="item.description"
:icon="item.icon"
:badge="getTypeLabel(item.type)"
:badge-class="getTypeColor(item.type)"
:stats="[
{ icon: 'pi pi-user', value: item.author },
{ icon: 'pi pi-chart-bar', value: formatUses(item.uses) }
]"
:updated-at="item.updatedAt"
/>
</div>
<!-- List View -->
<div v-else class="rounded-lg border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900">
<!-- List Header -->
<div class="flex items-center gap-4 border-b border-zinc-100 px-5 py-3 text-xs font-medium uppercase tracking-wide text-zinc-500 dark:border-zinc-800 dark:text-zinc-400">
<div class="w-12">Icon</div>
<div class="flex-1">Name</div>
<div class="w-24">Type</div>
<div class="w-24 text-right">Uses</div>
<div class="w-28 text-right">Author</div>
<div class="w-28 text-right">Updated</div>
<div class="w-10"></div>
</div>
<!-- List Items -->
<div class="divide-y divide-zinc-100 dark:divide-zinc-800">
<div
v-for="item in filteredItems"
:key="item.id"
class="flex cursor-pointer items-center gap-4 px-5 py-4 transition-colors hover:bg-zinc-50 dark:hover:bg-zinc-800/50"
>
<div class="w-12">
<div class="h-10 w-10 overflow-hidden rounded-md">
<img :src="item.thumbnail" :alt="item.name" class="h-full w-full object-cover" />
</div>
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<p class="font-medium text-zinc-900 dark:text-zinc-100">{{ item.name }}</p>
<i v-if="item.isFavorited" class="pi pi-star-fill text-xs text-amber-400" />
<i v-if="item.isShared" class="pi pi-share-alt text-xs text-zinc-400" />
</div>
<p class="truncate text-sm text-zinc-500 dark:text-zinc-400">{{ item.description }}</p>
</div>
<div class="w-24">
<span :class="['rounded px-2 py-1 text-xs font-medium', getTypeColor(item.type)]">
{{ getTypeLabel(item.type) }}
</span>
</div>
<div class="w-24 text-right text-sm text-zinc-500 dark:text-zinc-400">
{{ formatUses(item.uses) }}
</div>
<div class="w-28 truncate text-right text-sm text-zinc-500 dark:text-zinc-400">
{{ item.author }}
</div>
<div class="w-28 text-right text-sm text-zinc-400 dark:text-zinc-500">
{{ item.updatedAt }}
</div>
<div class="w-10">
<button
class="rounded p-1.5 text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-700 dark:hover:text-zinc-300"
@click.stop
>
<i class="pi pi-ellipsis-h text-sm" />
</button>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -8,6 +8,7 @@ import {
WorkspaceSearchInput,
WorkspaceSortSelect,
CreateProjectDialog,
WorkspaceCard,
} from '@/components/v2/workspace'
const route = useRoute()
@@ -31,10 +32,10 @@ const sortOptions = [
// Projects data
const projects = ref([
{ id: 'img-gen', name: 'Image Generation', description: 'AI image generation workflows', canvasCount: 5, modelCount: 12, updatedAt: '2 hours ago', updatedTimestamp: Date.now() - 2 * 60 * 60 * 1000 },
{ id: 'video-proc', name: 'Video Processing', description: 'Video enhancement and editing', canvasCount: 3, modelCount: 8, updatedAt: '1 day ago', updatedTimestamp: Date.now() - 24 * 60 * 60 * 1000 },
{ id: 'audio-enh', name: 'Audio Enhancement', description: 'Audio processing pipelines', canvasCount: 2, modelCount: 4, updatedAt: '3 days ago', updatedTimestamp: Date.now() - 3 * 24 * 60 * 60 * 1000 },
{ id: 'upscale', name: 'Upscaling', description: 'Image and video upscaling', canvasCount: 4, modelCount: 6, updatedAt: '1 week ago', updatedTimestamp: Date.now() - 7 * 24 * 60 * 60 * 1000 }
{ id: 'img-gen', name: 'Image Generation', description: 'AI image generation workflows', canvasCount: 5, modelCount: 12, updatedAt: '2 hours ago', updatedTimestamp: Date.now() - 2 * 60 * 60 * 1000, thumbnail: '/thumbnails/project-1.jpg' },
{ id: 'video-proc', name: 'Video Processing', description: 'Video enhancement and editing', canvasCount: 3, modelCount: 8, updatedAt: '1 day ago', updatedTimestamp: Date.now() - 24 * 60 * 60 * 1000, thumbnail: '/thumbnails/project-2.jpg' },
{ id: 'audio-enh', name: 'Audio Enhancement', description: 'Audio processing pipelines', canvasCount: 2, modelCount: 4, updatedAt: '3 days ago', updatedTimestamp: Date.now() - 3 * 24 * 60 * 60 * 1000, thumbnail: '/assets/card_images/28e9f7ea-ef00-48e8-849d-8752a34939c7.webp' },
{ id: 'upscale', name: 'Upscaling', description: 'Image and video upscaling', canvasCount: 4, modelCount: 6, updatedAt: '1 week ago', updatedTimestamp: Date.now() - 7 * 24 * 60 * 60 * 1000, thumbnail: '/assets/card_images/comfyui_workflow.jpg' }
])
// Create dialog
@@ -125,42 +126,19 @@ const emptyStateDescription = computed(() =>
v-else-if="viewMode === 'grid'"
class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5"
>
<div
<WorkspaceCard
v-for="project in filteredProjects"
:key="project.id"
class="group aspect-square cursor-pointer rounded-lg border border-zinc-200 bg-white p-4 text-left transition-all hover:border-zinc-300 hover:shadow-sm dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700"
:thumbnail="project.thumbnail"
:title="project.name"
:description="project.description"
icon="pi pi-folder"
:stats="[
{ icon: 'pi pi-objects-column', value: project.canvasCount },
{ icon: 'pi pi-box', value: project.modelCount }
]"
@click="openProject(project.id)"
>
<div class="flex h-full flex-col">
<div class="flex items-start justify-between">
<div class="flex h-10 w-10 items-center justify-center rounded-md bg-zinc-100 dark:bg-zinc-800">
<i class="pi pi-folder text-zinc-500 dark:text-zinc-400" />
</div>
<button
class="rounded p-1 text-zinc-400 opacity-0 transition-opacity hover:bg-zinc-100 hover:text-zinc-600 group-hover:opacity-100 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
@click.stop
>
<i class="pi pi-ellipsis-h text-sm" />
</button>
</div>
<div class="mt-auto">
<h3 class="font-medium text-zinc-900 dark:text-zinc-100">{{ project.name }}</h3>
<p class="mt-1 line-clamp-2 text-sm text-zinc-500 dark:text-zinc-400">
{{ project.description }}
</p>
<div class="mt-2 flex items-center gap-3 text-xs text-zinc-400 dark:text-zinc-500">
<span class="flex items-center gap-1">
<i class="pi pi-objects-column" />
{{ project.canvasCount }}
</span>
<span class="flex items-center gap-1">
<i class="pi pi-box" />
{{ project.modelCount }}
</span>
</div>
</div>
</div>
</div>
/>
</div>
<!-- List View -->

View File

@@ -1,6 +1,15 @@
<script setup lang="ts">
import { ref } from 'vue'
import WorkspaceViewHeader from '@/components/v2/workspace/WorkspaceViewHeader.vue'
import { ref, computed } from 'vue'
import {
WorkspaceViewHeader,
WorkspaceSearchInput,
WorkspaceViewToggle,
WorkspaceSortSelect,
WorkspaceFilterSelect,
WorkspaceCard,
} from '@/components/v2/workspace'
type ViewMode = 'grid' | 'list'
interface RecentItem {
id: string
@@ -8,20 +17,63 @@ interface RecentItem {
type: 'canvas' | 'workflow' | 'asset' | 'project'
icon: string
updatedAt: string
thumbnail?: string
thumbnail: string
}
const searchQuery = ref('')
const viewMode = ref<ViewMode>('grid')
const sortBy = ref('recent')
const filterBy = ref('all')
const sortOptions = [
{ value: 'recent', label: 'Most Recent' },
{ value: 'name', label: 'Name' },
{ value: 'type', label: 'Type' },
]
const filterOptions = [
{ value: 'all', label: 'All Types' },
{ value: 'canvas', label: 'Canvas' },
{ value: 'workflow', label: 'Workflow' },
{ value: 'project', label: 'Project' },
{ value: 'asset', label: 'Asset' },
]
const recentItems = ref<RecentItem[]>([
{ id: '1', name: 'Portrait Generation', type: 'canvas', icon: 'pi-objects-column', updatedAt: '2 minutes ago' },
{ id: '2', name: 'SDXL Workflow', type: 'workflow', icon: 'pi-sitemap', updatedAt: '15 minutes ago' },
{ id: '3', name: 'Product Shots', type: 'project', icon: 'pi-folder', updatedAt: '1 hour ago' },
{ id: '4', name: 'reference_image.png', type: 'asset', icon: 'pi-image', updatedAt: '2 hours ago' },
{ id: '5', name: 'Inpainting Canvas', type: 'canvas', icon: 'pi-objects-column', updatedAt: '3 hours ago' },
{ id: '6', name: 'ControlNet Pipeline', type: 'workflow', icon: 'pi-sitemap', updatedAt: '5 hours ago' },
{ id: '7', name: 'Marketing Assets', type: 'project', icon: 'pi-folder', updatedAt: 'Yesterday' },
{ id: '8', name: 'logo_v2.png', type: 'asset', icon: 'pi-image', updatedAt: 'Yesterday' },
{ id: '1', name: 'Portrait Generation', type: 'canvas', icon: 'pi pi-objects-column', updatedAt: '2 minutes ago', thumbnail: '/thumbnails/canvas-1.jpg' },
{ id: '2', name: 'SDXL Workflow', type: 'workflow', icon: 'pi pi-sitemap', updatedAt: '15 minutes ago', thumbnail: '/thumbnails/workflow-1.jpg' },
{ id: '3', name: 'Product Shots', type: 'project', icon: 'pi pi-folder', updatedAt: '1 hour ago', thumbnail: '/thumbnails/project-1.jpg' },
{ id: '4', name: 'reference_image.png', type: 'asset', icon: 'pi pi-image', updatedAt: '2 hours ago', thumbnail: '/thumbnails/asset-1.jpg' },
{ id: '5', name: 'Inpainting Canvas', type: 'canvas', icon: 'pi pi-objects-column', updatedAt: '3 hours ago', thumbnail: '/thumbnails/canvas-2.jpg' },
{ id: '6', name: 'ControlNet Pipeline', type: 'workflow', icon: 'pi pi-sitemap', updatedAt: '5 hours ago', thumbnail: '/thumbnails/workflow-2.jpg' },
{ id: '7', name: 'Marketing Assets', type: 'project', icon: 'pi pi-folder', updatedAt: 'Yesterday', thumbnail: '/thumbnails/project-2.jpg' },
{ id: '8', name: 'logo_v2.png', type: 'asset', icon: 'pi pi-image', updatedAt: 'Yesterday', thumbnail: '/thumbnails/asset-2.jpg' },
])
const filteredItems = computed(() => {
let items = [...recentItems.value]
// Filter by type
if (filterBy.value !== 'all') {
items = items.filter(item => item.type === filterBy.value)
}
// Filter by search query
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
items = items.filter(item => item.name.toLowerCase().includes(query))
}
// Sort items
if (sortBy.value === 'name') {
items.sort((a, b) => a.name.localeCompare(b.name))
} else if (sortBy.value === 'type') {
items.sort((a, b) => a.type.localeCompare(b.type))
}
return items
})
function getTypeLabel(type: string): string {
const labels: Record<string, string> = {
canvas: 'Canvas',
@@ -41,6 +93,7 @@ function getTypeColor(type: string): string {
}
return colors[type] || 'bg-zinc-500/20 text-zinc-400'
}
</script>
<template>
@@ -51,28 +104,68 @@ function getTypeColor(type: string): string {
:show-create-buttons="true"
/>
<div class="space-y-2">
<div
v-for="item in recentItems"
<!-- Search & Actions Toolbar -->
<div class="mb-4 flex items-center gap-3">
<WorkspaceSearchInput
v-model="searchQuery"
placeholder="Search recents..."
/>
<WorkspaceViewToggle v-model="viewMode" />
<WorkspaceSortSelect v-model="sortBy" :options="sortOptions" />
<WorkspaceFilterSelect v-model="filterBy" :options="filterOptions" />
</div>
<!-- Grid View -->
<div
v-if="viewMode === 'grid'"
class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5"
>
<WorkspaceCard
v-for="item in filteredItems"
:key="item.id"
class="flex items-center gap-4 rounded-lg border border-zinc-200 bg-white p-4 transition-colors hover:border-zinc-300 hover:bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700 dark:hover:bg-zinc-800/50"
>
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-zinc-100 dark:bg-zinc-800">
<i :class="['pi', item.icon, 'text-lg text-zinc-500 dark:text-zinc-400']" />
</div>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span class="font-medium text-zinc-900 dark:text-zinc-100">{{ item.name }}</span>
<span :class="['rounded px-1.5 py-0.5 text-[10px] font-medium', getTypeColor(item.type)]">
{{ getTypeLabel(item.type) }}
</span>
:thumbnail="item.thumbnail"
:title="item.name"
:icon="item.icon"
:badge="getTypeLabel(item.type)"
:badge-class="getTypeColor(item.type)"
:updated-at="item.updatedAt"
/>
</div>
<!-- List View -->
<div v-else class="rounded-lg border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900">
<div class="divide-y divide-zinc-100 dark:divide-zinc-800">
<div
v-for="item in filteredItems"
:key="item.id"
class="flex w-full cursor-pointer items-center gap-4 px-5 py-4 text-left transition-colors hover:bg-zinc-50 dark:hover:bg-zinc-800/50"
>
<div class="flex h-10 w-10 items-center justify-center rounded-md bg-zinc-100 dark:bg-zinc-800">
<i :class="['pi', item.icon, 'text-zinc-500 dark:text-zinc-400']" />
</div>
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ item.updatedAt }}</p>
<div class="flex-1 min-w-0">
<p class="font-medium text-zinc-900 dark:text-zinc-100">{{ item.name }}</p>
<p class="text-sm text-zinc-500 dark:text-zinc-400">
<span :class="['rounded px-1.5 py-0.5 text-[10px] font-medium', getTypeColor(item.type)]">
{{ getTypeLabel(item.type) }}
</span>
</p>
</div>
<span class="text-sm text-zinc-400 dark:text-zinc-500">{{ item.updatedAt }}</span>
<button
class="rounded p-1 text-zinc-400 hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-700 dark:hover:text-zinc-300"
@click.stop
>
<i class="pi pi-ellipsis-h text-sm" />
</button>
</div>
<button class="flex h-8 w-8 items-center justify-center rounded-md text-zinc-400 transition-colors hover:bg-zinc-100 hover:text-zinc-600 dark:hover:bg-zinc-800 dark:hover:text-zinc-300">
<i class="pi pi-ellipsis-v text-sm" />
</button>
</div>
</div>
<!-- Empty State -->
<div v-if="filteredItems.length === 0" class="py-12 text-center">
<i class="pi pi-search mb-4 text-4xl text-zinc-300 dark:text-zinc-600" />
<p class="text-zinc-500 dark:text-zinc-400">No items found</p>
</div>
</div>
</template>

View File

@@ -46,29 +46,28 @@ const sections = computed(() => {
</p>
</div>
<div class="flex gap-8">
<!-- Sidebar Navigation -->
<nav class="w-48 flex-shrink-0">
<ul class="flex flex-col gap-1">
<li v-for="section in sections" :key="section.id">
<button
:class="[
'flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors',
activeSection === section.id
? 'bg-zinc-100 font-medium text-zinc-900 dark:bg-zinc-800 dark:text-zinc-100'
: 'text-zinc-600 hover:bg-zinc-50 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-800/50 dark:hover:text-zinc-100'
]"
@click="activeSection = section.id"
>
<i :class="[section.icon, 'text-sm']" />
{{ section.label }}
</button>
</li>
</ul>
<!-- Tab Navigation -->
<div class="mb-6 border-b border-zinc-200 dark:border-zinc-800">
<nav class="flex gap-1">
<button
v-for="section in sections"
:key="section.id"
:class="[
'flex items-center gap-2 border-b-2 px-4 py-2.5 text-sm font-medium transition-colors',
activeSection === section.id
? 'border-zinc-900 text-zinc-900 dark:border-zinc-100 dark:text-zinc-100'
: 'border-transparent text-zinc-500 hover:border-zinc-300 hover:text-zinc-700 dark:text-zinc-400 dark:hover:border-zinc-600 dark:hover:text-zinc-300'
]"
@click="activeSection = section.id"
>
<i :class="[section.icon, 'text-sm']" />
{{ section.label }}
</button>
</nav>
</div>
<!-- Content -->
<div class="flex-1">
<!-- Content -->
<div class="max-w-2xl">
<!-- General Settings -->
<div v-if="activeSection === 'general'" class="space-y-6">
<div class="rounded-lg border border-zinc-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-900">
@@ -266,7 +265,6 @@ const sections = computed(() => {
</div>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,180 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
WorkspaceViewHeader,
WorkspaceSearchInput,
WorkspaceViewToggle,
WorkspaceSortSelect,
WorkspaceFilterSelect,
WorkspaceCard,
} from '@/components/v2/workspace'
const route = useRoute()
const router = useRouter()
const workspaceId = computed(() => route.params.workspaceId as string)
type ViewMode = 'grid' | 'list'
const searchQuery = ref('')
const viewMode = ref<ViewMode>('grid')
const sortBy = ref('popular')
const filterBy = ref('all')
const sortOptions = [
{ value: 'popular', label: 'Most Popular' },
{ value: 'recent', label: 'Recently Added' },
{ value: 'name', label: 'Name' },
]
const filterOptions = [
{ value: 'all', label: 'All Categories' },
{ value: 'official', label: 'Official' },
{ value: 'sdxl', label: 'SDXL' },
{ value: 'controlnet', label: 'ControlNet' },
{ value: 'video', label: 'Video' },
{ value: 'community', label: 'Community' },
]
const templates = ref([
{ id: 'txt2img', name: 'Text to Image', description: 'Generate images from text prompts', category: 'official', icon: 'pi pi-image', thumbnail: '/assets/card_images/workflow_01.webp', uses: 12500 },
{ id: 'img2img', name: 'Image to Image', description: 'Transform existing images', category: 'official', icon: 'pi pi-images', thumbnail: '/assets/card_images/2690a78c-c210-4a52-8c37-3cb5bc4d9e71.webp', uses: 8900 },
{ id: 'upscale', name: 'Upscale 4x', description: '4x image upscaling workflow', category: 'official', icon: 'pi pi-expand', thumbnail: '/assets/card_images/bacb46ea-7e63-4f19-a253-daf41461e98f.webp', uses: 7200 },
{ id: 'inpaint', name: 'Inpainting', description: 'Edit parts of an image', category: 'official', icon: 'pi pi-pencil', thumbnail: '/assets/card_images/228616f4-12ad-426d-84fb-f20e488ba7ee.webp', uses: 6100 },
{ id: 'controlnet', name: 'ControlNet Pose', description: 'Pose-guided generation', category: 'controlnet', icon: 'pi pi-sliders-h', thumbnail: '/assets/card_images/683255d3-1d10-43d9-a6ff-ef142061e88a.webp', uses: 5400 },
{ id: 'video', name: 'Video Generation', description: 'Create videos from prompts', category: 'video', icon: 'pi pi-video', thumbnail: '/assets/card_images/91f1f589-ddb4-4c4f-b3a7-ba30fc271987.webp', uses: 4800 },
{ id: 'sdxl-turbo', name: 'SDXL Turbo', description: 'Fast SDXL generation', category: 'sdxl', icon: 'pi pi-bolt', thumbnail: '/assets/card_images/28e9f7ea-ef00-48e8-849d-8752a34939c7.webp', uses: 4200 },
{ id: 'canny', name: 'ControlNet Canny', description: 'Edge-guided generation', category: 'controlnet', icon: 'pi pi-stop', thumbnail: '/assets/card_images/comfyui_workflow.jpg', uses: 3800 },
{ id: 'depth', name: 'ControlNet Depth', description: 'Depth-guided generation', category: 'controlnet', icon: 'pi pi-box', thumbnail: '/assets/card_images/can-you-rate-my-comfyui-workflow-v0-o9clchhji39c1.webp', uses: 3500 },
{ id: 'sdxl-refiner', name: 'SDXL + Refiner', description: 'Two-stage SDXL workflow', category: 'sdxl', icon: 'pi pi-sparkles', thumbnail: '/assets/card_images/dda28581-37c8-44da-8822-57d1ccc2118c_2130x1658.png', uses: 3200 },
{ id: 'animatediff', name: 'AnimateDiff', description: 'Animate images to video', category: 'video', icon: 'pi pi-play', thumbnail: '/thumbnails/workflow-1.jpg', uses: 2900 },
{ id: 'face-swap', name: 'Face Swap', description: 'Swap faces in images', category: 'community', icon: 'pi pi-user', thumbnail: '/thumbnails/workflow-2.jpg', uses: 2600 },
])
const filteredTemplates = computed(() => {
let result = [...templates.value]
if (filterBy.value !== 'all') {
result = result.filter(t => t.category === filterBy.value)
}
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
result = result.filter(t =>
t.name.toLowerCase().includes(query) ||
t.description.toLowerCase().includes(query)
)
}
if (sortBy.value === 'name') {
result.sort((a, b) => a.name.localeCompare(b.name))
} else if (sortBy.value === 'popular') {
result.sort((a, b) => b.uses - a.uses)
}
return result
})
function getCategoryColor(category: string): string {
const colors: Record<string, string> = {
official: 'bg-blue-500/20 text-blue-400',
sdxl: 'bg-purple-500/20 text-purple-400',
controlnet: 'bg-green-500/20 text-green-400',
video: 'bg-amber-500/20 text-amber-400',
community: 'bg-pink-500/20 text-pink-400',
}
return colors[category] || 'bg-zinc-500/20 text-zinc-400'
}
function formatUses(uses: number): string {
if (uses >= 1000) {
return `${(uses / 1000).toFixed(1)}k`
}
return uses.toString()
}
function openTemplate(templateId: string): void {
router.push(`/${workspaceId.value}/default/${templateId}`)
}
</script>
<template>
<div class="p-6">
<WorkspaceViewHeader
title="Templates"
:subtitle="`${templates.length} templates available`"
:show-create-buttons="true"
/>
<!-- Search & Filters -->
<div class="mb-4 flex items-center gap-3">
<WorkspaceSearchInput
v-model="searchQuery"
placeholder="Search templates..."
/>
<WorkspaceViewToggle v-model="viewMode" />
<WorkspaceSortSelect v-model="sortBy" :options="sortOptions" />
<WorkspaceFilterSelect v-model="filterBy" :options="filterOptions" />
</div>
<!-- Grid View -->
<div
v-if="viewMode === 'grid'"
class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5"
>
<WorkspaceCard
v-for="template in filteredTemplates"
:key="template.id"
:thumbnail="template.thumbnail"
:title="template.name"
:description="template.description"
:icon="template.icon"
:badge="template.category"
:badge-class="getCategoryColor(template.category)"
action-label="Run"
action-icon="pi pi-play"
:stats="[{ icon: 'pi pi-users', value: formatUses(template.uses) }]"
@click="openTemplate(template.id)"
/>
</div>
<!-- List View -->
<div v-else class="rounded-lg border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900">
<div class="divide-y divide-zinc-100 dark:divide-zinc-800">
<div
v-for="template in filteredTemplates"
:key="template.id"
class="flex w-full cursor-pointer items-center gap-4 px-5 py-4 text-left transition-colors hover:bg-zinc-50 dark:hover:bg-zinc-800/50"
@click="openTemplate(template.id)"
>
<div class="h-12 w-12 overflow-hidden rounded-md">
<img :src="template.thumbnail" :alt="template.name" class="h-full w-full object-cover" />
</div>
<div class="flex-1 min-w-0">
<p class="font-medium text-zinc-900 dark:text-zinc-100">{{ template.name }}</p>
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ template.description }}</p>
</div>
<span :class="['rounded px-2 py-1 text-xs font-medium capitalize', getCategoryColor(template.category)]">
{{ template.category }}
</span>
<span class="flex items-center gap-1 text-sm text-zinc-400 dark:text-zinc-500">
<i class="pi pi-users text-xs" />
{{ formatUses(template.uses) }}
</span>
<button
class="inline-flex items-center gap-1 rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-blue-700"
>
<i class="pi pi-play text-xs" />
Run
</button>
</div>
</div>
</div>
<!-- Empty State -->
<div v-if="filteredTemplates.length === 0" class="py-12 text-center">
<i class="pi pi-search mb-4 text-4xl text-zinc-300 dark:text-zinc-600" />
<p class="text-zinc-500 dark:text-zinc-400">No templates found</p>
</div>
</div>
</template>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import { WorkspaceCard } from '@/components/v2/workspace'
const route = useRoute()
const workspaceId = computed(() => route.params.workspaceId as string)
@@ -21,10 +22,10 @@ const sortOptions: { value: SortOption; label: string }[] = [
// Mock workflows data
const workflows = ref([
{ id: 'txt2img-basic', name: 'Text to Image Basic', description: 'Simple text to image generation', nodeCount: 8, updatedAt: '1 day ago', updatedTimestamp: Date.now() - 24 * 60 * 60 * 1000 },
{ id: 'img2img-refine', name: 'Image Refinement', description: 'Refine and enhance images', nodeCount: 12, updatedAt: '2 days ago', updatedTimestamp: Date.now() - 2 * 24 * 60 * 60 * 1000 },
{ id: 'upscale-4x', name: '4x Upscale', description: 'High quality image upscaling', nodeCount: 5, updatedAt: '3 days ago', updatedTimestamp: Date.now() - 3 * 24 * 60 * 60 * 1000 },
{ id: 'controlnet-pose', name: 'ControlNet Pose', description: 'Pose-guided generation', nodeCount: 15, updatedAt: '1 week ago', updatedTimestamp: Date.now() - 7 * 24 * 60 * 60 * 1000 }
{ id: 'txt2img-basic', name: 'Text to Image Basic', description: 'Simple text to image generation', nodeCount: 8, updatedAt: '1 day ago', updatedTimestamp: Date.now() - 24 * 60 * 60 * 1000, thumbnail: '/thumbnails/workflow-1.jpg' },
{ id: 'img2img-refine', name: 'Image Refinement', description: 'Refine and enhance images', nodeCount: 12, updatedAt: '2 days ago', updatedTimestamp: Date.now() - 2 * 24 * 60 * 60 * 1000, thumbnail: '/thumbnails/workflow-2.jpg' },
{ id: 'upscale-4x', name: '4x Upscale', description: 'High quality image upscaling', nodeCount: 5, updatedAt: '3 days ago', updatedTimestamp: Date.now() - 3 * 24 * 60 * 60 * 1000, thumbnail: '/assets/card_images/can-you-rate-my-comfyui-workflow-v0-o9clchhji39c1.webp' },
{ id: 'controlnet-pose', name: 'ControlNet Pose', description: 'Pose-guided generation', nodeCount: 15, updatedAt: '1 week ago', updatedTimestamp: Date.now() - 7 * 24 * 60 * 60 * 1000, thumbnail: '/assets/card_images/dda28581-37c8-44da-8822-57d1ccc2118c_2130x1658.png' }
])
// Search and sort
@@ -166,38 +167,16 @@ const filteredWorkflows = computed(() => {
v-else-if="viewMode === 'grid'"
class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5"
>
<div
<WorkspaceCard
v-for="workflow in filteredWorkflows"
:key="workflow.id"
class="group aspect-square cursor-pointer rounded-lg border border-zinc-200 bg-white p-4 text-left transition-all hover:border-zinc-300 hover:shadow-sm dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700"
>
<div class="flex h-full flex-col">
<div class="flex items-start justify-between">
<div class="flex h-10 w-10 items-center justify-center rounded-md bg-zinc-100 dark:bg-zinc-800">
<i class="pi pi-sitemap text-zinc-500 dark:text-zinc-400" />
</div>
<button
class="rounded p-1 text-zinc-400 opacity-0 transition-opacity hover:bg-zinc-100 hover:text-zinc-600 group-hover:opacity-100 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
@click.stop
>
<i class="pi pi-ellipsis-h text-sm" />
</button>
</div>
<div class="mt-auto">
<h3 class="font-medium text-zinc-900 dark:text-zinc-100">{{ workflow.name }}</h3>
<p class="mt-1 line-clamp-2 text-sm text-zinc-500 dark:text-zinc-400">
{{ workflow.description }}
</p>
<div class="mt-2 flex items-center gap-3 text-xs text-zinc-400 dark:text-zinc-500">
<span class="flex items-center gap-1">
<i class="pi pi-stop" />
{{ workflow.nodeCount }}
</span>
<span>{{ workflow.updatedAt }}</span>
</div>
</div>
</div>
</div>
:thumbnail="workflow.thumbnail"
:title="workflow.name"
:description="workflow.description"
icon="pi pi-sitemap"
:stats="[{ icon: 'pi pi-stop', value: workflow.nodeCount }]"
:updated-at="workflow.updatedAt"
/>
</div>
<!-- List View -->