refactor(*): new button
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { createBackgroundRemovalAdapter } from '@proj-airi/stage-ui/libs/inference/adapters/background-removal'
|
||||
import { Button, Checkbox, InputFile } from '@proj-airi/ui'
|
||||
import { Button, Checkbox, GhostButton, InputFile } from '@proj-airi/ui'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
const adapter = createBackgroundRemovalAdapter()
|
||||
@@ -228,14 +228,14 @@ function hidePreview() {
|
||||
/>
|
||||
<Button
|
||||
v-if="doneCount > 0"
|
||||
variant="secondary"
|
||||
|
||||
:label="`Download All (${doneCount})`"
|
||||
icon="i-solar:download-minimalistic-bold"
|
||||
@click="downloadAllImages"
|
||||
/>
|
||||
<Button
|
||||
<GhostButton
|
||||
v-if="imageItems.length > 0"
|
||||
variant="secondary-muted"
|
||||
|
||||
label="Clear All"
|
||||
icon="i-solar:trash-bin-trash-line-duotone"
|
||||
@click="clearAllImages"
|
||||
@@ -343,11 +343,12 @@ function hidePreview() {
|
||||
@click="downloadImage(index)"
|
||||
/>
|
||||
<Button
|
||||
variant="danger"
|
||||
|
||||
size="sm"
|
||||
icon="i-solar:trash-bin-trash-bold"
|
||||
:disabled="item.status === 'processing'"
|
||||
@click="removeImage(index)"
|
||||
color="red"
|
||||
variant="primary" @click="removeImage(index)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { CapacitorBarcodeScanner, CapacitorBarcodeScannerTypeHint } from '@capac
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { parseServerChannelQrPayload } from '@proj-airi/stage-shared/server-channel-qr'
|
||||
import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server'
|
||||
import { Button } from '@proj-airi/ui'
|
||||
import { GhostButton } from '@proj-airi/ui'
|
||||
import { shallowRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { toast } from 'vue-sonner'
|
||||
@@ -56,8 +56,8 @@ async function scanServerChannelQrCode() {
|
||||
{{ t('settings.pages.connection.qr-scan.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary-muted"
|
||||
<GhostButton
|
||||
|
||||
:loading="scanning"
|
||||
:label="t('settings.pages.connection.qr-scan.action')"
|
||||
@click="scanServerChannelQrCode"
|
||||
|
||||
@@ -45,6 +45,7 @@ import { setupCaptionWindowManager } from './windows/caption'
|
||||
import { setupChatWindowReusableFunc } from './windows/chat'
|
||||
import { isDesktopOverlayEnabled, setupDesktopOverlayWindow } from './windows/desktop-overlay'
|
||||
import { setupDevtoolsWindow } from './windows/devtools'
|
||||
import { setupEditorWindowManager } from './windows/editor'
|
||||
import { setupMainWindow } from './windows/main'
|
||||
import { setupNoticeWindowManager } from './windows/notice'
|
||||
import { setupOnboardingWindowManager } from './windows/onboarding'
|
||||
@@ -212,6 +213,11 @@ app.whenReady().then(async () => {
|
||||
build: ({ dependsOn }) => setupSpotlightWindowManager(dependsOn),
|
||||
})
|
||||
|
||||
const editorWindow = injeca.provide('windows:editor', {
|
||||
dependsOn: { serverChannel, i18n },
|
||||
build: ({ dependsOn }) => setupEditorWindowManager(dependsOn),
|
||||
})
|
||||
|
||||
const settingsWindow = injeca.provide('windows:settings', {
|
||||
dependsOn: { widgetsManager, beatSync, autoUpdater, devtoolsWindow: devtoolsMarkdownStressWindow, serverChannel, godotStageManager, mcpStdioManager, i18n, windowAuthManager, globalShortcut, spotlightWindow },
|
||||
build: async ({ dependsOn }) =>
|
||||
@@ -222,7 +228,7 @@ app.whenReady().then(async () => {
|
||||
})
|
||||
|
||||
const mainWindow = injeca.provide('windows:main', {
|
||||
dependsOn: { settingsWindow, chatWindow, widgetsManager, noticeWindow, beatSync, autoUpdater, serverChannel, godotStageManager, mcpStdioManager, i18n, onboardingWindowManager, windowAuthManager },
|
||||
dependsOn: { editorWindow, settingsWindow, chatWindow, widgetsManager, noticeWindow, beatSync, autoUpdater, serverChannel, godotStageManager, mcpStdioManager, i18n, onboardingWindowManager, windowAuthManager },
|
||||
build: async ({ dependsOn }) => setupMainWindow({
|
||||
...dependsOn,
|
||||
onWindowCreated: (window) => {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { BrowserWindow } from 'electron'
|
||||
|
||||
import type { I18n } from '../../libs/i18n'
|
||||
import type { ServerChannel } from '../../services/airi/channel-server'
|
||||
|
||||
import { join, resolve } from 'node:path'
|
||||
|
||||
import { BrowserWindow as ElectronBrowserWindow } from 'electron'
|
||||
|
||||
import icon from '../../../../resources/icon.png?asset'
|
||||
|
||||
import { baseUrl, getElectronMainDirname, load, withHashRoute } from '../../libs/electron/location'
|
||||
import { createReusableWindow } from '../../libs/electron/window-manager'
|
||||
import { protectPrivilegedWindowNavigation, toggleWindowShow } from '../shared'
|
||||
import { setupEditorWindowInvokes } from './rpc/index.electron'
|
||||
|
||||
export interface EditorWindowManager {
|
||||
/** Returns the live editor window, creating it when necessary. */
|
||||
getWindow: () => Promise<BrowserWindow>
|
||||
/** Opens and focuses the reusable editor window. */
|
||||
openWindow: () => Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the reusable window boundary for the Tamagotchi editor.
|
||||
*
|
||||
* The renderer starts at an empty Tamagotchi-owned route so the editor can
|
||||
* evolve independently from the current settings window and shared pages.
|
||||
*/
|
||||
export function setupEditorWindowManager(params: {
|
||||
i18n: I18n
|
||||
serverChannel: ServerChannel
|
||||
}): EditorWindowManager {
|
||||
const rendererBase = baseUrl(resolve(getElectronMainDirname(), '..', 'renderer'))
|
||||
const reusable = createReusableWindow(async () => {
|
||||
const window = new ElectronBrowserWindow({
|
||||
title: 'AIRI Editor',
|
||||
width: 1200,
|
||||
height: 800,
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
show: false,
|
||||
icon,
|
||||
webPreferences: {
|
||||
preload: join(getElectronMainDirname(), '../preload/index.mjs'),
|
||||
sandbox: false,
|
||||
},
|
||||
})
|
||||
|
||||
window.on('ready-to-show', () => window.show())
|
||||
protectPrivilegedWindowNavigation(window)
|
||||
|
||||
await setupEditorWindowInvokes({
|
||||
window,
|
||||
i18n: params.i18n,
|
||||
serverChannel: params.serverChannel,
|
||||
})
|
||||
await load(window, withHashRoute(rendererBase, '/editor'))
|
||||
|
||||
return window
|
||||
})
|
||||
|
||||
async function openWindow() {
|
||||
toggleWindowShow(await reusable.getWindow())
|
||||
}
|
||||
|
||||
return {
|
||||
getWindow: reusable.getWindow,
|
||||
openWindow,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { BrowserWindow } from 'electron'
|
||||
|
||||
import type { I18n } from '../../../libs/i18n'
|
||||
import type { ServerChannel } from '../../../services/airi/channel-server'
|
||||
|
||||
import { createContext } from '@moeru/eventa/adapters/electron/main'
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
import { setupBaseWindowElectronInvokes } from '../../shared/window'
|
||||
|
||||
/**
|
||||
* Registers only the Electron services required by the empty editor shell.
|
||||
* Feature-specific RPC handlers should be added here as the editor gains capabilities.
|
||||
*/
|
||||
export async function setupEditorWindowInvokes(params: {
|
||||
window: BrowserWindow
|
||||
i18n: I18n
|
||||
serverChannel: ServerChannel
|
||||
}) {
|
||||
// TODO: Remove this once Eventa supports window-namespaced Electron contexts.
|
||||
ipcMain.setMaxListeners(0)
|
||||
|
||||
const { context } = createContext(ipcMain, params.window)
|
||||
|
||||
await setupBaseWindowElectronInvokes({
|
||||
context,
|
||||
window: params.window,
|
||||
i18n: params.i18n,
|
||||
serverChannel: params.serverChannel,
|
||||
})
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import type { ServerChannel } from '../../services/airi/channel-server'
|
||||
import type { GodotStageManager } from '../../services/airi/godot-stage'
|
||||
import type { McpStdioManager } from '../../services/airi/mcp-servers'
|
||||
import type { AutoUpdater } from '../../services/electron/auto-updater'
|
||||
import type { EditorWindowManager } from '../editor'
|
||||
import type { NoticeWindowManager } from '../notice'
|
||||
import type { OnboardingWindowManager } from '../onboarding'
|
||||
import type { SettingsWindowManager } from '../settings'
|
||||
@@ -50,6 +51,7 @@ const appConfigSchema = object({
|
||||
type AppConfig = InferOutput<typeof appConfigSchema>
|
||||
|
||||
export async function setupMainWindow(params: {
|
||||
editorWindow: EditorWindowManager
|
||||
settingsWindow: SettingsWindowManager
|
||||
chatWindow: () => Promise<BrowserWindow>
|
||||
widgetsManager: WidgetsWindowManager
|
||||
@@ -175,6 +177,7 @@ export async function setupMainWindow(params: {
|
||||
|
||||
await setupMainWindowElectronInvokes({
|
||||
window,
|
||||
editorWindow: params.editorWindow,
|
||||
settingsWindow: params.settingsWindow,
|
||||
chatWindow: params.chatWindow,
|
||||
widgetsManager: params.widgetsManager,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ServerChannel } from '../../../services/airi/channel-server'
|
||||
import type { GodotStageManager } from '../../../services/airi/godot-stage'
|
||||
import type { McpStdioManager } from '../../../services/airi/mcp-servers'
|
||||
import type { AutoUpdater } from '../../../services/electron/auto-updater'
|
||||
import type { EditorWindowManager } from '../../editor'
|
||||
import type { NoticeWindowManager } from '../../notice'
|
||||
import type { OnboardingWindowManager } from '../../onboarding'
|
||||
import type { SettingsWindowManager } from '../../settings'
|
||||
@@ -15,7 +16,7 @@ import { defineInvokeHandler } from '@moeru/eventa'
|
||||
import { createContext } from '@moeru/eventa/adapters/electron/main'
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
import { electronCenterMainWindow, electronOpenChat, electronOpenMainDevtools, electronOpenSettings, noticeWindowEventa } from '../../../../shared/eventa'
|
||||
import { electronCenterMainWindow, electronOpenChat, electronOpenEditor, electronOpenMainDevtools, electronOpenSettings, noticeWindowEventa } from '../../../../shared/eventa'
|
||||
import { createAuthService } from '../../../services/airi/auth'
|
||||
import { createGodotStageService } from '../../../services/airi/godot-stage'
|
||||
import { createMcpServersService } from '../../../services/airi/mcp-servers'
|
||||
@@ -28,6 +29,7 @@ import { setupBaseWindowElectronInvokes } from '../../shared/window'
|
||||
|
||||
export async function setupMainWindowElectronInvokes(params: {
|
||||
window: BrowserWindow
|
||||
editorWindow: EditorWindowManager
|
||||
settingsWindow: SettingsWindowManager
|
||||
chatWindow: () => Promise<BrowserWindow>
|
||||
widgetsManager: WidgetsWindowManager
|
||||
@@ -57,6 +59,7 @@ export async function setupMainWindowElectronInvokes(params: {
|
||||
|
||||
defineInvokeHandler(context, electronCenterMainWindow, () => centerWindowOnDisplay(params.window))
|
||||
defineInvokeHandler(context, electronOpenMainDevtools, () => params.window.webContents.openDevTools({ mode: 'detach' }))
|
||||
defineInvokeHandler(context, electronOpenEditor, () => params.editorWindow.openWindow())
|
||||
defineInvokeHandler(context, electronOpenSettings, payload => params.settingsWindow.openWindow(payload?.route))
|
||||
defineInvokeHandler(context, electronOpenChat, async () => toggleWindowShow(await params.chatWindow()))
|
||||
defineInvokeHandler(context, noticeWindowEventa.openWindow, payload => params.noticeWindow.open(payload))
|
||||
|
||||
@@ -71,7 +71,8 @@ const setLocale = useElectronEventaInvoke(i18nSetLocale)
|
||||
const initialWindowRoutePath = resolveInitialChatSyncRoutePath(route.path)
|
||||
const chatSyncLifecycle = createChatSyncWindowLifecycle(route.path)
|
||||
const isSpotlightWindowRoute = initialWindowRoutePath === '/spotlight'
|
||||
const isSettingsWindowRoute = initialWindowRoutePath.startsWith('/settings')
|
||||
const isSettingsWindowRoute = initialWindowRoutePath === '/settings' || initialWindowRoutePath.startsWith('/settings/')
|
||||
const isEditorWindowRoute = initialWindowRoutePath === '/editor'
|
||||
|
||||
if (!isSpotlightWindowRoute)
|
||||
useAuthProviderSync()
|
||||
@@ -253,7 +254,7 @@ function createFullStageRuntime() {
|
||||
}
|
||||
}
|
||||
|
||||
const fullStageRuntime = isSpotlightWindowRoute ? null : createFullStageRuntime()
|
||||
const fullStageRuntime = isSpotlightWindowRoute || isEditorWindowRoute ? null : createFullStageRuntime()
|
||||
|
||||
const { restore: restoreLocale } = useLanguage(language, getMainLocale, setLocale)
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ watch(hasPermissions, (nextHasPermissions, previousHasPermissions) => {
|
||||
|
||||
<div flex="~ row gap-2 mt-4 justify-end" w-full>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
@click="showDialog = false"
|
||||
>
|
||||
{{ t('tamagotchi.settings.screen-capture.permissions-prompt.dismiss') }}
|
||||
|
||||
+6
-7
@@ -1,18 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { OverlayButton } from '@proj-airi/ui'
|
||||
|
||||
const props = defineProps<{ buttonStyle?: string }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
<OverlayButton
|
||||
size="sm"
|
||||
:class="[
|
||||
'border-2 border-solid border-neutral-200/60 dark:border-neutral-800/10',
|
||||
'bg-neutral-50/80 dark:bg-neutral-800/70',
|
||||
'w-fit flex items-center self-end justify-center p-2',
|
||||
'rounded-xl backdrop-blur-md',
|
||||
'transition-all hover:transition-none transition-duration-300 transition-ease-out',
|
||||
'p-2!',
|
||||
props.buttonStyle,
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</OverlayButton>
|
||||
</template>
|
||||
|
||||
+2
-2
@@ -25,14 +25,14 @@ const { stopAllSpeaking } = useStopSpeakingButton()
|
||||
v-if="nowSpeaking"
|
||||
key="active"
|
||||
:class="iconClass"
|
||||
i-carbon:face-activated
|
||||
i-ph:speaker-simple-high
|
||||
text-red-500
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
key="idle"
|
||||
:class="iconClass"
|
||||
i-carbon:face-neutral
|
||||
i-ph:speaker-none
|
||||
text="neutral-800 dark:neutral-300"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
@@ -315,7 +315,7 @@ onMounted(() => {
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
:loading="isBusy"
|
||||
icon="i-solar:download-minimalistic-outline"
|
||||
:label="t('tamagotchi.stage.about.update.actions.download')"
|
||||
@@ -349,7 +349,8 @@ onMounted(() => {
|
||||
</div>
|
||||
<div>
|
||||
<DoubleCheckButton
|
||||
variant="primary"
|
||||
color="neutral"
|
||||
variant="secondary"
|
||||
@confirm="handleQuitAndInstall()"
|
||||
>
|
||||
{{ restartButtonLabel }}
|
||||
@@ -379,7 +380,8 @@ onMounted(() => {
|
||||
<div :class="['flex flex-wrap gap-2']">
|
||||
<Button
|
||||
v-track-button="{ name: 'update_check_clicked', channel: selectedUpdateChannel }"
|
||||
:variant="isError ? 'caution' : 'secondary'"
|
||||
:color="isError ? 'orange' : 'neutral'"
|
||||
:variant="isError ? 'primary' : 'secondary'"
|
||||
:loading="isBusy"
|
||||
:disabled="isDisabled"
|
||||
:icon="isLatestVersion ? 'i-solar:check-circle-outline' : isDisabled ? 'i-solar:forbidden-circle-outline' : 'i-solar:refresh-outline'"
|
||||
@@ -419,10 +421,10 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end gap-3">
|
||||
<Button variant="secondary" @click="showChangelog = false">
|
||||
<Button @click="showChangelog = false">
|
||||
{{ t('tamagotchi.stage.about.common.cancel') }}
|
||||
</Button>
|
||||
<Button variant="primary" icon="i-solar:download-minimalistic-outline" @click="confirmDownload">
|
||||
<Button icon="i-solar:download-minimalistic-outline" @click="confirmDownload">
|
||||
{{ t('tamagotchi.stage.about.update.actions.confirm-download') }}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -449,10 +451,10 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex gap-3">
|
||||
<Button variant="secondary" block @click="showChangelog = false">
|
||||
<Button block @click="showChangelog = false">
|
||||
{{ t('tamagotchi.stage.about.common.cancel') }}
|
||||
</Button>
|
||||
<Button variant="primary" block icon="i-solar:download-minimalistic-outline" @click="confirmDownload">
|
||||
<Button block icon="i-solar:download-minimalistic-outline" @click="confirmDownload">
|
||||
{{ t('tamagotchi.stage.about.update.actions.download-short') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -211,17 +211,18 @@ onUnmounted(() => {
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<Button variant="primary" :disabled="busy" @click="handleRegister">
|
||||
<Button :disabled="busy" @click="handleRegister">
|
||||
Register
|
||||
</Button>
|
||||
<Button variant="secondary" :disabled="busy" @click="refreshList">
|
||||
<Button :disabled="busy" @click="refreshList">
|
||||
Refresh List
|
||||
</Button>
|
||||
<Button
|
||||
class="ml-auto"
|
||||
variant="danger"
|
||||
|
||||
:disabled="busy"
|
||||
@click="handleUnregisterAll"
|
||||
color="red"
|
||||
variant="primary" @click="handleUnregisterAll"
|
||||
>
|
||||
Unregister All
|
||||
</Button>
|
||||
@@ -305,7 +306,7 @@ onUnmounted(() => {
|
||||
<td class="px-3 py-2 text-right">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
|
||||
:disabled="busy"
|
||||
@click="handleUnregister(b.id)"
|
||||
>
|
||||
@@ -325,7 +326,7 @@ onUnmounted(() => {
|
||||
</h3>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
|
||||
:disabled="triggers.length === 0"
|
||||
@click="clearLog"
|
||||
>
|
||||
|
||||
@@ -35,21 +35,21 @@ const diagnosticsEntries = computed(() => {
|
||||
<div :class="['flex flex-col gap-4', 'pb-8']">
|
||||
<div :class="['flex flex-wrap gap-2']">
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
:loading="isBusy"
|
||||
icon="i-solar:refresh-outline"
|
||||
label="Check for updates"
|
||||
@click="checkForUpdates()"
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
:disabled="updateState.status !== 'available'"
|
||||
icon="i-solar:download-minimalistic-outline"
|
||||
label="Download update"
|
||||
@click="downloadUpdate()"
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
:disabled="updateState.status !== 'downloaded'"
|
||||
icon="i-solar:restart-bold-duotone"
|
||||
label="Restart to install"
|
||||
|
||||
@@ -281,21 +281,21 @@ function applyExtensionUiPreset() {
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
:disabled="busy"
|
||||
@click="applyWeatherPreset"
|
||||
>
|
||||
Weather Preset
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
:disabled="busy"
|
||||
@click="applyMapPreset"
|
||||
>
|
||||
Map Preset
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
:disabled="busy"
|
||||
@click="applyExtensionUiPreset"
|
||||
>
|
||||
@@ -306,21 +306,21 @@ function applyExtensionUiPreset() {
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
:disabled="busy"
|
||||
@click="handleAdd"
|
||||
>
|
||||
Spawn / Replace
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
:disabled="busy"
|
||||
@click="handleUpdate"
|
||||
>
|
||||
Update Props
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
:disabled="busy"
|
||||
@click="handleRemove"
|
||||
>
|
||||
@@ -328,9 +328,10 @@ function applyExtensionUiPreset() {
|
||||
</Button>
|
||||
<Button
|
||||
class="ml-auto"
|
||||
variant="danger"
|
||||
|
||||
:disabled="busy"
|
||||
@click="handleClear"
|
||||
color="red"
|
||||
variant="primary" @click="handleClear"
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<main :class="['h-full w-full', 'bg-$bg-color']" />
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: plain
|
||||
</route>
|
||||
@@ -216,7 +216,7 @@ async function handleAction(action: 'confirm' | 'cancel' | 'close') {
|
||||
|
||||
<div class="mt-3 flex flex-col gap-2 sm:flex-row">
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
size="md"
|
||||
block
|
||||
:label="t('tamagotchi.stage.notice.fade-on-hover.confirm')"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { isStageTamagotchi } from '@proj-airi/stage-shared'
|
||||
import { ConnectionSettings } from '@proj-airi/stage-ui/components'
|
||||
import { Button, Callout, FieldCheckbox, FieldInput, Input, SelectTab } from '@proj-airi/ui'
|
||||
import { Callout, FieldCheckbox, FieldInput, IconButton, Input, SelectTab } from '@proj-airi/ui'
|
||||
import { refDebounced, useClipboard } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, shallowRef, watch } from 'vue'
|
||||
@@ -130,22 +130,16 @@ watch(authTokenInputDebounced, (value) => {
|
||||
:type="authTokenInputType"
|
||||
:placeholder="t('settings.pages.connection.server-auth-token.placeholder')"
|
||||
/>
|
||||
<Button
|
||||
<IconButton
|
||||
type="button"
|
||||
variant="secondary-muted"
|
||||
size="sm"
|
||||
shape="square"
|
||||
:icon="authTokenVisible ? 'i-solar:eye-closed-bold-duotone' : 'i-solar:eye-bold-duotone'"
|
||||
:aria-label="authTokenVisible ? 'Hide auth token' : 'Show auth token'"
|
||||
:title="authTokenVisible ? 'Hide auth token' : 'Show auth token'"
|
||||
data-testid="server-auth-token-visibility-toggle"
|
||||
@click="authTokenVisible = !authTokenVisible"
|
||||
/>
|
||||
<Button
|
||||
<IconButton
|
||||
type="button"
|
||||
variant="secondary-muted"
|
||||
size="sm"
|
||||
shape="square"
|
||||
:icon="authTokenCopied ? 'i-solar:check-circle-bold-duotone' : 'i-solar:copy-line-duotone'"
|
||||
:disabled="!canCopyAuthToken"
|
||||
:aria-label="authTokenCopied ? 'Copied auth token' : 'Copy auth token'"
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@ import type { ServerChannelQrPayload } from '@proj-airi/stage-shared/server-chan
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import { Button, Callout, Collapsible } from '@proj-airi/ui'
|
||||
import { Callout, Collapsible, GhostButton } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { renderSVG } from 'uqr'
|
||||
import { computed, shallowRef, watch } from 'vue'
|
||||
@@ -145,9 +145,9 @@ watch([hostname, tlsConfig, authToken], () => {
|
||||
:class="['h-48 w-48']"
|
||||
>
|
||||
|
||||
<Button
|
||||
<GhostButton
|
||||
size="sm"
|
||||
variant="secondary-muted"
|
||||
|
||||
:loading="loading"
|
||||
:label="t('settings.pages.connection.qr.refresh')"
|
||||
@click="refreshPayload"
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ async function triggerOpenDesktopUserDataFolder() {
|
||||
</p>
|
||||
</div>
|
||||
<div :class="['flex flex-col items-start gap-2']">
|
||||
<Button variant="secondary" @click="triggerOpenDesktopUserDataFolder">
|
||||
<Button @click="triggerOpenDesktopUserDataFolder">
|
||||
{{ t('settings.pages.data.sections.desktop-folder.open') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -57,10 +57,10 @@ async function handleCenterMainWindow() {
|
||||
</p>
|
||||
</div>
|
||||
<div :class="['flex flex-col items-start gap-2']">
|
||||
<Button variant="caution" icon="i-solar:target-linear" @click="handleCenterMainWindow">
|
||||
<Button icon="i-solar:target-linear" color="orange" variant="primary" @click="handleCenterMainWindow">
|
||||
{{ t('settings.pages.data.sections.desktop.center') }}
|
||||
</Button>
|
||||
<DoubleCheckButton variant="caution" @confirm="resetDesktopState">
|
||||
<DoubleCheckButton color="orange" @confirm="resetDesktopState">
|
||||
{{ t('settings.pages.data.sections.desktop.reset') }}
|
||||
<template #confirm>
|
||||
{{ t('settings.pages.data.confirmations.yes') }}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useElectronEventaContext, useElectronEventaInvoke } from '@proj-airi/el
|
||||
import { ModelSettingsPanel } from '@proj-airi/stage-ui/components/scenarios/settings/model-settings'
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { Button, Callout } from '@proj-airi/ui'
|
||||
import { Callout, GhostButton } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
@@ -389,14 +389,14 @@ onUnmounted(() => {
|
||||
@patch-godot-view-state="handleGodotViewPatch"
|
||||
>
|
||||
<template #actions>
|
||||
<Button
|
||||
variant="secondary"
|
||||
<GhostButton
|
||||
|
||||
:loading="switchingGodotStage"
|
||||
:toggled="usesGodotStage"
|
||||
:active="usesGodotStage"
|
||||
@click="handleGodotStageToggle"
|
||||
>
|
||||
{{ godotToggleLabel }}
|
||||
</Button>
|
||||
</GhostButton>
|
||||
</template>
|
||||
</ModelSettingsPanel>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ const PANEL = 'flex flex-col gap-3 rounded-xl border-2 border-solid border-neutr
|
||||
:disabled="!props.options.length"
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
size="md"
|
||||
:loading="props.running"
|
||||
:disabled="props.running || !props.options.length"
|
||||
|
||||
+6
-6
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Button, Callout } from '@proj-airi/ui'
|
||||
import { Button, Callout, GhostButton } from '@proj-airi/ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -34,9 +34,9 @@ const JSON_TEXTAREA = 'w-full rounded-lg border-2 border-solid border-primary-10
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Button variant="ghost" size="sm" icon="i-solar:folder-open-bold-duotone" :label="tn('actions.open-config')" @click="emit('openConfig')" />
|
||||
<Button variant="ghost" size="sm" icon="i-solar:restart-line-duotone" :label="tn('actions.reset-draft')" @click="emit('resetDraft')" />
|
||||
<Button variant="ghost" size="sm" icon="i-solar:magic-stick-3-bold-duotone" :label="tn('actions.format-json')" @click="emit('format')" />
|
||||
<GhostButton size="sm" icon="i-solar:folder-open-bold-duotone" :label="tn('actions.open-config')" @click="emit('openConfig')" />
|
||||
<GhostButton size="sm" icon="i-solar:restart-line-duotone" :label="tn('actions.reset-draft')" @click="emit('resetDraft')" />
|
||||
<GhostButton size="sm" icon="i-solar:magic-stick-3-bold-duotone" :label="tn('actions.format-json')" @click="emit('format')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -53,8 +53,8 @@ const JSON_TEXTAREA = 'w-full rounded-lg border-2 border-solid border-primary-10
|
||||
</Callout>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-end gap-2 pt-1">
|
||||
<Button variant="secondary" size="sm" :label="tn('actions.cancel')" @click="emit('close')" />
|
||||
<Button variant="primary" size="sm" icon="i-solar:check-circle-bold-duotone" :label="tn('actions.apply-json')" @click="emit('apply')" />
|
||||
<Button size="sm" :label="tn('actions.cancel')" @click="emit('close')" />
|
||||
<Button size="sm" icon="i-solar:check-circle-bold-duotone" :label="tn('actions.apply-json')" @click="emit('apply')" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
+6
-5
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { ServerForm } from '../mcp-config'
|
||||
|
||||
import { Button, FieldInput, FieldKeyValues } from '@proj-airi/ui'
|
||||
import { Button, FieldInput, FieldKeyValues, GhostButton } from '@proj-airi/ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
defineEmits<{ remove: [] }>()
|
||||
@@ -55,8 +55,8 @@ const tn = (k: string) => t(`settings.pages.modules.mcp-server.${k}`)
|
||||
@remove="(i) => model.envEntries.splice(i, 1)"
|
||||
/>
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
variant="ghost" size="sm"
|
||||
<GhostButton
|
||||
size="sm"
|
||||
icon="i-solar:add-circle-bold-duotone" :label="tn('actions.add-env')"
|
||||
@click="model.envEntries.push({ key: '', value: '' })"
|
||||
/>
|
||||
@@ -65,9 +65,10 @@ const tn = (k: string) => t(`settings.pages.modules.mcp-server.${k}`)
|
||||
|
||||
<div class="flex justify-end border-t border-neutral-200/70 pt-2 dark:border-neutral-800">
|
||||
<Button
|
||||
variant="danger" size="sm"
|
||||
size="sm"
|
||||
icon="i-solar:trash-bin-2-bold-duotone" :label="tn('actions.remove')"
|
||||
@click="$emit('remove')"
|
||||
color="red"
|
||||
variant="primary" @click="$emit('remove')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,12 +9,7 @@ import type { ServerForm } from './mcp-config'
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import {
|
||||
Button,
|
||||
Callout,
|
||||
Checkbox,
|
||||
TransitionVertical,
|
||||
} from '@proj-airi/ui'
|
||||
import { Button, Callout, Checkbox, GhostButton, TransitionVertical } from '@proj-airi/ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -358,8 +353,8 @@ onMounted(async () => {
|
||||
<span class="font-medium">{{ tn('config-path') }}:</span> {{ configPath || '-' }}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
variant="secondary" size="sm" :toggled="jsonOpen"
|
||||
<GhostButton
|
||||
size="sm" :active="jsonOpen"
|
||||
:icon="jsonOpen ? 'i-solar:close-square-bold-duotone' : 'i-solar:document-text-bold-duotone'"
|
||||
:label="jsonOpen ? tn('actions.close-json') : tn('actions.edit-json')"
|
||||
@click="toggleJsonPanel"
|
||||
@@ -474,16 +469,16 @@ onMounted(async () => {
|
||||
<McpServerForm :model-value="server" @remove="removeServer(server.rowId)" />
|
||||
</article>
|
||||
|
||||
<Button
|
||||
<GhostButton
|
||||
v-track-button="{ name: 'mcp_server_added' }"
|
||||
variant="secondary-muted" size="md" block :disabled="isBusy"
|
||||
size="md" block :disabled="isBusy"
|
||||
icon="i-solar:add-circle-bold-duotone" :label="tn('actions.add-server')"
|
||||
@click="addServer"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<Button
|
||||
variant="primary" size="md" block :disabled="isBusy" :loading="isBusy"
|
||||
size="md" block :disabled="isBusy" :loading="isBusy"
|
||||
icon="i-solar:rocket-2-bold-duotone" :label="restartActionLabel"
|
||||
@click="applyRestartAction"
|
||||
/>
|
||||
|
||||
@@ -35,6 +35,7 @@ export const electronStartDraggingWindow = defineInvokeEventa('eventa:invoke:ele
|
||||
|
||||
export const electronOpenMainDevtools = defineInvokeEventa('eventa:invoke:electron:windows:main:devtools:open')
|
||||
export const electronCenterMainWindow = defineInvokeEventa<Rectangle>('eventa:invoke:electron:windows:main:center')
|
||||
export const electronOpenEditor = defineInvokeEventa<void>('eventa:invoke:electron:windows:editor:open')
|
||||
export const electronOpenSettings = defineInvokeEventa<void, { route?: string }>('eventa:invoke:electron:windows:settings:open')
|
||||
export const electronSettingsNavigate = defineEventa<{ route: string }>('eventa:event:electron:windows:settings:navigate')
|
||||
export const electronOpenChat = defineInvokeEventa('eventa:invoke:electron:windows:chat:open')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { createBackgroundRemovalAdapter } from '@proj-airi/stage-ui/libs/inference/adapters/background-removal'
|
||||
import { Button, Checkbox, InputFile } from '@proj-airi/ui'
|
||||
import { Button, Checkbox, GhostButton, InputFile } from '@proj-airi/ui'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
const adapter = createBackgroundRemovalAdapter()
|
||||
@@ -228,14 +228,14 @@ function hidePreview() {
|
||||
/>
|
||||
<Button
|
||||
v-if="doneCount > 0"
|
||||
variant="secondary"
|
||||
|
||||
:label="`Download All (${doneCount})`"
|
||||
icon="i-solar:download-minimalistic-bold"
|
||||
@click="downloadAllImages"
|
||||
/>
|
||||
<Button
|
||||
<GhostButton
|
||||
v-if="imageItems.length > 0"
|
||||
variant="secondary-muted"
|
||||
|
||||
label="Clear All"
|
||||
icon="i-solar:trash-bin-trash-line-duotone"
|
||||
@click="clearAllImages"
|
||||
@@ -343,11 +343,12 @@ function hidePreview() {
|
||||
@click="downloadImage(index)"
|
||||
/>
|
||||
<Button
|
||||
variant="danger"
|
||||
|
||||
size="sm"
|
||||
icon="i-solar:trash-bin-trash-bold"
|
||||
:disabled="item.status === 'processing'"
|
||||
@click="removeImage(index)"
|
||||
color="red"
|
||||
variant="primary" @click="removeImage(index)"
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -7,24 +7,28 @@ const presetButtons = [
|
||||
name: 'success',
|
||||
label: 'Success',
|
||||
icon: 'i-solar:check-circle-bold-duotone',
|
||||
variant: 'primary',
|
||||
color: 'neutral',
|
||||
variant: 'secondary',
|
||||
},
|
||||
{
|
||||
name: 'nudge',
|
||||
label: 'Nudge',
|
||||
icon: 'i-solar:hand-stars-bold-duotone',
|
||||
variant: 'caution',
|
||||
color: 'orange',
|
||||
variant: 'primary',
|
||||
},
|
||||
{
|
||||
name: 'error',
|
||||
label: 'Error',
|
||||
icon: 'i-solar:danger-triangle-bold-duotone',
|
||||
variant: 'danger',
|
||||
color: 'red',
|
||||
variant: 'primary',
|
||||
},
|
||||
{
|
||||
name: 'buzz',
|
||||
label: 'Buzz',
|
||||
icon: 'i-solar:alarm-bold-duotone',
|
||||
color: 'neutral',
|
||||
variant: 'secondary',
|
||||
},
|
||||
] as const
|
||||
@@ -58,6 +62,7 @@ const { trigger, isSupported } = useWebHaptics({
|
||||
block
|
||||
size="md"
|
||||
shape="rounded"
|
||||
:color="preset.color"
|
||||
:variant="preset.variant"
|
||||
:icon="preset.icon"
|
||||
@click="trigger(preset.name)"
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { Character, CreateCharacterPayload } from '@proj-airi/stage-ui/type
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables/use-analytics'
|
||||
import { useCharacterStore } from '@proj-airi/stage-ui/stores/characters'
|
||||
import { CreateCharacterSchema } from '@proj-airi/stage-ui/types/character'
|
||||
import { Button, FieldInput } from '@proj-airi/ui'
|
||||
import { Button, FieldInput, GhostButton } from '@proj-airi/ui'
|
||||
import {
|
||||
DialogContent,
|
||||
DialogOverlay,
|
||||
@@ -291,9 +291,9 @@ const isOpen = computed({
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-end gap-2 border-t border-neutral-100 p-4 dark:border-neutral-800">
|
||||
<Button variant="ghost" @click="isOpen = false">
|
||||
<GhostButton @click="isOpen = false">
|
||||
Cancel
|
||||
</Button>
|
||||
</GhostButton>
|
||||
<Button :loading="isSubmitting" @click="handleSubmit">
|
||||
{{ character ? 'Save Changes' : 'Create' }}
|
||||
</Button>
|
||||
|
||||
@@ -138,7 +138,7 @@ const accentClasses = computed(() => {
|
||||
<Button
|
||||
v-if="secondaryActionLabel"
|
||||
block
|
||||
variant="secondary"
|
||||
|
||||
@click="emit('secondaryAction')"
|
||||
>
|
||||
{{ secondaryActionLabel }}
|
||||
|
||||
@@ -58,7 +58,7 @@ const router = useRouter()
|
||||
<Button @click="router.push('/sign-in')">
|
||||
Sign in
|
||||
</Button>
|
||||
<Button variant="secondary" @click="router.go(-1)">
|
||||
<Button @click="router.go(-1)">
|
||||
Go back
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -237,7 +237,7 @@ onMounted(() => {
|
||||
v-if="viewModel.primaryActionLabel"
|
||||
:disabled="viewModel.primaryActionDisabled"
|
||||
:class="['inline-flex']"
|
||||
variant="secondary"
|
||||
|
||||
@click="openRelayUrl"
|
||||
>
|
||||
{{ viewModel.primaryActionLabel }}
|
||||
|
||||
@@ -586,7 +586,7 @@ function formatLinkedSince(iso: string): string {
|
||||
|
||||
<Button
|
||||
v-if="linkedAccountsByProvider.get(provider.id)"
|
||||
variant="secondary"
|
||||
|
||||
:class="['shrink-0 px-3 py-1 text-xs']"
|
||||
:loading="linkActionInFlight === provider.id"
|
||||
:disabled="!!linkActionInFlight && linkActionInFlight !== provider.id"
|
||||
@@ -627,7 +627,7 @@ function formatLinkedSince(iso: string): string {
|
||||
<div :class="['max-w-sm w-full flex flex-col gap-2']">
|
||||
<Button
|
||||
:class="['w-full', 'py-2', 'flex', 'items-center', 'justify-center']"
|
||||
variant="secondary"
|
||||
|
||||
:loading="signOutLoading"
|
||||
@click="handleSignOut"
|
||||
>
|
||||
|
||||
@@ -102,24 +102,61 @@ renders the fallback when the URL is missing, loading, or fails.
|
||||
|
||||
**Slots**: `fallback` (optional override for the built-in user icon)
|
||||
|
||||
### Button
|
||||
### BasicButton
|
||||
|
||||
Versatile button with variants, sizes, and states.
|
||||
Behavioral foundation for custom buttons. It owns disabled/loading behavior,
|
||||
press animation, sizing, icon/label rendering, and block layout, but supplies no
|
||||
surface, border, shape, or color styling.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `toggled` | `boolean?` | `false` | Toggle state |
|
||||
| `icon` | `string?` | — | UnoCSS/Iconify icon class |
|
||||
| `label` | `string?` | — | Button text |
|
||||
| `disabled` | `boolean?` | `false` | Disabled state |
|
||||
| `loading` | `boolean?` | `false` | Loading state |
|
||||
| `variant` | `'primary' \| 'secondary' \| 'secondary-muted' \| 'danger' \| 'caution' \| 'pure' \| 'ghost'` | `'primary'` | Visual variant |
|
||||
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size |
|
||||
| `shape` | `'rounded' \| 'pill' \| 'square'` | `'pill'` | Shape |
|
||||
| `block` | `boolean?` | `false` | Full width |
|
||||
|
||||
**Slots**: `default` (fallback when no `label`)
|
||||
|
||||
### Button
|
||||
|
||||
Solid general-purpose action. It has no border and shows offset outlines on
|
||||
hover and keyboard focus. Choose a Wind3 color family independently from its
|
||||
primary or secondary visual emphasis.
|
||||
|
||||
Includes all `BasicButton` props, plus:
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `shape` | `'rect' \| 'rounded' \| 'circle' \| 'parallelogram'` | `'rect'` | Rectangular, pill-like, circular, or rounded angled geometry |
|
||||
| `color` | `'neutral' \| 'primary' \| 'cyan' \| 'blue' \| 'green' \| 'lime' \| 'amber' \| 'red' \| 'orange' \| 'purple' \| 'pink'` | `'neutral'` | Wind3 color family; `primary` follows the configured theme hue |
|
||||
| `variant` | `'primary' \| 'secondary'` | `'secondary'` | Solid high-emphasis or subtle low-emphasis treatment |
|
||||
| `outline` | `boolean?` | `true` | Show the offset outline on hover and keyboard focus |
|
||||
|
||||
### GhostButton
|
||||
|
||||
Low-emphasis contextual action with a transparent default surface and compact
|
||||
spacing. Hover, pressed, and selected states use a subtle primary surface and
|
||||
text treatment without an outline. The offset outline is reserved for keyboard
|
||||
focus. Includes all `BasicButton` props.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `active` | `boolean?` | `false` | Persist the subtle selected surface for toggle controls |
|
||||
|
||||
### IconButton
|
||||
|
||||
Minimal icon-only action for controls such as favorite, like, copy, or retry.
|
||||
It directly uses `BasicButton`, removes its padding, and does not add a
|
||||
background, outline, shape, or forced square dimensions. Supply an accessible
|
||||
`aria-label`. Props: `icon`, `disabled`, and `loading`.
|
||||
|
||||
### OverlayButton
|
||||
|
||||
Translucent, backdrop-blurred action without an outline, intended for floating
|
||||
controls such as the Tamagotchi Controls Island. Includes all `BasicButton` props.
|
||||
|
||||
### Callout
|
||||
|
||||
Alert/callout box with themed accent bar.
|
||||
@@ -170,8 +207,8 @@ Two-stage confirmation button — click once to reveal confirm/cancel.
|
||||
|
||||
| Prop | Type | Default | Description |
|
||||
|------|------|---------|-------------|
|
||||
| `variant` | `ButtonVariant` | `'danger'` | Confirm button variant |
|
||||
| `cancelVariant` | `ButtonVariant` | `'secondary'` | Cancel button variant |
|
||||
| `color` | `ButtonColor` | `'red'` | Confirm button color family |
|
||||
| `variant` | `ButtonVariant` | `'primary'` | Confirm button visual emphasis |
|
||||
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size |
|
||||
| `block` | `boolean?` | `false` | Full width |
|
||||
| `disabled` | `boolean?` | `false` | Disabled |
|
||||
|
||||
+5
-5
@@ -2,7 +2,7 @@
|
||||
import { defaultControlConfig as threeCtrlConf, supportedControl as threeSupportedControl, useThreeViewControl } from '@proj-airi/stage-ui-three'
|
||||
import { defaultControlConfig as l2dCtrlConf, supportedControl as l2dSupportedCtrl, useL2dViewControl } from '@proj-airi/stage-ui/stores/live2d'
|
||||
import { useSettingsStageModel } from '@proj-airi/stage-ui/stores/settings/stage-model'
|
||||
import { Button } from '@proj-airi/ui'
|
||||
import { GhostButton } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
@@ -32,12 +32,12 @@ function handleViewControlsToggle(targetMode: string) {
|
||||
<div w-full flex flex-1 items-center self-end justify-end gap-2>
|
||||
<Transition name="fade">
|
||||
<div v-if="controlEnabled?.enabled.value" w-full flex justify-between gap-2>
|
||||
<Button
|
||||
v-for="control in controlEnabled.supported" :key="control" variant="secondary-muted"
|
||||
:toggled="controlEnabled.mode.value === control" w-full @click="handleViewControlsToggle(control)"
|
||||
<GhostButton
|
||||
v-for="control in controlEnabled.supported" :key="control"
|
||||
:active="controlEnabled.mode.value === control" w-full @click="handleViewControlsToggle(control)"
|
||||
>
|
||||
{{ (controlEnabled.conf as any)[control].buttonText }}
|
||||
</Button>
|
||||
</GhostButton>
|
||||
</div>
|
||||
</Transition>
|
||||
<button
|
||||
|
||||
@@ -221,7 +221,7 @@ function hitVSequence() {
|
||||
label="Hit V sequence"
|
||||
icon="i-solar:repeat-one-minimalistic-bold-duotone"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
|
||||
@click="hitVSequence"
|
||||
/>
|
||||
<FieldCheckbox
|
||||
|
||||
@@ -32,7 +32,8 @@ const subsystemFilters = SUBSYSTEM_CONFIGS.map(config => ({
|
||||
:class="[
|
||||
'flex items-center gap-1.5',
|
||||
]"
|
||||
:variant="isRecording ? 'danger' : 'primary'"
|
||||
:color="isRecording ? 'red' : 'neutral'"
|
||||
:variant="isRecording ? 'primary' : 'secondary'"
|
||||
@click="emit('toggleRecording')"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { Section } from '@proj-airi/stage-ui/components'
|
||||
import { usePluginHostInspectorStore } from '@proj-airi/stage-ui/stores/devtools/plugin-host-debug'
|
||||
import { Button, Callout, Input } from '@proj-airi/ui'
|
||||
import { Button, Callout, GhostButton, Input } from '@proj-airi/ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
@@ -306,7 +306,7 @@ onMounted(async () => {
|
||||
<div :class="['flex', 'flex-wrap', 'items-center', 'gap-2']">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
|
||||
:label="plugin.autoReload ? 'Auto Reload Off' : 'Auto Reload On'"
|
||||
:icon="plugin.autoReload ? 'i-solar:refresh-circle-bold' : 'i-solar:refresh-bold-duotone'"
|
||||
:loading="store.loading"
|
||||
@@ -314,7 +314,7 @@ onMounted(async () => {
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
|
||||
:label="plugin.enabled ? 'Disable' : 'Enable'"
|
||||
:icon="plugin.enabled ? 'i-solar:lock-keyhole-minimalistic-unlocked-bold-duotone' : 'i-solar:lock-keyhole-bold-duotone'"
|
||||
:loading="store.loading"
|
||||
@@ -322,16 +322,16 @@ onMounted(async () => {
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
|
||||
label="Load"
|
||||
icon="i-solar:play-bold-duotone"
|
||||
:disabled="plugin.loaded"
|
||||
:loading="store.loading"
|
||||
@click="loadPlugin(plugin)"
|
||||
/>
|
||||
<Button
|
||||
<GhostButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
|
||||
label="Unload"
|
||||
icon="i-solar:stop-bold-duotone"
|
||||
:disabled="!plugin.loaded"
|
||||
|
||||
+3
-3
@@ -417,7 +417,7 @@ onBeforeUnmount(async () => {
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<Button
|
||||
:disabled="!canStartRecording"
|
||||
variant="primary"
|
||||
|
||||
@click="startRecording"
|
||||
>
|
||||
Start Recording
|
||||
@@ -425,7 +425,7 @@ onBeforeUnmount(async () => {
|
||||
|
||||
<Button
|
||||
:disabled="!canStopRecording"
|
||||
variant="primary"
|
||||
|
||||
@click="stopRecording"
|
||||
>
|
||||
Stop Recording
|
||||
@@ -434,7 +434,7 @@ onBeforeUnmount(async () => {
|
||||
<Button
|
||||
v-if="isTranscribing"
|
||||
:disabled="!canAbortTranscription"
|
||||
variant="secondary"
|
||||
|
||||
@click="abortTranscription"
|
||||
>
|
||||
Abort Transcription
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useWebSocketInspectorStore } from '@proj-airi/stage-ui/stores/devtools/websocket-inspector'
|
||||
import { Button, FieldCheckbox, Input } from '@proj-airi/ui'
|
||||
import { FieldCheckbox, GhostButton, Input } from '@proj-airi/ui'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
const store = useWebSocketInspectorStore()
|
||||
@@ -124,11 +124,11 @@ function payloadClasses(direction: 'incoming' | 'outgoing') {
|
||||
placeholder="Filter payload..."
|
||||
class="w-64"
|
||||
/>
|
||||
<Button
|
||||
<GhostButton
|
||||
label="Clear"
|
||||
icon="i-solar:trash-bin-trash-bold-duotone"
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
|
||||
@click="store.clear()"
|
||||
/>
|
||||
<div class="flex flex-shrink-0 items-center text-xs">
|
||||
|
||||
@@ -5,7 +5,7 @@ import { resolveLinkedAccountOAuthErrorMessageKey, useAnalytics, useLinkedAccoun
|
||||
import { authClient } from '@proj-airi/stage-ui/libs/auth'
|
||||
import { SERVER_URL } from '@proj-airi/stage-ui/libs/server'
|
||||
import { useAuthStore } from '@proj-airi/stage-ui/stores/auth'
|
||||
import { Avatar, Button, FieldInput } from '@proj-airi/ui'
|
||||
import { Avatar, Button, FieldInput, GhostButton } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { DialogClose, DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogRoot, DialogTitle } from 'reka-ui'
|
||||
import { computed, reactive, ref, shallowRef, watch } from 'vue'
|
||||
@@ -438,18 +438,13 @@ async function handleConfirmDelete(event: Event) {
|
||||
|
||||
<div :class="['my-2 border-t border-neutral-200/70 dark:border-neutral-800/60']" />
|
||||
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'w-full text-left rounded-lg px-3 py-2 text-sm transition-colors cursor-pointer',
|
||||
'flex items-center gap-2',
|
||||
'text-neutral-600 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-800/60',
|
||||
]"
|
||||
<GhostButton
|
||||
block
|
||||
:class="['justify-start!']"
|
||||
icon="i-solar:logout-3-bold-duotone"
|
||||
:label="t('settings.pages.account.logout')"
|
||||
@click="emit('logout')"
|
||||
>
|
||||
<div :class="['i-solar:logout-3-bold-duotone', 'size-4 flex-shrink-0']" />
|
||||
{{ t('settings.pages.account.logout') }}
|
||||
</button>
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<!-- Main column. max-w-3xl keeps long lines (descriptions, the Flux
|
||||
@@ -716,12 +711,13 @@ async function handleConfirmDelete(event: Event) {
|
||||
{{ t('settings.pages.account.connections.message.loading') }}
|
||||
</div>
|
||||
|
||||
<ul v-else :class="['flex flex-col gap-2 max-w-md']">
|
||||
<ul v-else :class="['flex flex-col gap-2 max-w-unset md:max-w-md']">
|
||||
<li
|
||||
v-for="provider in defaultSignInProviders"
|
||||
:key="provider.id"
|
||||
:class="[
|
||||
'flex items-center justify-between gap-3 rounded-lg border border-neutral-200 dark:border-neutral-700 px-3 py-2',
|
||||
'flex items-center justify-between gap-3 rounded-lg px-3 py-2',
|
||||
'bg-neutral-100 dark:bg-neutral-900',
|
||||
]"
|
||||
>
|
||||
<div :class="['flex items-center gap-2 min-w-0']">
|
||||
@@ -745,7 +741,7 @@ async function handleConfirmDelete(event: Event) {
|
||||
|
||||
<Button
|
||||
v-if="linkedAccountsByProvider.get(provider.id)"
|
||||
variant="secondary"
|
||||
|
||||
:class="['shrink-0 px-3 py-1 text-xs']"
|
||||
:loading="linkActionInFlight === provider.id"
|
||||
:disabled="!!linkActionInFlight && linkActionInFlight !== provider.id"
|
||||
@@ -811,9 +807,9 @@ async function handleConfirmDelete(event: Event) {
|
||||
</div>
|
||||
<div :class="['flex-shrink-0']">
|
||||
<Button
|
||||
variant="danger"
|
||||
:label="t('settings.pages.account.danger.deleteAccount.action')"
|
||||
@click="openDeleteDialog"
|
||||
color="red"
|
||||
variant="primary" @click="openDeleteDialog"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -877,17 +873,18 @@ async function handleConfirmDelete(event: Event) {
|
||||
<DialogClose as-child>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
|
||||
:disabled="deleteLoading"
|
||||
:label="t('settings.pages.account.danger.deleteAccount.modal.cancel')"
|
||||
/>
|
||||
</DialogClose>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="danger"
|
||||
|
||||
:loading="deleteLoading"
|
||||
:disabled="!deleteEmailMatches"
|
||||
:label="t('settings.pages.account.danger.deleteAccount.modal.confirm')"
|
||||
color="red" variant="primary"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
@@ -901,18 +898,11 @@ async function handleConfirmDelete(event: Event) {
|
||||
reversible (just sign back in) — putting it in the destructive
|
||||
group would over-signal severity. -->
|
||||
<div :class="['md:hidden pt-2']">
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm cursor-pointer',
|
||||
'text-neutral-600 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-800/60',
|
||||
'transition-colors',
|
||||
]"
|
||||
<GhostButton
|
||||
icon="i-solar:logout-3-bold-duotone"
|
||||
:label="t('settings.pages.account.logout')"
|
||||
@click="emit('logout')"
|
||||
>
|
||||
<div :class="['i-solar:logout-3-bold-duotone', 'size-4']" />
|
||||
{{ t('settings.pages.account.logout') }}
|
||||
</button>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -680,14 +680,13 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
||||
|
||||
<div class="ml-auto mr-1 flex flex-row gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
icon="i-solar:undo-left-bold-duotone"
|
||||
:label="t('settings.pages.card.cancel')"
|
||||
:disabled="false"
|
||||
@click="modelValue = false"
|
||||
/>
|
||||
<Button
|
||||
:variant="isEditingActiveCard ? 'primary' : 'secondary'"
|
||||
icon="i-solar:check-circle-bold-duotone"
|
||||
:label="t('settings.pages.card.save')"
|
||||
:disabled="false"
|
||||
@@ -695,7 +694,7 @@ function getDefaultPlaceholder(defaultValue: string | undefined): string {
|
||||
/>
|
||||
<Button
|
||||
v-if="!isEditingActiveCard"
|
||||
variant="primary"
|
||||
|
||||
icon="i-solar:play-circle-bold-duotone"
|
||||
:label="t('settings.pages.card.save_and_activate')"
|
||||
:disabled="false"
|
||||
|
||||
@@ -359,7 +359,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
|
||||
<!-- Action buttons -->
|
||||
<div flex="~ row" gap-2>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
icon="i-solar:download-minimalistic-bold-duotone"
|
||||
:label="t('settings.pages.card.export')"
|
||||
:disabled="isExportingCard"
|
||||
@@ -367,7 +367,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
|
||||
/>
|
||||
<!-- Activation button -->
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
:icon="isActive ? 'i-solar:check-circle-bold-duotone' : 'i-solar:play-circle-broken'"
|
||||
:label="isActive ? t('settings.pages.card.active') : t('settings.pages.card.activate')"
|
||||
:disabled="isActive"
|
||||
@@ -375,7 +375,7 @@ function getModuleDisplayValue(value: string | undefined, defaultValue: string |
|
||||
@click="handleActivate"
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
icon="i-solar:close-circle-bold-duotone"
|
||||
:label="t('settings.pages.card.cancel')"
|
||||
@click="emit('update:modelValue', false)"
|
||||
|
||||
@@ -55,16 +55,17 @@ function handleConfirm() {
|
||||
<div class="flex flex-row justify-end gap-3">
|
||||
<AlertDialogCancel as-child>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
:label="t('settings.pages.card.cancel')"
|
||||
@click="handleCancel"
|
||||
/>
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction as-child>
|
||||
<Button
|
||||
variant="danger"
|
||||
|
||||
:label="t('settings.pages.card.delete')"
|
||||
@click="handleConfirm"
|
||||
color="red"
|
||||
variant="primary" @click="handleConfirm"
|
||||
/>
|
||||
</AlertDialogAction>
|
||||
</div>
|
||||
|
||||
+7
-10
@@ -3,7 +3,7 @@ import type { ComfyUIWorkflowTemplate } from '@proj-airi/stage-ui/stores/modules
|
||||
|
||||
import { REPLICATE_IMAGEGEN_PRESETS } from '@proj-airi/stage-shared'
|
||||
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
|
||||
import { Button, Checkbox, FieldInput, FieldRange, Select } from '@proj-airi/ui'
|
||||
import { Button, Checkbox, FieldInput, FieldRange, IconButton, Select } from '@proj-airi/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -171,7 +171,7 @@ function openReplicateModel() {
|
||||
<Button
|
||||
v-for="model in REPLICATE_IMAGEGEN_PRESETS"
|
||||
:key="model.id"
|
||||
variant="secondary"
|
||||
|
||||
:class="[
|
||||
'h-auto min-h-20 flex flex-col items-center justify-center rounded-xl border p-3 transition-all',
|
||||
selectedArtistryModel === model.id
|
||||
@@ -202,7 +202,7 @@ function openReplicateModel() {
|
||||
<Button
|
||||
v-for="wf in comfyuiWorkflows"
|
||||
:key="wf.id"
|
||||
variant="secondary"
|
||||
|
||||
:class="[
|
||||
'h-auto min-h-20 flex flex-col items-center justify-center rounded-xl border p-3 transition-all',
|
||||
selectedArtistryModel === wf.id
|
||||
@@ -225,11 +225,8 @@ function openReplicateModel() {
|
||||
:description="t('settings.pages.modules.artistry.model.description')"
|
||||
placeholder="e.g. black-forest-labs/flux-schnell"
|
||||
/>
|
||||
<Button
|
||||
<IconButton
|
||||
v-if="selectedArtistryProvider === 'replicate' && selectedArtistryModel"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
shape="square"
|
||||
:class="[
|
||||
'absolute right-3 top-9',
|
||||
]"
|
||||
@@ -237,7 +234,7 @@ function openReplicateModel() {
|
||||
@click="openReplicateModel"
|
||||
>
|
||||
<div i-solar:link-round-bold-duotone class="text-xl" />
|
||||
</Button>
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -253,14 +250,14 @@ function openReplicateModel() {
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
size="sm"
|
||||
@click="applyRecommendedInstructions"
|
||||
>
|
||||
{{ t('settings.pages.modules.artistry.card.instruction_sync.apply') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
size="sm"
|
||||
@click="pendingInstructionWf = null"
|
||||
>
|
||||
|
||||
@@ -99,14 +99,14 @@ async function handleImport(event: Event) {
|
||||
</div>
|
||||
<div :class="['flex flex-col items-start gap-2 sm:items-end']">
|
||||
<div :class="['flex flex-wrap gap-2']">
|
||||
<Button variant="secondary" @click="triggerExport">
|
||||
<Button @click="triggerExport">
|
||||
{{ t('settings.pages.data.sections.chats.export') }}
|
||||
</Button>
|
||||
<Button variant="primary" @click="triggerImportPicker">
|
||||
<Button @click="triggerImportPicker">
|
||||
{{ t('settings.pages.data.sections.chats.import') }}
|
||||
</Button>
|
||||
</div>
|
||||
<DoubleCheckButton variant="danger" @confirm="deleteChats">
|
||||
<DoubleCheckButton @confirm="deleteChats">
|
||||
{{ t('settings.pages.data.sections.chats.delete') }}
|
||||
<template #confirm>
|
||||
{{ t('settings.pages.data.confirmations.yes') }}
|
||||
|
||||
@@ -61,7 +61,7 @@ async function deleteAll() {
|
||||
</p>
|
||||
</div>
|
||||
<div :class="['flex flex-col items-start gap-2']">
|
||||
<DoubleCheckButton variant="danger" @confirm="resetProviders">
|
||||
<DoubleCheckButton @confirm="resetProviders">
|
||||
{{ t('settings.pages.data.sections.providers.reset') }}
|
||||
<template #confirm>
|
||||
{{ t('settings.pages.data.confirmations.yes') }}
|
||||
@@ -83,7 +83,7 @@ async function deleteAll() {
|
||||
</p>
|
||||
</div>
|
||||
<div :class="['flex flex-col items-start gap-2']">
|
||||
<DoubleCheckButton variant="danger" @confirm="deleteAll">
|
||||
<DoubleCheckButton @confirm="deleteAll">
|
||||
{{ t('settings.pages.data.sections.all.delete') }}
|
||||
<template #confirm>
|
||||
{{ t('settings.pages.data.confirmations.yes') }}
|
||||
|
||||
@@ -50,7 +50,7 @@ function resetModules() {
|
||||
</p>
|
||||
</div>
|
||||
<div :class="['flex flex-col items-start gap-2']">
|
||||
<DoubleCheckButton variant="danger" @confirm="deleteModels">
|
||||
<DoubleCheckButton @confirm="deleteModels">
|
||||
{{ t('settings.pages.data.sections.models.delete') }}
|
||||
<template #confirm>
|
||||
{{ t('settings.pages.data.confirmations.yes') }}
|
||||
@@ -72,7 +72,7 @@ function resetModules() {
|
||||
</p>
|
||||
</div>
|
||||
<div :class="['flex flex-col items-start gap-2']">
|
||||
<DoubleCheckButton variant="caution" @confirm="resetModules">
|
||||
<DoubleCheckButton color="orange" @confirm="resetModules">
|
||||
{{ t('settings.pages.data.sections.modules.reset') }}
|
||||
<template #confirm>
|
||||
{{ t('settings.pages.data.confirmations.yes') }}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createContext } from '@moeru/eventa/adapters/electron/renderer'
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { artistryTestComfyUIConnection, isStageTamagotchi } from '@proj-airi/stage-shared'
|
||||
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
|
||||
import { Button, FieldInput } from '@proj-airi/ui'
|
||||
import { Button, FieldInput, GhostButton } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -299,7 +299,7 @@ function copyToClipboard(text: string) {
|
||||
</div>
|
||||
<Button
|
||||
class="mb-0.5"
|
||||
variant="primary"
|
||||
|
||||
size="md"
|
||||
:icon="connectionStatus === 'testing' ? undefined : 'i-solar:plug-circle-bold-duotone'"
|
||||
:loading="connectionStatus === 'testing'"
|
||||
@@ -347,7 +347,7 @@ function copyToClipboard(text: string) {
|
||||
{{ t('settings.pages.providers.provider.comfyui.settings.workflows.title') }}
|
||||
</h3>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
size="sm"
|
||||
@click="showUploadSection = !showUploadSection"
|
||||
>
|
||||
@@ -384,14 +384,14 @@ function copyToClipboard(text: string) {
|
||||
}) }}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
<GhostButton
|
||||
|
||||
size="sm"
|
||||
class="!text-red-400 hover:!text-red-500"
|
||||
@click="removeWorkflow(wf.id)"
|
||||
>
|
||||
{{ t('settings.pages.providers.provider.comfyui.settings.workflows.remove') }}
|
||||
</Button>
|
||||
</GhostButton>
|
||||
</div>
|
||||
|
||||
<!-- Expanded Details -->
|
||||
@@ -424,7 +424,7 @@ function copyToClipboard(text: string) {
|
||||
{{ t('settings.pages.providers.provider.comfyui.settings.workflows.config_snippet') }}
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
size="sm"
|
||||
@click="copyToClipboard(generateExampleJson(wf))"
|
||||
>
|
||||
@@ -525,7 +525,7 @@ function copyToClipboard(text: string) {
|
||||
<div class="mt-2 flex items-center justify-between">
|
||||
<span class="text-xs text-neutral-400">{{ t('settings.pages.providers.provider.comfyui.settings.upload.fields_exposed', { count: totalExposed }) }}</span>
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
size="sm"
|
||||
:disabled="!pendingWorkflowName.trim() || totalExposed === 0"
|
||||
@click="saveWorkflow"
|
||||
|
||||
+2
-2
@@ -428,10 +428,10 @@ onBeforeUnmount(async () => {
|
||||
<div class="border border-neutral-200/80 rounded-xl bg-neutral-50/60 p-4 dark:border-neutral-700 dark:bg-neutral-900/40">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="space-x-3">
|
||||
<Button :disabled="!canStart" variant="primary" @click="startStreaming">
|
||||
<Button :disabled="!canStart" @click="startStreaming">
|
||||
{{ isRecording ? 'Streaming...' : 'Start Realtime Transcription' }}
|
||||
</Button>
|
||||
<Button :disabled="!canStop" variant="secondary" @click="stopStreaming">
|
||||
<Button :disabled="!canStop" @click="stopStreaming">
|
||||
Stop
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -103,7 +103,7 @@ function clearDefault() {
|
||||
@change="handleFileChange"
|
||||
>
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
class="flex-1"
|
||||
@click="triggerUpload"
|
||||
>
|
||||
@@ -112,7 +112,7 @@ function clearDefault() {
|
||||
</Button>
|
||||
<Button
|
||||
v-if="activeBackgroundId !== 'none'"
|
||||
variant="secondary"
|
||||
|
||||
@click="clearDefault"
|
||||
>
|
||||
<div :class="['i-solar:trash-bin-trash-bold-duotone', 'mr-2']" />
|
||||
@@ -171,7 +171,7 @@ function clearDefault() {
|
||||
<Button
|
||||
v-if="bg.id !== activeBackgroundId"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
|
||||
@click="setAsBackground(bg.id)"
|
||||
>
|
||||
<div :class="['i-solar:check-read-bold-duotone']" />
|
||||
@@ -179,7 +179,7 @@ function clearDefault() {
|
||||
<Button
|
||||
v-if="bg.type !== 'builtin'"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
|
||||
:class="['!bg-red-500 hover:!bg-red-600 !text-white']"
|
||||
@click="removeBackground(bg.id)"
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from '@proj-airi/stage-ui/stores/auth'
|
||||
import { useCharacterStore } from '@proj-airi/stage-ui/stores/characters'
|
||||
import { Button } from '@proj-airi/ui'
|
||||
import { GhostButton } from '@proj-airi/ui'
|
||||
import { computed, onMounted } from 'vue'
|
||||
|
||||
const characterStore = useCharacterStore()
|
||||
@@ -170,7 +170,7 @@ const characters = computed(() => Array.from(characterStore.characters.values())
|
||||
</div>
|
||||
<div :class="['grid grid-cols-3 items-center']">
|
||||
<div :class="['flex items-center justify-start']">
|
||||
<Button variant="ghost" size="sm" aria-label="Bookmark" @click="characterStore.bookmark(character.id)">
|
||||
<GhostButton size="sm" aria-label="Bookmark" @click="characterStore.bookmark(character.id)">
|
||||
<div
|
||||
:class="[
|
||||
character.bookmarked ? 'i-solar-star-bold' : 'i-solar-star-linear',
|
||||
@@ -186,10 +186,10 @@ const characters = computed(() => Array.from(characterStore.characters.values())
|
||||
>
|
||||
{{ formatCount(character.bookmarks) }}
|
||||
</span>
|
||||
</Button>
|
||||
</GhostButton>
|
||||
</div>
|
||||
<div :class="['flex items-center justify-center']">
|
||||
<Button variant="ghost" size="sm" aria-label="Like" @click="characterStore.like(character.id)">
|
||||
<GhostButton size="sm" aria-label="Like" @click="characterStore.like(character.id)">
|
||||
<div
|
||||
:class="[
|
||||
character.liked ? 'i-solar-heart-bold' : 'i-solar-heart-outline',
|
||||
@@ -205,7 +205,7 @@ const characters = computed(() => Array.from(characterStore.characters.values())
|
||||
>
|
||||
{{ formatCount(character.likes) }}
|
||||
</span>
|
||||
</Button>
|
||||
</GhostButton>
|
||||
</div>
|
||||
<div :class="['flex items-center justify-end']">
|
||||
<div
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from '@proj-airi/stage-ui/components'
|
||||
import { getDefinedProvider, getSchemaDefault, getValidatorsOfProvider, validateProvider } from '@proj-airi/stage-ui/libs'
|
||||
import { useProviderCatalogStore } from '@proj-airi/stage-ui/stores/provider-catalog'
|
||||
import { Button, Callout, FieldCombobox, FieldInput, FieldKeyValues } from '@proj-airi/ui'
|
||||
import { Button, Callout, FieldCombobox, FieldInput, FieldKeyValues, GhostButton } from '@proj-airi/ui'
|
||||
import { useCloned, useDebounceFn } from '@vueuse/core'
|
||||
import { DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuRoot, DropdownMenuTrigger } from 'reka-ui'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
@@ -361,7 +361,7 @@ function handleDeleteProvider() {
|
||||
<div :class="['flex', 'items-center', 'gap-2']">
|
||||
<DropdownMenuRoot>
|
||||
<DropdownMenuTrigger as-child :aria-label="t('settings.pages.providers.catalog.edit.actions.more-options')">
|
||||
<Button size="sm" variant="secondary">
|
||||
<Button size="sm">
|
||||
<div :class="['i-solar:menu-dots-bold']" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -510,7 +510,7 @@ function handleDeleteProvider() {
|
||||
<div :class="['text-xs', 'text-neutral-400']">
|
||||
{{ t('settings.pages.providers.catalog.edit.validators.title') }}
|
||||
</div>
|
||||
<Button size="sm" variant="secondary" :loading="isValidating" :disabled="isValidating" @click="runValidation">
|
||||
<Button size="sm" :loading="isValidating" :disabled="isValidating" @click="runValidation">
|
||||
{{ t('settings.pages.providers.catalog.edit.validators.actions.validate') }}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -532,9 +532,9 @@ function handleDeleteProvider() {
|
||||
{{ step.label }}
|
||||
</div>
|
||||
<div :class="['flex', 'flex-col', 'items-end', 'gap-2']">
|
||||
<Button
|
||||
<GhostButton
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
|
||||
:title="
|
||||
step.status === 'valid' ? t('settings.pages.providers.catalog.edit.validators.status.valid')
|
||||
: step.status === 'invalid' ? t('settings.pages.providers.catalog.edit.validators.status.invalid')
|
||||
@@ -552,7 +552,7 @@ function handleDeleteProvider() {
|
||||
step.status === 'idle' ? 'i-solar:minus-circle-line-duotone text-neutral-300 dark:text-neutral-600' : '',
|
||||
]"
|
||||
/>
|
||||
</Button>
|
||||
</GhostButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -573,7 +573,7 @@ function handleDeleteProvider() {
|
||||
<span :class="['text-xs', 'text-neutral-600', 'dark:text-neutral-300']">
|
||||
{{ t('settings.pages.providers.catalog.edit.validation.failed.description') }}
|
||||
</span>
|
||||
<Button size="sm" variant="caution" @click="handleSaveAnyway">
|
||||
<Button size="sm" color="orange" variant="primary" @click="handleSaveAnyway">
|
||||
{{ t('settings.pages.providers.catalog.edit.validation.failed.action') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -181,6 +181,7 @@
|
||||
"@iconify-json/eos-icons": "catalog:",
|
||||
"@iconify-json/lucide": "catalog:",
|
||||
"@iconify-json/mingcute": "catalog:",
|
||||
"@iconify-json/ph": "catalog:",
|
||||
"@iconify-json/simple-icons": "catalog:",
|
||||
"@iconify-json/solar": "catalog:",
|
||||
"@iconify-json/svg-spinners": "catalog:",
|
||||
|
||||
@@ -86,7 +86,7 @@ function handleSelect(provider: OAuthProvider) {
|
||||
<Button
|
||||
v-for="provider in providers"
|
||||
:key="provider.id"
|
||||
variant="secondary"
|
||||
|
||||
size="lg"
|
||||
block
|
||||
:icon="provider.icon"
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { Button } from '@proj-airi/ui'
|
||||
import { Button, GhostButton, IconButton, OverlayButton } from '@proj-airi/ui'
|
||||
import { computed, shallowRef } from 'vue'
|
||||
|
||||
const buttonColors = ['neutral', 'primary', 'cyan', 'blue', 'green', 'lime', 'amber', 'red', 'orange', 'purple', 'pink'] as const
|
||||
|
||||
const liked = shallowRef(false)
|
||||
const favorited = shallowRef(false)
|
||||
|
||||
const heartIcon = computed(() => liked.value ? 'i-ph:heart-fill' : 'i-ph:heart')
|
||||
const starIcon = computed(() => favorited.value ? 'i-ph:star-fill' : 'i-ph:star')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -12,109 +21,89 @@ import { Button } from '@proj-airi/ui'
|
||||
<ThemeColorsHueControl />
|
||||
</template>
|
||||
|
||||
<Variant
|
||||
id="basic"
|
||||
title="Basic Button"
|
||||
<Variant id="variant" title="Variant">
|
||||
<div :class="['flex flex-wrap items-center gap-4 p-4']">
|
||||
<Button label="Primary" color="primary" variant="primary" />
|
||||
<Button label="Secondary" color="primary" variant="secondary" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant id="color" title="Color">
|
||||
<div :class="['grid grid-cols-2 gap-4 p-4 sm:grid-cols-4']">
|
||||
<div
|
||||
v-for="color in buttonColors"
|
||||
:key="color"
|
||||
:class="['flex flex-col gap-3']"
|
||||
>
|
||||
<div flex flex-col>
|
||||
<div class="flex flex-row gap-4 p-2">
|
||||
<Button label="Primary Button" />
|
||||
<Button label="Secondary Button" variant="secondary" />
|
||||
<Button label="Caution Button" variant="caution" />
|
||||
<Button label="Danger Button" variant="danger" />
|
||||
<Button label="Ghost Button" variant="ghost" />
|
||||
<Button :label="`${color} primary`" :color="color" variant="primary" />
|
||||
<Button :label="`${color} secondary`" :color="color" variant="secondary" />
|
||||
<Button :label="`${color} disabled`" :color="color" disabled />
|
||||
</div>
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant
|
||||
id="disabled"
|
||||
title="Disabled State"
|
||||
>
|
||||
<div class="flex flex-row gap-4 p-2">
|
||||
<Button label="Disabled Primary" disabled />
|
||||
<Button label="Disabled Secondary" variant="secondary" disabled />
|
||||
<Button label="Disabled Caution" variant="caution" disabled />
|
||||
<Button label="Disabled Danger" variant="danger" disabled />
|
||||
<Button label="Disabled Ghost" variant="ghost" disabled />
|
||||
<Variant id="shape" title="Shape">
|
||||
<div :class="['flex flex-wrap items-center gap-4 p-4']">
|
||||
<Button label="Rect" shape="rect" />
|
||||
<Button label="Rounded" shape="rounded" />
|
||||
<Button label="Parallelogram" shape="parallelogram" />
|
||||
<Button icon="i-ph:plus" shape="circle" aria-label="Circle" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant
|
||||
id="loading"
|
||||
title="Loading State"
|
||||
>
|
||||
<div class="flex flex-row gap-4 p-2">
|
||||
<Button label="Loading Primary" loading />
|
||||
<Button label="Loading Secondary" variant="secondary" loading />
|
||||
<Button label="Loading Caution" variant="caution" loading />
|
||||
<Button label="Loading Danger" variant="danger" loading />
|
||||
<Button label="Loading Ghost" variant="ghost" loading />
|
||||
<Variant id="states" title="States">
|
||||
<div :class="['flex flex-wrap items-center gap-4 p-4']">
|
||||
<Button label="Default" />
|
||||
<Button label="Disabled" disabled />
|
||||
<Button label="Loading" loading />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant
|
||||
id="with-icon"
|
||||
title="With Icon"
|
||||
>
|
||||
<div class="flex flex-row gap-4 p-2">
|
||||
<Button label="Add Item" icon="i-solar:add-circle-line-duotone" />
|
||||
<Button label="Warn" variant="caution" icon="i-solar:danger-triangle-line-duotone" />
|
||||
<Button label="Delete Item" variant="danger" icon="i-solar:trash-bin-trash-line-duotone" />
|
||||
<Button label="Settings" variant="secondary" icon="i-solar:settings-line-duotone" />
|
||||
<Button label="Plain" variant="ghost" icon="i-solar:emoji-funny-circle-line-duotone" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant
|
||||
id="icon-only"
|
||||
title="Icon Only"
|
||||
>
|
||||
<div class="flex gap-4 p-2">
|
||||
<Button icon="i-solar:add-circle-line-duotone" />
|
||||
<Button variant="secondary" icon="i-solar:settings-line-duotone" />
|
||||
<Button variant="caution" icon="i-solar:danger-triangle-line-duotone" />
|
||||
<Button variant="danger" icon="i-solar:trash-bin-trash-line-duotone" />
|
||||
<Button variant="ghost" icon="i-solar:neutral-face-outline" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant
|
||||
id="sizes"
|
||||
title="Button Sizes"
|
||||
>
|
||||
<div class="flex items-center gap-4 p-2">
|
||||
<Variant id="size" title="Size">
|
||||
<div :class="['flex flex-wrap items-center gap-4 p-4']">
|
||||
<Button label="Small" size="sm" />
|
||||
<Button label="Medium" size="md" />
|
||||
<Button label="Large" size="lg" />
|
||||
<Button label="Caution" variant="caution" />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant
|
||||
id="block"
|
||||
title="Block Button"
|
||||
>
|
||||
<div class="flex flex-col gap-4 p-2">
|
||||
<Button label="Block Primary" block />
|
||||
<Button label="Block Secondary" variant="secondary" block />
|
||||
<Button label="Block Caution" variant="caution" block />
|
||||
<Button label="Block Danger" variant="danger" block />
|
||||
<Variant id="ghost-button" title="Ghost Button">
|
||||
<div :class="['flex flex-wrap items-center gap-4 p-4']">
|
||||
<GhostButton label="Action" icon="i-solar:pen-new-square-linear" />
|
||||
<GhostButton label="Active" icon="i-solar:check-circle-linear" active />
|
||||
<GhostButton label="Disabled" icon="i-solar:forbidden-circle-linear" disabled />
|
||||
<GhostButton label="Loading" loading />
|
||||
</div>
|
||||
</Variant>
|
||||
|
||||
<Variant
|
||||
id="slot"
|
||||
title="Using Slot Content"
|
||||
>
|
||||
<div class="flex flex-row gap-4 p-2">
|
||||
<Button>Custom Content</Button>
|
||||
<Button variant="secondary">
|
||||
<div class="flex items-center gap-2">
|
||||
<div i-solar:star-line-duotone />
|
||||
Complex Content
|
||||
<Variant id="icon-button" title="Icon Button">
|
||||
<div :class="['flex flex-wrap items-center gap-4 p-4']">
|
||||
<IconButton
|
||||
:icon="heartIcon"
|
||||
:class="[liked && 'text-red', 'text-2xl']"
|
||||
:aria-label="liked ? 'Unlike' : 'Like'"
|
||||
:aria-pressed="liked"
|
||||
size="unset"
|
||||
@click="liked = !liked"
|
||||
/>
|
||||
<IconButton
|
||||
:icon="starIcon"
|
||||
:class="[favorited && 'text-amber', 'text-2xl']"
|
||||
:aria-label="favorited ? 'Remove favorite' : 'Favorite'"
|
||||
:aria-pressed="favorited"
|
||||
size="unset"
|
||||
@click="favorited = !favorited"
|
||||
/>
|
||||
<IconButton icon="i-ph:trash" disabled aria-label="Delete disabled" />
|
||||
<IconButton loading aria-label="Loading" />
|
||||
</div>
|
||||
</Button>
|
||||
</Variant>
|
||||
|
||||
<Variant id="overlay-button" title="Overlay Button">
|
||||
<div :class="['flex flex-wrap items-center gap-4 p-4']">
|
||||
<OverlayButton label="Overlay" icon="i-solar:widget-2-linear" />
|
||||
<OverlayButton label="Disabled" icon="i-solar:lock-linear" disabled />
|
||||
<OverlayButton label="Loading" loading />
|
||||
</div>
|
||||
</Variant>
|
||||
</Story>
|
||||
|
||||
@@ -34,7 +34,7 @@ function note(message: string) {
|
||||
</template>
|
||||
</DoubleCheckButton>
|
||||
<DoubleCheckButton
|
||||
variant="caution"
|
||||
color="orange"
|
||||
@confirm="note('Confirmed reset')"
|
||||
@cancel="note('Cancelled reset')"
|
||||
>
|
||||
@@ -66,7 +66,7 @@ function note(message: string) {
|
||||
Never mind
|
||||
</template>
|
||||
<template #cancel-botton-icon>
|
||||
<div class="i-solar:close-square-line-duotone h-4 w-4" />
|
||||
<div class="i-ph:x h-4 w-4" />
|
||||
</template>
|
||||
</DoubleCheckButton>
|
||||
</Variant>
|
||||
|
||||
@@ -72,7 +72,7 @@ function saveSettings() {
|
||||
<div>
|
||||
<Button
|
||||
:label="t('settings.common.save')"
|
||||
variant="primary"
|
||||
|
||||
@click="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ function saveSettings() {
|
||||
<div>
|
||||
<Button
|
||||
:label="t('settings.common.save')"
|
||||
variant="primary"
|
||||
|
||||
@click="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,7 @@ function saveSettings() {
|
||||
<div>
|
||||
<Button
|
||||
:label="t('settings.common.save')"
|
||||
variant="primary"
|
||||
|
||||
@click="saveSettings"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { ChatHistoryItem, ErrorMessage } from '../../../../types/chat'
|
||||
|
||||
import { isStageCapacitor, isStageWeb } from '@proj-airi/stage-shared'
|
||||
import { Button } from '@proj-airi/ui'
|
||||
import { IconButton } from '@proj-airi/ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { MarkdownRenderer } from '../../../markdown'
|
||||
@@ -86,10 +86,7 @@ const copyText = computed(() => getChatHistoryItemCopyText(props.message as Chat
|
||||
'self-end mt-1 w-fit',
|
||||
]"
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
shape="square"
|
||||
<IconButton
|
||||
icon="i-solar:refresh-bold"
|
||||
:aria-label="retryLabel"
|
||||
@click="emit('retry')"
|
||||
|
||||
+2
-1
@@ -56,7 +56,7 @@ function onSubmit(payload: BugReportDialogSubmitPayload) {
|
||||
]"
|
||||
>
|
||||
<Button
|
||||
variant="danger"
|
||||
|
||||
:class="[
|
||||
'w-fit',
|
||||
]"
|
||||
@@ -71,6 +71,7 @@ function onSubmit(payload: BugReportDialogSubmitPayload) {
|
||||
:class="[
|
||||
'i-solar:danger-circle-outline h-4 w-4',
|
||||
]"
|
||||
color="red" variant="primary"
|
||||
/>
|
||||
{{ resolvedTriggerLabel }}
|
||||
</span>
|
||||
|
||||
+9
-9
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { Live2DValidationReport } from '@proj-airi/stage-ui-live2d'
|
||||
|
||||
import { Button } from '@proj-airi/ui'
|
||||
import { Button, GhostButton } from '@proj-airi/ui'
|
||||
import { useMediaQuery, useResizeObserver, useScreenSafeArea } from '@vueuse/core'
|
||||
import { DialogContent, DialogOverlay, DialogPortal, DialogRoot, DialogTitle } from 'reka-ui'
|
||||
import { DrawerContent, DrawerHandle, DrawerOverlay, DrawerPortal, DrawerRoot } from 'vaul-vue'
|
||||
@@ -55,7 +55,7 @@ function handleFix(err: string) {
|
||||
<DialogTitle class="text-lg text-neutral-900 font-semibold dark:text-neutral-100">
|
||||
Live2D Model Audit Report
|
||||
</DialogTitle>
|
||||
<Button size="sm" variant="secondary" @click="handleClose">
|
||||
<Button size="sm" @click="handleClose">
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
@@ -96,9 +96,9 @@ function handleFix(err: string) {
|
||||
<ul class="list-none pl-0 space-y-1">
|
||||
<li v-for="(err, i) in report.errors" :key="i" class="flex items-center justify-between gap-2 rounded bg-red-50/50 p-2 text-red-800 dark:bg-red-900/20 dark:text-red-300">
|
||||
<span>{{ err }}</span>
|
||||
<Button v-if="canFixError(err)" size="sm" variant="secondary-muted" class="h-6 px-2 text-[10px] tracking-wider uppercase" @click="handleFix(err)">
|
||||
<GhostButton v-if="canFixError(err)" size="sm" class="h-6 px-2 text-[10px] tracking-wider uppercase" @click="handleFix(err)">
|
||||
Quick Fix
|
||||
</Button>
|
||||
</GhostButton>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -117,7 +117,7 @@ function handleFix(err: string) {
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="mt-2 flex justify-end gap-2">
|
||||
<Button variant="secondary" @click="handleClose">
|
||||
<Button @click="handleClose">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button v-if="report.status !== 'INVALID'" @click="handleConfirm">
|
||||
@@ -145,7 +145,7 @@ function handleFix(err: string) {
|
||||
<div class="text-lg text-neutral-900 font-semibold dark:text-neutral-100">
|
||||
Model Audit Report
|
||||
</div>
|
||||
<Button size="sm" variant="secondary" @click="handleClose">
|
||||
<Button size="sm" @click="handleClose">
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
@@ -170,9 +170,9 @@ function handleFix(err: string) {
|
||||
<ul class="list-none pl-0 space-y-1">
|
||||
<li v-for="(err, i) in report.errors" :key="i" class="flex items-center justify-between gap-2 rounded bg-red-50/50 p-2 text-red-800 dark:bg-red-900/20 dark:text-red-300">
|
||||
<span>{{ err }}</span>
|
||||
<Button v-if="canFixError(err)" size="sm" variant="secondary-muted" class="h-6 px-2 text-[10px] tracking-wider uppercase" @click="handleFix(err)">
|
||||
<GhostButton v-if="canFixError(err)" size="sm" class="h-6 px-2 text-[10px] tracking-wider uppercase" @click="handleFix(err)">
|
||||
Fix
|
||||
</Button>
|
||||
</GhostButton>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -189,7 +189,7 @@ function handleFix(err: string) {
|
||||
<Button v-if="report.status !== 'INVALID'" @click="handleConfirm">
|
||||
{{ report.status === 'WARNING' ? 'Import Anyway' : 'Confirm Import' }}
|
||||
</Button>
|
||||
<Button variant="secondary" @click="handleClose">
|
||||
<Button @click="handleClose">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -430,7 +430,7 @@ mmdDialog.onChange(handleAddMMDModel)
|
||||
<div>{{ mapFormatRenderer[model.format] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button class="hidden md:block" variant="secondary" @click="handlePick(model)">
|
||||
<Button class="hidden md:block" @click="handlePick(model)">
|
||||
{{ t('settings.model-select.select-model.select') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
+4
-4
@@ -57,7 +57,7 @@ function confirm() {
|
||||
<DialogTitle class="text-lg text-neutral-900 font-semibold dark:text-neutral-100">
|
||||
Tachie Model Audit Report
|
||||
</DialogTitle>
|
||||
<Button size="sm" variant="secondary" @click="close">
|
||||
<Button size="sm" @click="close">
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
@@ -133,7 +133,7 @@ function confirm() {
|
||||
</div>
|
||||
|
||||
<div :class="['mt-2', 'flex justify-end gap-2']">
|
||||
<Button variant="secondary" @click="close">
|
||||
<Button @click="close">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button v-if="report.status !== 'INVALID'" @click="confirm">
|
||||
@@ -161,7 +161,7 @@ function confirm() {
|
||||
<div class="text-lg text-neutral-900 font-semibold dark:text-neutral-100">
|
||||
Tachie Model Audit Report
|
||||
</div>
|
||||
<Button size="sm" variant="secondary" @click="close">
|
||||
<Button size="sm" @click="close">
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
@@ -199,7 +199,7 @@ function confirm() {
|
||||
<Button v-if="report.status !== 'INVALID'" @click="confirm">
|
||||
{{ report.status === 'WARNING' ? 'Import Anyway' : 'Confirm Import' }}
|
||||
</Button>
|
||||
<Button variant="secondary" @click="close">
|
||||
<Button @click="close">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ const { privacyPolicyUrl } = useAnalytics()
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
:label="t('settings.dialogs.onboarding.next')"
|
||||
@click="props.onNext"
|
||||
/>
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ const {
|
||||
<div class="w-full flex-shrink-0">
|
||||
<!-- Action Buttons -->
|
||||
<Button
|
||||
variant="primary"
|
||||
|
||||
class="w-full"
|
||||
:disabled="!activeModel"
|
||||
:loading="isLoadingActiveProviderModels"
|
||||
|
||||
@@ -138,7 +138,7 @@ function handleLocalSetup() {
|
||||
:enter="{ opacity: 1 }"
|
||||
:duration="500"
|
||||
:delay="250"
|
||||
variant="secondary"
|
||||
|
||||
:label="t('settings.dialogs.onboarding.setupWithoutSigningIn')"
|
||||
:class="['flex-1']"
|
||||
@click="handleLocalSetup"
|
||||
|
||||
+2
-2
@@ -42,7 +42,7 @@ const selectedSteps = computed(() => {
|
||||
<DialogTitle :class="['text-lg', 'font-semibold', 'text-neutral-900', 'dark:text-neutral-100']">
|
||||
{{ props.title }}
|
||||
</DialogTitle>
|
||||
<Button size="sm" variant="secondary" @click="() => showDialog = false">
|
||||
<Button size="sm" @click="() => showDialog = false">
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
@@ -81,7 +81,7 @@ const selectedSteps = computed(() => {
|
||||
<div :class="['text-lg', 'font-semibold', 'text-neutral-900', 'dark:text-neutral-100']">
|
||||
{{ props.title }}
|
||||
</div>
|
||||
<Button size="sm" variant="secondary" @click="() => showDialog = false">
|
||||
<Button size="sm" @click="() => showDialog = false">
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { clearModelCache, formatBytes, getModelCacheSize, isModelCached } from '@proj-airi/stage-ui/libs/inference'
|
||||
import { Button } from '@proj-airi/ui'
|
||||
import { Button, GhostButton } from '@proj-airi/ui'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const cacheSize = ref(0)
|
||||
@@ -109,8 +109,8 @@ onMounted(refresh)
|
||||
|
||||
<!-- Actions -->
|
||||
<div flex items-center justify-between>
|
||||
<Button
|
||||
variant="secondary-muted"
|
||||
<GhostButton
|
||||
|
||||
size="sm"
|
||||
label="Refresh"
|
||||
icon="i-solar:refresh-linear"
|
||||
@@ -119,13 +119,14 @@ onMounted(refresh)
|
||||
/>
|
||||
<Button
|
||||
v-if="cacheSize > 0"
|
||||
variant="danger"
|
||||
|
||||
size="sm"
|
||||
:label="clearing ? 'Clearing...' : 'Clear All Cache'"
|
||||
icon="i-solar:trash-bin-trash-bold"
|
||||
:disabled="clearing || loading"
|
||||
:loading="clearing"
|
||||
@click="handleClearCache"
|
||||
color="red"
|
||||
variant="primary" @click="handleClearCache"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ defineProps<{
|
||||
<label
|
||||
class="w-full flex cursor-pointer items-center justify-between rounded-lg px-4 py-3 text-sm outline-none transition-all duration-250 ease-in-out"
|
||||
bg="neutral-50 dark:neutral-800"
|
||||
hover="bg-neutral-200 dark:bg-neutral-700"
|
||||
hover="bg-neutral-100 dark:bg-neutral-700"
|
||||
>
|
||||
<div>
|
||||
{{ $t(text) }}
|
||||
|
||||
@@ -16,7 +16,7 @@ const emits = defineEmits<{
|
||||
|
||||
<template>
|
||||
<Bar :text="text" :description="description">
|
||||
<Button @click="emits('click')">
|
||||
<Button :outline="false" @click="emits('click')">
|
||||
<slot />
|
||||
</Button>
|
||||
</Bar>
|
||||
|
||||
@@ -283,7 +283,7 @@ function handleMotionSelect(selectedMotionPath: string | number | undefined) {
|
||||
{{ t('settings.live2d.theme-color-from-model.description') }}
|
||||
</p>
|
||||
<ColorPalette class="mb-4 mt-2" :colors="palette.map(hex => ({ hex, name: hex }))" mx-auto />
|
||||
<Button variant="secondary" :disabled="!canExtractColors" @click="$emit('extractColorsFromModel')">
|
||||
<Button :disabled="!canExtractColors" @click="$emit('extractColorsFromModel')">
|
||||
{{ t('settings.live2d.theme-color-from-model.button-extract.title') }}
|
||||
</Button>
|
||||
</Section>
|
||||
@@ -438,7 +438,7 @@ function handleMotionSelect(selectedMotionPath: string | number | undefined) {
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
class="mt-4 w-full"
|
||||
@click="resetToDefaultParameters"
|
||||
>
|
||||
@@ -446,7 +446,7 @@ function handleMotionSelect(selectedMotionPath: string | number | undefined) {
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
class="mt-2 w-full"
|
||||
:disabled="clearingCache"
|
||||
:loading="clearingCache"
|
||||
@@ -774,10 +774,10 @@ function handleMotionSelect(selectedMotionPath: string | number | undefined) {
|
||||
|
||||
<!-- Action buttons -->
|
||||
<div mt-4 flex gap-2>
|
||||
<Button variant="secondary" @click="expressionStore.saveDefaults()">
|
||||
<Button @click="expressionStore.saveDefaults()">
|
||||
{{ t('settings.live2d.expressions.save-default') }}
|
||||
</Button>
|
||||
<Button variant="secondary" @click="expressionStore.resetAll()">
|
||||
<Button @click="expressionStore.resetAll()">
|
||||
{{ t('settings.live2d.expressions.reset') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { MMDGazeMode, MorphSlot } from '@proj-airi/stage-ui-mmd'
|
||||
import type { ModelSettingsRuntimeSnapshot } from './runtime'
|
||||
|
||||
import { controlConfig, useMMD } from '@proj-airi/stage-ui-mmd'
|
||||
import { Button, FieldCheckbox, FieldCombobox, FieldRange } from '@proj-airi/ui'
|
||||
import { Button, FieldCheckbox, FieldCombobox, FieldRange, GhostButton } from '@proj-airi/ui'
|
||||
import { useFileDialog } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
@@ -139,7 +139,7 @@ function removeMotion(id: string) {
|
||||
>
|
||||
<template v-if="allowExtractColors">
|
||||
<ColorPalette class="mb-4 mt-2" :colors="palette.map(hex => ({ hex, name: hex }))" mx-auto />
|
||||
<Button variant="secondary" :disabled="!canExtractColors" @click="$emit('extractColorsFromModel')">
|
||||
<Button :disabled="!canExtractColors" @click="$emit('extractColorsFromModel')">
|
||||
{{ t('settings.mmd.theme-color-from-model.button-extract.title') }}
|
||||
</Button>
|
||||
</template>
|
||||
@@ -177,10 +177,10 @@ function removeMotion(id: string) {
|
||||
</div>
|
||||
<div />
|
||||
<template v-for="option in trackingOptions" :key="option.value">
|
||||
<Button
|
||||
<GhostButton
|
||||
:class="[option.class, 'w-auto']"
|
||||
size="sm"
|
||||
:variant="gazeMode === option.value ? 'primary' : 'secondary'"
|
||||
:active="gazeMode === option.value"
|
||||
:label="option.label"
|
||||
@click="gazeMode = option.value"
|
||||
/>
|
||||
@@ -266,7 +266,7 @@ function removeMotion(id: string) {
|
||||
<p :class="['mb-2', 'text-xs', 'text-neutral-500', 'dark:text-neutral-400']">
|
||||
{{ t('settings.mmd.animation.description') }}
|
||||
</p>
|
||||
<Button variant="secondary" :class="['mb-2']" @click="vmdDialog.open()">
|
||||
<Button :class="['mb-2']" @click="vmdDialog.open()">
|
||||
{{ t('settings.mmd.animation.import-motion') }}
|
||||
</Button>
|
||||
<FieldCombobox
|
||||
@@ -286,11 +286,11 @@ function removeMotion(id: string) {
|
||||
{{ motion.name }}
|
||||
</div>
|
||||
<div :class="['flex', 'shrink-0', 'items-center', 'gap-1']">
|
||||
<Button variant="secondary" @click="playMotionOnce(motion.name)">
|
||||
<Button @click="playMotionOnce(motion.name)">
|
||||
{{ t('settings.mmd.animation.play-once') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
|
||||
:aria-label="t('settings.mmd.animation.remove')"
|
||||
@click="removeMotion(motion.id)"
|
||||
>
|
||||
|
||||
@@ -98,7 +98,7 @@ async function handleModelPick(selectedModel: DisplayModel | undefined) {
|
||||
</Callout>
|
||||
<div :class="['flex flex-wrap items-center gap-2']">
|
||||
<ModelSelectorDialog v-model:show="modelSelectorOpen" :selected-model="stageModelSelectedDisplayModel" @pick="handleModelPick">
|
||||
<Button variant="secondary">
|
||||
<Button>
|
||||
{{ t('settings.model-select.select-model.button') }}
|
||||
</Button>
|
||||
</ModelSelectorDialog>
|
||||
|
||||
@@ -122,7 +122,7 @@ function handleSkinSelect(skinName: string | number | undefined) {
|
||||
:expand="false"
|
||||
>
|
||||
<ColorPalette class="mb-4 mt-2" :colors="palette.map(hex => ({ hex, name: hex }))" mx-auto />
|
||||
<Button variant="secondary" :disabled="!canExtractColors" @click="$emit('extractColorsFromModel')">
|
||||
<Button :disabled="!canExtractColors" @click="$emit('extractColorsFromModel')">
|
||||
{{ t('settings.spine.theme-color-from-model.button-extract.title') }}
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
@@ -82,7 +82,7 @@ const canExtractColors = computed(() => props.runtimeSnapshot.canCapturePreview)
|
||||
:expand="false"
|
||||
>
|
||||
<ColorPalette class="mb-4 mt-2" :colors="palette.map(hex => ({ hex, name: hex }))" mx-auto />
|
||||
<Button variant="secondary" :disabled="!canExtractColors" @click="emit('extractColorsFromModel')">
|
||||
<Button :disabled="!canExtractColors" @click="emit('extractColorsFromModel')">
|
||||
{{ t('settings.tachie.theme-color-from-model.button-extract.title') }}
|
||||
</Button>
|
||||
</Section>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { ModelSettingsRuntimeSnapshot } from './runtime'
|
||||
|
||||
import { useModelStore } from '@proj-airi/stage-ui-three'
|
||||
import { Button, Callout, SelectTab } from '@proj-airi/ui'
|
||||
import { Button, Callout, GhostButton, SelectTab } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -94,7 +94,7 @@ const envOptions = computed(() => [
|
||||
>
|
||||
<template v-if="allowExtractColors">
|
||||
<ColorPalette class="mb-4 mt-2" :colors="palette.map(hex => ({ hex, name: hex }))" mx-auto />
|
||||
<Button variant="secondary" :disabled="controlsLocked || !canExtractColors" @click="$emit('extractColorsFromModel')">
|
||||
<Button :disabled="controlsLocked || !canExtractColors" @click="$emit('extractColorsFromModel')">
|
||||
{{ t('settings.vrm.theme-color-from-model.button-extract.title') }}
|
||||
</Button>
|
||||
</template>
|
||||
@@ -132,11 +132,11 @@ const envOptions = computed(() => [
|
||||
</div>
|
||||
<div />
|
||||
<template v-for="option in trackingOptions" :key="option.value">
|
||||
<Button
|
||||
<GhostButton
|
||||
:class="[option.class, 'w-auto']"
|
||||
:disabled="controlsLocked"
|
||||
size="sm"
|
||||
:variant="trackingMode === option.value ? 'primary' : 'secondary'"
|
||||
:active="trackingMode === option.value"
|
||||
:label="option.label"
|
||||
@click="trackingMode = option.value"
|
||||
/>
|
||||
|
||||
@@ -44,7 +44,7 @@ function handleNotNow() {
|
||||
'w-full flex items-center gap-2',
|
||||
]"
|
||||
>
|
||||
<Button w-full size="sm" variant="secondary" @click="() => handleNotNow()">
|
||||
<Button w-full size="sm" @click="() => handleNotNow()">
|
||||
<div i-solar:close-circle-line-duotone />
|
||||
<div text-nowrap>
|
||||
{{ t('base.toaster.pwaUpdateReady.action.notNow') }}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import type { BasicButtonProps, ButtonSize } from './button-types'
|
||||
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { TransitionBidirectional } from '../animations'
|
||||
|
||||
const props = withDefaults(defineProps<BasicButtonProps>(), {
|
||||
disabled: false,
|
||||
loading: false,
|
||||
size: 'md',
|
||||
block: false,
|
||||
})
|
||||
|
||||
const disabled = computed(() => props.disabled || props.loading)
|
||||
|
||||
const sizeClasses: Record<ButtonSize, string> = {
|
||||
sm: 'px-3 py-1.5 text-xs',
|
||||
md: 'px-4 py-2 text-sm',
|
||||
lg: 'px-5 py-3 text-base',
|
||||
unset: '',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:disabled="disabled"
|
||||
:class="[
|
||||
'inline-flex items-center justify-center whitespace-nowrap font-medium',
|
||||
'transition-all duration-200 ease-in-out active:scale-95',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100',
|
||||
sizeClasses[size],
|
||||
block && 'w-full',
|
||||
]"
|
||||
>
|
||||
<span class="basic-button-content inline-flex items-center justify-center gap-2">
|
||||
<TransitionBidirectional
|
||||
from-class="opacity-0 mr-0! w-0!"
|
||||
active-class="transition-[width,margin] ease-in-out overflow-hidden transition-100"
|
||||
>
|
||||
<span v-if="loading || icon" class="h-4 w-4 shrink-0">
|
||||
<span v-if="loading" class="i-svg-spinners:ring-resize block h-4 w-4" />
|
||||
<span v-else-if="icon" :class="[icon, 'block', props.size !== 'unset' ? 'h-4 w-4' : '']" />
|
||||
</span>
|
||||
</TransitionBidirectional>
|
||||
<span v-if="label">{{ label }}</span>
|
||||
<slot v-else />
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
@@ -0,0 +1,39 @@
|
||||
/** Available button sizes shared by the button primitives. */
|
||||
export type ButtonSize = 'sm' | 'md' | 'lg' | 'unset'
|
||||
|
||||
/** Geometry used by solid buttons. */
|
||||
export type ButtonShape = 'rect' | 'rounded' | 'circle' | 'parallelogram'
|
||||
|
||||
/** Supported button color families backed by the Wind3 palette. */
|
||||
export type ButtonColor = 'neutral' | 'primary' | 'cyan' | 'blue' | 'green' | 'lime' | 'amber' | 'red' | 'orange' | 'purple' | 'pink'
|
||||
|
||||
/** Visual emphasis within a button color family. */
|
||||
export type ButtonVariant = 'primary' | 'secondary'
|
||||
|
||||
/** Shared interaction and content props implemented by {@link BasicButton}. */
|
||||
export interface BasicButtonProps {
|
||||
/** UnoCSS/Iconify class rendered before the label or default slot. */
|
||||
icon?: string
|
||||
/** Text content used instead of the default slot. */
|
||||
label?: string
|
||||
/** Prevents interaction. */
|
||||
disabled?: boolean
|
||||
/** Replaces the icon with a spinner and prevents interaction. */
|
||||
loading?: boolean
|
||||
/** Controls padding and type scale. @default 'md' */
|
||||
size?: ButtonSize
|
||||
/** Expands the button to the width of its container. @default false */
|
||||
block?: boolean
|
||||
}
|
||||
|
||||
/** Visual props for the solid button. */
|
||||
export interface ButtonProps extends BasicButtonProps {
|
||||
/** Controls the button geometry. @default 'rect' */
|
||||
shape?: ButtonShape
|
||||
/** Selects the button color family. @default 'neutral' */
|
||||
color?: ButtonColor
|
||||
/** Selects solid or subtle emphasis within the color family. @default 'secondary' */
|
||||
variant?: ButtonVariant
|
||||
/** Shows the offset outline on hover and keyboard focus. @default true */
|
||||
outline?: boolean
|
||||
}
|
||||
@@ -1,190 +1,450 @@
|
||||
<script setup lang="ts">
|
||||
import type { ButtonColor, ButtonProps, ButtonShape, ButtonSize, ButtonVariant } from './button-types'
|
||||
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { TransitionBidirectional } from '../animations'
|
||||
|
||||
// Define button variants for better type safety and maintainability
|
||||
type ButtonVariant = 'primary' | 'secondary' | 'secondary-muted' | 'danger' | 'caution' | 'pure' | 'ghost'
|
||||
|
||||
type ButtonTheme = 'default'
|
||||
|
||||
// Define size options for better flexibility
|
||||
type ButtonSize = 'sm' | 'md' | 'lg'
|
||||
|
||||
interface ButtonProps {
|
||||
toggled?: boolean // Optional toggled state for toggle buttons
|
||||
icon?: string // Icon class name
|
||||
label?: string // Button text label
|
||||
disabled?: boolean // Disabled state
|
||||
loading?: boolean // Loading state
|
||||
variant?: ButtonVariant // Button style variant
|
||||
size?: ButtonSize // Button size variant
|
||||
shape?: 'rounded' | 'pill' | 'square' // Button shape
|
||||
theme?: ButtonTheme // Button theme
|
||||
block?: boolean // Full width button
|
||||
}
|
||||
import BasicButton from './basic-button.vue'
|
||||
|
||||
const props = withDefaults(defineProps<ButtonProps>(), {
|
||||
toggled: false,
|
||||
variant: 'primary',
|
||||
disabled: false,
|
||||
loading: false,
|
||||
color: 'neutral',
|
||||
outline: true,
|
||||
shape: 'rect',
|
||||
size: 'md',
|
||||
shape: 'pill',
|
||||
theme: 'default',
|
||||
block: false,
|
||||
variant: 'secondary',
|
||||
})
|
||||
|
||||
const isDisabled = computed(() => props.disabled || props.loading)
|
||||
const shapeClasses: Record<ButtonShape, string> = {
|
||||
rect: 'rounded-lg',
|
||||
rounded: 'rounded-full',
|
||||
circle: 'rounded-full p-0!',
|
||||
parallelogram: 'button-shape-parallelogram rounded-lg',
|
||||
}
|
||||
|
||||
// Extract variant styles for better organization
|
||||
const variantClasses: Record<ButtonVariant, Record<ButtonTheme, {
|
||||
default: string[]
|
||||
nonToggled?: string
|
||||
toggled?: string
|
||||
}>> = {
|
||||
'primary': {
|
||||
default: {
|
||||
default: [
|
||||
'rounded-lg',
|
||||
'backdrop-blur-md',
|
||||
'bg-primary-500/15 hover:bg-primary-500/20 active:bg-primary-500/30 dark:bg-primary-700/30 dark:hover:bg-primary-700/40 dark:active:bg-primary-700/30',
|
||||
'focus:ring-none',
|
||||
'border-2 border-solid border-primary-500/5 dark:border-primary-900/40',
|
||||
'text-primary-950 dark:text-primary-100',
|
||||
'focus:ring-2',
|
||||
const circleSizeClasses: Record<ButtonSize, string> = {
|
||||
sm: 'h-8 w-8',
|
||||
md: 'h-10 w-10',
|
||||
lg: 'h-12 w-12',
|
||||
}
|
||||
|
||||
const colorClasses: Record<ButtonColor, Record<ButtonVariant, string[]>> = {
|
||||
neutral: {
|
||||
primary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-neutral-50 dark:text-neutral-900',
|
||||
'hover:text-neutral-100 dark:hover:text-neutral-900',
|
||||
'active:text-neutral-100 dark:active:text-neutral-800',
|
||||
|
||||
'bg-neutral-800 dark:bg-neutral-200',
|
||||
'hover:bg-neutral-800 dark:hover:bg-neutral-300',
|
||||
'active:bg-neutral-800 dark:active:bg-neutral-300',
|
||||
'focus-visible:bg-neutral-800 dark:focus-visible:bg-neutral-300',
|
||||
|
||||
props.outline ? 'hover:outline-neutral-800 dark:hover:outline-neutral-300' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-neutral-800 dark:focus-visible:outline-neutral-300' : '',
|
||||
],
|
||||
secondary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-neutral-950 dark:text-white',
|
||||
'hover:text-neutral-900 dark:hover:text-white',
|
||||
'active:text-neutral-900 dark:active:text-neutral-100',
|
||||
|
||||
'bg-neutral-200/75 dark:bg-neutral-700',
|
||||
'hover:bg-neutral-200 dark:hover:bg-neutral-600/75',
|
||||
'active:bg-neutral-200 dark:active:bg-neutral-600/50',
|
||||
'focus-visible:bg-neutral-200/75 dark:focus-visible:bg-neutral-600/50',
|
||||
|
||||
props.outline ? 'hover:outline-neutral-300 dark:hover:outline-neutral-600/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-neutral-300 dark:focus-visible:outline-neutral-600/50' : '',
|
||||
],
|
||||
},
|
||||
},
|
||||
'secondary': {
|
||||
default: {
|
||||
default: [
|
||||
'rounded-lg',
|
||||
'backdrop-blur-md',
|
||||
'bg-neutral-100/55 hover:bg-neutral-400/20 active:bg-neutral-400/30 dark:bg-neutral-700/60 dark:hover:bg-neutral-700/80 dark:active:bg-neutral-700/60',
|
||||
'focus:ring-none',
|
||||
'border-2 border-solid border-neutral-300/30 dark:border-neutral-700/30',
|
||||
'text-neutral-950 dark:text-neutral-100',
|
||||
'focus:ring-2',
|
||||
primary: {
|
||||
primary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-white dark:text-white',
|
||||
'hover:text-primary-100 dark:hover:text-white',
|
||||
'active:text-primary-100 dark:active:text-primary-100',
|
||||
|
||||
'bg-primary-500/85 dark:bg-primary-400/75',
|
||||
'hover:bg-primary-500/85 dark:hover:bg-primary-400/75',
|
||||
'active:bg-primary-500 dark:active:bg-primary-400/50',
|
||||
'focus-visible:bg-primary-500/85 dark:focus-visible:bg-primary-400/50',
|
||||
|
||||
props.outline ? 'hover:outline-primary-300/75 dark:hover:outline-primary-400/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-primary-300/75 dark:focus-visible:outline-primary-400/50' : '',
|
||||
],
|
||||
secondary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-primary-900 dark:text-primary-200',
|
||||
'hover:text-primary-900 dark:hover:text-primary-200',
|
||||
'active:text-primary-900 dark:active:text-primary-200',
|
||||
|
||||
'bg-primary-200/75 dark:bg-primary-800/75',
|
||||
'hover:bg-primary-200/75 dark:hover:bg-primary-800/75',
|
||||
'active:bg-primary-200 dark:active:bg-primary-800/50',
|
||||
'focus-visible:bg-primary-200/75 dark:focus-visible:bg-primary-800/50',
|
||||
|
||||
props.outline ? 'hover:outline-primary-300/75 dark:hover:outline-primary-800/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-primary-300/75 dark:focus-visible:outline-primary-800/50' : '',
|
||||
],
|
||||
},
|
||||
},
|
||||
'secondary-muted': {
|
||||
default: {
|
||||
default: [
|
||||
'rounded-lg',
|
||||
'backdrop-blur-md',
|
||||
'hover:bg-neutral-50/50 active:bg-neutral-50/90 hover:dark:bg-neutral-800/50 active:dark:bg-neutral-800/90',
|
||||
'border-2 border-solid border-neutral-100/60 dark:border-neutral-800/30',
|
||||
'focus:ring-none',
|
||||
cyan: {
|
||||
primary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-white dark:text-white',
|
||||
'hover:text-cyan-100 dark:hover:text-white',
|
||||
'active:text-cyan-100 dark:active:text-cyan-100',
|
||||
|
||||
'bg-cyan-500/85 dark:bg-cyan-400/75',
|
||||
'hover:bg-cyan-500/85 dark:hover:bg-cyan-400/75',
|
||||
'active:bg-cyan-500 dark:active:bg-cyan-400/50',
|
||||
'focus-visible:bg-cyan-500/85 dark:focus-visible:bg-cyan-400/50',
|
||||
|
||||
props.outline ? 'hover:outline-cyan-300/75 dark:hover:outline-cyan-400/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-cyan-300/75 dark:focus-visible:outline-cyan-400/50' : '',
|
||||
],
|
||||
nonToggled: 'bg-neutral-50/70 dark:bg-neutral-800/70 text-neutral-500 dark:text-neutral-400',
|
||||
toggled: 'bg-white/90 dark:bg-neutral-500/70 ring-neutral-300/30 dark:ring-neutral-600/60 ring-2 dark:ring-neutral-600/30 text-primary-500 dark:text-primary-100',
|
||||
},
|
||||
},
|
||||
'danger': {
|
||||
default: {
|
||||
default: [
|
||||
'rounded-lg',
|
||||
'backdrop-blur-md',
|
||||
'bg-red-500/15 hover:bg-red-500/20 active:bg-red-500/30 dark:bg-red-700/30 dark:hover:bg-red-700/40 dark:active:bg-red-700/30',
|
||||
'focus:ring-none',
|
||||
'border-2 border-solid border-red-200/30 dark:border-red-900/30',
|
||||
'text-red-950 dark:text-red-100',
|
||||
secondary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-cyan-900 dark:text-cyan-200',
|
||||
'hover:text-cyan-900 dark:hover:text-cyan-200',
|
||||
'active:text-cyan-900 dark:active:text-cyan-200',
|
||||
|
||||
'bg-cyan-200/75 dark:bg-cyan-900/75',
|
||||
'hover:bg-cyan-200/75 dark:hover:bg-cyan-900/75',
|
||||
'active:bg-cyan-200 dark:active:bg-cyan-900/50',
|
||||
'focus-visible:bg-cyan-200/75 dark:focus-visible:bg-cyan-900/50',
|
||||
|
||||
props.outline ? 'hover:outline-cyan-300/75 dark:hover:outline-cyan-900/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-cyan-300/75 dark:focus-visible:outline-cyan-900/50' : '',
|
||||
],
|
||||
},
|
||||
},
|
||||
'caution': {
|
||||
default: {
|
||||
default: [
|
||||
'rounded-lg',
|
||||
'backdrop-blur-md',
|
||||
'bg-amber-400/20 hover:bg-amber-400/25 active:bg-amber-400/35 dark:bg-amber-500/20 dark:hover:bg-amber-500/30 dark:active:bg-amber-500/20',
|
||||
'focus:ring-none',
|
||||
'border-2 border-solid border-amber-300/30 dark:border-amber-500/15',
|
||||
'text-amber-900 dark:text-amber-50',
|
||||
blue: {
|
||||
primary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-white dark:text-white',
|
||||
'hover:text-blue-100 dark:hover:text-white',
|
||||
'active:text-blue-100 dark:active:text-blue-100',
|
||||
|
||||
'bg-blue-500/85 dark:bg-blue-400/75',
|
||||
'hover:bg-blue-500/85 dark:hover:bg-blue-400/75',
|
||||
'active:bg-blue-500 dark:active:bg-blue-400/50',
|
||||
'focus-visible:bg-blue-500/85 dark:focus-visible:bg-blue-400/50',
|
||||
|
||||
props.outline ? 'hover:outline-blue-300/75 dark:hover:outline-blue-400/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-blue-300/75 dark:focus-visible:outline-blue-400/50' : '',
|
||||
],
|
||||
secondary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-blue-900 dark:text-blue-200',
|
||||
'hover:text-blue-900 dark:hover:text-blue-200',
|
||||
'active:text-blue-900 dark:active:text-blue-200',
|
||||
|
||||
'bg-blue-200/75 dark:bg-blue-900/75',
|
||||
'hover:bg-blue-200/75 dark:hover:bg-blue-900/75',
|
||||
'active:bg-blue-200 dark:active:bg-blue-900/50',
|
||||
'focus-visible:bg-blue-200/75 dark:focus-visible:bg-blue-900/50',
|
||||
|
||||
props.outline ? 'hover:outline-blue-300/75 dark:hover:outline-blue-900/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-blue-300/75 dark:focus-visible:outline-blue-900/50' : '',
|
||||
],
|
||||
},
|
||||
},
|
||||
'pure': {
|
||||
default: {
|
||||
default: [
|
||||
'rounded-lg',
|
||||
'bg-transparent',
|
||||
'text-neutral-900 dark:text-neutral-50',
|
||||
'!px-0 !py-0',
|
||||
green: {
|
||||
primary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-white dark:text-white',
|
||||
'hover:text-green-100 dark:hover:text-white',
|
||||
'active:text-green-100 dark:active:text-green-100',
|
||||
|
||||
'bg-green-500/85 dark:bg-green-400/75',
|
||||
'hover:bg-green-500/85 dark:hover:bg-green-400/75',
|
||||
'active:bg-green-500 dark:active:bg-green-400/50',
|
||||
'focus-visible:bg-green-500/85 dark:focus-visible:bg-green-400/50',
|
||||
|
||||
props.outline ? 'hover:outline-green-300/75 dark:hover:outline-green-400/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-green-300/75 dark:focus-visible:outline-green-400/50' : '',
|
||||
],
|
||||
secondary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-green-900 dark:text-green-200',
|
||||
'hover:text-green-900 dark:hover:text-green-200',
|
||||
'active:text-green-900 dark:active:text-green-200',
|
||||
|
||||
'bg-green-200/75 dark:bg-green-900/75',
|
||||
'hover:bg-green-200/75 dark:hover:bg-green-900/75',
|
||||
'active:bg-green-200 dark:active:bg-green-900/50',
|
||||
'focus-visible:bg-green-200/75 dark:focus-visible:bg-green-900/50',
|
||||
|
||||
props.outline ? 'hover:outline-green-300/75 dark:hover:outline-green-900/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-green-300/75 dark:focus-visible:outline-green-900/50' : '',
|
||||
],
|
||||
},
|
||||
},
|
||||
'ghost': {
|
||||
default: {
|
||||
default: [
|
||||
'rounded-lg',
|
||||
'bg-transparent',
|
||||
'hover:bg-neutral-100/50 dark:hover:bg-neutral-800/50',
|
||||
'text-neutral-500 dark:text-neutral-400',
|
||||
'focus:ring-none',
|
||||
lime: {
|
||||
primary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-white dark:text-white',
|
||||
'hover:text-lime-100 dark:hover:text-white',
|
||||
'active:text-lime-100 dark:active:text-lime-100',
|
||||
|
||||
'bg-lime-500/85 dark:bg-lime-400/75',
|
||||
'hover:bg-lime-500/85 dark:hover:bg-lime-400/75',
|
||||
'active:bg-lime-500 dark:active:bg-lime-400/50',
|
||||
'focus-visible:bg-lime-500/85 dark:focus-visible:bg-lime-400/50',
|
||||
|
||||
props.outline ? 'hover:outline-lime-300/75 dark:hover:outline-lime-400/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-lime-300/75 dark:focus-visible:outline-lime-400/50' : '',
|
||||
],
|
||||
secondary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-lime-900 dark:text-lime-200',
|
||||
'hover:text-lime-900 dark:hover:text-lime-200',
|
||||
'active:text-lime-900 dark:active:text-lime-200',
|
||||
|
||||
'bg-lime-200/75 dark:bg-lime-900/75',
|
||||
'hover:bg-lime-200/75 dark:hover:bg-lime-900/75',
|
||||
'active:bg-lime-200 dark:active:bg-lime-900/50',
|
||||
'focus-visible:bg-lime-200/75 dark:focus-visible:bg-lime-900/50',
|
||||
|
||||
props.outline ? 'hover:outline-lime-300/75 dark:hover:outline-lime-900/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-lime-300/75 dark:focus-visible:outline-lime-900/50' : '',
|
||||
],
|
||||
},
|
||||
amber: {
|
||||
primary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-white dark:text-white',
|
||||
'hover:text-amber-100 dark:hover:text-white',
|
||||
'active:text-amber-100 dark:active:text-amber-100',
|
||||
|
||||
'bg-amber-500/85 dark:bg-amber-400/75',
|
||||
'hover:bg-amber-500/85 dark:hover:bg-amber-400/75',
|
||||
'active:bg-amber-500 dark:active:bg-amber-400/50',
|
||||
'focus-visible:bg-amber-500/85 dark:focus-visible:bg-amber-400/50',
|
||||
|
||||
props.outline ? 'hover:outline-amber-300/75 dark:hover:outline-amber-400/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-amber-300/75 dark:focus-visible:outline-amber-400/50' : '',
|
||||
],
|
||||
secondary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-amber-900 dark:text-amber-200',
|
||||
'hover:text-amber-900 dark:hover:text-amber-200',
|
||||
'active:text-amber-900 dark:active:text-amber-200',
|
||||
|
||||
'bg-amber-200/75 dark:bg-amber-900/75',
|
||||
'hover:bg-amber-200/75 dark:hover:bg-amber-900/75',
|
||||
'active:bg-amber-200 dark:active:bg-amber-900/50',
|
||||
'focus-visible:bg-amber-200/75 dark:focus-visible:bg-amber-900/50',
|
||||
|
||||
props.outline ? 'hover:outline-amber-300/75 dark:hover:outline-amber-900/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-amber-300/75 dark:focus-visible:outline-amber-900/50' : '',
|
||||
],
|
||||
},
|
||||
red: {
|
||||
primary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-white dark:text-white',
|
||||
'hover:text-red-100 dark:hover:text-white',
|
||||
'active:text-red-100 dark:active:text-red-100',
|
||||
|
||||
'bg-red-500/85 dark:bg-red-400/75',
|
||||
'hover:bg-red-500/85 dark:hover:bg-red-400/75',
|
||||
'active:bg-red-500 dark:active:bg-red-400/50',
|
||||
'focus-visible:bg-red-500/85 dark:focus-visible:bg-red-400/50',
|
||||
|
||||
props.outline ? 'hover:outline-red-300/75 dark:hover:outline-red-400/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-red-300/75 dark:focus-visible:outline-red-400/50' : '',
|
||||
],
|
||||
secondary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-red-900 dark:text-red-200',
|
||||
'hover:text-red-900 dark:hover:text-red-200',
|
||||
'active:text-red-900 dark:active:text-red-200',
|
||||
|
||||
'bg-red-200/75 dark:bg-red-900/75',
|
||||
'hover:bg-red-200/75 dark:hover:bg-red-900/75',
|
||||
'active:bg-red-200 dark:active:bg-red-900/50',
|
||||
'focus-visible:bg-red-200/75 dark:focus-visible:bg-red-900/50',
|
||||
|
||||
props.outline ? 'hover:outline-red-300/75 dark:hover:outline-red-900/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-red-300/75 dark:focus-visible:outline-red-900/50' : '',
|
||||
],
|
||||
},
|
||||
orange: {
|
||||
primary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-white dark:text-white',
|
||||
'hover:text-orange-100 dark:hover:text-white',
|
||||
'active:text-orange-100 dark:active:text-orange-100',
|
||||
|
||||
'bg-orange-500/85 dark:bg-orange-400/75',
|
||||
'hover:bg-orange-500/85 dark:hover:bg-orange-400/75',
|
||||
'active:bg-orange-500 dark:active:bg-orange-400/50',
|
||||
'focus-visible:bg-orange-500/85 dark:focus-visible:bg-orange-400/50',
|
||||
|
||||
props.outline ? 'hover:outline-orange-300/75 dark:hover:outline-orange-400/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-orange-300/75 dark:focus-visible:outline-orange-400/50' : '',
|
||||
],
|
||||
secondary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-orange-900 dark:text-orange-200',
|
||||
'hover:text-orange-900 dark:hover:text-orange-200',
|
||||
'active:text-orange-900 dark:active:text-orange-200',
|
||||
|
||||
'bg-orange-200/75 dark:bg-orange-900/75',
|
||||
'hover:bg-orange-200/75 dark:hover:bg-orange-900/75',
|
||||
'active:bg-orange-200 dark:active:bg-orange-900/50',
|
||||
'focus-visible:bg-orange-200/75 dark:focus-visible:bg-orange-900/50',
|
||||
|
||||
props.outline ? 'hover:outline-orange-300/75 dark:hover:outline-orange-900/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-orange-300/75 dark:focus-visible:outline-orange-900/50' : '',
|
||||
],
|
||||
},
|
||||
purple: {
|
||||
primary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-white dark:text-white',
|
||||
'hover:text-purple-100 dark:hover:text-white',
|
||||
'active:text-purple-100 dark:active:text-purple-100',
|
||||
|
||||
'bg-purple-500/85 dark:bg-purple-400/75',
|
||||
'hover:bg-purple-500/85 dark:hover:bg-purple-400/75',
|
||||
'active:bg-purple-500 dark:active:bg-purple-400/50',
|
||||
'focus-visible:bg-purple-500/85 dark:focus-visible:bg-purple-400/50',
|
||||
|
||||
props.outline ? 'hover:outline-purple-300/75 dark:hover:outline-purple-400/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-purple-300/75 dark:focus-visible:outline-purple-400/50' : '',
|
||||
],
|
||||
secondary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-purple-900 dark:text-purple-200',
|
||||
'hover:text-purple-900 dark:hover:text-purple-200',
|
||||
'active:text-purple-900 dark:active:text-purple-200',
|
||||
|
||||
'bg-purple-200/75 dark:bg-purple-900/75',
|
||||
'hover:bg-purple-200/75 dark:hover:bg-purple-900/75',
|
||||
'active:bg-purple-200 dark:active:bg-purple-900/50',
|
||||
'focus-visible:bg-purple-200/75 dark:focus-visible:bg-purple-900/50',
|
||||
|
||||
props.outline ? 'hover:outline-purple-300/75 dark:hover:outline-purple-900/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-purple-300/75 dark:focus-visible:outline-purple-900/50' : '',
|
||||
],
|
||||
},
|
||||
pink: {
|
||||
primary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-white dark:text-white',
|
||||
'hover:text-pink-100 dark:hover:text-white',
|
||||
'active:text-pink-100 dark:active:text-pink-100',
|
||||
|
||||
'bg-pink-500/85 dark:bg-pink-400/75',
|
||||
'hover:bg-pink-500/85 dark:hover:bg-pink-400/75',
|
||||
'active:bg-pink-500 dark:active:bg-pink-400/50',
|
||||
'focus-visible:bg-pink-500/85 dark:focus-visible:bg-pink-400/50',
|
||||
|
||||
props.outline ? 'hover:outline-pink-300/75 dark:hover:outline-pink-400/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-pink-300/75 dark:focus-visible:outline-pink-400/50' : '',
|
||||
],
|
||||
secondary: [
|
||||
'backdrop-blur-lg',
|
||||
|
||||
'text-pink-900 dark:text-pink-200',
|
||||
'hover:text-pink-900 dark:hover:text-pink-200',
|
||||
'active:text-pink-900 dark:active:text-pink-200',
|
||||
|
||||
'bg-pink-200/75 dark:bg-pink-900/75',
|
||||
'hover:bg-pink-200/75 dark:hover:bg-pink-900/75',
|
||||
'active:bg-pink-200 dark:active:bg-pink-900/50',
|
||||
'focus-visible:bg-pink-200/75 dark:focus-visible:bg-pink-900/50',
|
||||
|
||||
props.outline ? 'hover:outline-pink-300/75 dark:hover:outline-pink-900/50' : '',
|
||||
props.outline ? 'active:outline-none dark:active:outline-none' : '',
|
||||
props.outline ? 'focus-visible:outline-pink-300/75 dark:focus-visible:outline-pink-900/50' : '',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
// Extract size styles for better organization
|
||||
const sizeClasses: Record<ButtonSize, string> = {
|
||||
sm: props.shape === 'pill'
|
||||
? 'px-3 py-1.5 text-xs'
|
||||
: props.shape === 'square'
|
||||
? 'p-2 text-xs'
|
||||
: 'px-4 py-2 text-sm',
|
||||
md: props.shape === 'pill'
|
||||
? 'px-4 py-2 text-sm'
|
||||
: props.shape === 'square'
|
||||
? 'p-3 text-sm'
|
||||
: 'px-5 py-3 text-base',
|
||||
lg: props.shape === 'pill'
|
||||
? 'px-6 py-3 text-base'
|
||||
: props.shape === 'square'
|
||||
? 'p-4 text-base'
|
||||
: 'px-6 py-3 text-base',
|
||||
}
|
||||
const classes = computed(() => [
|
||||
'border-none',
|
||||
props.outline && [
|
||||
'outline outline-2 outline-offset-0 outline-transparent',
|
||||
'hover:outline-offset-2 focus-visible:outline-offset-2',
|
||||
],
|
||||
shapeClasses[props.shape],
|
||||
props.shape === 'circle' && circleSizeClasses[props.size],
|
||||
colorClasses[props.color][props.variant],
|
||||
])
|
||||
|
||||
// Base classes that are always applied
|
||||
const baseClasses = computed(() => {
|
||||
const variant = variantClasses[props.variant] || variantClasses.primary
|
||||
const theme = variant[props.theme] || variant.default
|
||||
|
||||
return [
|
||||
'font-medium outline-none whitespace-nowrap',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'backdrop-blur-md',
|
||||
props.block ? 'w-full' : '',
|
||||
sizeClasses[props.size],
|
||||
theme.default,
|
||||
props.toggled ? theme.toggled || '' : theme.nonToggled || '',
|
||||
{ 'opacity-50 cursor-not-allowed': isDisabled.value },
|
||||
'focus:ring-2',
|
||||
]
|
||||
})
|
||||
const basicProps = computed(() => ({
|
||||
icon: props.icon,
|
||||
label: props.label,
|
||||
disabled: props.disabled,
|
||||
loading: props.loading,
|
||||
size: props.size,
|
||||
block: props.block,
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:disabled="isDisabled"
|
||||
:class="baseClasses"
|
||||
<BasicButton
|
||||
v-bind="basicProps"
|
||||
:class="classes"
|
||||
>
|
||||
<div class="flex flex-row items-center justify-center gap-2">
|
||||
<TransitionBidirectional
|
||||
from-class="opacity-0 mr-0! w-0!"
|
||||
active-class="transition-[width,margin] ease-in-out overflow-hidden transition-100"
|
||||
>
|
||||
<div v-if="loading || icon" class="w-4">
|
||||
<div v-if="loading" class="i-svg-spinners:ring-resize h-4 w-4" />
|
||||
<div v-else-if="icon" class="h-4 w-4" :class="icon" />
|
||||
</div>
|
||||
</TransitionBidirectional>
|
||||
<span v-if="label">{{ label }}</span>
|
||||
<slot v-else />
|
||||
</div>
|
||||
</button>
|
||||
<slot />
|
||||
</BasicButton>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Equal and opposite skews keep the button silhouette angled without distorting its content. */
|
||||
.button-shape-parallelogram {
|
||||
transform: skewX(-10deg);
|
||||
}
|
||||
|
||||
.button-shape-parallelogram :deep(.basic-button-content) {
|
||||
transform: skewX(10deg);
|
||||
}
|
||||
|
||||
/* Preserve the silhouette while composing BasicButton's pressed scale. */
|
||||
.button-shape-parallelogram:active {
|
||||
transform: skewX(-10deg) scale(0.95);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { errorCauseFrom, errorMessageFrom, errorNameFrom, errorStackFrom } from
|
||||
import { ScrollAreaCorner, ScrollAreaRoot, ScrollAreaScrollbar, ScrollAreaThumb, ScrollAreaViewport } from 'reka-ui'
|
||||
import { computed, shallowRef } from 'vue'
|
||||
|
||||
import Button from './button.vue'
|
||||
import IconButton from './icon-button.vue'
|
||||
|
||||
type HeightPreset = 'sm' | 'md' | 'lg' | 'xl' | 'auto'
|
||||
|
||||
@@ -161,22 +161,16 @@ async function copyContent() {
|
||||
]"
|
||||
>
|
||||
<div :class="['absolute right-2 -translate-x-full top-2 z-10']">
|
||||
<Button
|
||||
<IconButton
|
||||
v-if="showCopyButton"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
shape="square"
|
||||
icon="i-solar:copy-line-duotone"
|
||||
:title="copied ? copiedButtonLabel : copyButtonLabel"
|
||||
:aria-label="copied ? copiedButtonLabel : copyButtonLabel"
|
||||
@click="copyContent"
|
||||
/>
|
||||
|
||||
<Button
|
||||
<IconButton
|
||||
v-if="showFeedbackButton"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
shape="square"
|
||||
icon="i-solar:square-share-line-line-duotone"
|
||||
:title="feedbackButtonLabel"
|
||||
:aria-label="feedbackButtonLabel"
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { ButtonColor, ButtonSize, ButtonVariant } from './button-types'
|
||||
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import Button from './button.vue'
|
||||
|
||||
type ButtonVariant = 'primary' | 'secondary' | 'secondary-muted' | 'danger' | 'caution'
|
||||
type ButtonSize = 'sm' | 'md' | 'lg'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
color?: ButtonColor
|
||||
variant?: ButtonVariant
|
||||
cancelVariant?: ButtonVariant
|
||||
size?: ButtonSize
|
||||
block?: boolean
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
}>(), {
|
||||
variant: 'danger',
|
||||
cancelVariant: 'secondary',
|
||||
color: 'red',
|
||||
variant: 'primary',
|
||||
size: 'md',
|
||||
block: false,
|
||||
disabled: false,
|
||||
@@ -64,12 +63,16 @@ function handleCancel() {
|
||||
|
||||
<template>
|
||||
<div :class="wrapperClasses">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<Transition name="double-check-slide">
|
||||
<TransitionGroup
|
||||
name="double-check-slide"
|
||||
tag="div"
|
||||
:class="['relative flex flex-wrap items-center gap-2']"
|
||||
>
|
||||
<Button
|
||||
v-if="confirming"
|
||||
key="cancel"
|
||||
:variant="cancelVariant"
|
||||
color="neutral"
|
||||
variant="secondary"
|
||||
:size="size"
|
||||
:block="block"
|
||||
class="whitespace-nowrap"
|
||||
@@ -83,9 +86,10 @@ function handleCancel() {
|
||||
<span><slot name="cancel">Cancel</slot></span>
|
||||
</div>
|
||||
</Button>
|
||||
</Transition>
|
||||
|
||||
<Button
|
||||
key="primary"
|
||||
:color="color"
|
||||
:variant="variant"
|
||||
:size="size"
|
||||
:block="block"
|
||||
@@ -107,16 +111,25 @@ function handleCancel() {
|
||||
</slot>
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.double-check-slide-move {
|
||||
transition: transform 180ms ease;
|
||||
}
|
||||
|
||||
.double-check-slide-enter-active,
|
||||
.double-check-slide-leave-active {
|
||||
transition: opacity 160ms ease, transform 160ms ease;
|
||||
}
|
||||
|
||||
/* Removing the cancel button from flex flow lets TransitionGroup animate the primary button's layout shift. */
|
||||
.double-check-slide-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.double-check-slide-enter-from,
|
||||
.double-check-slide-leave-to {
|
||||
opacity: 0;
|
||||
@@ -124,7 +137,7 @@ function handleCancel() {
|
||||
}
|
||||
|
||||
.double-check-primary {
|
||||
transition: min-width 180ms ease, padding 180ms ease;
|
||||
transition: min-width 180ms ease, padding 180ms ease, transform 180ms ease;
|
||||
}
|
||||
|
||||
.double-check-primary--default {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import type { BasicButtonProps, ButtonSize } from './button-types'
|
||||
|
||||
import { computed } from 'vue'
|
||||
|
||||
import BasicButton from './basic-button.vue'
|
||||
|
||||
interface GhostButtonProps extends BasicButtonProps {
|
||||
/** Keeps the emphasized interaction surface visible for toggle controls. */
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<GhostButtonProps>(), {
|
||||
active: false,
|
||||
size: 'md',
|
||||
})
|
||||
|
||||
const sizeClasses: Record<ButtonSize, string> = {
|
||||
sm: 'min-h-7 px-2! py-1! text-xs',
|
||||
md: 'min-h-8 px-3! py-1.5! text-sm',
|
||||
lg: 'min-h-10 px-4! py-2! text-base',
|
||||
}
|
||||
|
||||
const basicProps = computed(() => ({
|
||||
icon: props.icon,
|
||||
label: props.label,
|
||||
disabled: props.disabled,
|
||||
loading: props.loading,
|
||||
size: props.size,
|
||||
block: props.block,
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicButton
|
||||
v-bind="basicProps"
|
||||
:class="[
|
||||
'rounded-lg border-none bg-transparent outline-none',
|
||||
'text-neutral-700 dark:text-neutral-200',
|
||||
'hover:bg-primary-500/10 hover:text-primary-700 dark:hover:bg-primary-400/10 dark:hover:text-primary-300',
|
||||
'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-300',
|
||||
'active:bg-primary-500/20 active:text-primary-800 dark:active:text-primary-200',
|
||||
active && 'bg-primary-500/15 text-primary-800 dark:bg-primary-400/15 dark:text-primary-200',
|
||||
sizeClasses[size],
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
</BasicButton>
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { BasicButtonProps } from './button-types'
|
||||
|
||||
import BasicButton from './basic-button.vue'
|
||||
|
||||
type IconButtonProps = Pick<BasicButtonProps, 'icon' | 'disabled' | 'loading'>
|
||||
|
||||
const props = defineProps<IconButtonProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicButton
|
||||
v-bind="props"
|
||||
:class="['shrink-0 p-0!']"
|
||||
>
|
||||
<slot />
|
||||
</BasicButton>
|
||||
</template>
|
||||
@@ -1,7 +1,12 @@
|
||||
export { default as Avatar } from './avatar.vue'
|
||||
export { default as BasicButton } from './basic-button.vue'
|
||||
export type { BasicButtonProps, ButtonColor, ButtonProps, ButtonShape, ButtonSize, ButtonVariant } from './button-types'
|
||||
export { default as Button } from './button.vue'
|
||||
export { default as Callout } from './callout.vue'
|
||||
export { default as ContainerError } from './container-error.vue'
|
||||
export { default as DoubleCheckButton } from './double-check-button.vue'
|
||||
export { default as ErrorBoundary } from './error-boundary.vue'
|
||||
export { default as GhostButton } from './ghost-button.vue'
|
||||
export { default as IconButton } from './icon-button.vue'
|
||||
export { default as OverlayButton } from './overlay-button.vue'
|
||||
export { default as Progress } from './progress.vue'
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import type { BasicButtonProps } from './button-types'
|
||||
|
||||
import BasicButton from './basic-button.vue'
|
||||
|
||||
const props = withDefaults(defineProps<BasicButtonProps>(), {
|
||||
size: 'md',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicButton
|
||||
v-bind="props"
|
||||
:class="[
|
||||
'w-fit self-end rounded-xl border-none backdrop-blur-md',
|
||||
'bg-neutral-50/80 text-neutral-900 dark:bg-neutral-800/70 dark:text-neutral-50',
|
||||
'hover:text-primary-700 dark:hover:text-primary-200',
|
||||
'active:bg-primary-200/80 active:text-primary-900',
|
||||
'dark:active:bg-primary-200/80 dark:active:text-primary-900',
|
||||
]"
|
||||
>
|
||||
<slot />
|
||||
</BasicButton>
|
||||
</template>
|
||||
Generated
+3
@@ -4545,6 +4545,9 @@ importers:
|
||||
'@iconify-json/mingcute':
|
||||
specifier: 'catalog:'
|
||||
version: 1.2.7
|
||||
'@iconify-json/ph':
|
||||
specifier: 'catalog:'
|
||||
version: 1.2.2
|
||||
'@iconify-json/simple-icons':
|
||||
specifier: 'catalog:'
|
||||
version: 1.2.78
|
||||
|
||||
Reference in New Issue
Block a user