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 @@
-
+
{targetColorValue}
{#if !isInGamut}
@@ -30,3 +32,21 @@
{/if}
+
+
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: () => {} };
});