├── .nvmrc ├── .env ├── src ├── routes │ ├── +layout.ts │ ├── styleguide │ │ └── +server.ts │ ├── +layout.svelte │ └── +page.svelte ├── sass │ ├── config │ │ ├── animation │ │ │ ├── _times.scss │ │ │ ├── _easing.scss │ │ │ └── _index.scss │ │ ├── color │ │ │ ├── _index.scss │ │ │ ├── _docs.scss │ │ │ ├── _brand.scss │ │ │ └── _ui.scss │ │ ├── scale │ │ │ ├── _index.scss │ │ │ ├── _ratio.scss │ │ │ ├── _layout.scss │ │ │ ├── _text.scss │ │ │ ├── _spacing.scss │ │ │ ├── _ui.scss │ │ │ └── _docs.scss │ │ ├── _tools.scss │ │ ├── _utilities.scss │ │ ├── _focus.scss │ │ ├── _index.scss │ │ └── _fonts.scss │ ├── app.scss │ ├── initial │ │ ├── _index.scss │ │ ├── _root.scss │ │ ├── _links.scss │ │ ├── _layout.scss │ │ └── _type.scss │ ├── components │ │ ├── _index.scss │ │ ├── _social-nav.scss │ │ └── _color-settings.scss │ ├── json.scss │ └── patterns │ │ ├── _animation.scss │ │ ├── _index.scss │ │ ├── _themes.scss │ │ ├── _a11y.scss │ │ ├── _lists.scss │ │ ├── _buttons.scss │ │ ├── _forms.scss │ │ └── _icons.scss ├── @types │ └── global.d.ts ├── lib │ ├── components │ │ ├── colors │ │ │ ├── SwitchButton.svelte │ │ │ ├── Output.svelte │ │ │ ├── SupportWarning.svelte │ │ │ ├── index.svelte │ │ │ ├── Formats.svelte │ │ │ ├── FormatGroup.svelte │ │ │ ├── Sliders.svelte │ │ │ └── Header.svelte │ │ ├── GamutSelect.svelte │ │ ├── util │ │ │ ├── ExternalLink.svelte │ │ │ ├── CopyButton.svelte │ │ │ └── Icon.svelte │ │ ├── Header.svelte │ │ ├── Footer.svelte │ │ ├── SpaceSelect.svelte │ │ └── ratio │ │ │ ├── Result.svelte │ │ │ ├── ColorIssues.svelte │ │ │ └── index.svelte │ ├── icons │ │ ├── LinkedIn.svelte │ │ ├── Switch.svelte │ │ ├── Warning.svelte │ │ ├── Check.svelte │ │ ├── Twitter.svelte │ │ ├── NewTab.svelte │ │ ├── Clipboard.svelte │ │ ├── Copy.svelte │ │ ├── GitHub.svelte │ │ ├── OddBird.svelte │ │ ├── Mastodon.svelte │ │ └── Logo.svelte │ ├── stores.ts │ ├── constants.ts │ └── utils.ts └── app.html ├── static ├── social.png └── favicon.svg ├── .prettierrc ├── .prettierignore ├── .stylelintignore ├── .yarnrc.yml ├── docker-compose.yml ├── .gitignore ├── test ├── tsconfig.json ├── @types │ └── vitest.d.ts ├── setup.ts ├── lib │ ├── stores.spec.ts │ ├── components │ │ ├── GamutSelect.spec.ts │ │ ├── Footer.spec.ts │ │ ├── colors │ │ │ ├── Formats.spec.ts │ │ │ ├── Output.spec.ts │ │ │ ├── Sliders.spec.ts │ │ │ ├── FormatGroup.spec.ts │ │ │ └── Header.spec.ts │ │ ├── SpaceSelect.spec.ts │ │ ├── Ratio.spec.ts │ │ └── CopyButton.spec.ts │ └── utils.spec.ts ├── routes │ └── page.spec.ts └── fixtures.ts ├── .devcontainer ├── docker-compose.dev.yml └── devcontainer.json ├── .vscode ├── extensions.json └── settings.json ├── Dockerfile ├── tsconfig.json ├── .sassdocrc ├── .dockerignore ├── svelte.config.js ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── vite.config.ts ├── .svelte-kit └── tsconfig.json ├── .stylelintrc.yml ├── LICENSE ├── README.md ├── package.json └── eslint.config.js /.nvmrc: -------------------------------------------------------------------------------- 1 | v22 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | CONTEXT=dev 2 | -------------------------------------------------------------------------------- /src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | -------------------------------------------------------------------------------- /src/sass/config/animation/_times.scss: -------------------------------------------------------------------------------- 1 | $fast: 300ms; 2 | $slow: 1000ms; 3 | -------------------------------------------------------------------------------- /src/sass/config/color/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'brand'; 2 | @forward 'ui'; 3 | -------------------------------------------------------------------------------- /src/sass/config/animation/_easing.scss: -------------------------------------------------------------------------------- 1 | $springy: cubic-bezier(0.175, 0.885, 0.32, 1.275); 2 | -------------------------------------------------------------------------------- /static/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oddbird/oddcontrast/HEAD/static/social.png -------------------------------------------------------------------------------- /src/sass/app.scss: -------------------------------------------------------------------------------- 1 | // App 2 | // === 3 | 4 | @use 'config'; 5 | @use 'initial'; 6 | @use 'patterns'; 7 | @use 'components'; 8 | -------------------------------------------------------------------------------- /src/sass/initial/_index.scss: -------------------------------------------------------------------------------- 1 | // Initial Styles 2 | @forward 'root'; 3 | @forward 'layout'; 4 | @forward 'links'; 5 | @forward 'type'; 6 | -------------------------------------------------------------------------------- /src/sass/config/scale/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'ratio'; 2 | @forward 'layout'; 3 | @forward 'spacing'; 4 | @forward 'text'; 5 | @forward 'ui'; 6 | -------------------------------------------------------------------------------- /src/sass/components/_index.scss: -------------------------------------------------------------------------------- 1 | // Components Manifest 2 | // =================== 3 | 4 | @forward 'color-settings'; 5 | @forward 'social-nav'; 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "plugins": ["prettier-plugin-svelte"], 4 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 5 | } 6 | -------------------------------------------------------------------------------- /src/sass/config/_tools.scss: -------------------------------------------------------------------------------- 1 | @forward 'pkg:accoutrement' with ( 2 | $color-var-prefix: '', 3 | $easing-var-prefix: '', 4 | $ratio-var-prefix: '', 5 | $size-var-prefix: '', 6 | $time-var-prefix: '' 7 | ); 8 | -------------------------------------------------------------------------------- /src/sass/json.scss: -------------------------------------------------------------------------------- 1 | @use 'config'; 2 | @use 'config/color/docs' as color-docs; 3 | @use 'config/scale/docs' as scale-docs; 4 | @use 'pkg:sassdoc-theme-herman' as herman; 5 | @include herman.export(herman.$herman); 6 | -------------------------------------------------------------------------------- /src/sass/config/scale/_ratio.scss: -------------------------------------------------------------------------------- 1 | /// ## Line Height 2 | /// -------------- 3 | /// Going for a readable line-height that adapts to context 4 | /// @group scale 5 | /// @sizes ratio-sizes 6 | 7 | $line-height: 1.4; 8 | -------------------------------------------------------------------------------- /src/sass/config/_utilities.scss: -------------------------------------------------------------------------------- 1 | @use 'tools' as *; 2 | 3 | // ## Z-index 4 | // ---------- 5 | /// Named z-index stack (allows nested stacks as needed) 6 | /// @type list 7 | /// @group utilities 8 | $z-index: ('bump'); 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | !.* 2 | .git/ 3 | .vscode/ 4 | .yarn/ 5 | .yarnrc.yml 6 | /.svelte-kit 7 | /build 8 | /package 9 | coverage.xml 10 | coverage/ 11 | node_modules/ 12 | static/built/ 13 | static/styleguide/ 14 | yarn.lock 15 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | !.* 2 | .git/ 3 | .vscode/ 4 | .yarn/ 5 | .yarnrc.yml 6 | /.svelte-kit 7 | /build 8 | /package 9 | coverage.xml 10 | coverage/ 11 | node_modules/ 12 | static/built/ 13 | static/styleguide/ 14 | yarn.lock 15 | -------------------------------------------------------------------------------- /src/sass/patterns/_animation.scss: -------------------------------------------------------------------------------- 1 | // Animation Patterns 2 | // ------------------ 3 | 4 | @keyframes grow-in { 5 | 0% { 6 | transform: scale(0); 7 | } 8 | 9 | 25% { 10 | transform: scale(1); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/sass/patterns/_index.scss: -------------------------------------------------------------------------------- 1 | // Pattern Manifest 2 | // ================ 3 | 4 | @forward 'a11y'; 5 | @forward 'animation'; 6 | @forward 'forms'; 7 | @forward 'buttons'; 8 | @forward 'icons'; 9 | @forward 'lists'; 10 | @forward 'themes'; 11 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | 7 | packageExtensions: 8 | markdown-it-anchor@*: 9 | peerDependenciesMeta: 10 | "@types/markdown-it": 11 | optional: true 12 | -------------------------------------------------------------------------------- /src/@types/global.d.ts: -------------------------------------------------------------------------------- 1 | import { ColorSpace, PlainColorObject } from 'colorjs.io/fn'; 2 | 3 | declare global { 4 | interface Window { 5 | bg?: PlainColorObject; 6 | fg?: PlainColorObject; 7 | ColorSpace?: typeof ColorSpace; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/routes/styleguide/+server.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/require-await 2 | export async function GET() { 3 | return new Response(undefined, { 4 | status: 302, 5 | headers: { 6 | location: '/styleguide/index.html', 7 | }, 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/sass/config/color/_docs.scss: -------------------------------------------------------------------------------- 1 | @use 'pkg:sassdoc-theme-herman' as herman; 2 | @use 'sass:meta'; 3 | @use 'brand'; 4 | @use 'ui'; 5 | @include herman.add('colors', 'brand-colors', meta.module-variables('brand')); 6 | @include herman.add('colors', 'ui-colors', meta.module-variables('ui')); 7 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | web: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | volumes: 9 | - .:/app:cached 10 | - /app/node_modules 11 | ports: 12 | - '3000:3000' 13 | - '24678:24678' # For Vite's HMR 14 | stdin_open: true 15 | tty: true 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.log 3 | *~ 4 | /.svelte-kit 5 | /build 6 | /coverage 7 | /package 8 | /static/built 9 | /static/styleguide 10 | node_modules 11 | vite.config.js.timestamp-* 12 | vite.config.ts.timestamp-* 13 | 14 | .pnp.* 15 | .yarn/* 16 | !.yarn/patches 17 | !.yarn/plugins 18 | !.yarn/releases 19 | !.yarn/sdks 20 | !.yarn/versions 21 | -------------------------------------------------------------------------------- /src/sass/config/scale/_layout.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | @use 'spacing'; 3 | 4 | /// ## Layout Sizes 5 | /// --------------- 6 | /// @group scale 7 | /// @sizes layout-sizes {ruler-large} 8 | 9 | $page: 60rem; 10 | $sm-column-break: 30em; 11 | $sm-page-break: 42em; 12 | $lg-page-break: 80em; 13 | $page-margin: calc(spacing.$quarter-shim + 4vw); 14 | -------------------------------------------------------------------------------- /src/lib/components/colors/SwitchButton.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "isolatedModules": false, 5 | "types": ["vitest/globals"] 6 | }, 7 | "include": [ 8 | "./**/*.ts", 9 | "../eslint.config.js", 10 | "../svelte.config.js", 11 | "../vite.config.ts", 12 | "../vitest.config.ts", 13 | "../src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/sass/components/_social-nav.scss: -------------------------------------------------------------------------------- 1 | [data-nav='social'] { 2 | --li-padding-bottom: 0; 3 | --link: var(--action-light); 4 | --link-focus: var(--action); 5 | 6 | a { 7 | &:link, 8 | &:visited { 9 | --outline-width: 0; 10 | 11 | display: block; 12 | 13 | &:focus-visible { 14 | transform: scale(1.15); 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/sass/patterns/_themes.scss: -------------------------------------------------------------------------------- 1 | @use '../config'; 2 | 3 | // Themes for Status 4 | // ----------------- 5 | 6 | // If the ratio is too low, make sure important info has high enough contrast 7 | [data-pass='false'] { 8 | --link: var(--status-result-fg); 9 | --link-focus: var(--status-result-fg); 10 | --status-result-bg: var(--text); 11 | --status-result-fg: var(--bg); 12 | } 13 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | web: 5 | volumes: 6 | - .:/app:delegated 7 | - vscode-server:/root/.vscode-server:cached 8 | - /var/run/docker.sock:/var/run/docker.sock:cached 9 | # Override command to prevent container crashing if webpack build exits 10 | command: sleep infinity 11 | 12 | volumes: 13 | vscode-server: {} 14 | -------------------------------------------------------------------------------- /src/sass/config/scale/_text.scss: -------------------------------------------------------------------------------- 1 | /// ## Text Sizes 2 | /// ------------- 3 | /// @group scale 4 | /// @sizes text-sizes {text} 5 | $rem: calc(1rem + 0.125vw); 6 | $small: calc(0.875rem + 0.125vw); 7 | $medium: calc(1.25rem + 0.125vw); 8 | $large: calc(2.75rem + 0.8vw); 9 | $label-size: calc(0.815rem + 0.125vw); 10 | $warning-size: $small; 11 | $input-small: calc(1rem + 0.25vw); 12 | $input-large: calc(1.25rem + 0.35vw); 13 | -------------------------------------------------------------------------------- /test/@types/vitest.d.ts: -------------------------------------------------------------------------------- 1 | import { TestingLibraryMatchers } from '@testing-library/jest-dom/matchers'; 2 | 3 | declare global { 4 | namespace jest { 5 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars 6 | interface Matchers extends TestingLibraryMatchers< 7 | typeof expect.stringContaining, 8 | R 9 | > {} 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | 3 | beforeAll(() => { 4 | window.CSS.supports = () => true; 5 | // @ts-expect-error Clipboard isn't writable... 6 | // eslint-disable-next-line @typescript-eslint/no-empty-function 7 | window.navigator.clipboard = { writeText: () => {} }; 8 | // @ts-expect-error Allow partial MediaQueryList response 9 | window.matchMedia = () => ({ matches: true }); 10 | }); 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "csstools.postcss", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode", 6 | "formulahendry.auto-rename-tag", 7 | "ms-vscode.sublime-keybindings", 8 | "naumovs.color-highlight", 9 | "stkb.rewrap", 10 | "stylelint.vscode-stylelint", 11 | "svelte.svelte-vscode", 12 | "syler.sass-indented", 13 | "tyriar.sort-lines" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/sass/config/color/_brand.scss: -------------------------------------------------------------------------------- 1 | /// # Brand Colors 2 | /// The brand is defined by our primary brand blue, 3 | /// along with secondary pink and orange base colors. 4 | /// These colors are rarely used directly, 5 | /// but form the basis of our CSS color variables. 6 | /// @colors brand-colors 7 | /// @group color 8 | 9 | $brand-blue: hsl(195deg 52% 31%); 10 | $brand-orange: hsl(24deg 100% 62%); 11 | $brand-pink: hsl(330deg 100% 45%); 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22 2 | 3 | ARG BUILD_ENV=development 4 | WORKDIR /app 5 | 6 | COPY ./package.json package.json 7 | COPY ./yarn.lock yarn.lock 8 | # Use local version of Yarn: 9 | COPY ./.yarnrc.yml .yarnrc.yml 10 | RUN corepack enable 11 | RUN yarn install --immutable 12 | 13 | COPY . /app 14 | 15 | # Avoid building prod assets in development 16 | RUN if [ "${BUILD_ENV}" = "production" ] ; then yarn build ; else mkdir -p dist ; fi 17 | 18 | CMD yarn serve 19 | -------------------------------------------------------------------------------- /src/sass/initial/_root.scss: -------------------------------------------------------------------------------- 1 | @use '../config'; 2 | @include config.import-webfonts; 3 | 4 | html { 5 | @include config.font-family('body'); 6 | @include config.colors--; 7 | @include config.ratios--; 8 | @include config.sizes--; 9 | @include config.times--; 10 | @include config.easing--; 11 | 12 | font-size: config.size('rem'); 13 | line-height: config.ratio('line-height'); 14 | 15 | &::selection { 16 | @include config.colors--; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/lib/stores.spec.ts: -------------------------------------------------------------------------------- 1 | import { get } from 'svelte/store'; 2 | 3 | import { bg, fg, INITIAL_VALUES, reset } from '$lib/stores'; 4 | import { HSL_WHITE } from '$test/fixtures'; 5 | 6 | describe('reset', () => { 7 | it('resets to initial values', () => { 8 | bg.set(HSL_WHITE); 9 | fg.set(HSL_WHITE); 10 | reset(); 11 | 12 | expect(get(fg).space.id).toEqual(INITIAL_VALUES.format); 13 | expect(get(bg).space.id).toEqual(INITIAL_VALUES.format); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/sass/config/scale/_spacing.scss: -------------------------------------------------------------------------------- 1 | @use 'sass:math'; 2 | 3 | /// ## Spacing Sizes 4 | /// ---------------- 5 | /// @group scale 6 | /// @sizes spacing-sizes {ruler-large} 7 | 8 | $gutter: 0.75rem; 9 | $gutter-plus: $gutter * 1.5; 10 | $double-gutter: $gutter * 2; 11 | $triple-gutter: $gutter * 3; 12 | $shim: math.div($gutter, 2); 13 | $shim-plus: $gutter * 0.75; 14 | $half-shim: math.div($gutter, 4); 15 | $quarter-shim: math.div($gutter, 8); 16 | $spacer: calc($double-gutter + 3vw); 17 | -------------------------------------------------------------------------------- /src/lib/icons/LinkedIn.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/lib/components/GamutSelect.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 | 15 |
16 | -------------------------------------------------------------------------------- /src/sass/components/_color-settings.scss: -------------------------------------------------------------------------------- 1 | @use '../config'; 2 | 3 | [data-setting] { 4 | align-items: center; 5 | column-gap: var(--gutter); 6 | display: grid; 7 | grid-template: 8 | 'format-label' auto 9 | 'format-input' auto / 1fr; 10 | justify-content: end; 11 | 12 | @include config.above('sm-page-break') { 13 | grid-template: 'format-label format-input' auto / 1fr auto; 14 | } 15 | 16 | label { 17 | grid-area: format-label; 18 | } 19 | 20 | select { 21 | grid-area: format-input; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/sass/config/scale/_ui.scss: -------------------------------------------------------------------------------- 1 | /// ## UI Sizes 2 | /// ----------- 3 | /// @group scale 4 | /// @sizes ui-sizes {ruler-large} 5 | 6 | $border-width: 1px; 7 | $border-width-md: 2px; 8 | $border-width-lg: 4px; 9 | $border-radius: var(--shim); 10 | $logo: 12rem; 11 | $swatch: 3.25rem; 12 | $icon-size-default: 1.125em; 13 | $icon-small: 0.65em; 14 | $icon-medium: 1.5em; 15 | $range-thumb-size: 1.35rem; 16 | $range-input: 0.85rem; 17 | $triangle-width: var(--shim); 18 | $triangle-height: var(--shim-plus); 19 | $switch-space: var(--double-gutter); 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "exactOptionalPropertyTypes": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitOverride": true, 12 | "noImplicitReturns": true, 13 | "noUncheckedIndexedAccess": true, 14 | "resolveJsonModule": true, 15 | "skipLibCheck": true, 16 | "sourceMap": true, 17 | "strict": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/sass/config/scale/_docs.scss: -------------------------------------------------------------------------------- 1 | @use 'pkg:sassdoc-theme-herman' as herman; 2 | @use 'sass:meta'; 3 | @use 'ratio'; 4 | @use 'layout'; 5 | @use 'spacing'; 6 | @use 'text'; 7 | @use 'ui'; 8 | @include herman.add('sizes', 'ratio-sizes', meta.module-variables('ratio')); 9 | @include herman.add('sizes', 'layout-sizes', meta.module-variables('layout')); 10 | @include herman.add('sizes', 'spacing-sizes', meta.module-variables('spacing')); 11 | @include herman.add('sizes', 'text-sizes', meta.module-variables('text')); 12 | @include herman.add('sizes', 'ui-sizes', meta.module-variables('ui')); 13 | -------------------------------------------------------------------------------- /test/routes/page.spec.ts: -------------------------------------------------------------------------------- 1 | import { render, type RenderResult } from '@testing-library/svelte'; 2 | 3 | import Page from '$src/routes/+page.svelte'; 4 | 5 | interface TestContext { 6 | result: RenderResult; 7 | } 8 | 9 | describe('Page', () => { 10 | beforeEach((context) => { 11 | vi.useFakeTimers(); 12 | context.result = render(Page); 13 | }); 14 | 15 | it('shows the proper heading', ({ result }) => { 16 | const { getByText } = result; 17 | 18 | expect(getByText('OddContrast')).toBeInTheDocument(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/lib/icons/Switch.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /test/fixtures.ts: -------------------------------------------------------------------------------- 1 | import { type PlainColorObject, serialize } from 'colorjs.io/fn'; 2 | 3 | import { ColorSpace } from '$lib/stores'; 4 | 5 | export const HSL = ColorSpace.get('hsl'); 6 | 7 | export const HSL_WHITE: PlainColorObject = { 8 | space: HSL, 9 | coords: HSL.white, 10 | alpha: 1, 11 | }; 12 | 13 | export const HSL_BLACK: PlainColorObject = { 14 | space: HSL, 15 | coords: [0, 0, 0], 16 | alpha: 1, 17 | }; 18 | 19 | export const HSL_WHITE_SERIALIZED = serialize(HSL_WHITE, { inGamut: false }); 20 | 21 | export const OUT_OF_BOUNDED_GAMUTS: PlainColorObject = { 22 | space: ColorSpace.get('oklch'), 23 | coords: [1, 1, 1], 24 | alpha: 1, 25 | }; 26 | -------------------------------------------------------------------------------- /src/sass/config/color/_ui.scss: -------------------------------------------------------------------------------- 1 | @use '../tools'; 2 | @use 'brand'; 3 | @use 'sass:color'; 4 | 5 | /// ## UI Colors 6 | /// ------------ 7 | /// @group color 8 | /// @colors ui-colors 9 | 10 | $text: hsl(230deg 25% 20%); 11 | $bg: white; 12 | $bg-messages: tools.tint(brand.$brand-blue, 94%); 13 | $border: tools.tint($text, 50%); 14 | $border-light: tools.tint(brand.$brand-blue, 65%); 15 | $warning: tools.shade(brand.$brand-pink, 5%); 16 | $action: tools.shade(brand.$brand-pink, 15%); 17 | $active: brand.$brand-blue; 18 | $success: oklab(51.527% -0.099 0.0131); 19 | $action-light: color.adjust( 20 | brand.$brand-blue, 21 | $saturation: -45%, 22 | $lightness: 10% 23 | ); 24 | -------------------------------------------------------------------------------- /.sassdocrc: -------------------------------------------------------------------------------- 1 | src: './src/sass/**/*.scss' 2 | dest: './static/styleguide' 3 | theme: 'herman' 4 | shortcutIcon: './static/favicon.ico' 5 | verbose: true 6 | herman: 7 | extraLinks: 8 | - name: 'Accoutrement' 9 | url: 'https://oddbird.net/accoutrement/' 10 | - name: 'Herman' 11 | url: 'https://oddbird.net/herman/' 12 | displayColors: 13 | - 'hex' 14 | - 'hsl' 15 | customCSS: './static/built/app.css' 16 | sass: 17 | implementation: 'sass-embedded' 18 | jsonFile: './static/built/json.css' 19 | sassOptions: 20 | loadPaths: 21 | - 'src/sass' 22 | use: 23 | - 'config' 24 | display: 25 | access: 26 | - 'public' 27 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/*.DS_Store 2 | **/*.cover 3 | **/*.css.map 4 | **/*.js.map 5 | **/*.log 6 | **/*.md 7 | **/*.mo 8 | **/*.orig 9 | **/*.pot 10 | **/*.py[cod] 11 | **/*.rst 12 | **/*.sage.py 13 | **/*.spec 14 | **/*.sql 15 | **/*.swo 16 | **/*.swp 17 | **/*~ 18 | **/Dockerfile 19 | .devcontainer/ 20 | .dockerignore 21 | .git 22 | .github/ 23 | .nvmrc 24 | .svelte-kit/ 25 | .vscode/ 26 | coverage/ 27 | docker-compose.yml 28 | node_modules/ 29 | npm-debug.log 30 | static/built/ 31 | static/styleguide/ 32 | 33 | # Yarn 34 | .pnp.* 35 | .yarn/* 36 | yarn-error.log 37 | !.yarn/patches 38 | !.yarn/plugins 39 | !.yarn/releases 40 | !.yarn/sdks 41 | !.yarn/versions 42 | 43 | !.svelte-kit/tsconfig.json 44 | !README.md 45 | -------------------------------------------------------------------------------- /test/lib/components/GamutSelect.spec.ts: -------------------------------------------------------------------------------- 1 | import { fireEvent, render } from '@testing-library/svelte'; 2 | import { get } from 'svelte/store'; 3 | 4 | import Gamut from '$lib/components/GamutSelect.svelte'; 5 | import { gamut, INITIAL_VALUES, reset } from '$lib/stores'; 6 | 7 | describe('Space', () => { 8 | afterEach(() => { 9 | reset(); 10 | }); 11 | 12 | it('renders editable gamut select', async () => { 13 | const { getByLabelText } = render(Gamut); 14 | 15 | expect(get(gamut)).toBe(INITIAL_VALUES.gamut); 16 | 17 | const select = getByLabelText('Show Gamut'); 18 | await fireEvent.change(select, { target: { value: 'rec2020' } }); 19 | 20 | expect(get(gamut)).toBe('rec2020'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | OddContrast 18 | 19 | {#if CONTEXT === 'production'} 20 | 25 | {/if} 26 | 27 | 28 | {@render children()} 29 | -------------------------------------------------------------------------------- /src/lib/icons/Warning.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /test/lib/components/Footer.spec.ts: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/svelte'; 2 | import MockDate from 'mockdate'; 3 | 4 | import Footer from '$lib/components/Footer.svelte'; 5 | 6 | describe('Footer', () => { 7 | afterEach(() => { 8 | MockDate.reset(); 9 | }); 10 | 11 | it('shows the copyright year (current year)', () => { 12 | MockDate.set('2022-04-01'); 13 | const { getByText } = render(Footer); 14 | 15 | expect(getByText('© 2022 OddBird.', { exact: false })).toBeVisible(); 16 | }); 17 | 18 | it('shows the copyright year (range of years)', () => { 19 | MockDate.set('2023-04-01'); 20 | const { getByText } = render(Footer); 21 | 22 | expect(getByText('© 2022–2023 OddBird.', { exact: false })).toBeVisible(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/lib/components/util/ExternalLink.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | {@render children?.()}{#if showNewTabIcon}(opens in a new tab){/if} 21 | -------------------------------------------------------------------------------- /src/lib/icons/Check.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | 17 | 18 | 19 | 25 | -------------------------------------------------------------------------------- /test/lib/components/colors/Formats.spec.ts: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/svelte'; 2 | 3 | import Formats from '$lib/components/colors/Formats.svelte'; 4 | import { HSL_WHITE } from '$test/fixtures'; 5 | 6 | describe('Formats', () => { 7 | it('shows the background header', () => { 8 | const { getByText } = render(Formats, { 9 | type: 'bg', 10 | color: HSL_WHITE, 11 | format: 'hsl', 12 | }); 13 | 14 | expect(getByText('Background Color')).toBeVisible(); 15 | }); 16 | 17 | it('shows the foreground header', () => { 18 | const { getByText } = render(Formats, { 19 | type: 'fg', 20 | color: HSL_WHITE, 21 | format: 'hsl', 22 | }); 23 | 24 | expect(getByText('Foreground Color')).toBeVisible(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-auto'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors 7 | // for more information about preprocessors 8 | preprocess: [vitePreprocess()], 9 | 10 | onwarn: ( 11 | /** @type {{ code: string; }} */ warning, 12 | /** @type {(...arg: any[]) => void} */ handler, 13 | ) => { 14 | if (warning.code === 'vite-plugin-svelte-preprocess-many-dependencies') { 15 | return; 16 | } 17 | handler(warning); 18 | }, 19 | 20 | kit: { 21 | alias: { $src: 'src', $test: 'test' }, 22 | adapter: adapter(), 23 | env: { publicPrefix: '' }, 24 | }, 25 | }; 26 | 27 | export default config; 28 | -------------------------------------------------------------------------------- /test/lib/components/SpaceSelect.spec.ts: -------------------------------------------------------------------------------- 1 | import { fireEvent, render } from '@testing-library/svelte'; 2 | import { get } from 'svelte/store'; 3 | 4 | import Space from '$lib/components/SpaceSelect.svelte'; 5 | import { bg, fg, INITIAL_VALUES, reset } from '$lib/stores'; 6 | 7 | describe('Space', () => { 8 | afterEach(() => { 9 | reset(); 10 | }); 11 | 12 | it('renders editable space select', async () => { 13 | const { getByLabelText } = render(Space); 14 | 15 | expect(get(bg).space.id).toBe(INITIAL_VALUES.format); 16 | expect(get(fg).space.id).toBe(INITIAL_VALUES.format); 17 | 18 | const select = getByLabelText('Color Format'); 19 | await fireEvent.change(select, { target: { value: 'hsl' } }); 20 | 21 | expect(get(bg).space.id).toBe('hsl'); 22 | expect(get(fg).space.id).toBe('hsl'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/lib/components/Header.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |

9 | 10 | OddContrast 11 |

12 |
13 | 14 | 15 |
16 |
17 | 18 | 32 | -------------------------------------------------------------------------------- /src/sass/config/_focus.scss: -------------------------------------------------------------------------------- 1 | /// # Focus Styling 2 | /// @group focus 3 | 4 | /// Consistent focus ring 5 | /// is applied by default on element-focus, 6 | /// but some patterns (like radio-buttons) also require 7 | /// focus-ring applied to a sibling or parent element. 8 | /// @example html 9 | ///
10 | /// 13 | ///
14 | /// @group focus 15 | @mixin focus-ring() { 16 | /* stylelint-disable declaration-block-no-redundant-longhand-properties */ 17 | outline-color: var(--focus-ring, currentColor); 18 | outline-offset: var(--outline-offset, 0); 19 | outline-style: var(--outline-style, solid); 20 | outline-width: var(--outline-width, var(--border-width-md)); 21 | /* stylelint-enable declaration-block-no-redundant-longhand-properties */ 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/components/Footer.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | 12 | © {start}{year > start ? `–${year}` : ''} OddBird. Built with Color.js. 15 | 16 |
    17 | {#each SOCIAL_LINKS as { name, icon, href } (icon)} 18 |
  • 19 | 20 | 21 | {name} 23 |
  • 24 | {/each} 25 |
26 |
27 | -------------------------------------------------------------------------------- /src/sass/patterns/_a11y.scss: -------------------------------------------------------------------------------- 1 | @use '../config'; 2 | 3 | /// # Accessibility 4 | /// Helpers and utilities formaking the site more accessible. 5 | /// @group a11y 6 | 7 | // Hidden Everywhere 8 | // ----------------- 9 | /// The `hidden` html attribute 10 | /// hides content from both display and screen readers. 11 | /// @group a11y 12 | [hidden] { 13 | display: none !important; /* stylelint-disable-line declaration-no-important */ 14 | } 15 | 16 | // Screen Reader Only 17 | // ------------------ 18 | /// A class to hide content visually, 19 | /// while remaining visible to screen readers. 20 | /// Interactive elements will also become visble 21 | /// when focused or active. 22 | /// @group a11y 23 | .sr-only { 24 | &:not(:focus, :active) { 25 | @include config.is-hidden; 26 | } 27 | } 28 | 29 | .small-only { 30 | @include config.above('sm-page-break') { 31 | @include config.is-hidden; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/lib/components/colors/Output.spec.ts: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/svelte'; 2 | import { serialize, to } from 'colorjs.io/fn'; 3 | 4 | import Output from '$lib/components/colors/Output.svelte'; 5 | import { HSL_WHITE, HSL_WHITE_SERIALIZED } from '$test/fixtures'; 6 | 7 | describe('Output', () => { 8 | it('renders color in selected format', () => { 9 | const { getByText } = render(Output, { 10 | type: 'bg', 11 | color: HSL_WHITE, 12 | format: 'hsl', 13 | }); 14 | 15 | expect(getByText(HSL_WHITE_SERIALIZED)).toBeVisible(); 16 | }); 17 | 18 | it('renders color in other format', () => { 19 | const { getByText } = render(Output, { 20 | type: 'bg', 21 | color: HSL_WHITE, 22 | format: 'oklch', 23 | }); 24 | 25 | expect( 26 | getByText(serialize(to(HSL_WHITE, 'oklch'), { inGamut: false })), 27 | ).toBeVisible(); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/lib/components/util/CopyButton.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 33 | -------------------------------------------------------------------------------- /test/lib/components/colors/Sliders.spec.ts: -------------------------------------------------------------------------------- 1 | import { fireEvent, render } from '@testing-library/svelte'; 2 | import { get, writable } from 'svelte/store'; 3 | 4 | import Sliders from '$lib/components/colors/Sliders.svelte'; 5 | import { HSL_WHITE } from '$test/fixtures'; 6 | 7 | describe('Sliders', () => { 8 | it('renders editable sliders', async () => { 9 | const color = writable(HSL_WHITE); 10 | const { getByLabelText } = render(Sliders, { 11 | type: 'bg', 12 | color, 13 | format: 'hsl', 14 | }); 15 | const sliders = { 16 | h: getByLabelText('Hue'), 17 | s: getByLabelText('Saturation'), 18 | l: getByLabelText('Lightness'), 19 | }; 20 | await fireEvent.input(sliders.h, { target: { value: '1' } }); 21 | await fireEvent.input(sliders.s, { target: { value: '2' } }); 22 | await fireEvent.input(sliders.l, { target: { value: '3' } }); 23 | 24 | expect(get(color).coords).toEqual([1, 2, 3]); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/lib/icons/Twitter.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/lib/icons/NewTab.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | 17 | 22 | 23 | 24 | 29 | -------------------------------------------------------------------------------- /src/lib/icons/Clipboard.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /src/sass/patterns/_lists.scss: -------------------------------------------------------------------------------- 1 | ul { 2 | list-style: none; 3 | margin: 0; 4 | padding-left: 0; 5 | } 6 | 7 | li { 8 | padding-bottom: var(--li-padding-bottom, var(--gutter)); 9 | } 10 | 11 | [data-list='inline'] { 12 | align-items: center; 13 | display: flex; 14 | gap: var(--shim); 15 | } 16 | 17 | dl { 18 | display: grid; 19 | grid-template-columns: auto 1fr; 20 | margin-block: var(--half-shim) var(--double-gutter); 21 | 22 | &:last-child { 23 | margin-block-end: 0; 24 | } 25 | } 26 | 27 | dd { 28 | margin-inline-start: var(--description-margin-inline, var(--gutter)); 29 | } 30 | 31 | [data-list-item-heading] { 32 | font-size: var(--small); 33 | font-weight: bold; 34 | transition: 35 | color var(--fast), 36 | opacity var(--fast); 37 | } 38 | 39 | [data-list-item-heading~='target'] { 40 | --icon-size: var(--small); 41 | 42 | display: flex; 43 | gap: var(--shim); 44 | 45 | [data-icon] { 46 | margin-block-start: var(--quarter-shim); 47 | opacity: var(--target-icon-opacity, 0); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: '/' 6 | schedule: 7 | interval: weekly 8 | time: '04:00' 9 | timezone: America/New_York 10 | 11 | - package-ecosystem: npm 12 | directory: '/' 13 | versioning-strategy: increase 14 | schedule: 15 | interval: weekly 16 | time: '04:00' 17 | timezone: America/New_York 18 | groups: 19 | prod-major: 20 | dependency-type: production 21 | update-types: 22 | - 'major' 23 | prod-minor: 24 | dependency-type: production 25 | update-types: 26 | - 'minor' 27 | - 'patch' 28 | dev-major: 29 | dependency-type: development 30 | update-types: 31 | - 'major' 32 | dev-minor: 33 | dependency-type: development 34 | update-types: 35 | - 'minor' 36 | - 'patch' 37 | ignore: 38 | - dependency-name: '@types/node' 39 | update-types: 40 | - 'version-update:semver-major' 41 | -------------------------------------------------------------------------------- /src/lib/icons/Copy.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /static/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/lib/components/Ratio.spec.ts: -------------------------------------------------------------------------------- 1 | import { render, waitFor } from '@testing-library/svelte'; 2 | import { tick } from 'svelte'; 3 | 4 | import Ratio from '$lib/components/ratio/index.svelte'; 5 | import { fg, reset } from '$lib/stores'; 6 | 7 | describe('Ratio', () => { 8 | afterEach(() => { 9 | reset(); 10 | }); 11 | 12 | it('displays passing/failing ratio', () => { 13 | const { getByText, queryAllByText, queryByText } = render(Ratio); 14 | 15 | expect(getByText('7.09:1')).toBeVisible(); 16 | expect(queryAllByText('Pass')).not.toBeNull(); 17 | expect(queryByText('Fail')).toBeNull(); 18 | }); 19 | 20 | it('updates ratio when color changes', async () => { 21 | const { getByText, queryByText, queryAllByText } = render(Ratio); 22 | fg.update((val) => { 23 | val.coords = [0.5, 0.5, 0.5]; 24 | return val; 25 | }); 26 | await tick(); 27 | 28 | await waitFor(() => { 29 | expect(getByText('3.22:1')).toBeVisible(); 30 | expect(queryByText('Pass')).not.toBeNull(); 31 | expect(queryAllByText('Fail')).not.toBeNull(); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/lib/icons/GitHub.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 11 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url'; 2 | 3 | import { sveltekit } from '@sveltejs/kit/vite'; 4 | import { svelteTesting } from '@testing-library/svelte/vite'; 5 | import { NodePackageImporter } from 'sass-embedded'; 6 | import { defineConfig } from 'vitest/config'; 7 | 8 | export default defineConfig({ 9 | plugins: [sveltekit(), svelteTesting()], 10 | css: { 11 | preprocessorOptions: { 12 | scss: { 13 | loadPaths: [fileURLToPath(new URL('./src/sass/', import.meta.url))], 14 | importers: [new NodePackageImporter()], 15 | }, 16 | }, 17 | }, 18 | test: { 19 | include: ['./test/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 20 | globals: true, 21 | environment: 'jsdom', 22 | watch: false, 23 | setupFiles: './test/setup.ts', 24 | clearMocks: true, 25 | reporters: 'dot', 26 | coverage: { 27 | enabled: true, 28 | reporter: ['text-summary', 'html'], 29 | include: ['src/**/*.{js,ts,svelte}'], 30 | exclude: [ 31 | 'src/**/*.d.ts', 32 | 'src/routes/styleguide/**/*', 33 | 'src/routes/+layout.*', 34 | ], 35 | skipFull: true, 36 | reportOnFailure: true, 37 | }, 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /src/sass/config/animation/_index.scss: -------------------------------------------------------------------------------- 1 | @use 'pkg:sassdoc-theme-herman' as herman; 2 | @use '../tools'; 3 | @use 'easing'; 4 | @use 'times'; 5 | @use 'sass:meta'; 6 | @forward 'easing'; 7 | @forward 'times'; 8 | 9 | /// # Animation Config 10 | /// Accoutrement maps for storing global animation tokens. 11 | /// @link https://www.oddbird.net/accoutrement/docs/animate.html 12 | /// Accoutrement Animate 13 | /// @group animation 14 | 15 | /// ## Easing 16 | /// --------- 17 | /// Named easings that can be re-used to create consistent movement. 18 | /// @group animation 19 | /// @example scss 20 | /// @use 'config/animation/easing'; 21 | /// @use 'config/tools'; 22 | /// @use 'sass:meta'; 23 | /// 24 | /// @each $name, $easing in tools.compile-easing(meta.module-variables('easing')) { 25 | /// /* #{$name}: #{$easing}; */ 26 | /// } 27 | 28 | /// ## Times 29 | /// --------- 30 | /// Named times that can be re-used to create consistent motion timing. 31 | /// @group animation 32 | /// @example scss 33 | /// @use 'config/animation/times'; 34 | /// @use 'config/tools'; 35 | /// @use 'sass:meta'; 36 | /// 37 | /// @each $name, $time in tools.compile-times(meta.module-variables('times')) { 38 | /// /* #{$name}: #{$time}; */ 39 | /// } 40 | -------------------------------------------------------------------------------- /src/lib/icons/OddBird.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 9 | 10 | 11 | 17 | -------------------------------------------------------------------------------- /src/lib/components/SpaceSelect.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 |
27 | 28 | 35 |
36 | -------------------------------------------------------------------------------- /src/sass/initial/_links.scss: -------------------------------------------------------------------------------- 1 | @use '../config'; 2 | 3 | /// # Link Defaults 4 | /// Initial global defaults for links 5 | /// @group links 6 | 7 | // Hide default browser focus for mouse-users but interactive elements will 8 | // have custom focus styles applied 9 | :focus:not(:focus-visible) { 10 | outline: none; 11 | } 12 | 13 | // Focus 14 | // ----- 15 | /// Show focus with keyboard navigation using focus-visible 16 | /// @group links 17 | :focus-visible { 18 | @include config.focus-ring; 19 | } 20 | 21 | // Links 22 | // ----- 23 | /// Basic link (and link-states) apply action and interaction text colors. 24 | /// Text underlines are also applied 25 | /// @group links 26 | /// @example html 27 | ///

This text contains a link

28 | a { 29 | &:link, 30 | &:visited { 31 | color: var(--link, var(--action)); 32 | text-decoration: solid underline; 33 | text-decoration-thickness: var(--line-thickness, var(--border-width)); 34 | transition: 35 | color var(--fast), 36 | text-decoration-thickness var(--fast) transform var(--fast); 37 | transform: scale(1); 38 | } 39 | 40 | &:hover, 41 | &:focus { 42 | color: var(--link-focus, var(--active)); 43 | 44 | --line-thickness: var(--border-width-lg); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/lib/components/colors/FormatGroup.spec.ts: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/svelte'; 2 | 3 | import FormatGroup from '$lib/components/colors/FormatGroup.svelte'; 4 | import { FORMAT_GROUPS } from '$src/lib/constants'; 5 | import { HSL_WHITE, OUT_OF_BOUNDED_GAMUTS } from '$test/fixtures'; 6 | 7 | describe('FormatGroup', () => { 8 | it('renders selected group', () => { 9 | const FORMAT_GROUP = FORMAT_GROUPS[0]!; 10 | const { getByTestId, getByText } = render(FormatGroup, { 11 | type: 'bg', 12 | color: HSL_WHITE, 13 | formatGroup: FORMAT_GROUP, 14 | }); 15 | 16 | expect(getByText(FORMAT_GROUP.name)).toBeVisible(); 17 | FORMAT_GROUP.formats.forEach((format) => { 18 | expect(getByTestId(`format-${format}`)).toBeVisible(); 19 | }); 20 | }); 21 | 22 | it('renders warning if out of gamut', () => { 23 | const FORMAT_GROUP = FORMAT_GROUPS[0]!; 24 | const { getByTestId, getByText } = render(FormatGroup, { 25 | type: 'bg', 26 | color: OUT_OF_BOUNDED_GAMUTS, 27 | formatGroup: FORMAT_GROUP, 28 | }); 29 | 30 | expect(getByText(FORMAT_GROUP.name)).toBeVisible(); 31 | FORMAT_GROUP.formats.forEach((format) => { 32 | expect(getByTestId(`format-${format}`)).toBeVisible(); 33 | }); 34 | expect(getByText('outside the sRGB gamut.')).toBeVisible(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /.svelte-kit/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "$src": [ 5 | "../src" 6 | ], 7 | "$src/*": [ 8 | "../src/*" 9 | ], 10 | "$test": [ 11 | "../test" 12 | ], 13 | "$test/*": [ 14 | "../test/*" 15 | ], 16 | "$lib": [ 17 | "../src/lib" 18 | ], 19 | "$lib/*": [ 20 | "../src/lib/*" 21 | ], 22 | "$app/types": [ 23 | "./types/index.d.ts" 24 | ] 25 | }, 26 | "rootDirs": [ 27 | "..", 28 | "./types" 29 | ], 30 | "verbatimModuleSyntax": true, 31 | "isolatedModules": true, 32 | "lib": [ 33 | "esnext", 34 | "DOM", 35 | "DOM.Iterable" 36 | ], 37 | "moduleResolution": "bundler", 38 | "module": "esnext", 39 | "noEmit": true, 40 | "target": "esnext" 41 | }, 42 | "include": [ 43 | "ambient.d.ts", 44 | "non-ambient.d.ts", 45 | "./types/**/$types.d.ts", 46 | "../vite.config.js", 47 | "../vite.config.ts", 48 | "../src/**/*.js", 49 | "../src/**/*.ts", 50 | "../src/**/*.svelte", 51 | "../tests/**/*.js", 52 | "../tests/**/*.ts", 53 | "../tests/**/*.svelte" 54 | ], 55 | "exclude": [ 56 | "../node_modules/**", 57 | "../src/service-worker.js", 58 | "../src/service-worker/**/*.js", 59 | "../src/service-worker.ts", 60 | "../src/service-worker/**/*.ts", 61 | "../src/service-worker.d.ts", 62 | "../src/service-worker/**/*.d.ts" 63 | ] 64 | } -------------------------------------------------------------------------------- /src/sass/config/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'tools'; 2 | @forward 'animation'; 3 | @forward 'color'; 4 | @forward 'focus'; 5 | @forward 'fonts'; 6 | @forward 'scale'; 7 | @forward 'utilities'; 8 | 9 | // To turn Sass tokens into CSS custom properties: 10 | // - load each color module with `@use` 11 | // - use the `tools.add-*` mixin with the `sass:meta` module to create a 12 | // map of the variables in the imported module 13 | // - in `initial/_root.scss` call the `config.*--()` mixin to create 14 | // custom properties 15 | @use 'tools'; 16 | @use 'sass:meta'; 17 | @use 'animation/easing'; 18 | @use 'animation/times'; 19 | @use 'color/brand'; 20 | @use 'color/ui' as color-ui; 21 | @use 'scale/ratio'; 22 | @use 'scale/layout'; 23 | @use 'scale/spacing'; 24 | @use 'scale/text'; 25 | @use 'scale/ui' as scale-ui; 26 | @include tools.add-colors(meta.module-variables('brand')); 27 | @include tools.add-colors(meta.module-variables('color-ui')); 28 | @include tools.add-sizes(meta.module-variables('ratio')); 29 | @include tools.add-sizes(meta.module-variables('layout')); 30 | @include tools.add-sizes(meta.module-variables('spacing')); 31 | @include tools.add-sizes(meta.module-variables('text')); 32 | @include tools.add-sizes(meta.module-variables('scale-ui')); 33 | @include tools.add-easing(meta.module-variables('easing')); 34 | @include tools.add-times(meta.module-variables('times')); 35 | -------------------------------------------------------------------------------- /src/lib/components/colors/Output.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 |
    28 |
  • 29 | 30 | {targetColorValue} 31 | 32 |
  • 33 |
34 | 35 | 47 | -------------------------------------------------------------------------------- /src/lib/components/colors/SupportWarning.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 | {#if !isSupported} 30 | 31 | {spaceObject.name} is 32 | 35 | not supported by your current browser. 37 | 38 | {/if} 39 | 40 | 45 | -------------------------------------------------------------------------------- /src/lib/components/util/Icon.svelte: -------------------------------------------------------------------------------- 1 | 41 | 42 |