Skip to content
Open
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
9 changes: 8 additions & 1 deletion package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@gropius/graph-editor": "^0.0.4",
"@material/material-color-utilities": "^0.3.0",
"@mdi/font": "7.4.47",
"@mdi/js": "^7.4.47",
"@vee-validate/yup": "^4.15.1",
"@vueuse/core": "^13.8.0",
"async-mutex": "^0.5.0",
Expand Down
82 changes: 82 additions & 0 deletions src/components/EditableCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<template>
<v-card :variant="props.variant ?? 'outlined'" :color="props.color" class="my-2 me-10">
<div v-if="!editMode" class="d-flex align-center justify-space-between">
<div class="d-flex align-center flex-grow-1 overflow-hidden">
<v-icon v-if="props.prependIcon" class="ms-2" size="small">{{ props.prependIcon }}</v-icon>
<div class="text-h8 font-weight-medium mx-2 text-truncate">
{{ props.content }}
</div>
</div>
<div class="d-flex align-center flex-shrink-0 ms-2">
<IconButton v-if="props.editable" @click="toggleEditMode">
<v-icon>mdi-pencil</v-icon>
</IconButton>
<IconButton color="error" class="me-1" @click="emit('delete')">
<v-icon>mdi-delete</v-icon>
</IconButton>
</div>
</div>
<div v-else class="d-flex align-center">
<v-text-field v-model="localContent" class="ma-2" hide-details density="compact" />
<div class="d-flex justify-end">
<v-btn
v-if="props.editable"
class="me-1"
@click="
() => {
toggleEditMode();
localContent = props.content;
}
"
>Cancel</v-btn
>
<v-btn
class="mx-1"
@click="
() => {
emit('confirm', { content: localContent });
toggleEditMode();
}
"
>Confirm</v-btn
>
</div>
</div>
</v-card>
</template>

<script setup lang="ts">
import { ref, watch } from "vue";

const props = withDefaults(
defineProps<{
content: string;
color?: string;
editable?: boolean;
variant?: "flat" | "text" | "elevated" | "tonal" | "outlined" | "plain";
prependIcon?: string;
}>(),
{
editable: true
}
);

const emit = defineEmits<{
(e: "delete"): void;
(e: "confirm", payload: { content: string }): void;
}>();

const editMode = ref(false);
const localContent = ref<string>(props.content);

watch(
() => props.content,
(newContent) => {
localContent.value = newContent;
}
);

function toggleEditMode() {
editMode.value = !editMode.value;
}
</script>
101 changes: 101 additions & 0 deletions src/components/ExpandableCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<template>
<v-card variant="outlined" class="my-4" :class="{ 'opacity-60': props.editable === false }">
<div v-if="!isExpanded" class="d-flex align-center justify-space-between">
<slot name="previewLeft" />

<div class="d-flex align-center flex-grow-1 overflow-hidden">
<div class="text-h6 font-weight-medium ma-2 text-truncate">
{{ props.name }}
</div>
</div>

<slot name="previewRight" />

<div class="d-flex align-center flex-shrink-0 ms-2">
<IconButton :disabled="!props.editable" @click="emit('expand')">
<v-icon>mdi-pencil</v-icon>
</IconButton>
<IconButton :disabled="!props.editable" color="error" class="me-1" @click="emit('delete')">
<v-icon>mdi-delete</v-icon>
</IconButton>
</div>
</div>

<div v-else class="mt-2">
<v-text-field
class="mx-2 mb-2"
label="Name"
v-model="localName"
density="compact"
:error="!!props.nameErrorMessage"
:error-messages="props.nameErrorMessage"
/>
<v-textarea
v-if="props.description !== undefined"
class="mx-2"
label="Description"
v-model="localDescription"
auto-grow
rows="1"
max-rows="2"
density="compact"
/>
Comment on lines +33 to +42

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

confirm always emits description: "" even when the description field is hidden.

When props.description is undefined, the textarea is not rendered (line 34), but the confirm event still includes description: localDescription which defaults to "". Consumers may misinterpret this as the user intentionally clearing the description. Consider emitting description: undefined when description wasn't provided.

Also applies to: 48-48

🤖 Prompt for AI Agents
In `@src/components/ExpandableCard.vue` around lines 33 - 42, The confirm emit
currently always includes description: localDescription which defaults to ""
even when the textarea is not rendered (props.description === undefined); update
the emit logic in the component (where the confirm event is emitted) to check
props.description and only include description: localDescription when
props.description !== undefined, otherwise emit description: undefined so
consumers can distinguish "not provided" from an intentional empty string; apply
the same conditional behavior to the other emit site referenced (the second
confirm/cancel emission) that also uses localDescription.


<slot name="extra" />

<div class="d-flex justify-end ga-1">
<v-btn class="mb-2" @click="emit('cancel')">Cancel</v-btn>
<v-btn class="me-2" @click="emit('confirm', { name: localName, description: localDescription })">
Confirm
</v-btn>
</div>
</div>
</v-card>
</template>

<script setup lang="ts">
import { ref, computed, watch } from "vue";

type ExpandedKey = {
nameID: string;
type: string;
} | null;
Comment on lines +59 to +62

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Using props.name as the card identity is fragile and can cause multiple cards to expand simultaneously.

isExpanded matches expandedCardKey.nameID against props.name. If two cards share the same name and type, both will compute isExpanded = true. Consider introducing a dedicated unique id prop instead of relying on the display name for identity.

The ExpandedKey type already names the field nameID, which suggests it's intended to be a unique identifier — but comparing it to the mutable display name undermines that intent.

Proposed fix: add an explicit `id` prop
 const props = withDefaults(
     defineProps<{
+        id: string;
         name: string;
         description?: string;
         expandedCardKey: ExpandedKey;
         type: string;
         nameErrorMessage?: string;
         editable?: boolean;
     }>(),
     {
         editable: true
     }
 );

 const isExpanded = computed(
-    () => props.editable && props.expandedCardKey?.type === props.type && props.expandedCardKey?.nameID === props.name
+    () => props.editable && props.expandedCardKey?.type === props.type && props.expandedCardKey?.nameID === props.id
 );

Also applies to: 85-87

🤖 Prompt for AI Agents
In `@src/components/ExpandableCard.vue` around lines 59 - 62, The component uses
props.name to compute isExpanded against ExpandedKey.nameID, which is fragile;
add an explicit unique id prop (e.g., prop name "id" or "cardId") and update the
ExpandedKey.type to use that id field consistently: change comparisons in
isExpanded (and anywhere else using props.name for identity, e.g., the code
around isExpanded and the expand/collapse handlers) to compare
ExpandedKey.nameID with props.id (or props.cardId) instead of props.name, and
ensure the prop is required/validated so each card supplies a stable unique
identifier.


const props = withDefaults(
defineProps<{
name: string;
description?: string;
expandedCardKey: ExpandedKey;
type: string;
nameErrorMessage?: string;
editable?: boolean;
}>(),
{
editable: true
}
);

const emit = defineEmits<{
(e: "expand"): void;
(e: "cancel"): void;
(e: "confirm", payload: { name: string; description: string }): void;
(e: "delete"): void;
}>();

const isExpanded = computed(
() => props.editable && props.expandedCardKey?.type === props.type && props.expandedCardKey?.nameID === props.name
);

const localName = ref(props.name);
const localDescription = ref(props.description ?? "");

watch(
() => props.expandedCardKey,
() => {
if (isExpanded.value) {
localName.value = props.name;
localDescription.value = props.description ?? "";
}
}
);
</script>
6 changes: 5 additions & 1 deletion src/components/SideBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
<div v-if="idx > 0" class="mx-3 mb-3 mt-1 align-self-stretch">
<v-divider></v-divider>
</div>
<div v-for="(item, index) in itemGroup" :key="index" class="sidebar-item mb-2">
<div
v-for="(item, index) in itemGroup"
:key="index"
class="sidebar-item mb-2 d-flex align-center justify-center"
>
<SideBarButton
v-if="'name' in item"
:icon="item.icon"
Expand Down
3 changes: 3 additions & 0 deletions src/components/SideBarButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ function chooseActive(isActive: boolean, isExactActive: boolean): boolean {
@use "vuetify/settings" as *;

.icon-container {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
width: 56px;
height: 32px;
Expand Down
13 changes: 13 additions & 0 deletions src/components/SvgWrapper.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="32" height="32">
<path :d="normalizedPath" />
</svg>
</template>

<script lang="ts" setup>
import { computed } from "vue";

const props = defineProps<{ path: string }>();

const normalizedPath = computed(() => props.path.trim().replace(/"/g, ""));
</script>
Loading