The recent refactor of CloseButton (from on:close component events to onClick prop) broke tag removal in MultiSelect/TagsToListenTo. The refactor changed on:pointerdown (component event) to onPointerdown (native DOM event), which stopped native pointerdown propagation and broke the drag tracking in DraggableTags, causing the dropdown to open on every close button click. Reverts CloseButton and all callers back to using createEventDispatcher and on:close. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
305 lines
9.2 KiB
Svelte
305 lines
9.2 KiB
Svelte
<script lang="ts">
|
|
import { getContext } from 'svelte'
|
|
import type { AppViewerContext, ComponentCssProperty } from '../../types'
|
|
import { ccomponents, type AppComponent } from '../component'
|
|
|
|
import Badge from '$lib/components/common/badge/Badge.svelte'
|
|
import Button from '$lib/components/common/button/Button.svelte'
|
|
|
|
import { twMerge } from 'tailwind-merge'
|
|
import { fade } from 'svelte/transition'
|
|
import css from 'svelte-highlight/languages/css'
|
|
import { Highlight } from 'svelte-highlight'
|
|
import { MoveRight } from 'lucide-svelte'
|
|
import { customisationByComponent } from '../componentsPanel/cssUtils'
|
|
import { sendUserToast } from '$lib/toast'
|
|
import CloseButton from '$lib/components/common/CloseButton.svelte'
|
|
import HighlightTheme from '$lib/components/HighlightTheme.svelte'
|
|
import Portal from '$lib/components/Portal.svelte'
|
|
interface Props {
|
|
component: AppComponent | undefined
|
|
}
|
|
|
|
let { component = $bindable() }: Props = $props()
|
|
|
|
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
|
|
|
function fadeFast(node: HTMLElement) {
|
|
return fade(node, { duration: 100 })
|
|
}
|
|
let migrationModalOpen: boolean = $state(false)
|
|
|
|
export function open() {
|
|
migrationModalOpen = true
|
|
}
|
|
|
|
function generateCodeFromMigrations(migrations: Map<string, string[]>) {
|
|
let code = ''
|
|
for (const [key, value] of migrations) {
|
|
code += `${key} {\n\t${value.join(';\n\t')}\n}\n\n`
|
|
}
|
|
|
|
return code
|
|
}
|
|
|
|
function getSelector(key: string) {
|
|
return customisationByComponent
|
|
.find((c) => c.components.includes(component?.type ?? ''))
|
|
?.selectors.find((s) => {
|
|
return s.customCssKey === key
|
|
})?.selector
|
|
}
|
|
|
|
function setOrUpdateMigration(key: string, value: string) {
|
|
const selector = getSelector(key)
|
|
if (!selector) {
|
|
return
|
|
}
|
|
|
|
if (migrations.has(selector)) {
|
|
const arr = migrations.get(selector)
|
|
|
|
if (arr) {
|
|
arr.push(value)
|
|
|
|
migrations.set(selector, arr)
|
|
}
|
|
} else {
|
|
migrations.set(selector, [value])
|
|
}
|
|
|
|
migrations = migrations
|
|
}
|
|
|
|
function appendMigrationsToCss(migrations: Map<string, string[]>) {
|
|
const theme = $app.theme
|
|
|
|
if (theme?.type === 'path') {
|
|
sendUserToast(
|
|
'Cannot migrate to CSS editor when using a theme by path. Fork a global theme to edit CSS.',
|
|
true
|
|
)
|
|
return
|
|
} else if (theme?.type === 'inlined') {
|
|
let cssString = theme.css
|
|
|
|
if (!cssString) {
|
|
cssString = ''
|
|
}
|
|
|
|
for (const [key, value] of migrations) {
|
|
if (cssString.includes(`${key} {` || `${key}{`)) {
|
|
// append value to existing value
|
|
const regex = new RegExp(`\\${key}\\s*{\\s*([\\s\\S]*?)\\s*}`, 'g')
|
|
|
|
const match = regex.exec(cssString)
|
|
|
|
if (match) {
|
|
const existingValue = match[1]
|
|
|
|
cssString = cssString.replace(
|
|
regex,
|
|
`${key} {\n\t${existingValue}\n\t${value.join('\n\t')}\n}`
|
|
)
|
|
}
|
|
} else {
|
|
const firstBreakline = cssString === '' ? '' : '\n\n'
|
|
|
|
// append key and value
|
|
cssString += `${firstBreakline}${key} {\n\t${value.join('\n\t')}\n}\n\n`
|
|
}
|
|
}
|
|
|
|
theme.css = cssString
|
|
|
|
$app.theme = theme
|
|
}
|
|
}
|
|
|
|
function hasStyles(customCss: Record<string, ComponentCssProperty> | undefined) {
|
|
if (!customCss) {
|
|
return false
|
|
}
|
|
|
|
return Object.keys(customCss ?? {})
|
|
.map((key) => customCss[key])
|
|
.some((c) => c.style !== '')
|
|
}
|
|
|
|
let type: string | undefined = component?.type
|
|
let migrations = $derived(new Map<string, string[]>())
|
|
let generatedCode = $derived(generateCodeFromMigrations(migrations))
|
|
</script>
|
|
|
|
<HighlightTheme />
|
|
|
|
{#if migrationModalOpen}
|
|
<Portal name="css-global-migration-modal" class="z-[5000] absolute">
|
|
<div transition:fadeFast|local class={' top-0 bottom-0 left-0 right-0 z-[5000]'} role="dialog">
|
|
<div
|
|
class={twMerge(
|
|
'fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity',
|
|
migrationModalOpen
|
|
? 'ease-out duration-300 opacity-100'
|
|
: 'ease-in duration-200 opacity-0'
|
|
)}
|
|
></div>
|
|
|
|
<div class="fixed inset-0 z-10 overflow-y-auto">
|
|
<div class="flex min-h-full items-center justify-center p-4">
|
|
<div
|
|
class={twMerge(
|
|
'relative transform overflow-hidden rounded-lg bg-surface px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-5xl sm:p-6 ',
|
|
migrationModalOpen
|
|
? 'ease-out duration-300 opacity-100 translate-y-0 sm:scale-100'
|
|
: 'ease-in duration-200 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
|
|
)}
|
|
>
|
|
<div class="leading-6 font-semibold text-sm w-full flex justify-between">
|
|
<div>Migrate to CSS editor</div><CloseButton
|
|
on:close={() => (migrationModalOpen = false)}
|
|
/>
|
|
</div>
|
|
|
|
<div class="">
|
|
<div class="">
|
|
{#if hasStyles(component?.customCss)}
|
|
<div class="leading-6 text-xs font-semibold">
|
|
ID <Badge color="indigo" size="xs">
|
|
{component?.id}
|
|
</Badge>
|
|
</div>
|
|
{/if}
|
|
{#if component?.type && $app.css}
|
|
{#each Object.keys(component.customCss ?? {}) as cssKey}
|
|
{#if component.customCss?.[cssKey].style != undefined && component.customCss[cssKey].style !== ''}
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<div>
|
|
<div class="flex flex-row justify-between items-center py-0.5">
|
|
<div class="leading-6 text-xs font-semibold">
|
|
<Badge>
|
|
{cssKey}
|
|
</Badge>
|
|
</div>
|
|
<Button
|
|
variant="accent"
|
|
size="xs"
|
|
on:click={() => {
|
|
if (component?.customCss?.[cssKey]?.style != undefined) {
|
|
setOrUpdateMigration(
|
|
cssKey,
|
|
component.customCss[cssKey].style ?? ''
|
|
)
|
|
component.customCss[cssKey].style = ''
|
|
}
|
|
}}
|
|
endIcon={{ icon: MoveRight }}
|
|
>
|
|
Migrate
|
|
</Button>
|
|
</div>
|
|
<div class="border p-2 rounded-md">
|
|
<Highlight code={component.customCss[cssKey].style} language={css} />
|
|
</div>
|
|
</div>
|
|
<div class="">
|
|
<div class="leading-6 text-xs font-semibold my-1">Preview</div>
|
|
<div class="border rounded-md p-2">
|
|
<Highlight
|
|
code={`${getSelector(cssKey)} {\n\t${
|
|
component.customCss[cssKey].style
|
|
}\n}`}
|
|
language={css}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{/each}
|
|
{/if}
|
|
|
|
{#if hasStyles(component?.type ? $app.css?.[component?.type] : undefined)}
|
|
<div class="leading-6 text-xs font-semibold">
|
|
Global: {component?.type ? ccomponents[component.type]?.name : ''}
|
|
</div>
|
|
{/if}
|
|
|
|
{#if component?.type && $app.css}
|
|
{#each Object.keys($app.css[component?.type] ?? {}) as cssKey}
|
|
{#if type && $app.css?.[type]?.[cssKey].style != undefined && $app.css[type]?.[cssKey].style !== ''}
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<div>
|
|
<div class="flex flex-row justify-between items-center py-0.5">
|
|
<div class="leading-6 text-xs font-semibold">
|
|
<Badge>
|
|
{cssKey}
|
|
</Badge>
|
|
</div>
|
|
<Button
|
|
variant="accent"
|
|
size="xs"
|
|
on:click={() => {
|
|
if (type && $app.css?.[type]) {
|
|
setOrUpdateMigration(cssKey, $app.css[type][cssKey].style)
|
|
$app.css[type][cssKey].style = ''
|
|
}
|
|
}}
|
|
endIcon={{ icon: MoveRight }}
|
|
>
|
|
Migrate
|
|
</Button>
|
|
</div>
|
|
<div class="border p-2 rounded-md">
|
|
<Highlight code={$app.css[type][cssKey].style} language={css} />
|
|
</div>
|
|
</div>
|
|
<div class="">
|
|
<div class="leading-6 text-xs font-semibold my-1">Preview</div>
|
|
<div class="border rounded-md p-2">
|
|
<Highlight
|
|
code={`${getSelector(cssKey)} {\n\t${$app.css[type][cssKey].style}\n}`}
|
|
language={css}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="leading-6 text-xs font-semibold my-1">Current migrations</div>
|
|
{#if migrations.size > 0}
|
|
<div class="border rounded-md p-2">
|
|
<Highlight code={generatedCode} language={css} />
|
|
</div>
|
|
{:else}
|
|
<div class="text-gray-500 text-xs">No migrations</div>
|
|
{/if}
|
|
</div>
|
|
<div class="mt-2 flex flex-row justify-end items-center gap-2">
|
|
<div class="text-xs">
|
|
If the class is already present in the CSS editor, the migration will append the new
|
|
values to the existing ones.
|
|
</div>
|
|
<Button
|
|
size="xs"
|
|
variant="accent"
|
|
on:click={() => {
|
|
appendMigrationsToCss(migrations)
|
|
migrationModalOpen = false
|
|
}}
|
|
disabled={migrations.size === 0}
|
|
endIcon={{ icon: MoveRight }}
|
|
>
|
|
Apply migration
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Portal>
|
|
{/if}
|