Skip to content
Closed
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"dialog-closedby-polyfill": "^1.1.0",
"fractional-indexing": "^3.2.0",
"hdr-color-input": "^0.2.5",
"interestfor": "^1.0.7",
"invokers-polyfill": "^0.5.7",
"json-stringify-pretty-compact": "^4.0.0",
"keyux": "^0.11.3",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

239 changes: 239 additions & 0 deletions src/alias-token.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
<script lang="ts">
import { Link2, X } from "@lucide/svelte";
import {
treeState,
resolveTokenValue,
isAliasCircular,
} from "./state.svelte";
import type { Value } from "./schema";

let {
nodeId,
type,
reference,
onAlias,
}: {
/** makes sure alias is not circular */
nodeId: string;
/** shows tokens only for specified type */
type: Value["type"];
reference: undefined | string;
onAlias: (newReference: undefined | string) => void;
} = $props();
const key = $props.id();

let aliasSearchInput = $state("");
let selectedAliasIndex = $state(0);
let popoverElement: undefined | HTMLDivElement;
let aliasSearchInputElement: undefined | HTMLInputElement;

const getTokenPath = (nodeId: string): string[] => {
const path: string[] = [];
let currentId: string | undefined = nodeId;
const nodes = treeState.nodes();
while (currentId !== undefined) {
const currentNode = nodes.get(currentId);
if (!currentNode) break;
path.unshift(currentNode.meta.name);
currentId = currentNode.parentId;
}
return path;
};

const availableTokens = $derived.by(() => {
const nodes = treeState.nodes();
const compatibleTokens = Array.from(nodes.values())
.filter((item) => {
if (item.nodeId !== nodeId && item.meta.nodeType === "token") {
const otherTokenType = resolveTokenValue(item, nodes).type;
// Filter by type compatibility and check for circular dependencies
return (
otherTokenType === type &&
!isAliasCircular(nodeId, item.nodeId, nodes)
);
}
return false;
})
.map((node) => ({
nodeId: node.nodeId,
path: getTokenPath(node.nodeId),
name: node.meta.name,
}))
.sort((a, b) => a.path.join(".").localeCompare(b.path.join(".")));
return compatibleTokens;
});

const filteredAliasTokens = $derived.by(() => {
if (!aliasSearchInput.trim()) {
return availableTokens;
}
const query = aliasSearchInput.toLowerCase();
return availableTokens.filter((token) =>
token.path.some((part) => part.toLowerCase().includes(query)),
);
});

const makeAlias = (targetNodeId: string) => {
const targetNode = treeState.getNode(targetNodeId);
if (targetNode?.meta.nodeType === "token") {
const newReference = `{${getTokenPath(targetNodeId).join(".")}}`;
onAlias(newReference);
}
};

const handleAliasKeyDown = (event: KeyboardEvent) => {
if (!filteredAliasTokens.length) return;
switch (event.key) {
case "ArrowDown":
event.preventDefault();
if (selectedAliasIndex === filteredAliasTokens.length - 1) {
selectedAliasIndex = 0;
} else {
selectedAliasIndex = selectedAliasIndex + 1;
}
break;
case "ArrowUp":
event.preventDefault();
if (selectedAliasIndex === 0) {
selectedAliasIndex = filteredAliasTokens.length - 1;
} else {
selectedAliasIndex = selectedAliasIndex - 1;
}
break;
case "Enter":
if (selectedAliasIndex >= 0) {
event.preventDefault();
makeAlias(filteredAliasTokens[selectedAliasIndex].nodeId);
aliasSearchInput = "";
selectedAliasIndex = 0;
popoverElement?.hidePopover();
}
break;
case "Escape":
aliasSearchInput = "";
selectedAliasIndex = 0;
popoverElement?.hidePopover();
break;
}
};

const handleSelectAlias = (nodeId: string) => {
makeAlias(nodeId);
aliasSearchInput = "";
selectedAliasIndex = 0;
popoverElement?.hidePopover();
};

const handleRemoveAlias = () => {
onAlias(undefined);
aliasSearchInput = "";
selectedAliasIndex = 0;
popoverElement?.hidePopover();
};

$effect(() => {
if (popoverElement?.matches(":popover-open")) {
aliasSearchInputElement?.focus();
}
});
</script>

<button
class="a-button"
interestfor="alias-token-tooltip-{key}"
commandfor="alias-token-popopver-{key}"
command="toggle-popover"
aria-pressed={reference !== undefined}
>
<Link2 size={16} />
</button>

<div id="alias-token-tooltip-{key}" class="a-tooltip" popover="hint">
{#if reference}
{reference.replace(/[{}]/g, "").split(".").join(" > ")}
{:else}
Make an alias for another token
{/if}
</div>

<div
bind:this={popoverElement}
id="alias-token-popopver-{key}"
class="a-popover a-menu"
popover="auto"
>
<div class="input-container">
<!-- svelte-ignore a11y_autofocus -->
<input
bind:this={aliasSearchInputElement}
class="a-field"
type="text"
placeholder="Search token..."
autofocus
autocomplete="off"
value={aliasSearchInput}
oninput={(event) => {
aliasSearchInput = event.currentTarget.value;
selectedAliasIndex = 0;
}}
onkeydown={handleAliasKeyDown}
/>
{#if reference}
<button
class="a-button"
aria-label="Remove alias"
type="button"
onclick={handleRemoveAlias}
>
<X size={16} />
</button>
{/if}
</div>
<div class="menu" role="menu">
{#each filteredAliasTokens as token, index (token.nodeId)}
<button
class="a-item"
class:selected={index === selectedAliasIndex}
role="menuitem"
type="button"
onclick={() => handleSelectAlias(token.nodeId)}
>
{token.path.join(" > ")}
</button>
{:else}
<div class="a-label no-results">No matching tokens</div>
{/each}
</div>
</div>

<style>
.a-popover {
width: 320px;
}

.input-container {
position: relative;
display: grid;
align-items: center;
padding: 8px;
gap: 4px;
&:has(button:last-child) {
grid-template-columns: 1fr max-content;
}
}

.menu {
overflow-y: auto;
max-height: 200px;
}

.a-item.selected {
background: var(--bg-hover);
}

.no-results {
padding-bottom: 8px;
text-align: center;
color: var(--text-secondary);
}
</style>
47 changes: 39 additions & 8 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,31 @@
--bg-secondary: #393939;
--bg-tertiary: #383838;
--bg-hover: #404040;
--bg-reference: oklch(42% 0.2 300);
--bg-reference-hover: oklch(50% 0.2 300);
--border-color: #454545;
--text-primary: #e6e6e6;
--text-secondary: #999999;
--accent: #18a0fb;
--accent-hover: #27affe;
--popover-shadow:
0 2px 5px 0 rgb(0 0 0 / 35%), inset 0 0 0.5px 0 rgb(255 255 255 / 35%),
0 10px 16px 0 rgb(0 0 0 / 35%), inset 0 0.5px 0 0 rgb(255 255 255 / 8%);
--panel-header-height: 40px;

font-family: var(--typography-geometric-humanist);
accent-color: var(--bg-secondary);
}

/* reduce show delay when another tooltip is shown already */
:root:has([interestfor]:has-interest) [interestfor] {
interest-show-delay: 100ms;
}

:root:has([interestfor].has-interest) [interestfor] {
--interest-show-delay: 100ms;
}

* {
box-sizing: border-box;
scrollbar-width: thin;
Expand Down Expand Up @@ -119,19 +133,26 @@ color-input::part(trigger) {
align-items: center;
justify-content: center;
border: 1px solid transparent;
background: transparent;
background-color: transparent;
border-radius: 4px;
color: var(--text-secondary);
transition: all 0.2s ease;
font-family: inherit;
font-size: 14px;
font-weight: 600;

&[aria-pressed="true"] {
background-color: var(--bg-reference);
&:hover {
background-color: var(--bg-reference-hover);
}
}
}

.a-button:hover,
/* cannot use css nesting inside of pseudo-element */
color-input::part(trigger):hover {
background: var(--bg-hover);
background-color: var(--bg-hover);
color: var(--text-primary);
}

Expand Down Expand Up @@ -166,20 +187,30 @@ color-input::part(trigger):focus-visible {
background: var(--bg-primary);
border: 0;
padding: 0;
box-shadow:
0 2px 5px 0 rgb(0 0 0 / 35%),
inset 0 0 0.5px 0 rgb(255 255 255 / 35%),
0 10px 16px 0 rgb(0 0 0 / 35%),
inset 0 0.5px 0 0 rgb(255 255 255 / 8%);
box-shadow: var(--popover-shadow);
}

.a-menu {
position-area: center bottom;
position-area: bottom;
position-try-fallbacks: flip-block;
margin-inline: 0;
margin-block: 8px;
}

.a-tooltip {
width: max-content;
background: var(--bg-secondary);
color: var(--text-primary);
border: 0;
padding: 4px 12px;
margin-inline: 0;
margin-block: 8px;
border-radius: 4px;
box-shadow: var(--popover-shadow);
position-area: center top;
position-try-fallbacks: flip-block;
}

.a-item {
display: block;
width: 100%;
Expand Down
7 changes: 7 additions & 0 deletions src/app.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
declare module "svelte/elements" {
export interface DOMAttributes<T extends EventTarget> {
interestfor?: string;
}
}

export {};
1 change: 1 addition & 0 deletions src/app.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import "invokers-polyfill";
import "dialog-closedby-polyfill";
import "hdr-color-input";
import "interestfor";
</script>

<script lang="ts">
Expand Down
Loading