Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 151 additions & 3 deletions tools/ui/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,107 @@ import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint';

const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
// Require a blank line between sibling element-like nodes in a Svelte template
// (elements, components, and the {#if} / {#each} / {#await} / {#snippet} /
// {@render} blocks) that sit on separate lines at the same nesting level.
// Whitespace between siblings is a whitespace-only SvelteText node; when it
// holds a single newline (no blank line) the fix adds one, keeping the
// indentation of the second sibling. Real text content (e.g. `foo\n\nbar`)
// is left alone.
const ELEMENT_LIKE_TYPES = new Set([
'SvelteAwaitBlock',
'SvelteComponent',
'SvelteEachBlock',
'SvelteElement',
'SvelteIfBlock',
'SvelteKeyBlock',
'SvelteRenderTag',
'SvelteSelf',
'SvelteSnippetBlock'
]);
const paddingLineBetweenElements = {
create(context) {
// Check one list of template children. Each children array holds the
// element-like nodes plus the whitespace/comment text between them.
function checkChildren(children) {
if (!Array.isArray(children)) return;

let lastElement = null;
let lastWhitespace = null;

for (const child of children) {
if (child.type === 'SvelteText' && /^\s*$/.test(child.value)) {
lastWhitespace = child;

continue;
}

if (!ELEMENT_LIKE_TYPES.has(child.type)) continue;

if (
lastElement &&
lastWhitespace &&
child.loc.start.line - lastElement.loc.end.line === 1
) {
const textNode = lastWhitespace;

context.report({
fix(fixer) {
// Add a second newline so the two siblings are separated by a
// blank line, keeping the trailing indentation.
return fixer.replaceText(textNode, textNode.value.replace(/\n/, '\n\n'));
},
message: 'Expected a blank line between sibling elements.',
node: child
});
}

lastElement = child;
lastWhitespace = null;
}
}

return {
SvelteAwaitBlock(node) {
checkChildren(node.children);
checkChildren(node.then?.children);
checkChildren(node.else?.children);
},
SvelteComponent(node) {
checkChildren(node.children);
},
SvelteEachBlock(node) {
checkChildren(node.children);
checkChildren(node.else?.children);
},
SvelteElement(node) {
checkChildren(node.children);
},
SvelteFragment(node) {
checkChildren(node.children);
},
SvelteIfBlock(node) {
checkChildren(node.children);
checkChildren(node.else?.children);
},
SvelteKeyBlock(node) {
checkChildren(node.children);
},
SvelteProgram(node) {
checkChildren(node.children);
},
SvelteSnippetBlock(node) {
checkChildren(node.children);
}
};
},
meta: {
docs: { description: 'Require a blank line between sibling elements in a Svelte template.' },
fixable: 'whitespace',
schema: [],
type: 'layout'
}
};
// Require a blank line between consecutive class accessors (get/set). The core
// `padding-line-between-statements` rule only handles statements, not class
// members, so this is enforced with a small custom rule.
Expand Down Expand Up @@ -66,7 +167,12 @@ export default ts.config(
{
languageOptions: { globals: { ...globals.browser, ...globals.node } },
plugins: {
local: { rules: { 'blank-line-between-accessors': blankLineBetweenAccessors } },
local: {
rules: {
'blank-line-between-accessors': blankLineBetweenAccessors,
'padding-line-between-elements': paddingLineBetweenElements
}
},
perfectionist,
'simple-import-sort': simpleImportSort
},
Expand All @@ -82,6 +188,8 @@ export default ts.config(
'eol-last': 'error',
// Enforce a blank line between consecutive get/set accessors
'local/blank-line-between-accessors': 'error',
// Require a blank line between sibling elements in a Svelte template
'local/padding-line-between-elements': 'error',
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
'no-undef': 'off',
Expand Down Expand Up @@ -156,9 +264,49 @@ export default ts.config(
// grouping); Prettier normalizes comma spacing afterwards.
'simple-import-sort/imports': ['error', { groups: [['.*']] }],
'svelte/no-at-html-tags': 'off',

// This app uses hash-based routing (#/) where resolve() from $app/paths does not apply
'svelte/no-navigation-without-resolve': 'off'
'svelte/no-navigation-without-resolve': 'off',

// Sort HTML attributes alphabetically in the markup. The Svelte directives
// (bind:/use:/animate:/style:/in:/out:/transition:/class:) sort first,
// alphabetically among themselves, then all remaining attributes sort
// alphabetically. The rule keeps spread attributes in place and does not cross
// them. `this` stays first on <svelte:element> because Prettier forces it there
// - reordering it alphabetically would fight the formatter.
'svelte/sort-attributes': [
'error',
{
order: [
'this',
{
match: [
'/^bind:/u',
'/^use:/u',
'/^animate:/u',
'/^style:/u',
'/^in:/u',
'/^out:/u',
'/^transition:/u',
'/^class:/u'
],
sort: 'alphabetical'
},
{
match: [
'!/^bind:/u',
'!/^use:/u',
'!/^animate:/u',
'!/^style:/u',
'!/^in:/u',
'!/^out:/u',
'!/^transition:/u',
'!/^class:/u'
],
sort: 'alphabetical'
}
]
}
]
}
},
{
Expand Down
10 changes: 5 additions & 5 deletions tools/ui/src/lib/components/app/actions/ActionIcon.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,17 @@
{#snippet button(props = {})}
<Button
{...props}
{href}
{variant}
{size}
aria-label={ariaLabel || tooltip}
class="h-6 w-6 p-0 {className} flex hover:bg-transparent data-[state=open]:bg-transparent!"
{disabled}
{href}
onclick={(e: MouseEvent) => {
if (stopPropagationOnClick) e.stopPropagation();

onclick?.(e);
}}
class="h-6 w-6 p-0 {className} flex hover:bg-transparent data-[state=open]:bg-transparent!"
aria-label={ariaLabel || tooltip}
{size}
{variant}
>
{#if icon}
{@const IconComponent = icon}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
</script>

<ActionIcon
disabled={!canCopy}
icon={Copy}
tooltip={ariaLabel}
iconSize={ICON_CLASS_DEFAULT}
disabled={!canCopy}
onclick={() => canCopy && copyToClipboard(text)}
tooltip={ariaLabel}
/>
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,13 @@
{/if}

<DialogChatAttachmentsPreview
bind:open={viewAllDialogOpen}
{activeModelId}
{attachments}
bind:open={viewAllDialogOpen}
{previewFocusIndex}
{uploadedFiles}
/>

{#if mcpResourcePreviewExtra}
<DialogMcpResourcePreview extra={mcpResourcePreviewExtra} bind:open={mcpResourcePreviewOpen} />
<DialogMcpResourcePreview bind:open={mcpResourcePreviewOpen} extra={mcpResourcePreviewExtra} />
{/if}
Original file line number Diff line number Diff line change
Expand Up @@ -75,58 +75,58 @@
{#if mcpPrompt}
<ChatAttachmentsListItemMcpPrompt
class="max-w-[300px] min-w-[200px] flex-shrink-0 {className} {scrollClasses}"
prompt={mcpPrompt}
{readonly}
isLoading={item.isLoading}
loadError={item.loadError}
onRemove={onFileRemove ? () => onFileRemove(item.id) : undefined}
prompt={mcpPrompt}
{readonly}
/>
{/if}
{:else if isMcpResource(item)}
{@const mcpResource = item.attachment as DatabaseMessageExtraMcpResource}

<ChatAttachmentsListItemMcpResource
class="flex-shrink-0 {className} {scrollClasses}"
attachment={toMcpResourceAttachment(mcpResource, item.id)}
class="flex-shrink-0 {className} {scrollClasses}"
onclick={() => onMcpResourcePreview?.(mcpResource)}
/>
{:else if item.isImage && item.preview}
<ChatAttachmentsListItemThumbnailImage
class="flex-shrink-0 cursor-pointer {className} {scrollClasses}"
height={imageHeight}
id={item.id}
{imageClass}
name={item.name}
onRemove={onFileRemove}
onclick={() => onPreview?.(item)}
preview={item.preview}
{readonly}
onRemove={onFileRemove}
height={imageHeight}
width={imageWidth}
{imageClass}
onclick={() => onPreview?.(item)}
/>
{:else if isPdfFile(item.attachment, item.uploadedFile)}
<ChatAttachmentsListItemThumbnailFile
attachment={item.attachment}
class="flex-shrink-0 cursor-pointer {className} {scrollClasses}"
id={item.id}
name={item.name}
size={item.size}
{readonly}
onRemove={onFileRemove}
onclick={() => onPreview?.(item)}
{readonly}
size={item.size}
textContent={item.textContent}
attachment={item.attachment}
uploadedFile={item.uploadedFile}
onclick={() => onPreview?.(item)}
/>
{:else}
<ChatAttachmentsListItemThumbnailFile
attachment={item.attachment}
class="flex-shrink-0 cursor-pointer {className} {scrollClasses}"
id={item.id}
name={item.name}
size={item.size}
{readonly}
onRemove={onFileRemove}
onclick={() => onPreview?.(item)}
{readonly}
size={item.size}
textContent={item.textContent}
attachment={item.attachment}
uploadedFile={item.uploadedFile}
onclick={() => onPreview?.(item)}
/>
{/if}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<div
class="absolute top-10 right-2 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100"
>
<ActionIcon icon={X} tooltip="Remove" stopPropagationOnClick onclick={() => onRemove?.()} />
<ActionIcon icon={X} onclick={() => onRemove?.()} stopPropagationOnClick tooltip="Remove" />
</div>
{/if}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
<div
class="absolute top-2 right-2 opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100"
>
<ActionIcon icon={X} tooltip="Remove" stopPropagationOnClick onclick={() => onRemove?.(id)} />
<ActionIcon icon={X} onclick={() => onRemove?.(id)} stopPropagationOnClick tooltip="Remove" />
</div>
{/snippet}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
</script>

{#snippet image()}
<img src={preview} alt={name} class="{height} {width} cursor-pointer object-cover {imageClass}" />
<img alt={name} class="{height} {width} cursor-pointer object-cover {imageClass}" src={preview} />
{/snippet}

<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,30 +185,30 @@

<div class="{className} flex flex-col text-white">
<div class="relative flex min-h-0 flex-1 items-center justify-center overflow-hidden">
<ChatAttachmentsPreviewNavButtons onPrev={prev} onNext={next} show={allItems.length > 1} />
<ChatAttachmentsPreviewNavButtons onNext={next} onPrev={prev} show={allItems.length > 1} />

<div class="flex h-full w-full flex-col items-center justify-start overflow-auto py-4">
{#if currentItem}
<ChatAttachmentsPreviewFileInfo {displayName} {fileSize} />

<ChatAttachmentsPreviewCurrentItem
{activeModelId}
{audioSrc}
{currentItem}
{isImage}
{displayPreview}
{displayTextContent}
{hasVisionModality}
{isAudio}
{isVideo}
{isImage}
{isPdf}
{isText}
{displayPreview}
{displayTextContent}
{audioSrc}
{videoSrc}
{isVideo}
{language}
{hasVisionModality}
{activeModelId}
{videoSrc}
/>
{/if}

<ChatAttachmentsPreviewThumbnailStrip items={allItems} {currentIndex} {onNavigate} />
<ChatAttachmentsPreviewThumbnailStrip {currentIndex} items={allItems} {onNavigate} />
</div>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,18 @@
{#key currentItem.id}
{#if isPdf}
<ChatAttachmentsPreviewCurrentItemPdf
{activeModelId}
{currentItem}
displayName={currentItem.name}
{displayTextContent}
{hasVisionModality}
{activeModelId}
/>
{:else if isImage}
<ChatAttachmentsPreviewCurrentItemImage {currentItem} {displayPreview} />
{:else if isText && displayTextContent}
<ChatAttachmentsPreviewCurrentItemText {displayTextContent} {language} />
{:else if isAudio}
<ChatAttachmentsPreviewCurrentItemAudio {currentItem} {audioSrc} />
<ChatAttachmentsPreviewCurrentItemAudio {audioSrc} {currentItem} />
{:else if isVideo}
<ChatAttachmentsPreviewCurrentItemVideo {currentItem} {videoSrc} />
{:else if isUnavailable}
Expand Down
Loading
Loading