diff --git a/.sassdocrc b/.sassdocrc index b0366f1b..29b27743 100644 --- a/.sassdocrc +++ b/.sassdocrc @@ -18,6 +18,7 @@ herman: sassOptions: loadPaths: - 'src/sass' + - 'node_modules' use: - 'config' display: diff --git a/src/lib/components/Footer.svelte b/src/lib/components/Footer.svelte index 71ae6003..81bb8095 100644 --- a/src/lib/components/Footer.svelte +++ b/src/lib/components/Footer.svelte @@ -24,11 +24,3 @@ {/each} - - diff --git a/src/lib/components/colors/Header.svelte b/src/lib/components/colors/Header.svelte index 7e339c1b..3d96363f 100644 --- a/src/lib/components/colors/Header.svelte +++ b/src/lib/components/colors/Header.svelte @@ -3,6 +3,7 @@ import type { PlainColorObject } from 'colorjs.io/types/src/color'; import type { Writable } from 'svelte/store'; + import CopyButton from '$lib/components/util/CopyButton.svelte'; import type { ColorFormatId } from '$lib/constants'; import { getSpaceFromFormatId } from '$lib/utils'; @@ -13,8 +14,8 @@ $: targetSpace = getSpaceFromFormatId(format); $: display = serialize($color, { inGamut: false, format }); $: displayType = type === 'bg' ? 'Background' : 'Foreground'; - $: editing = false; - $: inputValue = ''; + let editing = false; + let inputValue = ''; let hasError = false; // When not editing, sync input value with color (e.g. when sliders change) @@ -86,6 +87,7 @@ on:input={handleInput} on:keydown={handleKeydown} /> + {#if hasError}
Could not parse input as a valid color.
{/if} @@ -95,12 +97,13 @@ @use 'config'; [data-colors] { + align-items: center; display: grid; grid-template: - 'label' auto - 'swatch' var(--swatch-height, var(--swatch)) - 'input' auto - 'error' minmax(var(--double-gutter), auto) / 1fr; + 'label label' auto + 'swatch swatch' var(--swatch-height, var(--swatch)) + 'copy input' auto + '.... error' minmax(var(--double-gutter), auto) / auto 1fr; @include config.above('sm-page-break') { --swatch-height: calc(2 * var(--swatch)); @@ -154,9 +157,7 @@ } [data-input='color'] { - border-width: 0 0 var(--border-width) 0; grid-area: input; - padding: var(--shim) 0.25ch; } [data-color-info='warning'] { diff --git a/src/lib/components/colors/Output.svelte b/src/lib/components/colors/Output.svelte index 437698dd..6a906f44 100644 --- a/src/lib/components/colors/Output.svelte +++ b/src/lib/components/colors/Output.svelte @@ -3,6 +3,7 @@ import type { PlainColorObject } from 'colorjs.io/types/src/color'; import SupportWarning from '$lib/components/colors/SupportWarning.svelte'; + import CopyButton from '$lib/components/util/CopyButton.svelte'; import type { ColorFormatId } from '$lib/constants'; import { getSpaceFromFormatId } from '$lib/utils'; @@ -21,6 +22,7 @@ + + diff --git a/src/lib/components/ratio/index.svelte b/src/lib/components/ratio/index.svelte index b1be86dc..953f01ae 100644 --- a/src/lib/components/ratio/index.svelte +++ b/src/lib/components/ratio/index.svelte @@ -98,9 +98,10 @@ @include config.between('sm-column-break', 'lg-page-break') { gap: var(--shim) var(--gutter); + // fixed width column to prevent layout shift as the ratio number changes grid-template: 'heading number' min-content - 'intro intro' 1fr / auto 1fr; + 'intro intro' 1fr / auto var(--ratio-width); } @include config.above('lg-page-break') { @@ -134,6 +135,7 @@ align-items: start; display: inline-flex; grid-area: number; + justify-content: flex-end; line-height: 0.7; // weird number alignment } diff --git a/src/lib/components/util/CopyButton.svelte b/src/lib/components/util/CopyButton.svelte new file mode 100644 index 00000000..ca174b2c --- /dev/null +++ b/src/lib/components/util/CopyButton.svelte @@ -0,0 +1,28 @@ + + + diff --git a/src/lib/components/util/Icon.svelte b/src/lib/components/util/Icon.svelte index cbe85247..c21816dd 100644 --- a/src/lib/components/util/Icon.svelte +++ b/src/lib/components/util/Icon.svelte @@ -2,6 +2,8 @@ import type { SvelteComponent } from 'svelte'; import Check from '$lib/icons/Check.svelte'; + import Clipboard from '$lib/icons/Clipboard.svelte'; + import Copy from '$lib/icons/Copy.svelte'; import GitHub from '$lib/icons/GitHub.svelte'; import LinkedIn from '$lib/icons/LinkedIn.svelte'; import Logo from '$lib/icons/Logo.svelte'; @@ -14,6 +16,8 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any const icons: { [key: string]: typeof SvelteComponent } = { check: Check, + clipboard: Clipboard, + copy: Copy, logo: Logo, newtab: NewTab, warning: Warning, diff --git a/src/lib/icons/Clipboard.svelte b/src/lib/icons/Clipboard.svelte new file mode 100644 index 00000000..65dde9ed --- /dev/null +++ b/src/lib/icons/Clipboard.svelte @@ -0,0 +1,14 @@ + + + + diff --git a/src/lib/icons/Copy.svelte b/src/lib/icons/Copy.svelte new file mode 100644 index 00000000..d596cc4a --- /dev/null +++ b/src/lib/icons/Copy.svelte @@ -0,0 +1,15 @@ + + + + diff --git a/src/sass/app.scss b/src/sass/app.scss index 07df236c..a0446cc1 100644 --- a/src/sass/app.scss +++ b/src/sass/app.scss @@ -4,3 +4,4 @@ @use 'config'; @use 'initial'; @use 'patterns'; +@use 'components'; diff --git a/src/sass/components/_index.scss b/src/sass/components/_index.scss new file mode 100644 index 00000000..33703f58 --- /dev/null +++ b/src/sass/components/_index.scss @@ -0,0 +1,4 @@ +// Components Manifest +// =================== + +@forward 'social-nav'; diff --git a/src/sass/components/_social-nav.scss b/src/sass/components/_social-nav.scss new file mode 100644 index 00000000..9f5da3da --- /dev/null +++ b/src/sass/components/_social-nav.scss @@ -0,0 +1,18 @@ +[data-nav='social'] { + --li-padding-bottom: 0; + --link: var(--action-light); + --link-focus: var(--action); + + a { + &:link, + &:visited { + --outline-width: 0; + + display: block; + + &:focus-visible { + transform: scale(1.15); + } + } + } +} diff --git a/src/sass/config/_animation.scss b/src/sass/config/_animation.scss deleted file mode 100644 index de0e8dca..00000000 --- a/src/sass/config/_animation.scss +++ /dev/null @@ -1,9 +0,0 @@ -@use 'tools'; -@use 'sassdoc-theme-herman/scss/utilities' as herman; - -/// # Animation Configuration -/// @group animation -/// @link https://www.oddbird.net/accoutrement/docs/animate -/// Accoutrement Scale - -tools.$times: ('fast': 300ms); diff --git a/src/sass/config/_focus.scss b/src/sass/config/_focus.scss index c2494f1c..4be84ace 100644 --- a/src/sass/config/_focus.scss +++ b/src/sass/config/_focus.scss @@ -14,9 +14,9 @@ /// @group focus @mixin focus-ring() { /* stylelint-disable declaration-block-no-redundant-longhand-properties */ - outline-color: var(--focus-ring, var(--text, currentColor)); + outline-color: var(--focus-ring, currentColor); outline-offset: var(--outline-offset, 0); - outline-style: var(--outline-style, dotted); - outline-width: var(--outline-width, var(--border-width)); + outline-style: var(--outline-style, solid); + outline-width: var(--outline-width, var(--border-width-md)); /* stylelint-enable declaration-block-no-redundant-longhand-properties */ } diff --git a/src/sass/config/_index.scss b/src/sass/config/_index.scss index 85c49908..10e0bd4a 100644 --- a/src/sass/config/_index.scss +++ b/src/sass/config/_index.scss @@ -6,14 +6,16 @@ @forward 'scale'; @forward 'utilities'; -/** - * To turn Sass tokens into CSS custom properties: - * - load each color module with `@use` - * - use the `tools.add-colors` mixin with the `sass:meta` module to create a map of the variables in the imported module - * - in `initial/_root.scss` call the `config.colors--()` mixin to create custom properties - */ +// To turn Sass tokens into CSS custom properties: +// - load each color module with `@use` +// - use the `tools.add-*` mixin with the `sass:meta` module to create a +// map of the variables in the imported module +// - in `initial/_root.scss` call the `config.*--()` mixin to create +// custom properties @use 'tools'; @use 'sass:meta'; +@use 'animation/easing'; +@use 'animation/times'; @use 'color/brand'; @use 'color/ui' as color-ui; @use 'scale/ratio'; @@ -28,3 +30,5 @@ @include tools.add-sizes(meta.module-variables('spacing')); @include tools.add-sizes(meta.module-variables('text')); @include tools.add-sizes(meta.module-variables('scale-ui')); +@include tools.add-easing(meta.module-variables('easing')); +@include tools.add-times(meta.module-variables('times')); diff --git a/src/sass/config/_tools.scss b/src/sass/config/_tools.scss index fec40929..c4a965ad 100644 --- a/src/sass/config/_tools.scss +++ b/src/sass/config/_tools.scss @@ -1,6 +1,7 @@ @forward 'accoutrement/sass/tools' with ( - $size-var-prefix: '', - $ratio-var-prefix: '', $color-var-prefix: '', + $easing-var-prefix: '', + $ratio-var-prefix: '', + $size-var-prefix: '', $time-var-prefix: '' ); diff --git a/src/sass/config/animation/_easing.scss b/src/sass/config/animation/_easing.scss new file mode 100644 index 00000000..97611d9c --- /dev/null +++ b/src/sass/config/animation/_easing.scss @@ -0,0 +1 @@ +$springy: cubic-bezier(0.175, 0.885, 0.32, 1.275); diff --git a/src/sass/config/animation/_index.scss b/src/sass/config/animation/_index.scss new file mode 100644 index 00000000..41c3a2b8 --- /dev/null +++ b/src/sass/config/animation/_index.scss @@ -0,0 +1,39 @@ +@use 'sassdoc-theme-herman/scss/utilities' as herman; +@use '../tools'; +@use 'easing'; +@use 'times'; +@use 'sass:meta'; +@forward 'easing'; +@forward 'times'; + +/// # Animation Config +/// Accoutrement maps for storing global animation tokens. +/// @link https://www.oddbird.net/accoutrement/docs/animate.html +/// Accoutrement Animate +/// @group animation + +/// ## Easing +/// --------- +/// Named easings that can be re-used to create consistent movement. +/// @group animation +/// @example scss +/// @use 'config/animation/easing'; +/// @use 'config/tools'; +/// @use 'sass:meta'; +/// +/// @each $name, $easing in tools.compile-easing(meta.module-variables('easing')) { +/// /* #{$name}: #{$easing}; */ +/// } + +/// ## Times +/// --------- +/// Named times that can be re-used to create consistent motion timing. +/// @group animation +/// @example scss +/// @use 'config/animation/times'; +/// @use 'config/tools'; +/// @use 'sass:meta'; +/// +/// @each $name, $time in tools.compile-times(meta.module-variables('times')) { +/// /* #{$name}: #{$time}; */ +/// } diff --git a/src/sass/config/animation/_times.scss b/src/sass/config/animation/_times.scss new file mode 100644 index 00000000..921b737b --- /dev/null +++ b/src/sass/config/animation/_times.scss @@ -0,0 +1,2 @@ +$fast: 300ms; +$slow: 1000ms; diff --git a/src/sass/config/color/_ui.scss b/src/sass/config/color/_ui.scss index 2224197f..a913a09f 100644 --- a/src/sass/config/color/_ui.scss +++ b/src/sass/config/color/_ui.scss @@ -15,3 +15,9 @@ $border-light: tools.tint(brand.$brand-blue, 65%); $warning: tools.shade(brand.$brand-pink, 5%); $action: tools.shade(brand.$brand-pink, 15%); $active: brand.$brand-blue; +$success: oklab(51.527% -0.099 0.0131); +$action-light: color.adjust( + brand.$brand-blue, + $saturation: -45%, + $lightness: 10% +); diff --git a/src/sass/config/scale/_layout.scss b/src/sass/config/scale/_layout.scss index 0e7d0ebc..70c594a3 100644 --- a/src/sass/config/scale/_layout.scss +++ b/src/sass/config/scale/_layout.scss @@ -8,6 +8,6 @@ $page: 60rem; $sm-column-break: 30em; -$sm-page-break: 40em; +$sm-page-break: 42em; $lg-page-break: 80em; $page-margin: calc(spacing.$quarter-shim + 4vw); diff --git a/src/sass/config/scale/_ui.scss b/src/sass/config/scale/_ui.scss index d10aabbc..0ce73c07 100644 --- a/src/sass/config/scale/_ui.scss +++ b/src/sass/config/scale/_ui.scss @@ -11,5 +11,7 @@ $logo: 12rem; $swatch: 3.25rem; $icon-size-default: 1.125em; $icon-small: 0.65em; +$icon-medium: 1.5em; +$ratio-width: 10rem; $range-thumb-size: 1.35rem; $range-input: 0.85rem; diff --git a/src/sass/initial/_links.scss b/src/sass/initial/_links.scss index 3b4e123c..2517cc7c 100644 --- a/src/sass/initial/_links.scss +++ b/src/sass/initial/_links.scss @@ -4,15 +4,18 @@ /// Initial global defaults for links /// @group links +// Hide default browser focus for mouse-users but interactive elements will +// have custom focus styles applied +:focus:not(:focus-visible) { + outline: none; +} + // Focus // ----- -/// By default, all elements get a dotted outline on focus. -/// In practice, we can override this when other focus styles exist. +/// Show focus with keyboard navigation using focus-visible /// @group links -:focus { +:focus-visible { @include config.focus-ring; - - --outline-offset: -1px; } // Links @@ -30,7 +33,8 @@ a { text-decoration-thickness: var(--line-thickness, var(--border-width)); transition: color var(--fast), - text-decoration-thickness var(--fast); + text-decoration-thickness var(--fast) transform var(--fast); + transform: scale(1); } &:hover, diff --git a/src/sass/initial/_root.scss b/src/sass/initial/_root.scss index 821674c2..d331bd70 100644 --- a/src/sass/initial/_root.scss +++ b/src/sass/initial/_root.scss @@ -7,6 +7,7 @@ html { @include config.ratios--; @include config.sizes--; @include config.times--; + @include config.easing--; font-size: config.size('rem'); line-height: config.ratio('line-height'); diff --git a/src/sass/initial/_type.scss b/src/sass/initial/_type.scss index 128e2c57..f3f42728 100644 --- a/src/sass/initial/_type.scss +++ b/src/sass/initial/_type.scss @@ -92,7 +92,11 @@ strong { // Container widths here were determined by when the longer color values (p3) // would overflow the container - @container tool (min-width: 20rem) { + @container tool (min-width: 15rem) { + --tool-font-size: 5cqi; + } + + @container tool (min-width: 21rem) { --tool-font-size: var(--input-small); } diff --git a/src/sass/patterns/_animation.scss b/src/sass/patterns/_animation.scss new file mode 100644 index 00000000..35c10c9c --- /dev/null +++ b/src/sass/patterns/_animation.scss @@ -0,0 +1,12 @@ +// Animation Patterns +// ------------------ + +@keyframes grow-in { + 0% { + transform: scale(0); + } + + 25% { + transform: scale(1); + } +} diff --git a/src/sass/patterns/_buttons.scss b/src/sass/patterns/_buttons.scss index 9d6f0e33..134f9751 100644 --- a/src/sass/patterns/_buttons.scss +++ b/src/sass/patterns/_buttons.scss @@ -2,8 +2,8 @@ /// @group buttons button { - font-size: inherit; font-family: inherit; + font-size: inherit; } // Basic Buttons @@ -14,33 +14,36 @@ button { [data-btn] { appearance: none; align-items: center; - background-color: var(--btn-bg-color-active, var(--btn-bg-color, var(--bg))); + background-color: var(--btn-bg-color, var(--bg)); border: var(--btn-border-width, var(--border-width, 0)) solid var(--btn-border-color-active, var(--btn-border-color, var(--text))); border-radius: var(--border-radius); - color: var(--btn-text-color-active, var(--btn-text-color, var(--text))); + color: var(--btn-color, var(--text)); cursor: pointer; display: inline-flex; padding: var(--btn-padding-block, var(--half-shim)) var(--btn-padding-inline, var(--gutter)); transition: color var(--fast), - background-color var(--fast); + background-color var(--fast) transform var(--fast); &:hover, &:focus { - --btn-bg-color-active: var(--text); - --btn-text-color: var(--bg); - } - - &:active, - &[aria-pressed='true'] { - --btn-bg-color-active: var(--text); - --btn-text-color: var(--bg); + background-color: var(--btn-bg-color-active, var(--text)); + color: var(--btn-color-active, var(--action)); } } [data-btn~='icon'] { + --btn-bg-color-active: transparent; --btn-border-width: 0; --btn-padding-inline: var(--half-shim); + --btn-color-active: var(--action); + --btn-color: var(--action-light); + + &:focus-visible { + --outline-width: 0; + + transform: scale(1.15); + } } diff --git a/src/sass/patterns/_forms.scss b/src/sass/patterns/_forms.scss index 1cecbddd..65dacf9b 100644 --- a/src/sass/patterns/_forms.scss +++ b/src/sass/patterns/_forms.scss @@ -10,28 +10,35 @@ label { input, select { - border-color: var(--border); font-family: inherit; font-size: inherit; width: 100%; } +input { + border-color: var(--input-border-color, var(--border)); + border-width: 0 0 var(--border-width) 0; + font-family: inherit; + font-size: inherit; + padding: var(--shim) 0.25ch; + + &:focus-visible { + box-shadow: 0 3px 2px -2px var(--input-shadow-color, currentColor); + outline: none; + } + + [data-needs-changes~='true'] & { + --input-border-color: var(--warning); + --input-shadow-color: var(--warning); + } +} + select { border: 0; box-shadow: 1px 1px var(--half-shim) var(--border-light); padding: var(--half-shim) var(--shim); } -input { - transition: - outline-color var(--fast), - outline-style var(--fast); - - [data-needs-changes] & { - --focus-ring: var(--warning); - } -} - // ## Range Thumbs // --------------- /// For some reason you have to style the webkit and moz ranges separately. @@ -44,13 +51,13 @@ input { border-radius: var(--range-thumb-size); cursor: pointer; height: var(--range-thumb-size); - outline: var(--thumb-outline-color, transparent) solid var(--border-width); + outline: var(--thumb-outline-color, transparent) solid var(--border-width-md); transition: outline var(--fast); width: var(--range-thumb-size); } @mixin range-thumb-focus { - --thumb-outline-color: var(--text); + --thumb-outline-color: var(--action); } input[type='range'] { diff --git a/src/sass/patterns/_icons.scss b/src/sass/patterns/_icons.scss index a54aa96b..357c6543 100644 --- a/src/sass/patterns/_icons.scss +++ b/src/sass/patterns/_icons.scss @@ -2,14 +2,44 @@ // ---- /// By default, icons take on the size of surrounding text. /// @group icons +/// @example html +/// +/// [data-icon] { - fill: currentcolor; + fill: var(--icon-color, currentcolor); display: inline-block; height: var(--icon-height, var(--icon-size, var(--icon-size-default))); overflow: visible; width: var(--icon-width, var(--icon-size, var(--icon-size-default))); } +/// Small Icon +/// @group icons +/// @example html +/// +/// [data-icon-size='small'] { --icon-size: var(--icon-small); } + +/// Medium Icon +/// @group icons +/// @example html +/// +/// +[data-icon-size='medium'] { + --icon-size: var(--icon-medium); +} + +/// Success Icon +/// @group icons +/// @example html +/// +/// +/// +[data-icon-theme='success'] { + animation: grow-in var(--slow) var(--springy); + + --icon-color: var(--success); +} diff --git a/src/sass/patterns/_index.scss b/src/sass/patterns/_index.scss index 4db942a6..85b59cbe 100644 --- a/src/sass/patterns/_index.scss +++ b/src/sass/patterns/_index.scss @@ -2,6 +2,7 @@ // ================ @forward 'a11y'; +@forward 'animation'; @forward 'forms'; @forward 'buttons'; @forward 'icons'; diff --git a/test/js/lib/components/CopyButton.spec.ts b/test/js/lib/components/CopyButton.spec.ts new file mode 100644 index 00000000..566a4324 --- /dev/null +++ b/test/js/lib/components/CopyButton.spec.ts @@ -0,0 +1,54 @@ +import { fireEvent, render, waitFor } from '@testing-library/svelte'; + +import CopyButton from '$lib/components/util/CopyButton.svelte'; + +describe('Copy Button', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('renders a button', () => { + const { getByRole } = render(CopyButton, { + props: { text: 'Copy' }, + }); + const button = getByRole('button'); + + expect(button.firstChild).toHaveAttribute('data-icon', 'clipboard'); + }); + + it('copies content', async () => { + const spy = vi.spyOn(navigator.clipboard, 'writeText'); + const { getByRole } = render(CopyButton, { + props: { text: 'Copy' }, + }); + const button = getByRole('button'); + + expect(button.firstChild).toHaveAttribute('data-icon', 'clipboard'); + + await fireEvent.click(button); + + expect(spy).toHaveBeenCalledWith('Copy'); + }); + + it('swaps icons', async () => { + const { getByRole } = render(CopyButton, { + props: { text: 'Copy' }, + }); + const button = getByRole('button'); + + expect(button.firstChild).toHaveAttribute('data-icon', 'clipboard'); + + await fireEvent.click(button); + + expect(button.firstChild).toHaveAttribute('data-icon', 'copy'); + + vi.runAllTimers(); + await waitFor(() => { + expect(button.firstChild).toHaveAttribute('data-icon', 'clipboard'); + }); + }); +}); diff --git a/test/js/setup.js b/test/js/setup.js index efcd8bba..6c7a2776 100644 --- a/test/js/setup.js +++ b/test/js/setup.js @@ -2,4 +2,5 @@ import '@testing-library/jest-dom'; beforeAll(() => { window.CSS.supports = () => true; + window.navigator.clipboard = { writeText: () => {} }; });