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
57 changes: 57 additions & 0 deletions .claude/skills/components/display-dialog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ open/close state management.
|---|---|---|---|
| `dataDialogId` | `string` | — | **Required.** Unique identifier rendered as `data-dialog-id` attribute. |
| `variant` | `'dialog' \| 'modal' \| 'confirm' \| 'alert' \| 'fullscreen'` | `'dialog'` | Controls panel sizing and ARIA role. |
| `theme` | `SemanticTheme` | `undefined` | Optional header accent — `data-theme` on `.header` only; adds a coloured bottom border and tints the close icon. |
| `v-model` | `boolean` | — | Controls open/closed state. |
| `allowContentScroll` | `boolean` | `false` | Enables independent scroll on `.dialog-content`. |
| `lockViewport` | `boolean` | `true` | Adds/removes `lock` class on `<body>` on mount/close. |
Expand Down Expand Up @@ -119,6 +120,62 @@ CSS over per-instance overrides — dialogs are site-wide UI:
}
```

## Nested dialogs

When a dialog needs to launch its own sub-dialog (e.g. "Discard changes?" before closing an edit
form), the cleanest approach is to call `useDialogControls` **inside the dialog component itself**.
The sub-dialog is entirely self-contained — the parent page doesn't need to know about it.

```vue
<!-- EditProfileDialog.vue -->
<script setup lang="ts">
const emit = defineEmits<{ close: [] }>();

const { dialogsConfig, openDialog, closeDialog } = useDialogControls({
confirmDiscard: {
onConfirm: () => emit("close"),
onCancel: () => {},
},
});

const handleClose = () => {
if (hasUnsavedChanges.value) {
openDialog("confirmDiscard");
} else {
emit("close");
}
};
</script>

<template>
<!-- main dialog content, close button calls handleClose() -->

<!-- sub-dialog rendered inside the same component -->
<DisplayDialog
v-if="dialogsConfig['confirmDiscard']"
v-model="dialogsConfig['confirmDiscard']"
variant="confirm"
theme="warning"
data-dialog-id="confirmDiscard"
>
<template #dialogTitle><p>Discard changes?</p></template>
<template #dialogContent><p>Your unsaved changes will be lost.</p></template>
<template #actionButtonLeft>
<button @click="closeDialog('confirmDiscard', 'cancel')">Keep editing</button>
</template>
<template #actionButtonRight>
<button @click="closeDialog('confirmDiscard', 'confirm')">Discard</button>
</template>
</DisplayDialog>
</template>
```

Both dialogs are open simultaneously — native `<dialog>` stacking order ensures the confirm
renders on top. The confirm resolves first; its `onConfirm` callback triggers the parent close.

**Avoid** registering the sub-dialog on the page level — that couples the page to the dialog's
internal concern and leaks implementation detail upward.

## Notes

- Always use `v-if="dialogsConfig['id']"` (not `v-show`) — `DisplayDialog` runs body-lock logic
Expand Down
126 changes: 126 additions & 0 deletions .vscode/srcdev-component-dialog.code-snippets
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
{
"SRCDEV DisplayDialog Script Setup": {
"description": "useDialogControls setup for one or more DisplayDialog instances",
"scope": "javascript,typescript",
"body": [
"const { dialogsConfig, openDialog, closeDialog } = useDialogControls({",
" $1myDialog: {",
" onConfirm: () => {},",
" onCancel: () => {},",
" },",
"});"
]
},
"SRCDEV DisplayDialog Trigger Button": {
"description": "Button that opens a DisplayDialog",
"scope": "vue,html",
"body": [
"<button type=\"button\" @click.prevent=\"openDialog('$1myDialog')\">$2Open dialog</button>"
]
},
"SRCDEV DisplayDialog Confirm Template": {
"description": "DisplayDialog confirm variant — content-sized, requires explicit action",
"scope": "vue,html",
"body": [
"<DisplayDialog",
" v-if=\"dialogsConfig['$1myDialog']\"",
" v-model=\"dialogsConfig['$1myDialog']\"",
" variant=\"confirm\"",
" data-dialog-id=\"$1myDialog\"",
">",
" <template #dialogTitle>",
" <p class=\"text-normal wght-700 m-0\">$2Confirm action?</p>",
" </template>",
" <template #dialogContent>",
" <div class=\"m-0\">",
" <p class=\"text-normal\">$3Are you sure you want to continue?</p>",
" </div>",
" </template>",
" <template #actionButtonLeft>",
" <button type=\"button\" @click=\"closeDialog('$1myDialog', 'cancel')\">Cancel</button>",
" </template>",
" <template #actionButtonRight>",
" <button type=\"button\" @click=\"closeDialog('$1myDialog', 'confirm')\">Confirm</button>",
" </template>",
"</DisplayDialog>"
]
},
"SRCDEV DisplayDialog Alert Template": {
"description": "DisplayDialog alert variant — cannot be dismissed by Escape or outside-click",
"scope": "vue,html",
"body": [
"<DisplayDialog",
" v-if=\"dialogsConfig['$1myDialog']\"",
" v-model=\"dialogsConfig['$1myDialog']\"",
" variant=\"alert\"",
" theme=\"error\"",
" data-dialog-id=\"$1myDialog\"",
">",
" <template #dialogTitle>",
" <p class=\"text-normal wght-700 m-0\">$2Delete item?</p>",
" </template>",
" <template #dialogContent>",
" <div class=\"m-0\">",
" <p class=\"text-normal\">$3This action is permanent and cannot be undone.</p>",
" </div>",
" </template>",
" <template #actionButtonLeft>",
" <button type=\"button\" @click=\"closeDialog('$1myDialog', 'cancel')\">Cancel</button>",
" </template>",
" <template #actionButtonRight>",
" <button type=\"button\" @click=\"closeDialog('$1myDialog', 'confirm')\">$4Delete</button>",
" </template>",
"</DisplayDialog>"
]
},
"SRCDEV DisplayDialog Scrollable Template": {
"description": "DisplayDialog dialog variant — tall panel with scrollable content area",
"scope": "vue,html",
"body": [
"<DisplayDialog",
" v-if=\"dialogsConfig['$1myDialog']\"",
" v-model=\"dialogsConfig['$1myDialog']\"",
" variant=\"dialog\"",
" :allow-content-scroll=\"true\"",
" data-dialog-id=\"$1myDialog\"",
">",
" <template #dialogTitle>",
" <p class=\"text-normal wght-700 m-0\">$2Dialog title</p>",
" </template>",
" <template #dialogContent>",
" <div class=\"pt-12 pb-12\">",
" $3",
" </div>",
" </template>",
" <template #actionButtonLeft>",
" <button type=\"button\" @click=\"closeDialog('$1myDialog', 'cancel')\">Cancel</button>",
" </template>",
" <template #actionButtonRight>",
" <button type=\"button\" @click=\"closeDialog('$1myDialog', 'confirm')\">Confirm</button>",
" </template>",
"</DisplayDialog>"
]
},
"SRCDEV DisplayDialog CSS Override": {
"description": "CSS override scaffold for DisplayDialog — scope to a page or section class",
"scope": "css",
"body": [
".$1my-page {",
" .display-dialog {",
" --display-dialog-inner-border-radius: ;",
" --display-dialog-inner-background: ;",
" --display-dialog-inner-border: ;",
" --display-dialog-backdrop-background: ;",
" --display-dialog-backdrop-blur: ;",
" --display-dialog-transition-duration: ;",
"",
" .inner {",
" .header {}",
" .dialog-content {}",
" .footer {}",
" }",
" }",
"}"
]
}
}
66 changes: 66 additions & 0 deletions .vscode/srcdev-component-display-prompt.code-snippets
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"SRCDEV DisplayPrompt Template": {
"description": "DisplayPrompt inline notification banner with all slots",
"scope": "vue,html",
"body": [
"<DisplayPrompt",
" theme=\"$1info\"",
" :dismissible=\"true\"",
" :style-class-passthrough=\"['$2']\"",
">",
" <template #title>$3Prompt title</template>",
" <template #content>$4Prompt body text.</template>",
"</DisplayPrompt>"
]
},
"SRCDEV DisplayPrompt Masked Template": {
"description": "DisplayPrompt with SVG glass border — place over a coloured background",
"scope": "vue,html",
"body": [
"<DisplayPrompt",
" theme=\"$1info\"",
" :masked=\"true\"",
" :dismissible=\"true\"",
">",
" <template #title>$2Prompt title</template>",
" <template #content>$3Prompt body text.</template>",
"</DisplayPrompt>"
]
},
"SRCDEV DisplayPrompt v-model Template": {
"description": "DisplayPrompt with parent-controlled dismiss via v-model",
"scope": "vue,html",
"body": [
"<DisplayPrompt",
" v-model=\"$1showPrompt\"",
" theme=\"$2info\"",
" :dismissible=\"true\"",
">",
" <template #title>$3Prompt title</template>",
" <template #content>$4Prompt body text.</template>",
"</DisplayPrompt>"
]
},
"SRCDEV DisplayPrompt Script Setup": {
"description": "Script refs for DisplayPrompt v-model and theme",
"scope": "javascript,typescript",
"body": [
"const showPrompt = ref(true);",
"const promptTheme = ref<'info' | 'success' | 'warning' | 'error'>('$1info');"
]
},
"SRCDEV DisplayPrompt CSS Override": {
"description": "CSS override scaffold for DisplayPrompt — scope to a page or section class",
"scope": "css",
"body": [
".$1my-page {",
" .display-prompt-wrapper {",
" --theme-accent: ;",
" --theme-border: ;",
" --theme-surface: ;",
" --theme-text: ;",
" }",
"}"
]
}
}
78 changes: 78 additions & 0 deletions .vscode/srcdev-component-display-toast.code-snippets
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"SRCDEV DisplayToastProvider Layout Setup": {
"description": "DisplayToastProvider — place once in your default layout",
"scope": "vue,html",
"body": [
"<DisplayToastProvider",
" position=\"$1top\"",
" alignment=\"$2right\"",
" :max-visible=\"1\"",
"/>"
]
},
"SRCDEV useToastQueue Script Setup": {
"description": "useToastQueue — trigger app-wide toasts from anywhere",
"scope": "javascript,typescript",
"body": [
"const { show, dismiss, clear } = useToastQueue();",
"",
"const show$1Success = () => {",
" show({",
" appearance: { theme: 'success' },",
" behavior: { autoDismiss: true, duration: 4000 },",
" content: { title: '$2Done', description: '$3Your changes have been saved.' },",
" });",
"};",
"",
"const show$1Error = () => {",
" show({",
" appearance: { theme: 'error' },",
" behavior: { autoDismiss: false },",
" content: { title: '$4Something went wrong', description: '$5Please try again.' },",
" });",
"};"
]
},
"SRCDEV DisplayToast Standalone Template": {
"description": "DisplayToast standalone — v-model driven, no queue",
"scope": "vue,html",
"body": [
"<DisplayToast",
" v-model=\"$1toastVisible\"",
" :config=\"{",
" appearance: { theme: '$2info' },",
" behavior: { autoDismiss: true, duration: 4000 },",
" content: { title: '$3Title', description: '$4Description.' },",
" }\"",
"/>"
]
},
"SRCDEV DisplayToast Standalone Script Setup": {
"description": "Script ref for standalone DisplayToast v-model",
"scope": "javascript,typescript",
"body": [
"const $1toastVisible = ref(false);"
]
},
"SRCDEV DisplayToast Masked Config": {
"description": "Masked SVG glass variant — add to any toast config appearance block",
"scope": "javascript,typescript",
"body": [
"appearance: { theme: '$1info', masked: true },"
]
},
"SRCDEV DisplayToast CSS Override": {
"description": "CSS override scaffold for DisplayToastProvider items",
"scope": "css",
"body": [
".$1my-page {",
" .display-toast-provider-item {",
" --theme-surface: ;",
" --theme-text: ;",
" --theme-accent: ;",
" --theme-border: ;",
" }",
"}"
]
}
}
Loading
Loading