From 785b843ce1a875072cab3eb34245932472fcc67e Mon Sep 17 00:00:00 2001 From: Simon Cornforth Date: Sat, 20 Jun 2026 00:37:29 +0100 Subject: [PATCH 1/3] Dialog themes added - also diverted into a snippet migration session --- .claude/skills/components/display-dialog.md | 57 +++ .vscode/srcdev-component-dialog.code-snippets | 126 ++++++ ...dev-component-display-prompt.code-snippets | 66 +++ ...cdev-component-display-toast.code-snippets | 78 ++++ ...rcdev-component-pinia-store.code-snippets} | 0 ...ev-nuxt3-accordian-component.code-snippets | 119 ------ ...xt3-carousel-basic-component.code-snippets | 230 ---------- ...uxt3-carousel-flip-component.code-snippets | 241 ----------- ...-nuxt3-component-boilerplate.code-snippets | 55 --- ...xt3-container-glow-component.code-snippets | 91 ---- ...rcdev-nuxt3-dialog-component.code-snippets | 164 ------- ...xt3-display-banner-component.code-snippets | 63 --- ...t3-display-details-component.code-snippets | 71 --- ...xt3-display-prompt-component.code-snippets | 38 -- ...t3-expanding-panel-component.code-snippets | 85 ---- .vscode/srcdev-nuxt3-layout-row.code-snippets | 12 - .../srcdev-nuxt3-page-layout.code-snippets | 50 --- ...-responsive-header-component.code-snippets | 403 ------------------ .../srcdev-nuxt3-tabs-component.code-snippets | 139 ------ Claude.md | 3 +- MIGRATION.md | 210 +++++++++ .../01.atoms/display-dialog/DisplayDialog.vue | 32 +- app/composables/useDialogControls.ts | 22 +- app/pages/ui/display-dialog.vue | 402 +++++++++++------ 24 files changed, 852 insertions(+), 1905 deletions(-) create mode 100644 .vscode/srcdev-component-dialog.code-snippets create mode 100644 .vscode/srcdev-component-display-prompt.code-snippets create mode 100644 .vscode/srcdev-component-display-toast.code-snippets rename .vscode/{srcdev-nuxt3-pinia-store-setup.code-snippets => srcdev-component-pinia-store.code-snippets} (100%) delete mode 100644 .vscode/srcdev-nuxt3-accordian-component.code-snippets delete mode 100644 .vscode/srcdev-nuxt3-carousel-basic-component.code-snippets delete mode 100644 .vscode/srcdev-nuxt3-carousel-flip-component.code-snippets delete mode 100644 .vscode/srcdev-nuxt3-component-boilerplate.code-snippets delete mode 100644 .vscode/srcdev-nuxt3-container-glow-component.code-snippets delete mode 100644 .vscode/srcdev-nuxt3-dialog-component.code-snippets delete mode 100644 .vscode/srcdev-nuxt3-display-banner-component.code-snippets delete mode 100644 .vscode/srcdev-nuxt3-display-details-component.code-snippets delete mode 100644 .vscode/srcdev-nuxt3-display-prompt-component.code-snippets delete mode 100644 .vscode/srcdev-nuxt3-expanding-panel-component.code-snippets delete mode 100644 .vscode/srcdev-nuxt3-layout-row.code-snippets delete mode 100644 .vscode/srcdev-nuxt3-page-layout.code-snippets delete mode 100644 .vscode/srcdev-nuxt3-responsive-header-component.code-snippets delete mode 100644 .vscode/srcdev-nuxt3-tabs-component.code-snippets create mode 100644 MIGRATION.md diff --git a/.claude/skills/components/display-dialog.md b/.claude/skills/components/display-dialog.md index 10b68056..905dedc6 100644 --- a/.claude/skills/components/display-dialog.md +++ b/.claude/skills/components/display-dialog.md @@ -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 `` on mount/close. | @@ -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 + + + + +``` + +Both dialogs are open simultaneously — native `` 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 diff --git a/.vscode/srcdev-component-dialog.code-snippets b/.vscode/srcdev-component-dialog.code-snippets new file mode 100644 index 00000000..223d7fa4 --- /dev/null +++ b/.vscode/srcdev-component-dialog.code-snippets @@ -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": [ + "" + ] + }, + "SRCDEV DisplayDialog Confirm Template": { + "description": "DisplayDialog confirm variant — content-sized, requires explicit action", + "scope": "vue,html", + "body": [ + "", + " ", + " ", + " ", + " ", + "" + ] + }, + "SRCDEV DisplayDialog Alert Template": { + "description": "DisplayDialog alert variant — cannot be dismissed by Escape or outside-click", + "scope": "vue,html", + "body": [ + "", + " ", + " ", + " ", + " ", + "" + ] + }, + "SRCDEV DisplayDialog Scrollable Template": { + "description": "DisplayDialog dialog variant — tall panel with scrollable content area", + "scope": "vue,html", + "body": [ + "", + " ", + " ", + " ", + " ", + "" + ] + }, + "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 {}", + " }", + " }", + "}" + ] + } +} diff --git a/.vscode/srcdev-component-display-prompt.code-snippets b/.vscode/srcdev-component-display-prompt.code-snippets new file mode 100644 index 00000000..1fcefb60 --- /dev/null +++ b/.vscode/srcdev-component-display-prompt.code-snippets @@ -0,0 +1,66 @@ +{ + "SRCDEV DisplayPrompt Template": { + "description": "DisplayPrompt inline notification banner with all slots", + "scope": "vue,html", + "body": [ + "", + " ", + " ", + "" + ] + }, + "SRCDEV DisplayPrompt Masked Template": { + "description": "DisplayPrompt with SVG glass border — place over a coloured background", + "scope": "vue,html", + "body": [ + "", + " ", + " ", + "" + ] + }, + "SRCDEV DisplayPrompt v-model Template": { + "description": "DisplayPrompt with parent-controlled dismiss via v-model", + "scope": "vue,html", + "body": [ + "", + " ", + " ", + "" + ] + }, + "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: ;", + " }", + "}" + ] + } +} diff --git a/.vscode/srcdev-component-display-toast.code-snippets b/.vscode/srcdev-component-display-toast.code-snippets new file mode 100644 index 00000000..812ccbfe --- /dev/null +++ b/.vscode/srcdev-component-display-toast.code-snippets @@ -0,0 +1,78 @@ +{ + "SRCDEV DisplayToastProvider Layout Setup": { + "description": "DisplayToastProvider — place once in your default layout", + "scope": "vue,html", + "body": [ + "" + ] + }, + "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": [ + "" + ] + }, + "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: ;", + " }", + "}" + ] + } +} diff --git a/.vscode/srcdev-nuxt3-pinia-store-setup.code-snippets b/.vscode/srcdev-component-pinia-store.code-snippets similarity index 100% rename from .vscode/srcdev-nuxt3-pinia-store-setup.code-snippets rename to .vscode/srcdev-component-pinia-store.code-snippets diff --git a/.vscode/srcdev-nuxt3-accordian-component.code-snippets b/.vscode/srcdev-nuxt3-accordian-component.code-snippets deleted file mode 100644 index 549bf3ce..00000000 --- a/.vscode/srcdev-nuxt3-accordian-component.code-snippets +++ /dev/null @@ -1,119 +0,0 @@ -{ - "SRCDEV Nuxt Accordian Component From Data": { - "description": "SRCDEV Nuxt Accordian Component From Data", - "scope": "vue, html", - "body": [ - "", - " ", - " ", - " ", - "", - ] - }, - "SRCDEV Nuxt Accordian Component From Data + Static content": { - "description": "SRCDEV Nuxt Accordian Component From Data + Static content", - "scope": "vue, html", - "body": [ - "", - " ", - " ", - " ", - " ", - " ", - "" - ] - }, - "SRCDEV Nuxt Accordian Script Setup Template": { - "description": "Add bootstrap script setup for Accordian component", - "scope": "javascript,typescript", - "body": [ - "interface IAccordianData {", - " title: string;", - " content: string;", - "}", - "", - "const data = ref([", - " {", - " title: 'Trigger Item 1',", - " content: 'Lorem ipsum dolor sit amet consectetur, adipisicing elit. Iusto, amet!. Lorem ipsum dolor sit, adipisicing elit. Iusto, amet!',", - " },", - " {", - " title: 'Trigger Item 2',", - " content: 'Lorem ipsum dolor sit amet consectetur, adipisicing elit. Iusto, amet!',", - " },", - " {", - " title: 'Trigger Item 3',", - " content: 'Lorem ipsum dolor sit amet consectetur, adipisicing elit. Iusto, amet!, Lorem ipsum dolor sit amet consectetur, adipisicing elit. Iusto, amet! Lorem ipsum dolor sit amet consectetur, adipisicing elit. Iusto, amet!',", - " },", - "]);", - ] - }, - "SRCDEV Nuxt Accordian Component CSS Configuration": { - "description": "Add a Nuxt Expanding Panel Component CSS configuration", - "scope": "css", - "body": [ - ".class-modifier-narrow {", - " &.display-accordian {", - " max-width: 300px;", - " margin: 0 auto;", - "", - " .accordian-item {", - " &.expanding-panel {", - " .expanding-panel-details {", - " .expanding-panel-summary {", - " /* Custom css */", - "", - " .label-wrapper {}", - " .icon-wrapper {", - " /* Custom css */", - "", - " .icon {", - " /* Custom css */", - " }", - " }", - " }", - "", - " &[open", - " ] {", - " .expanding-panel-summary {", - " .icon-wrapper {", - " .icon {", - " /* Custom css */", - " }", - " }", - " }", - " + .expanding-panel-content {", - " /* Custom css */", - " .inner {", - " /* Custom css */", - " }", - " }", - " }", - " }", - "", - " .expanding-panel-content {", - " /* Custom css */", - "", - " .inner {", - " /* Custom css */", - " }", - " }", - " }", - " }", - " }", - "}" - ] - } -} diff --git a/.vscode/srcdev-nuxt3-carousel-basic-component.code-snippets b/.vscode/srcdev-nuxt3-carousel-basic-component.code-snippets deleted file mode 100644 index 34573b73..00000000 --- a/.vscode/srcdev-nuxt3-carousel-basic-component.code-snippets +++ /dev/null @@ -1,230 +0,0 @@ -{ - "SRCDEV Nuxt Carousel Basic Component HTML Template": { - "description": "Carousel Basic Component HTML Template", - "scope": "vue, html", - "body": [ - "", - " ", - "", - ], - }, - "SRCDEV Nuxt Carousel Basic Component JS Navigation Object Configuration": { - "description": "Carousel Basic Component JS Navigation Object Configuration", - "scope": "javascript,typescript", - "body": [ - "import type { ICarouselBasic } from '@/types/components/types/components.carousel-basic'; // Import the interface for type safety", - "", - "// Fetch API carousel data structure", - "const {", - " data: carouselData,", - " execute: carouselExecute,", - " status: carouselStatus, // See v-if on CarouselBasic", - " error: carouselError,", - "} = await useFetch('/api/carousel',", - "{", - " immediate: true,", - "});", - "", - "// Define the static carousel data structure", - "// Require items to have an id", - "const carouselDataStatic = {", - " items: [", - " {", - " id: 1,", - " url: '/images/spotlights/v2/dining-out.webp',", - " alt: 'Dining out',", - " },", - " {", - " id: 2,", - " url: '/images/spotlights/v2/beer-gardens.webp',", - " alt: 'Entertaining the kids',", - " },", - " {", - " id: 3,", - " url: '/images/spotlights/v2/will-it-rain.webp',", - " alt: 'Will it rain?',", - " },", - " {", - " id: 4,", - " url: '/images/spotlights/v2/nightlife.webp',", - " alt: 'Nightlife',", - " },", - " {", - " id: 5,", - " url: '/images/spotlights/v2/got-toothache-2.webp',", - " alt: 'Dental Services',", - " },", - " {", - " id: 6,", - " url: '/images/spotlights/v2/days-out.webp',", - " alt: 'Days Out',", - " },", - " ],", - " total: 6,", - " skip: 0,", - " limit: 4,", - "};", - "", - "const carouselDataIds = computed(() => {", - " // return carouselData.value?.items.map((item) => item.id) || [];", - " return carouselDataStatic?.items.map((item) => item.id) || [];", - "});", - "" - ] - }, - "SRCDEV Nuxt Carousel Basic Component CSS Configuration": { - "description": "Carousel Basic Component CSS Configuration", - "scope": "css", - "body": [ - ".carousel-basic-demo { /* This class is the namespace added to the component props */", - " &.carousel-basic {", - " /* Var used in calcs */", - " --_carousel-item-track-gap: 10px;", - "", - " .timeline-container {", - " padding-block: 10px;", - " padding-inline: 10px;", - " outline: 1px solid light-dark(#00000090, #f00ff090);", - "", - " .timeline-item {", - " max-inline-size: 800px;", - "", - " color: light-dark(#aaa, #333);", - " padding-block: 10px;", - " border-radius: 4px;", - " outline: 1px solid light-dark(#00000090, #f00ff090);", - "", - " &: :before {", - " content: '';", - " position: absolute;", - " height: 2px;", - " background-color: #fff;", - " left: 70px;", - " right: 0;", - " }", - "", - " .count {", - " font-size: 1.2rem;", - " border-radius: 8px;", - " color: light-dark(#fff, #000);", - " background-color: light-dark(#000, #fff);", - " padding-block: 6px;", - " padding-inline: 12px;", - " }", - " }", - " }", - "", - " .item-container {", - " padding-block: 10px;", - " padding-inline: 10px;", - " outline: 1px solid light-dark(#00000090, #f00ff090);", - "", - " .item {", - " max-inline-size: 800px;", - "", - " background-color: light-dark(#f00, #00f);", - "", - " &:nth-child(odd) {", - " background-color: light-dark(#00f, #f00);", - " }", - "", - " .case-study-item {", - " flex-direction: column;", - " align-items: center;", - " justify-content: center;", - "", - " aspect-ratio: 4 / 3;", - " inline-size: 100%;", - "", - " color: light-dark(#aaa, #333);", - " padding-block: 10px;", - " padding-inline: 10px;", - " border-radius: 4px;", - " outline: 1px solid light-dark(#00000090, #f00ff090);", - " }", - " }", - " }", - "", - " .controls-container {", - " gap: 20px;", - "", - " .markers-container {", - " .markers-list {", - " .markers-item {", - " line-height: 3px;", - "", - " &.active {", - " background-color: light-dark(#f00, #0f0);", - " }", - "", - " .btn-marker {", - " width: 22px;", - " height: 3px;", - " background-color: lightgray;", - " line-height: 3px;", - "", - " &.active {", - " background-color: red;", - " }", - "", - " &:focus-visible {", - " outline: 1px solid light-dark(#000, #fff);", - " }", - " }", - " }", - " }", - " }", - "", - " .buttons-container {", - " display: flex;", - " align-items: center;", - " justify-content: end;", - " gap: 20px;", - "", - " .btn-action {", - " padding: 10px 20px;", - " border-radius: 4px;", - " background-color: light-dark(#000, #fff);", - " color: light-dark(#fff, #000);", - " border: none;", - "", - " &:hover {", - " background-color: light-dark(#0009, #fff9);", - " }", - "", - " &:active,", - " &.active {", - " background-color: light-dark(#0009, #fff9);", - " }", - "", - " &:focus-visible {", - " outline: 1px solid light-dark(#0f0, #f0f);", - " }", - " .arrows-icon {", - " width: 24px;", - " height: 24px;", - "", - " &:hover {", - " background-color: light-dark(#0009, #fff9);", - " }", - "", - " &:active,", - " &.active {", - " background-color: light-dark(#0009, #fff9);", - " }", - "", - " &:focus-visible {", - " outline: 1px solid light-dark(#0f0, #f0f);", - " }", - " }", - " }", - " }", - " }", - " }", - "}", - ] - } -} diff --git a/.vscode/srcdev-nuxt3-carousel-flip-component.code-snippets b/.vscode/srcdev-nuxt3-carousel-flip-component.code-snippets deleted file mode 100644 index 90bee761..00000000 --- a/.vscode/srcdev-nuxt3-carousel-flip-component.code-snippets +++ /dev/null @@ -1,241 +0,0 @@ -{ - "SRCDEV Nuxt Carousel Flip Component HTML Template": { - "description": "Carousel Flip Component HTML Template", - "scope": "vue, html", - "body": [ - "", - " ", - "", - ] - }, - "SRCDEV Nuxt Carousel Flip Component JS Navigation Object Configuration": { - "description": "Carousel Flip Component JS Navigation Object Configuration", - "scope": "javascript,typescript", - "body": [ - "import type { ICarouselBasic } from '@/types/components/types/components.carousel-basic'; // Import the interface for type safety", - "", - "// Fetch API carousel data structure", - "const {", - " data: carouselData,", - " execute: carouselExecute,", - " status: carouselStatus, // See v-if on CarouselBasic", - " error: carouselError,", - "} = await useFetch('/api/carousel',", - "{", - " immediate: true,", - "});", - "", - "// Define the static carousel data structure", - "// Require items to have an id", - "const carouselDataStatic = {", - " items: [", - " {", - " id: 1, /* or 'item-1' */", - " url: '/images/spotlights/v2/dining-out.webp',", - " alt: 'Dining out',", - " },", - " {", - " id: 2, /* or 'item-2' */", - " url: '/images/spotlights/v2/beer-gardens.webp',", - " alt: 'Entertaining the kids',", - " },", - " {", - " id: 3, /* or 'item-3' */", - " url: '/images/spotlights/v2/will-it-rain.webp',", - " alt: 'Will it rain?',", - " },", - " {", - " id: 4, /* or 'item-4' */", - " url: '/images/spotlights/v2/nightlife.webp',", - " alt: 'Nightlife',", - " },", - " {", - " id: 5, /* or 'item-5' */", - " url: '/images/spotlights/v2/got-toothache-2.webp',", - " alt: 'Dental Services',", - " },", - " {", - " id: 6, /* or 'item-6' */", - " url: '/images/spotlights/v2/days-out.webp',", - " alt: 'Days Out',", - " },", - " ],", - " total: 6,", - " skip: 0,", - " limit: 4,", - "};", - "", - "const carouselDataIds = computed(() => {", - " // return carouselData.value?.items.map((item) => item.id) || [];", - " return carouselDataStatic?.items.map((item) => item.id) || [];", - "});", - "" - ] - }, - "SRCDEV Nuxt Carousel Flip Component CSS Configuration": { - "description": "Carousel Flip Component CSS Configuration", - "scope": "css", - "body": [ - ".carousel-flip-demo { /* This class is the namespace added to the component props */", - " &.carousel-flip {", - " /* Var used in calcs */", - " --_carousel-item-track-gap: 10px;", - " --_carousel-container-max-inline-size: 800px;", - " --_carousel-item-edge-preview-width: 60px; /* Must be at 2x var(--_carousel-item-track-gap) */", - "", - " .item-container {", - " max-inline-size: 800px;", - " margin-inline: auto;", - " outline: 1px solid light-dark(#00000090, #f00ff090);", - " padding-block: 12px;", - " padding-inline: 12px;", - "", - " .item {", - " background-color: light-dark(var(--slate-05), var(--slate-06));", - "", - " &:nth-child(odd) {", - " background-color: light-dark(var(--slate-06), var(--slate-05));", - " }", - "", - " .custom-carousel-item { /* This is the item that is sent to component via dynamic slots */", - " flex-direction: column;", - " align-items: center;", - " justify-content: center;", - "", - " aspect-ratio: 4 / 3;", - " inline-size: 100%;", - "", - " color: light-dark(#aaa, #333);", - " padding-block: 10px;", - " padding-inline: 10px;", - " border-radius: 4px;", - " outline: 1px solid light-dark(#00000090, #f00ff090);", - " }", - " }", - " }", - "", - " .timeline-container {", - " max-inline-size: 800px;", - " margin-inline: auto;", - " outline: 1px solid light-dark(#00000090, #f00ff090);", - " padding-block: 12px;", - " padding-inline: 12px;", - "", - " .timeline-item {", - " max-inline-size: 800px;", - "", - " color: light-dark(#aaa, #333);", - " padding-block: 10px;", - " border-radius: 4px;", - " outline: 1px solid light-dark(#00000090, #f00ff090);", - "", - " &: :before {", - " content: '';", - " position: absolute;", - " height: 2px;", - " background-color: #fff;", - " left: 70px;", - " right: 0;", - " }", - "", - " .count {", - " font-size: 1.2rem;", - " border-radius: 8px;", - " color: light-dark(#fff, #000);", - " background-color: light-dark(#000, #fff);", - " padding-block: 6px;", - " padding-inline: 12px;", - " }", - " }", - " }", - "", - " .controls-container {", - " gap: 20px;", - "", - " .markers-container {", - " .markers-list {", - " .markers-item {", - " line-height: 3px;", - "", - " &.active {", - " background-color: light-dark(#f00, #0f0);", - " }", - "", - " .btn-marker {", - " width: 22px;", - " height: 3px;", - " background-color: lightgray;", - " line-height: 3px;", - "", - " &.active {", - " background-color: red;", - " }", - "", - " &:focus-visible {", - " outline: 1px solid light-dark(#000, #fff);", - " }", - " }", - " }", - " }", - " }", - "", - " .buttons-container {", - " display: flex;", - " align-items: center;", - " justify-content: end;", - " gap: 20px;", - "", - " .btn-action {", - " padding: 10px 20px;", - " border-radius: 4px;", - " background-color: light-dark(#000, #fff);", - " color: light-dark(#fff, #000);", - " border: none;", - "", - " &:hover {", - " background-color: light-dark(#0009, #fff9);", - " }", - "", - " &:active,", - " &.active {", - " background-color: light-dark(#0009, #fff9);", - " }", - "", - " &:focus-visible {", - " outline: 1px solid light-dark(#0f0, #f0f);", - " }", - " .arrows-icon {", - " width: 24px;", - " height: 24px;", - "", - " &:hover {", - " background-color: light-dark(#0009, #fff9);", - " }", - "", - " &:active,", - " &.active {", - " background-color: light-dark(#0009, #fff9);", - " }", - "", - " &:focus-visible {", - " outline: 1px solid light-dark(#0f0, #f0f);", - " }", - " }", - " }", - " }", - " }", - " }", - "}", - ] - } -} diff --git a/.vscode/srcdev-nuxt3-component-boilerplate.code-snippets b/.vscode/srcdev-nuxt3-component-boilerplate.code-snippets deleted file mode 100644 index ae5429fd..00000000 --- a/.vscode/srcdev-nuxt3-component-boilerplate.code-snippets +++ /dev/null @@ -1,55 +0,0 @@ -{ - "SRCDEV Bootstrap Nuxt3 component template": { - "description": "Create a new Nuxt3 component with a basic template", - "scope": "vue", - "body": [ - "", - "", - "", - "", - "", - "" - ] - } -} diff --git a/.vscode/srcdev-nuxt3-container-glow-component.code-snippets b/.vscode/srcdev-nuxt3-container-glow-component.code-snippets deleted file mode 100644 index fa229d68..00000000 --- a/.vscode/srcdev-nuxt3-container-glow-component.code-snippets +++ /dev/null @@ -1,91 +0,0 @@ -{ - "SRCDEV Nuxt Container Glow Component (Static) HTML Template": { - "description": "Add component with static slots", - "scope": "vue, html", - "body": [ - "", - " ", - "" - ] - }, - "SRCDEV Nuxt Container Glow Component Script Setup Template": { - "description": "Add bootstrap script setup for ContainerGlowCore component", - "scope": "javascript,typescript", - "body": [ - "// NOTE:", - "// Instead of using data array, can just pass itemCount as hard coded value and manually create slots", - "const data = [", - " {", - " title: 'Trigger Item 1',", - " content: 'Lorem ipsum dolor sit amet consectetur, adipisicing elit. Iusto, amet!. Lorem ipsum dolor sit, adipisicing elit. Iusto, amet!',", - " },", - " {", - " title: 'Trigger Item 2',", - " content: 'Lorem ipsum dolor sit amet consectetur, adipisicing elit. Iusto, amet!',", - " },", - " {", - " title: 'Trigger Item 3',", - " content: 'Lorem ipsum dolor sit amet consectetur, adipisicing elit. Iusto, amet!, Lorem ipsum dolor sit amet consectetur, adipisicing elit. Iusto, amet! Lorem ipsum dolor sit amet consectetur, adipisicing elit. Iusto, amet!',", - " },", - "];", - ] - }, - "SRCDEV Nuxt Container Glow Component CSS Configuration": { - "description": "Add user styles CSS Configuration", - "scope": "css", - "body": [ - ".class-scope {", - " &.container-glow-wrapper {", - " .container-glow-core {", - " --gradient: conic-gradient(from 180deg at 50% 70%, hsla(0, 0%, 98%, 1) 0deg, #54ff0b 144.0000021457672deg, hsla(0, 0%, 98%, 1) 1turn);", - "", - " height: 100%;", - " background: light-dark(white, hsl(246 44% 7%));", - " padding: 2rem;", - " aspect-ratio: 330 / 400;", - " border-radius: 12px;", - " min-width: 280px;", - " max-width: 280px;", - " display: flex;", - " flex-direction: column;", - " gap: 0.25rem;", - "", - " &: :before {", - " /* Custom styles */", - " }", - "", - " &: :after {", - " /* Custom styles */", - " }", - "", - " .glows {", - " /* Custom styles */", - "", - " &: :after,", - " &: :before {", - " /* Custom styles */", - " }", - " }", - " }", - " }", - "}", - ] - } -} diff --git a/.vscode/srcdev-nuxt3-dialog-component.code-snippets b/.vscode/srcdev-nuxt3-dialog-component.code-snippets deleted file mode 100644 index 3c2ef19d..00000000 --- a/.vscode/srcdev-nuxt3-dialog-component.code-snippets +++ /dev/null @@ -1,164 +0,0 @@ -{ - "SRCDEV Nuxt Dialog Component HTML Template": { - "description": "Add a Nuxt Dialog Trigger HTML template", - "scope": "vue, html", - "body": [ - "" - ] - }, - "SRCDEV Nuxt DisplayDialogScrollableContent Component HTML Template": { - "description": "Add a Nuxt Dialog Trigger HTML template", - "scope": "vue, html", - "body": [ - "", - " ", - "", - " ", - " ", - " ", - "", - ] - }, - "SRCDEV Nuxt DisplayDialogConfirm Component HTML Template": { - "description": "Add DisplayDialogConfirm HTML template", - "scope": "vue, html", - "body": [ - "", - " ", - "", - " ", - " ", - " ", - "", - ] - }, - "SRCDEV Nuxt Dialog Script Setup Example": { - "description": "Example dialog setup, and how to use the controlDialogs function", - "scope": "javascript,typescript", - "body": [ - "const dialogBtnSampleAction = () => {", - " console.log('Dialog button action clicked');", - "};", - "", - "const { dialogsConfig, controlDialogs, initialiseDialogs, registerDialogCallbacks } = useDialogControls();", - "", - "onMounted(() => {", - " const dialogIds = ['$0yourDialogName']; // Add your dialog IDs here, eg. ['yourDialogName', 'anotherDialogName']", - " initialiseDialogs(dialogIds);", - "", - " // Register callbacks for each dialog", - " registerDialogCallbacks('yourDialogName', {", - " onConfirm: dialogBtnSampleAction,", - " onCancel: () => console.log('yourDialogName dialog cancelled'),", - " });", - "", - " // Alternatively, you can register callbacks using a function", - " registerDialogCallbacks('yourDialogName', {", - " onConfirm: () => {", - " dialogBtnSampleAction();", - " console.log('dialogBtnSampleAction() called');", - " },", - " onCancel: () => console.log('yourDialogName cancelled'),", - " });", - "});", - ] - }, - "SRCDEV Nuxt Dialog Component CSS Configuration": { - "description": "Add a Nuxt Dialog Component CSS configuration", - "scope": "css", - "body": [ - ".your-modifier-class {", - " &.display-dialog-core {", - " /* Dialog core styles */", - " /* Delete or add styles as needed */", - "", - " &[open] {", - " @starting-style {}", - " }", - " /* Positioning the dialog */", - " &[justify-dialog='start'] {}", - "", - " &[justify-dialog='center'] {}", - "", - " &[justify-dialog='end'] {}", - "", - " &[align-dialog='start'] {}", - "", - " &[align-dialog='center'] {}", - " &[align-dialog='end'] {}", - "", - " .inner {", - " &.confirm {}", - "", - " &.dialog {}", - "", - " &.form {}", - "", - " &.fullscreen {}", - "", - " &.modal {}", - "", - " .header {", - " .col-left {}", - "", - " .col-center {}", - "", - " .col-right {", - " .display-prompt-action {", - " &:hover {}", - " &:focus-visible {}", - "", - " .icon {}", - " }", - " }", - " }", - "", - " .dialog-content {", - " &.allow-content-scroll {", - " &: :-webkit-scrollbar {}", - " }", - " }", - "", - " .footer {}", - " }", - " }", - "}" - ] - } -} diff --git a/.vscode/srcdev-nuxt3-display-banner-component.code-snippets b/.vscode/srcdev-nuxt3-display-banner-component.code-snippets deleted file mode 100644 index 910b7c1d..00000000 --- a/.vscode/srcdev-nuxt3-display-banner-component.code-snippets +++ /dev/null @@ -1,63 +0,0 @@ -{ - "SRCDEV Nuxt Display Banner Component HTML Template": { - "description": "Display Banner Component HTML Template", - "scope": "vue, html", - "prefix": "srcdev-nuxt-display-banner-component-html-template", - "body": [ - "", - " ", - " ", - "" - ] - }, - "SRCDEV Nuxt Display Banner Component CSS Configuration": { - "description": "Display Banner Component CSS Configuration", - "scope": "css", - "body": [ - ".display-banner-page-class {", - "", - " .display-banner {", - " .canvas {", - " grid-area: banner;", - " width: 100%;", - " height: 150px;", - "", - " @container (min-width: 768px) {", - " height: 200px;", - " }", - "", - " @container (min-width: 1024px) {", - " height: 250px;", - " }", - "", - " @container (min-width: 1280px) {", - " height: 300px;", - " }", - "", - " transition: height 0.3s ease-in-out;", - "", - " .image {", - " object-fit: cover;", - " width: 100%;", - " height: 100%;", - " }", - " }", - "", - " .content {", - " grid-area: banner;", - "", - " /* Custom styles */", - " align-self: center;", - " justify-self: center;", - " }", - " }", - "}" - ] - } -} diff --git a/.vscode/srcdev-nuxt3-display-details-component.code-snippets b/.vscode/srcdev-nuxt3-display-details-component.code-snippets deleted file mode 100644 index 66a9a407..00000000 --- a/.vscode/srcdev-nuxt3-display-details-component.code-snippets +++ /dev/null @@ -1,71 +0,0 @@ -{ - "SRCDEV Nuxt Display Details Component HTML Template": { - "description": "Display Details Component HTML Template", - "scope": "vue, html", - "body": [ - "", - " ", - " ", - " ", - "" - ] - }, - "SRCDEV Nuxt Display Details Component CSS Configuration": { - "description": "Display Details Component CSS Configuration", - "scope": "css", - "body": [ - ".display-details {", - " &.custom-style-1 {", - " $0", - " border: none;", - " outline: none;", - " box-shadow: none;", - " border-radius: 0;", - "", - " &[open] {", - " .display-details-summary {", - " .icon {", - " transform: scaleY(-1);", - " }", - " }", - " }", - "", - " .display-details-summary {", - " /* Custom styles */", - "", - " .label {", - " /* Custom styles */", - " }", - "", - " .icon {", - " /* Custom styles */", - " &.medium {", - " font-size: 1.8rem;", - " }", - " &.large {", - " font-size: 2.4rem;", - " }", - " }", - " }", - "", - " .display-details-content {", - " /* Custom styles */", - " }", - " }", - "}" - ] - } -} diff --git a/.vscode/srcdev-nuxt3-display-prompt-component.code-snippets b/.vscode/srcdev-nuxt3-display-prompt-component.code-snippets deleted file mode 100644 index f35333c0..00000000 --- a/.vscode/srcdev-nuxt3-display-prompt-component.code-snippets +++ /dev/null @@ -1,38 +0,0 @@ -{ - "SRCDEV Nuxt Display Prompt Component HTML Template": { - "description": "Display Prompt Component HTML Template", - "scope": "vue, html", - "body": [ - "", - " ", - " ", - " ", - " ", - " ", - "", - ], - }, - "SRCDEV Nuxt Display Prompt Script Setup Template": { - "description": "Display Prompt setup themeing and properties", - "scope": "javascript,typescript", - "body": [ - "// Set required theme for the display prompt component", - "const theme = ref<'error' | 'info' | 'success' | 'warning' | 'secondary'>('error');", - "$0", - ] - }, - "SRCDEV Nuxt Display Details Component CSS Configuration": { - "description": "Display Prompt CSS Configuration", - "scope": "css", - "body": [ - "// Globally styled component", - "// see: ~/assets/styles/extends-layer/srcdev-components/display-prompt-core/index.css", - "// This file is used to configure the display prompt component styles", - "// Copy css structure for locally scoped components", - ], - } -} diff --git a/.vscode/srcdev-nuxt3-expanding-panel-component.code-snippets b/.vscode/srcdev-nuxt3-expanding-panel-component.code-snippets deleted file mode 100644 index 74364414..00000000 --- a/.vscode/srcdev-nuxt3-expanding-panel-component.code-snippets +++ /dev/null @@ -1,85 +0,0 @@ -{ - "SRCDEV Nuxt Expanding Panel Component HTML Template": { - "description": "Add a Nuxt Expanding Panel Component HTML template", - "scope": "vue, html", - "body": [ - "", - " ", - " ", - " ", - "" - ] - }, - "SRCDEV Nuxt Expanding Panel Component CSS Configuration": { - "description": "Add a Nuxt Expanding Panel Component CSS configuration", - "scope": "css", - "body": [ - ".expanding-panel {", - " $0&.custom-style-1 [ /* css class modifier set via :style-class-passthrough on template */", - " + .expanding-panel {", - " /* Custom styles for the next expanding panel */", - " }", - " .expanding-panel-details {", - " .expanding-panel-summary {", - " flex-direction: row;", - " gap: 1rem;", - " padding-inline: 0.5rem;", - "", - " .label-wrapper {}", - " .icon-wrapper {", - " overflow: hidden;", - "", - " .icon {", - " font-size: 1.2rem;", - " }", - " }", - " }", - "", - " &[open] {", - " .expanding-panel-summary {", - " .icon-wrapper {", - " .icon {", - "", - " }", - " }", - " }", - " }", - " + .expanding-panel-content {", - "", - " .inner {", - " /* transform: scaleY(1); */", - " }", - " }", - " }", - " }", - "", - " .expanding-panel-content {", - "", - " .inner {", - " padding: 0.5rem;", - "", - " /* transform: scaleY(0); */", - " /* transition: transform v-bind(animationDurationStr) ease-in-out; */", - " }", - " }", - " }", - "}", - ] - } -} diff --git a/.vscode/srcdev-nuxt3-layout-row.code-snippets b/.vscode/srcdev-nuxt3-layout-row.code-snippets deleted file mode 100644 index 72fd6ebc..00000000 --- a/.vscode/srcdev-nuxt3-layout-row.code-snippets +++ /dev/null @@ -1,12 +0,0 @@ -{ - "SRCDEV Nuxt Layout Row Component HTML Template": { - "description": "Add Layout Row Component HTML Template", - "scope": "vue, html", - "body": [ - "", - "", - "", - "" - ], - } -} diff --git a/.vscode/srcdev-nuxt3-page-layout.code-snippets b/.vscode/srcdev-nuxt3-page-layout.code-snippets deleted file mode 100644 index 593bc021..00000000 --- a/.vscode/srcdev-nuxt3-page-layout.code-snippets +++ /dev/null @@ -1,50 +0,0 @@ -{ - "Bootstrap Nuxt3 page with layout template": { - "description": "Bootstrap Nuxt3 page with layout template", - "scope": "vue, html", - "body": [ - "", - "", - "", - "", - "", - "" - ] - } -} diff --git a/.vscode/srcdev-nuxt3-responsive-header-component.code-snippets b/.vscode/srcdev-nuxt3-responsive-header-component.code-snippets deleted file mode 100644 index b20f448c..00000000 --- a/.vscode/srcdev-nuxt3-responsive-header-component.code-snippets +++ /dev/null @@ -1,403 +0,0 @@ -{ - "SRCDEV Nuxt Responsive Header Component HTML Template": { - "description": "Responsive Header Component HTML Template", - "scope": "vue, html", - "body": [ - "", - " ", - " ", - "" - ] - }, - "SRCDEV Nuxt Responsive Header Component JS Navigation Object Configuration": { - "description": "Responsive Header Component JS Navigation Object Configuration", - "scope": "javascript,typescript", - "body": [ - "import type { ResponsiveHeaderProp } from '@/types/components/responsiveHeader';", - "", - " const responsiveNavLinks = {", - " firstNav: [", - " { name: 'Home', path: '/' },", - " {", - " name: 'Components',", - " childLinksTitle: 'UI Components',", - " childLinks: [", - " { name: 'Animated SVG Text', path: '/ui/animated-svg-text' },", - " { name: 'Slider Gallery', path: '/ui/slider-gallery' },", - " { name: 'Container Glow', path: '/ui/container-glow' },", - " { name: 'Accordian', path: '/ui/accordian' },", - " { name: 'Details', path: '/ui/display-details' },", - " { name: 'Dialogs', path: '/ui/dialog' },", - " ],", - " },", - " {", - " name: 'Layouts',", - " childLinksTitle: 'UI Layouts',", - " childLinks: [", - " { name: 'Layout Row', path: '/ui/layout-row' },", - " { name: 'Layout Grid A', path: '/ui/layout-grid-a' },", - " { name: 'Layout Grid B', path: '/ui/layout-grid-b' },", - " { name: 'Simple Grid', path: '/ui/simple-grid' },", - " { name: 'Masonry Grid Simple', path: '/ui/masonry-grid' },", - " { name: 'Masonry Grid Sorted', path: '/ui/masonry-grid-sorted' },", - " ],", - " },", - " { name: 'About', path: '/' },", - " ],", - " secondNav: [", - " { name: 'Some other link', path: '/' },", - " {", - " name: 'Help',", - " childLinksTitle: 'Find Help',", - " childLinks: [", - " { name: 'FAQs', path: '#' },", - " { name: 'Contact Us', path: '#' },", - " { name: 'Logout', path: '#' },", - " ],", - " },", - " {", - " name: 'Account',", - " childLinksTitle: 'Manage Your Account',", - " childLinks: [", - " { name: 'Your Profile', path: '#' },", - " { name: 'Settings', path: '#' },", - " { name: 'Logout', path: '#' },", - " ],", - " },", - " ],", - " } as ResponsiveHeaderProp;", - "" - ] - }, - "SRCDEV Nuxt Responsive Header Component CSS Configuration": { - "description": "Responsive Header Component CSS Configuration", - "scope": "css", - "body": [ - " /* Modifiers for ResposiveHeader */", - ".navigation.your-scope-class {", - "", - " margin: 12px;", - " border-radius: 8px;", - " background-color: #efefef05;", - " border: 1px solid #efefef75;", - " padding: 12px;", - " max-height: 48px; /* Prevents a flicker of deeper navigation onload */", - "", - " .main-navigation {", - " /* Set up some global vars */", - " --_link-padding-block: 0.8rem;", - " --_link-padding-inline: 0.2rem;", - " --_link-margin-block: 0.1rem;", - " --_link-margin-inline: 0.1rem;", - " --_link-focus-visible-outline-width: 0.2rem;", - " --_link-border-default: 2px solid transparent;", - " --_link-border-bottom-hover: var(--green-08);", - "", - " gap: 60px;", - "", - " .main-navigation-list {", - "", - " &:nth-of-type(1) {", - " gap: 30px;", - " }", - "", - " &:nth-of-type(2) {", - " gap: 30px;", - " }", - "", - " .main-navigation-item {", - "", - " .main-navigation-link {", - " gap: 6px; /* gap for .decorator-icon if present */", - " color: inherit;", - " text-decoration: none;", - " margin-inline-start: 0;", - " padding-block: var(--_link-padding-block);", - " padding-inline: var(--_link-padding-inline);", - " margin-block: var(--_link-margin-block);", - " margin-inline: var(--_link-margin-inline);", - " border-bottom: var(--_link-border-default);", - "", - " &:hover {", - " cursor: pointer;", - " border-bottom-color: var(--_link-border-bottom-hover);", - " }", - "", - " }", - "", - " .main-navigation-details {", - "", - " margin-inline-start: 0;", - "", - " &[open] {", - " .main-navigation-details-summary {", - " border-bottom-color: var(--_link-border-bottom-hover);", - " }", - " }", - "", - " .has-toggle-icon {", - " gap: 6px;", - "", - " .icon {", - " /* color: red; */", - " }", - " }", - "", - " .main-navigation-details-summary {", - " padding-block: var(--_link-padding-block);", - " padding-inline: var(--_link-padding-inline);", - " margin-block: var(--_link-margin-block);", - " margin-inline: var(--_link-margin-inline);", - " border-bottom: var(--_link-border-default);", - "", - " &:hover {", - " border-bottom-color: var(--_link-border-bottom-hover);", - " }", - "", - " .decorator-icon {", - " margin-inline-start: 8px;", - " }", - " }", - "", - " .main-navigation-sub-nav {", - " padding: 12px;", - " border: 1px solid #efefef75;", - " border-radius: 8px;", - " background-color: #000;", - " translate: 0 12px;", - "", - " .main-navigation-sub-nav-list {", - "", - " display: grid;", - " grid-template-columns: repeat(2, auto);", - " gap: 12px;", - "", - " .main-navigation-sub-nav-item {", - " margin-bottom: 8px;", - "", - " &:last-child {", - " margin-bottom: 0;", - " }", - "", - " .main-navigation-sub-nav-link {", - " display: block;", - " text-wrap-mode: nowrap;", - " text-decoration: none;", - " color: inherit;", - " }", - " }", - " }", - " }", - " }", - " }", - " }", - " }", - "", - " .secondary-navigation {", - " gap: 12px;", - "", - " .secondary-navigation-list {", - "", - " .secondary-navigation-item {", - "", - " .secondary-navigation-link {", - " font: inherit;", - " color: inherit;", - "", - " .icon {", - " height: 1.35em;", - " width: 1.35em;", - " }", - " }", - " }", - " }", - "", - " .main-navigation-link {", - " .icon {", - " height: 1.35em;", - " width: 1.35em;", - " }", - " }", - "", - " .overflow-details {", - " padding: 0;", - " margin: 0;", - "", - " .overflow-details-summary {", - " --_icon-zoom: 1;", - " --_icon-size: 20px;", - " --_border-width: 1px;", - " --_outline-width: 1px;", - " --_transition-duration: 0.2s;", - " padding-inline: 5px;", - "", - " border-radius: 4px;", - " border: var(--_border-width) solid #ffffff90;", - " outline: var(--_outline-width) solid #ffffff10;", - " background-color: Canvas;", - " width: var(--_icon-size); /* For reference */", - " height: var(--_icon-size); /* For reference */", - " transition-property: all; /* For reference */", - " transition-timing-function: linear; /* For reference */", - " transition-duration: var(--_transition-duration); /* For reference */", - "", - " &:hover,", - " &:focus-visible {", - " --_icon-zoom: 1.2;", - " outline: var(--_outline-width) solid #ffffff;", - " }", - "", - " .icon {", - " color: liht-dark(#000000, #ffffff);", - " scale: var(--_icon-zoom);", - " width: calc(var(--_icon-size) - var(--_border-width) * 2 - var(--_outline-width) * 2);", - " height: calc(var(--_icon-size) - var(--_border-width) * 2 - var(--_outline-width) * 2);", - " transition-property: opacity, transform; /* For reference */", - " transition-timing-function: linear; /* For reference */", - " transition-duration: var(--_transition-duration); /* For reference */", - " }", - " }", - "", - "", - " .overflow-details-nav {", - " top: 135%;", - " right: 0;", - " background-color: #000;", - " border: 0.1rem solid #ffffff90;", - " border-radius: 8px;", - " padding-block: 0;", - " margin-block-end: -0.1rem;", - " gap: 0.8rem;", - "", - " /* Override for NavigationItems START */", - "", - " .overflow-navigation-wrapper {", - " --overflow-nav-padding-inline: 0.8rem;", - " --overflow-nav-items-gap: 0px;", - " --overflow-nav-items-padding-block: 0.8rem;", - " display: flex;", - " flex-direction: column;", - " gap: var(--overflow-nav-items-gap);", - "", - " .overflow-navigation-list {", - " display: none;", - "", - " &.visible {", - " display: flex;", - " flex-direction: column;", - " gap: var(--overflow-nav-items-gap);", - " min-width: var(--_overflow-navigation-list-min-width, auto);", - " }", - "", - " .overflow-navigation-item {", - " display: none;", - "", - " &.visible {", - " display: block;", - " }", - "", - " .overflow-navigation-link {", - " text-decoration: none;", - " color: inherit;", - " padding-block: var(--overflow-nav-items-padding-block);", - " padding-inline: var(--overflow-nav-padding-inline);", - " display: flex;", - " border-bottom: 0.1rem solid #efefef75;", - " }", - "", - " .overflow-navigation-details {", - " &.expanding-panel {", - " margin-block-end: 0;", - "", - " .expanding-panel-details {", - " .expanding-panel-summary {", - " padding-block: var(--overflow-nav-items-padding-block);", - " padding-inline: var(--overflow-nav-padding-inline);", - " gap: 1rem;", - " /* background-color: red; */", - " border-bottom: 0.1rem solid #efefef75;", - "", - " .label-wrapper {", - " .overflow-navigation-text {", - " text-wrap: nowrap;", - " }", - " }", - " .icon-wrapper {", - " padding: 0;", - " }", - " }", - " &[open] {", - " .expanding-panel-summary {", - " border-bottom: 0.1rem solid transparent;", - " }", - " + .expanding-panel-content {", - " border-bottom: 0.1rem solid #efefef75;", - " .inner {", - " .overflow-navigation-sub-nav-inner {", - " margin-top: var(--overflow-nav-items-gap);", - " }", - " }", - " }", - " }", - " }", - "", - " .expanding-panel-content {", - " border-bottom: 0.1rem solid transparent;", - "", - " .inner {", - " margin-top: 0;", - "", - " .overflow-navigation-sub-nav-inner {", - " margin-top: 0;", - "", - " .overflow-navigation-sub-nav-list {", - " display: flex;", - " flex-direction: column;", - " gap: 2px;", - "", - " .overflow-navigation-sub-nav-item {", - " padding-block: var(--overflow-nav-items-padding-block);", - " padding-inline: var(--overflow-nav-padding-inline);", - "", - " .overflow-navigation-sub-nav-link {", - " text-decoration: none;", - " color: inherit;", - " }", - " }", - " }", - " }", - " }", - " }", - " }", - " }", - " }", - " }", - " }", - " /* Override for NavigationItems END */", - " }", - " }", - " }", - "}" - ] - } -} diff --git a/.vscode/srcdev-nuxt3-tabs-component.code-snippets b/.vscode/srcdev-nuxt3-tabs-component.code-snippets deleted file mode 100644 index 22ec7d36..00000000 --- a/.vscode/srcdev-nuxt3-tabs-component.code-snippets +++ /dev/null @@ -1,139 +0,0 @@ -{ - "SRCDEV Nuxt Tabs Component (Static) HTML Template": { - "description": "Add component with static slots", - "scope": "vue, html", - "body": [ - "", - " ", - " ", - " ", - " ", - " ", - " ", - "", - ] - }, - "SRCDEV Nuxt Tabs Component (Data Driven) HTML Template": { - "description": "Add component with data driven slots", - "scope": "vue, html", - "body": [ - "", - " ", - " ", - " ", - " ", - "", - ], - }, - "SRCDEV Nuxt Tabs Component Script Setup Template": { - "description": "Add bootstrap script setup for TabsCore component", - "scope": "javascript,typescript", - "body": [ - "interface ITabNav {", - " action?: string;", - " name: string;", - " path?: string;", - "}", - "", - "const yourDataObject = [", - " { name: 'Home', path: '/' },", - " { name: 'Layout Row', path: '/ui/layout-row' },", - " { name: 'Dialogs', path: '/ui/dialog' },", - " { name: 'Tabs', path: '/ui/tabs' }", - "];", - ] - }, - "SRCDEV Nuxt Tabs Component CSS Configuration": { - "description": "Add user styles CSS Configuration", - "scope": "css", - "body": [ - ".class-modifier {", - " &.tabs-core {", - " .tabs-list {", - " /* custom css */", - "", - " .nav__hovered {", - " /* custom css */", - " }", - "", - " .nav__active {", - " /* custom css */", - " }", - "", - " .nav__active-indicator {", - " /* custom css */", - " }", - "", - " .tabs-list-item {", - " /* custom css */", - "", - " &:hover {", - " /* custom css */", - " }", - "", - " &[aria-selected='true'] {", - " /* custom css */", - " }", - " }", - "", - " .nav__hovered {", - " /* custom css */", - " }", - "", - " .nav__active {", - " /* custom css */", - " }", - "", - " .nav__active-indicator {", - " /* custom css */", - " }", - "", - " .tabs-list-item {", - " /* custom css */", - "", - " &:hover {", - " /* custom css */", - " }", - "", - " &[aria-selected='true'] {", - " /* custom css */", - " }", - "", - " &.transitioning {", - " /* custom css */", - " }", - " }", - " }", - "", - " .tab-content-wrapper {", - " /* custom css */", - "", - " .tab-content {", - " /* custom css */", - " }", - " }", - " }", - "}", - ] - } -} diff --git a/Claude.md b/Claude.md index e0f746e4..7137cfb6 100644 --- a/Claude.md +++ b/Claude.md @@ -310,7 +310,8 @@ See `.claude/skills/storybook-add-font.md` for the step-by-step process to add a 3. **Style**: Functional base styles with CSS custom properties 4. **Test**: Comprehensive test suite with `mountSuspended` 5. **Document**: Update MCP reference for new patterns -6. **Verify**: Ensure TypeScript strict mode compliance +6. **Snippet**: Create or update `.vscode/srcdev-component-{name}.code-snippets` — required for every new or changed component +7. **Verify**: Ensure TypeScript strict mode compliance ## CI/CD diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 00000000..83b4a5b4 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,210 @@ +# Component Migration Tracker + +Tracks progress toward a fully migrated component library. A component is considered **migrated** when it lives in a tiered folder (`01.atoms` – `05.forms`) and has both supporting documents complete. + +## Legend + +| Symbol | Meaning | +|---|---| +| ✅ | Exists | +| ☐ | Required — not yet created | +| — | N/A — internal/sub-component, not consumed directly | + +**CS** = `CONSUMER-STYLING.md` in the component folder +**SN** = VS Code snippet in `.vscode/srcdev-component-{name}.code-snippets` + +--- + +## 01. Atoms + +| Component | File | CS | SN | +|---|---|---|---| +| EntryAnimation | `animations/entry/EntryAnimation.vue` | ☐ | ☐ | +| ScrollRevealFrame | `animations/scroll-reveal-frame/ScrollRevealFrame.vue` | ☐ | ☐ | +| ScrollRevealImage | `animations/scroll-reveal-image/ScrollRevealImage.vue` | ☐ | ☐ | +| BannerVideo | `banner-video/BannerVideo.vue` | ☐ | ☐ | +| CardCore | `card/CardCore.vue` | ✅ | ☐ | +| ContentColumns2 | `content-wrappers/content-columns-2/ContentColumns2.vue` | ☐ | ☐ | +| ContentWidth | `content-wrappers/content-width/ContentWidth.vue` | ☐ | ☐ | +| LayoutGridByCols | `content-wrappers/layout-grid/layout-grid-by-cols/` | ☐ | ☐ | +| LayoutGridByWidth | `content-wrappers/layout-grid/layout-grid-by-width/` | ☐ | ☐ | +| DisplayAvatar | `display-avatar/DisplayAvatar.vue` | ☐ | ☐ | +| DisplayDialog | `display-dialog/DisplayDialog.vue` | ✅ | ✅ `dialog` | +| DisplayPill | `display-pill/DisplayPill.vue` | ☐ | ☐ | +| GlassPanel | `glass-panel/GlassPanel.vue` | ☐ | ☐ | +| AutoGrid | `grids/data-grid/AutoGrid.vue` | ☐ | ☐ | +| GridStack | `grids/grid-stack/GridStack.vue` | ☐ | ☐ | +| PageRow | `page-row/PageRow.vue` | ✅ | ☐ | +| DisplayPrompt | `prompt/DisplayPrompt.vue` | ☐ | ✅ `display-prompt` | +| DisplayQrCode | `qr-code/DisplayQrCode.vue` | ☐ | ☐ | +| TextBlock | `text-block/TextBlock.vue` | ☐ | ☐ | +| EyebrowText | `text-blocks/eyebrow-text/EyebrowText.vue` | ☐ | ☐ | +| HeroText | `text-blocks/hero-text/HeroText.vue` | ☐ | ☐ | +| LinkText | `text-blocks/link-text/LinkText.vue` | ☐ | ☐ | +| DisplayToast | `toast/DisplayToast.vue` | ☐ | ✅ `display-toast` | +| DisplayToastProvider | `toast/DisplayToastProvider.vue` | ☐ | ✅ `display-toast` | + +--- + +## 02. Molecules + +| Component | File | CS | SN | +|---|---|---|---| +| ActionMenu | `action-menu/ActionMenu.vue` | ✅ | ☐ | +| ActionMenuItemCore | `action-menu/ActionMenuItemCore.vue` | — | — | +| AlertContent | `alert-content/AlertContent.vue` | — | — | +| AlertContentInner | `alert-content/AlertContentInner.vue` | — | — | +| AlertMaskedContent | `alert-masked-content/AlertMaskedContent.vue` | — | — | +| ContactSection | `contact-section/ContactSection.vue` | ☐ | ☐ | +| DisplayChip | `display-chip/DisplayChip.vue` | ☐ | ☐ | +| AccordianCore | `expandable/accordian/AccordianCore.vue` | ☐ | ☐ | +| ExpandingPanel | `expandable/expanding-panel/ExpandingPanel.vue` | ☐ | ☐ | +| NavigationHorizontal | `navigation/navigation-horizontal/NavigationHorizontal.vue` | ☐ | ☐ | +| SiteNavigation | `navigation/site-navigation/SiteNavigation.vue` | ☐ | ☐ | +| TabNavigation | `navigation/tab-navigation/TabNavigation.vue` | ✅ | ☐ | +| PriceList | `price-list/PriceList.vue` | ☐ | ☐ | +| ProfileSection | `profile-section/ProfileSection.vue` | ☐ | ☐ | +| CaptureQrCode | `qr-code/CaptureQrCode.vue` | ☐ | ☐ | +| DecodeQrCode | `qr-code/DecodeQrCode.vue` | ☐ | ☐ | +| SamaritanPrompt | `samaritan-prompt/SamaritanPrompt.vue` | ☐ | ☐ | +| SamaritanPromptMixed | `samaritan-prompt/SamaritanPromptMixed.vue` | ☐ | ☐ | +| SocialIconsList | `social-icons-list/SocialIconsList.vue` | ☐ | ☐ | +| StepperList | `stepper-list/StepperList.vue` | ✅ | ☐ | + +--- + +## 03. Organisms + +| Component | File | CS | SN | +|---|---|---|---| +| ColourFinder | `colour-finder/ColourFinder.vue` | ☐ | ☐ | +| SliderGallery | `image-galleries/slider-gallery/SliderGallery.vue` | ☐ | ☐ | +| ServicesCard | `services/services-card/ServicesCard.vue` | ☐ | ☐ | +| ServicesCardGrid | `services/services-grids/ServicesCardGrid.vue` | ☐ | ☐ | +| ServicesSectionGrid | `services/services-grids/ServicesSectionGrid.vue` | ☐ | ☐ | +| ServicesSection | `services/services-section/ServicesSection.vue` | ☐ | ☐ | + +--- + +## 04. Templates + +| Component | File | CS | SN | +|---|---|---|---| +| PageHeroHighlights | `page-hero-highlights/PageHeroHighlights.vue` | ☐ | ☐ | +| PageHeroHighlightsHeader | `page-hero-highlights/PageHeroHighlightsHeader.vue` | — | — | + +--- + +## 05. Forms + +### Form Structure + +| Component | File | CS | SN | +|---|---|---|---| +| FormWrapper | `form-wrapper/FormWrapper.vue` | ☐ | ☐ | +| FormFieldset | `form-fieldset/FormFieldset.vue` | ☐ | ☐ | +| FormField | `form-field/FormField.vue` | ☐ | ☐ | +| InputError | `form-errors/InputError.vue` | — | — | +| InputLabel | `input-label/InputLabel.vue` | — | — | +| InputDescription | `input-description/InputDescription.vue` | — | — | + +### Button + +| Component | File | CS | SN | +|---|---|---|---| +| InputButtonCore | `input-button/InputButtonCore.vue` | ☐ | ☐ | + +### Text Inputs + +| Component | File | CS | SN | +|---|---|---|---| +| InputTextCore | `input-text/InputTextCore.vue` | ☐ | ☐ | +| InputTextWithLabel | `input-text/variants/InputTextWithLabel.vue` | ☐ | ☐ | +| InputTextAsNumberWithLabel | `input-text/variants/InputTextAsNumberWithLabel.vue` | ☐ | ☐ | +| InputPasswordWithLabel | `input-text/variants/InputPasswordWithLabel.vue` | ☐ | ☐ | +| InputTextareaCore | `input-textarea/InputTextareaCore.vue` | ☐ | ☐ | +| InputTextareaWithLabel | `input-textarea/variants/InputTextareaWithLabel.vue` | ☐ | ☐ | +| InputCopyCore | `input-copy/InputCopyCore.vue` | ☐ | ☐ | + +### Select & Number + +| Component | File | CS | SN | +|---|---|---|---| +| InputSelectCore | `input-select/InputSelectCore.vue` | ☐ | ☐ | +| InputSelectWithLabel | `input-select/variants/InputSelectWithLabel.vue` | ☐ | ☐ | +| InputNumberCore | `input-number/InputNumberCore.vue` | ☐ | ☐ | +| InputNumberDefault | `input-number/variants/InputNumberDefault.vue` | ☐ | ☐ | + +### Checkboxes & Radios + +| Component | File | CS | SN | +|---|---|---|---| +| InputCheckboxRadioCore | `input-checkbox-radio/InputCheckboxRadioCore.vue` | — | — | +| InputCheckboxRadioButton | `input-checkbox-radio/InputCheckboxRadioButton.vue` | — | — | +| InputCheckboxRadioWithLabel | `input-checkbox-radio/InputCheckboxRadioWithLabel.vue` | — | — | +| SingleCheckbox | `input-checkbox/SingleCheckbox.vue` | ☐ | ☐ | +| MultipleCheckboxes | `input-checkbox/MultipleCheckboxes.vue` | ☐ | ☐ | +| MultipleRadiobuttons | `input-radio/MultipleRadiobuttons.vue` | ☐ | ☐ | + +### Toggles & Range + +| Component | File | CS | SN | +|---|---|---|---| +| ToggleSwitchCore | `toggle-switch/ToggleSwitchCore.vue` | ☐ | ☐ | +| ToggleSwitchWithLabel | `toggle-switch/variants/ToggleSwitchWithLabel.vue` | ☐ | ☐ | +| ToggleSwitchWithLabelInline | `toggle-switch/variants/ToggleSwitchWithLabelInline.vue` | ☐ | ☐ | +| TripleToggleSwitchCore | `triple-toggle-switch/TripleToggleSwitchCore.vue` | ☐ | ☐ | +| InputRangeCore | `input-range/InputRangeCore.vue` | ☐ | ☐ | +| InputRangeDefault | `input-range/variants/InputRangeDefault.vue` | ☐ | ☐ | +| InputRangeFancyCore | `input-range-fancy/InputRangeFancyCore.vue` | ☐ | ☐ | +| InputRangeFancyWithLabel | `input-range-fancy/InputRangeFancyWithLabel.vue` | ☐ | ☐ | + +### Utility + +| Component | File | CS | SN | +|---|---|---|---| +| PendingEffect | `pending-effect/PendingEffect.vue` | — | — | + +--- + +## Not Yet Migrated + +These components still live in the root `app/components/` folder and need to be assessed, tiered, and moved before CS and SN work can begin. + +| Component | File | Notes | +|---|---|---| +| AlertMaskCore | `alert-mask/AlertMaskCore.vue` | SVG mask utility — may stay internal | +| AnimatedSvgText | `animated-svg-text/AnimatedSvgText.vue` | | +| CanvasSwitcher | `canvas-switcher/CanvasSwitcher.vue` | | +| CarouselBasic | `carousels/CarouselBasic.vue` | | +| CarouselFlip | `carousels/CarouselFlip.vue` | | +| CarouselInfinite | `carousels/CarouselInfinite.vue` | | +| ClipElement | `clip-element/ClipElement.vue` | | +| ClippedPanel | `clipped-panels/ClippedPanel.vue` | | +| ContainerGlowCore | `container-glow/ContainerGlowCore.vue` | | +| DeepExpandingMenu | `deep-expanding-menu/DeepExpandingMenu.vue` | | +| DeepExpandingMenuOld | `deep-expanding-menu/DeepExpandingMenuOld.vue` | Likely deprecated — review before migrating | +| DisplayBanner | `display-banner/DisplayBanner.vue` | | +| DisplayDetailsCore | `display-details/DisplayDetailsCore.vue` | | +| DisplayThemeSwitch | `display-theme-switch/DisplayThemeSwitch.vue` | | +| DisplayTooltip | `display-tooltip/DisplayTooltip.vue` | | +| DisplayTooltipDefined | `display-tooltip/DisplayTooltipDefined.vue` | | +| GlowingBorder | `glowing-border/GlowingBorder.vue` | | +| LayoutGridA | `layout-grids/LayoutGridA.vue` | | +| LayoutGridB | `layout-grids/LayoutGridB.vue` | | +| MagneticNavigation | `magnetic-navigation/MagneticNavigation.vue` | | +| MarqueeScroller | `marquee-scroller/MarqueeScroller.vue` | | +| MasonryGrid | `masonry-grid/MasonryGrid.vue` | | +| MasonryGridOrdered | `masonry-grid-ordered/MasonryGridOrdered.vue` | | +| MasonryGridOrderedGridExperiment | `masonry-grid-ordered/MasonryGridOrderedGridExperiment.vue` | Experimental — review before migrating | +| MasonryGridSorted | `masonry-grid-sorted/MasonryGridSorted.vue` | | +| SectionParallax | `parallax/SectionParallax.vue` | | +| PopOver | `pop-over/PopOver.vue` | | +| ResponsiveHeader | `responsive-header/ResponsiveHeader.vue` | | +| NavigationItems | `responsive-header/NavigationItems.vue` | Internal sub-component of ResponsiveHeader | +| RotatingCarouselImage | `rotating-carousel/RotatingCarouselImage.vue` | | +| SkipLinks | `skip-links/SkipLinks.vue` | | +| TabsCore | `tabs/TabsCore.vue` | | +| HeaderBlock | `typography/HeaderBlock.vue` | | +| UiBlockDecorated | `ui-block-decorated/UiBlockDecorated.vue` | | +| WipeAwayVertical | `view-timeline/WipeAwayVertical.vue` | | diff --git a/app/components/01.atoms/display-dialog/DisplayDialog.vue b/app/components/01.atoms/display-dialog/DisplayDialog.vue index 02a50ae8..485c9747 100644 --- a/app/components/01.atoms/display-dialog/DisplayDialog.vue +++ b/app/components/01.atoms/display-dialog/DisplayDialog.vue @@ -2,7 +2,9 @@
-
-
+
+
@@ -49,6 +52,8 @@ From cb145119f5c05137acbf1b3e0c607df0c9945468 Mon Sep 17 00:00:00 2001 From: Simon Cornforth Date: Sat, 20 Jun 2026 00:46:57 +0100 Subject: [PATCH 2/3] Tests updated --- .../01.atoms/display-dialog/tests/DisplayDialog.spec.ts | 8 ++++---- .../tests/__snapshots__/DisplayDialog.spec.ts.snap | 6 +++--- app/composables/useDialogControls.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts b/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts index beaa8891..2eb17880 100644 --- a/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts +++ b/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts @@ -80,20 +80,20 @@ describe("DisplayDialog", () => { // ─── role ───────────────────────────────────────────────────────────────── - it("has role='dialog' by default", async () => { + it("has no explicit role by default — implicit from element", async () => { const wrapper = await mountSuspended(DisplayDialog, { props: { dataDialogId: "test" }, }); - expect(wrapper.attributes("role")).toBe("dialog"); + expect(wrapper.attributes("role")).toBeUndefined(); }); it.each(["dialog", "modal", "confirm", "fullscreen"] as const)( - "has role='dialog' for variant='%s'", + "has no explicit role for variant='%s' — implicit from element", async (variant) => { const wrapper = await mountSuspended(DisplayDialog, { props: { dataDialogId: "test", variant }, }); - expect(wrapper.attributes("role")).toBe("dialog"); + expect(wrapper.attributes("role")).toBeUndefined(); } ); diff --git a/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap b/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap index 3cef2def..5cdea120 100644 --- a/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap +++ b/app/components/01.atoms/display-dialog/tests/__snapshots__/DisplayDialog.spec.ts.snap @@ -1,13 +1,13 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`DisplayDialog > renders correct HTML structure with all slots 1`] = ` -" +"
-
+

Title

-
+

Content

diff --git a/app/composables/useDialogControls.ts b/app/composables/useDialogControls.ts index f4cce4df..502a308b 100644 --- a/app/composables/useDialogControls.ts +++ b/app/composables/useDialogControls.ts @@ -3,7 +3,7 @@ type DialogCallbacks = { onCancel?: () => void; }; -export const useDialogControls = (config: Record) => { +export const useDialogControls = (config: Record = {} as Record) => { const dialogsConfig = reactive>( Object.fromEntries(Object.keys(config).map((id) => [id, false])) as Record ); From 3a9d149569a96b62f983bce0ea62110962ae07c9 Mon Sep 17 00:00:00 2001 From: Simon Cornforth Date: Sat, 20 Jun 2026 01:06:49 +0100 Subject: [PATCH 3/3] Dialog themes added - pr updates --- .../01.atoms/display-dialog/DisplayDialog.vue | 29 ++++++++++----- .../tests/DisplayDialog.spec.ts | 35 ++++++++++++++----- app/composables/useBodyLock.ts | 21 +++++++++++ 3 files changed, 67 insertions(+), 18 deletions(-) create mode 100644 app/composables/useBodyLock.ts diff --git a/app/components/01.atoms/display-dialog/DisplayDialog.vue b/app/components/01.atoms/display-dialog/DisplayDialog.vue index 485c9747..bffe01af 100644 --- a/app/components/01.atoms/display-dialog/DisplayDialog.vue +++ b/app/components/01.atoms/display-dialog/DisplayDialog.vue @@ -84,9 +84,6 @@ const open = defineModel(); const closeDialog = () => { open.value = false; - if (props.lockViewport) { - document.body.classList.remove("lock"); - } }; const slots = useSlots(); @@ -94,15 +91,20 @@ const hasTitle = computed(() => !!slots.dialogTitle); const hasContent = computed(() => !!slots.dialogContent); const hasFooter = computed(() => !!(slots.actionButtonLeft || slots.actionButtonRight)); +const { lock, unlock } = useBodyLock(); +let hasLocked = false; + onMounted(() => { if (props.lockViewport) { - document.body.classList.add("lock"); + lock(); + hasLocked = true; } }); onUnmounted(() => { - if (props.lockViewport) { - document.body.classList.remove("lock"); + if (hasLocked) { + unlock(); + hasLocked = false; } }); @@ -126,9 +128,18 @@ onUnmounted(() => { --_header-button-border: var(--display-dialog-header-button-border, 0.1rem solid transparent); --_header-button-border-radius: var(--display-dialog-header-button-border-radius, 0.4rem); --_header-button-outline: var(--display-dialog-header-button-outline, 0.1rem solid transparent); - --_header-button-border-hover: var(--display-dialog-header-button-border-hover, 0.1rem solid light-dark(var(--slate-08), var(--slate-04))); - --_header-button-outline-hover: var(--display-dialog-header-button-outline-hover, 0.1rem solid light-dark(var(--slate-08), var(--slate-04))); - --_header-button-icon-color: var(--display-dialog-header-button-icon-color, light-dark(var(--slate-09), var(--slate-02))); + --_header-button-border-hover: var( + --display-dialog-header-button-border-hover, + 0.1rem solid light-dark(var(--slate-08), var(--slate-04)) + ); + --_header-button-outline-hover: var( + --display-dialog-header-button-outline-hover, + 0.1rem solid light-dark(var(--slate-08), var(--slate-04)) + ); + --_header-button-icon-color: var( + --display-dialog-header-button-icon-color, + light-dark(var(--slate-09), var(--slate-02)) + ); --_header-button-icon-size: var(--display-dialog-header-button-icon-size, 2.4rem); --_content-padding: var(--display-dialog-content-padding, 1.2rem); diff --git a/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts b/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts index 2eb17880..376c274c 100644 --- a/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts +++ b/app/components/01.atoms/display-dialog/tests/DisplayDialog.spec.ts @@ -1,5 +1,4 @@ -import { describe, it, expect, vi } from "vitest"; -import { nextTick } from "vue"; +import { describe, it, expect, vi, afterEach } from "vitest"; import { mountSuspended } from "@nuxt/test-utils/runtime"; import DisplayDialog from "../DisplayDialog.vue"; @@ -13,6 +12,11 @@ vi.mock("focus-trap-vue", () => ({ })); describe("DisplayDialog", () => { + afterEach(() => { + document.body.classList.remove("lock"); + delete document.body.dataset.lockCount; + }); + // ─── Mount ──────────────────────────────────────────────────────────────── it("mounts without error", async () => { @@ -238,11 +242,11 @@ describe("DisplayDialog", () => { // ─── Body viewport lock ─────────────────────────────────────────────────── it("adds lock class to body on mount when lockViewport=true", async () => { - await mountSuspended(DisplayDialog, { + const wrapper = await mountSuspended(DisplayDialog, { props: { dataDialogId: "test", lockViewport: true }, }); expect(document.body.classList.contains("lock")).toBe(true); - document.body.classList.remove("lock"); + wrapper.unmount(); }); it("does not add lock class to body when lockViewport=false", async () => { @@ -252,13 +256,26 @@ describe("DisplayDialog", () => { expect(document.body.classList.contains("lock")).toBe(false); }); - it("removes lock class from body when close button is clicked", async () => { - document.body.classList.add("lock"); + it("removes lock class from body on unmount when lockViewport=true", async () => { const wrapper = await mountSuspended(DisplayDialog, { - props: { dataDialogId: "test", modelValue: true, lockViewport: true }, + props: { dataDialogId: "test", lockViewport: true }, }); - await wrapper.find('[data-test-id="display-dialog-header-close"]').trigger("click"); - await nextTick(); + expect(document.body.classList.contains("lock")).toBe(true); + wrapper.unmount(); + expect(document.body.classList.contains("lock")).toBe(false); + }); + + it("does not remove lock when a second locking dialog is still open", async () => { + const wrapper1 = await mountSuspended(DisplayDialog, { + props: { dataDialogId: "a", lockViewport: true }, + }); + const wrapper2 = await mountSuspended(DisplayDialog, { + props: { dataDialogId: "b", lockViewport: true }, + }); + expect(document.body.classList.contains("lock")).toBe(true); + wrapper2.unmount(); + expect(document.body.classList.contains("lock")).toBe(true); + wrapper1.unmount(); expect(document.body.classList.contains("lock")).toBe(false); }); }); diff --git a/app/composables/useBodyLock.ts b/app/composables/useBodyLock.ts new file mode 100644 index 00000000..ee76a736 --- /dev/null +++ b/app/composables/useBodyLock.ts @@ -0,0 +1,21 @@ +const getCount = (): number => Number(document.body.dataset.lockCount ?? 0); + +const setCount = (n: number) => { + if (n > 0) document.body.dataset.lockCount = String(n); + else delete document.body.dataset.lockCount; +}; + +export const useBodyLock = () => { + const lock = () => { + setCount(getCount() + 1); + document.body.classList.add("lock"); + }; + + const unlock = () => { + const next = Math.max(0, getCount() - 1); + setCount(next); + if (next === 0) document.body.classList.remove("lock"); + }; + + return { lock, unlock }; +};