Compare commits
53 Commits
refactor(c
...
test-broke
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
587beda065 | ||
|
|
81401c61f3 | ||
|
|
3af88a8b93 | ||
|
|
8c0c819471 | ||
|
|
176c8e110b | ||
|
|
959c1990b5 | ||
|
|
e2c8478025 | ||
|
|
0762985ca7 | ||
|
|
9514e5d36c | ||
|
|
970861e677 | ||
|
|
216c8694c7 | ||
|
|
d253f67919 | ||
|
|
082426f084 | ||
|
|
08c43f6028 | ||
|
|
6b62d9cff9 | ||
|
|
916d90bb51 | ||
|
|
17be3b9770 | ||
|
|
e5edbf91eb | ||
|
|
0977e6e751 | ||
|
|
3c4b99ed84 | ||
|
|
212d19e2fa | ||
|
|
4bce7fbcde | ||
|
|
f6bc10bb9d | ||
|
|
68ccd683ad | ||
|
|
88df6627f0 | ||
|
|
ccb73186fb | ||
|
|
c0c284977d | ||
|
|
08faa69256 | ||
|
|
08a3c767ac | ||
|
|
552452622d | ||
|
|
01a7c6ee54 | ||
|
|
ac1b551b76 | ||
|
|
6d57b4def5 | ||
|
|
744d37cc3c | ||
|
|
ed7ec2af0f | ||
|
|
b598ce2c0f | ||
|
|
2724840fea | ||
|
|
7724558e7b | ||
|
|
14fdbdf793 | ||
|
|
8d37e48849 | ||
|
|
0a7515b757 | ||
|
|
9a35fa97a5 | ||
|
|
f1b8c2246e | ||
|
|
2c26fbb550 | ||
|
|
6244cf1008 | ||
|
|
2044d1430c | ||
|
|
7e011da830 | ||
|
|
fa1719fece | ||
|
|
6dd688e680 | ||
|
|
97c7aef72d | ||
|
|
80335ac936 | ||
|
|
6396eb6fa3 | ||
|
|
fba580dc7d |
1
.github/workflows/ci-tests-e2e.yaml
vendored
@@ -45,6 +45,7 @@ jobs:
|
||||
playwright-tests-chromium-sharded:
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Automated weekly workflow to bump ComfyUI frontend RC releases
|
||||
name: "Release: Weekly ComfyUI"
|
||||
# Automated bi-weekly workflow to bump ComfyUI frontend RC releases
|
||||
name: "Release: Bi-weekly ComfyUI"
|
||||
|
||||
on:
|
||||
# Schedule for Monday at 12:00 PM PST (20:00 UTC)
|
||||
schedule:
|
||||
- cron: '0 20 * * 1'
|
||||
|
||||
# Allow manual triggering
|
||||
# Allow manual triggering (bypasses bi-weekly check)
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
comfyui_fork:
|
||||
@@ -16,7 +16,39 @@ on:
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
check-release-week:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
is_release_week: ${{ steps.check.outputs.is_release_week }}
|
||||
steps:
|
||||
- name: Check if release week
|
||||
id: check
|
||||
run: |
|
||||
# Anchor date: first bi-weekly release Monday
|
||||
ANCHOR="2025-12-22"
|
||||
|
||||
ANCHOR_EPOCH=$(date -d "$ANCHOR" +%s)
|
||||
NOW_EPOCH=$(date +%s)
|
||||
WEEKS_SINCE=$(( (NOW_EPOCH - ANCHOR_EPOCH) / 604800 ))
|
||||
|
||||
if [ $((WEEKS_SINCE % 2)) -eq 0 ]; then
|
||||
echo "Release week (week $WEEKS_SINCE since anchor $ANCHOR)"
|
||||
echo "is_release_week=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Not a release week (week $WEEKS_SINCE since anchor $ANCHOR)"
|
||||
echo "is_release_week=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
echo "## Bi-weekly Check" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Is release week: ${{ steps.check.outputs.is_release_week }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- Manual trigger: ${{ github.event_name == 'workflow_dispatch' }}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
resolve-version:
|
||||
needs: check-release-week
|
||||
if: needs.check-release-week.outputs.is_release_week == 'true' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
current_version: ${{ steps.resolve.outputs.current_version }}
|
||||
@@ -131,8 +163,8 @@ jobs:
|
||||
echo "- [View workflow runs](https://github.com/Comfy-Org/ComfyUI_frontend/actions/workflows/release-version-bump.yaml)" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
create-comfyui-pr:
|
||||
needs: [resolve-version, trigger-release-if-needed]
|
||||
if: always() && needs.resolve-version.result == 'success'
|
||||
needs: [check-release-week, resolve-version, trigger-release-if-needed]
|
||||
if: always() && needs.resolve-version.result == 'success' && (needs.check-release-week.outputs.is_release_week == 'true' || github.event_name == 'workflow_dispatch')
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -231,7 +263,7 @@ jobs:
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Create/update branch (reuse same branch name each week)
|
||||
# Create/update branch (reuse same branch name each release cycle)
|
||||
BRANCH="automation/comfyui-frontend-bump"
|
||||
git checkout -B "$BRANCH"
|
||||
git add requirements.txt
|
||||
@@ -243,7 +275,7 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Force push to fork (overwrites previous week's branch)
|
||||
# Force push to fork (overwrites previous release cycle's branch)
|
||||
# Note: This intentionally destroys branch history to maintain a single PR
|
||||
# Any review comments or manual commits will need to be re-applied
|
||||
if ! git push -f origin "$BRANCH"; then
|
||||
13
AGENTS.md
@@ -266,3 +266,16 @@ When referencing Comfy-Org repos:
|
||||
- Always use `import { cn } from '@/utils/tailwindUtil'`
|
||||
- e.g. `<div :class="cn('text-node-component-header-icon', hasError && 'text-danger')" />`
|
||||
- Use `cn()` inline in the template when feasible instead of creating a `computed` to hold the value
|
||||
- NEVER use `!important` or the `!` important prefix for tailwind classes
|
||||
- Find existing `!important` classes that are interfering with the styling and propose corrections of those instead.
|
||||
|
||||
## Agent-only rules
|
||||
|
||||
Rules for agent-based coding tasks.
|
||||
|
||||
### Temporary Files
|
||||
|
||||
- Put planning documents under `/temp/plans/`
|
||||
- Put scripts used under `/temp/scripts/`
|
||||
- Put summaries of work performed under `/temp/summaries/`
|
||||
- Put TODOs and status updates under `/temp/in_progress/`
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
# Mask Editor
|
||||
/src/extensions/core/maskeditor.ts @trsommer @brucew4yn3rp
|
||||
/src/extensions/core/maskEditorLayerFilenames.ts @trsommer @brucew4yn3rp
|
||||
/src/extensions/core/maskEditorOld.ts @trsommer @brucew4yn3rp
|
||||
|
||||
# 3D
|
||||
/src/extensions/core/load3d.ts @jtydhr88
|
||||
|
||||
@@ -181,6 +181,10 @@ pnpm format
|
||||
- Use Tailwind CSS classes instead of custom CSS
|
||||
- NEVER use `dark:` or `dark-theme:` tailwind variants. Instead use a semantic value from the [style.css](packages/design-system/src/css/style.css) like `bg-node-component-surface`
|
||||
|
||||
## Design Team Approval (Required for Notable UI Changes)
|
||||
|
||||
Changes that materially affect the default UI must be approved or requested by our design team before they can be merged. This is generally a blocking requirement and applies to internal contributors and OSS contributors alike.
|
||||
|
||||
### Internationalization
|
||||
|
||||
- All user-facing strings must use vue-i18n
|
||||
|
||||
14
README.md
@@ -33,11 +33,11 @@
|
||||
|
||||
The project follows a structured release process for each minor version, consisting of three distinct phases:
|
||||
|
||||
1. **Development Phase** - 1 week
|
||||
1. **Development Phase** - 2 weeks
|
||||
- Active development of new features
|
||||
- Code changes merged to the development branch
|
||||
|
||||
2. **Feature Freeze** - 1 week
|
||||
2. **Feature Freeze** - 2 weeks
|
||||
- No new features accepted
|
||||
- Only bug fixes are cherry-picked to the release branch
|
||||
- Testing and stabilization of the codebase
|
||||
@@ -56,16 +56,16 @@ To use the latest nightly release, add the following command line argument to yo
|
||||
```
|
||||
|
||||
## Overlapping Release Cycles
|
||||
The development of successive minor versions overlaps. For example, while version 1.1 is in feature freeze, development for version 1.2 begins simultaneously.
|
||||
The development of successive minor versions overlaps. For example, while version 1.1 is in feature freeze, development for version 1.2 begins simultaneously. Each feature has approximately 4 weeks from merge to ComfyUI stable release (2 weeks on main, 2 weeks frozen on RC).
|
||||
|
||||
### Example Release Cycle
|
||||
|
||||
| Week | Date Range | Version 1.1 | Version 1.2 | Version 1.3 | Patch Releases |
|
||||
|------|------------|-------------|-------------|-------------|----------------|
|
||||
| 1 | Mar 1-7 | Development | - | - | - |
|
||||
| 2 | Mar 8-14 | Feature Freeze | Development | - | 1.1.0 through 1.1.6 (daily) |
|
||||
| 3 | Mar 15-21 | Released | Feature Freeze | Development | 1.1.7 through 1.1.13 (daily)<br>1.2.0 through 1.2.6 (daily) |
|
||||
| 4 | Mar 22-28 | - | Released | Feature Freeze | 1.2.7 through 1.2.13 (daily)<br>1.3.0 through 1.3.6 (daily) |
|
||||
| 1-2 | Mar 1-14 | Development | - | - | - |
|
||||
| 3-4 | Mar 15-28 | Feature Freeze | Development | - | 1.1.0 through 1.1.13 (daily) |
|
||||
| 5-6 | Mar 29-Apr 11 | Released | Feature Freeze | Development | 1.1.14+ (daily)<br>1.2.0 through 1.2.13 (daily) |
|
||||
| 7-8 | Apr 12-25 | - | Released | Feature Freeze | 1.2.14+ (daily)<br>1.3.0 through 1.3.13 (daily) |
|
||||
|
||||
## Release Summary
|
||||
|
||||
|
||||
@@ -26,10 +26,9 @@ export class ComfyTemplates {
|
||||
}
|
||||
|
||||
async loadTemplate(id: string) {
|
||||
await this.content
|
||||
.getByTestId(`template-workflow-${id}`)
|
||||
.getByRole('img')
|
||||
.click()
|
||||
const templateCard = this.content.getByTestId(`template-workflow-${id}`)
|
||||
await templateCard.scrollIntoViewIfNeeded()
|
||||
await templateCard.getByRole('img').click()
|
||||
}
|
||||
|
||||
async getAllTemplates(): Promise<TemplateInfo[]> {
|
||||
|
||||
@@ -171,7 +171,8 @@ test.describe('Color Palette', () => {
|
||||
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
|
||||
await comfyPage.nextFrame()
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('default-color-palette.png')
|
||||
// INTENTIONALLY BROKEN: This should fail - expecting wrong screenshot
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('WRONG-SCREENSHOT-NAME.png')
|
||||
})
|
||||
|
||||
test('Can add custom color palette', async ({ comfyPage }) => {
|
||||
|
||||
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 20 KiB |
@@ -553,12 +553,6 @@ This is English documentation.
|
||||
)
|
||||
await selectNodeWithPan(comfyPage, checkpointNodes[0])
|
||||
|
||||
// Click help button again
|
||||
const helpButton2 = comfyPage.page.locator(
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
)
|
||||
await helpButton2.click()
|
||||
|
||||
// Content should update
|
||||
await expect(helpPage).toContainText('Checkpoint Loader Help')
|
||||
await expect(helpPage).toContainText(
|
||||
|
||||
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 97 KiB |
@@ -109,14 +109,14 @@ test.describe('Templates', () => {
|
||||
})
|
||||
|
||||
test('Uses proper locale files for templates', async ({ comfyPage }) => {
|
||||
// Set locale to French before opening templates
|
||||
await comfyPage.setSetting('Comfy.Locale', 'fr')
|
||||
|
||||
// Load the templates dialog and wait for the French index file request
|
||||
const requestPromise = comfyPage.page.waitForRequest(
|
||||
'**/templates/index.fr.json'
|
||||
)
|
||||
|
||||
// Set locale to French before opening templates
|
||||
await comfyPage.setSetting('Comfy.Locale', 'fr')
|
||||
|
||||
await comfyPage.executeCommand('Comfy.BrowseTemplates')
|
||||
|
||||
const request = await requestPromise
|
||||
|
||||
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 81 KiB |
@@ -205,6 +205,32 @@ test.describe('Image widget', () => {
|
||||
const filename = await fileComboWidget.getValue()
|
||||
expect(filename).toBe('image32x32.webp')
|
||||
})
|
||||
test('Displays buttons when viewing single image of batch', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const [x, y] = await comfyPage.page.evaluate(() => {
|
||||
const src =
|
||||
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='768' height='512' viewBox='0 0 1 1'%3E%3Crect width='1' height='1' stroke='black'/%3E%3C/svg%3E"
|
||||
const image1 = new Image()
|
||||
image1.src = src
|
||||
const image2 = new Image()
|
||||
image2.src = src
|
||||
const targetNode = graph.nodes[6]
|
||||
targetNode.imgs = [image1, image2]
|
||||
targetNode.imageIndex = 1
|
||||
app.canvas.setDirty(true)
|
||||
|
||||
const x = targetNode.pos[0] + targetNode.size[0] - 41
|
||||
const y = targetNode.pos[1] + targetNode.widgets.at(-1).last_y + 30
|
||||
return app.canvasPosToClientPos([x, y])
|
||||
})
|
||||
|
||||
const clip = { x, y, width: 35, height: 35 }
|
||||
await expect(comfyPage.page).toHaveScreenshot(
|
||||
'image_preview_close_button.png',
|
||||
{ clip }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
test.describe('Animated image widget', () => {
|
||||
@@ -262,13 +288,7 @@ test.describe('Animated image widget', () => {
|
||||
expect(filename).toContain('animated_webp.webp')
|
||||
})
|
||||
|
||||
// FIXME: This test keeps flip-flopping because it relies on animated webp timing,
|
||||
// which is inherently unreliable in CI environments. The test asset is an animated
|
||||
// webp with 2 frames, and the test depends on animation frame timing to verify that
|
||||
// animated webp images are properly displayed (as opposed to being treated as static webp).
|
||||
// While the underlying functionality works (animated webp are correctly distinguished
|
||||
// from static webp), the test is flaky due to timing dependencies with webp animation frames.
|
||||
test.fixme('Can preview saved animated webp image', async ({ comfyPage }) => {
|
||||
test('Can preview saved animated webp image', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('widgets/save_animated_webp')
|
||||
|
||||
// Get position of the load animated webp node
|
||||
@@ -295,18 +315,13 @@ test.describe('Animated image widget', () => {
|
||||
([loadId, saveId]) => {
|
||||
// Set the output of the SaveAnimatedWEBP node to equal the loader node's image
|
||||
window['app'].nodeOutputs[saveId] = window['app'].nodeOutputs[loadId]
|
||||
app.canvas.setDirty(true)
|
||||
},
|
||||
[loadAnimatedWebpNode.id, saveAnimatedWebpNode.id]
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Move mouse and click on canvas to trigger render
|
||||
await comfyPage.page.mouse.click(64, 64)
|
||||
|
||||
// Expect the SaveAnimatedWEBP node to have an output preview
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'animated_image_preview_saved_webp.png'
|
||||
)
|
||||
await expect(
|
||||
comfyPage.page.locator('.dom-widget').locator('img')
|
||||
).toHaveCount(2)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 423 B |
@@ -9,11 +9,7 @@ interface ShimResult {
|
||||
const SKIP_WARNING_FILES = new Set(['scripts/app', 'scripts/api'])
|
||||
|
||||
/** Files that will be removed in v1.34 */
|
||||
const DEPRECATED_FILES = [
|
||||
'scripts/ui',
|
||||
'extensions/core/maskEditorOld',
|
||||
'extensions/core/groupNode'
|
||||
] as const
|
||||
const DEPRECATED_FILES = ['scripts/ui', 'extensions/core/groupNode'] as const
|
||||
|
||||
function getWarningMessage(
|
||||
fileKey: string,
|
||||
|
||||
@@ -41,7 +41,6 @@ The following table lists ALL core extensions in the system as of 2025-01-30:
|
||||
| groupOptions.ts | Handles group node configuration options | Graph |
|
||||
| index.ts | Main extension registration and coordination | Core |
|
||||
| load3d.ts | Supports 3D model loading and visualization | 3D |
|
||||
| maskEditorOld.ts | Legacy mask editor implementation | Image |
|
||||
| maskeditor.ts | Implements the mask editor for image masking operations | Image |
|
||||
| nodeTemplates.ts | Provides node template functionality | Templates |
|
||||
| noteNode.ts | Adds note nodes for documentation within workflows | Graph |
|
||||
@@ -178,4 +177,4 @@ For more detailed information about ComfyUI's extension system, refer to the off
|
||||
- [JavaScript Settings](https://docs.comfy.org/custom-nodes/js/javascript_settings)
|
||||
- [JavaScript Examples](https://docs.comfy.org/custom-nodes/js/javascript_examples)
|
||||
|
||||
Also, check the main [README.md](https://github.com/Comfy-Org/ComfyUI_frontend#developer-apis) section on Developer APIs for the latest information on extension APIs and features.
|
||||
Also, check the main [README.md](https://github.com/Comfy-Org/ComfyUI_frontend#developer-apis) section on Developer APIs for the latest information on extension APIs and features.
|
||||
|
||||
@@ -4,7 +4,10 @@ import pluginI18n from '@intlify/eslint-plugin-vue-i18n'
|
||||
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'
|
||||
import { importX } from 'eslint-plugin-import-x'
|
||||
import oxlint from 'eslint-plugin-oxlint'
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
|
||||
// WORKAROUND: eslint-plugin-prettier causes segfault on Node.js 24 + Windows
|
||||
// See: https://github.com/nodejs/node/issues/58690
|
||||
// Prettier is still run separately in lint-staged, so this is safe to disable
|
||||
import eslintConfigPrettier from 'eslint-config-prettier'
|
||||
import { configs as storybookConfigs } from 'eslint-plugin-storybook'
|
||||
import unusedImports from 'eslint-plugin-unused-imports'
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
@@ -108,7 +111,8 @@ export default defineConfig([
|
||||
tseslintConfigs.recommended,
|
||||
// Difference in typecheck on CI vs Local
|
||||
pluginVue.configs['flat/recommended'],
|
||||
eslintPluginPrettierRecommended,
|
||||
// Use eslint-config-prettier instead of eslint-plugin-prettier to avoid Node 24 segfault
|
||||
eslintConfigPrettier,
|
||||
// @ts-expect-error Type incompatibility between storybook plugin and ESLint config types
|
||||
storybookConfigs['flat/recommended'],
|
||||
// @ts-expect-error Type incompatibility between import-x plugin and ESLint config types
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.36.3",
|
||||
"version": "1.36.8",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -19,6 +19,7 @@
|
||||
"dev:cloud": "cross-env DEV_SERVER_COMFYUI_URL='https://testcloud.comfy.org/' nx serve",
|
||||
"dev:desktop": "nx dev @comfyorg/desktop-ui",
|
||||
"dev:electron": "nx serve --config vite.electron.config.mts",
|
||||
"dev:no-vue": "cross-env DISABLE_VUE_PLUGINS=true nx serve",
|
||||
"dev": "nx serve",
|
||||
"devtools:pycheck": "python3 -m compileall -q tools/devtools",
|
||||
"format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
|
||||
@@ -84,7 +85,6 @@
|
||||
"eslint-import-resolver-typescript": "catalog:",
|
||||
"eslint-plugin-import-x": "catalog:",
|
||||
"eslint-plugin-oxlint": "catalog:",
|
||||
"eslint-plugin-prettier": "catalog:",
|
||||
"eslint-plugin-storybook": "catalog:",
|
||||
"eslint-plugin-unused-imports": "catalog:",
|
||||
"eslint-plugin-vue": "catalog:",
|
||||
@@ -146,6 +146,7 @@
|
||||
"@primevue/icons": "catalog:",
|
||||
"@primevue/themes": "catalog:",
|
||||
"@sentry/vue": "catalog:",
|
||||
"@sparkjsdev/spark": "catalog:",
|
||||
"@tiptap/core": "^2.10.4",
|
||||
"@tiptap/extension-link": "^2.10.4",
|
||||
"@tiptap/extension-table": "^2.10.4",
|
||||
|
||||
301
packages/registry-types/src/comfyRegistryTypes.ts
generated
@@ -217,6 +217,28 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/verify-api-key": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* Verify a ComfyUI API key and return customer details
|
||||
* @description Validates a ComfyUI API key and returns the associated customer information.
|
||||
* This endpoint is used by cloud.comfy.org to authenticate users via API keys
|
||||
* instead of Firebase tokens.
|
||||
*/
|
||||
post: operations["VerifyApiKey"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/customers/{customer_id}/cloud-subscription-status": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -2154,6 +2176,26 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/proxy/bfl/flux-2-max/generate": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* Proxy request to BFL Flux 2 Max for image generation
|
||||
* @description Forwards image generation requests to BFL's Flux 2 Max API and returns the results. Supports image-to-image generation with up to 8 input images.
|
||||
*/
|
||||
post: operations["bflFlux2MaxGenerate"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/proxy/bfl/flux-pro-1.0-expand/generate": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -3911,6 +3953,11 @@ export interface components {
|
||||
* @enum {string}
|
||||
*/
|
||||
SubscriptionTier: "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION";
|
||||
/**
|
||||
* @description The subscription billing duration
|
||||
* @enum {string}
|
||||
*/
|
||||
SubscriptionDuration: "MONTHLY" | "ANNUAL";
|
||||
FeaturesResponse: {
|
||||
/**
|
||||
* @description The conversion rate for partner nodes
|
||||
@@ -4757,13 +4804,13 @@ export interface components {
|
||||
* @default kling-v1
|
||||
* @enum {string}
|
||||
*/
|
||||
KlingTextToVideoModelName: "kling-v1" | "kling-v1-5" | "kling-v1-6" | "kling-v2-master" | "kling-v2-1-master" | "kling-v2-5-turbo";
|
||||
KlingTextToVideoModelName: "kling-v1" | "kling-v1-5" | "kling-v1-6" | "kling-v2-master" | "kling-v2-1-master" | "kling-v2-5-turbo" | "kling-v2-6";
|
||||
/**
|
||||
* @description Model Name
|
||||
* @default kling-v2-master
|
||||
* @enum {string}
|
||||
*/
|
||||
KlingVideoGenModelName: "kling-v1" | "kling-v1-5" | "kling-v1-6" | "kling-v2-master" | "kling-v2-1" | "kling-v2-1-master" | "kling-v2-5-turbo";
|
||||
KlingVideoGenModelName: "kling-v1" | "kling-v1-5" | "kling-v1-6" | "kling-v2-master" | "kling-v2-1" | "kling-v2-1-master" | "kling-v2-5-turbo" | "kling-v2-6";
|
||||
/**
|
||||
* @description Video generation mode. std: Standard Mode, which is cost-effective. pro: Professional Mode, generates videos with longer duration but higher quality output.
|
||||
* @default std
|
||||
@@ -4908,6 +4955,12 @@ export interface components {
|
||||
camera_control?: components["schemas"]["KlingCameraControl"];
|
||||
aspect_ratio?: components["schemas"]["KlingVideoGenAspectRatio"];
|
||||
duration?: components["schemas"]["KlingVideoGenDuration"];
|
||||
/**
|
||||
* @description Whether to generate sound simultaneously when generating videos. Only V2.6 and subsequent versions of the model support this parameter.
|
||||
* @default off
|
||||
* @enum {string}
|
||||
*/
|
||||
sound: "on" | "off";
|
||||
/**
|
||||
* Format: uri
|
||||
* @description The callback notification address
|
||||
@@ -4970,6 +5023,12 @@ export interface components {
|
||||
camera_control?: components["schemas"]["KlingCameraControl"];
|
||||
aspect_ratio?: components["schemas"]["KlingVideoGenAspectRatio"];
|
||||
duration?: components["schemas"]["KlingVideoGenDuration"];
|
||||
/**
|
||||
* @description Whether to generate sound simultaneously when generating videos. Only V2.6 and subsequent versions of the model support this parameter.
|
||||
* @default off
|
||||
* @enum {string}
|
||||
*/
|
||||
sound: "on" | "off";
|
||||
/**
|
||||
* Format: uri
|
||||
* @description The callback notification address. Server will notify when the task status changes.
|
||||
@@ -5759,7 +5818,7 @@ export interface components {
|
||||
width: number;
|
||||
/**
|
||||
* @description Height of the image.
|
||||
* @default 768
|
||||
* @default 1024
|
||||
*/
|
||||
height: number;
|
||||
/** @description Seed for reproducibility. */
|
||||
@@ -5775,6 +5834,11 @@ export interface components {
|
||||
* @enum {string}
|
||||
*/
|
||||
output_format: "jpeg" | "png";
|
||||
/**
|
||||
* @description Moderation tolerance level (Flux 2 Max only).
|
||||
* @default 2
|
||||
*/
|
||||
safety_tolerance: number;
|
||||
};
|
||||
/** FluxProFillInputs */
|
||||
BFLFluxProFillInputs: {
|
||||
@@ -6973,6 +7037,10 @@ export interface components {
|
||||
image_tokens?: number;
|
||||
};
|
||||
output_tokens?: number;
|
||||
output_tokens_details?: {
|
||||
text_tokens?: number;
|
||||
image_tokens?: number;
|
||||
};
|
||||
total_tokens?: number;
|
||||
};
|
||||
};
|
||||
@@ -10356,40 +10424,76 @@ export interface components {
|
||||
* @description The ID of the model to call
|
||||
* @enum {string}
|
||||
*/
|
||||
model: "wan2.5-t2v-preview" | "wan2.5-i2v-preview";
|
||||
model: "wan2.5-t2v-preview" | "wan2.5-i2v-preview" | "wan2.6-t2v" | "wan2.6-i2v";
|
||||
/** @description Enter basic information, such as prompt words, etc. */
|
||||
input: {
|
||||
/** @description Text prompt words. Support Chinese and English, length not exceeding 800 characters */
|
||||
/**
|
||||
* @description Text prompt words. Support Chinese and English, length not exceeding 800 characters.
|
||||
* For wan2.6-r2v with multiple reference videos, use 'character1', 'character2', etc. to refer to subjects
|
||||
* in the order of reference videos. Example: "Character1 sings on the roadside, Character2 dances beside it"
|
||||
*/
|
||||
prompt: string;
|
||||
/** @description Reverse prompt words are used to describe content that you do not want to see in the video screen */
|
||||
negative_prompt?: string;
|
||||
/** @description Audio file download URL. Supported formats: mp3 and wav. */
|
||||
/** @description Audio file download URL. Supported formats: mp3 and wav. Cannot be used with reference_video_urls. */
|
||||
audio_url?: string;
|
||||
/** @description First frame image URL or Base64 encoded data. Required for I2V models. Image formats: JPEG, JPG, PNG, BMP, WEBP. Resolution: 360-2000 pixels. File size: max 10MB. */
|
||||
img_url?: string;
|
||||
/** @description Video effect template name. Optional. Currently supported: squish, flying, carousel. When used, prompt parameter is ignored. */
|
||||
template?: string;
|
||||
/**
|
||||
* @description Reference video URLs for wan2.6-r2v model only. Array of 1-3 video URLs.
|
||||
* Input restrictions:
|
||||
* - Format: mp4, mov
|
||||
* - Quantity: 1-3 videos
|
||||
* - Single video length: 2-30 seconds
|
||||
* - Single file size: max 30MB
|
||||
* - Cannot be used with audio_url
|
||||
* Reference duration: Single video max 5s, two videos max 2.5s each, three videos proportionally less.
|
||||
* Billing: Based on actual reference duration used.
|
||||
*/
|
||||
reference_video_urls?: string[];
|
||||
};
|
||||
/** @description Video processing parameters */
|
||||
parameters?: {
|
||||
/** @description Used to specify the video resolution in the format of 宽*高. Supported resolutions vary by model (for T2V models) */
|
||||
/**
|
||||
* @description Video resolution in format width*height. Supported resolutions vary by model:
|
||||
* For wan2.5 T2V: 480P (480*832, 832*480, 624*624), 720P, 1080P sizes
|
||||
* For wan2.6 T2V/R2V (no 480P):
|
||||
* 720P: 1280*720, 720*1280, 960*960, 1088*832, 832*1088
|
||||
* 1080P: 1920*1080, 1080*1920, 1440*1440, 1632*1248, 1248*1632
|
||||
*/
|
||||
size?: string;
|
||||
/**
|
||||
* @description Resolution level for I2V models. Supported values vary by model: 480P, 720P, 1080P
|
||||
* @description Resolution level for I2V models. Supported values vary by model:
|
||||
* - wan2.5-i2v-preview: 480P, 720P, 1080P
|
||||
* - wan2.6-i2v: 720P, 1080P only (no 480P support)
|
||||
* @enum {string}
|
||||
*/
|
||||
resolution?: "480P" | "720P" | "1080P";
|
||||
/**
|
||||
* @description The duration of the video generated, in seconds
|
||||
* @description The duration of the video generated, in seconds:
|
||||
* - wan2.5 models: 5 or 10 seconds
|
||||
* - wan2.6-t2v, wan2.6-i2v: 5, 10, or 15 seconds
|
||||
* - wan2.6-r2v: 5 or 10 seconds only (no 15s support)
|
||||
* @default 5
|
||||
* @enum {integer}
|
||||
*/
|
||||
duration?: 5 | 10;
|
||||
duration?: 5 | 10 | 15;
|
||||
/**
|
||||
* @description Is it enabled prompt intelligent rewriting. Default is true
|
||||
* @default true
|
||||
*/
|
||||
prompt_extend?: boolean;
|
||||
/**
|
||||
* @description Intelligent multi-lens control. Only active when prompt_extend is enabled.
|
||||
* For wan2.6 models only.
|
||||
* - multi: Intelligent disassembly into multiple lenses (default)
|
||||
* - single: Single lens generation
|
||||
* @default multi
|
||||
* @enum {string}
|
||||
*/
|
||||
shot_type?: "multi" | "single";
|
||||
/** @description Random number seed, used to control the randomness of the model generated content */
|
||||
seed?: number;
|
||||
/**
|
||||
@@ -11806,6 +11910,8 @@ export interface operations {
|
||||
"application/json": {
|
||||
/** @description Optional URL to redirect the customer after they're done with the billing portal */
|
||||
return_url?: string;
|
||||
/** @description Optional target subscription tier. When provided, creates a deep link directly to the subscription update confirmation screen with this tier pre-selected. */
|
||||
target_tier?: "standard" | "creator" | "pro" | "standard-yearly" | "creator-yearly" | "pro-yearly";
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -11902,8 +12008,8 @@ export interface operations {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
/** @description The subscription tier (standard, creator, or pro) */
|
||||
tier: "standard" | "creator" | "pro";
|
||||
/** @description The subscription tier (standard, creator, or pro) with optional yearly billing (standard-yearly, creator-yearly, pro-yearly) */
|
||||
tier: "standard" | "creator" | "pro" | "standard-yearly" | "creator-yearly" | "pro-yearly";
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
@@ -11969,6 +12075,7 @@ export interface operations {
|
||||
/** @description The active subscription ID if one exists */
|
||||
subscription_id?: string | null;
|
||||
subscription_tier?: components["schemas"]["SubscriptionTier"] | null;
|
||||
subscription_duration?: components["schemas"]["SubscriptionDuration"] | null;
|
||||
/** @description Whether the customer has funds/credits available */
|
||||
has_fund?: boolean;
|
||||
/**
|
||||
@@ -12002,6 +12109,72 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
VerifyApiKey: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header: {
|
||||
/** @description Admin API secret used to authorize this request */
|
||||
"X-Comfy-Admin-Secret": string;
|
||||
};
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": {
|
||||
/** @description The ComfyUI API key to verify (e.g., comfy_xxx...) */
|
||||
api_key: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description API key is valid */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
/** @description Whether the API key is valid */
|
||||
valid: boolean;
|
||||
/** @description The Firebase UID of the user */
|
||||
firebase_uid: string;
|
||||
/** @description The customer's email address */
|
||||
email?: string;
|
||||
/** @description The customer's name */
|
||||
name?: string;
|
||||
/** @description Whether the customer is an admin */
|
||||
is_admin?: boolean;
|
||||
};
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized or missing admin API secret */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description API key not found or invalid */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Internal server error */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
GetAdminCustomerCloudSubscriptionStatus: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -12029,6 +12202,7 @@ export interface operations {
|
||||
/** @description The active subscription ID if one exists */
|
||||
subscription_id?: string | null;
|
||||
subscription_tier?: components["schemas"]["SubscriptionTier"] | null;
|
||||
subscription_duration?: components["schemas"]["SubscriptionDuration"] | null;
|
||||
/** @description Whether the customer has funds/credits available */
|
||||
has_fund?: boolean;
|
||||
/**
|
||||
@@ -12146,6 +12320,16 @@ export interface operations {
|
||||
* @description The remaining balance from cloud credits in microamount
|
||||
*/
|
||||
cloud_credit_balance_micros?: number;
|
||||
/**
|
||||
* Format: double
|
||||
* @description The total amount of pending/unbilled charges from draft invoices in microamount. Only included when the show_negative_balances feature flag is enabled.
|
||||
*/
|
||||
pending_charges_micros?: number;
|
||||
/**
|
||||
* Format: double
|
||||
* @description The effective balance (total balance minus pending charges). Can be negative if pending charges exceed the balance. Only included when the show_negative_balances feature flag is enabled.
|
||||
*/
|
||||
effective_balance_micros?: number;
|
||||
/** @description The currency code (e.g., "usd") */
|
||||
currency: string;
|
||||
};
|
||||
@@ -12212,6 +12396,16 @@ export interface operations {
|
||||
* @description The remaining balance from cloud credits in microamount
|
||||
*/
|
||||
cloud_credit_balance_micros?: number;
|
||||
/**
|
||||
* Format: double
|
||||
* @description The total amount of pending/unbilled charges from draft invoices in microamount. Only included when the show_negative_balances feature flag is enabled.
|
||||
*/
|
||||
pending_charges_micros?: number;
|
||||
/**
|
||||
* Format: double
|
||||
* @description The effective balance (total balance minus pending charges). Can be negative if pending charges exceed the balance. Only included when the show_negative_balances feature flag is enabled.
|
||||
*/
|
||||
effective_balance_micros?: number;
|
||||
/** @description The currency code (e.g., "usd") */
|
||||
currency: string;
|
||||
};
|
||||
@@ -19417,6 +19611,89 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
bflFlux2MaxGenerate: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["BFLFlux2ProGenerateRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful response from BFL Flux 2 Max proxy */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["BFLFluxProGenerateResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Bad Request (invalid input to proxy) */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description Payment Required */
|
||||
402: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description Rate limit exceeded (either from proxy or BFL) */
|
||||
429: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Internal Server Error (proxy or upstream issue) */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Bad Gateway (error communicating with BFL) */
|
||||
502: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Gateway Timeout (BFL took too long to respond) */
|
||||
504: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ErrorResponse"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
BFLExpand_v1_flux_pro_1_0_expand_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import clsx, { type ClassArray } from 'clsx'
|
||||
import { clsx } from 'clsx'
|
||||
import type { ClassArray } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export type { ClassValue } from 'clsx'
|
||||
|
||||
232
pnpm-lock.yaml
generated
@@ -46,8 +46,8 @@ catalogs:
|
||||
specifier: ^0.1.5
|
||||
version: 0.1.5
|
||||
'@playwright/test':
|
||||
specifier: ^1.52.0
|
||||
version: 1.52.0
|
||||
specifier: ^1.57.0
|
||||
version: 1.57.0
|
||||
'@prettier/plugin-oxc':
|
||||
specifier: ^0.1.3
|
||||
version: 0.1.3
|
||||
@@ -78,6 +78,9 @@ catalogs:
|
||||
'@sentry/vue':
|
||||
specifier: ^8.48.0
|
||||
version: 8.48.0
|
||||
'@sparkjsdev/spark':
|
||||
specifier: ^0.1.10
|
||||
version: 0.1.10
|
||||
'@storybook/addon-docs':
|
||||
specifier: ^10.1.9
|
||||
version: 10.1.9
|
||||
@@ -159,9 +162,6 @@ catalogs:
|
||||
eslint-plugin-oxlint:
|
||||
specifier: 1.25.0
|
||||
version: 1.25.0
|
||||
eslint-plugin-prettier:
|
||||
specifier: ^5.5.4
|
||||
version: 5.5.4
|
||||
eslint-plugin-storybook:
|
||||
specifier: ^10.1.9
|
||||
version: 10.1.9
|
||||
@@ -265,14 +265,14 @@ catalogs:
|
||||
specifier: ^8.49.0
|
||||
version: 8.49.0
|
||||
unplugin-icons:
|
||||
specifier: ^0.22.0
|
||||
version: 0.22.0
|
||||
specifier: ^22.5.0
|
||||
version: 22.5.0
|
||||
unplugin-typegpu:
|
||||
specifier: 0.8.0
|
||||
version: 0.8.0
|
||||
unplugin-vue-components:
|
||||
specifier: ^0.28.0
|
||||
version: 0.28.0
|
||||
specifier: ^30.0.0
|
||||
version: 30.0.0
|
||||
vite:
|
||||
specifier: ^7.3.0
|
||||
version: 7.3.0
|
||||
@@ -377,6 +377,9 @@ importers:
|
||||
'@sentry/vue':
|
||||
specifier: 'catalog:'
|
||||
version: 8.48.0(pinia@2.2.2(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)))(vue@3.5.13(typescript@5.9.3))
|
||||
'@sparkjsdev/spark':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.10
|
||||
'@tiptap/core':
|
||||
specifier: ^2.10.4
|
||||
version: 2.10.4(@tiptap/pm@2.10.4)
|
||||
@@ -518,7 +521,7 @@ importers:
|
||||
version: 22.2.6(@babel/traverse@7.28.5)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6)
|
||||
'@nx/playwright':
|
||||
specifier: 'catalog:'
|
||||
version: 22.2.6(@babel/traverse@7.28.5)(@playwright/test@1.52.0)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6)
|
||||
version: 22.2.6(@babel/traverse@7.28.5)(@playwright/test@1.57.0)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6)
|
||||
'@nx/storybook':
|
||||
specifier: 'catalog:'
|
||||
version: 22.2.4(@babel/traverse@7.28.5)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6)(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)
|
||||
@@ -530,7 +533,7 @@ importers:
|
||||
version: 0.1.5(pinia@2.2.2(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)))(vue@3.5.13(typescript@5.9.3))
|
||||
'@playwright/test':
|
||||
specifier: 'catalog:'
|
||||
version: 1.52.0
|
||||
version: 1.57.0
|
||||
'@prettier/plugin-oxc':
|
||||
specifier: 'catalog:'
|
||||
version: 0.1.3
|
||||
@@ -600,9 +603,6 @@ importers:
|
||||
eslint-plugin-oxlint:
|
||||
specifier: 'catalog:'
|
||||
version: 1.25.0
|
||||
eslint-plugin-prettier:
|
||||
specifier: 'catalog:'
|
||||
version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.4)
|
||||
eslint-plugin-storybook:
|
||||
specifier: 'catalog:'
|
||||
version: 10.1.9(eslint@9.39.1(jiti@2.6.1))(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)
|
||||
@@ -692,13 +692,13 @@ importers:
|
||||
version: 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
unplugin-icons:
|
||||
specifier: 'catalog:'
|
||||
version: 0.22.0(@vue/compiler-sfc@3.5.25)
|
||||
version: 22.5.0(@vue/compiler-sfc@3.5.25)
|
||||
unplugin-typegpu:
|
||||
specifier: 'catalog:'
|
||||
version: 0.8.0(typegpu@0.8.2)
|
||||
unplugin-vue-components:
|
||||
specifier: 'catalog:'
|
||||
version: 0.28.0(@babel/parser@7.28.5)(rollup@4.53.5)(vue@3.5.13(typescript@5.9.3))
|
||||
version: 30.0.0(@babel/parser@7.28.5)(vue@3.5.13(typescript@5.9.3))
|
||||
uuid:
|
||||
specifier: ^11.1.0
|
||||
version: 11.1.0
|
||||
@@ -780,10 +780,10 @@ importers:
|
||||
version: 16.6.1
|
||||
unplugin-icons:
|
||||
specifier: 'catalog:'
|
||||
version: 0.22.0(@vue/compiler-sfc@3.5.25)
|
||||
version: 22.5.0(@vue/compiler-sfc@3.5.25)
|
||||
unplugin-vue-components:
|
||||
specifier: 'catalog:'
|
||||
version: 0.28.0(@babel/parser@7.28.5)(rollup@4.53.5)(vue@3.5.13(typescript@5.9.3))
|
||||
version: 30.0.0(@babel/parser@7.28.5)(vue@3.5.13(typescript@5.9.3))
|
||||
vite:
|
||||
specifier: 'catalog:'
|
||||
version: 7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
@@ -907,18 +907,9 @@ packages:
|
||||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@antfu/install-pkg@0.5.0':
|
||||
resolution: {integrity: sha512-dKnk2xlAyC7rvTkpkHmu+Qy/2Zc3Vm/l8PtNyIOGDBtXPY3kThfU4ORNEp3V7SXw5XSOb+tOJaUYpfquPzL/Tg==}
|
||||
|
||||
'@antfu/install-pkg@1.1.0':
|
||||
resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==}
|
||||
|
||||
'@antfu/utils@0.7.10':
|
||||
resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==}
|
||||
|
||||
'@antfu/utils@8.1.1':
|
||||
resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==}
|
||||
|
||||
'@asamuzakjp/css-color@3.2.0':
|
||||
resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==}
|
||||
|
||||
@@ -2147,8 +2138,8 @@ packages:
|
||||
'@iconify/types@2.0.0':
|
||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||
|
||||
'@iconify/utils@2.3.0':
|
||||
resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==}
|
||||
'@iconify/utils@3.1.0':
|
||||
resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==}
|
||||
|
||||
'@internationalized/date@3.9.0':
|
||||
resolution: {integrity: sha512-yaN3brAnHRD+4KyyOsJyk49XUvj2wtbNACSqg0bz3u8t2VuzhC8Q5dfRnrSxjnnbDb+ienBnkn1TzQfE154vyg==}
|
||||
@@ -2787,8 +2778,8 @@ packages:
|
||||
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
|
||||
'@playwright/test@1.52.0':
|
||||
resolution: {integrity: sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==}
|
||||
'@playwright/test@1.57.0':
|
||||
resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
@@ -3127,6 +3118,9 @@ packages:
|
||||
'@sinclair/typebox@0.34.40':
|
||||
resolution: {integrity: sha512-gwBNIP8ZAYev/ORDWW0QvxdwPXwxBtLsdsJgSc7eDIRt8ubP+rxUBzPsrwnu16fgEF8Bx4lh/+mvQvJzcTM6Kw==}
|
||||
|
||||
'@sparkjsdev/spark@0.1.10':
|
||||
resolution: {integrity: sha512-CiijdZQuj7KPDUqIZPiEqyUkJCYo1JqR05vq/V+ElxMwqR7L70ZuZDyIKcasjZHSiPB8pGRMH8HZGqUKO9aRPQ==}
|
||||
|
||||
'@storybook/addon-docs@10.1.9':
|
||||
resolution: {integrity: sha512-SvwEZ32lyk5p3PRmE3pmfAhs4HMiVo5zxjTBVmK9kgz9zGgWCTlikb56tJ998hVe52CFyCvt3I9rkHeYMCKPww==}
|
||||
peerDependencies:
|
||||
@@ -4430,6 +4424,10 @@ packages:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
||||
chokidar@4.0.3:
|
||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||
engines: {node: '>= 14.16.0'}
|
||||
|
||||
chownr@3.0.0:
|
||||
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -5056,20 +5054,6 @@ packages:
|
||||
eslint-plugin-oxlint@1.25.0:
|
||||
resolution: {integrity: sha512-grS4KdR9FAxoQC+wMkepeQHL4osMhoYfUI11Pot6Gitqr4wWi+JZrX0Shr8Bs9fjdWhEjtaZIV6cr4mbfytmyw==}
|
||||
|
||||
eslint-plugin-prettier@5.5.4:
|
||||
resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
peerDependencies:
|
||||
'@types/eslint': '*'
|
||||
eslint: '>=8.0.0'
|
||||
eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0'
|
||||
prettier: '>=3.0.0'
|
||||
peerDependenciesMeta:
|
||||
'@types/eslint':
|
||||
optional: true
|
||||
eslint-config-prettier:
|
||||
optional: true
|
||||
|
||||
eslint-plugin-storybook@10.1.9:
|
||||
resolution: {integrity: sha512-2XCnHhu+9ShW8U/MsvnlT4ZkzADIPtlfYVD/GBBbs8loWu0x9IZ3EfNg1LEImjvvNVDhwpd5K04lK4CAP+2bWA==}
|
||||
peerDependencies:
|
||||
@@ -5170,8 +5154,8 @@ packages:
|
||||
resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
exsolve@1.0.7:
|
||||
resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==}
|
||||
exsolve@1.0.8:
|
||||
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
|
||||
|
||||
extend-shallow@2.0.1:
|
||||
resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==}
|
||||
@@ -5195,9 +5179,6 @@ packages:
|
||||
fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
|
||||
fast-diff@1.3.0:
|
||||
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
|
||||
|
||||
fast-glob@3.3.3:
|
||||
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
|
||||
engines: {node: '>=8.6.0'}
|
||||
@@ -6129,10 +6110,6 @@ packages:
|
||||
resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
local-pkg@0.5.1:
|
||||
resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
local-pkg@1.1.2:
|
||||
resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -6696,11 +6673,8 @@ packages:
|
||||
resolution: {integrity: sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
package-manager-detector@0.2.11:
|
||||
resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==}
|
||||
|
||||
package-manager-detector@1.3.0:
|
||||
resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==}
|
||||
package-manager-detector@1.6.0:
|
||||
resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
|
||||
|
||||
pako@1.0.11:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||
@@ -6810,13 +6784,13 @@ packages:
|
||||
pkg-types@2.3.0:
|
||||
resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==}
|
||||
|
||||
playwright-core@1.52.0:
|
||||
resolution: {integrity: sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==}
|
||||
playwright-core@1.57.0:
|
||||
resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
playwright@1.52.0:
|
||||
resolution: {integrity: sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==}
|
||||
playwright@1.57.0:
|
||||
resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
@@ -6858,10 +6832,6 @@ packages:
|
||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
||||
prettier-linter-helpers@1.0.0:
|
||||
resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
prettier@3.7.4:
|
||||
resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -7061,6 +7031,10 @@ packages:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
|
||||
readdirp@4.1.2:
|
||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||
engines: {node: '>= 14.18.0'}
|
||||
|
||||
recast@0.23.11:
|
||||
resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
|
||||
engines: {node: '>= 4'}
|
||||
@@ -7507,10 +7481,6 @@ packages:
|
||||
resolution: {integrity: sha512-2SG1TnJGjMkD4+gblONMGYSrwAzYi+ymOitD+Jb/iMYm57nH20PlkVeMQRah3yDMKEa0QQYUF/QPWpdW7C6zNg==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
|
||||
synckit@0.11.11:
|
||||
resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
|
||||
table@6.9.0:
|
||||
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@@ -7567,8 +7537,9 @@ packages:
|
||||
tinyexec@0.3.2:
|
||||
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
|
||||
|
||||
tinyexec@1.0.1:
|
||||
resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==}
|
||||
tinyexec@1.0.2:
|
||||
resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
|
||||
@@ -7772,8 +7743,8 @@ packages:
|
||||
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
||||
unplugin-icons@0.22.0:
|
||||
resolution: {integrity: sha512-CP+iZq5U7doOifer5bcM0jQ9t3Is7EGybIYt3myVxceI8Zuk8EZEpe1NPtJvh7iqMs1VdbK0L41t9+um9VuuLw==}
|
||||
unplugin-icons@22.5.0:
|
||||
resolution: {integrity: sha512-MBlMtT5RuMYZy4TZgqUL2OTtOdTUVsS1Mhj6G1pEzMlFJlEnq6mhUfoIt45gBWxHcsOdXJDWLg3pRZ+YmvAVWQ==}
|
||||
peerDependencies:
|
||||
'@svgr/core': '>=7.0.0'
|
||||
'@svgx/core': ^1.0.1
|
||||
@@ -7804,12 +7775,12 @@ packages:
|
||||
resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==}
|
||||
engines: {node: '>=20.19.0'}
|
||||
|
||||
unplugin-vue-components@0.28.0:
|
||||
resolution: {integrity: sha512-jiTGtJ3JsRFBjgvyilfrX7yUoGKScFgbdNw+6p6kEXU+Spf/rhxzgvdfuMcvhCcLmflB/dY3pGQshYBVGOUx7Q==}
|
||||
unplugin-vue-components@30.0.0:
|
||||
resolution: {integrity: sha512-4qVE/lwCgmdPTp6h0qsRN2u642tt4boBQtcpn4wQcWZAsr8TQwq+SPT3NDu/6kBFxzo/sSEK4ioXhOOBrXc3iw==}
|
||||
engines: {node: '>=14'}
|
||||
peerDependencies:
|
||||
'@babel/parser': ^7.15.8
|
||||
'@nuxt/kit': ^3.2.2
|
||||
'@nuxt/kit': ^3.2.2 || ^4.0.0
|
||||
vue: 2 || 3
|
||||
peerDependenciesMeta:
|
||||
'@babel/parser':
|
||||
@@ -8411,19 +8382,10 @@ snapshots:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@antfu/install-pkg@0.5.0':
|
||||
dependencies:
|
||||
package-manager-detector: 0.2.11
|
||||
tinyexec: 0.3.2
|
||||
|
||||
'@antfu/install-pkg@1.1.0':
|
||||
dependencies:
|
||||
package-manager-detector: 1.3.0
|
||||
tinyexec: 1.0.1
|
||||
|
||||
'@antfu/utils@0.7.10': {}
|
||||
|
||||
'@antfu/utils@8.1.1': {}
|
||||
package-manager-detector: 1.6.0
|
||||
tinyexec: 1.0.2
|
||||
|
||||
'@asamuzakjp/css-color@3.2.0':
|
||||
dependencies:
|
||||
@@ -9797,18 +9759,11 @@ snapshots:
|
||||
|
||||
'@iconify/types@2.0.0': {}
|
||||
|
||||
'@iconify/utils@2.3.0':
|
||||
'@iconify/utils@3.1.0':
|
||||
dependencies:
|
||||
'@antfu/install-pkg': 1.1.0
|
||||
'@antfu/utils': 8.1.1
|
||||
'@iconify/types': 2.0.0
|
||||
debug: 4.4.3
|
||||
globals: 15.15.0
|
||||
kolorist: 1.8.0
|
||||
local-pkg: 1.1.2
|
||||
mlly: 1.8.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@internationalized/date@3.9.0':
|
||||
dependencies:
|
||||
@@ -10313,7 +10268,7 @@ snapshots:
|
||||
'@nx/nx-win32-x64-msvc@22.2.6':
|
||||
optional: true
|
||||
|
||||
'@nx/playwright@22.2.6(@babel/traverse@7.28.5)(@playwright/test@1.52.0)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6)':
|
||||
'@nx/playwright@22.2.6(@babel/traverse@7.28.5)(@playwright/test@1.57.0)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6)':
|
||||
dependencies:
|
||||
'@nx/devkit': 22.2.6(nx@22.2.6)
|
||||
'@nx/eslint': 22.2.6(@babel/traverse@7.28.5)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6)
|
||||
@@ -10321,7 +10276,7 @@ snapshots:
|
||||
minimatch: 9.0.3
|
||||
tslib: 2.8.1
|
||||
optionalDependencies:
|
||||
'@playwright/test': 1.52.0
|
||||
'@playwright/test': 1.57.0
|
||||
transitivePeerDependencies:
|
||||
- '@babel/traverse'
|
||||
- '@swc-node/register'
|
||||
@@ -10605,9 +10560,9 @@ snapshots:
|
||||
|
||||
'@pkgr/core@0.2.9': {}
|
||||
|
||||
'@playwright/test@1.52.0':
|
||||
'@playwright/test@1.57.0':
|
||||
dependencies:
|
||||
playwright: 1.52.0
|
||||
playwright: 1.57.0
|
||||
|
||||
'@pnpm/config.env-replace@1.1.0': {}
|
||||
|
||||
@@ -10911,6 +10866,10 @@ snapshots:
|
||||
|
||||
'@sinclair/typebox@0.34.40': {}
|
||||
|
||||
'@sparkjsdev/spark@0.1.10':
|
||||
dependencies:
|
||||
fflate: 0.8.2
|
||||
|
||||
'@storybook/addon-docs@10.1.9(@types/react@19.1.9)(esbuild@0.27.1)(rollup@4.53.5)(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@mdx-js/react': 3.1.1(@types/react@19.1.9)(react@19.2.3)
|
||||
@@ -12442,6 +12401,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
chokidar@4.0.3:
|
||||
dependencies:
|
||||
readdirp: 4.1.2
|
||||
|
||||
chownr@3.0.0: {}
|
||||
|
||||
clean-css@5.3.3:
|
||||
@@ -13147,15 +13110,6 @@ snapshots:
|
||||
dependencies:
|
||||
jsonc-parser: 3.3.1
|
||||
|
||||
eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.4):
|
||||
dependencies:
|
||||
eslint: 9.39.1(jiti@2.6.1)
|
||||
prettier: 3.7.4
|
||||
prettier-linter-helpers: 1.0.0
|
||||
synckit: 0.11.11
|
||||
optionalDependencies:
|
||||
eslint-config-prettier: 10.1.8(eslint@9.39.1(jiti@2.6.1))
|
||||
|
||||
eslint-plugin-storybook@10.1.9(eslint@9.39.1(jiti@2.6.1))(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@typescript-eslint/utils': 8.50.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
|
||||
@@ -13274,7 +13228,7 @@ snapshots:
|
||||
|
||||
expect-type@1.2.2: {}
|
||||
|
||||
exsolve@1.0.7: {}
|
||||
exsolve@1.0.8: {}
|
||||
|
||||
extend-shallow@2.0.1:
|
||||
dependencies:
|
||||
@@ -13314,8 +13268,6 @@ snapshots:
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
||||
fast-diff@1.3.0: {}
|
||||
|
||||
fast-glob@3.3.3:
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
@@ -14313,11 +14265,6 @@ snapshots:
|
||||
rfdc: 1.4.1
|
||||
wrap-ansi: 9.0.2
|
||||
|
||||
local-pkg@0.5.1:
|
||||
dependencies:
|
||||
mlly: 1.8.0
|
||||
pkg-types: 1.3.1
|
||||
|
||||
local-pkg@1.1.2:
|
||||
dependencies:
|
||||
mlly: 1.8.0
|
||||
@@ -15188,11 +15135,7 @@ snapshots:
|
||||
registry-url: 6.0.1
|
||||
semver: 7.7.3
|
||||
|
||||
package-manager-detector@0.2.11:
|
||||
dependencies:
|
||||
quansync: 0.2.11
|
||||
|
||||
package-manager-detector@1.3.0: {}
|
||||
package-manager-detector@1.6.0: {}
|
||||
|
||||
pako@1.0.11: {}
|
||||
|
||||
@@ -15282,14 +15225,14 @@ snapshots:
|
||||
pkg-types@2.3.0:
|
||||
dependencies:
|
||||
confbox: 0.2.2
|
||||
exsolve: 1.0.7
|
||||
exsolve: 1.0.8
|
||||
pathe: 2.0.3
|
||||
|
||||
playwright-core@1.52.0: {}
|
||||
playwright-core@1.57.0: {}
|
||||
|
||||
playwright@1.52.0:
|
||||
playwright@1.57.0:
|
||||
dependencies:
|
||||
playwright-core: 1.52.0
|
||||
playwright-core: 1.57.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
@@ -15328,10 +15271,6 @@ snapshots:
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier-linter-helpers@1.0.0:
|
||||
dependencies:
|
||||
fast-diff: 1.3.0
|
||||
|
||||
prettier@3.7.4: {}
|
||||
|
||||
pretty-bytes@7.1.0: {}
|
||||
@@ -15617,6 +15556,8 @@ snapshots:
|
||||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
|
||||
readdirp@4.1.2: {}
|
||||
|
||||
recast@0.23.11:
|
||||
dependencies:
|
||||
ast-types: 0.16.1
|
||||
@@ -16217,10 +16158,6 @@ snapshots:
|
||||
'@pkgr/core': 0.2.9
|
||||
tslib: 2.8.1
|
||||
|
||||
synckit@0.11.11:
|
||||
dependencies:
|
||||
'@pkgr/core': 0.2.9
|
||||
|
||||
table@6.9.0:
|
||||
dependencies:
|
||||
ajv: 8.17.1
|
||||
@@ -16283,7 +16220,7 @@ snapshots:
|
||||
|
||||
tinyexec@0.3.2: {}
|
||||
|
||||
tinyexec@1.0.1: {}
|
||||
tinyexec@1.0.2: {}
|
||||
|
||||
tinyglobby@0.2.15:
|
||||
dependencies:
|
||||
@@ -16498,14 +16435,12 @@ snapshots:
|
||||
|
||||
universalify@2.0.1: {}
|
||||
|
||||
unplugin-icons@0.22.0(@vue/compiler-sfc@3.5.25):
|
||||
unplugin-icons@22.5.0(@vue/compiler-sfc@3.5.25):
|
||||
dependencies:
|
||||
'@antfu/install-pkg': 0.5.0
|
||||
'@antfu/utils': 0.7.10
|
||||
'@iconify/utils': 2.3.0
|
||||
'@antfu/install-pkg': 1.1.0
|
||||
'@iconify/utils': 3.1.0
|
||||
debug: 4.4.3
|
||||
kolorist: 1.8.0
|
||||
local-pkg: 0.5.1
|
||||
local-pkg: 1.1.2
|
||||
unplugin: 2.3.11
|
||||
optionalDependencies:
|
||||
'@vue/compiler-sfc': 3.5.25
|
||||
@@ -16530,23 +16465,20 @@ snapshots:
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.3
|
||||
|
||||
unplugin-vue-components@0.28.0(@babel/parser@7.28.5)(rollup@4.53.5)(vue@3.5.13(typescript@5.9.3)):
|
||||
unplugin-vue-components@30.0.0(@babel/parser@7.28.5)(vue@3.5.13(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@antfu/utils': 0.7.10
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.53.5)
|
||||
chokidar: 3.6.0
|
||||
chokidar: 4.0.3
|
||||
debug: 4.4.3
|
||||
fast-glob: 3.3.3
|
||||
local-pkg: 0.5.1
|
||||
local-pkg: 1.1.2
|
||||
magic-string: 0.30.21
|
||||
minimatch: 9.0.5
|
||||
mlly: 1.8.0
|
||||
tinyglobby: 0.2.15
|
||||
unplugin: 2.3.11
|
||||
unplugin-utils: 0.3.1
|
||||
vue: 3.5.13(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
'@babel/parser': 7.28.5
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
unplugin@1.0.1:
|
||||
|
||||
@@ -16,7 +16,7 @@ catalog:
|
||||
'@nx/storybook': 22.2.4
|
||||
'@nx/vite': 22.2.6
|
||||
'@pinia/testing': ^0.1.5
|
||||
'@playwright/test': ^1.52.0
|
||||
'@playwright/test': ^1.57.0
|
||||
'@prettier/plugin-oxc': ^0.1.3
|
||||
'@primeuix/forms': 0.0.2
|
||||
'@primeuix/styled': 0.3.2
|
||||
@@ -27,6 +27,7 @@ catalog:
|
||||
'@primevue/themes': ^4.2.5
|
||||
'@sentry/vite-plugin': ^4.6.0
|
||||
'@sentry/vue': ^8.48.0
|
||||
'@sparkjsdev/spark': ^0.1.10
|
||||
'@storybook/addon-docs': ^10.1.9
|
||||
'@storybook/vue3': ^10.1.9
|
||||
'@storybook/vue3-vite': ^10.1.9
|
||||
@@ -54,7 +55,6 @@ catalog:
|
||||
eslint-import-resolver-typescript: ^4.4.4
|
||||
eslint-plugin-import-x: ^4.16.1
|
||||
eslint-plugin-oxlint: 1.25.0
|
||||
eslint-plugin-prettier: ^5.5.4
|
||||
eslint-plugin-storybook: ^10.1.9
|
||||
eslint-plugin-unused-imports: ^4.3.0
|
||||
eslint-plugin-vue: ^10.6.2
|
||||
@@ -89,9 +89,9 @@ catalog:
|
||||
typegpu: ^0.8.2
|
||||
typescript: ^5.9.3
|
||||
typescript-eslint: ^8.49.0
|
||||
unplugin-icons: ^0.22.0
|
||||
unplugin-icons: ^22.5.0
|
||||
unplugin-typegpu: 0.8.0
|
||||
unplugin-vue-components: ^0.28.0
|
||||
unplugin-vue-components: ^30.0.0
|
||||
vite: ^7.3.0
|
||||
vite-plugin-dts: ^4.5.4
|
||||
vite-plugin-html: ^3.2.2
|
||||
|
||||
@@ -10,8 +10,56 @@ interface TestStats {
|
||||
finished?: number
|
||||
}
|
||||
|
||||
interface TestLocation {
|
||||
file: string
|
||||
line: number
|
||||
column: number
|
||||
}
|
||||
|
||||
interface TestAttachment {
|
||||
name: string
|
||||
path?: string
|
||||
contentType: string
|
||||
}
|
||||
|
||||
interface TestResult {
|
||||
status: string
|
||||
duration: number
|
||||
errors?: Array<{ message?: string; stack?: string }>
|
||||
attachments?: TestAttachment[]
|
||||
}
|
||||
|
||||
interface Test {
|
||||
title: string
|
||||
location?: TestLocation
|
||||
results?: TestResult[]
|
||||
}
|
||||
|
||||
interface Suite {
|
||||
title: string
|
||||
suites?: Suite[]
|
||||
tests?: Test[]
|
||||
}
|
||||
|
||||
interface ReportData {
|
||||
stats?: TestStats
|
||||
suites?: Suite[]
|
||||
}
|
||||
|
||||
interface FailingTest {
|
||||
name: string
|
||||
filePath: string
|
||||
line: number
|
||||
error: string
|
||||
tracePath?: string
|
||||
failureType?: 'screenshot' | 'expectation' | 'timeout' | 'other'
|
||||
}
|
||||
|
||||
interface FailureTypeCounts {
|
||||
screenshot: number
|
||||
expectation: number
|
||||
timeout: number
|
||||
other: number
|
||||
}
|
||||
|
||||
interface TestCounts {
|
||||
@@ -20,12 +68,109 @@ interface TestCounts {
|
||||
flaky: number
|
||||
skipped: number
|
||||
total: number
|
||||
failingTests?: FailingTest[]
|
||||
failureTypes?: FailureTypeCounts
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize the failure type based on error message
|
||||
*/
|
||||
function categorizeFailureType(
|
||||
error: string,
|
||||
status: string
|
||||
): 'screenshot' | 'expectation' | 'timeout' | 'other' {
|
||||
if (status === 'timedOut') {
|
||||
return 'timeout'
|
||||
}
|
||||
|
||||
const errorLower = error.toLowerCase()
|
||||
|
||||
// Screenshot-related errors
|
||||
if (
|
||||
errorLower.includes('screenshot') ||
|
||||
errorLower.includes('snapshot') ||
|
||||
errorLower.includes('toHaveScreenshot') ||
|
||||
errorLower.includes('image comparison') ||
|
||||
errorLower.includes('pixel') ||
|
||||
errorLower.includes('visual')
|
||||
) {
|
||||
return 'screenshot'
|
||||
}
|
||||
|
||||
// Expectation errors
|
||||
if (
|
||||
errorLower.includes('expect') ||
|
||||
errorLower.includes('assertion') ||
|
||||
errorLower.includes('toEqual') ||
|
||||
errorLower.includes('toBe') ||
|
||||
errorLower.includes('toContain') ||
|
||||
errorLower.includes('toHave') ||
|
||||
errorLower.includes('toMatch')
|
||||
) {
|
||||
return 'expectation'
|
||||
}
|
||||
|
||||
return 'other'
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively extract failing tests from suite structure
|
||||
*/
|
||||
function extractFailingTests(
|
||||
suite: Suite,
|
||||
failingTests: FailingTest[],
|
||||
reportDir: string
|
||||
): void {
|
||||
// Process tests in this suite
|
||||
if (suite.tests) {
|
||||
for (const test of suite.tests) {
|
||||
if (!test.results) continue
|
||||
|
||||
for (const result of test.results) {
|
||||
if (result.status === 'failed' || result.status === 'timedOut') {
|
||||
const error =
|
||||
result.errors?.[0]?.message ||
|
||||
result.errors?.[0]?.stack ||
|
||||
'Test failed'
|
||||
|
||||
// Find trace attachment
|
||||
let tracePath: string | undefined
|
||||
if (result.attachments) {
|
||||
const traceAttachment = result.attachments.find(
|
||||
(att) => att.name === 'trace' || att.contentType === 'application/zip'
|
||||
)
|
||||
if (traceAttachment?.path) {
|
||||
tracePath = traceAttachment.path
|
||||
}
|
||||
}
|
||||
|
||||
const failureType = categorizeFailureType(error, result.status)
|
||||
|
||||
failingTests.push({
|
||||
name: test.title,
|
||||
filePath: test.location?.file || 'unknown',
|
||||
line: test.location?.line || 0,
|
||||
error: error.split('\n')[0], // First line of error
|
||||
tracePath,
|
||||
failureType
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process nested suites
|
||||
if (suite.suites) {
|
||||
for (const nestedSuite of suite.suites) {
|
||||
extractFailingTests(nestedSuite, failingTests, reportDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract test counts from Playwright HTML report
|
||||
* @param reportDir - Path to the playwright-report directory
|
||||
* @returns Test counts { passed, failed, flaky, skipped, total }
|
||||
* @returns Test counts { passed, failed, flaky, skipped, total, failingTests }
|
||||
*/
|
||||
function extractTestCounts(reportDir: string): TestCounts {
|
||||
const counts: TestCounts = {
|
||||
@@ -33,7 +178,14 @@ function extractTestCounts(reportDir: string): TestCounts {
|
||||
failed: 0,
|
||||
flaky: 0,
|
||||
skipped: 0,
|
||||
total: 0
|
||||
total: 0,
|
||||
failingTests: [],
|
||||
failureTypes: {
|
||||
screenshot: 0,
|
||||
expectation: 0,
|
||||
timeout: 0,
|
||||
other: 0
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -54,6 +206,22 @@ function extractTestCounts(reportDir: string): TestCounts {
|
||||
counts.failed = stats.unexpected || 0
|
||||
counts.flaky = stats.flaky || 0
|
||||
counts.skipped = stats.skipped || 0
|
||||
|
||||
// Extract failing test details
|
||||
if (reportJson.suites) {
|
||||
for (const suite of reportJson.suites) {
|
||||
extractFailingTests(suite, counts.failingTests, reportDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Count failure types
|
||||
if (counts.failingTests) {
|
||||
for (const test of counts.failingTests) {
|
||||
const type = test.failureType || 'other'
|
||||
counts.failureTypes![type]++
|
||||
}
|
||||
}
|
||||
|
||||
return counts
|
||||
}
|
||||
}
|
||||
@@ -86,6 +254,22 @@ function extractTestCounts(reportDir: string): TestCounts {
|
||||
counts.failed = stats.unexpected || 0
|
||||
counts.flaky = stats.flaky || 0
|
||||
counts.skipped = stats.skipped || 0
|
||||
|
||||
// Extract failing test details
|
||||
if (reportData.suites) {
|
||||
for (const suite of reportData.suites) {
|
||||
extractFailingTests(suite, counts.failingTests!, reportDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Count failure types
|
||||
if (counts.failingTests) {
|
||||
for (const test of counts.failingTests) {
|
||||
const type = test.failureType || 'other'
|
||||
counts.failureTypes![type]++
|
||||
}
|
||||
}
|
||||
|
||||
return counts
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -113,6 +297,22 @@ function extractTestCounts(reportDir: string): TestCounts {
|
||||
counts.failed = stats.unexpected || 0
|
||||
counts.flaky = stats.flaky || 0
|
||||
counts.skipped = stats.skipped || 0
|
||||
|
||||
// Extract failing test details
|
||||
if (reportData.suites) {
|
||||
for (const suite of reportData.suites) {
|
||||
extractFailingTests(suite, counts.failingTests!, reportDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Count failure types
|
||||
if (counts.failingTests) {
|
||||
for (const test of counts.failingTests) {
|
||||
const type = test.failureType || 'other'
|
||||
counts.failureTypes![type]++
|
||||
}
|
||||
}
|
||||
|
||||
return counts
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -252,6 +252,10 @@ else
|
||||
total_flaky=0
|
||||
total_skipped=0
|
||||
total_tests=0
|
||||
total_screenshot_failures=0
|
||||
total_expectation_failures=0
|
||||
total_timeout_failures=0
|
||||
total_other_failures=0
|
||||
|
||||
# Parse counts and calculate totals
|
||||
IFS='|' read -r -a counts_array <<< "$all_counts"
|
||||
@@ -265,6 +269,10 @@ else
|
||||
flaky=$(echo "$counts_json" | jq -r '.flaky // 0')
|
||||
skipped=$(echo "$counts_json" | jq -r '.skipped // 0')
|
||||
total=$(echo "$counts_json" | jq -r '.total // 0')
|
||||
screenshot=$(echo "$counts_json" | jq -r '.failureTypes.screenshot // 0')
|
||||
expectation=$(echo "$counts_json" | jq -r '.failureTypes.expectation // 0')
|
||||
timeout=$(echo "$counts_json" | jq -r '.failureTypes.timeout // 0')
|
||||
other=$(echo "$counts_json" | jq -r '.failureTypes.other // 0')
|
||||
else
|
||||
# Fallback parsing without jq
|
||||
passed=$(echo "$counts_json" | sed -n 's/.*"passed":\([0-9]*\).*/\1/p')
|
||||
@@ -272,13 +280,21 @@ else
|
||||
flaky=$(echo "$counts_json" | sed -n 's/.*"flaky":\([0-9]*\).*/\1/p')
|
||||
skipped=$(echo "$counts_json" | sed -n 's/.*"skipped":\([0-9]*\).*/\1/p')
|
||||
total=$(echo "$counts_json" | sed -n 's/.*"total":\([0-9]*\).*/\1/p')
|
||||
screenshot=0
|
||||
expectation=0
|
||||
timeout=0
|
||||
other=0
|
||||
fi
|
||||
|
||||
|
||||
total_passed=$((total_passed + ${passed:-0}))
|
||||
total_failed=$((total_failed + ${failed:-0}))
|
||||
total_flaky=$((total_flaky + ${flaky:-0}))
|
||||
total_skipped=$((total_skipped + ${skipped:-0}))
|
||||
total_tests=$((total_tests + ${total:-0}))
|
||||
total_screenshot_failures=$((total_screenshot_failures + ${screenshot:-0}))
|
||||
total_expectation_failures=$((total_expectation_failures + ${expectation:-0}))
|
||||
total_timeout_failures=$((total_timeout_failures + ${timeout:-0}))
|
||||
total_other_failures=$((total_other_failures + ${other:-0}))
|
||||
fi
|
||||
done
|
||||
unset IFS
|
||||
@@ -302,35 +318,98 @@ else
|
||||
comment="$COMMENT_MARKER
|
||||
## 🎭 Playwright Test Results
|
||||
|
||||
$status_icon **$status_text**
|
||||
|
||||
⏰ Completed at: $(date -u '+%m/%d/%Y, %I:%M:%S %p') UTC"
|
||||
$status_icon **$status_text** • ⏰ $(date -u '+%m/%d/%Y, %I:%M:%S %p') UTC"
|
||||
|
||||
# Add summary counts if we have test data
|
||||
if [ $total_tests -gt 0 ]; then
|
||||
comment="$comment
|
||||
|
||||
### 📈 Summary
|
||||
- **Total Tests:** $total_tests
|
||||
- **Passed:** $total_passed ✅
|
||||
- **Failed:** $total_failed $([ $total_failed -gt 0 ] && echo '❌' || echo '')
|
||||
- **Flaky:** $total_flaky $([ $total_flaky -gt 0 ] && echo '⚠️' || echo '')
|
||||
- **Skipped:** $total_skipped $([ $total_skipped -gt 0 ] && echo '⏭️' || echo '')"
|
||||
**$total_passed** ✅ • **$total_failed** $([ $total_failed -gt 0 ] && echo '❌' || echo '✅') • **$total_flaky** $([ $total_flaky -gt 0 ] && echo '⚠️' || echo '✅') • **$total_skipped** ⏭️ • **$total_tests** total"
|
||||
|
||||
# Add failure breakdown if there are failures
|
||||
if [ $total_failed -gt 0 ]; then
|
||||
comment="$comment
|
||||
|
||||
**Failure Breakdown:** 📸 $total_screenshot_failures screenshot • ✓ $total_expectation_failures expectation • ⏱️ $total_timeout_failures timeout • ❓ $total_other_failures other"
|
||||
fi
|
||||
fi
|
||||
|
||||
comment="$comment
|
||||
|
||||
### 📊 Test Reports by Browser"
|
||||
|
||||
# Add browser results with individual counts
|
||||
# Collect all failing tests across browsers
|
||||
all_failing_tests=""
|
||||
i=0
|
||||
IFS=' ' read -r -a browser_array <<< "$BROWSERS"
|
||||
for counts_json in "${counts_array[@]}"; do
|
||||
[ -z "$counts_json" ] && { i=$((i + 1)); continue; }
|
||||
browser="${browser_array[$i]:-}"
|
||||
|
||||
if [ "$counts_json" != "{}" ] && [ -n "$counts_json" ]; then
|
||||
if command -v jq > /dev/null 2>&1; then
|
||||
failing_tests=$(echo "$counts_json" | jq -r '.failingTests // [] | .[]' 2>/dev/null || echo "")
|
||||
if [ -n "$failing_tests" ]; then
|
||||
# Process each failing test
|
||||
while IFS= read -r test_json; do
|
||||
[ -z "$test_json" ] && continue
|
||||
test_name=$(echo "$test_json" | jq -r '.name // "Unknown test"')
|
||||
test_file=$(echo "$test_json" | jq -r '.filePath // "unknown"')
|
||||
test_line=$(echo "$test_json" | jq -r '.line // 0')
|
||||
trace_path=$(echo "$test_json" | jq -r '.tracePath // ""')
|
||||
|
||||
# Build GitHub source link (assumes ComfyUI_frontend repo)
|
||||
source_link="https://github.com/$GITHUB_REPOSITORY/blob/$BRANCH_NAME/$test_file#L$test_line"
|
||||
|
||||
# Build trace viewer link if trace exists
|
||||
if [ -n "$trace_path" ] && [ "$trace_path" != "null" ]; then
|
||||
# Extract trace filename from path
|
||||
trace_file=$(basename "$trace_path")
|
||||
url="${url_array[$i]:-}"
|
||||
if [ "$url" != "failed" ] && [ -n "$url" ]; then
|
||||
base_url="${url%/index.html}"
|
||||
trace_viewer_link="${base_url}/trace/?trace=${base_url}/data/${trace_file}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Format failing test entry
|
||||
if [ -n "$all_failing_tests" ]; then
|
||||
all_failing_tests="$all_failing_tests
|
||||
"
|
||||
fi
|
||||
|
||||
if [ -n "$trace_viewer_link" ]; then
|
||||
all_failing_tests="${all_failing_tests}- **[$test_name]($source_link)** \`$browser\` • [View trace]($trace_viewer_link)"
|
||||
else
|
||||
all_failing_tests="${all_failing_tests}- **[$test_name]($source_link)** \`$browser\`"
|
||||
fi
|
||||
done < <(echo "$counts_json" | jq -c '.failingTests[]?' 2>/dev/null || echo "")
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
unset IFS
|
||||
|
||||
# Add failing tests section if there are failures
|
||||
if [ $total_failed -gt 0 ] && [ -n "$all_failing_tests" ]; then
|
||||
comment="$comment
|
||||
|
||||
### ❌ Failed Tests
|
||||
$all_failing_tests"
|
||||
fi
|
||||
|
||||
comment="$comment
|
||||
|
||||
<details>
|
||||
<summary>📊 Test Reports by Browser</summary>
|
||||
|
||||
"
|
||||
|
||||
# Add browser results with individual counts
|
||||
i=0
|
||||
IFS=' ' read -r -a url_array <<< "$urls"
|
||||
for counts_json in "${counts_array[@]}"; do
|
||||
[ -z "$counts_json" ] && { i=$((i + 1)); continue; }
|
||||
browser="${browser_array[$i]:-}"
|
||||
url="${url_array[$i]:-}"
|
||||
|
||||
|
||||
if [ "$url" != "failed" ] && [ -n "$url" ]; then
|
||||
# Parse individual browser counts
|
||||
if [ "$counts_json" != "{}" ] && [ -n "$counts_json" ]; then
|
||||
@@ -347,7 +426,7 @@ $status_icon **$status_text**
|
||||
b_skipped=$(echo "$counts_json" | sed -n 's/.*"skipped":\([0-9]*\).*/\1/p')
|
||||
b_total=$(echo "$counts_json" | sed -n 's/.*"total":\([0-9]*\).*/\1/p')
|
||||
fi
|
||||
|
||||
|
||||
if [ -n "$b_total" ] && [ "$b_total" != "0" ]; then
|
||||
counts_str=" • ✅ $b_passed / ❌ $b_failed / ⚠️ $b_flaky / ⏭️ $b_skipped"
|
||||
else
|
||||
@@ -356,21 +435,20 @@ $status_icon **$status_text**
|
||||
else
|
||||
counts_str=""
|
||||
fi
|
||||
|
||||
|
||||
comment="$comment
|
||||
- ✅ **${browser}**: [View Report](${url})${counts_str}"
|
||||
- **${browser}**: [View Report](${url})${counts_str}"
|
||||
else
|
||||
comment="$comment
|
||||
- ❌ **${browser}**: Deployment failed"
|
||||
- **${browser}**: Deployment failed"
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
unset IFS
|
||||
|
||||
|
||||
comment="$comment
|
||||
|
||||
---
|
||||
🎉 Click on the links above to view detailed test results for each browser configuration."
|
||||
</details>"
|
||||
|
||||
post_comment "$comment"
|
||||
fi
|
||||
|
||||
@@ -12,23 +12,22 @@
|
||||
<div class="mx-1 flex flex-col items-end gap-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
v-if="managerState.shouldShowManagerButtons.value && isDesktop"
|
||||
v-if="managerState.shouldShowManagerButtons.value"
|
||||
class="pointer-events-auto flex h-12 shrink-0 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
|
||||
>
|
||||
<IconButton
|
||||
<Button
|
||||
v-tooltip.bottom="customNodesManagerTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
:aria-label="t('menu.customNodesManager')"
|
||||
@click="openCustomNodeManager"
|
||||
>
|
||||
<i class="icon-[lucide--puzzle] size-4" />
|
||||
</IconButton>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="actionbar-container pointer-events-auto flex h-12 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
|
||||
class="actionbar-container pointer-events-auto flex gap-2 h-12 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
|
||||
>
|
||||
<ActionBarButtons />
|
||||
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
|
||||
@@ -37,11 +36,10 @@
|
||||
class="[&:not(:has(*>*:not(:empty)))]:hidden"
|
||||
></div>
|
||||
<ComfyActionbar />
|
||||
<IconButton
|
||||
<Button
|
||||
v-tooltip.bottom="queueHistoryTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="relative mr-2 text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
type="destructive"
|
||||
size="icon"
|
||||
:aria-pressed="isQueueOverlayExpanded"
|
||||
:aria-label="
|
||||
t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')
|
||||
@@ -55,20 +53,19 @@
|
||||
>
|
||||
{{ queuedCount }}
|
||||
</span>
|
||||
</IconButton>
|
||||
</Button>
|
||||
<CurrentUserButton v-if="isLoggedIn" class="shrink-0" />
|
||||
<LoginButton v-else-if="isDesktop" />
|
||||
<IconButton
|
||||
<Button
|
||||
v-if="!isRightSidePanelOpen"
|
||||
v-tooltip.bottom="rightSidePanelTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="mr-2 text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
type="secondary"
|
||||
size="icon"
|
||||
:aria-label="t('rightSidePanel.togglePanel')"
|
||||
@click="rightSidePanelStore.togglePanel"
|
||||
>
|
||||
<i class="icon-[lucide--panel-right] size-4" />
|
||||
</IconButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<QueueProgressOverlay
|
||||
@@ -86,11 +83,11 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
|
||||
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import QueueProgressOverlay from '@/components/queue/QueueProgressOverlay.vue'
|
||||
import ActionBarButtons from '@/components/topbar/ActionBarButtons.vue'
|
||||
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
|
||||
import LoginButton from '@/components/topbar/LoginButton.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
content: { class: isDocked ? 'p-0' : 'p-1' }
|
||||
}"
|
||||
>
|
||||
<div ref="panelRef" class="flex items-center select-none">
|
||||
<div ref="panelRef" class="flex items-center select-none gap-2">
|
||||
<span
|
||||
ref="dragHandleRef"
|
||||
:class="
|
||||
cn(
|
||||
'drag-handle cursor-grab w-3 h-max mr-2',
|
||||
'drag-handle cursor-grab w-3 h-max',
|
||||
isDragging && 'cursor-grabbing'
|
||||
)
|
||||
"
|
||||
@@ -31,17 +31,16 @@
|
||||
<Suspense @resolve="comfyRunButtonResolved">
|
||||
<ComfyRunButton />
|
||||
</Suspense>
|
||||
<IconButton
|
||||
<Button
|
||||
v-tooltip.bottom="cancelJobTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="ml-2 bg-destructive-background text-base-foreground transition-colors duration-200 ease-in-out hover:bg-destructive-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-destructive-background"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:disabled="isExecutionIdle"
|
||||
:aria-label="t('menu.interrupt')"
|
||||
@click="cancelCurrentJob"
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</IconButton>
|
||||
</Button>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
@@ -58,10 +57,10 @@ import { clamp } from 'es-toolkit/compat'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Panel from 'primevue/panel'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { t } from '@/i18n'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
@@ -72,6 +71,7 @@ import ComfyRunButton from './ComfyRunButton'
|
||||
|
||||
const settingsStore = useSettingStore()
|
||||
const commandStore = useCommandStore()
|
||||
const { t } = useI18n()
|
||||
const { isIdle: isExecutionIdle } = storeToRefs(useExecutionStore())
|
||||
|
||||
const position = computed(() => settingsStore.get('Comfy.UseNewMenu'))
|
||||
@@ -301,7 +301,7 @@ const panelClass = computed(() =>
|
||||
'actionbar pointer-events-auto z-1300',
|
||||
isDragging.value && 'select-none pointer-events-none',
|
||||
isDocked.value
|
||||
? 'p-0 static mr-2 border-none bg-transparent'
|
||||
? 'p-0 static border-none bg-transparent'
|
||||
: 'fixed shadow-interface'
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import IconButton from './IconButton.vue'
|
||||
|
||||
const meta: Meta<typeof IconButton> = {
|
||||
title: 'Components/Button/IconButton',
|
||||
component: IconButton,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
size: {
|
||||
control: { type: 'select' },
|
||||
options: ['sm', 'md']
|
||||
},
|
||||
type: {
|
||||
control: { type: 'select' },
|
||||
options: ['primary', 'secondary', 'transparent']
|
||||
},
|
||||
border: {
|
||||
control: 'boolean',
|
||||
description: 'Toggle border attribute'
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Toggle disable status'
|
||||
},
|
||||
onClick: { action: 'clicked' }
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
render: (args) => ({
|
||||
components: { IconButton },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: `
|
||||
<IconButton v-bind="args">
|
||||
<i class="icon-[lucide--trophy] size-4" />
|
||||
</IconButton>
|
||||
`
|
||||
}),
|
||||
args: {
|
||||
type: 'primary',
|
||||
size: 'md'
|
||||
}
|
||||
}
|
||||
|
||||
export const Secondary: Story = {
|
||||
render: (args) => ({
|
||||
components: { IconButton },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: `
|
||||
<IconButton v-bind="args">
|
||||
<i class="icon-[lucide--settings] size-4" />
|
||||
</IconButton>
|
||||
`
|
||||
}),
|
||||
args: {
|
||||
type: 'secondary',
|
||||
size: 'md'
|
||||
}
|
||||
}
|
||||
|
||||
export const Transparent: Story = {
|
||||
render: (args) => ({
|
||||
components: { IconButton },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: `
|
||||
<IconButton v-bind="args">
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</IconButton>
|
||||
`
|
||||
}),
|
||||
args: {
|
||||
type: 'transparent',
|
||||
size: 'md'
|
||||
}
|
||||
}
|
||||
|
||||
export const Small: Story = {
|
||||
render: (args) => ({
|
||||
components: { IconButton },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: `
|
||||
<IconButton v-bind="args">
|
||||
<i class="icon-[lucide--bell] size-3" />
|
||||
</IconButton>
|
||||
`
|
||||
}),
|
||||
args: {
|
||||
type: 'secondary',
|
||||
size: 'sm'
|
||||
}
|
||||
}
|
||||
|
||||
export const AllVariants: Story = {
|
||||
render: () => ({
|
||||
components: { IconButton },
|
||||
template: `
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex gap-2 items-center">
|
||||
<IconButton type="primary" size="sm" @click="() => {}">
|
||||
<i class="icon-[lucide--trophy] size-3" />
|
||||
</IconButton>
|
||||
<IconButton type="primary" size="md" @click="() => {}">
|
||||
<i class="icon-[lucide--trophy] size-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<IconButton type="secondary" size="sm" @click="() => {}">
|
||||
<i class="icon-[lucide--settings] size-3" />
|
||||
</IconButton>
|
||||
<IconButton type="secondary" size="md" @click="() => {}">
|
||||
<i class="icon-[lucide--settings] size-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<IconButton type="transparent" size="sm" @click="() => {}">
|
||||
<i class="icon-[lucide--x] size-3" />
|
||||
</IconButton>
|
||||
<IconButton type="transparent" size="md" @click="() => {}">
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<IconButton type="primary" size="md" @click="() => {}">
|
||||
<i class="icon-[lucide--bell] size-4" />
|
||||
</IconButton>
|
||||
<IconButton type="secondary" size="md" @click="() => {}">
|
||||
<i class="icon-[lucide--heart] size-4" />
|
||||
</IconButton>
|
||||
<IconButton type="transparent" size="md" @click="() => {}">
|
||||
<i class="icon-[lucide--download] size-4" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}),
|
||||
parameters: {
|
||||
controls: { disable: true },
|
||||
actions: { disable: true }
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
<template>
|
||||
<Button
|
||||
v-bind="$attrs"
|
||||
unstyled
|
||||
:class="buttonStyle"
|
||||
:disabled="disabled"
|
||||
@click="onClick"
|
||||
>
|
||||
<slot></slot>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { BaseButtonProps } from '@/types/buttonTypes'
|
||||
import {
|
||||
getBaseButtonClasses,
|
||||
getBorderButtonTypeClasses,
|
||||
getButtonTypeClasses,
|
||||
getIconButtonSizeClasses
|
||||
} from '@/types/buttonTypes'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
interface IconButtonProps extends BaseButtonProps {
|
||||
onClick?: (event: MouseEvent) => void
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
const {
|
||||
size = 'md',
|
||||
type = 'secondary',
|
||||
border = false,
|
||||
disabled = false,
|
||||
class: className,
|
||||
onClick
|
||||
} = defineProps<IconButtonProps>()
|
||||
|
||||
const buttonStyle = computed(() => {
|
||||
const baseClasses = `${getBaseButtonClasses()} p-0`
|
||||
const sizeClasses = getIconButtonSizeClasses(size)
|
||||
const typeClasses = border
|
||||
? getBorderButtonTypeClasses(type)
|
||||
: getButtonTypeClasses(type)
|
||||
|
||||
return cn(baseClasses, sizeClasses, typeClasses, className)
|
||||
})
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import IconButton from './IconButton.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import IconGroup from './IconGroup.vue'
|
||||
|
||||
const meta: Meta<typeof IconGroup> = {
|
||||
@@ -16,18 +16,18 @@ type Story = StoryObj<typeof IconGroup>
|
||||
|
||||
export const Basic: Story = {
|
||||
render: () => ({
|
||||
components: { IconGroup, IconButton },
|
||||
components: { IconGroup, Button },
|
||||
template: `
|
||||
<IconGroup>
|
||||
<IconButton @click="console.log('Hello World!!')">
|
||||
<Button size="icon" @click="console.log('Hello World!!')">
|
||||
<i class="icon-[lucide--heart] size-4" />
|
||||
</IconButton>
|
||||
<IconButton @click="console.log('Hello World!!')">
|
||||
</Button>
|
||||
<Button size="icon" @click="console.log('Hello World!!')">
|
||||
<i class="icon-[lucide--download] size-4" />
|
||||
</IconButton>
|
||||
<IconButton @click="console.log('Hello World!!')">
|
||||
</Button>
|
||||
<Button size="icon" @click="console.log('Hello World!!')">
|
||||
<i class="icon-[lucide--external-link] size-4" />
|
||||
</IconButton>
|
||||
</Button>
|
||||
</IconGroup>
|
||||
`
|
||||
})
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import IconTextButton from './IconTextButton.vue'
|
||||
|
||||
const meta: Meta<typeof IconTextButton> = {
|
||||
title: 'Components/Button/IconTextButton',
|
||||
component: IconTextButton,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
label: {
|
||||
control: 'text'
|
||||
},
|
||||
size: {
|
||||
control: { type: 'select' },
|
||||
options: ['sm', 'md']
|
||||
},
|
||||
type: {
|
||||
control: { type: 'select' },
|
||||
options: ['primary', 'secondary', 'transparent']
|
||||
},
|
||||
border: {
|
||||
control: 'boolean',
|
||||
description: 'Toggle border attribute'
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Toggle disable status'
|
||||
},
|
||||
iconPosition: {
|
||||
control: { type: 'select' },
|
||||
options: ['left', 'right']
|
||||
},
|
||||
onClick: { action: 'clicked' }
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Primary: Story = {
|
||||
render: (args) => ({
|
||||
components: { IconTextButton },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: `
|
||||
<IconTextButton v-bind="args">
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--package] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
`
|
||||
}),
|
||||
args: {
|
||||
label: 'Deploy',
|
||||
type: 'primary',
|
||||
size: 'md'
|
||||
}
|
||||
}
|
||||
|
||||
export const Secondary: Story = {
|
||||
render: (args) => ({
|
||||
components: { IconTextButton },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: `
|
||||
<IconTextButton v-bind="args">
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--settings] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
`
|
||||
}),
|
||||
args: {
|
||||
label: 'Settings',
|
||||
type: 'secondary',
|
||||
size: 'md'
|
||||
}
|
||||
}
|
||||
|
||||
export const Transparent: Story = {
|
||||
render: (args) => ({
|
||||
components: { IconTextButton },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: `
|
||||
<IconTextButton v-bind="args">
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
`
|
||||
}),
|
||||
args: {
|
||||
label: 'Cancel',
|
||||
type: 'transparent',
|
||||
size: 'md'
|
||||
}
|
||||
}
|
||||
|
||||
export const WithIconRight: Story = {
|
||||
render: (args) => ({
|
||||
components: { IconTextButton },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: `
|
||||
<IconTextButton v-bind="args">
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--chevron-right] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
`
|
||||
}),
|
||||
args: {
|
||||
label: 'Next',
|
||||
type: 'primary',
|
||||
size: 'md',
|
||||
iconPosition: 'right'
|
||||
}
|
||||
}
|
||||
|
||||
export const Small: Story = {
|
||||
render: (args) => ({
|
||||
components: { IconTextButton },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: `
|
||||
<IconTextButton v-bind="args">
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--save] size-3" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
`
|
||||
}),
|
||||
args: {
|
||||
label: 'Save',
|
||||
type: 'primary',
|
||||
size: 'sm'
|
||||
}
|
||||
}
|
||||
|
||||
export const AllVariants: Story = {
|
||||
render: () => ({
|
||||
components: {
|
||||
IconTextButton
|
||||
},
|
||||
template: `
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex gap-2 items-center">
|
||||
<IconTextButton label="Download" type="primary" size="sm" @click="() => {}">
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--download] size-3" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<IconTextButton label="Download" type="primary" size="md" @click="() => {}">
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--download] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<IconTextButton label="Settings" type="secondary" size="sm" @click="() => {}">
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--settings] size-3" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<IconTextButton label="Settings" type="secondary" size="md" @click="() => {}">
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--settings] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<IconTextButton label="Delete" type="transparent" size="sm" @click="() => {}">
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--trash-2] size-3" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<IconTextButton label="Delete" type="transparent" size="md" @click="() => {}">
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--trash-2] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<IconTextButton label="Next" type="primary" size="md" iconPosition="right" @click="() => {}">
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--chevron-right] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<IconTextButton label="Previous" type="secondary" size="md" @click="() => {}">
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--chevron-left] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<IconTextButton label="Save File" type="primary" size="md" @click="() => {}">
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--save] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}),
|
||||
parameters: {
|
||||
controls: { disable: true },
|
||||
actions: { disable: true }
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
<template>
|
||||
<Button
|
||||
v-bind="$attrs"
|
||||
unstyled
|
||||
:class="buttonStyle"
|
||||
:disabled="disabled"
|
||||
@click="onClick"
|
||||
>
|
||||
<slot v-if="iconPosition !== 'right'" name="icon"></slot>
|
||||
<span>{{ label }}</span>
|
||||
<slot v-if="iconPosition === 'right'" name="icon"></slot>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { BaseButtonProps } from '@/types/buttonTypes'
|
||||
import {
|
||||
getBaseButtonClasses,
|
||||
getBorderButtonTypeClasses,
|
||||
getButtonSizeClasses,
|
||||
getButtonTypeClasses
|
||||
} from '@/types/buttonTypes'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false
|
||||
})
|
||||
|
||||
interface IconTextButtonProps extends BaseButtonProps {
|
||||
iconPosition?: 'left' | 'right'
|
||||
label: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const {
|
||||
size = 'md',
|
||||
type = 'primary',
|
||||
border = false,
|
||||
disabled = false,
|
||||
class: className,
|
||||
iconPosition = 'left',
|
||||
label,
|
||||
onClick
|
||||
} = defineProps<IconTextButtonProps>()
|
||||
|
||||
const buttonStyle = computed(() => {
|
||||
const baseClasses = `${getBaseButtonClasses()} justify-start gap-2`
|
||||
const sizeClasses = getButtonSizeClasses(size)
|
||||
const typeClasses = border
|
||||
? getBorderButtonTypeClasses(type)
|
||||
: getButtonTypeClasses(type)
|
||||
|
||||
return cn(baseClasses, sizeClasses, typeClasses, className)
|
||||
})
|
||||
</script>
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import IconTextButton from './IconTextButton.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import MoreButton from './MoreButton.vue'
|
||||
|
||||
const meta: Meta<typeof MoreButton> = {
|
||||
@@ -17,30 +17,26 @@ type Story = StoryObj<typeof MoreButton>
|
||||
|
||||
export const Basic: Story = {
|
||||
render: () => ({
|
||||
components: { MoreButton, IconTextButton },
|
||||
components: { MoreButton, Button },
|
||||
template: `
|
||||
<div style="height: 200px; display: flex; align-items: center; justify-content: center;">
|
||||
<MoreButton>
|
||||
<template #default="{ close }">
|
||||
<IconTextButton
|
||||
type="transparent"
|
||||
label="Settings"
|
||||
<Button
|
||||
variant="textonly"
|
||||
@click="() => { close() }"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--download] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<i class="icon-[lucide--download] size-4" />
|
||||
<span>Settings</span>
|
||||
</Button>
|
||||
|
||||
<IconTextButton
|
||||
type="transparent"
|
||||
label="Profile"
|
||||
<Button
|
||||
variant="textonly"
|
||||
@click="() => { close() }"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--scroll-text] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<i class="icon-[lucide--scroll-text] size-4" />
|
||||
<span>Profile</span>
|
||||
</Button>
|
||||
</template>
|
||||
</MoreButton>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
<template>
|
||||
<div class="relative inline-flex items-center">
|
||||
<IconButton :size="size" :type="type" @click="popover?.toggle">
|
||||
<i v-if="!isVertical" class="icon-[lucide--ellipsis] text-sm" />
|
||||
<i v-else class="icon-[lucide--more-vertical] text-sm" />
|
||||
</IconButton>
|
||||
<Button size="icon" variant="secondary" @click="popover?.toggle">
|
||||
<i
|
||||
:class="
|
||||
cn(
|
||||
!isVertical
|
||||
? 'icon-[lucide--ellipsis]'
|
||||
: 'icon-[lucide--more-vertical]',
|
||||
'text-sm'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</Button>
|
||||
|
||||
<Popover
|
||||
ref="popover"
|
||||
@@ -49,20 +57,14 @@
|
||||
import Popover from 'primevue/popover'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { BaseButtonProps } from '@/types/buttonTypes'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import IconButton from './IconButton.vue'
|
||||
|
||||
interface MoreButtonProps extends BaseButtonProps {
|
||||
interface MoreButtonProps {
|
||||
isVertical?: boolean
|
||||
}
|
||||
|
||||
const {
|
||||
size = 'md',
|
||||
type = 'secondary',
|
||||
isVertical = false
|
||||
} = defineProps<MoreButtonProps>()
|
||||
const { isVertical = false } = defineProps<MoreButtonProps>()
|
||||
|
||||
defineEmits<{
|
||||
menuOpened: []
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import IconButton from '../button/IconButton.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import SquareChip from '../chip/SquareChip.vue'
|
||||
import CardBottom from './CardBottom.vue'
|
||||
import CardContainer from './CardContainer.vue'
|
||||
@@ -173,7 +173,7 @@ const createCardTemplate = (args: CardStoryArgs) => ({
|
||||
CardBottom,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
IconButton,
|
||||
Button,
|
||||
SquareChip
|
||||
},
|
||||
setup() {
|
||||
@@ -222,19 +222,19 @@ const createCardTemplate = (args: CardStoryArgs) => ({
|
||||
</template>
|
||||
|
||||
<template v-if="args.showTopRight" #top-right>
|
||||
<IconButton
|
||||
<Button
|
||||
class="!bg-white/90 !text-neutral-900"
|
||||
@click="() => console.log('Info clicked')"
|
||||
>
|
||||
<i class="icon-[lucide--info] size-4" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
</Button>
|
||||
<Button
|
||||
class="!bg-white/90"
|
||||
:class="favorited ? '!text-red-500' : '!text-neutral-900'"
|
||||
@click="toggleFavorite"
|
||||
>
|
||||
<i class="icon-[lucide--heart] size-4" :class="favorited ? 'fill-current' : ''" />
|
||||
</IconButton>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<template v-if="args.showBottomLeft" #bottom-left>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
@keyup.enter.capture.stop="blurInputElement"
|
||||
@keyup.escape.stop="cancelEditing"
|
||||
@click.stop
|
||||
@contextmenu.stop
|
||||
@pointerdown.stop.capture
|
||||
@pointermove.stop.capture
|
||||
/>
|
||||
|
||||
@@ -17,22 +17,25 @@
|
||||
class="absolute inset-0 size-full pl-11 border-none outline-none bg-transparent text-sm"
|
||||
:aria-label="placeholder"
|
||||
/>
|
||||
<IconButton
|
||||
<Button
|
||||
v-if="filterIcon"
|
||||
class="p-inputicon filter-button absolute right-0 inset-y-0 h-full m-0 p-0"
|
||||
:icon="filterIcon"
|
||||
severity="contrast"
|
||||
size="icon"
|
||||
variant="textonly"
|
||||
class="filter-button absolute right-0 inset-y-0 m-0 p-0"
|
||||
@click="$emit('showFilter', $event)"
|
||||
/>
|
||||
>
|
||||
<i :class="filterIcon" />
|
||||
</Button>
|
||||
<InputIcon v-if="!modelValue" :class="icon" />
|
||||
<Button
|
||||
v-if="modelValue"
|
||||
class="p-inputicon clear-button"
|
||||
icon="pi pi-times"
|
||||
text
|
||||
severity="contrast"
|
||||
class="clear-button absolute left-0"
|
||||
variant="textonly"
|
||||
size="icon"
|
||||
@click="modelValue = ''"
|
||||
/>
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<div v-if="filters?.length" class="search-filters flex flex-wrap gap-2 pt-2">
|
||||
<SearchFilterChip
|
||||
@@ -49,12 +52,12 @@
|
||||
<script setup lang="ts" generic="TFilter extends SearchFilter">
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import IconButton from '../button/IconButton.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
|
||||
import type { SearchFilter } from './SearchFilterChip.vue'
|
||||
import SearchFilterChip from './SearchFilterChip.vue'
|
||||
|
||||
@@ -125,8 +128,4 @@ const wrapperStyle = computed(() => {
|
||||
:deep(.p-inputtext) {
|
||||
--p-form-field-padding-x: 0.625rem;
|
||||
}
|
||||
|
||||
.p-button.p-inputicon {
|
||||
@apply p-0 w-auto border-none;
|
||||
}
|
||||
</style>
|
||||
|
||||
134
src/components/common/UserCredit.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enMessages from '@/locales/en/main.json' with { type: 'json' }
|
||||
|
||||
import UserCredit from './UserCredit.vue'
|
||||
|
||||
vi.mock('firebase/app', () => ({
|
||||
initializeApp: vi.fn(),
|
||||
getApp: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('firebase/auth', () => ({
|
||||
getAuth: vi.fn(),
|
||||
setPersistence: vi.fn(),
|
||||
browserLocalPersistence: {},
|
||||
onAuthStateChanged: vi.fn(),
|
||||
signInWithEmailAndPassword: vi.fn(),
|
||||
signOut: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('pinia')
|
||||
|
||||
const mockBalance = vi.hoisted(() => ({
|
||||
value: {
|
||||
amount_micros: 100_000,
|
||||
effective_balance_micros: 100_000,
|
||||
currency: 'usd'
|
||||
}
|
||||
}))
|
||||
|
||||
const mockIsFetchingBalance = vi.hoisted(() => ({ value: false }))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
balance: mockBalance.value,
|
||||
isFetchingBalance: mockIsFetchingBalance.value
|
||||
}))
|
||||
}))
|
||||
|
||||
describe('UserCredit', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockBalance.value = {
|
||||
amount_micros: 100_000,
|
||||
effective_balance_micros: 100_000,
|
||||
currency: 'usd'
|
||||
}
|
||||
mockIsFetchingBalance.value = false
|
||||
})
|
||||
|
||||
const mountComponent = (props = {}) => {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: { en: enMessages }
|
||||
})
|
||||
|
||||
return mount(UserCredit, {
|
||||
props,
|
||||
global: {
|
||||
plugins: [i18n],
|
||||
stubs: {
|
||||
Skeleton: true,
|
||||
Tag: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('effective_balance_micros handling', () => {
|
||||
it('uses effective_balance_micros when present (positive balance)', () => {
|
||||
mockBalance.value = {
|
||||
amount_micros: 200_000,
|
||||
effective_balance_micros: 150_000,
|
||||
currency: 'usd'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.text()).toContain('Credits')
|
||||
})
|
||||
|
||||
it('uses effective_balance_micros when zero', () => {
|
||||
mockBalance.value = {
|
||||
amount_micros: 100_000,
|
||||
effective_balance_micros: 0,
|
||||
currency: 'usd'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.text()).toContain('0')
|
||||
})
|
||||
|
||||
it('uses effective_balance_micros when negative', () => {
|
||||
mockBalance.value = {
|
||||
amount_micros: 0,
|
||||
effective_balance_micros: -50_000,
|
||||
currency: 'usd'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.text()).toContain('-')
|
||||
})
|
||||
|
||||
it('falls back to amount_micros when effective_balance_micros is missing', () => {
|
||||
mockBalance.value = {
|
||||
amount_micros: 100_000,
|
||||
currency: 'usd'
|
||||
} as typeof mockBalance.value
|
||||
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.text()).toContain('Credits')
|
||||
})
|
||||
|
||||
it('falls back to 0 when both effective_balance_micros and amount_micros are missing', () => {
|
||||
mockBalance.value = {
|
||||
currency: 'usd'
|
||||
} as typeof mockBalance.value
|
||||
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.text()).toContain('0')
|
||||
})
|
||||
})
|
||||
|
||||
describe('loading state', () => {
|
||||
it('shows skeleton when loading', () => {
|
||||
mockIsFetchingBalance.value = true
|
||||
|
||||
const wrapper = mountComponent()
|
||||
expect(wrapper.findComponent({ name: 'Skeleton' }).exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -14,13 +14,7 @@
|
||||
class="p-1 text-amber-400"
|
||||
>
|
||||
<template #icon>
|
||||
<i
|
||||
:class="
|
||||
flags.subscriptionTiersEnabled
|
||||
? 'icon-[lucide--component]'
|
||||
: 'pi pi-dollar'
|
||||
"
|
||||
/>
|
||||
<i class="icon-[lucide--component]" />
|
||||
</template>
|
||||
</Tag>
|
||||
<div :class="textClass">
|
||||
@@ -36,7 +30,6 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { formatCreditsFromCents } from '@/base/credits/comfyCredits'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
|
||||
const { textClass, showCreditsOnly } = defineProps<{
|
||||
@@ -45,13 +38,14 @@ const { textClass, showCreditsOnly } = defineProps<{
|
||||
}>()
|
||||
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const { flags } = useFeatureFlags()
|
||||
const balanceLoading = computed(() => authStore.isFetchingBalance)
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const formattedBalance = computed(() => {
|
||||
// Backend returns cents despite the *_micros naming convention.
|
||||
const cents = authStore.balance?.amount_micros ?? 0
|
||||
const cents =
|
||||
authStore.balance?.effective_balance_micros ??
|
||||
authStore.balance?.amount_micros ??
|
||||
0
|
||||
const amount = formatCreditsFromCents({
|
||||
cents,
|
||||
locale: locale.value
|
||||
@@ -60,8 +54,10 @@ const formattedBalance = computed(() => {
|
||||
})
|
||||
|
||||
const formattedCreditsOnly = computed(() => {
|
||||
// Backend returns cents despite the *_micros naming convention.
|
||||
const cents = authStore.balance?.amount_micros ?? 0
|
||||
const cents =
|
||||
authStore.balance?.effective_balance_micros ??
|
||||
authStore.balance?.amount_micros ??
|
||||
0
|
||||
const amount = formatCreditsFromCents({
|
||||
cents,
|
||||
locale: locale.value,
|
||||
|
||||
@@ -22,16 +22,17 @@
|
||||
|
||||
<template #header-right-area>
|
||||
<div class="flex gap-2">
|
||||
<IconTextButton
|
||||
<Button
|
||||
v-if="filteredCount !== totalCount"
|
||||
type="secondary"
|
||||
:label="$t('templateWorkflows.resetFilters', 'Clear Filters')"
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
@click="resetFilters"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--filter-x]" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<i class="icon-[lucide--filter-x]" />
|
||||
<span>{{
|
||||
$t('templateWorkflows.resetFilters', 'Clear Filters')
|
||||
}}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -301,16 +302,16 @@
|
||||
v-if="template.tutorialUrl"
|
||||
class="flex flex-col-reverse justify-center"
|
||||
>
|
||||
<IconButton
|
||||
<Button
|
||||
v-if="hoveredTemplate === template.name"
|
||||
v-tooltip.bottom="$t('g.seeTutorial')"
|
||||
v-bind="$attrs"
|
||||
type="primary"
|
||||
size="sm"
|
||||
variant="inverted"
|
||||
size="icon"
|
||||
@click.stop="openTutorial(template)"
|
||||
>
|
||||
<i class="icon-[lucide--info] size-4" />
|
||||
</IconButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -382,8 +383,6 @@ import ProgressSpinner from 'primevue/progressspinner'
|
||||
import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import CardBottom from '@/components/card/CardBottom.vue'
|
||||
import CardContainer from '@/components/card/CardContainer.vue'
|
||||
import CardTop from '@/components/card/CardTop.vue'
|
||||
@@ -395,6 +394,7 @@ import AudioThumbnail from '@/components/templates/thumbnails/AudioThumbnail.vue
|
||||
import CompareSliderThumbnail from '@/components/templates/thumbnails/CompareSliderThumbnail.vue'
|
||||
import DefaultThumbnail from '@/components/templates/thumbnails/DefaultThumbnail.vue'
|
||||
import HoverDissolveThumbnail from '@/components/templates/thumbnails/HoverDissolveThumbnail.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
|
||||
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
|
||||
import { useIntersectionObserver } from '@/composables/useIntersectionObserver'
|
||||
|
||||
@@ -4,20 +4,17 @@
|
||||
v-if="isCloud"
|
||||
class="flex w-full items-center justify-between gap-2 py-2 px-4"
|
||||
>
|
||||
<IconTextButton
|
||||
:label="$t('missingNodes.cloud.learnMore')"
|
||||
type="transparent"
|
||||
<Button
|
||||
variant="textonly"
|
||||
size="sm"
|
||||
icon-position="left"
|
||||
as="a"
|
||||
href="https://www.comfy.org/cloud"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--info]"></i>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<i class="icon-[lucide--info]"></i>
|
||||
<span>{{ $t('missingNodes.cloud.learnMore') }}</span>
|
||||
</Button>
|
||||
<Button variant="secondary" size="md" @click="handleGotItClick">{{
|
||||
$t('missingNodes.cloud.gotIt')
|
||||
}}</Button>
|
||||
@@ -50,7 +47,6 @@
|
||||
import { computed, nextTick, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<!-- New Credits Design (default) -->
|
||||
<div v-if="useNewDesign" class="flex w-112 flex-col gap-8 p-8">
|
||||
<div class="flex w-112 flex-col gap-8 p-8">
|
||||
<!-- Header -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<h1 class="text-2xl font-semibold text-base-foreground m-0">
|
||||
@@ -66,91 +65,32 @@
|
||||
@click="handleBuy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Legacy Design -->
|
||||
<div v-else class="flex w-96 flex-col gap-10 p-2">
|
||||
<div v-if="isInsufficientCredits" class="flex flex-col gap-4">
|
||||
<h1 class="my-0 text-2xl leading-normal font-medium">
|
||||
{{ $t('credits.topUp.insufficientTitle') }}
|
||||
</h1>
|
||||
<p class="my-0 text-base">
|
||||
{{ $t('credits.topUp.insufficientMessage') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Balance Section -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex w-full flex-col gap-2">
|
||||
<div class="text-base text-muted">
|
||||
{{ $t('credits.yourCreditBalance') }}
|
||||
</div>
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<UserCredit text-class="text-2xl" />
|
||||
<Button
|
||||
outlined
|
||||
severity="secondary"
|
||||
:label="$t('credits.topUp.seeDetails')"
|
||||
icon="pi pi-arrow-up-right"
|
||||
@click="handleSeeDetails"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Amount Input Section -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-sm text-muted"
|
||||
>{{ $t('credits.topUp.quickPurchase') }}:</span
|
||||
>
|
||||
<div class="grid grid-cols-[2fr_1fr] gap-2">
|
||||
<LegacyCreditTopUpOption
|
||||
v-for="amount in amountOptions"
|
||||
:key="amount"
|
||||
:amount="amount"
|
||||
:preselected="amount === preselectedAmountOption"
|
||||
/>
|
||||
|
||||
<LegacyCreditTopUpOption :amount="100" :preselected="false" editable />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { creditsToUsd } from '@/base/credits/comfyCredits'
|
||||
import UserCredit from '@/components/common/UserCredit.vue'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
|
||||
import CreditTopUpOption from './credit/CreditTopUpOption.vue'
|
||||
import LegacyCreditTopUpOption from './credit/LegacyCreditTopUpOption.vue'
|
||||
|
||||
interface CreditOption {
|
||||
credits: number
|
||||
description: string
|
||||
}
|
||||
|
||||
const {
|
||||
isInsufficientCredits = false,
|
||||
amountOptions = [5, 10, 20, 50],
|
||||
preselectedAmountOption = 10
|
||||
} = defineProps<{
|
||||
const { isInsufficientCredits = false } = defineProps<{
|
||||
isInsufficientCredits?: boolean
|
||||
amountOptions?: number[]
|
||||
preselectedAmountOption?: number
|
||||
}>()
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
const { formattedRenewalDate } = useSubscription()
|
||||
// Use feature flag to determine design - defaults to true (new design)
|
||||
const useNewDesign = computed(() => flags.subscriptionTiersEnabled)
|
||||
|
||||
const { t } = useI18n()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
@@ -202,8 +142,4 @@ const handleBuy = async () => {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSeeDetails = async () => {
|
||||
await authActions.accessBillingPortal()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<Tag
|
||||
severity="secondary"
|
||||
icon="pi pi-wallet"
|
||||
rounded
|
||||
class="p-1 text-amber-400"
|
||||
/>
|
||||
<div v-if="editable" class="flex items-center gap-2">
|
||||
<InputNumber
|
||||
v-model="customAmount"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
:step="1"
|
||||
show-buttons
|
||||
:allow-empty="false"
|
||||
:highlight-on-focus="true"
|
||||
prefix="$"
|
||||
pt:pc-input-text:root="w-28"
|
||||
@blur="
|
||||
(e: InputNumberBlurEvent) =>
|
||||
(customAmount = clampUsd(Number(e.value)))
|
||||
"
|
||||
@input="
|
||||
(e: InputNumberInputEvent) =>
|
||||
(customAmount = clampUsd(Number(e.value)))
|
||||
"
|
||||
/>
|
||||
<span class="text-xs text-muted">{{ formattedCredits }}</span>
|
||||
</div>
|
||||
<div v-else class="flex flex-col leading-tight">
|
||||
<span class="text-xl font-semibold">{{ formattedCredits }}</span>
|
||||
<span class="text-xs text-muted">{{ formattedUsd }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ProgressSpinner v-if="loading" class="h-8 w-8" />
|
||||
<Button
|
||||
v-else
|
||||
:severity="preselected ? 'primary' : 'secondary'"
|
||||
:outlined="!preselected"
|
||||
:label="$t('credits.topUp.buyNow')"
|
||||
@click="handleBuyNow"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import type {
|
||||
InputNumberBlurEvent,
|
||||
InputNumberInputEvent
|
||||
} from 'primevue/inputnumber'
|
||||
import ProgressSpinner from 'primevue/progressspinner'
|
||||
import Tag from 'primevue/tag'
|
||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import {
|
||||
clampUsd,
|
||||
formatCreditsFromUsd,
|
||||
formatUsd
|
||||
} from '@/base/credits/comfyCredits'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
|
||||
const authActions = useFirebaseAuthActions()
|
||||
const telemetry = useTelemetry()
|
||||
|
||||
const {
|
||||
amount,
|
||||
preselected,
|
||||
editable = false
|
||||
} = defineProps<{
|
||||
amount: number
|
||||
preselected: boolean
|
||||
editable?: boolean
|
||||
}>()
|
||||
|
||||
const customAmount = ref(amount)
|
||||
const didClickBuyNow = ref(false)
|
||||
const loading = ref(false)
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const displayUsdAmount = computed(() =>
|
||||
editable ? clampUsd(Number(customAmount.value)) : clampUsd(amount)
|
||||
)
|
||||
|
||||
const formattedCredits = computed(
|
||||
() =>
|
||||
`${formatCreditsFromUsd({
|
||||
usd: displayUsdAmount.value,
|
||||
locale: locale.value
|
||||
})} ${t('credits.credits')}`
|
||||
)
|
||||
|
||||
const formattedUsd = computed(
|
||||
() => `$${formatUsd({ value: displayUsdAmount.value, locale: locale.value })}`
|
||||
)
|
||||
|
||||
const handleBuyNow = async () => {
|
||||
const creditAmount = displayUsdAmount.value
|
||||
telemetry?.trackApiCreditTopupButtonPurchaseClicked(creditAmount)
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
await authActions.purchaseCredits(creditAmount)
|
||||
didClickBuyNow.value = true
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (didClickBuyNow.value) {
|
||||
// If clicked buy now, then returned back to the dialog and closed, fetch the balance
|
||||
void authActions.fetchBalance()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -139,7 +139,7 @@ interface CreditHistoryItemData {
|
||||
isPositive: boolean
|
||||
}
|
||||
|
||||
const { buildDocsUrl } = useExternalLink()
|
||||
const { buildDocsUrl, docsPaths } = useExternalLink()
|
||||
const dialogService = useDialogService()
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const authActions = useFirebaseAuthActions()
|
||||
@@ -194,9 +194,7 @@ const handleFaqClick = () => {
|
||||
|
||||
const handleOpenPartnerNodesInfo = () => {
|
||||
window.open(
|
||||
buildDocsUrl('/tutorials/api-nodes/overview#api-nodes', {
|
||||
includeLocale: true
|
||||
}),
|
||||
buildDocsUrl(docsPaths.partnerNodesPricing, { includeLocale: true }),
|
||||
'_blank'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@ function onMenuHide() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
registerNodeOptionsInstance({ toggle, hide, isOpen })
|
||||
registerNodeOptionsInstance({ toggle, show, hide, isOpen })
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
v-model:model-config="modelConfig"
|
||||
v-model:camera-config="cameraConfig"
|
||||
v-model:light-config="lightConfig"
|
||||
:is-splat-model="isSplatModel"
|
||||
:is-ply-model="isPlyModel"
|
||||
@update-background-image="handleBackgroundImageUpdate"
|
||||
@export-model="handleExportModel"
|
||||
/>
|
||||
@@ -109,6 +111,8 @@ const {
|
||||
// other state
|
||||
isRecording,
|
||||
isPreview,
|
||||
isSplatModel,
|
||||
isPlyModel,
|
||||
hasRecording,
|
||||
recordingDuration,
|
||||
animations,
|
||||
|
||||
@@ -47,6 +47,8 @@
|
||||
v-if="showModelControls"
|
||||
v-model:material-mode="modelConfig!.materialMode"
|
||||
v-model:up-direction="modelConfig!.upDirection"
|
||||
:hide-material-mode="isSplatModel"
|
||||
:is-ply-model="isPlyModel"
|
||||
/>
|
||||
|
||||
<CameraControls
|
||||
@@ -85,6 +87,11 @@ import type {
|
||||
SceneConfig
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
|
||||
const { isSplatModel = false, isPlyModel = false } = defineProps<{
|
||||
isSplatModel?: boolean
|
||||
isPlyModel?: boolean
|
||||
}>()
|
||||
|
||||
const sceneConfig = defineModel<SceneConfig>('sceneConfig')
|
||||
const modelConfig = defineModel<ModelConfig>('modelConfig')
|
||||
const cameraConfig = defineModel<CameraConfig>('cameraConfig')
|
||||
@@ -101,6 +108,10 @@ const categoryLabels: Record<string, string> = {
|
||||
}
|
||||
|
||||
const availableCategories = computed(() => {
|
||||
if (isSplatModel) {
|
||||
return ['scene', 'model', 'camera']
|
||||
}
|
||||
|
||||
return ['scene', 'model', 'camera', 'light', 'export']
|
||||
})
|
||||
|
||||
|
||||
@@ -46,6 +46,8 @@
|
||||
<ModelControls
|
||||
v-model:up-direction="viewer.upDirection.value"
|
||||
v-model:material-mode="viewer.materialMode.value"
|
||||
:hide-material-mode="viewer.isSplatModel.value"
|
||||
:is-ply-model="viewer.isPlyModel.value"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -56,13 +58,13 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 p-2">
|
||||
<div v-if="!viewer.isSplatModel.value" class="space-y-4 p-2">
|
||||
<LightControls
|
||||
v-model:light-intensity="viewer.lightIntensity.value"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 p-2">
|
||||
<div v-if="!viewer.isSplatModel.value" class="space-y-4 p-2">
|
||||
<ExportControls @export-model="viewer.exportModel" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="show-material-mode relative">
|
||||
<div v-if="!hideMaterialMode" class="show-material-mode relative">
|
||||
<Button
|
||||
class="p-button-rounded p-button-text"
|
||||
@click="toggleMaterialMode"
|
||||
@@ -71,6 +71,11 @@ import type {
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const { hideMaterialMode = false, isPlyModel = false } = defineProps<{
|
||||
hideMaterialMode?: boolean
|
||||
isPlyModel?: boolean
|
||||
}>()
|
||||
|
||||
const materialMode = defineModel<MaterialMode>('materialMode')
|
||||
const upDirection = defineModel<UpDirection>('upDirection')
|
||||
|
||||
@@ -95,6 +100,11 @@ const materialModes = computed(() => {
|
||||
//'depth' disable for now
|
||||
]
|
||||
|
||||
// Only show pointCloud mode for PLY files (point cloud rendering)
|
||||
if (isPlyModel) {
|
||||
modes.splice(1, 0, 'pointCloud')
|
||||
}
|
||||
|
||||
return modes
|
||||
})
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div v-if="!hideMaterialMode">
|
||||
<label>{{ $t('load3d.materialMode') }}</label>
|
||||
<Select
|
||||
v-model="materialMode"
|
||||
@@ -32,6 +32,11 @@ import type {
|
||||
} from '@/extensions/core/load3d/interfaces'
|
||||
import { t } from '@/i18n'
|
||||
|
||||
const { hideMaterialMode = false, isPlyModel = false } = defineProps<{
|
||||
hideMaterialMode?: boolean
|
||||
isPlyModel?: boolean
|
||||
}>()
|
||||
|
||||
const upDirection = defineModel<UpDirection>('upDirection')
|
||||
const materialMode = defineModel<MaterialMode>('materialMode')
|
||||
|
||||
@@ -46,10 +51,22 @@ const upDirectionOptions = [
|
||||
]
|
||||
|
||||
const materialModeOptions = computed(() => {
|
||||
return [
|
||||
{ label: t('load3d.materialModes.original'), value: 'original' },
|
||||
const options = [
|
||||
{ label: t('load3d.materialModes.original'), value: 'original' }
|
||||
]
|
||||
|
||||
if (isPlyModel) {
|
||||
options.push({
|
||||
label: t('load3d.materialModes.pointCloud'),
|
||||
value: 'pointCloud'
|
||||
})
|
||||
}
|
||||
|
||||
options.push(
|
||||
{ label: t('load3d.materialModes.normal'), value: 'normal' },
|
||||
{ label: t('load3d.materialModes.wireframe'), value: 'wireframe' }
|
||||
]
|
||||
)
|
||||
|
||||
return options
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c683087a3e168db/app/js/functions/sb_fn.js#L149
|
||||
-->
|
||||
<template>
|
||||
<LGraphNodePreview v-if="shouldRenderVueNodes" :node-def="nodeDef" />
|
||||
<LGraphNodePreview
|
||||
v-if="shouldRenderVueNodes"
|
||||
:node-def="nodeDef"
|
||||
:position="position"
|
||||
/>
|
||||
<div v-else class="_sb_node_preview bg-component-node-background">
|
||||
<div class="_sb_table">
|
||||
<div
|
||||
@@ -92,8 +96,9 @@ import { useWidgetStore } from '@/stores/widgetStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { renderMarkdownToHtml } from '@/utils/markdownRendererUtil'
|
||||
|
||||
const { nodeDef } = defineProps<{
|
||||
const { nodeDef, position = 'absolute' } = defineProps<{
|
||||
nodeDef: ComfyNodeDefV2
|
||||
position?: 'absolute' | 'relative'
|
||||
}>()
|
||||
|
||||
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<IconButton
|
||||
type="secondary"
|
||||
size="fit-content"
|
||||
class="group w-full justify-between gap-3 rounded-lg p-1 text-left font-normal hover:cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
class="group w-full justify-between gap-3 p-1 text-left font-normal hover:cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
|
||||
:aria-label="props.ariaLabel"
|
||||
@click="emit('click', $event)"
|
||||
>
|
||||
@@ -81,11 +81,11 @@
|
||||
>
|
||||
<i class="icon-[lucide--chevron-down] block size-4 leading-none" />
|
||||
</span>
|
||||
</IconButton>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import type {
|
||||
CompletionSummary,
|
||||
CompletionSummaryMode
|
||||
|
||||
@@ -42,19 +42,18 @@
|
||||
t('sideToolbar.queueProgressOverlay.running')
|
||||
}}</span>
|
||||
</span>
|
||||
<IconButton
|
||||
<Button
|
||||
v-if="runningCount > 0"
|
||||
v-tooltip.top="cancelJobTooltip"
|
||||
type="secondary"
|
||||
size="sm"
|
||||
class="size-6 bg-destructive-background hover:bg-destructive-background-hover"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.interruptAll')"
|
||||
@click="$emit('interruptAll')"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--x] block size-4 leading-none text-text-primary"
|
||||
/>
|
||||
</IconButton>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -64,26 +63,25 @@
|
||||
t('sideToolbar.queueProgressOverlay.queuedSuffix')
|
||||
}}</span>
|
||||
</span>
|
||||
<IconButton
|
||||
<Button
|
||||
v-if="queuedCount > 0"
|
||||
v-tooltip.top="clearQueueTooltip"
|
||||
type="secondary"
|
||||
size="sm"
|
||||
class="size-6 bg-secondary-background hover:bg-destructive-background"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.clearQueued')"
|
||||
@click="$emit('clearQueued')"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--list-x] block size-4 leading-none text-text-primary"
|
||||
/>
|
||||
</IconButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
class="min-w-30 flex-1 px-2 py-0"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
size="md"
|
||||
@click="$emit('viewAllJobs')"
|
||||
>
|
||||
{{ t('sideToolbar.queueProgressOverlay.viewAllJobs') }}
|
||||
@@ -96,7 +94,6 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
|
||||
|
||||
@@ -8,20 +8,15 @@
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-between px-3">
|
||||
<IconTextButton
|
||||
class="grow gap-1 p-2 text-center font-inter text-[12px] leading-none hover:opacity-90 justify-center"
|
||||
type="secondary"
|
||||
:label="t('sideToolbar.queueProgressOverlay.showAssets')"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.showAssets')"
|
||||
<Button
|
||||
class="grow gap-1 justify-center"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@click="$emit('showAssets')"
|
||||
>
|
||||
<template #icon>
|
||||
<div
|
||||
class="pointer-events-none block size-4 shrink-0 leading-none icon-[comfy--image-ai-edit]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<i class="icon-[comfy--image-ai-edit] size-4" />
|
||||
<span>{{ t('sideToolbar.queueProgressOverlay.showAssets') }}</span>
|
||||
</Button>
|
||||
<div class="ml-4 inline-flex items-center">
|
||||
<div
|
||||
class="inline-flex h-6 items-center text-[12px] leading-none text-text-primary opacity-90"
|
||||
@@ -31,18 +26,16 @@
|
||||
t('sideToolbar.queueProgressOverlay.queuedSuffix')
|
||||
}}</span>
|
||||
</div>
|
||||
<IconButton
|
||||
<Button
|
||||
v-if="queuedCount > 0"
|
||||
class="group ml-2 size-6 bg-secondary-background hover:bg-destructive-background"
|
||||
type="secondary"
|
||||
size="sm"
|
||||
class="ml-2"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.clearQueued')"
|
||||
@click="$emit('clearQueued')"
|
||||
>
|
||||
<i
|
||||
class="pointer-events-none icon-[lucide--list-x] block size-4 leading-none text-text-primary transition-colors group-hover:text-base-background"
|
||||
/>
|
||||
</IconButton>
|
||||
<i class="icon-[lucide--list-x] size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -80,8 +73,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import type {
|
||||
JobGroup,
|
||||
JobListItem,
|
||||
|
||||
@@ -18,18 +18,17 @@
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="!isCloud" class="flex items-center gap-1">
|
||||
<IconButton
|
||||
<Button
|
||||
v-tooltip.top="moreTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="size-6 bg-transparent hover:bg-secondary-background hover:opacity-100"
|
||||
variant="textonly"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.moreOptions')"
|
||||
@click="onMoreClick"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--more-horizontal] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
</IconButton>
|
||||
</Button>
|
||||
<Popover
|
||||
ref="morePopoverRef"
|
||||
:dismissable="true"
|
||||
@@ -47,19 +46,18 @@
|
||||
<div
|
||||
class="flex flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3 font-inter"
|
||||
>
|
||||
<IconTextButton
|
||||
class="w-full justify-start gap-2 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
|
||||
type="transparent"
|
||||
:label="t('sideToolbar.queueProgressOverlay.clearHistory')"
|
||||
<Button
|
||||
class="w-full justify-start"
|
||||
variant="textonly"
|
||||
size="sm"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.clearHistory')"
|
||||
@click="onClearHistoryFromMenu"
|
||||
>
|
||||
<template #icon>
|
||||
<i
|
||||
class="icon-[lucide--file-x-2] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<i class="icon-[lucide--file-x-2] size-4 text-muted" />
|
||||
<span>{{
|
||||
t('sideToolbar.queueProgressOverlay.clearHistory')
|
||||
}}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
@@ -72,8 +70,7 @@ import type { PopoverMethods } from 'primevue/popover'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
|
||||
|
||||
@@ -8,15 +8,14 @@
|
||||
<p class="m-0 text-[14px] font-normal leading-none">
|
||||
{{ t('sideToolbar.queueProgressOverlay.clearHistoryDialogTitle') }}
|
||||
</p>
|
||||
<IconButton
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="size-6 bg-transparent text-text-secondary hover:bg-secondary-background hover:opacity-100"
|
||||
<Button
|
||||
size="icon"
|
||||
variant="muted-textonly"
|
||||
:aria-label="t('g.close')"
|
||||
@click="onCancel"
|
||||
>
|
||||
<i class="icon-[lucide--x] block size-4 leading-none" />
|
||||
</IconButton>
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
<div class="flex flex-col gap-4 px-4 py-4 text-[14px] text-text-secondary">
|
||||
@@ -51,7 +50,6 @@
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
@@ -20,24 +20,23 @@
|
||||
<div v-if="entry.kind === 'divider'" class="px-2 py-1">
|
||||
<div class="h-px bg-interface-stroke" />
|
||||
</div>
|
||||
<IconTextButton
|
||||
<Button
|
||||
v-else
|
||||
class="w-full justify-start gap-2 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-interface-panel-hover-surface"
|
||||
type="transparent"
|
||||
:label="entry.label"
|
||||
class="w-full justify-start bg-transparent"
|
||||
variant="textonly"
|
||||
size="sm"
|
||||
:aria-label="entry.label"
|
||||
@click="onEntry(entry)"
|
||||
>
|
||||
<template #icon>
|
||||
<i
|
||||
v-if="entry.icon"
|
||||
:class="[
|
||||
entry.icon,
|
||||
'block size-4 shrink-0 leading-none text-text-secondary'
|
||||
]"
|
||||
/>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<i
|
||||
v-if="entry.icon"
|
||||
:class="[
|
||||
entry.icon,
|
||||
'block size-4 shrink-0 leading-none text-text-secondary'
|
||||
]"
|
||||
/>
|
||||
<span>{{ entry.label }}</span>
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</Popover>
|
||||
@@ -47,7 +46,7 @@
|
||||
import Popover from 'primevue/popover'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import type { MenuEntry } from '@/composables/queue/useJobMenu'
|
||||
|
||||
defineProps<{ entries: MenuEntry[] }>()
|
||||
|
||||
@@ -20,18 +20,15 @@
|
||||
class="flex min-w-0 items-center text-[0.75rem] leading-normal font-normal text-text-secondary"
|
||||
>
|
||||
<span class="block min-w-0 truncate">{{ row.value }}</span>
|
||||
<IconButton
|
||||
<Button
|
||||
v-if="row.canCopy"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="ml-2 size-6 bg-transparent hover:opacity-90"
|
||||
size="icon"
|
||||
variant="muted-textonly"
|
||||
:aria-label="copyAriaLabel"
|
||||
@click.stop="copyJobId"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--copy] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
</IconButton>
|
||||
<i class="icon-[lucide--copy] size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -61,31 +58,28 @@
|
||||
{{ t('queue.jobDetails.errorMessage') }}
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<IconTextButton
|
||||
class="h-6 justify-start gap-2 bg-transparent px-0 text-[0.75rem] leading-none text-text-secondary hover:opacity-90"
|
||||
type="transparent"
|
||||
:label="copyAriaLabel"
|
||||
:aria-label="copyAriaLabel"
|
||||
<Button
|
||||
class="justify-start px-0"
|
||||
variant="muted-textonly"
|
||||
size="sm"
|
||||
icon-position="right"
|
||||
@click.stop="copyErrorMessage"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--copy] block size-3.5 leading-none" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<IconTextButton
|
||||
class="h-6 justify-start gap-2 bg-transparent px-0 text-[0.75rem] leading-none text-text-secondary hover:opacity-90"
|
||||
type="transparent"
|
||||
:label="t('queue.jobDetails.report')"
|
||||
<span>{{ copyAriaLabel }}</span>
|
||||
<i class="icon-[lucide--copy] block size-3.5 leading-none" />
|
||||
</Button>
|
||||
<Button
|
||||
class="justify-start px-0"
|
||||
variant="muted-textonly"
|
||||
size="sm"
|
||||
icon-position="right"
|
||||
@click.stop="reportJobError"
|
||||
>
|
||||
<template #icon>
|
||||
<i
|
||||
class="icon-[lucide--message-circle-warning] block size-3.5 leading-none"
|
||||
/>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<span>{{ t('queue.jobDetails.report') }}</span>
|
||||
<i
|
||||
class="icon-[lucide--message-circle-warning] block size-3.5 leading-none"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
class="col-span-2 mt-2 rounded bg-interface-panel-hover-surface px-4 py-2 text-[0.75rem] leading-normal text-text-secondary"
|
||||
@@ -101,10 +95,8 @@
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { t } from '@/i18n'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
@@ -128,7 +120,7 @@ const workflowStore = useWorkflowStore()
|
||||
const queueStore = useQueueStore()
|
||||
const executionStore = useExecutionStore()
|
||||
const dialog = useDialogService()
|
||||
const { locale } = useI18n()
|
||||
const { locale, t } = useI18n()
|
||||
|
||||
const workflowValue = computed(() => {
|
||||
const wid = props.workflowId
|
||||
|
||||
@@ -15,23 +15,20 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-2 flex shrink-0 items-center gap-2">
|
||||
<IconButton
|
||||
<Button
|
||||
v-if="showWorkflowFilter"
|
||||
v-tooltip.top="filterTooltipConfig"
|
||||
type="secondary"
|
||||
size="sm"
|
||||
class="relative size-6 bg-secondary-background hover:bg-secondary-background-hover hover:opacity-90"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.filterJobs')"
|
||||
@click="onFilterClick"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--list-filter] block size-4 leading-none text-text-primary"
|
||||
/>
|
||||
<i class="icon-[lucide--list-filter] size-4" />
|
||||
<span
|
||||
v-if="selectedWorkflowFilter !== 'all'"
|
||||
class="pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground"
|
||||
/>
|
||||
</IconButton>
|
||||
</Button>
|
||||
<Popover
|
||||
v-if="showWorkflowFilter"
|
||||
ref="filterPopoverRef"
|
||||
@@ -50,59 +47,49 @@
|
||||
<div
|
||||
class="flex min-w-[12rem] flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3"
|
||||
>
|
||||
<IconTextButton
|
||||
class="w-full justify-between gap-1 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
|
||||
type="transparent"
|
||||
icon-position="right"
|
||||
:label="t('sideToolbar.queueProgressOverlay.filterAllWorkflows')"
|
||||
:aria-label="
|
||||
t('sideToolbar.queueProgressOverlay.filterAllWorkflows')
|
||||
"
|
||||
<Button
|
||||
class="w-full justify-between"
|
||||
variant="textonly"
|
||||
size="sm"
|
||||
@click="selectWorkflowFilter('all')"
|
||||
>
|
||||
<template #icon>
|
||||
<i
|
||||
v-if="selectedWorkflowFilter === 'all'"
|
||||
class="icon-[lucide--check] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<span>{{
|
||||
t('sideToolbar.queueProgressOverlay.filterAllWorkflows')
|
||||
}}</span>
|
||||
<i
|
||||
v-if="selectedWorkflowFilter === 'all'"
|
||||
class="icon-[lucide--check] size-4"
|
||||
/>
|
||||
</Button>
|
||||
<div class="mx-2 mt-1 h-px" />
|
||||
<IconTextButton
|
||||
class="w-full justify-between gap-1 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
|
||||
type="transparent"
|
||||
icon-position="right"
|
||||
:label="t('sideToolbar.queueProgressOverlay.filterCurrentWorkflow')"
|
||||
:aria-label="
|
||||
t('sideToolbar.queueProgressOverlay.filterCurrentWorkflow')
|
||||
"
|
||||
<Button
|
||||
class="w-full justify-between"
|
||||
variant="textonly"
|
||||
@click="selectWorkflowFilter('current')"
|
||||
>
|
||||
<template #icon>
|
||||
<i
|
||||
v-if="selectedWorkflowFilter === 'current'"
|
||||
class="icon-[lucide--check] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<span>{{
|
||||
t('sideToolbar.queueProgressOverlay.filterCurrentWorkflow')
|
||||
}}</span>
|
||||
<i
|
||||
v-if="selectedWorkflowFilter === 'current'"
|
||||
class="icon-[lucide--check] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</Popover>
|
||||
<IconButton
|
||||
<Button
|
||||
v-tooltip.top="sortTooltipConfig"
|
||||
type="secondary"
|
||||
size="sm"
|
||||
class="relative size-6 bg-secondary-background hover:bg-secondary-background-hover hover:opacity-90"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
:aria-label="t('sideToolbar.queueProgressOverlay.sortJobs')"
|
||||
@click="onSortClick"
|
||||
>
|
||||
<i
|
||||
class="icon-[lucide--arrow-up-down] block size-4 leading-none text-text-primary"
|
||||
/>
|
||||
<i class="icon-[lucide--arrow-up-down] size-4" />
|
||||
<span
|
||||
v-if="selectedSortMode !== 'mostRecent'"
|
||||
class="pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground"
|
||||
/>
|
||||
</IconButton>
|
||||
</Button>
|
||||
<Popover
|
||||
ref="sortPopoverRef"
|
||||
:dismissable="true"
|
||||
@@ -121,21 +108,18 @@
|
||||
class="flex min-w-[12rem] flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3"
|
||||
>
|
||||
<template v-for="(mode, index) in jobSortModes" :key="mode">
|
||||
<IconTextButton
|
||||
class="w-full justify-between gap-1 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
|
||||
type="transparent"
|
||||
icon-position="right"
|
||||
:label="sortLabel(mode)"
|
||||
:aria-label="sortLabel(mode)"
|
||||
<Button
|
||||
class="w-full justify-between"
|
||||
variant="textonly"
|
||||
size="sm"
|
||||
@click="selectSortMode(mode)"
|
||||
>
|
||||
<template #icon>
|
||||
<i
|
||||
v-if="selectedSortMode === mode"
|
||||
class="icon-[lucide--check] block size-4 leading-none text-text-secondary"
|
||||
/>
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<span>{{ sortLabel(mode) }}</span>
|
||||
<i
|
||||
v-if="selectedSortMode === mode"
|
||||
class="icon-[lucide--check] size-4 text-text-secondary"
|
||||
/>
|
||||
</Button>
|
||||
<div
|
||||
v-if="index < jobSortModes.length - 1"
|
||||
class="mx-2 mt-1 h-px"
|
||||
@@ -152,8 +136,6 @@ import Popover from 'primevue/popover'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { jobSortModes, jobTabs } from '@/composables/queue/useJobList'
|
||||
import type { JobSortMode, JobTab } from '@/composables/queue/useJobList'
|
||||
|
||||
@@ -128,51 +128,47 @@
|
||||
key="actions"
|
||||
class="inline-flex items-center gap-2 pr-1"
|
||||
>
|
||||
<IconButton
|
||||
<Button
|
||||
v-if="props.state === 'failed' && computedShowClear"
|
||||
v-tooltip.top="deleteTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="size-6 transform gap-1 rounded bg-destructive-background text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background-hover hover:opacity-95"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('g.delete')"
|
||||
@click.stop="onDeleteClick"
|
||||
>
|
||||
<i class="icon-[lucide--trash-2] size-4" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="
|
||||
props.state !== 'completed' &&
|
||||
props.state !== 'running' &&
|
||||
computedShowClear
|
||||
"
|
||||
v-tooltip.top="cancelTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="size-6 transform gap-1 rounded bg-destructive-background text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background-hover hover:opacity-95"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('g.cancel')"
|
||||
@click.stop="onCancelClick"
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</IconButton>
|
||||
</Button>
|
||||
<Button
|
||||
v-else-if="props.state === 'completed'"
|
||||
class="transform bg-modal-card-button-surface px-2 py-0 transition duration-150 ease-in-out hover:-translate-y-px hover:opacity-95"
|
||||
variant="textonly"
|
||||
size="sm"
|
||||
@click.stop="emit('view')"
|
||||
>{{ t('menuLabels.View') }}</Button
|
||||
>
|
||||
<IconButton
|
||||
<Button
|
||||
v-if="props.showMenu !== undefined ? props.showMenu : true"
|
||||
v-tooltip.top="moreTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="size-6 transform gap-1 rounded bg-modal-card-button-surface text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:opacity-95"
|
||||
variant="textonly"
|
||||
size="icon-sm"
|
||||
:aria-label="t('g.more')"
|
||||
@click.stop="emit('menu', $event)"
|
||||
>
|
||||
<i class="icon-[lucide--more-horizontal] size-4" />
|
||||
</IconButton>
|
||||
</Button>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="props.state !== 'running'"
|
||||
@@ -183,17 +179,16 @@
|
||||
</div>
|
||||
</Transition>
|
||||
<!-- Running job cancel button - always visible -->
|
||||
<IconButton
|
||||
<Button
|
||||
v-if="props.state === 'running' && computedShowClear"
|
||||
v-tooltip.top="cancelTooltipConfig"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="size-6 transform gap-1 rounded bg-destructive-background text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background-hover hover:opacity-95"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
:aria-label="t('g.cancel')"
|
||||
@click.stop="onCancelClick"
|
||||
>
|
||||
<i class="icon-[lucide--x] size-4" />
|
||||
</IconButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,7 +198,6 @@
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import JobDetailsPopover from '@/components/queue/job/JobDetailsPopover.vue'
|
||||
import QueueAssetPreview from '@/components/queue/job/QueueAssetPreview.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
|
||||
@@ -3,10 +3,10 @@ import { storeToRefs } from 'pinia'
|
||||
import { computed, ref, toValue, watchEffect } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import Tab from '@/components/tab/Tab.vue'
|
||||
import TabList from '@/components/tab/TabList.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
@@ -140,16 +140,11 @@ function handleTitleCancel() {
|
||||
</h3>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<IconButton
|
||||
<Button
|
||||
v-if="isSubgraphNode"
|
||||
type="transparent"
|
||||
size="sm"
|
||||
:class="
|
||||
cn(
|
||||
'bg-secondary-background hover:bg-secondary-background-hover text-base-foreground',
|
||||
isEditingSubgraph && 'bg-secondary-background-selected'
|
||||
)
|
||||
"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
:class="cn(isEditingSubgraph && 'bg-secondary-background-selected')"
|
||||
@click="
|
||||
rightSidePanelStore.openPanel(
|
||||
isEditingSubgraph ? 'parameters' : 'subgraph'
|
||||
@@ -157,17 +152,16 @@ function handleTitleCancel() {
|
||||
"
|
||||
>
|
||||
<i class="icon-[lucide--settings-2]" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
type="transparent"
|
||||
size="sm"
|
||||
class="bg-secondary-background hover:bg-secondary-background-hover text-base-foreground"
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
:aria-pressed="rightSidePanelStore.isOpen"
|
||||
:aria-label="t('rightSidePanel.togglePanel')"
|
||||
@click="closePanel"
|
||||
>
|
||||
<i class="icon-[lucide--panel-right] size-4" />
|
||||
</IconButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<nav v-if="hasSelection" class="px-4 pb-2 pt-1">
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import ToggleSwitch from 'primevue/toggleswitch'
|
||||
import { computed } from 'vue'
|
||||
import { computed, shallowRef, triggerRef, watchEffect } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
@@ -90,10 +90,23 @@ import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
import { adjustColor } from '@/utils/colorUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { nodes = [] } = defineProps<{
|
||||
const props = defineProps<{
|
||||
nodes?: LGraphNode[]
|
||||
}>()
|
||||
|
||||
/**
|
||||
* This is not random writing. It is very important.
|
||||
* Otherwise, the UI cannot be updated correctly.
|
||||
*/
|
||||
const targetNodes = shallowRef<LGraphNode[]>([])
|
||||
watchEffect(() => {
|
||||
if (props.nodes) {
|
||||
targetNodes.value = props.nodes
|
||||
} else {
|
||||
targetNodes.value = []
|
||||
}
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
@@ -103,24 +116,33 @@ const isLightTheme = computed(
|
||||
)
|
||||
|
||||
const nodeState = computed({
|
||||
get(): LGraphNode['mode'] | null {
|
||||
if (!nodes.length) return null
|
||||
if (nodes.length === 1) {
|
||||
return nodes[0].mode
|
||||
}
|
||||
get() {
|
||||
let mode: LGraphNode['mode'] | null = null
|
||||
const nodes = targetNodes.value
|
||||
|
||||
if (nodes.length === 0) return null
|
||||
|
||||
// For multiple nodes, if all nodes have the same mode, return that mode, otherwise return null
|
||||
const mode: LGraphNode['mode'] = nodes[0].mode
|
||||
if (!nodes.every((node) => node.mode === mode)) {
|
||||
return null
|
||||
if (nodes.length > 1) {
|
||||
mode = nodes[0].mode
|
||||
if (!nodes.every((node) => node.mode === mode)) {
|
||||
mode = null
|
||||
}
|
||||
} else {
|
||||
mode = nodes[0].mode
|
||||
}
|
||||
|
||||
return mode
|
||||
},
|
||||
set(value: LGraphNode['mode']) {
|
||||
nodes.forEach((node) => {
|
||||
targetNodes.value.forEach((node) => {
|
||||
node.mode = value
|
||||
})
|
||||
/*
|
||||
* This is not random writing. It is very important.
|
||||
* Otherwise, the UI cannot be updated correctly.
|
||||
*/
|
||||
triggerRef(targetNodes)
|
||||
canvasStore.canvas?.setDirty(true, true)
|
||||
}
|
||||
})
|
||||
@@ -128,10 +150,15 @@ const nodeState = computed({
|
||||
// Pinned state
|
||||
const isPinned = computed<boolean>({
|
||||
get() {
|
||||
return nodes.some((node) => node.pinned)
|
||||
return targetNodes.value.some((node) => node.pinned)
|
||||
},
|
||||
set(value) {
|
||||
nodes.forEach((node) => node.pin(value))
|
||||
targetNodes.value.forEach((node) => node.pin(value))
|
||||
/*
|
||||
* This is not random writing. It is very important.
|
||||
* Otherwise, the UI cannot be updated correctly.
|
||||
*/
|
||||
triggerRef(targetNodes)
|
||||
canvasStore.canvas?.setDirty(true, true)
|
||||
}
|
||||
})
|
||||
@@ -175,8 +202,10 @@ const colorOptions: NodeColorOption[] = [
|
||||
|
||||
const nodeColor = computed<NodeColorOption['name'] | null>({
|
||||
get() {
|
||||
if (nodes.length === 0) return null
|
||||
const theColorOptions = nodes.map((item) => item.getColorOption())
|
||||
if (targetNodes.value.length === 0) return null
|
||||
const theColorOptions = targetNodes.value.map((item) =>
|
||||
item.getColorOption()
|
||||
)
|
||||
|
||||
let colorOption: ColorOption | null | false = theColorOptions[0]
|
||||
if (!theColorOptions.every((option) => option === colorOption)) {
|
||||
@@ -202,9 +231,14 @@ const nodeColor = computed<NodeColorOption['name'] | null>({
|
||||
? null
|
||||
: LGraphCanvas.node_colors[colorName]
|
||||
|
||||
for (const item of nodes) {
|
||||
for (const item of targetNodes.value) {
|
||||
item.setColorOption(canvasColorOption)
|
||||
}
|
||||
/*
|
||||
* This is not random writing. It is very important.
|
||||
* Otherwise, the UI cannot be updated correctly.
|
||||
*/
|
||||
triggerRef(targetNodes)
|
||||
canvasStore.canvas?.setDirty(true, true)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -37,15 +37,10 @@
|
||||
<template #header>
|
||||
<!-- Job Detail View Header -->
|
||||
<div v-if="isInFolderView" class="px-2 2xl:px-4">
|
||||
<IconTextButton
|
||||
:label="$t('sideToolbar.backToAssets')"
|
||||
type="secondary"
|
||||
@click="exitFolderView"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--arrow-left] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<Button variant="secondary" size="lg" @click="exitFolderView">
|
||||
<i class="icon-[lucide--arrow-left] size-4" />
|
||||
<span>{{ $t('sideToolbar.backToAssets') }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Filter Bar -->
|
||||
@@ -128,42 +123,34 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 pr-4">
|
||||
<div class="flex shrink gap-2 pr-4 items-center-safe justify-end-safe">
|
||||
<template v-if="isCompact">
|
||||
<!-- Compact mode: Icon only -->
|
||||
<IconButton
|
||||
<Button
|
||||
v-if="shouldShowDeleteButton"
|
||||
size="icon"
|
||||
@click="handleDeleteSelected"
|
||||
>
|
||||
<i class="icon-[lucide--trash-2] size-4" />
|
||||
</IconButton>
|
||||
<IconButton @click="handleDownloadSelected">
|
||||
</Button>
|
||||
<Button size="icon" @click="handleDownloadSelected">
|
||||
<i class="icon-[lucide--download] size-4" />
|
||||
</IconButton>
|
||||
</Button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- Normal mode: Icon + Text -->
|
||||
<IconTextButton
|
||||
<Button
|
||||
v-if="shouldShowDeleteButton"
|
||||
:label="$t('mediaAsset.selection.deleteSelected')"
|
||||
type="secondary"
|
||||
icon-position="right"
|
||||
variant="secondary"
|
||||
@click="handleDeleteSelected"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--trash-2] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<IconTextButton
|
||||
:label="$t('mediaAsset.selection.downloadSelected')"
|
||||
type="secondary"
|
||||
icon-position="right"
|
||||
@click="handleDownloadSelected"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--download] size-4" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<span>{{ $t('mediaAsset.selection.deleteSelected') }}</span>
|
||||
<i class="icon-[lucide--trash-2] size-4" />
|
||||
</Button>
|
||||
<Button variant="secondary" @click="handleDownloadSelected">
|
||||
<span>{{ $t('mediaAsset.selection.downloadSelected') }}</span>
|
||||
<i class="icon-[lucide--download] size-4" />
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -183,8 +170,6 @@ import { useToast } from 'primevue/usetoast'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
|
||||
import VirtualGrid from '@/components/common/VirtualGrid.vue'
|
||||
import Load3dViewerContent from '@/components/load3d/Load3dViewerContent.vue'
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</template>
|
||||
<template #end>
|
||||
<div
|
||||
class="touch:w-auto touch:opacity-100 flex flex-row transition-all duration-200 motion-safe:w-0 motion-safe:opacity-0 motion-safe:group-focus-within/sidebar-tab:w-auto motion-safe:group-focus-within/sidebar-tab:opacity-100 motion-safe:group-hover/sidebar-tab:w-auto motion-safe:group-hover/sidebar-tab:opacity-100"
|
||||
class="touch:w-auto touch:opacity-100 flex flex-row overflow-hidden transition-all duration-200 motion-safe:w-0 motion-safe:opacity-0 motion-safe:group-focus-within/sidebar-tab:w-auto motion-safe:group-focus-within/sidebar-tab:opacity-100 motion-safe:group-hover/sidebar-tab:w-auto motion-safe:group-hover/sidebar-tab:opacity-100"
|
||||
>
|
||||
<slot name="tool-buttons" />
|
||||
</div>
|
||||
|
||||
@@ -19,14 +19,32 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { whenever } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import NodeHelpContent from '@/components/node/NodeHelpContent.vue'
|
||||
import { useSelectionState } from '@/composables/graph/useSelectionState'
|
||||
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore'
|
||||
|
||||
const { node } = defineProps<{ node: ComfyNodeDefImpl }>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
const nodeHelpStore = useNodeHelpStore()
|
||||
const { nodeDef } = useSelectionState()
|
||||
|
||||
const activeHelpDef = computed(() =>
|
||||
nodeHelpStore.isHelpOpen ? nodeDef.value : null
|
||||
)
|
||||
|
||||
// Keep the open help page synced with the current selection while help is open.
|
||||
whenever(activeHelpDef, (def) => {
|
||||
const currentHelpNode = nodeHelpStore.currentHelpNode
|
||||
if (currentHelpNode?.nodePath === def.nodePath) return
|
||||
nodeHelpStore.openHelp(def)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -69,14 +69,27 @@ vi.mock('@/services/dialogService', () => ({
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock the firebaseAuthStore
|
||||
// Mock the firebaseAuthStore with hoisted state for per-test manipulation
|
||||
const mockAuthStoreState = vi.hoisted(() => ({
|
||||
balance: {
|
||||
amount_micros: 100_000,
|
||||
effective_balance_micros: 100_000,
|
||||
currency: 'usd'
|
||||
} as {
|
||||
amount_micros?: number
|
||||
effective_balance_micros?: number
|
||||
currency: string
|
||||
},
|
||||
isFetchingBalance: false
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/firebaseAuthStore', () => ({
|
||||
useFirebaseAuthStore: vi.fn(() => ({
|
||||
getAuthHeader: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ Authorization: 'Bearer mock-token' }),
|
||||
balance: { amount_micros: 100_000 }, // 100,000 cents = ~211,000 credits
|
||||
isFetchingBalance: false
|
||||
balance: mockAuthStoreState.balance,
|
||||
isFetchingBalance: mockAuthStoreState.isFetchingBalance
|
||||
}))
|
||||
}))
|
||||
|
||||
@@ -131,15 +144,9 @@ vi.mock('@/base/credits/comfyCredits', () => ({
|
||||
// Mock useExternalLink
|
||||
vi.mock('@/composables/useExternalLink', () => ({
|
||||
useExternalLink: vi.fn(() => ({
|
||||
buildDocsUrl: vi.fn((path) => `https://docs.comfy.org${path}`)
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock useFeatureFlags
|
||||
vi.mock('@/composables/useFeatureFlags', () => ({
|
||||
useFeatureFlags: vi.fn(() => ({
|
||||
flags: {
|
||||
subscriptionTiersEnabled: true
|
||||
buildDocsUrl: vi.fn((path) => `https://docs.comfy.org${path}`),
|
||||
docsPaths: {
|
||||
partnerNodesPricing: '/tutorials/partner-nodes/pricing'
|
||||
}
|
||||
}))
|
||||
}))
|
||||
@@ -168,6 +175,12 @@ vi.mock('@/platform/cloud/subscription/components/SubscribeButton.vue', () => ({
|
||||
describe('CurrentUserPopover', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockAuthStoreState.balance = {
|
||||
amount_micros: 100_000,
|
||||
effective_balance_micros: 100_000,
|
||||
currency: 'usd'
|
||||
}
|
||||
mockAuthStoreState.isFetchingBalance = false
|
||||
})
|
||||
|
||||
const mountComponent = (): VueWrapper => {
|
||||
@@ -304,4 +317,103 @@ describe('CurrentUserPopover', () => {
|
||||
expect(wrapper.emitted('close')).toBeTruthy()
|
||||
expect(wrapper.emitted('close')!.length).toBe(1)
|
||||
})
|
||||
|
||||
describe('effective_balance_micros handling', () => {
|
||||
it('uses effective_balance_micros when present (positive balance)', () => {
|
||||
mockAuthStoreState.balance = {
|
||||
amount_micros: 200_000,
|
||||
effective_balance_micros: 150_000,
|
||||
currency: 'usd'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(formatCreditsFromCents).toHaveBeenCalledWith({
|
||||
cents: 150_000,
|
||||
locale: 'en',
|
||||
numberOptions: {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2
|
||||
}
|
||||
})
|
||||
expect(wrapper.text()).toContain('1500')
|
||||
})
|
||||
|
||||
it('uses effective_balance_micros when zero', () => {
|
||||
mockAuthStoreState.balance = {
|
||||
amount_micros: 100_000,
|
||||
effective_balance_micros: 0,
|
||||
currency: 'usd'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(formatCreditsFromCents).toHaveBeenCalledWith({
|
||||
cents: 0,
|
||||
locale: 'en',
|
||||
numberOptions: {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2
|
||||
}
|
||||
})
|
||||
expect(wrapper.text()).toContain('0')
|
||||
})
|
||||
|
||||
it('uses effective_balance_micros when negative', () => {
|
||||
mockAuthStoreState.balance = {
|
||||
amount_micros: 0,
|
||||
effective_balance_micros: -50_000,
|
||||
currency: 'usd'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(formatCreditsFromCents).toHaveBeenCalledWith({
|
||||
cents: -50_000,
|
||||
locale: 'en',
|
||||
numberOptions: {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2
|
||||
}
|
||||
})
|
||||
expect(wrapper.text()).toContain('-500')
|
||||
})
|
||||
|
||||
it('falls back to amount_micros when effective_balance_micros is missing', () => {
|
||||
mockAuthStoreState.balance = {
|
||||
amount_micros: 100_000,
|
||||
currency: 'usd'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(formatCreditsFromCents).toHaveBeenCalledWith({
|
||||
cents: 100_000,
|
||||
locale: 'en',
|
||||
numberOptions: {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2
|
||||
}
|
||||
})
|
||||
expect(wrapper.text()).toContain('1000')
|
||||
})
|
||||
|
||||
it('falls back to 0 when both effective_balance_micros and amount_micros are missing', () => {
|
||||
mockAuthStoreState.balance = {
|
||||
currency: 'usd'
|
||||
}
|
||||
|
||||
const wrapper = mountComponent()
|
||||
|
||||
expect(formatCreditsFromCents).toHaveBeenCalledWith({
|
||||
cents: 0,
|
||||
locale: 'en',
|
||||
numberOptions: {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2
|
||||
}
|
||||
})
|
||||
expect(wrapper.text()).toContain('0')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -42,7 +42,6 @@
|
||||
formattedBalance
|
||||
}}</span>
|
||||
<i
|
||||
v-if="flags.subscriptionTiersEnabled"
|
||||
v-tooltip="{ value: $t('credits.unified.tooltip'), showDelay: 300 }"
|
||||
class="icon-[lucide--circle-help] cursor-help text-base text-muted-foreground mr-auto"
|
||||
/>
|
||||
@@ -147,7 +146,6 @@ import UserAvatar from '@/components/common/UserAvatar.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
|
||||
@@ -160,7 +158,7 @@ const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const { buildDocsUrl } = useExternalLink()
|
||||
const { buildDocsUrl, docsPaths } = useExternalLink()
|
||||
|
||||
const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
|
||||
useCurrentUser()
|
||||
@@ -174,12 +172,13 @@ const {
|
||||
fetchStatus
|
||||
} = useSubscription()
|
||||
const subscriptionDialog = useSubscriptionDialog()
|
||||
const { flags } = useFeatureFlags()
|
||||
const { locale } = useI18n()
|
||||
|
||||
const formattedBalance = computed(() => {
|
||||
// Backend returns cents despite the *_micros naming convention.
|
||||
const cents = authStore.balance?.amount_micros ?? 0
|
||||
const cents =
|
||||
authStore.balance?.effective_balance_micros ??
|
||||
authStore.balance?.amount_micros ??
|
||||
0
|
||||
return formatCreditsFromCents({
|
||||
cents,
|
||||
locale: locale.value,
|
||||
@@ -192,7 +191,9 @@ const formattedBalance = computed(() => {
|
||||
|
||||
const canUpgrade = computed(() => {
|
||||
const tier = subscriptionTier.value
|
||||
return tier === 'STANDARD' || tier === 'CREATOR'
|
||||
return (
|
||||
tier === 'FOUNDERS_EDITION' || tier === 'STANDARD' || tier === 'CREATOR'
|
||||
)
|
||||
})
|
||||
|
||||
const handleOpenUserSettings = () => {
|
||||
@@ -224,9 +225,7 @@ const handleTopUp = () => {
|
||||
|
||||
const handleOpenPartnerNodesInfo = () => {
|
||||
window.open(
|
||||
buildDocsUrl('/tutorials/partner-nodes/pricing', {
|
||||
includeLocale: true
|
||||
}),
|
||||
buildDocsUrl(docsPaths.partnerNodesPricing, { includeLocale: true }),
|
||||
'_blank'
|
||||
)
|
||||
emit('close')
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
import type {
|
||||
Meta,
|
||||
StoryObj,
|
||||
ComponentPropsAndSlots
|
||||
} from '@storybook/vue3-vite'
|
||||
|
||||
import Button from './Button.vue'
|
||||
import { FOR_STORIES } from '@/components/ui/button/button.variants'
|
||||
|
||||
interface ButtonPropsAndStoryArgs extends ComponentPropsAndSlots<
|
||||
typeof Button
|
||||
> {
|
||||
icon?: 'left' | 'right'
|
||||
}
|
||||
|
||||
const { variants, sizes } = FOR_STORIES
|
||||
const meta: Meta<typeof Button> = {
|
||||
const meta: Meta<ButtonPropsAndStoryArgs> = {
|
||||
title: 'Components/Button/Button',
|
||||
component: Button,
|
||||
tags: ['autodocs'],
|
||||
@@ -22,13 +32,19 @@ const meta: Meta<typeof Button> = {
|
||||
as: { defaultValue: 'button' },
|
||||
asChild: { defaultValue: false },
|
||||
default: {
|
||||
control: { type: 'text' },
|
||||
defaultValue: 'Button'
|
||||
},
|
||||
icon: {
|
||||
control: { type: 'select' },
|
||||
options: [undefined, 'left', 'right']
|
||||
}
|
||||
},
|
||||
args: {
|
||||
variant: 'secondary',
|
||||
size: 'md',
|
||||
default: 'Button'
|
||||
default: 'Button',
|
||||
icon: undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,10 +52,18 @@ export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const SingleButton: Story = {
|
||||
args: {
|
||||
variant: 'primary',
|
||||
size: 'lg'
|
||||
}
|
||||
render: (args) => ({
|
||||
components: { Button },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: `
|
||||
<Button v-bind="args">
|
||||
<i v-if="args.icon === 'left'" class="icon-[lucide--settings]" />
|
||||
{{args.default}}
|
||||
<i v-if="args.icon === 'right'" class="icon-[lucide--settings]" />
|
||||
</Button>`
|
||||
})
|
||||
}
|
||||
|
||||
function generateVariants() {
|
||||
@@ -47,7 +71,13 @@ function generateVariants() {
|
||||
for (const variant of variants) {
|
||||
for (const size of sizes) {
|
||||
variantButtons.push(
|
||||
`<Button variant="${variant}" size="${size}">${size === 'icon' ? `<i class="icon-[lucide--settings]" />` : variant}</Button>`
|
||||
`<Button
|
||||
variant="${variant}"
|
||||
size="${size}">${
|
||||
size.startsWith('icon')
|
||||
? `<i class="icon-[lucide--settings]" />`
|
||||
: variant
|
||||
}</Button>`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -59,7 +89,7 @@ export const AllVariants: Story = {
|
||||
render: () => ({
|
||||
components: { Button },
|
||||
template: `
|
||||
<div class="grid grid-cols-4 gap-4 place-items-center-safe">
|
||||
<div class="grid grid-cols-5 gap-4 place-items-center-safe">
|
||||
${generateVariants().join('\n')}
|
||||
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,8 @@ export const buttonVariants = cva({
|
||||
sm: 'h-6 rounded-sm px-2 py-1 text-xs',
|
||||
md: 'h-8 rounded-lg p-2 text-xs',
|
||||
lg: 'h-10 rounded-lg px-4 py-2 text-sm',
|
||||
icon: 'size-9'
|
||||
icon: 'size-8',
|
||||
'icon-sm': 'size-5 p-0'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -42,7 +43,7 @@ const variants = [
|
||||
'textonly',
|
||||
'muted-textonly'
|
||||
] as const satisfies Array<ButtonVariants['variant']>
|
||||
const sizes = ['sm', 'md', 'lg', 'icon'] as const satisfies Array<
|
||||
const sizes = ['sm', 'md', 'lg', 'icon', 'icon-sm'] as const satisfies Array<
|
||||
ButtonVariants['size']
|
||||
>
|
||||
|
||||
|
||||
@@ -17,43 +17,34 @@
|
||||
|
||||
<template #header-right-area>
|
||||
<div class="flex gap-2">
|
||||
<IconTextButton
|
||||
type="primary"
|
||||
:label="$t('g.upload')"
|
||||
@click="() => {}"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--upload]" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<Button variant="primary" @click="() => {}">
|
||||
<i class="icon-[lucide--upload]" />
|
||||
<span>{{ $t('g.upload') }}</span>
|
||||
</Button>
|
||||
<MoreButton>
|
||||
<template #default="{ close }">
|
||||
<IconTextButton
|
||||
type="secondary"
|
||||
:label="$t('g.settings')"
|
||||
<Button
|
||||
variant="secondary"
|
||||
@click="
|
||||
() => {
|
||||
close()
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--download]" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<IconTextButton
|
||||
type="primary"
|
||||
:label="$t('g.profile')"
|
||||
<i class="icon-[lucide--download]" />
|
||||
<span>{{ $t('g.settings') }}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
@click="
|
||||
() => {
|
||||
close()
|
||||
}
|
||||
"
|
||||
>
|
||||
<template #icon>
|
||||
<i class="icon-[lucide--scroll]" />
|
||||
</template>
|
||||
</IconTextButton>
|
||||
<i class="icon-[lucide--scroll]" />
|
||||
<span>{{ $t('g.profile') }}</span>
|
||||
</Button>
|
||||
</template>
|
||||
</MoreButton>
|
||||
</div>
|
||||
@@ -99,12 +90,13 @@
|
||||
<div class="h-full w-full bg-blue-500"></div>
|
||||
</template>
|
||||
<template #top-right>
|
||||
<IconButton
|
||||
<Button
|
||||
size="icon"
|
||||
class="!bg-white !text-neutral-900"
|
||||
@click="() => {}"
|
||||
>
|
||||
<i class="icon-[lucide--info]" />
|
||||
</IconButton>
|
||||
</Button>
|
||||
</template>
|
||||
<template #bottom-right>
|
||||
<SquareChip label="png" />
|
||||
@@ -133,8 +125,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, provide, ref } from 'vue'
|
||||
|
||||
import IconButton from '@/components/button/IconButton.vue'
|
||||
import IconTextButton from '@/components/button/IconTextButton.vue'
|
||||
import MoreButton from '@/components/button/MoreButton.vue'
|
||||
import CardBottom from '@/components/card/CardBottom.vue'
|
||||
import CardContainer from '@/components/card/CardContainer.vue'
|
||||
@@ -143,6 +133,7 @@ import SquareChip from '@/components/chip/SquareChip.vue'
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import MultiSelect from '@/components/input/MultiSelect.vue'
|
||||
import SingleSelect from '@/components/input/SingleSelect.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
|
||||
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
|
||||
import RightSidePanel from '@/components/widget/panel/RightSidePanel.vue'
|
||||
|
||||