diff --git a/.gitignore b/.gitignore index c2facee..111e996 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ test-results/ tests/figma-diffs/ tests/figma-screenshots/ + +# Generated demo screenshots (kept locally for visual review) +screenshots/ diff --git a/demo/index.html b/demo/index.html index f4a04a6..991ac2c 100644 --- a/demo/index.html +++ b/demo/index.html @@ -9,6 +9,7 @@ + -
-
+ +

VMK UI Kit

Demo page placeholder. Component sections will be added here.

@@ -82,7 +75,24 @@ @@include('./partials/timeline.html') @@include('./partials/loader.html')
-
+ diff --git a/demo/partials/advanced-select.html b/demo/partials/advanced-select.html index a1697b7..63b4828 100644 --- a/demo/partials/advanced-select.html +++ b/demo/partials/advanced-select.html @@ -1,11 +1,10 @@

Advanced select

-
+
diff --git a/demo/partials/file-upload.html b/demo/partials/file-upload.html index ea350ff..43302a2 100644 --- a/demo/partials/file-upload.html +++ b/demo/partials/file-upload.html @@ -23,7 +23,7 @@

File attachment cards

-
+
PDF diff --git a/demo/partials/palette.html b/demo/partials/palette.html index 8d1d481..126be62 100644 --- a/demo/partials/palette.html +++ b/demo/partials/palette.html @@ -2,49 +2,49 @@

Palette

Neutral

-
-
0 #FFFFFF
-
25 #FCFCFC
-
50 #F7F7F8
-
100 #F0F0F0
-
200 #E1E2E3
-
300 #D9D9D9
-
400 #B7BABE
-
500 #9CA3AF
-
600 #82868D
-
700 #6B7280
-
800 #3F4145
-
900 #1F1F1F
+
+
0#FFFFFF
+
25#FCFCFC
+
50#F7F7F8
+
100#F0F0F0
+
200#E1E2E3
+
300#D9D9D9
+
400#B7BABE
+
500#9CA3AF
+
600#82868D
+
700#6B7280
+
800#3F4145
+
900#1F1F1F

Mint

-
-
50 #F2FAFB
-
200 #DFF1F4
-
300 #D9EEF2
-
600 #7EC0C9
-
800 #316873
-
900 #1F4B52
+
+
50#F2FAFB
+
200#DFF1F4
+
300#D9EEF2
+
600#7EC0C9
+
800#316873
+
900#1F4B52

Yellow

-
-
50 #FEFEF0
-
100 #FDFCB8
-
300 #FCFD76
-
400 #F0F166
-
500 #E8E94A
+
+
50#FEFEF0
+
100#FDFCB8
+
300#FCFD76
+
400#F0F166
+
500#E8E94A

Semantic

-
-
Success bg #DDF3E4
-
Success text #1E7A46
-
Warning bg #FEF3E5
-
Warning text #B45309
-
Info bg #EAF4FE
-
Info text #2563A6
-
Danger bg #FBE4E2
-
Danger text #D64545
+
+
Success bg#DDF3E4
+
Success text#1E7A46
+
Warning bg#FEF3E5
+
Warning text#B45309
+
Info bg#EAF4FE
+
Info text#2563A6
+
Danger bg#FBE4E2
+
Danger text#D64545
diff --git a/package.json b/package.json index 7956158..47389f9 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,8 @@ "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", "test:visual": "playwright test --grep @visual", + "test:screenshots": "playwright test tests/demo-screenshot.spec.js", + "screenshots": "playwright test tests/demo-screenshot.spec.js", "test:vue-adapter": "npm run build:vue && npm run build:example:vue", "build:example:vue": "npm --prefix examples/vue run build" }, diff --git a/src/js/components/advanced-select.js b/src/js/components/advanced-select.js index d3d0ecc..b36314b 100644 --- a/src/js/components/advanced-select.js +++ b/src/js/components/advanced-select.js @@ -67,6 +67,11 @@ const wrapper = document.createElement("div"); wrapper.className = "advanced-select-container"; input.parentElement?.insertBefore(wrapper, input); + + if(!input.classList.contains("input")) { + input.classList.add("input"); + } + wrapper.append(input); const container = document.createElement("div"); diff --git a/src/scss/components/_advanced-select.scss b/src/scss/components/_advanced-select.scss index 0bee752..e8077a9 100644 --- a/src/scss/components/_advanced-select.scss +++ b/src/scss/components/_advanced-select.scss @@ -9,6 +9,48 @@ display: inline-flex; width: 100%; max-width: 320px; + + input { + width: 100%; + min-height: 48px; + padding: $space-3 $space-8 $space-3 $space-4; + font-family: $font-family-base; + font-size: 15px; + font-weight: $font-weight-medium; + line-height: 1.25; + color: $neutral-900; + background-color: $neutral-0; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%236B7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right $space-3 center; + background-size: 20px 20px; + border: $border-width-base $border-style-base $neutral-300; + border-radius: $radius-lg; + transition: + border-color $motion-base $motion-ease, + box-shadow $motion-base $motion-ease, + background-color $motion-base $motion-ease; + + &:hover:not(:disabled):not([readonly]) { + border-color: $mint-600; + } + + &:focus { + outline: none; + border-color: $mint-600; + box-shadow: 0 0 0 3px #{rgba($mint-600, 0.24)}; + background-color: $neutral-0; + } + + &::placeholder { + color: $neutral-600; + font-weight: $font-weight-regular; + } + } + + .input { + padding-right: $space-8; + } } .advanced-select { diff --git a/src/scss/components/_background.scss b/src/scss/components/_background.scss index 9f336f1..70fef63 100644 --- a/src/scss/components/_background.scss +++ b/src/scss/components/_background.scss @@ -11,8 +11,9 @@ min-height: 100vh; background-color: $neutral-50; background-repeat: no-repeat; - background-position: center; + background-position: center top; background-size: cover; + background-attachment: fixed; overflow: hidden; // Variant + device combinations @@ -40,16 +41,54 @@ background-image: url("/assets/backgrounds/shape-mobile.svg"); } - // Fixed to viewport - &.is-fixed { - position: fixed; - inset: 0; - z-index: -1; - } - // Inline preview sizing helper used by the Figma comparison page &.is-preview { min-height: auto; height: 100%; + background-attachment: scroll; } } + +// Fixed background layer used by the demo page so the gradient stays pinned +// while the content scrolls over it. +.vmk-bg-layer { + position: fixed; + inset: 0; + z-index: -1; + background-color: $neutral-50; + background-repeat: no-repeat; + background-position: center top; + background-size: cover; + + &[data-variant="gradient"][data-device="desktop"] { + background-image: url("/assets/backgrounds/gradient-desktop.svg"); + } + + &[data-variant="gradient"][data-device="tablet"] { + background-image: url("/assets/backgrounds/gradient-tablet.svg"); + } + + &[data-variant="gradient"][data-device="mobile"] { + background-image: url("/assets/backgrounds/gradient-mobile.svg"); + } + + &[data-variant="shape"][data-device="desktop"] { + background-image: url("/assets/backgrounds/shape-desktop.svg"); + } + + &[data-variant="shape"][data-device="tablet"] { + background-image: url("/assets/backgrounds/shape-tablet.svg"); + } + + &[data-variant="shape"][data-device="mobile"] { + background-image: url("/assets/backgrounds/shape-mobile.svg"); + } +} + +// Very subtle parallax drift of the background during scroll. +// It runs in JS by updating --vmk-bg-y on the background layer. +.vmk-bg-layer.is-parallax { + will-change: transform; + transform: translateY(calc(var(--vmk-bg-y, 0px) * -0.05)); + transition: transform 0.1s linear; +} diff --git a/src/scss/components/_file-upload.scss b/src/scss/components/_file-upload.scss index 725d783..b34e0de 100644 --- a/src/scss/components/_file-upload.scss +++ b/src/scss/components/_file-upload.scss @@ -6,7 +6,7 @@ @use "../kit-deps" as *; -$file-attachment-size: 420px; +$file-attachment-size: 160px; .file-upload-panel { width: 100%; @@ -118,7 +118,15 @@ .file-upload-preview { display: grid; grid-template-columns: repeat(auto-fill, minmax($file-attachment-size, 1fr)); - gap: $space-7; + gap: $space-4; +} + +.file-attachment-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax($file-attachment-size, 1fr)); + gap: $space-4; + width: 100%; + max-width: 760px; } .file-upload-preview-item, @@ -131,30 +139,31 @@ height: $file-attachment-size; padding: 0; border: 0; - border-radius: 40px; + border-radius: $radius-xl; background: $neutral-0; color: $neutral-900; box-sizing: border-box; overflow: hidden; + box-shadow: 0 1px 2px rgba($neutral-900, 0.06); } .file-upload-preview-remove, .file-attachment-remove { position: absolute; - top: $space-4; - right: $space-4; + top: $space-2; + right: $space-2; z-index: 3; display: inline-flex; align-items: center; justify-content: center; - width: 32px; - height: 32px; + width: 24px; + height: 24px; padding: 0; border: 0; border-radius: $radius-full; background: transparent; color: $neutral-700; - font-size: 24px; + font-size: 16px; cursor: pointer; transition: color $motion-base $motion-ease, background-color $motion-base $motion-ease; @@ -186,8 +195,8 @@ } svg { - width: 56px; - height: 56px; + width: 40px; + height: 40px; } } @@ -196,13 +205,13 @@ display: inline-flex; align-items: center; justify-content: center; - width: 72px; - height: 72px; - border-radius: $radius-lg; + width: 48px; + height: 48px; + border-radius: $radius-md; background: $neutral-100; color: $neutral-600; font-family: $font-family-base; - font-size: 14px; + font-size: 11px; font-weight: $font-weight-bold; text-transform: uppercase; } @@ -213,10 +222,10 @@ z-index: 1; display: block; margin-top: auto; - padding: 0 $space-5 $space-5; + padding: 0 $space-3 $space-3; color: $neutral-900; font-family: $font-family-base; - font-size: 18px; + font-size: 13px; font-weight: $font-weight-semibold; line-height: 1.3; white-space: nowrap; @@ -229,10 +238,11 @@ position: relative; z-index: 1; display: block; - margin-top: $space-1; + margin-top: 0; + padding: 0 $space-3 $space-3; color: $neutral-600; font-family: $font-family-base; - font-size: 12px; + font-size: 11px; line-height: 1.3; } @@ -243,18 +253,18 @@ align-items: flex-start; justify-content: space-between; gap: $space-2; - padding: $space-4 $space-4 $space-3; + padding: $space-3 $space-3 $space-2; } .file-attachment-format { display: inline-flex; align-items: center; padding: $space-1 $space-2; - border-radius: $radius-md; + border-radius: $radius-sm; background: $neutral-100; color: $neutral-700; font-family: $font-family-base; - font-size: 24px; + font-size: 13px; font-weight: $font-weight-bold; text-transform: uppercase; line-height: 1; @@ -313,13 +323,13 @@ .file-attachment-visual, .file-upload-preview-visual { color: $danger-text; - font-size: 56px; + font-size: 40px; &::before { content: ""; position: absolute; - width: 112px; - height: 112px; + width: 72px; + height: 72px; border-radius: $radius-full; background: $neutral-0; z-index: -1; diff --git a/src/scss/demo.scss b/src/scss/demo.scss index 1c81634..d2abd0e 100644 --- a/src/scss/demo.scss +++ b/src/scss/demo.scss @@ -12,29 +12,76 @@ } .demo-section { - margin-bottom: $space-8; + margin-bottom: $space-12; h2 { font-size: 28px; font-weight: $font-weight-bold; - margin-bottom: $space-4; + margin-bottom: $space-5; } h3 { font-size: 18px; font-weight: $font-weight-semibold; - margin-top: $space-6; - margin-bottom: $space-3; + margin-top: $space-8; + margin-bottom: $space-4; color: $neutral-700; } + + > *:last-child { + margin-bottom: 0; + } + + // Keep headings visually separated from the previous component block. + h3:first-child { + margin-top: 0; + } } .demo-row { display: flex; flex-wrap: wrap; - align-items: center; + align-items: flex-start; + gap: $space-4; + margin-bottom: $space-5; + + &:last-child { + margin-bottom: 0; + } +} + +.color-card { + display: flex; + flex-direction: column; + gap: $space-1; + min-width: 80px; + padding: $space-2; + border-radius: $radius-md; + background: $neutral-0; + box-shadow: 0 1px 2px rgba($neutral-900, 0.06); + font-size: 11px; + color: $neutral-700; + + .color-card-swatch { + width: 72px; + height: 72px; + border-radius: $radius-sm; + box-shadow: inset 0 0 0 1px rgba($neutral-900, 0.08); + } + + .color-card-name { + font-weight: $font-weight-semibold; + color: $neutral-900; + } + + .color-card-hex { + font-family: "IBM Plex Mono", monospace; + text-transform: uppercase; + } +} + +.demo-row-palette { gap: $space-3; - margin-bottom: $space-3; } .swatch { @@ -45,6 +92,7 @@ color: $neutral-700; .swatch-color { + display: block; width: 64px; height: 64px; border-radius: $radius-md; @@ -84,3 +132,11 @@ &.radius-2xl { border-radius: $radius-2xl; } &.radius-full { border-radius: $radius-full; } } + +// Vue component mounts get a subtle outline so sections are visible in screenshots. +.vue-mount { + width: 100%; + padding: $space-2; + border-radius: $radius-md; + background: rgba($neutral-900, 0.02); +} diff --git a/src/vue/components/GnDescriptionList.js b/src/vue/components/GnDescriptionList.js index 644dc69..ab29126 100644 --- a/src/vue/components/GnDescriptionList.js +++ b/src/vue/components/GnDescriptionList.js @@ -25,7 +25,7 @@ ...attrs, class: cx("description-list", { "description-list-compact": props.compact }, attrs.class) }, props.items.map(item => h("div", { class: "description-list-row" }, [ - h("dt", { class: "description-list-term" }, item.term || item.label), + h("dt", { class: "description-list-term" }, item.term || item.label || item.key), h("dd", { class: cx("description-list-value", { "description-list-value-muted": item.muted }) }, slots[item.key]?.({ item }) || item.value) diff --git a/tests/demo-screenshot.spec.js b/tests/demo-screenshot.spec.js new file mode 100644 index 0000000..beb5b60 --- /dev/null +++ b/tests/demo-screenshot.spec.js @@ -0,0 +1,52 @@ +const { test } = require("@playwright/test"); +const fs = require("fs"); +const path = require("path"); + +const SCREENSHOT_DIR = path.join(__dirname, "..", "screenshots"); + +// Sections that are worth reviewing manually after each visual pass. +const SECTIONS = [ + { name: "palette", label: "Palette" }, + { name: "buttons", label: "Buttons" }, + { name: "forms", label: "Forms" }, + { name: "alerts", label: "Alerts" }, + { name: "badges", label: "Badges" }, + { name: "cards", label: "Cards" }, + { name: "tables", label: "Tables" }, + { name: "file-upload", label: "File Upload" }, + { name: "advanced-select", label: "Advanced Select" }, + { name: "description-list", label: "Description List" }, + { name: "page-header", label: "Page Header" }, + { name: "backgrounds", label: "Backgrounds" } +]; + +test.describe("@screenshot", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/"); + await page.waitForTimeout(1500); + }); + + test("capture full demo page", async ({ page }) => { + fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); + await page.screenshot({ + path: path.join(SCREENSHOT_DIR, "demo-full.png"), + fullPage: true + }); + }); + + test("capture component section screenshots", async ({ page }) => { + fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); + + for (const section of SECTIONS) { + const locator = page.locator(".demo-section", { has: page.locator(`h2:has-text("${section.label}")`) }); + const count = await locator.count(); + if (count === 0) continue; + const element = locator.first(); + await element.scrollIntoViewIfNeeded(); + await page.waitForTimeout(300); + await element.screenshot({ + path: path.join(SCREENSHOT_DIR, `section-${section.name}.png`) + }); + } + }); +});