mirror of
https://github.com/shoelace-style/webawesome.git
synced 2026-01-14 13:09:14 +00:00
Compare commits
1 Commits
custom-pal
...
copy-butto
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a41c31851 |
@@ -13,4 +13,3 @@ package-lock.json
|
||||
tsconfig.json
|
||||
cdn
|
||||
_site
|
||||
docs/assets/scripts/prism-downloaded.js
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as path from 'node:path';
|
||||
import { anchorHeadingsPlugin } from './_utils/anchor-headings.js';
|
||||
import { codeExamplesPlugin } from './_utils/code-examples.js';
|
||||
import { copyCodePlugin } from './_utils/copy-code.js';
|
||||
@@ -9,7 +8,6 @@ import { removeDataAlphaElements } from './_utils/remove-data-alpha-elements.js'
|
||||
// import { formatCodePlugin } from './_utils/format-code.js';
|
||||
import litPlugin from '@lit-labs/eleventy-plugin-lit';
|
||||
import { readFile } from 'fs/promises';
|
||||
import nunjucks from 'nunjucks';
|
||||
import componentList from './_data/componentList.js';
|
||||
import * as filters from './_utils/filters.js';
|
||||
import { outlinePlugin } from './_utils/outline.js';
|
||||
@@ -18,10 +16,7 @@ import { searchPlugin } from './_utils/search.js';
|
||||
|
||||
import process from 'process';
|
||||
|
||||
import * as url from 'url';
|
||||
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
|
||||
|
||||
const packageData = JSON.parse(await readFile(path.join(__dirname, '..', 'package.json'), 'utf-8'));
|
||||
const packageData = JSON.parse(await readFile('./package.json', 'utf-8'));
|
||||
const isAlpha = process.argv.includes('--alpha');
|
||||
const isDev = process.argv.includes('--develop');
|
||||
|
||||
@@ -29,23 +24,12 @@ const globalData = {
|
||||
package: packageData,
|
||||
isAlpha,
|
||||
layout: 'page.njk',
|
||||
|
||||
server: {
|
||||
head: '',
|
||||
loginOrAvatar: '',
|
||||
flashes: '',
|
||||
},
|
||||
};
|
||||
|
||||
const passThroughExtensions = ['js', 'css', 'png', 'svg', 'jpg', 'mp4'];
|
||||
const passThrough = [...passThroughExtensions.map(ext => 'docs/**/*.' + ext)];
|
||||
|
||||
export default function (eleventyConfig) {
|
||||
/**
|
||||
* This is the guard we use for now to make sure our final built files dont need a 2nd pass by the server. This keeps us able to still deploy the bare HTML files on Vercel until the app is ready.
|
||||
*/
|
||||
const serverBuild = process.env.WEBAWESOME_SERVER === 'true';
|
||||
|
||||
// NOTE - alpha setting removes certain pages
|
||||
if (isAlpha) {
|
||||
eleventyConfig.ignores.add('**/experimental/**');
|
||||
@@ -71,38 +55,7 @@ export default function (eleventyConfig) {
|
||||
|
||||
// Shortcodes - {% shortCode arg1, arg2 %}
|
||||
eleventyConfig.addShortcode('cdnUrl', location => {
|
||||
return `https://early.webawesome.com/webawesome@${packageData.version}/dist/` + (location || '').replace(/^\//, '');
|
||||
});
|
||||
|
||||
// Turns `{% server "foo" %} into `{{ server.foo | safe }}` when the WEBAWESOME_SERVER variable is set to "true"
|
||||
eleventyConfig.addShortcode('server', function (property) {
|
||||
if (serverBuild) {
|
||||
return `{{ server.${property} | safe }}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
eleventyConfig.addTransform('second-nunjucks-transform', function NunjucksTransform(content) {
|
||||
// For a server build, we expect a server to run the second transform.
|
||||
if (serverBuild) {
|
||||
return content;
|
||||
}
|
||||
|
||||
// Only run the transform on files nunjucks would transform.
|
||||
if (!this.page.inputPath.match(/.(md|html|njk)$/)) {
|
||||
return content;
|
||||
}
|
||||
|
||||
/** This largely mimics what an app would do and just stubs out what we don't care about. */
|
||||
return nunjucks.renderString(content, {
|
||||
// Stub the server EJS shortcodes.
|
||||
server: {
|
||||
head: '',
|
||||
loginOrAvatar: '',
|
||||
flashes: '',
|
||||
},
|
||||
});
|
||||
return `https://early.webawesome.com/webawesome@${packageData.version}/dist/` + location.replace(/^\//, '');
|
||||
});
|
||||
|
||||
// Paired shortcodes - {% shortCode %}content{% endShortCode %}
|
||||
@@ -141,7 +94,7 @@ export default function (eleventyConfig) {
|
||||
eleventyConfig.addPlugin(highlightCodePlugin());
|
||||
|
||||
// Add copy code buttons to code blocks
|
||||
eleventyConfig.addPlugin(copyCodePlugin);
|
||||
eleventyConfig.addPlugin(copyCodePlugin());
|
||||
|
||||
// Various text replacements
|
||||
eleventyConfig.addPlugin(
|
||||
@@ -164,6 +117,29 @@ export default function (eleventyConfig) {
|
||||
]),
|
||||
);
|
||||
|
||||
// SSR plugin
|
||||
if (!isDev) {
|
||||
//
|
||||
// Problematic components in SSR land:
|
||||
// - animation (breaks on navigation + ssr with Turbo)
|
||||
// - mutation-observer (why SSR this?)
|
||||
// - resize-observer (why SSR this?)
|
||||
// - tooltip (why SSR this?)
|
||||
//
|
||||
const omittedModules = [];
|
||||
const componentModules = componentList
|
||||
.filter(component => !omittedModules.includes(component.tagName.split(/wa-/)[1]))
|
||||
.map(component => {
|
||||
const name = component.tagName.split(/wa-/)[1];
|
||||
return `./dist/components/${name}/${name}.js`;
|
||||
});
|
||||
|
||||
eleventyConfig.addPlugin(litPlugin, {
|
||||
mode: 'worker',
|
||||
componentModules,
|
||||
});
|
||||
}
|
||||
|
||||
// Build the search index
|
||||
eleventyConfig.addPlugin(
|
||||
searchPlugin({
|
||||
@@ -190,31 +166,6 @@ export default function (eleventyConfig) {
|
||||
eleventyConfig.addPassthroughCopy(glob);
|
||||
}
|
||||
|
||||
// SSR plugin
|
||||
// Make sure this is the last thing, we dont want to run the risk of accidentally transforming shadow roots with the nunjucks 2nd transform.
|
||||
if (!isDev) {
|
||||
//
|
||||
// Problematic components in SSR land:
|
||||
// - animation (breaks on navigation + ssr with Turbo)
|
||||
// - mutation-observer (why SSR this?)
|
||||
// - resize-observer (why SSR this?)
|
||||
// - tooltip (why SSR this?)
|
||||
//
|
||||
const omittedModules = [];
|
||||
const componentModules = componentList
|
||||
.filter(component => !omittedModules.includes(component.tagName.split(/wa-/)[1]))
|
||||
.map(component => {
|
||||
const name = component.tagName.split(/wa-/)[1];
|
||||
const componentDirectory = process.env.UNBUNDLED_DIST_DIRECTORY || path.join('.', 'dist');
|
||||
return path.join(componentDirectory, 'components', name, `${name}.js`);
|
||||
});
|
||||
|
||||
eleventyConfig.addPlugin(litPlugin, {
|
||||
mode: 'worker',
|
||||
componentModules,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
markdownTemplateEngine: 'njk',
|
||||
dir: {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { HUE_RANGES as default } from '../assets/scripts/tweak/data.js';
|
||||
@@ -1 +1 @@
|
||||
["red", "orange", "yellow", "green", "cyan", "blue", "indigo", "purple", "pink", "gray"]
|
||||
["red", "yellow", "green", "cyan", "blue", "indigo", "purple", "gray"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as default } from '../../src/styles/color/scripts/palettes-analyzed.js';
|
||||
export { default as default } from '../../src/styles/color/palettes.js';
|
||||
|
||||
@@ -5,18 +5,16 @@
|
||||
<meta name="theme-color" content="#f36944">
|
||||
|
||||
<script type="module" src="/assets/scripts/code-examples.js"></script>
|
||||
<script type="module" src="/assets/scripts/copy-code.js"></script>
|
||||
|
||||
<script type="module" src="/assets/scripts/scroll.js"></script>
|
||||
<script type="module" src="/assets/scripts/turbo.js"></script>
|
||||
<script type="module" src="/assets/scripts/search.js"></script>
|
||||
<script type="module" src="/assets/scripts/outline.js"></script>
|
||||
{% if hasSidebar %}<script type="module" src="/assets/scripts/sidebar-tweaks.js"></script>{% endif %}
|
||||
<script defer data-domain="backers.webawesome.com" src="https://plausible.io/js/script.js"></script>
|
||||
|
||||
{# Docs styles #}
|
||||
<link rel="stylesheet" href="/assets/styles/docs.css" />
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body class="layout-{{ layout | stripExtension }}{{ ' page-wide' if wide }}">
|
||||
<!-- use view="desktop" as default to reduce layout jank on desktop site. -->
|
||||
@@ -50,9 +48,6 @@
|
||||
Search
|
||||
<kbd slot="suffix" class="only-desktop">/</kbd>
|
||||
</wa-button>
|
||||
|
||||
{# Login #}
|
||||
{% server "loginOrAvatar" %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -79,19 +74,14 @@
|
||||
</aside>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{# Main #}
|
||||
<main id="content">
|
||||
{# Expandable outline #}
|
||||
{% if hasOutline %}
|
||||
<nav id="outline-expandable">
|
||||
<details class="outline-links">
|
||||
<summary>On this page</summary>
|
||||
</details>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<div id="flashes">{% server "flashes" %}</div>
|
||||
|
||||
{% block header %}
|
||||
{% include 'breadcrumbs.njk' %}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
{% set ancestors = page.url | ancestors %}
|
||||
|
||||
{% if ancestors.length > 0 %}
|
||||
{% set breadcrumbs = page.url | breadcrumbs %}
|
||||
{% if breadcrumbs.length > 0 %}
|
||||
<wa-breadcrumb id="docs-breadcrumbs">
|
||||
{% for ancestor in ancestors %}
|
||||
{% if ancestor.page.url != "/" %}
|
||||
<wa-breadcrumb-item href="{{ ancestor.page.url }}">{{ ancestor.data.title }}</wa-breadcrumb-item>
|
||||
{% endif %}
|
||||
{% for crumb in breadcrumbs %}
|
||||
<wa-breadcrumb-item href="{{ crumb.url }}">{{ crumb.title }}</wa-breadcrumb-item>
|
||||
{% endfor %}
|
||||
<wa-breadcrumb-item>{# Current page #}</wa-breadcrumb-item>
|
||||
</wa-breadcrumb>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<table class="colors wa-palette-{{ paletteId }} contrast-table" data-min-contrast="{{ minContrast }}">
|
||||
<table class="colors wa-palette-{{ paletteId }}">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
@@ -12,31 +12,19 @@
|
||||
</tr>
|
||||
</thead>
|
||||
{% for hue in hues -%}
|
||||
<tr data-hue="{{ hue }}" v-if="'{{hue}}' in paletteScales">
|
||||
<tr>
|
||||
<th>{{ hue | capitalize }}</th>
|
||||
{% for tint_bg in tints -%}
|
||||
{% set color_bg = palettes[paletteId][hue][tint_bg] %}
|
||||
{% for tint_fg in tints | reverse -%}
|
||||
{% set color_fg = palettes[paletteId][hue][tint_fg] %}
|
||||
{% if (tint_fg - tint_bg) | abs == difference %}
|
||||
{% set contrast_wcag = '' %}
|
||||
{% if color_fg and color_bg -%}
|
||||
{% set contrast_wcag = color_bg.contrast(color_fg, 'WCAG21') %}
|
||||
{%- endif %}
|
||||
<td v-for="contrast of [contrasts.{{ hue }}['{{ tint_bg }}']['{{ tint_fg }}']]"
|
||||
data-tint-bg="{{ tint_bg }}" data-tint-fg="{{ tint_fg }}" data-original-contrast="{{ contrast_wcag }}">
|
||||
<div v-content:number="contrast.value"
|
||||
class="color swatch" :class="{
|
||||
'value-up': contrast.value - contrast.original > 0.0001,
|
||||
'value-down': contrast.original - contrast.value > 0.0001,
|
||||
'contrast-fail': contrast.value < {{ minContrast }}
|
||||
}"
|
||||
style="--color: var(--wa-color-{{ hue }}-{{ tint_bg }}); color: var(--wa-color-{{ hue }}-{{ tint_fg }})"
|
||||
:style="{
|
||||
'--color': contrast.bgColor,
|
||||
color: contrast.fgColor,
|
||||
}"
|
||||
>
|
||||
<td>
|
||||
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint_bg }}); color: var(--wa-color-{{ hue }}-{{ tint_fg }})">
|
||||
{% set contrast_wcag = '' %}
|
||||
{% if color_fg and color_bg %}
|
||||
{% set contrast_wcag = color_bg.contrast(color_fg, 'WCAG21') %}
|
||||
{% endif %}
|
||||
{% if contrast_wcag %}
|
||||
{{ contrast_wcag | number({maximumSignificantDigits: 2}) }}
|
||||
{% else %}
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
{# Cards for pages listed by category #}
|
||||
|
||||
<section id="grid" class="index-grid">
|
||||
{% set groupedPages = allPages | groupPages(categories, page) %}
|
||||
{% for category, pages in groupedPages -%}
|
||||
{% if groupedPages.meta.groupCount > 1 %}
|
||||
<h2 class="index-category">
|
||||
{% if pages.meta.url %}<a href="{{ pages.meta.url }}">{{ pages.meta.title }}</a>
|
||||
{% else %}
|
||||
{{ pages.meta.title }}
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% endif %}
|
||||
{%- for page in pages -%}
|
||||
{% include "page-card.njk" %}
|
||||
{%- endfor -%}
|
||||
{% for category, pages in allPages | groupByTags(categories) -%}
|
||||
<h2 class="index-category">{{ category | getCategoryTitle(categories) }}</h2>
|
||||
{%- for page in pages -%}
|
||||
{%- if not page.data.parent or listChildren -%}
|
||||
{% include "page-card.njk" %}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- endfor -%}
|
||||
</section>
|
||||
|
||||
@@ -47,7 +47,3 @@
|
||||
<link rel="stylesheet" href="/dist/styles/webawesome.css" />
|
||||
<link id="color-stylesheet" rel="stylesheet" href="/dist/styles/utilities.css" />
|
||||
<link rel="stylesheet" href="/dist/styles/forms.css" />
|
||||
|
||||
|
||||
{# Used by Web Awesome App to inject other assets into the head. #}
|
||||
{% server "head" %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<wa-tab-group class="import-stylesheet-code">
|
||||
<wa-tab-group>
|
||||
<wa-tab panel="html">In HTML</wa-tab>
|
||||
<wa-tab panel="css">In CSS</wa-tab>
|
||||
<wa-tab-panel name="html">
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
<a href="{{ page.url }}"{{ page.data.keywords | attr('data-keywords') }}>
|
||||
<wa-card with-header>
|
||||
<div slot="header">
|
||||
{% include "svgs/" + (page.data.icon or "thumbnail-placeholder") + ".njk" ignore missing %}
|
||||
{% include "svgs/" + (page.data.icon or "thumbnail-placeholder") + ".njk" %}
|
||||
</div>
|
||||
<span class="page-name">{{ page.data.title }}</span>
|
||||
{% if pageSubtitle -%}
|
||||
<div class="wa-caption-s">{{ pageSubtitle }}</div>
|
||||
{%- endif %}
|
||||
</wa-card>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<table class="colors main wa-palette-{{ paletteId }} static-palette">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="core-column">Core tint</th>
|
||||
{% for tint in tints -%}
|
||||
<th>{{ tint }}</th>
|
||||
{%- endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- set hueBefore = hues[hues|length - 2] -%}
|
||||
{% for hue in hues -%}
|
||||
{% set scale = palettes[paletteId][hue] %}
|
||||
{% set coreTint = scale.maxChromaTint %}
|
||||
{%- set coreColor = scale[coreTint] -%}
|
||||
{%- set maxChroma = coreColor.c if coreColor.c > maxChroma else maxChroma -%}
|
||||
<tr data-hue="{{ hue }}" class="color-scale" style="--swatch-text-color: light-dark(var(--wa-color-{{ hue }}-10), white)">
|
||||
<th>{{ hue | capitalize }}</th>
|
||||
<td class="core-column" style="--color: var(--wa-color-{{ hue }})">
|
||||
<div class="color swatch" style="color-scheme: {{ 'light' if scale.maxChromaTint > 60 else 'dark' }};">
|
||||
{{ scale.maxChromaTint }}
|
||||
</div>
|
||||
</td>
|
||||
{% for tint in tints -%}
|
||||
{%- set color = scale[tint] -%}
|
||||
<td style="--color: var(--wa-color-{{ hue }}-{{ tint }}); color-scheme: ">
|
||||
<div class="color swatch" style="color-scheme: {{ 'light' if tint > 60 else 'dark' }};">
|
||||
<wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button>
|
||||
</div>
|
||||
</td>
|
||||
{%- endfor -%}
|
||||
</tr>
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
@@ -1,12 +1,9 @@
|
||||
{# Some collections (like "patterns") will not have any items in the alpha build for example. So this checks to make sure the collection exists. #}
|
||||
{% if collections[tag] -%}
|
||||
{% set groupUrl %}/docs/{{ tag }}/{% endset %}
|
||||
{% set groupItem = groupUrl | getCollectionItemFromUrl %}
|
||||
{% set children = groupItem.data.children if groupItem.data.children.length > 0 else (collections[tag] | sort) %}
|
||||
|
||||
<wa-details {{ ((tag in (tags or [])) or (groupUrl in page.url)) | attr('open') }}>
|
||||
<h2 slot="summary">
|
||||
{% if groupItem %}
|
||||
{% if groupUrl | getCollectionItemFromUrl %}
|
||||
<a href="{{ groupUrl }}" title="Overview">{{ title or (tag | capitalize) }}
|
||||
<wa-icon name="grid-2"></wa-icon>
|
||||
</a>
|
||||
@@ -15,8 +12,10 @@
|
||||
{% endif %}
|
||||
</h2>
|
||||
<ul>
|
||||
{% for page in children %}
|
||||
{% for page in collections[tag] | sort %}
|
||||
{% if not page.data.parent -%}
|
||||
{% include 'sidebar-link.njk' %}
|
||||
{%- endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</wa-details>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% if not (isAlpha and page.data.noAlpha) and not page.data.unlisted -%}
|
||||
{% if not (isAlpha and page.data.noAlpha) and page.fileSlug != tag and not page.data.unlisted -%}
|
||||
<li>
|
||||
<a href="{{ page.url }}">{{ page.data.title }}</a>
|
||||
{% if page.data.status == 'experimental' %}<wa-icon name="flask"></wa-icon>{% endif %}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
{% if since -%}
|
||||
<wa-badge variant="neutral" class="since">Since {{ since }}</wa-badge>
|
||||
<wa-badge variant="neutral">Since {{ since }}</wa-badge>
|
||||
{% endif -%}
|
||||
|
||||
{%- if status %}
|
||||
{%- if status == "wip" %}
|
||||
<wa-badge variant="danger" class="status">
|
||||
<wa-badge variant="danger">
|
||||
<wa-icon name="pickaxe"></wa-icon>
|
||||
Work In Progress
|
||||
</wa-badge>
|
||||
{%- elif status == "experimental" %}
|
||||
<wa-badge variant="warning" class="status">
|
||||
<wa-badge variant="warning">
|
||||
<wa-icon name="flask"></wa-icon>
|
||||
Experimental
|
||||
</wa-badge>
|
||||
{%- elif status == "stable" %}
|
||||
<wa-badge variant="brand" class="status">Stable</wa-badge>
|
||||
<wa-badge variant="brand">Stable</wa-badge>
|
||||
{%- else %}
|
||||
<wa-badge class="status">{{ status}}</wa-badge>
|
||||
<wa-badge>{{ status}}</wa-badge>
|
||||
{%- endif -%}
|
||||
{%- endif %}
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
{% set paletteId = palette.fileSlug or page.fileSlug %}
|
||||
{% set suffixes = ['-80', '', '-20'] %}
|
||||
{% set paletteId = page.fileSlug %}
|
||||
{% set tints = [80, 60, 40, 20] %}
|
||||
{% set width = 20 %}
|
||||
{% set height = 12 %}
|
||||
{% set height_core = 20 %}
|
||||
{% set gap_x = 4 %}
|
||||
{% set gap_y = 4 %}
|
||||
{% set height = 13 %}
|
||||
{% set gap_x = 3 %}
|
||||
{% set gap_y = 3 %}
|
||||
|
||||
{% set total_width = (width + gap_x) * hues|length %}
|
||||
{% set total_height = (height + gap_y) * suffixes|length + (height_core - height) %}
|
||||
<svg viewBox="0 0 {{ total_width }} {{ total_height }}" fill="none" xmlns="http://www.w3.org/2000/svg" class="wa-palette-{{ paletteId }} palette-icon">
|
||||
<svg viewBox="0 0 {{ (width + gap_x) * hues|length }} {{ (height + gap_y) * tints|length }}" fill="none" xmlns="http://www.w3.org/2000/svg" class="wa-palette-{{ paletteId }} palette-icon">
|
||||
<style>
|
||||
@import url('/dist/styles/color/{{ paletteId }}.css') layer(palette.{{ paletteId }});
|
||||
.palette-icon {
|
||||
@@ -18,14 +15,10 @@
|
||||
|
||||
{% for hue in hues -%}
|
||||
{% set hueIndex = loop.index0 %}
|
||||
{% set y = 0 %}
|
||||
{% for suffix in suffixes -%}
|
||||
{% set swatch_height = height if suffix else height_core %}
|
||||
|
||||
<rect x="{{ hueIndex * (width + gap_x) }}" y="{{ y }}"
|
||||
width="{{ width }}" height="{{ swatch_height }}"
|
||||
fill="var(--wa-color-{{ hue }}{{ suffix }})" rx="2" />
|
||||
{% set y = y + swatch_height + gap_y %}
|
||||
{% for tint in tints -%}
|
||||
<rect x="{{ hueIndex * (width + gap_x) }}" y="{{ loop.index0 * (height + gap_y) }}"
|
||||
width="{{ width }}" height="{{ height }}"
|
||||
fill="var(--wa-color-{{ hue }}-{{ tint }})" rx="4" />
|
||||
{%- endfor %}
|
||||
{% endfor %}
|
||||
</svg>
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
{% set themeId = theme.fileSlug %}
|
||||
|
||||
<div>
|
||||
<template shadowrootmode="open">
|
||||
<link rel="stylesheet" href="/dist/styles/utilities.css">
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ page.fileSlug or 'default' }}.css">
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ themeId }}/color.css">
|
||||
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
|
||||
|
||||
<div class="theme-color-icon wa-theme-{{ themeId }}">
|
||||
<div class="wa-brand wa-accent">A</div>
|
||||
<div class="wa-brand wa-outlined">A</div>
|
||||
<div class="wa-brand wa-filled">A</div>
|
||||
<div class="wa-brand wa-plain">A</div>
|
||||
{# <div class="wa-danger wa-outlined wa-filled"><wa-icon slot="icon" name="circle-exclamation" variant="regular"></wa-icon></div> #}
|
||||
|
||||
<div class="wa-neutral wa-accent">A</div>
|
||||
<div class="wa-neutral wa-outlined">A</div>
|
||||
<div class="wa-neutral wa-filled">A</div>
|
||||
<div class="wa-neutral wa-plain">A</div>
|
||||
{# <div class="wa-warning wa-outlined wa-filled"><wa-icon slot="icon" name="triangle-exclamation" variant="regular"></wa-icon></div> #}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -1,16 +0,0 @@
|
||||
{% set themeId = theme.fileSlug %}
|
||||
|
||||
<div>
|
||||
<template shadowrootmode="open">
|
||||
<link rel="stylesheet" href="/dist/styles/native/content.css">
|
||||
<link rel="stylesheet" href="/dist/styles/native/blockquote.css">
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ page.fileSlug or 'default' }}.css">
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ themeId }}/typography.css">
|
||||
<link rel="stylesheet" href="/assets/styles/theme-icons.css">
|
||||
|
||||
<div class="theme-typography-icon wa-theme-{{ themeId }}" data-no-outline data-no-anchor role="presentation">
|
||||
<h3>Title</h3>
|
||||
<p>Body text</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -93,7 +93,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><code>outlined</code></th>
|
||||
<th class="test-failure"><code>outlined</code></th>
|
||||
<td>
|
||||
<div class="wa-cluster wa-gap-2xs">
|
||||
<wa-badge variant="brand" appearance="outlined">Brand</wa-badge>
|
||||
@@ -495,7 +495,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><code>outlined</code></th>
|
||||
<th class="test-failure"><code>outlined</code></th>
|
||||
<td>
|
||||
<div class="wa-grid wa-gap-2xs">
|
||||
<wa-callout variant="brand" appearance="outlined">
|
||||
@@ -696,7 +696,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><code>outlined</code></th>
|
||||
<th class="test-failure"><code>outlined</code></th>
|
||||
<td>
|
||||
<div class="wa-cluster wa-gap-2xs">
|
||||
<wa-tag variant="brand" appearance="outlined">Brand</wa-tag>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
layout: page-outline
|
||||
tags: ["overview"]
|
||||
---
|
||||
{% set forTag = forTag or (page.url | split('/') | last) %}
|
||||
{% if description %}
|
||||
@@ -12,10 +13,8 @@ layout: page-outline
|
||||
</wa-input>
|
||||
</div>
|
||||
|
||||
{% set allPages = allPages or collections[forTag] %}
|
||||
{% if allPages and allPages.length > 0 %}
|
||||
{% set allPages = collections[forTag] %}
|
||||
{% include "grouped-pages.njk" %}
|
||||
{% endif %}
|
||||
|
||||
<link href="/assets/styles/filter.css" rel="stylesheet">
|
||||
<script type="module" src="/assets/scripts/filter.js"></script>
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{% if hasSidebar == undefined %}
|
||||
{% set hasSidebar = true %}
|
||||
{% endif %}
|
||||
|
||||
{% if hasOutline == undefined %}
|
||||
{% set hasOutline = false %}
|
||||
{% endif %}
|
||||
{% set hasSidebar = true %}
|
||||
{% set hasOutline = false %}
|
||||
|
||||
{% extends "../_includes/base.njk" %}
|
||||
|
||||
@@ -1,234 +1,39 @@
|
||||
{% set hasSidebar = true %}
|
||||
{% set hasOutline = true %}
|
||||
{% set paletteId = "default" if page.fileSlug == 'custom' else page.fileSlug %}
|
||||
{% set isCustom = page.fileSlug == 'custom' %}
|
||||
{% set tints = ["95", "90", "80", "70", "60", "50", "40", "30", "20", "10", "05"] %}
|
||||
{# {% set forceTheme = page.fileSlug %} #}
|
||||
|
||||
{% extends '../_includes/base.njk' %}
|
||||
{% extends '../_layouts/block.njk' %}
|
||||
|
||||
{% block head %}
|
||||
<style>@import url('/dist/styles/color/{{ paletteId }}.css') layer(palette.{{ paletteId }});</style>
|
||||
<link href="{{ page.url }}../app/tweak.css" rel="stylesheet">
|
||||
<script type="module" src="{{ page.url }}../app/tweak.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<div id="palette-app" data-slug="{{ page.fileSlug }}" data-palette-id="{{ page.fileSlug }}">
|
||||
<div
|
||||
:class="{
|
||||
seeded: isSeeded,
|
||||
'tweaked-chroma': tweaked?.chroma,
|
||||
'tweaked-hue': tweaked?.hue,
|
||||
'tweaked-any': Object.keys(tweaksHumanReadable).length,
|
||||
}"
|
||||
:style="{
|
||||
'--chroma-scale': chromaScale,
|
||||
'--gray-chroma': tweaked?.grayChroma ? grayChroma : originalGrayChroma,
|
||||
'--max-c': maxChroma,
|
||||
'--avg-l': L_RANGES[level].mid,
|
||||
}">
|
||||
|
||||
<header id="palette-info">
|
||||
{% include 'breadcrumbs.njk' %}
|
||||
|
||||
<h1 class="title">
|
||||
<span v-content="saved?.title || (step > 0 ? defaultPaletteTitle : paletteTitle)">{{ title }}</span>
|
||||
<template v-if="saved || step > 0">
|
||||
<wa-icon-button name="pencil" label="Rename palette" @click="rename"></wa-icon-button>
|
||||
<wa-icon-button v-if="saved" class="delete" name="trash" label="Delete palette" @click="deleteSaved"></wa-icon-button>
|
||||
<wa-button @click="save()" :disabled="!unsavedChanges"
|
||||
:variant="unsavedChanges ? 'success' : 'neutral'" size="small" :appearance="unsavedChanges ? 'accent' : 'outlined'">
|
||||
<span slot="prefix" class="icon-modifier">
|
||||
<wa-icon name="sidebar" variant="regular"></wa-icon>
|
||||
<wa-icon name="circle-plus" class="modifier" style="color: light-dark(var(--wa-color-green-70), var(--wa-color-green-60));"></wa-icon>
|
||||
</span>
|
||||
<span v-content="unsavedChanges ? 'Save' : 'Saved'">Save</span>
|
||||
</wa-button>
|
||||
</template>
|
||||
</h1>
|
||||
|
||||
<div class="block-info" v-cloak>
|
||||
<code class="class" v-if="saved || !isCustom || step > 0">.wa-palette-<span v-content="slug">{{ page.fileSlug }}</span></code>
|
||||
{% include '../_includes/status.njk' %}
|
||||
{% if not isPro %}
|
||||
<wa-badge class="pro" v-if="tweaked || isCustom">PRO</wa-badge>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if description %}
|
||||
<p class="summary">
|
||||
{{ description | inlineMarkdown | safe }}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% raw %}
|
||||
<div class="hue-wheel" v-if="!isCustom || step > 1">
|
||||
<template v-for="color, hue in coreColors">
|
||||
<template v-if="!isCustom || seedHues[hue]">
|
||||
<div :id="`hue-wheel-${hue}`" class="color"
|
||||
:style="{
|
||||
'--color': color,
|
||||
'--h': color.get('oklch.h'),
|
||||
'--c': color.get('oklch.c'),
|
||||
'--l': color.get('oklch.l'),
|
||||
}"></div>
|
||||
<wa-tooltip :for="`hue-wheel-${ hue }`" hoist>{{ capitalize(hue) }} {{ coreLevels[hue] }}</wa-tooltip>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
{% endraw %}
|
||||
</header>
|
||||
{% endblock %}
|
||||
{% set paletteId = page.fileSlug %}
|
||||
|
||||
{% block afterContent %}
|
||||
<style>@import url('/dist/styles/color/{{ paletteId }}.css') layer(palette.{{ paletteId }});</style>
|
||||
|
||||
<wa-callout size="small" class="tweaked-callout" variant="warning" v-if="!isCustom">
|
||||
<wa-icon name="sliders-simple" slot="icon" variant="regular"></wa-icon>
|
||||
This palette has been tweaked.
|
||||
<div class="wa-cluster wa-gap-xs">
|
||||
<wa-tag v-for="tweakHumanReadable, param in tweaksHumanReadable" removable @wa-remove="reset(param)">{% raw %}{{ tweakHumanReadable }}{% endraw %}</wa-tag>
|
||||
</div>
|
||||
{% set tints = ["95", "90", "80", "70", "60", "50", "40", "30", "20", "10", "05"] %}
|
||||
|
||||
<wa-button @click="reset()" appearance="outlined" variant="danger">
|
||||
<span slot="prefix" class="icon-modifier">
|
||||
<wa-icon name="circle-xmark" variant="regular"></wa-icon>
|
||||
</span>
|
||||
Reset
|
||||
</wa-button>
|
||||
</wa-callout>
|
||||
|
||||
<h2>Scales</h2>
|
||||
|
||||
{% include "palette.njk" %}
|
||||
|
||||
<table class="colors main wa-palette-{{ paletteId }}">
|
||||
<table class="colors wa-palette-{{ paletteId }}">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="core-column">Core tint</th>
|
||||
{% for tint in tints -%}
|
||||
<th>{{ tint }}</th>
|
||||
{%- endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
{% raw %}
|
||||
<tbody v-cloak>
|
||||
<tr v-for="hue in paletteScalesList" :data-hue="hue" :key="hue"
|
||||
class="color-scale" :class="{
|
||||
tweaked: hue === 'gray' ? tweaked.grayChroma || tweaked.grayColor : hueShifts[hue],
|
||||
}"
|
||||
:style="{
|
||||
'--swatch-text-color': `light-dark(var(--wa-color-${ hue }-10), white)`,
|
||||
'--hue-shift': hueShifts[hue] || ''
|
||||
}">
|
||||
<th>
|
||||
{{ capitalize(hue) }}
|
||||
<info-tip v-if="isCustom && !seedHues[hue]">
|
||||
<wa-icon name="sparkles" style="color: var(--wa-color-gray-50)"></wa-icon>
|
||||
<template #content>Generated scale</template>
|
||||
</info-tip>
|
||||
</th>
|
||||
<td class="core-column" :style="{'--original-color': `var(--wa-color-${ hue })`, '--color': colors[hue][coreLevels[hue]]}">
|
||||
<color-popup :title="capitalize(hue) + ' (core)'" :token="`--wa-color-${ hue }`" :color="coreColors[hue]"
|
||||
:pinned="!!seedColors[colorToIndex[hue].core]"
|
||||
:deletable="isCustom" @delete="deleteColor(colorToIndex[hue].core)"
|
||||
:pinnable="isCustom" @pin="addColor(coreColors[hue] + '')">
|
||||
<div slot="trigger" :id="`core-${ hue }-swatch`" class="color swatch" :style="{colorScheme: coreLevels[hue] > 60 ? 'light' : 'dark'}">
|
||||
<span v-content="coreLevels[hue]"></span>
|
||||
<wa-icon name="sliders-simple" class="tweak-icon"></wa-icon>
|
||||
</div>
|
||||
<template #content>
|
||||
<template v-if="hue === 'gray'">
|
||||
<color-swatch-picker :model-value="computedGrayColor" @update:model-value="grayColor = $event" label="Gray undertone" :colors="coreColors"></color-swatch-picker>
|
||||
</template>
|
||||
<template v-else>
|
||||
<color-slider v-if="isCustom && seedColors[colorToIndex[hue].core]"
|
||||
coord="h" type="shift"
|
||||
v-model:color="seedColors[colorToIndex[hue].core].color"
|
||||
:default-value="seedColors[colorToIndex[hue].core].inputColor.oklch.h"
|
||||
:min="HUE_RANGES[hue].min + 1" :max="HUE_RANGES[hue].max"
|
||||
label="Adjust hue" :label-min="moreHue[hueBefore[hue]]" :label-max="moreHue[hueAfter[hue]]"
|
||||
></color-slider>
|
||||
|
||||
<color-slider v-if="!isCustom && baseCoreColors[hue]"
|
||||
coord="h" type="shift"
|
||||
v-model="hueShifts[hue]"
|
||||
:default-color="baseCoreColors[hue]"
|
||||
:min="HUE_RANGES[hue].min + 1" :max="HUE_RANGES[hue].max"
|
||||
label="Adjust hue" :label-min="moreHue[hueBefore[hue]]" :label-max="moreHue[hueAfter[hue]]"
|
||||
></color-slider>
|
||||
</template>
|
||||
|
||||
<color-slider v-if="hue === 'gray'" coord="c" type="scale"
|
||||
:model-value="computedGrayChroma"
|
||||
@update:model-value="grayChroma = $event"
|
||||
:default-color="baseCoreColors[computedGrayColor]"
|
||||
:base-value="baseCoreColors[originalGrayColor].oklch.c"
|
||||
:default-value-relative="originalGrayChroma"
|
||||
:min="0" :max-relative="maxGrayChroma" :step="0.00001"
|
||||
label="Gray colorfulness" label-min="Neutral" :label-max="moreHue[computedGrayColor]"
|
||||
></color-slider>
|
||||
<color-slider v-else-if="isCustom" v-model:color="seedColors[colorToIndex[hue].core].color"
|
||||
:default-value="seedColors[colorToIndex[hue].core].inputColor?.oklch.c"
|
||||
coord="c"
|
||||
:min="Math.max(coreColors.gray.oklch.c, ...Object.keys(seedHues[hue]).filter(t => t !== coreLevels[hue]).map(t => seedHues[hue][t].oklch.c))"
|
||||
:max="getMaxChroma(colors[hue].core?.oklch.l, colors[hue].core?.oklch.h) - 0.001" :step="0.00001"
|
||||
label="Adjust colorfulness" label-min="More muted" label-max="More vibrant"
|
||||
label-default="Entered color"
|
||||
format-type="scale"
|
||||
></color-slider>
|
||||
|
||||
</template>
|
||||
</color-popup>
|
||||
</td>
|
||||
<td v-for="tint in tints.toReversed()" :data-tint="tint" :style="{'--original-color': `var(--wa-color-${ hue }-${tint})`, '--color': colors[hue][tint] }">
|
||||
<color-popup :title="capitalize(hue) + ' ' + tint" :token="`--wa-color-${ hue }-${ tint }`" :color="colors[hue][tint]"
|
||||
:pinned="!!seedColors[colorToIndex[hue][tint]]"
|
||||
:deletable="isCustom" @delete="deleteColor(colorToIndex[hue][tint])"
|
||||
:pinnable="isCustom" @pin="addColor({hue, pinnedHue: hue, level: tint})">
|
||||
<div slot="trigger" class="color swatch" :style="{ colorScheme: tint > 60 ? 'light' : 'dark' }">
|
||||
<wa-icon class="pinned-icon" name="thumbtack" variant="regular" v-if="seedColors[colorToIndex[hue][tint]]"></wa-icon>
|
||||
<wa-icon name="sliders-simple" class="tweak-icon"></wa-icon>
|
||||
</div>
|
||||
<template #content v-if="isCustom && seedHues[hue] && (tint == '95' || tint == '05' || seedColors[colorToIndex[hue][tint]]) && tweakBase[hue][tint]">
|
||||
<color-slider v-if="HUE_RANGES[hue]" v-model:color="colors[hue][tint]"
|
||||
:default-value="colors[hue][tweakBase[hue][tint]].oklch.h"
|
||||
@input="!seedColors[colorToIndex[hue][tint]] ? addColor({hue, pinnedHue: hue, level: tint}) : null"
|
||||
@update:color="seedColors[colorToIndex[hue][tint]] ? seedColors[colorToIndex[hue][tint]].color = $event : null"
|
||||
coord="h"
|
||||
:min="HUE_RANGES[hue].mid - 70" :max="HUE_RANGES[hue].mid + 70" :step="1"
|
||||
label="Hue shift" :label-min="moreHue[hueBefore[hue]]" :label-max="moreHue[hueAfter[hue]]"
|
||||
:label-default="`${capitalize(hue)} ${tweakBase[hue][tint]}`"
|
||||
format-type="shift"
|
||||
></color-slider>
|
||||
<color-slider v-if="hue != 'gray'" v-model:color="colors[hue][tint]"
|
||||
:default-value="colors[hue][tweakBase[hue][tint]].oklch.c"
|
||||
@input="!seedColors[colorToIndex[hue][tint]] ? addColor({hue, pinnedHue: hue, level: tint}) : null"
|
||||
@update:color="seedColors[colorToIndex[hue][tint]] ? seedColors[colorToIndex[hue][tint]].color = $event : null"
|
||||
coord="c"
|
||||
:min="coreColors.gray.oklch.c + 0.001"
|
||||
:max="tint == coreLevels[hue] ? maxChroma(colors[hue][tweakBase[hue][tint]].oklch.l, colors[hue][tweakBase[hue][tint]].oklch.h) : coreColors[hue].oklch.c - 0.001" :step="0.001"
|
||||
label="Colorfulness" label-min="More muted" label-max="More vibrant"
|
||||
format-type="scale"
|
||||
:label-default="`${capitalize(hue)} ${tweakBase[hue][tint]}`"
|
||||
></color-slider>
|
||||
</template>
|
||||
</color-popup>
|
||||
</td>
|
||||
</tr>
|
||||
{% endraw %}
|
||||
</tbody>
|
||||
{% for hue in hues -%}
|
||||
<tr>
|
||||
<th>{{ hue | capitalize }}</th>
|
||||
{% for tint in tints -%}
|
||||
<td>
|
||||
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint }})">
|
||||
<wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button>
|
||||
</div>
|
||||
</td>
|
||||
{%- endfor -%}
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
</table>
|
||||
|
||||
<color-slider v-if="!isCustom" :class="{ tweaked: chromaScale !== 1 }"
|
||||
type="scale"
|
||||
v-model="chromaScale"
|
||||
coord="c"
|
||||
:default-color="baseMaxChromaColor"
|
||||
:default-value="baseMaxChroma"
|
||||
:min="MAX_CHROMA_BOUNDS.min" :max="MAX_CHROMA_BOUNDS.max" :step="0.01"
|
||||
label="Overall colorfulness" label-min="More muted" label-max="More vibrant"
|
||||
></color-slider>
|
||||
|
||||
{% if page.fileSlug != 'custom' %}
|
||||
<h2>Used By</h2>
|
||||
|
||||
<section class="index-grid">
|
||||
@@ -238,7 +43,6 @@
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% markdown %}
|
||||
## Color Contrast
|
||||
@@ -254,7 +58,6 @@ A difference of `40` ensures a minimum **3:1** contrast ratio, suitable for larg
|
||||
{% endmarkdown %}
|
||||
|
||||
{% set difference = 40 %}
|
||||
{% set minContrast = 3 %}
|
||||
{% include "contrast-table.njk" %}
|
||||
|
||||
{% markdown %}
|
||||
@@ -274,7 +77,6 @@ A difference of `50` ensures a minimum **4.5:1** contrast ratio, suitable for no
|
||||
{% endmarkdown %}
|
||||
|
||||
{% set difference = 50 %}
|
||||
{% set minContrast = 4.5 %}
|
||||
{% include "contrast-table.njk" %}
|
||||
|
||||
{% markdown %}
|
||||
@@ -293,7 +95,6 @@ A difference of `60` ensures a minimum **7:1** contrast ratio, suitable for all
|
||||
{% endmarkdown %}
|
||||
|
||||
{% set difference = 60 %}
|
||||
{% set minContrast = 7 %}
|
||||
{% include "contrast-table.njk" %}
|
||||
|
||||
{% markdown %}
|
||||
@@ -306,48 +107,13 @@ This also goes for a difference of `65`:
|
||||
{% include "contrast-table.njk" %}
|
||||
|
||||
{% markdown %}
|
||||
## How to use this palette { #usage }
|
||||
## How to use this palette
|
||||
|
||||
If you are using a Web Awesome theme that uses this palette, it will already be included.
|
||||
To use a different palette than a theme default, or to use it in a custom theme, you can import this palette directly from the Web Awesome CDN.
|
||||
|
||||
{% set stylesheet = 'styles/color/' + page.fileSlug + '.css' %}
|
||||
<wa-tab-group class="import-stylesheet-code">
|
||||
<wa-tab panel="html">In HTML</wa-tab>
|
||||
<wa-tab panel="css">In CSS</wa-tab>
|
||||
<wa-tab-panel name="html">
|
||||
|
||||
Add the following code to the `<head>` of your page:
|
||||
```html { v-content:html="code.html.highlighted" }
|
||||
<link rel="stylesheet" href="{% cdnUrl stylesheet %}" />
|
||||
```
|
||||
</wa-tab-panel>
|
||||
<wa-tab-panel name="css">
|
||||
|
||||
Add the following code at the top of your CSS file:
|
||||
```css { v-content:html="code.css.highlighted" }
|
||||
@import url('{% cdnUrl stylesheet %}');
|
||||
```
|
||||
</wa-tab-panel>
|
||||
</wa-tab-group>
|
||||
{% include 'import-stylesheet-code.md.njk' %}
|
||||
|
||||
{% endmarkdown %}
|
||||
|
||||
<section id="saved" class="index-grid" v-if="savedVariations?.length">
|
||||
<h2 class="index-category">Saved {{ 'custom palettes' if page.fileSlug == 'custom' else title + ' variations' }}</h2>
|
||||
<a v-for="palette of savedVariations" :href="'/docs/palettes/' + palette.id">
|
||||
<wa-card with-header>
|
||||
<div slot="header">
|
||||
{# {% include "svgs/palette.njk" %} #}
|
||||
{% include "svgs/thumbnail-placeholder.njk" %}
|
||||
</div>
|
||||
<span class="page-name" v-text="palette.title"></span>
|
||||
</wa-card>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
|
||||
</div></div> {# end palette app #}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
@@ -4,143 +4,100 @@
|
||||
|
||||
{% extends '../_includes/base.njk' %}
|
||||
|
||||
{% block head %}
|
||||
<script>
|
||||
globalThis.wa_data ??= {};
|
||||
wa_data.baseTheme = "{{ page.fileSlug }}";
|
||||
wa_data.themes = {
|
||||
{% for theme in collections.theme -%}
|
||||
"{{ theme.fileSlug }}": {
|
||||
"title": "{{ theme.data.title }}",
|
||||
"palette": "{{ theme.data.palette }}",
|
||||
"brand": "{{ theme.data.brand }}"
|
||||
},
|
||||
{% endfor %}
|
||||
};
|
||||
wa_data.palettes = {
|
||||
{% for palette in collections.palette -%}
|
||||
"{{ palette.fileSlug }}": {
|
||||
"title": "{{ palette.data.title }}",
|
||||
},
|
||||
{% endfor %}
|
||||
};
|
||||
</script>
|
||||
<link href="{{ page.url }}../remix.css" rel="stylesheet">
|
||||
<script src="{{ page.url }}../remix.js" type="module"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
|
||||
<iframe src='{{ page.url }}demo.html' id="demo"></iframe>
|
||||
|
||||
<wa-details id="mix_and_match" class="wa-gap-m" >
|
||||
<h4 slot="summary" data-no-anchor data-no-outline id="remix">
|
||||
<wa-icon name="arrows-rotate"></wa-icon>
|
||||
Remix this theme
|
||||
<wa-icon id="what-is-remixing" href="#remixing" name="circle-question" slot="suffix" variant="regular"></wa-icon>
|
||||
<wa-tooltip for="what-is-remixing">Customize this theme by changing its colors and/or remixing it with design elements from other themes!</wa-tooltip>
|
||||
</h4>
|
||||
|
||||
<wa-select name="colors" label="Colors from…" value="" clearable>
|
||||
<p id="mix_and_match" class="wa-gap-m">
|
||||
<strong>
|
||||
<wa-icon name="merge" slot="prefix"></wa-icon>
|
||||
Remix
|
||||
<wa-icon-button href="#remixing" name="circle-question" slot="suffix" variant="regular" label="How to use?"></wa-icon-button>
|
||||
</strong>
|
||||
<wa-select name="colors" label="Colors from…" size="small">
|
||||
<wa-icon name="palette" slot="prefix" variant="regular"></wa-icon>
|
||||
<wa-option value="">(Theme default)</wa-option>
|
||||
<wa-divider></wa-divider>
|
||||
{% for theme in collections.theme | sort %}
|
||||
{% set currentTheme = theme.fileSlug == page.fileSlug %}
|
||||
<wa-option label="{{ theme.data.title }}" value="{{ theme.fileSlug if not currentTheme }}" {{ (theme.fileSlug if currentTheme) | attr('data-id') }}>
|
||||
<wa-card with-header>
|
||||
<div slot="header">
|
||||
{% include "svgs/theme-color.njk" %}
|
||||
</div>
|
||||
<span class="page-name">
|
||||
{{ theme.data.title }}
|
||||
{% if theme.data.isPro %}<wa-badge class="pro">PRO</wa-badge>{% endif %}
|
||||
{% if currentTheme %}<wa-badge variant="neutral" appearance="outlined">This theme</wa-badge>{% endif %}
|
||||
</span>
|
||||
</wa-card>
|
||||
</wa-option>
|
||||
{% if theme.fileSlug !== page.fileSlug %}
|
||||
<wa-option value="{{ theme.fileSlug }}">{{ theme.data.title }}</wa-option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</wa-select>
|
||||
|
||||
<wa-select name="palette" label="Palette" clearable>
|
||||
<wa-select name="palette" label="Palette" size="small">
|
||||
<wa-icon name="swatchbook" slot="prefix" variant="regular"></wa-icon>
|
||||
{% set defaultPalette = palette %}
|
||||
{% for palette in collections.palette | sort %}
|
||||
{% set currentPalette = palette.fileSlug == defaultPalette %}
|
||||
<wa-option label="{{ palette.data.title }}" value="{{ palette.fileSlug if not currentPalette }}" {{ (palette.fileSlug if currentPalette) | attr('data-id') }}>
|
||||
<wa-card with-header>
|
||||
<div slot="header">
|
||||
{% include "svgs/" + (palette.data.icon or "thumbnail-placeholder") + ".njk" ignore missing %}
|
||||
</div>
|
||||
<span class="page-name">
|
||||
{{ palette.data.title }}
|
||||
{% if palette.data.isPro %}<wa-badge class="pro">PRO</wa-badge>{% endif %}
|
||||
{% if currentPalette %}<wa-badge variant="neutral" appearance="outlined">Theme default</wa-badge>{% endif %}
|
||||
</span>
|
||||
</wa-card>
|
||||
</wa-option>
|
||||
{% endfor %}
|
||||
{% set palette = defaultPalette %}
|
||||
</wa-select>
|
||||
|
||||
<wa-select class="color-select" name="brand" label="Brand color" value="" clearable>
|
||||
<div class="selected-swatch" slot="prefix"></div>
|
||||
{% for hue in hues %}
|
||||
{% set currentBrand = hue == brand %}
|
||||
<wa-option label="{{ hue | capitalize }}" value="{{ hue if not currentBrand }}" {{ (hue if currentBrand) | attr('data-id') }} style="--color: var(--wa-color-{{ hue }})">
|
||||
{{ hue | capitalize }}
|
||||
{% if currentBrand %}<wa-badge variant="neutral" appearance="outlined">Theme default</wa-badge>{% endif %}
|
||||
</wa-option>
|
||||
<wa-option value="">(Theme default)</wa-option>
|
||||
<wa-divider></wa-divider>
|
||||
{% for p in collections.palette | sort %}
|
||||
{% if p.fileSlug !== palette %}
|
||||
<wa-option value="{{ p.fileSlug }}">{{ p.data.title }}</wa-option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</wa-select>
|
||||
|
||||
<wa-select name="typography" label="Typography from…" clearable>
|
||||
<wa-select name="typography" label="Typography from…" size="small">
|
||||
<wa-icon name="font-case" slot="prefix"></wa-icon>
|
||||
<wa-option value="">(Theme default)</wa-option>
|
||||
<wa-divider></wa-divider>
|
||||
{% for theme in collections.theme | sort %}
|
||||
{% set currentTheme = theme.fileSlug == page.fileSlug %}
|
||||
<wa-option label="{{ theme.data.title }}" value="{{ theme.fileSlug if not currentTheme }}" {{ (theme.fileSlug if currentTheme) | attr('data-id') }}>
|
||||
<wa-card with-header>
|
||||
<div slot="header">
|
||||
{% include "svgs/theme-typography.njk" %}
|
||||
</div>
|
||||
<span class="page-name">
|
||||
{{ theme.data.title }}
|
||||
{% if theme.data.isPro %}<wa-badge class="pro">PRO</wa-badge>{% endif %}
|
||||
{% if currentTheme %}<wa-badge variant="neutral" appearance="outlined">This theme</wa-badge>{% endif %}
|
||||
</span>
|
||||
</wa-card>
|
||||
</wa-option>
|
||||
{% if theme.fileSlug !== page.fileSlug %}
|
||||
<wa-option value="{{ theme.fileSlug }}">{{ theme.data.title }}</wa-option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</wa-select>
|
||||
</wa-details>
|
||||
|
||||
<h2>Color</h2>
|
||||
|
||||
</p>
|
||||
<script>
|
||||
document.querySelector('#mix_and_match').addEventListener('change', function(event) {
|
||||
let selects = document.querySelectorAll('#mix_and_match wa-select');
|
||||
let url = new URL(demo.src);
|
||||
|
||||
for (let select of selects) {
|
||||
url.searchParams.set(select.name, select.value);
|
||||
}
|
||||
|
||||
demo.src = url;
|
||||
});
|
||||
</script>
|
||||
|
||||
<h2>Default Color Palette</h2>
|
||||
{% set paletteURL = '/docs/palettes/' + palette + '/' %}
|
||||
|
||||
|
||||
{% set themePage = page %}
|
||||
{% set page = paletteURL | getCollectionItemFromUrl %}
|
||||
<div class="index-grid">
|
||||
{% set themePage = page %}
|
||||
{% set page = paletteURL | getCollectionItemFromUrl %}
|
||||
{% set pageSubtitle = "Default color palette" %}
|
||||
{% include 'page-card.njk' %}
|
||||
{% set page = themePage %}
|
||||
|
||||
<wa-card style="--header-background: var(--wa-color-{{ brand }})" class="wa-palette-{{ palette }}">
|
||||
<div slot="header"></div>
|
||||
<div class="page-name">{{ brand | capitalize }}</div>
|
||||
<div class="wa-caption-s">Default brand color</div>
|
||||
</wa-card>
|
||||
{% include 'page-card.njk' %}
|
||||
</div>
|
||||
{% set page = themePage %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block afterContent %}
|
||||
|
||||
<h2 id="usage">How to use this theme</h2>
|
||||
|
||||
{% markdown %}
|
||||
## How to use this theme
|
||||
|
||||
You can import this theme from the Web Awesome CDN.
|
||||
|
||||
{% set stylesheet = 'styles/themes/' + page.fileSlug + '.css' %}
|
||||
{% include 'import-stylesheet-code.md.njk' %}
|
||||
|
||||
### Remixing { #remixing }
|
||||
|
||||
If you want to combine the **colors** from this theme with another theme, you can import this CSS file *after* the other theme’s CSS file:
|
||||
|
||||
{% set stylesheet = 'styles/themes/' + page.fileSlug + '/color.css' %}
|
||||
{% include 'import-stylesheet-code.md.njk' %}
|
||||
|
||||
To use the **typography** from this theme with another theme, you can import this CSS file *after* the other theme’s CSS file:
|
||||
|
||||
{% set stylesheet = 'styles/themes/' + page.fileSlug + '/typography.css' %}
|
||||
{% include 'import-stylesheet-code.md.njk' %}
|
||||
|
||||
<wa-callout variant="warning">
|
||||
<wa-icon slot="icon" name="triangle-exclamation" variant="regular"></wa-icon>
|
||||
Please note that not all combinations will look good — once you’re mixing and matching, you’re on your own!
|
||||
</wa-callout>
|
||||
|
||||
## Dark mode
|
||||
|
||||
To activate the dark color scheme of the theme on any element and its contents, apply the class `wa-dark` to it.
|
||||
|
||||
@@ -37,8 +37,7 @@ export function anchorHeadingsPlugin(options = {}) {
|
||||
}
|
||||
|
||||
// Look for headings
|
||||
let selector = `:is(${options.headingSelector}):not([data-no-anchor], [data-no-anchor] *)`;
|
||||
container.querySelectorAll(selector).forEach(heading => {
|
||||
container.querySelectorAll(options.headingSelector).forEach(heading => {
|
||||
const hasAnchor = heading.querySelector('a');
|
||||
const existingId = heading.getAttribute('id');
|
||||
const clone = parse(heading.outerHTML);
|
||||
|
||||
@@ -3,39 +3,30 @@ import { parse } from 'node-html-parser';
|
||||
/**
|
||||
* Eleventy plugin to add copy buttons to code blocks.
|
||||
*/
|
||||
export function copyCodePlugin(eleventyConfig, options = {}) {
|
||||
export function copyCodePlugin(options = {}) {
|
||||
options = {
|
||||
container: 'body',
|
||||
...options,
|
||||
};
|
||||
|
||||
let codeCount = 0;
|
||||
eleventyConfig.addTransform('copy-code', content => {
|
||||
const doc = parse(content, { blockTextElements: { code: true } });
|
||||
const container = doc.querySelector(options.container);
|
||||
return function (eleventyConfig) {
|
||||
eleventyConfig.addTransform('copy-code', content => {
|
||||
const doc = parse(content, { blockTextElements: { code: true } });
|
||||
const container = doc.querySelector(options.container);
|
||||
|
||||
if (!container) {
|
||||
return content;
|
||||
}
|
||||
|
||||
// Look for code blocks
|
||||
container.querySelectorAll('pre > code').forEach(code => {
|
||||
const pre = code.closest('pre');
|
||||
let preId = pre.getAttribute('id') || `code-block-${++codeCount}`;
|
||||
let codeId = code.getAttribute('id') || `${preId}-inner`;
|
||||
|
||||
if (!code.getAttribute('id')) {
|
||||
code.setAttribute('id', codeId);
|
||||
}
|
||||
if (!pre.getAttribute('id')) {
|
||||
pre.setAttribute('id', preId);
|
||||
if (!container) {
|
||||
return content;
|
||||
}
|
||||
|
||||
// Add a copy button
|
||||
pre.innerHTML += `<wa-icon-button href="#${preId}" class="block-link-icon" name="link"></wa-icon-button>
|
||||
<wa-copy-button from="${codeId}" class="copy-button" hoist></wa-copy-button>`;
|
||||
// Look for code blocks
|
||||
container.querySelectorAll('pre > code').forEach(code => {
|
||||
const pre = code.closest('pre');
|
||||
|
||||
// Add a copy button (we set the copy data at runtime to reduce page bloat)
|
||||
pre.innerHTML = `<wa-copy-button class="copy-button" hoist></wa-copy-button>` + pre.innerHTML;
|
||||
});
|
||||
|
||||
return doc.toString();
|
||||
});
|
||||
|
||||
return doc.toString();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,9 +29,6 @@ function getCollection(name) {
|
||||
}
|
||||
|
||||
export function getCollectionItemFromUrl(url, collection) {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
collection ??= getCollection.call(this, 'all') || [];
|
||||
return collection.find(item => item.url === url);
|
||||
}
|
||||
@@ -45,33 +42,35 @@ export function split(text, separator) {
|
||||
return (text + '').split(separator).filter(Boolean);
|
||||
}
|
||||
|
||||
export function ancestors(url, { withCurrent = false, withRoot = false } = {}) {
|
||||
let ret = [];
|
||||
let currentUrl = url;
|
||||
let currentItem = getCollectionItemFromUrl.call(this, url);
|
||||
export function breadcrumbs(url, { withCurrent = false } = {}) {
|
||||
const parts = split(url, '/');
|
||||
const ret = [];
|
||||
|
||||
if (!currentItem) {
|
||||
// Might have eleventyExcludeFromCollections, jump to parent
|
||||
let parentUrl = this.ctx.parentUrl;
|
||||
if (parentUrl) {
|
||||
url = parentUrl;
|
||||
while (parts.length) {
|
||||
let partialUrl = '/' + parts.join('/') + '/';
|
||||
let item = getCollectionItemFromUrl.call(this, partialUrl);
|
||||
|
||||
if (item && (partialUrl !== url || withCurrent)) {
|
||||
let title = item.data.title;
|
||||
if (title) {
|
||||
ret.unshift({ url: partialUrl, title });
|
||||
}
|
||||
}
|
||||
|
||||
parts.pop();
|
||||
|
||||
if (item?.data.parent) {
|
||||
let parentURL = item.data.parent;
|
||||
if (!item.data.parent.startsWith('/')) {
|
||||
// Parent is in the same directory
|
||||
parts.push(item.data.parent);
|
||||
parentURL = '/' + parts.join('/') + '/';
|
||||
}
|
||||
|
||||
let parentBreadcrumbs = breadcrumbs.call(this, parentURL, { withCurrent: true });
|
||||
return [...parentBreadcrumbs, ...ret];
|
||||
}
|
||||
}
|
||||
|
||||
for (let item; (item = getCollectionItemFromUrl.call(this, url)); url = item.data.parentUrl) {
|
||||
ret.unshift(item);
|
||||
}
|
||||
|
||||
if (!withRoot && ret[0]?.page.url === '/') {
|
||||
// Remove root
|
||||
ret.shift();
|
||||
}
|
||||
|
||||
if (!withCurrent && ret.at(-1)?.page.url === currentUrl) {
|
||||
// Remove current page
|
||||
ret.pop();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -181,178 +180,69 @@ export function sort(arr, by = { 'data.order': 1, 'data.title': '' }) {
|
||||
/**
|
||||
* Group an 11ty collection (or any array of objects with a `data.tags` property) by certain tags.
|
||||
* @param {object[]} collection
|
||||
* @param { Object<string, string> | string[]} [options] Options object or array of tags to group by.
|
||||
* @param {string[] | true} [options.tags] Tags to group by. If true, groups by all tags.
|
||||
* If not provided/empty, defaults to grouping by page hierarchy, with any pages with more than 1 children becoming groups.
|
||||
* @param {string[]} [options.groups] The groups to use if only a subset or a specific order is desired. Defaults to `options.tags`.
|
||||
* @param {string[]} [options.titles] Any title overrides for groups.
|
||||
* @param {string | false} [options.other="Other"] The title to use for the "Other" group. If `false`, the "Other" group is removed..
|
||||
* @returns { Object.<string, object[]> } An object of group ids to arrays of page objects.
|
||||
* @param { Object<string, string> | (string | Object<string, string>)[]} [tags] The tags to group by. If not provided/empty, defaults to grouping by all tags.
|
||||
* @returns { Object.<string, object[]> } An object with keys for each tag, and an array of items for each tag.
|
||||
*/
|
||||
export function groupPages(collection, options = {}, page) {
|
||||
export function groupByTags(collection, tags) {
|
||||
if (!collection) {
|
||||
console.error(`Empty collection passed to groupPages() to group by ${JSON.stringify(options)}`);
|
||||
console.error(`Empty collection passed to groupByTags() to group by ${JSON.stringify(tags)}`);
|
||||
}
|
||||
if (!tags) {
|
||||
// Default to grouping by union of all tags
|
||||
tags = Array.from(new Set(collection.flatMap(item => item.data.tags)));
|
||||
} else if (Array.isArray(tags)) {
|
||||
// May contain objects of one-off tag -> label mappings
|
||||
tags = tags.map(tag => (typeof tag === 'object' ? Object.keys(tag)[0] : tag));
|
||||
} else if (typeof tags === 'object') {
|
||||
// tags is an object of tags to labels, so we just want the keys
|
||||
tags = Object.keys(tags);
|
||||
}
|
||||
|
||||
if (Array.isArray(options)) {
|
||||
options = { tags: options };
|
||||
}
|
||||
|
||||
let { tags, groups, titles = {}, other = 'Other' } = options;
|
||||
|
||||
if (groups === undefined && Array.isArray(tags)) {
|
||||
groups = tags;
|
||||
}
|
||||
|
||||
let grouping;
|
||||
|
||||
if (tags) {
|
||||
grouping = {
|
||||
isGroup: item => undefined,
|
||||
getCandidateGroups: item => item.data.tags,
|
||||
getGroupMeta: group => ({}),
|
||||
};
|
||||
} else {
|
||||
grouping = {
|
||||
isGroup: item => (item.data.children.length >= 2 ? item.page.url : undefined),
|
||||
getCandidateGroups: item => {
|
||||
let parentUrl = item.data.parentUrl;
|
||||
if (page?.url === parentUrl) {
|
||||
return [];
|
||||
}
|
||||
return [parentUrl];
|
||||
},
|
||||
getGroupMeta: group => {
|
||||
let item = byUrl[group] || getCollectionItemFromUrl.call(this, group);
|
||||
return {
|
||||
title: item?.data.title,
|
||||
url: group,
|
||||
item,
|
||||
};
|
||||
},
|
||||
sortGroups: groups => sort(groups.map(url => byUrl[url]).filter(Boolean)).map(item => item.page.url),
|
||||
};
|
||||
}
|
||||
|
||||
let byUrl = {};
|
||||
let byParentUrl = {};
|
||||
let ret = Object.fromEntries(tags.map(tag => [tag, []]));
|
||||
ret.other = [];
|
||||
|
||||
for (let item of collection) {
|
||||
let url = item.page.url;
|
||||
let parentUrl = item.data.parentUrl;
|
||||
let categorized = false;
|
||||
|
||||
byUrl[url] = item;
|
||||
|
||||
if (parentUrl) {
|
||||
byParentUrl[parentUrl] ??= [];
|
||||
byParentUrl[parentUrl].push(item);
|
||||
}
|
||||
}
|
||||
|
||||
let urlToGroups = {};
|
||||
|
||||
for (let item of collection) {
|
||||
let url = item.page.url;
|
||||
let parentUrl = item.data.parentUrl;
|
||||
|
||||
if (grouping.isGroup(item)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parentItem = byUrl[parentUrl];
|
||||
if (parentItem && !grouping.isGroup(parentItem)) {
|
||||
// Their parent is also here and is not a group
|
||||
continue;
|
||||
}
|
||||
|
||||
let candidateGroups = grouping.getCandidateGroups(item);
|
||||
|
||||
if (groups) {
|
||||
candidateGroups = candidateGroups.filter(group => groups.includes(group));
|
||||
}
|
||||
|
||||
urlToGroups[url] ??= [];
|
||||
|
||||
for (let group of candidateGroups) {
|
||||
urlToGroups[url].push(group);
|
||||
}
|
||||
}
|
||||
|
||||
let ret = {};
|
||||
|
||||
for (let url in urlToGroups) {
|
||||
let groups = urlToGroups[url];
|
||||
let item = byUrl[url];
|
||||
|
||||
if (groups.length === 0) {
|
||||
// Not filtered out but also not categorized
|
||||
groups = ['other'];
|
||||
}
|
||||
|
||||
for (let group of groups) {
|
||||
ret[group] ??= [];
|
||||
ret[group].push(item);
|
||||
|
||||
if (!ret[group].meta) {
|
||||
if (group === 'other') {
|
||||
ret[group].meta = { title: other };
|
||||
} else {
|
||||
ret[group].meta = grouping.getGroupMeta(group);
|
||||
ret[group].meta.title = titles[group] ?? ret[group].meta.title ?? capitalize(group);
|
||||
}
|
||||
for (let tag of tags) {
|
||||
if (item.data.tags.includes(tag)) {
|
||||
ret[tag].push(item);
|
||||
categorized = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (other === false) {
|
||||
delete ret.other;
|
||||
}
|
||||
|
||||
// Sort
|
||||
let sortedGroups = groups ?? grouping.sortGroups?.(Object.keys(ret));
|
||||
|
||||
if (sortedGroups) {
|
||||
ret = sortObject(ret, sortedGroups);
|
||||
}
|
||||
|
||||
Object.defineProperty(ret, 'meta', {
|
||||
value: {
|
||||
groupCount: Object.keys(ret).length,
|
||||
},
|
||||
enumerable: false,
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort an object by its keys
|
||||
* @param {*} obj
|
||||
* @param {function | string[]} order
|
||||
*/
|
||||
function sortObject(obj, order) {
|
||||
let ret = {};
|
||||
let sortedKeys = Array.isArray(order) ? order : Object.keys(obj).sort(order);
|
||||
|
||||
for (let key of sortedKeys) {
|
||||
if (key in obj) {
|
||||
ret[key] = obj[key];
|
||||
if (!categorized) {
|
||||
ret.other.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Add any keys that weren't in the order
|
||||
for (let key in obj) {
|
||||
if (!(key in ret)) {
|
||||
ret[key] = obj[key];
|
||||
// Remove empty categories
|
||||
for (let category in ret) {
|
||||
if (ret[category].length === 0) {
|
||||
delete ret[category];
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function capitalize(str) {
|
||||
str += '';
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
export function getCategoryTitle(category, categories) {
|
||||
let title;
|
||||
if (Array.isArray(categories)) {
|
||||
// Find relevant entry
|
||||
// [{id: "Title"}, id2, ...]
|
||||
title = categories.find(entry => typeof entry === 'object' && entry?.[category])?.[category];
|
||||
} else if (typeof categories === 'object') {
|
||||
// {id: "Title", id2: "Title 2", ...}
|
||||
title = categories[category];
|
||||
}
|
||||
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
|
||||
// Capitalized
|
||||
return category.charAt(0).toUpperCase() + category.slice(1);
|
||||
}
|
||||
|
||||
const IDENTITY = x => x;
|
||||
|
||||
15
docs/assets/scripts/copy-code.js
Normal file
15
docs/assets/scripts/copy-code.js
Normal file
@@ -0,0 +1,15 @@
|
||||
function setCopyValue() {
|
||||
document.querySelectorAll('.copy-button').forEach(copyButton => {
|
||||
const pre = copyButton.closest('pre');
|
||||
const code = pre?.querySelector('code');
|
||||
|
||||
if (code) {
|
||||
copyButton.value = code.textContent;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set data for all copy buttons when the page loads
|
||||
setCopyValue();
|
||||
|
||||
document.addEventListener('turbo:load', setCopyValue);
|
||||
File diff suppressed because one or more lines are too long
@@ -1,8 +0,0 @@
|
||||
globalThis.Prism = globalThis.Prism || {};
|
||||
globalThis.Prism.manual = true;
|
||||
|
||||
await import('./prism-downloaded.js');
|
||||
|
||||
Prism.plugins.customClass.prefix('code-');
|
||||
|
||||
export default Prism;
|
||||
@@ -1,269 +0,0 @@
|
||||
const sidebar = (globalThis.sidebar = {});
|
||||
|
||||
sidebar.palettes = {
|
||||
render() {
|
||||
if (this.saved.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let palette of this.saved) {
|
||||
sidebar.palette.render(palette);
|
||||
}
|
||||
|
||||
sidebar.updateCurrent();
|
||||
},
|
||||
|
||||
saved: [],
|
||||
|
||||
/**
|
||||
* Update saved palettes from local storage
|
||||
*/
|
||||
fromLocalStorage() {
|
||||
// Replace contents of array without breaking references
|
||||
let saved = localStorage.savedPalettes ? JSON.parse(localStorage.savedPalettes) : [];
|
||||
this.saved.splice(0, this.saved.length, ...saved);
|
||||
},
|
||||
|
||||
/**
|
||||
* Write palettes to local storage
|
||||
*/
|
||||
toLocalStorage() {
|
||||
if (this.saved.length > 0) {
|
||||
localStorage.savedPalettes = JSON.stringify(this.saved);
|
||||
} else {
|
||||
delete localStorage.savedPalettes;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
sidebar.palettes.fromLocalStorage();
|
||||
|
||||
// Palettes were updated in another tab
|
||||
addEventListener('storage', () => sidebar.palettes.fromLocalStorage());
|
||||
|
||||
sidebar.palette = {
|
||||
getUid() {
|
||||
let savedPalettes = sidebar.palettes.saved;
|
||||
let uids = new Set(savedPalettes.map(p => p.uid));
|
||||
|
||||
if (savedPalettes.length === 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Find first available number
|
||||
for (let i = 1; i <= savedPalettes.length + 1; i++) {
|
||||
if (!uids.has(i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
equals(p1, p2) {
|
||||
if (!p1 || !p2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return p1.id === p2.id && p1.uid === p2.uid;
|
||||
},
|
||||
|
||||
delete(palette) {
|
||||
let savedPalettes = sidebar.palettes.saved;
|
||||
let count = savedPalettes.length;
|
||||
|
||||
if (count === 0 || !palette.uid) {
|
||||
// No stored palettes or this palette has not been saved
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO improve UX of this
|
||||
if (!confirm(`Are you sure you want to delete palette “${palette.title}”?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let index; index > -1; index = savedPalettes.findIndex(p => p.uid === palette.uid)) {
|
||||
savedPalettes.splice(index, 1);
|
||||
}
|
||||
|
||||
if (savedPalettes.length === count) {
|
||||
// Nothing was removed
|
||||
return;
|
||||
}
|
||||
|
||||
// Update UI
|
||||
let pathname = `/docs/palettes/${palette.id}/`;
|
||||
let url = pathname + palette.search;
|
||||
let uls = new Set();
|
||||
|
||||
for (let a of document.querySelectorAll(`#sidebar a[href="${url}"]`)) {
|
||||
let li = a.closest('li');
|
||||
let ul = li.closest('ul');
|
||||
uls.add(ul);
|
||||
li.remove();
|
||||
}
|
||||
|
||||
// Remove empty lists
|
||||
for (let ul of uls) {
|
||||
if (!ul.children.length) {
|
||||
ul.remove();
|
||||
}
|
||||
}
|
||||
|
||||
sidebar.updateCurrent();
|
||||
|
||||
sidebar.palettes.toLocalStorage();
|
||||
|
||||
if (globalThis.paletteApp?.saved?.uid === palette.uid) {
|
||||
// We deleted the currently active palette
|
||||
paletteApp.postDelete();
|
||||
}
|
||||
},
|
||||
|
||||
render(palette) {
|
||||
// Find existing <a>
|
||||
let { title, id, search, uid } = palette;
|
||||
|
||||
for (let a of document.querySelectorAll(`#sidebar a[href^="/docs/palettes/${id}/"][data-uid="${uid}"]`)) {
|
||||
// Palette already in sidebar, just update it
|
||||
a.textContent = palette.title;
|
||||
a.href = `/docs/palettes/${id}/${search}`;
|
||||
return;
|
||||
}
|
||||
|
||||
let pathname = `/docs/palettes/${id}/`;
|
||||
let url = pathname + search;
|
||||
let parentA = document.querySelector(`a[href="${pathname}"]`);
|
||||
let parentLi = parentA?.closest('li');
|
||||
let a;
|
||||
|
||||
if (parentLi) {
|
||||
a = Object.assign(document.createElement('a'), { href: url, textContent: title });
|
||||
a.dataset.uid = uid;
|
||||
let badges = [...parentLi.querySelectorAll('wa-badge')].map(badge => badge.cloneNode(true));
|
||||
let ul = parentLi.querySelector('ul') ?? parentLi.appendChild(document.createElement('ul'));
|
||||
let li = document.createElement('li');
|
||||
let deleteButton = Object.assign(document.createElement('wa-icon-button'), {
|
||||
name: 'trash',
|
||||
label: 'Delete',
|
||||
className: 'delete',
|
||||
});
|
||||
|
||||
deleteButton.addEventListener('click', () => {
|
||||
let palette = { id, uid, title: a.textContent, search: a.search };
|
||||
sidebar.palette.delete(palette);
|
||||
});
|
||||
|
||||
li.append(a, ' ', ...badges, deleteButton);
|
||||
ul.appendChild(li);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Save a palette, either by updating its existing entry or creating a new one
|
||||
* @param {object} palette
|
||||
*/
|
||||
save(palette) {
|
||||
if (!palette.uid) {
|
||||
// First time saving
|
||||
palette.uid = this.getUid();
|
||||
}
|
||||
|
||||
let savedPalettes = sidebar.palettes.saved;
|
||||
let existingIndex = palette.uid ? sidebar.palettes.saved.findIndex(p => p.uid === palette.uid) : -1;
|
||||
let newIndex = existingIndex > -1 ? existingIndex : savedPalettes.length;
|
||||
|
||||
let [oldValues] = sidebar.palettes.saved.splice(newIndex, 1, palette);
|
||||
|
||||
this.render(palette, oldValues);
|
||||
sidebar.updateCurrent();
|
||||
sidebar.palettes.toLocalStorage();
|
||||
|
||||
return palette;
|
||||
},
|
||||
};
|
||||
|
||||
sidebar.updateCurrent = function () {
|
||||
// Find the sidebar link with the longest shared prefix with the current URL
|
||||
let pathParts = location.pathname.split('/').filter(Boolean);
|
||||
let prefixes = [];
|
||||
|
||||
if (pathParts.length === 1) {
|
||||
// If at /docs/ we just use that, otherwise we want at least two parts (/docs/xxx/)
|
||||
prefixes.push('/' + pathParts[0] + '/');
|
||||
} else {
|
||||
for (let i = 2; i <= pathParts.length; i++) {
|
||||
prefixes.push('/' + pathParts.slice(0, i).join('/') + '/');
|
||||
}
|
||||
}
|
||||
|
||||
// Last prefix includes the search too (if any)
|
||||
if (location.search) {
|
||||
let params = new URLSearchParams(location.search);
|
||||
params.sort();
|
||||
prefixes.push(prefixes.at(-1) + location.search);
|
||||
}
|
||||
|
||||
// We want to start from the longest prefix
|
||||
prefixes.reverse();
|
||||
let candidates;
|
||||
let matchingPrefix;
|
||||
|
||||
for (let prefix of prefixes) {
|
||||
candidates = document.querySelectorAll(`#sidebar a[href^="${prefix}"]`);
|
||||
|
||||
if (candidates.length > 0) {
|
||||
matchingPrefix = prefix;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchingPrefix) {
|
||||
// Abort mission
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchingPrefix === pathParts.at(-1)) {
|
||||
// Full path matches, check search
|
||||
if (location.search) {
|
||||
candidates = [...candidates];
|
||||
|
||||
let searchParams = new URLSearchParams(location.search);
|
||||
|
||||
if (searchParams.has('uid')) {
|
||||
// Only consider candidates with the same uid
|
||||
candidates = candidates.filter(a => {
|
||||
let params = new URLSearchParams(a.search);
|
||||
return params.get('uid') === searchParams.get('uid');
|
||||
});
|
||||
} else {
|
||||
// Sort candidates based on how many params they have in common, in descending order
|
||||
candidates = candidates.sort((a, b) => {
|
||||
return countSharedSearchParams(searchParams, b.search) - countSharedSearchParams(searchParams, a.search);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.length > 0) {
|
||||
for (let current of document.querySelectorAll('#sidebar a.current')) {
|
||||
current.classList.remove('current');
|
||||
}
|
||||
|
||||
candidates[0].classList.add('current');
|
||||
}
|
||||
};
|
||||
|
||||
sidebar.render = function () {
|
||||
this.palettes.render();
|
||||
};
|
||||
|
||||
sidebar.render();
|
||||
window.addEventListener('turbo:render', () => sidebar.render());
|
||||
|
||||
function countSharedSearchParams(searchParams, search) {
|
||||
if (!search || search === '?') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let params = new URLSearchParams(search);
|
||||
return [...searchParams.keys()].filter(k => params.get(k) === searchParams.get(k)).length;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Get import code for remixed themes and tweaked palettes.
|
||||
*/
|
||||
export { getThemeCode } from './tweak/code.js';
|
||||
export { HUE_RANGES, cdnUrl, hues, selectors, tints, urls } from './tweak/data.js';
|
||||
export { default as Permalink } from './tweak/permalink.js';
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* Get import code for remixed themes and tweaked palettes.
|
||||
*/
|
||||
import { urls } from './data.js';
|
||||
|
||||
export function cssImport(url, options = {}) {
|
||||
let { language = 'html', cdnUrl = '/dist/', attributes } = options;
|
||||
url = cdnUrl + url;
|
||||
|
||||
if (language === 'css') {
|
||||
return `@import url('${url}');`;
|
||||
} else {
|
||||
attributes = attributes ? ` ${attributes}` : '';
|
||||
return `<link rel="stylesheet" href="${url}"${attributes} />`;
|
||||
}
|
||||
}
|
||||
|
||||
export function cssLiteral(value, options = {}) {
|
||||
let { language = 'html' } = options;
|
||||
|
||||
if (language === 'css') {
|
||||
return value;
|
||||
} else {
|
||||
return `<style>\n${value}\n</style>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Params in correct order
|
||||
export const themeParams = ['colors', 'palette', 'brand', 'typography'];
|
||||
|
||||
export function getThemeCode(base, params, options) {
|
||||
let ret = [];
|
||||
|
||||
if (base) {
|
||||
ret.push(urls.theme(base));
|
||||
}
|
||||
|
||||
for (let aspect of themeParams) {
|
||||
let value = params[aspect];
|
||||
|
||||
if (value) {
|
||||
ret.push(urls[aspect](value));
|
||||
}
|
||||
}
|
||||
|
||||
return ret.map(url => cssImport(url, options)).join('\n');
|
||||
}
|
||||
|
||||
export function cssRule(selector, declarations, { indent = ' ' } = {}) {
|
||||
selector = Array.isArray(selector) ? selector.flat().join(',\n') : selector;
|
||||
declarations = Array.isArray(declarations) ? declarations.flat() : declarations;
|
||||
declarations = declarations.map(declaration => indent + declaration.trim()).join('\n');
|
||||
return `${selector} {\n${declarations.trimEnd()}\n}`;
|
||||
}
|
||||
@@ -1,213 +0,0 @@
|
||||
/**
|
||||
* Data related to theme remixing and palette tweaking
|
||||
* Must work in both browser and Node.js
|
||||
*/
|
||||
export const cdnUrl = globalThis.document ? document.documentElement.dataset.cdnUrl : '/dist/';
|
||||
|
||||
export const urls = {
|
||||
theme: id => `styles/themes/${id}.css`,
|
||||
colors: id => `styles/themes/${id}/color.css`,
|
||||
palette: id => `styles/color/${id}.css`,
|
||||
brand: id => `styles/brand/${id}.css`,
|
||||
typography: id => `styles/themes/${id}/typography.css`,
|
||||
};
|
||||
|
||||
export const docsURLs = {
|
||||
colors: '/docs/themes/',
|
||||
palette: '/docs/palettes/',
|
||||
typography: '/docs/themes/',
|
||||
};
|
||||
|
||||
export const icons = {
|
||||
colors: 'palette',
|
||||
palette: 'swatchbook',
|
||||
brand: 'droplet',
|
||||
typography: 'font-case',
|
||||
};
|
||||
|
||||
export const selectors = {
|
||||
palette: id =>
|
||||
[':where(:root)', ':host', ":where([class^='wa-theme-'], [class*=' wa-theme-'])", `.wa-palette-${id}`].join(',\n'),
|
||||
};
|
||||
|
||||
export const HUE_RANGES = {
|
||||
red: { min: 15, max: 35 }, // 20
|
||||
orange: { min: 35, max: 75 }, // 40
|
||||
yellow: { min: 75, max: 110 }, // 35
|
||||
green: { min: 115, max: 170 }, // 55
|
||||
cyan: { min: 170, max: 220 }, // 50
|
||||
blue: { min: 220, max: 265 }, // 45
|
||||
indigo: { min: 265, max: 290 }, // 25
|
||||
purple: { min: 290, max: 320 }, // 30
|
||||
pink: { min: 320, max: 375 }, // 55
|
||||
};
|
||||
|
||||
export const hues = Object.keys(HUE_RANGES);
|
||||
export const allHues = [...hues, 'gray'];
|
||||
export const tints = ['05', '10', '20', '30', '40', '50', '60', '70', '80', '90', '95'];
|
||||
|
||||
export const L_RANGES = {
|
||||
'05': { min: 0.18, max: 0.2 },
|
||||
10: { min: 0.23, max: 0.25 },
|
||||
20: { min: 0.31, max: 0.35 },
|
||||
30: { min: 0.38, max: 0.43 },
|
||||
40: { min: 0.45, max: 0.5 },
|
||||
50: { min: 0.55, max: 0.6 },
|
||||
60: { min: 0.65, max: 0.7 },
|
||||
70: { min: 0.73, max: 0.78 },
|
||||
80: { min: 0.82, max: 0.85 },
|
||||
90: { min: 0.91, max: 0.93 },
|
||||
95: { min: 0.95, max: 0.97 },
|
||||
};
|
||||
|
||||
for (let range of [HUE_RANGES, L_RANGES]) {
|
||||
for (let key in range) {
|
||||
range[key].mid = (range[key].min + range[key].max) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Most common tint per hue.
|
||||
* Largely the statistical mode, but also informed by the average and median.
|
||||
*/
|
||||
export const HUE_TOP_TINT = {
|
||||
red: 50,
|
||||
orange: 70,
|
||||
yellow: 80,
|
||||
green: 80,
|
||||
cyan: 70,
|
||||
blue: 50,
|
||||
indigo: 40,
|
||||
purple: 50,
|
||||
pink: 50,
|
||||
gray: 40,
|
||||
};
|
||||
|
||||
/*
|
||||
┌─────────┬──────┬──────┬────────┬──────┬────────┬───────┐
|
||||
│ (index) │ min │ max │ median │ avg │ stddev │ count │
|
||||
├─────────┼──────┼──────┼────────┼──────┼────────┼───────┤
|
||||
│ red │ 0.74 │ 1 │ 0.92 │ 0.88 │ 0.085 │ 9 │
|
||||
│ yellow │ 0.72 │ 1 │ 0.98 │ 0.92 │ 0.11 │ 8 │
|
||||
│ green │ 0.55 │ 0.93 │ 0.75 │ 0.75 │ 0.1 │ 8 │
|
||||
│ cyan │ 0.7 │ 0.88 │ 0.82 │ 0.81 │ 0.053 │ 8 │
|
||||
│ blue │ 0.54 │ 1 │ 0.83 │ 0.82 │ 0.15 │ 9 │
|
||||
│ indigo │ 0.63 │ 1 │ 0.87 │ 0.86 │ 0.13 │ 8 │
|
||||
│ purple │ 0.58 │ 0.99 │ 0.86 │ 0.84 │ 0.11 │ 8 │
|
||||
│ pink │ 0.74 │ 1 │ 0.93 │ 0.89 │ 0.089 │ 8 │
|
||||
└─────────┴──────┴──────┴────────┴──────┴────────┴───────┘
|
||||
*/
|
||||
/** Max(Average, Median) % of max P3 chroma per hue, relative to palette maximum and capped to 0.8 */
|
||||
export const HUE_CHROMA_SCALE = {
|
||||
red: 0.92,
|
||||
orange: 0.96, // interpolated
|
||||
yellow: 1,
|
||||
green: 0.7,
|
||||
cyan: 0.81,
|
||||
blue: 0.83,
|
||||
indigo: 0.87,
|
||||
purple: 0.86,
|
||||
pink: 0.92,
|
||||
};
|
||||
|
||||
export const CHROMA_SCALE_LIGHTEST = {
|
||||
95: 1,
|
||||
90: 0.8,
|
||||
80: 0.5,
|
||||
70: 0.2,
|
||||
60: 0.2,
|
||||
50: 0.15,
|
||||
40: 0.1,
|
||||
};
|
||||
|
||||
export const MAX_CHROMA_BY_TINT = {
|
||||
95: 0.11,
|
||||
};
|
||||
|
||||
/**
|
||||
* Chroma levels to identify gray.
|
||||
* First number: below this we identify as gray regardless
|
||||
* Second number: below this we identify as gray if it's also in the bottom 25% of colors when sorted by chroma
|
||||
*/
|
||||
export const GRAY_CHROMA_BY_TINT = {
|
||||
'05': [0.03, 0.05],
|
||||
10: [0.035, 0.06],
|
||||
20: [0.045, 0.06],
|
||||
30: [0.05, 0.06],
|
||||
40: [0.05, 0.06],
|
||||
50: [0.04, 0.06],
|
||||
60: [0.03, 0.05],
|
||||
70: [0.02, 0.04],
|
||||
80: [0.015, 0.03],
|
||||
90: [0.007, 0.01],
|
||||
95: [0.004, 0.005],
|
||||
};
|
||||
|
||||
export const moreHue = {
|
||||
red: 'Redder',
|
||||
orange: 'More orange', // https://www.reddit.com/r/grammar/comments/u9n0uo/is_it_oranger_or_more_orange/
|
||||
yellow: 'Yellower',
|
||||
green: 'Greener',
|
||||
cyan: 'More cyan',
|
||||
blue: 'Bluer',
|
||||
indigo: 'More indigo',
|
||||
purple: 'Purpler',
|
||||
pink: 'Pinker',
|
||||
};
|
||||
|
||||
export const hueBefore = {};
|
||||
export const hueAfter = {};
|
||||
|
||||
for (let i = 0; i < hues.length; i++) {
|
||||
hueBefore[hues[i]] = hues[i - 1] ?? hues.at(-1);
|
||||
hueAfter[hues[i]] = hues[i + 1] ?? hues[0];
|
||||
}
|
||||
|
||||
export const HUE_SHIFTS = [
|
||||
// Reds
|
||||
{ range: [0, 25], peak: [10, 25], shift: { dark: 15, light: -18 }, maxConsecutive: { dark: 4, light: -2 } },
|
||||
// Yellows
|
||||
{ range: [30, 112], peak: [70, 100], shift: { dark: -48, light: 16 }, maxConsecutive: { dark: -20, light: 4 } },
|
||||
|
||||
// Greens
|
||||
{ range: [140, 160], peak: [145, 155], shift: { dark: 15, light: -5 }, maxConsecutive: { dark: 7, light: -5 } },
|
||||
// Blues
|
||||
{ range: [240, 265], peak: [245, 260], shift: { dark: -3, light: -15 }, maxConsecutive: { dark: -3, light: -4 } },
|
||||
];
|
||||
|
||||
export const CHROMA_CURVES = {
|
||||
50: { dark: 0.9, light: 0.8 },
|
||||
60: { dark: 1, light: 1.2 },
|
||||
70: { light: 1.2 },
|
||||
80: { dark: 1.1, light: 2 },
|
||||
90: { dark: 3, light: 2 },
|
||||
};
|
||||
|
||||
export const MAX_CHROMA_BOUNDS = { min: 0.08, max: 0.3 };
|
||||
|
||||
/**
|
||||
* Max gray chroma (% of chroma of undertone) per hue
|
||||
*/
|
||||
export const MAX_GRAY_CHROMA_SCALE = {
|
||||
red: 0.2,
|
||||
orange: 0.2,
|
||||
yellow: 0.25,
|
||||
green: 0.25,
|
||||
cyan: 0.3,
|
||||
blue: 0.35,
|
||||
indigo: 0.35,
|
||||
purple: 0.3,
|
||||
pink: 0.25,
|
||||
};
|
||||
|
||||
/** Default accent tint if all chromas are 0, but also the tint accent colors will be nudged towards (see chromaTolerance) */
|
||||
export const DEFAULT_ACCENT = 60;
|
||||
|
||||
/** Min and max allowed tints */
|
||||
export const MIN_ACCENT = 40;
|
||||
export const MAX_ACCENT = 90;
|
||||
|
||||
/** Chroma tolerance: Chroma will need to differ more than this to gravitate away from defaultAccent */
|
||||
export const CHROMA_TOLERANCE = 0.000001;
|
||||
|
||||
export const ROLES = ['brand', 'neutral', 'success', 'warning', 'danger'];
|
||||
@@ -1,104 +0,0 @@
|
||||
const IDENTITY = x => x;
|
||||
|
||||
export default class Permalink extends URLSearchParams {
|
||||
/** Params changed since last URL I/O */
|
||||
changed = false;
|
||||
|
||||
constructor(params) {
|
||||
super(location.search);
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return Object.fromEntries(this.entries());
|
||||
}
|
||||
|
||||
set(key, value, defaultValue) {
|
||||
if (equals(value, defaultValue) || equals(value, '')) {
|
||||
value = null;
|
||||
}
|
||||
|
||||
value ??= null; // undefined -> null
|
||||
|
||||
let oldValue = Array.isArray(value) ? this.getAll(key) : this.get(key);
|
||||
let changed = !equals(value, oldValue);
|
||||
|
||||
if (!changed) {
|
||||
// Nothing to do here
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
super.delete(key);
|
||||
value = value.slice();
|
||||
|
||||
for (let v of value) {
|
||||
if (v || v === 0) {
|
||||
if (typeof v === 'object') {
|
||||
super.append(key, JSON.stringify(v));
|
||||
} else {
|
||||
super.append(key, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (value === null) {
|
||||
super.delete(key);
|
||||
} else {
|
||||
super.set(key, value);
|
||||
}
|
||||
|
||||
this.sort();
|
||||
this.changed ||= changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update page URL if it has changed since last time
|
||||
*/
|
||||
updateLocation() {
|
||||
if (this.changed) {
|
||||
// If there’s already a search, replace it.
|
||||
// We don’t want to clog the user’s history while they iterate
|
||||
let search = this.toString();
|
||||
let historyAction = location.search && search ? 'replaceState' : 'pushState';
|
||||
history[historyAction](null, '', `?${search}`);
|
||||
this.changed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function equals(value, oldValue) {
|
||||
if (Array.isArray(value) || Array.isArray(oldValue)) {
|
||||
value = toArray(value);
|
||||
oldValue = toArray(oldValue);
|
||||
|
||||
if (value.length !== oldValue.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value.every((v, i) => equals(v, oldValue[i]));
|
||||
}
|
||||
|
||||
// (value ?? oldValue ?? true) returns true if they're both empty (null or undefined)
|
||||
[value, oldValue] = [value, oldValue].map(v => (!v && v !== false && v !== 0 ? null : v));
|
||||
return value === oldValue || String(value) === String(oldValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a value to an array. `undefined` and `null` values are converted to an empty array.
|
||||
* @param {*} value - The value to convert.
|
||||
* @returns {any[]} The converted array.
|
||||
*/
|
||||
function toArray(value) {
|
||||
value ??= [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Don't convert "foo" into ["f", "o", "o"]
|
||||
if (typeof value !== 'string' && typeof value[Symbol.iterator] === 'function') {
|
||||
return Array.from(value);
|
||||
}
|
||||
|
||||
return [value];
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
// https://lea.verou.me/blog/2016/12/resolve-promises-externally-with-this-one-weird-trick/
|
||||
export function promise() {
|
||||
let res, rej;
|
||||
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
res = resolve;
|
||||
rej = reject;
|
||||
});
|
||||
|
||||
return Object.assign(promise, { resolve: res, reject: rej });
|
||||
}
|
||||
|
||||
export function normalizeAngles(angles) {
|
||||
// First, normalize each angle individually
|
||||
let normalizedAngles = angles.map(h => ((h % 360) + 360) % 360);
|
||||
|
||||
for (let i = 1; i < angles.length; i++) {
|
||||
let angle = normalizedAngles[i];
|
||||
let prevAngle = normalizedAngles[i - 1];
|
||||
let delta = angle - prevAngle;
|
||||
|
||||
if (Math.abs(delta) > 180) {
|
||||
let equivalent = [angle + 360, angle - 360];
|
||||
|
||||
// Offset hue to minimize difference in the direction that brings it closer to the previous hue
|
||||
let deltas = equivalent.map(e => Math.abs(e - prevAngle));
|
||||
|
||||
normalizedAngles[i] = equivalent[deltas[0] < deltas[1] ? 0 : 1];
|
||||
}
|
||||
}
|
||||
|
||||
return normalizedAngles;
|
||||
}
|
||||
|
||||
export function subtractAngles(θ1, θ2) {
|
||||
let [a, b] = normalizeAngles([θ1, θ2]);
|
||||
return a - b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an object of keys to ranges, find the closest range.
|
||||
* Ranges are assumed to be mutually exclusive.
|
||||
* @param {Object<string, {min: number, max: number}>} ranges
|
||||
* @param {number} value
|
||||
* @param {object} options
|
||||
* @param {"angle" | undefined} options.type
|
||||
* @param {number} [options.tolerance=Infinity] If value is not within any range, how close can it be?
|
||||
* @param {(range: {min: number, max: number}) => {min: number, max: number}} options.getRange
|
||||
* @returns {{key: string, distance: number}} The key of the closest range. Distance is 0 if the value is within the range, negative if below, positive if above.
|
||||
*/
|
||||
export function getRange(ranges, value, options) {
|
||||
let { type } = options || {};
|
||||
let keys = Object.keys(ranges);
|
||||
let closest = { key: keys[0], distance: Infinity };
|
||||
|
||||
for (let key of keys) {
|
||||
let range = ranges[key];
|
||||
|
||||
if (options?.getRange) {
|
||||
range = options.getRange(range);
|
||||
}
|
||||
|
||||
let { min, max } = range;
|
||||
|
||||
if (Array.isArray(range)) {
|
||||
[min, max] = range;
|
||||
}
|
||||
|
||||
let deltaMin = type === 'angle' ? subtractAngles(value, min) : value - min;
|
||||
let deltaMax = type === 'angle' ? subtractAngles(value, max) : value - max;
|
||||
|
||||
if (deltaMin >= 0 && deltaMax <= 0) {
|
||||
return { key, distance: 0 };
|
||||
}
|
||||
|
||||
if (Math.abs(deltaMin) < Math.abs(closest.distance)) {
|
||||
closest = { key, distance: deltaMin };
|
||||
}
|
||||
|
||||
if (deltaMax > 0 && Math.abs(deltaMax) < Math.abs(closest.distance)) {
|
||||
closest = { key, distance: deltaMax };
|
||||
}
|
||||
}
|
||||
|
||||
// TODO use angle functions to check tolerance against angles
|
||||
if (options?.tolerance !== undefined && Math.abs(closest.distance) > options.tolerance) {
|
||||
return;
|
||||
}
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
export function camelCase(str) {
|
||||
return (str + '').replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||
}
|
||||
|
||||
export function capitalize(str) {
|
||||
if (!str) {
|
||||
return str;
|
||||
}
|
||||
|
||||
str = str + '';
|
||||
return str[0].toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
export function arrayNext(array, element) {
|
||||
let index = array.indexOf(element);
|
||||
return array[(index + 1) % array.length];
|
||||
}
|
||||
|
||||
export function arrayPrevious(array, element) {
|
||||
let index = array.indexOf(element);
|
||||
return array[(index - 1 + array.length) % array.length];
|
||||
}
|
||||
|
||||
export function levelToIndex(level) {
|
||||
if (level === '05') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return level === '95' ? 10 : +level / 10;
|
||||
}
|
||||
|
||||
export function indexToLevel(i) {
|
||||
if (i === 0) {
|
||||
return '05';
|
||||
}
|
||||
|
||||
return (i === 10 ? 95 : i * 10) + '';
|
||||
}
|
||||
|
||||
export function previousLevel(level) {
|
||||
if (level === '05') {
|
||||
return;
|
||||
}
|
||||
|
||||
return indexToLevel(levelToIndex(level) - 1);
|
||||
}
|
||||
|
||||
export function nextLevel(level) {
|
||||
if (level === '95') {
|
||||
return;
|
||||
}
|
||||
|
||||
return indexToLevel(levelToIndex(level) + 1);
|
||||
}
|
||||
|
||||
export function relativeLevel(level, steps) {
|
||||
if (level == 100) {
|
||||
// loose intentional
|
||||
return relativeLevel(95, ++steps);
|
||||
}
|
||||
|
||||
if (level == 95) {
|
||||
// loose intentional
|
||||
return relativeLevel(90, ++steps);
|
||||
}
|
||||
|
||||
if (level == 0) {
|
||||
// loose intentional
|
||||
return relativeLevel(5, --steps);
|
||||
}
|
||||
|
||||
if (level == 5) {
|
||||
// loose intentional
|
||||
return relativeLevel(10, --steps);
|
||||
}
|
||||
|
||||
let index = clamp(0, levelToIndex(level) + steps, 10);
|
||||
|
||||
return indexToLevel(index);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} p Number from 0-1 where 0 is start and 1 is end
|
||||
* @param {*} start Number for p=0
|
||||
* @param {*} end Number for p=1
|
||||
* @returns
|
||||
*/
|
||||
export function interpolate(p, range = [0, 1], options) {
|
||||
let [start, end] = range;
|
||||
|
||||
if (p <= 0 || p >= 1 || range.length === 2) {
|
||||
let value = start + p * (end - start);
|
||||
return options?.unclamped ? value : clamp(start, value, end);
|
||||
}
|
||||
|
||||
// If we're here, there are more points in the range
|
||||
let interval = 1 / (range.length - 1);
|
||||
let index = Math.floor(p / interval);
|
||||
let intervalProgress = progress(p, [index * interval, (index + 1) * interval]);
|
||||
return interpolate(intervalProgress, range.slice(index, index + 2), options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverse of interpolate: given a value, find the progress between start and end.
|
||||
* @param {*} value
|
||||
* @param {*} range
|
||||
* @returns
|
||||
*/
|
||||
export function progress(value, range = [0, 1], options) {
|
||||
let [start, end] = range;
|
||||
|
||||
if (value <= start || value >= end || range.length === 2) {
|
||||
let ret = (value - start) / (end - start);
|
||||
|
||||
return options?.unclamped ? ret : clamp(0, ret, 1);
|
||||
}
|
||||
|
||||
// If we're here, there are more points in the range
|
||||
let index = range.findIndex((v, i) => value > range[i - 1] && value <= v);
|
||||
return (index - 1) / (range.length - 1);
|
||||
}
|
||||
|
||||
export function mapRange(value, { from, to, progression }) {
|
||||
let p = progress(value, from);
|
||||
|
||||
if (progression) {
|
||||
p = progression(p);
|
||||
}
|
||||
|
||||
return interpolate(p, to);
|
||||
}
|
||||
|
||||
export function clamp(min, value, max) {
|
||||
if (max < min) {
|
||||
[min, max] = [max, min];
|
||||
}
|
||||
|
||||
if (min !== undefined) {
|
||||
value = Math.max(min, value);
|
||||
}
|
||||
|
||||
if (max !== undefined) {
|
||||
value = Math.min(max, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function clampAngle(min, value, max) {
|
||||
[min, value, max] = normalizeAngles([min, value, max]);
|
||||
return clamp(min, value, max);
|
||||
}
|
||||
|
||||
export function interpolateAngles(p, range) {
|
||||
range = normalizeAngles(range);
|
||||
return interpolate(p, range, { unclamped: true });
|
||||
}
|
||||
|
||||
export function progressAngle(angle, range) {
|
||||
[angle, ...range] = normalizeAngles([angle, ...range]);
|
||||
return progress(angle, range, { unclamped: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Round a number to the nearest multiple of `roundTo` or to the closest number in an array of numbers
|
||||
* @param {number} value
|
||||
* @param {number | number[]} roundTo
|
||||
* @returns
|
||||
*/
|
||||
export function roundTo(value, roundTo = 1) {
|
||||
if (Array.isArray(roundTo)) {
|
||||
let closest = roundTo[0];
|
||||
let closestDistance = Math.abs(value - closest);
|
||||
|
||||
for (let candidate of roundTo) {
|
||||
let distance = Math.abs(value - candidate);
|
||||
|
||||
if (distance < closestDistance) {
|
||||
closest = candidate;
|
||||
closestDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
let decimals = roundTo.toString().split('.')[1]?.length ?? 0;
|
||||
let ret = Math.round(value / roundTo) * roundTo;
|
||||
|
||||
if (decimals > 0) {
|
||||
// Eliminate IEEE 754 floating point errors
|
||||
ret = +ret.toFixed(decimals);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function slugify(str) {
|
||||
return str
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '') // Convert accented letters to ASCII
|
||||
.replace(/[^\w\s-]/g, '') // Remove remaining non-ASCII characters
|
||||
.trim()
|
||||
.replace(/\s+/g, '-') // Convert whitespace to hyphens
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
export function log(...args) {
|
||||
console.log(...args);
|
||||
return args[0];
|
||||
}
|
||||
@@ -27,19 +27,3 @@ wa-copy-button.copy-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.block-link-icon {
|
||||
position: absolute;
|
||||
inset-block-start: 0;
|
||||
inset-inline-end: calc(100% + var(--wa-space-s));
|
||||
|
||||
transition: var(--wa-transition-slow);
|
||||
|
||||
&:not(:hover, :focus) {
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
:not(:hover, :focus-within) > & {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ wa-page > header {
|
||||
}
|
||||
|
||||
/* Pro badges */
|
||||
wa-badge.pro {
|
||||
wa-badge.pro::part(base) {
|
||||
background-color: var(--wa-brand-orange);
|
||||
border-color: var(--wa-brand-orange);
|
||||
}
|
||||
@@ -188,29 +188,6 @@ wa-badge.pro {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wa-icon-button.delete {
|
||||
vertical-align: -0.2em;
|
||||
margin-inline-start: var(--wa-space-xs);
|
||||
|
||||
&:not(li:hover > *, :focus) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wa-icon-button.delete {
|
||||
&:hover {
|
||||
color: var(--wa-color-danger-on-quiet);
|
||||
}
|
||||
|
||||
&::part(base):hover {
|
||||
background: var(--wa-color-danger-fill-quiet);
|
||||
}
|
||||
|
||||
&:not(:hover, :focus) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
#sidebar-close-button {
|
||||
@@ -255,32 +232,16 @@ wa-page > main {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
h1.title {
|
||||
wa-icon-button {
|
||||
font-size: var(--wa-font-size-l);
|
||||
color: var(--wa-color-text-quiet);
|
||||
h1.title wa-badge {
|
||||
vertical-align: middle;
|
||||
|
||||
&:not(:hover, :focus) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
wa-badge {
|
||||
vertical-align: middle;
|
||||
&::part(base) {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.block-info {
|
||||
display: flex;
|
||||
gap: var(--wa-space-xs);
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-block-end: var(--wa-flow-spacing);
|
||||
|
||||
code {
|
||||
line-height: var(--wa-line-height-condensed);
|
||||
}
|
||||
}
|
||||
|
||||
/* Current link */
|
||||
@@ -399,7 +360,7 @@ wa-page > main:has(> .index-grid) {
|
||||
}
|
||||
|
||||
&::part(header) {
|
||||
background-color: var(--header-background, var(--wa-color-neutral-fill-quiet));
|
||||
background-color: var(--wa-color-neutral-fill-quiet);
|
||||
border-bottom: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -439,23 +400,9 @@ wa-page > main:has(> .index-grid) {
|
||||
|
||||
&.color {
|
||||
border-color: transparent;
|
||||
transition: background var(--wa-transition-slow);
|
||||
background:
|
||||
linear-gradient(var(--color-top, transparent) 0% 100%) top no-repeat border-box,
|
||||
linear-gradient(var(--color-bottom, transparent) 0% 100%) bottom no-repeat border-box var(--color,);
|
||||
background-position: top, bottom;
|
||||
background-size:
|
||||
var(--color-top-width, 100%) var(--color-top-height, 30%),
|
||||
var(--color-bottom-width, 100%) var(--color-bottom-height, 30%);
|
||||
color: var(--swatch-text-color);
|
||||
|
||||
&.contrast-fail {
|
||||
outline: 1px dashed var(--wa-color-red);
|
||||
outline-offset: calc(-1 * var(--wa-space-2xs));
|
||||
}
|
||||
}
|
||||
|
||||
> wa-copy-button {
|
||||
wa-copy-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -490,7 +437,6 @@ table.colors {
|
||||
padding-block: 0;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
border: none;
|
||||
@@ -510,59 +456,6 @@ table.colors {
|
||||
padding-block: var(--wa-space-s);
|
||||
}
|
||||
}
|
||||
|
||||
.core-column {
|
||||
padding-inline-end: var(--wa-space-xl);
|
||||
}
|
||||
}
|
||||
|
||||
.value-up,
|
||||
.value-down {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: ' ' var(--icon);
|
||||
position: absolute;
|
||||
margin-inline-start: 3em;
|
||||
scale: 1 0.6;
|
||||
color: color-mix(in oklch, oklch(from var(--icon-color) none c h) 0%, oklch(from currentColor l none none));
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
.value-down {
|
||||
--icon: '▼';
|
||||
--icon-color: var(--wa-color-danger-fill-quiet);
|
||||
|
||||
&::after {
|
||||
margin-block-end: -0.2em;
|
||||
}
|
||||
}
|
||||
|
||||
.value-up {
|
||||
--icon: '▲';
|
||||
--icon-color: var(--wa-color-success-fill-quiet);
|
||||
}
|
||||
|
||||
.icon-modifier {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
|
||||
.modifier {
|
||||
position: absolute;
|
||||
bottom: -0.1em;
|
||||
right: -0.3em;
|
||||
font-size: 60%;
|
||||
|
||||
&::part(svg) {
|
||||
stroke: var(--background-color, var(--wa-color-surface-default));
|
||||
stroke-width: 100px;
|
||||
paint-order: stroke;
|
||||
overflow: visible;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Layout Examples */
|
||||
@@ -645,47 +538,23 @@ table.colors {
|
||||
height: 65vh;
|
||||
max-height: 21lh;
|
||||
}
|
||||
}
|
||||
|
||||
.color-select {
|
||||
&.default::part(display-input) {
|
||||
opacity: 0.6;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
> small {
|
||||
margin-inline-start: var(--wa-space-xs);
|
||||
padding-block: 0 var(--wa-space-xs);
|
||||
}
|
||||
|
||||
&::part(combobox)::before,
|
||||
wa-option::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 1.2em;
|
||||
aspect-ratio: 1;
|
||||
margin-inline-end: var(--wa-space-xs);
|
||||
flex: none;
|
||||
border-radius: var(--wa-border-radius-m);
|
||||
background: var(--color);
|
||||
border: 1px solid var(--wa-color-surface-default);
|
||||
}
|
||||
|
||||
wa-option {
|
||||
white-space: nowrap;
|
||||
|
||||
&::before {
|
||||
width: 1em;
|
||||
margin-inline: var(--wa-space-xs);
|
||||
#mix_and_match {
|
||||
strong {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-2xs);
|
||||
margin-top: 1.2em;
|
||||
}
|
||||
|
||||
&::part(checked-icon) {
|
||||
order: 2;
|
||||
wa-select::part(label) {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
wa-select[value='']::part(display-input),
|
||||
wa-option[value=''] {
|
||||
font-style: italic;
|
||||
color: var(--wa-color-text-quiet);
|
||||
}
|
||||
}
|
||||
|
||||
.default-badge {
|
||||
opacity: 0.6;
|
||||
margin-inline-start: var(--wa-space-xs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
.theme-color-icon {
|
||||
display: grid;
|
||||
gap: var(--wa-space-xs);
|
||||
grid-template-columns: repeat(4, auto);
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--wa-border-radius-m);
|
||||
background-color: var(--background-color);
|
||||
border: var(--wa-border-width-s) var(--wa-border-style) var(--border-color);
|
||||
padding: var(--wa-space-2xs) var(--wa-space-xs);
|
||||
color: var(--text-color);
|
||||
font-weight: var(--wa-font-weight-semibold);
|
||||
|
||||
&.plain {
|
||||
font-weight: var(--wa-font-weight-bold);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.theme-typography-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--wa-space-xs);
|
||||
|
||||
h3,
|
||||
p {
|
||||
margin-block: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
@@ -19,13 +19,13 @@ icon: card
|
||||
|
||||
<div slot="footer">
|
||||
<wa-button variant="brand" pill>More Info</wa-button>
|
||||
<wa-rating label="Rating"></wa-rating>
|
||||
<wa-rating></wa-rating>
|
||||
</div>
|
||||
</wa-card>
|
||||
|
||||
<style>
|
||||
.card-overview {
|
||||
width: 300px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.card-overview small {
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
title: Components
|
||||
description: Components are the essential building blocks to create intuitive, cohesive experiences. Browse the library of customizable, framework-friendly web components included in Web Awesome.
|
||||
layout: overview
|
||||
override:tags: []
|
||||
categories:
|
||||
tags: [actions, feedback, imagery, inputs, navigation, organization, helpers]
|
||||
titles:
|
||||
feedback: 'Feedback & Status'
|
||||
helpers: 'Utilities'
|
||||
- actions
|
||||
- feedback: 'Feedback & Status'
|
||||
- imagery
|
||||
- inputs
|
||||
- navigation
|
||||
- organization
|
||||
- helpers: 'Utilities'
|
||||
override:tags: []
|
||||
---
|
||||
|
||||
@@ -74,15 +74,3 @@ Add the `size` attribute to the [Radio Group](/docs/components/radio-group) to c
|
||||
<wa-radio value="3">Large 3</wa-radio>
|
||||
</wa-radio-group>
|
||||
```
|
||||
|
||||
### Hint
|
||||
|
||||
Add descriptive hint to a switch with the `hint` attribute. For hints that contain HTML, use the `hint` slot instead.
|
||||
|
||||
```html {.example}
|
||||
<wa-radio-group label="Select an option" name="a" value="1">
|
||||
<wa-radio value="1" hint="What should the user know about radio 1?">Option 1</wa-radio>
|
||||
<wa-radio value="2" hint="What should the user know about radio 2?">Option 2</wa-radio>
|
||||
<wa-radio value="3" hint="What should the user know about radio 3?">Option 3</wa-radio>
|
||||
</wa-radio-group>
|
||||
```
|
||||
|
||||
@@ -130,15 +130,6 @@ Note that multi-select options may wrap, causing the control to expand verticall
|
||||
|
||||
Use the `value` attribute to set the initial selection.
|
||||
|
||||
```html {.example}
|
||||
<wa-select value="option-1">
|
||||
<wa-option value="option-1">Option 1</wa-option>
|
||||
<wa-option value="option-2">Option 2</wa-option>
|
||||
<wa-option value="option-3">Option 3</wa-option>
|
||||
<wa-option value="option-4">Option 4</wa-option>
|
||||
</wa-select>
|
||||
```
|
||||
|
||||
When using `multiple`, the `value` _attribute_ uses space-delimited values to select more than one option. Because of this, `<wa-option>` values cannot contain spaces. If you're accessing the `value` _property_ through Javascript, it will be an array.
|
||||
|
||||
```html {.example}
|
||||
@@ -303,7 +294,7 @@ Remember that custom tags are rendered in a shadow root. To style them, you can
|
||||
return `
|
||||
<wa-tag removable>
|
||||
<wa-icon name="${name}" style="padding-inline-end: .5rem;"></wa-icon>
|
||||
${option.label}
|
||||
${option.getTextLabel()}
|
||||
</wa-tag>
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -1,80 +1,10 @@
|
||||
/**
|
||||
* Global data for all pages
|
||||
*/
|
||||
import { sort } from '../_utils/filters.js';
|
||||
|
||||
export default {
|
||||
eleventyComputed: {
|
||||
// Default parent. Can be overridden by explicitly setting parent in the data.
|
||||
// parent can refer to either an ancestor page in the URL or another page in the same directory
|
||||
parent(data) {
|
||||
let { parent, page } = data;
|
||||
|
||||
if (parent) {
|
||||
return parent;
|
||||
}
|
||||
|
||||
return page.url.split('/').filter(Boolean).at(-2);
|
||||
},
|
||||
|
||||
parentUrl(data) {
|
||||
let { parent, page } = data;
|
||||
return getParentUrl(page.url, parent);
|
||||
},
|
||||
|
||||
parentItem(data) {
|
||||
let { parentUrl } = data;
|
||||
return data.collections.all.find(item => item.url === parentUrl);
|
||||
},
|
||||
|
||||
children(data) {
|
||||
let { collections, page, parentOf } = data;
|
||||
let mainTag = data.tags?.[0];
|
||||
let collection = data.collections[mainTag] ?? [];
|
||||
|
||||
if (parentOf) {
|
||||
return collections[parentOf];
|
||||
}
|
||||
|
||||
let collection = collections.all ?? [];
|
||||
let url = page.url;
|
||||
|
||||
let ret = collection.filter(item => {
|
||||
return item.data.parentUrl === url;
|
||||
});
|
||||
|
||||
sort(ret);
|
||||
|
||||
return ret;
|
||||
return collection.filter(item => item.data.parent === data.page.fileSlug);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function getParentUrl(url, parent) {
|
||||
let parts = url.split('/').filter(Boolean);
|
||||
let ancestorIndex = parts.findLastIndex(part => part === parent);
|
||||
let retParts = parts.slice();
|
||||
|
||||
if (ancestorIndex > -1) {
|
||||
// parent is an ancestor
|
||||
retParts.splice(ancestorIndex + 1);
|
||||
} else {
|
||||
// parent is a sibling in the same directory
|
||||
retParts.splice(-1, 1, parent);
|
||||
}
|
||||
|
||||
let ret = retParts.join('/');
|
||||
|
||||
if (url.startsWith('/')) {
|
||||
ret = '/' + ret;
|
||||
}
|
||||
|
||||
if (!retParts.at(-1).includes('.') && !ret.endsWith('/')) {
|
||||
// If no extension, make sure to end with a slash
|
||||
ret += '/';
|
||||
}
|
||||
|
||||
if (ret === '/docs/') {
|
||||
ret = '/';
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
title: Clamped Color Tokens
|
||||
layout: block
|
||||
---
|
||||
|
||||
{% set tints = ['max-50', 'max-60', 'max-70', 'min-50', 'min-60', 'min-70'] %}
|
||||
{% set hues = ['red', 'orange', 'yellow', 'green', 'cyan', 'blue', 'indigo', 'purple', 'pink', 'gray'] %}
|
||||
|
||||
<table class="colors">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th class="core-column">Core tint</th>
|
||||
{% for tint in tints -%}
|
||||
<th>{{ tint }}</th>
|
||||
{%- endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
{% for hue in hues -%}
|
||||
<tr class="wa-color-{{ hue }}">
|
||||
<th>{{ hue | capitalize }}</th>
|
||||
<td class="core-column">
|
||||
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}); color: var(--wa-color-{{ hue }}-on); --key: var(--wa-color-{{ hue }}-key);">
|
||||
{{ palettes[paletteId][hue].maxChromaTint }}
|
||||
<wa-copy-button value="--wa-color-{{ hue }}" copy-label="--wa-color-{{ hue }}"></wa-copy-button>
|
||||
</div>
|
||||
</td>
|
||||
{% for tint in tints -%}
|
||||
<td>
|
||||
<div class="color swatch" style="background-color: var(--wa-color-{{ hue }}-{{ tint }})">
|
||||
<wa-copy-button value="--wa-color-{{ hue }}-{{ tint }}" copy-label="--wa-color-{{ hue }}-{{ tint }}"></wa-copy-button>
|
||||
</div>
|
||||
</td>
|
||||
{%- endfor -%}
|
||||
</tr>
|
||||
{%- endfor %}
|
||||
</table>
|
||||
|
||||
<style>
|
||||
.core-column .color.swatch::before {
|
||||
counter-reset: key var(--key);
|
||||
content: counter(key);
|
||||
}
|
||||
</style>
|
||||
@@ -2,7 +2,6 @@
|
||||
title: Layout
|
||||
description: Layout components and utility classes help you organize content that can adapt to any device or screen size. See the [installation instructions](#installation) to use Web Awesome's layout tools in your project.
|
||||
layout: overview
|
||||
parentOf: layout
|
||||
categories: ["components", "utilities"]
|
||||
override:tags: []
|
||||
---
|
||||
@@ -23,4 +22,4 @@ Or, you can choose to import _only_ the utilities:
|
||||
```html
|
||||
<link rel="stylesheet" href="{% cdnUrl 'styles/utilities.css' %}" />
|
||||
```
|
||||
{% endmarkdown %}
|
||||
{% endmarkdown %}
|
||||
@@ -42,14 +42,6 @@ wa-code-demo::part(preview) {
|
||||
<wa-input label="WA Input (url)" type="url"></wa-input>
|
||||
```
|
||||
|
||||
## Pill shaped text fields
|
||||
|
||||
Add the `wa-pill` class to an `<input>` to make it pill-shaped.
|
||||
|
||||
```html {.example}
|
||||
<label>Input <input type="text" placeholder="placeholder" class="wa-pill"></label>
|
||||
```
|
||||
|
||||
## Color Picker
|
||||
|
||||
Basic:
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { tints } from '/assets/scripts/tweak/data.js';
|
||||
|
||||
export function generateGrays(colors, { grayColor, grayChroma }) {
|
||||
let ret = {};
|
||||
let undertoneScale = colors[grayColor];
|
||||
|
||||
// These will be the same, since scaling them won't change the relationship
|
||||
ret.maxChromaTint = undertoneScale.maxChromaTint;
|
||||
Object.defineProperty(ret, 'core', {
|
||||
enumerable: false,
|
||||
get() {
|
||||
return this[this.maxChromaTint];
|
||||
},
|
||||
});
|
||||
ret.maxChromaTintRaw = undertoneScale.maxChromaTintRaw;
|
||||
|
||||
for (let tint of tints) {
|
||||
let colorUndertone = undertoneScale[tint].clone().to('oklch');
|
||||
ret[tint] = colorUndertone.set({ c: c => c * grayChroma });
|
||||
}
|
||||
|
||||
ret.maxChroma = ret[ret.maxChromaTint].get('oklch.c');
|
||||
ret.maxChromaRaw = ret[ret.maxChromaTintRaw].get('oklch.c');
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export default generateGrays;
|
||||
@@ -1,162 +0,0 @@
|
||||
// TODO move these to local imports
|
||||
import Color from 'https://colorjs.io/dist/color.js';
|
||||
import generateGrays from './generate-grays.js';
|
||||
import generateScale from './generate-scale.js';
|
||||
import getMaxChroma from './get-max-chroma.js';
|
||||
import { getCoreTint } from './util.js';
|
||||
import {
|
||||
HUE_CHROMA_SCALE,
|
||||
HUE_RANGES,
|
||||
HUE_TOP_TINT,
|
||||
L_RANGES,
|
||||
MAX_ACCENT,
|
||||
MIN_ACCENT,
|
||||
} from '/assets/scripts/tweak/data.js';
|
||||
import {
|
||||
clamp,
|
||||
clampAngle,
|
||||
interpolate,
|
||||
normalizeAngles,
|
||||
progressAngle,
|
||||
roundTo,
|
||||
subtractAngles,
|
||||
} from '/assets/scripts/tweak/util.js';
|
||||
|
||||
export default function generatePalette(seedHues, { huesAfter: allHuesAfter, ...options } = {}) {
|
||||
let ret = {};
|
||||
|
||||
// Generate scales from seed hues
|
||||
let firstSeedHue;
|
||||
|
||||
let coreLevels = {};
|
||||
let seedMeta = {};
|
||||
|
||||
for (let hue in seedHues) {
|
||||
let seedColors = seedHues[hue];
|
||||
|
||||
if (!seedColors) {
|
||||
continue;
|
||||
}
|
||||
|
||||
firstSeedHue ??= hue;
|
||||
|
||||
let coreLevel = (coreLevels[hue] = getCoreTint(seedColors));
|
||||
let coreColor = seedColors[coreLevel];
|
||||
let [l, c, h] = coreColor.getAll('oklch');
|
||||
|
||||
let lOffset = l - L_RANGES[coreLevel].mid;
|
||||
let cScale = c / getMaxChroma(l, h);
|
||||
let relativeCScale = cScale / HUE_CHROMA_SCALE[hue];
|
||||
let levelOffset = coreLevel - HUE_TOP_TINT[hue];
|
||||
seedMeta[hue] = { lOffset, cScale, relativeCScale, levelOffset };
|
||||
|
||||
ret[hue] = generateScale(seedColors);
|
||||
}
|
||||
|
||||
if (!firstSeedHue) {
|
||||
// No valid seed colors, abort mission
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fill in remaining hues
|
||||
let hueBefore = firstSeedHue;
|
||||
|
||||
for (let hue of allHuesAfter[firstSeedHue]) {
|
||||
if (hue in ret) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let huesAfter = allHuesAfter[hue];
|
||||
let seedHuesAfter = huesAfter.filter(hue => seedHues[hue]);
|
||||
let neighboringSeedHues = [seedHuesAfter.at(-1), seedHuesAfter[0]];
|
||||
|
||||
// A number from 0 to 1 indicating how close we are to each neighboring seed hue (0 if only one seed hue)
|
||||
let hueProgress =
|
||||
seedHuesAfter.length === 1
|
||||
? 0
|
||||
: progressAngle(
|
||||
HUE_RANGES[hue].mid,
|
||||
neighboringSeedHues.map(hue => HUE_RANGES[hue].mid),
|
||||
);
|
||||
|
||||
// Hue of the core color of the previous seed scale
|
||||
let hBefore = ret[hueBefore][ret[hueBefore].maxChromaTint].get('oklch.h');
|
||||
|
||||
// We start from the midpoint of the hue range
|
||||
let h = HUE_RANGES[hue].mid;
|
||||
|
||||
// Shift if too close to seed hues
|
||||
let hBeforeDelta = subtractAngles(h, hBefore);
|
||||
|
||||
if (Math.abs(hBeforeDelta) < 40) {
|
||||
h = hBefore + 40 * Math.sign(hBeforeDelta);
|
||||
}
|
||||
|
||||
if (seedHuesAfter.length > 1) {
|
||||
let hueAfter = seedHuesAfter[0];
|
||||
let hAfter = ret[hueAfter][ret[hueAfter].maxChromaTint].get('oklch.h');
|
||||
[hBefore, h, hAfter] = normalizeAngles([hBefore, h, hAfter]);
|
||||
let hAfterDelta = subtractAngles(hAfter, h);
|
||||
|
||||
if (hAfter - 40 < hBefore + 40) {
|
||||
// It's not possible to have a distance of at least 40deg from both neighboring hues
|
||||
// so at least maximize distance
|
||||
h = (hBefore + hAfter) / 2;
|
||||
} else if (hAfterDelta < 40) {
|
||||
h = hAfter - 40;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure hue is still within range for this scale
|
||||
h = clampAngle(HUE_RANGES[hue].min, h, HUE_RANGES[hue].max);
|
||||
|
||||
let coreLevelOffset = interpolate(
|
||||
hueProgress,
|
||||
neighboringSeedHues.map(hue => seedMeta[hue].levelOffset),
|
||||
);
|
||||
let coreLevel = clamp(MIN_ACCENT, roundTo(HUE_TOP_TINT[hue] + coreLevelOffset, 10), MAX_ACCENT);
|
||||
|
||||
coreLevels[hue] = coreLevel;
|
||||
let lOffsets = neighboringSeedHues.map(hue => seedMeta[hue].lOffset);
|
||||
let lOffset = interpolate(hueProgress, lOffsets);
|
||||
let l = L_RANGES[coreLevel].mid + lOffset;
|
||||
|
||||
let cScale = 1;
|
||||
|
||||
if (hue === 'yellow') {
|
||||
// Yellow tends to be the brighest hue in the palette
|
||||
cScale = Math.max(
|
||||
...Object.values(seedMeta)
|
||||
.map(meta => meta.relativeCScale)
|
||||
.filter(c => c > 0),
|
||||
);
|
||||
} else {
|
||||
cScale = interpolate(
|
||||
hueProgress,
|
||||
neighboringSeedHues.map(neighboringHue => seedMeta[neighboringHue].relativeCScale),
|
||||
);
|
||||
}
|
||||
|
||||
cScale *= HUE_CHROMA_SCALE[hue];
|
||||
|
||||
let maxC = getMaxChroma(l, h);
|
||||
let c = cScale * maxC;
|
||||
// let c = interpolate(
|
||||
// hueProgress,
|
||||
// pinnedScale.map(scale => scale.maxChroma),
|
||||
// );
|
||||
|
||||
let coreColor = new Color('oklch', [l, c, h]).toGamut('p3');
|
||||
|
||||
ret[hue] = generateScale(coreColor);
|
||||
hueBefore = hue;
|
||||
}
|
||||
|
||||
if ('gray' in seedHues) {
|
||||
ret.gray = generateScale(seedHues.gray);
|
||||
} else {
|
||||
ret.gray = generateGrays(ret, options);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import { getCoreTint, getHueShift, getLightness, identifyColor } from './util.js';
|
||||
import {
|
||||
CHROMA_CURVES,
|
||||
CHROMA_SCALE_LIGHTEST,
|
||||
L_RANGES,
|
||||
MAX_CHROMA_BY_TINT,
|
||||
tints,
|
||||
} from '/assets/scripts/tweak/data.js';
|
||||
import { clamp, interpolate, progress } from '/assets/scripts/tweak/util.js';
|
||||
|
||||
/**
|
||||
* Generate a scale of tints from one or more key colors
|
||||
* @param {Color | Record<number | string, Color>} seedColors
|
||||
* @returns {Record<number | string, Color>}
|
||||
*/
|
||||
export function generateScale(seedColors) {
|
||||
if (seedColors.constructor.name === 'Color') {
|
||||
// Single color given
|
||||
let { level } = identifyColor(seedColors);
|
||||
seedColors = { [level]: seedColors };
|
||||
}
|
||||
|
||||
// Find core color
|
||||
let coreLevel = getCoreTint(seedColors);
|
||||
let coreColor = seedColors[coreLevel];
|
||||
let coreChroma = coreColor.get('oklch.c');
|
||||
|
||||
let scale = {};
|
||||
|
||||
Object.defineProperties(scale, {
|
||||
maxChromaTint: { value: coreLevel, enumerable: false, configurable: true },
|
||||
maxChromaTintRaw: { value: coreLevel, enumerable: false, configurable: true },
|
||||
maxChroma: { value: coreChroma, enumerable: false, configurable: true },
|
||||
maxChromaRaw: { value: coreChroma, enumerable: false, configurable: true },
|
||||
core: {
|
||||
get() {
|
||||
return this[this.maxChromaTint];
|
||||
},
|
||||
enumerable: false,
|
||||
},
|
||||
});
|
||||
|
||||
// First, add pinned colors
|
||||
for (let tint in seedColors) {
|
||||
scale[tint] = seedColors[tint];
|
||||
}
|
||||
|
||||
// For finding lightest and darkest pinned colors
|
||||
let pinnedTints = Object.keys(seedColors).sort((a, b) => a - b);
|
||||
let chromaCurve = CHROMA_CURVES[clamp(50, coreLevel, 90)];
|
||||
|
||||
// Now generate the rest, starting from the edges
|
||||
if (!('95' in scale)) {
|
||||
let lightestPinnedTint = pinnedTints.at(-1);
|
||||
let lightest = seedColors[lightestPinnedTint];
|
||||
let lOffset = lightest.get('oklch.l') - L_RANGES[lightestPinnedTint].mid;
|
||||
let chromaScale = CHROMA_SCALE_LIGHTEST[lightestPinnedTint];
|
||||
let hueShift = getHueShift(lightest, lightestPinnedTint, '95');
|
||||
|
||||
let color = lightest.clone().to('oklch');
|
||||
color.set({
|
||||
l: getLightness(95, lOffset),
|
||||
c: clamp(0, lightest.get('oklch.c') * chromaScale, MAX_CHROMA_BY_TINT[95]),
|
||||
h: h => h + hueShift,
|
||||
});
|
||||
|
||||
scale[95] = color;
|
||||
}
|
||||
|
||||
if (!('05' in scale)) {
|
||||
let darkestPinnedTint = pinnedTints[0];
|
||||
let darkest = seedColors[darkestPinnedTint];
|
||||
let lOffset = darkest.get('oklch.l') - L_RANGES[darkestPinnedTint].mid;
|
||||
let color = darkest.clone().to('oklch');
|
||||
let hueShift = getHueShift(darkest, darkestPinnedTint, '05');
|
||||
|
||||
color.set({
|
||||
l: getLightness('05', lOffset),
|
||||
// TODO c
|
||||
h: h => h + hueShift,
|
||||
});
|
||||
|
||||
scale['05'] = color;
|
||||
}
|
||||
|
||||
let tintBefore = '05';
|
||||
|
||||
for (let tint of tints) {
|
||||
if (tint in scale) {
|
||||
// Pinned or already generated
|
||||
tintBefore = tint;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generated color
|
||||
// First, find closest pinned colors before and after
|
||||
let tintAfter = pinnedTints.find(level => level > tint) ?? '95';
|
||||
let neighboringTints = [tintBefore, tintAfter];
|
||||
let neighboringColors = neighboringTints.map(t => scale[t]);
|
||||
let tintProgress = progress(tint, neighboringTints);
|
||||
|
||||
let color = coreColor.clone().to('oklch');
|
||||
|
||||
// Lightness
|
||||
let lOffset = interpolate(
|
||||
tintProgress,
|
||||
neighboringTints.map(t => scale[t].get('oklch.l') - L_RANGES[t].mid),
|
||||
);
|
||||
|
||||
// Interpolate hue linearly and chroma with a power curve
|
||||
color.set({
|
||||
l: getLightness(tint, lOffset),
|
||||
c: interpolate(
|
||||
tintProgress,
|
||||
neighboringColors.map(c => c.get('oklch.c')),
|
||||
{
|
||||
progression: tint > coreLevel ? p => p ** chromaCurve.light : undefined,
|
||||
},
|
||||
),
|
||||
h: interpolate(
|
||||
tintProgress,
|
||||
neighboringColors.map(c => c.get('oklch.h')),
|
||||
),
|
||||
});
|
||||
|
||||
scale[tint] = color;
|
||||
}
|
||||
|
||||
for (let tint in scale) {
|
||||
if (!(tint in seedColors) && scale[tint].toGamut) {
|
||||
scale[tint] = scale[tint].toGamut('p3');
|
||||
}
|
||||
}
|
||||
|
||||
return scale;
|
||||
}
|
||||
|
||||
export default generateScale;
|
||||
@@ -1,3 +0,0 @@
|
||||
export { generateGrays, generateGrays as grays } from './generate-grays.js';
|
||||
export { generatePalette, generatePalette as palette } from './generate-palette.js';
|
||||
export { generateScale, generateScale as scale } from './generate-scale.js';
|
||||
@@ -1,91 +0,0 @@
|
||||
/**
|
||||
* Memoized calculation of OKLCH gamut boundary for a given L and H
|
||||
* Currently unused, but we can use it if existing code becomes too slow.
|
||||
*/
|
||||
import Color from 'https://colorjs.io/dist/color.js';
|
||||
import { interpolate, progress, progressAngle, roundTo } from '/assets/scripts/tweak/util.js';
|
||||
|
||||
/** Max oklch.c per h and l (rounded to 1 significant digit) */
|
||||
const maxChroma = {};
|
||||
const OOG_CHROMA = 0.4; // guaranteed to be OOG for every P3 color
|
||||
const C_THRESHOLD = 0.03;
|
||||
const MIN_H_STEP = 0.1;
|
||||
const MIN_L_STEP = 0.001;
|
||||
|
||||
export default function getMaxChroma(l, h) {
|
||||
let { hStep, lStep, count } = calculateBoundary(l, h);
|
||||
|
||||
let hRounded = roundTo(h, hStep);
|
||||
let lRounded = roundTo(l, lStep);
|
||||
|
||||
// Calculate gamut boundary around this point
|
||||
let hProgress = progressAngle(h - hRounded, [-hStep, 0, hStep]);
|
||||
let lProgress = progress(l - lRounded, [-lStep, 0, lStep]);
|
||||
let maxChromaH = [];
|
||||
|
||||
for (let i of [-1, 0, 1]) {
|
||||
let h = roundTo(hRounded + i * hStep, hStep);
|
||||
|
||||
let cs = [-1, 0, 1].map(j => {
|
||||
let l = roundTo(lRounded + j * lStep, lStep);
|
||||
|
||||
return maxChroma[l][h];
|
||||
});
|
||||
|
||||
maxChromaH.push(interpolate(lProgress, cs));
|
||||
}
|
||||
|
||||
// Interpolate between the 9 points using bilinear interpolation
|
||||
let c = interpolate(hProgress, maxChromaH);
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
function calculateBoundary(pointL, pointH, lStep = 0.1, hStep = 10) {
|
||||
let hRounded = roundTo(pointH, hStep);
|
||||
let lRounded = roundTo(pointL, lStep);
|
||||
let ret = { count: 0, hStep, lStep };
|
||||
|
||||
for (let i of [-1, 0, 1]) {
|
||||
let l = roundTo(lRounded + i * lStep, lStep);
|
||||
maxChroma[l] ??= {};
|
||||
|
||||
for (let j of [-1, 0, 1]) {
|
||||
let h = roundTo(hRounded + j * hStep, hStep);
|
||||
|
||||
if (maxChroma[l][h] !== undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let gamutBoundary = new Color('oklch', [l, OOG_CHROMA, h]).toGamut('p3', { method: 'oklch.c' });
|
||||
let c = gamutBoundary.get('c');
|
||||
maxChroma[l][h] = c;
|
||||
ret.count++;
|
||||
let tooFar = { h: false, l: false };
|
||||
|
||||
if (i > -1) {
|
||||
let lPrev = roundTo(lRounded + (i - 1) * lStep, lStep);
|
||||
let cPrev = maxChroma[lPrev][h];
|
||||
tooFar.l = Math.abs(c - cPrev) > C_THRESHOLD && lStep > MIN_L_STEP;
|
||||
|
||||
if (tooFar.l) {
|
||||
ret.lStep /= 2;
|
||||
ret.count += calculateBoundary(pointL, pointH, ret.lStep, ret.hStep).count;
|
||||
}
|
||||
}
|
||||
|
||||
if (j > -1) {
|
||||
let hPrev = roundTo(hRounded + (j - 1) * hStep, hStep);
|
||||
let cPrev = maxChroma[l][hPrev];
|
||||
tooFar.h = Math.abs(c - cPrev) > C_THRESHOLD && hStep > MIN_H_STEP;
|
||||
|
||||
if (tooFar.h) {
|
||||
ret.hStep /= 2;
|
||||
ret.count += calculateBoundary(pointL, pointH, ret.lStep, ret.hStep).count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { stringifyColor } from './util.js';
|
||||
import { cssImport, cssLiteral, cssRule } from '/assets/scripts/tweak/code.js';
|
||||
import { selectors, tints, urls } from '/assets/scripts/tweak/data.js';
|
||||
|
||||
export function getPaletteCode({ base, slug = base, colors, tweaked, roles, ...options }) {
|
||||
let imports = [];
|
||||
|
||||
if (base && options.imports !== false && !tweaked.seedColors) {
|
||||
imports.push(urls.palette(base));
|
||||
}
|
||||
|
||||
let ret = imports.map(url => cssImport(url, options)).join('\n');
|
||||
|
||||
let declarations = [];
|
||||
let prefix = options.prefix ?? 'wa-color';
|
||||
|
||||
let css = '';
|
||||
|
||||
if (tweaked) {
|
||||
for (let hue in colors) {
|
||||
if (!tweaked.seedColors) {
|
||||
if (hue === 'gray') {
|
||||
if (!tweaked.grayChroma && !tweaked.grayColor) {
|
||||
continue;
|
||||
}
|
||||
} else if (!tweaked.chromaScale && !tweaked.hue?.[hue]) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let scale = colors[hue];
|
||||
|
||||
for (let tint of tints) {
|
||||
let color = scale[tint];
|
||||
let stringified = stringifyColor(color);
|
||||
declarations.push(`--${prefix}-${hue}-${tint}: ${stringified};`);
|
||||
}
|
||||
|
||||
let coreTint = scale.maxChromaTint;
|
||||
if (coreTint) {
|
||||
declarations.push(
|
||||
`--${prefix}-${hue}: var(--${prefix}-${hue}-${coreTint});`,
|
||||
`--${prefix}-${hue}-key: ${coreTint};`,
|
||||
);
|
||||
}
|
||||
|
||||
declarations.push('');
|
||||
}
|
||||
}
|
||||
|
||||
if (roles) {
|
||||
for (let role in roles) {
|
||||
let hue = roles[role];
|
||||
|
||||
if (!hue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let suffix of [...tints.map(t => '-' + t), '', '-key']) {
|
||||
declarations.push(`--${prefix}-${role}${suffix}: var(--${prefix}-${hue}${suffix});`);
|
||||
}
|
||||
|
||||
declarations.push('');
|
||||
}
|
||||
}
|
||||
|
||||
if (declarations.length > 0) {
|
||||
let selector = options.selector ?? selectors.palette(slug);
|
||||
css += cssRule(selector, declarations);
|
||||
}
|
||||
|
||||
if (css) {
|
||||
if (imports.length) {
|
||||
ret += '\n\n';
|
||||
}
|
||||
|
||||
ret += `${cssLiteral(css, options)}`;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export default getPaletteCode;
|
||||
@@ -1,28 +0,0 @@
|
||||
// TODO move these to local imports
|
||||
import Color from 'https://colorjs.io/dist/color.js';
|
||||
import { tints } from '/assets/scripts/tweak/data.js';
|
||||
|
||||
let palettes = await fetch('/docs/palettes/data.json').then(r => r.json());
|
||||
|
||||
for (let palette in palettes) {
|
||||
for (let hue in palettes[palette].colors) {
|
||||
let scale = palettes[palette].colors[hue];
|
||||
for (let tint of tints) {
|
||||
let color = scale[tint];
|
||||
|
||||
if (Array.isArray(color)) {
|
||||
scale[tint] = new Color('oklch', color);
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(scale, 'core', {
|
||||
get() {
|
||||
return this[this.maxChromaTint];
|
||||
},
|
||||
enumerable: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.allPalettes = palettes;
|
||||
export default palettes;
|
||||
@@ -1,74 +0,0 @@
|
||||
// TODO move these to local imports
|
||||
import generateGrays from './generate-grays.js';
|
||||
import { tints } from '/assets/scripts/tweak/data.js';
|
||||
|
||||
export function tweakPalette(baseColors, tweaks, tweaked) {
|
||||
let ret = {};
|
||||
|
||||
if (!tweaked) {
|
||||
return baseColors;
|
||||
}
|
||||
|
||||
for (let hue in baseColors) {
|
||||
let originalScale = baseColors[hue];
|
||||
let scale = (ret[hue] = {});
|
||||
let descriptors = Object.getOwnPropertyDescriptors(originalScale);
|
||||
Object.defineProperties(scale, {
|
||||
maxChromaTint: { ...descriptors.maxChromaTint, enumerable: false },
|
||||
maxChromaTintRaw: { ...descriptors.maxChromaTintRaw, enumerable: false },
|
||||
core: {
|
||||
get() {
|
||||
return this[this.maxChromaTint];
|
||||
},
|
||||
enumerable: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (hue === 'gray') {
|
||||
if (tweaked.grayChroma || tweaked.grayColor) {
|
||||
let grayColor = tweaks.grayColor ?? this.originalGrayColor;
|
||||
let grayChroma = this.computedGrayChroma;
|
||||
ret.gray = generateGrays(baseColors, { grayColor, grayChroma });
|
||||
} else {
|
||||
ret.gray = originalScale;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let tint of tints) {
|
||||
scale[tint] = tweakColor(hue, originalScale[tint], tweaks, tweaked);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function tweakColor(hue, originalColor, tweaks, tweaked) {
|
||||
if (!tweaked) {
|
||||
return originalColor;
|
||||
}
|
||||
|
||||
let color = originalColor;
|
||||
let { hueShifts, chromaScale = 1, grayColor, grayChroma } = tweaks;
|
||||
|
||||
let tweak = {};
|
||||
let thisTweaked = false;
|
||||
|
||||
if (tweaked.hue && hueShifts[hue]) {
|
||||
tweak.h = h => h + hueShifts[hue];
|
||||
thisTweaked = true;
|
||||
}
|
||||
|
||||
if (tweaked.chromaScale && chromaScale !== 1) {
|
||||
tweak.c = c => c * chromaScale;
|
||||
thisTweaked = true;
|
||||
}
|
||||
|
||||
if (thisTweaked) {
|
||||
color = color.clone().to('oklch').set(tweak);
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
export default tweakPalette;
|
||||
@@ -1,154 +0,0 @@
|
||||
import {
|
||||
CHROMA_TOLERANCE,
|
||||
DEFAULT_ACCENT,
|
||||
GRAY_CHROMA_BY_TINT,
|
||||
HUE_RANGES,
|
||||
HUE_SHIFTS,
|
||||
L_RANGES,
|
||||
MAX_ACCENT,
|
||||
MIN_ACCENT,
|
||||
tints,
|
||||
} from '/assets/scripts/tweak/data.js';
|
||||
import { clamp, getRange, mapRange } from '/assets/scripts/tweak/util.js';
|
||||
|
||||
export function identifyColor(color, colors) {
|
||||
let [l, c, h] = color.getAll('oklch');
|
||||
let level = getRange(L_RANGES, l).key;
|
||||
let hue;
|
||||
|
||||
// Identify grays
|
||||
let grayBounds = GRAY_CHROMA_BY_TINT[level];
|
||||
if (c <= grayBounds[1]) {
|
||||
// Possibly gray
|
||||
if (c <= grayBounds[0]) {
|
||||
// Definitely gray
|
||||
hue = 'gray';
|
||||
} else if (colors) {
|
||||
// May or may not be gray, compare to palette max chroma
|
||||
// FIXME this does not take level into account, so is more likely to identify lighter colors as gray
|
||||
let maxChroma = Math.max(...colors.map(color => color.get('oklch.c')));
|
||||
|
||||
if (c / maxChroma < 0.2) {
|
||||
hue = 'gray';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hue ??= getRange(HUE_RANGES, h, { type: 'angle' }).key;
|
||||
|
||||
return { hue, level };
|
||||
}
|
||||
|
||||
export function getLightness(level, distance) {
|
||||
return clamp(L_RANGES[level].min, L_RANGES[level].mid + distance, L_RANGES[level].max);
|
||||
}
|
||||
|
||||
/**
|
||||
* How many tints are between two tints?
|
||||
* E.g. `getTintDistance('90', '95')` should return `1`
|
||||
* @param {number | string} tint1
|
||||
* @param {number | string} tint2
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getTintDistance(tint1, tint2) {
|
||||
tint1 = String(tint1);
|
||||
tint2 = String(tint2);
|
||||
return tints.indexOf(tint2) - tints.indexOf(tint1);
|
||||
}
|
||||
export function getHueShift(color, fromTint, toTint) {
|
||||
let tintDistance = getTintDistance(fromTint, toTint);
|
||||
let hueShift = getRange(HUE_SHIFTS, color.get('oklch.h'), {
|
||||
getRange: v => v.range,
|
||||
type: 'angle',
|
||||
tolerance: 0,
|
||||
});
|
||||
|
||||
if (!hueShift) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
hueShift = HUE_SHIFTS[hueShift.key];
|
||||
|
||||
let { peak, range } = hueShift;
|
||||
let h = color.get('oklch.h');
|
||||
let breakpoints = [range[0], ...peak, range[1]];
|
||||
let intensity = mapRange(h, breakpoints, [0, 1, 1, 0]);
|
||||
let type = tintDistance < 0 ? 'dark' : 'light';
|
||||
let shift = hueShift.shift[type];
|
||||
|
||||
let ret = shift * intensity;
|
||||
let maxConsecutive = hueShift.maxConsecutive[type] ?? hueShift.maxConsecutive;
|
||||
let maxShift = Math.sign(shift) * maxConsecutive * Math.abs(tintDistance);
|
||||
|
||||
ret = clamp(undefined, ret, maxShift);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function getCoreTint(scale) {
|
||||
let tintsInScale = Object.keys(scale);
|
||||
|
||||
if (tintsInScale.length <= 1) {
|
||||
return tintsInScale[0];
|
||||
}
|
||||
|
||||
let ret = DEFAULT_ACCENT in scale ? DEFAULT_ACCENT : tintsInScale[Math.floor(tintsInScale.length / 2)];
|
||||
let maxChroma = 0;
|
||||
|
||||
for (let tint in scale) {
|
||||
let color = scale[tint];
|
||||
let chroma = color.get('oklch.c');
|
||||
|
||||
if (chroma > maxChroma + CHROMA_TOLERANCE && tint >= MIN_ACCENT && tint <= MAX_ACCENT) {
|
||||
ret = tint;
|
||||
maxChroma = chroma;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function getContrasts(colors, originalContrasts) {
|
||||
let ret = {};
|
||||
|
||||
for (let hue in colors) {
|
||||
ret[hue] = {};
|
||||
|
||||
for (let tintBg of tints) {
|
||||
ret[hue][tintBg] = {};
|
||||
let bgColor = colors[hue][tintBg];
|
||||
|
||||
if (!bgColor || !bgColor.contrast) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let tintFg of tints) {
|
||||
let fgColor = colors[hue][tintFg];
|
||||
let value = bgColor.contrast(fgColor, 'WCAG21');
|
||||
if (originalContrasts) {
|
||||
let original = originalContrasts[hue][tintBg][tintFg];
|
||||
ret[hue][tintBg][tintFg] = { value, original, bgColor, fgColor };
|
||||
} else {
|
||||
ret[hue][tintBg][tintFg] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return hex code iff a color is within sRGB, otherwise fall back to its default string representation
|
||||
*
|
||||
* @param {Color} color
|
||||
* @returns {string}
|
||||
*/
|
||||
export function stringifyColor(color) {
|
||||
if (color?.constructor.name !== 'Color') {
|
||||
return color;
|
||||
}
|
||||
|
||||
let format = color.inGamut('srgb') ? 'hex' : undefined;
|
||||
return color.toString({ format });
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
/* CSS for custom palettes only */
|
||||
#seed-colors {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(22ch, 1fr));
|
||||
gap: var(--wa-space-m);
|
||||
|
||||
> .add-button {
|
||||
flex-flow: column wrap;
|
||||
height: auto;
|
||||
min-height: 15ch;
|
||||
border: var(--wa-panel-border-width) var(--wa-panel-border-style) var(--wa-color-surface-border);
|
||||
--border-color: var(--wa-color-surface-border);
|
||||
border-radius: var(--wa-panel-border-radius);
|
||||
background-color: var(--wa-color-surface-default);
|
||||
box-shadow: var(--wa-shadow-s);
|
||||
|
||||
wa-icon {
|
||||
font-size: 200%;
|
||||
margin: 0;
|
||||
margin-top: 0.35em;
|
||||
}
|
||||
}
|
||||
|
||||
> wa-card {
|
||||
--spacing: var(--wa-space-s);
|
||||
|
||||
[slot='image'] {
|
||||
position: relative;
|
||||
height: 5.5rem;
|
||||
width: 100%;
|
||||
border-start-start-radius: var(--inner-border-radius);
|
||||
border-start-end-radius: var(--inner-border-radius);
|
||||
background-color: var(--color);
|
||||
color: canvastext;
|
||||
|
||||
.tweak-icon {
|
||||
position: absolute;
|
||||
top: var(--wa-space-s);
|
||||
right: var(--wa-space-s);
|
||||
|
||||
--background-color-hover: oklab(from currentColor l a b / 15%);
|
||||
--text-color-hover: currentColor;
|
||||
|
||||
&:not(:hover, :focus, :has(+ :focus-within)) {
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
&:is(.tweaked *) {
|
||||
&::part(base) {
|
||||
transition: var(--wa-transition-normal);
|
||||
transition-property: padding, border, opacity;
|
||||
background-color: var(--color-original);
|
||||
padding: var(--wa-space-s);
|
||||
border: 1px solid hsl(0 0 100 / 60%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
display: flex;
|
||||
gap: var(--wa-space-xs);
|
||||
position: absolute;
|
||||
bottom: var(--wa-space-xs);
|
||||
left: var(--wa-space-s);
|
||||
font-weight: var(--wa-font-weight-semibold);
|
||||
|
||||
wa-dropdown.pin-hue {
|
||||
wa-button {
|
||||
--outlined-border-color: oklab(from currentColor l a b / 10%);
|
||||
--outlined-background-color-hover: transparent;
|
||||
--border-width: 1.5px;
|
||||
--text-color: currentColor;
|
||||
--wa-space: var(--wa-space-xs);
|
||||
--wa-space-smaller: var(--wa-space-2xs);
|
||||
}
|
||||
|
||||
&.pin-hue.pinned {
|
||||
wa-button {
|
||||
--outlined-border-color: oklab(from currentColor l a b / 40%);
|
||||
font-weight: var(--wa-font-weight-bold);
|
||||
}
|
||||
}
|
||||
|
||||
wa-icon[name='thumbtack'] {
|
||||
opacity: 60%;
|
||||
}
|
||||
}
|
||||
|
||||
.level {
|
||||
font-weight: var(--wa-font-weight-bold);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wa-input {
|
||||
margin-top: var(--wa-space-xs);
|
||||
}
|
||||
|
||||
wa-icon-button {
|
||||
color: light-dark(black, white);
|
||||
transition: opacity var(--wa-transition-slow);
|
||||
--background-color-hover: oklab(from currentColor l a b / 15%);
|
||||
--text-color-hover: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.color-to-role {
|
||||
--border-width: 0;
|
||||
margin-inline-start: calc(-1 * var(--wa-space-3xs));
|
||||
|
||||
&::part(tags) {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
&::part(combobox) {
|
||||
padding: var(--wa-space-3xs);
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wa-icon-button.delete-button {
|
||||
position: absolute;
|
||||
top: var(--wa-space-s);
|
||||
right: var(--wa-space-s);
|
||||
--text-color-hover: var(--wa-color-danger-on-normal);
|
||||
}
|
||||
|
||||
.pinned-icon {
|
||||
opacity: 70%;
|
||||
}
|
||||
|
||||
#suggested-colors {
|
||||
margin-top: var(--wa-space-2xl);
|
||||
|
||||
h3 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&::part(content) {
|
||||
padding-block-start: 0;
|
||||
}
|
||||
|
||||
p.wa-caption-m {
|
||||
margin-block: var(--wa-space-xs) var(--wa-space-m);
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--wa-space-s);
|
||||
|
||||
wa-button {
|
||||
/* --background-color-hover: var(--background-color); */
|
||||
height: var(--wa-form-control-height);
|
||||
aspect-ratio: 1.2;
|
||||
|
||||
wa-icon {
|
||||
transition: var(--wa-transition-normal);
|
||||
}
|
||||
|
||||
&:not(:focus, :hover) wa-icon {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#roles {
|
||||
margin-block: var(--wa-space-2xl);
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--wa-space-m);
|
||||
|
||||
> wa-select {
|
||||
flex: 1;
|
||||
max-width: 20ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.seed-color-tweak .popup {
|
||||
min-width: clamp(0ch, 50ch, 90vw);
|
||||
}
|
||||
@@ -1,390 +0,0 @@
|
||||
/* CSS included both in predefined palettes and custom ones */
|
||||
:root {
|
||||
--fa-sliders-simple: '\f1de';
|
||||
}
|
||||
|
||||
.core-column {
|
||||
position: relative;
|
||||
|
||||
> wa-dropdown {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
wa-dropdown > .color.swatch {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.color-slider {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
|
||||
wa-slider {
|
||||
grid-column: 1 / -1;
|
||||
--track-height: 1em;
|
||||
--track-color-inactive: transparent;
|
||||
--track-color-active: transparent;
|
||||
--thumb-color: var(--color);
|
||||
--thumb-shadow: 0 0 0 var(--thumb-gap) var(--wa-color-surface-default),
|
||||
var(--wa-shadow-offset-x-m) var(--wa-shadow-offset-y-m) var(--wa-shadow-blur-m)
|
||||
calc(var(--wa-shadow-offset-x-m) * -1 + var(--thumb-gap)) var(--wa-color-shadow);
|
||||
|
||||
&:active {
|
||||
--thumb-size: 2em;
|
||||
}
|
||||
|
||||
&::part(base) {
|
||||
position: relative;
|
||||
background: linear-gradient(to right in var(--color-interpolation-space, oklab), var(--color-1), var(--color-2));
|
||||
}
|
||||
|
||||
.tick {
|
||||
--width: 1px;
|
||||
--height: 0.5em;
|
||||
--tick-color: var(--wa-color-neutral-border-normal);
|
||||
width: 4px;
|
||||
height: 2.4em;
|
||||
background: no-repeat;
|
||||
background-image: linear-gradient(var(--tick-color) 0 100%), linear-gradient(var(--tick-color) 0 100%);
|
||||
background-position: top, bottom;
|
||||
background-size: var(--width) var(--height);
|
||||
position: absolute;
|
||||
left: calc(var(--default-value-progress) * 100% - (var(--default-value-progress) - 0.5) * var(--thumb-size));
|
||||
translate: -50% 0;
|
||||
bottom: -0.5em;
|
||||
|
||||
&:hover {
|
||||
--tick-color: var(--wa-color-neutral-border-loud);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[slot='label'] {
|
||||
min-height: 1.1lh;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
vertical-align: middle;
|
||||
font-size: var(--wa-font-size-xs);
|
||||
}
|
||||
|
||||
.label-min,
|
||||
.label-max {
|
||||
font-size: var(--wa-font-size-xs);
|
||||
}
|
||||
|
||||
.label-min {
|
||||
grid-column: 1;
|
||||
margin-inline-start: 0.15em;
|
||||
}
|
||||
|
||||
.label-max {
|
||||
grid-column: 3;
|
||||
margin-inline-end: 0.1em;
|
||||
}
|
||||
}
|
||||
|
||||
[data-component='h'] {
|
||||
--color-interpolation-space: oklch increasing hue;
|
||||
}
|
||||
|
||||
.popup {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: var(--wa-space-m);
|
||||
background: var(--wa-color-surface-default);
|
||||
color: var(--wa-color-text-normal);
|
||||
border: 1px solid var(--wa-color-surface-border);
|
||||
padding: var(--wa-space-m) var(--wa-space-l);
|
||||
border-radius: var(--wa-border-radius-m);
|
||||
color-scheme: light;
|
||||
|
||||
.copyable-code {
|
||||
display: flex;
|
||||
gap: var(--wa-space-xs);
|
||||
align-items: center;
|
||||
|
||||
code {
|
||||
flex: 1;
|
||||
max-width: 20ch;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
> legend {
|
||||
/* Force legend to be rendered inside the fieldset */
|
||||
float: left;
|
||||
clear: all;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.wa-heading-s {
|
||||
display: flex;
|
||||
gap: var(--wa-gap-xs);
|
||||
align-items: center;
|
||||
|
||||
> :nth-child(1 of .align-end) {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@scope (.wa-dark) to (.wa-light) {
|
||||
.popup {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
.color-scale {
|
||||
th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tweak-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
right: var(--wa-space-s);
|
||||
opacity: var(--tweak-icon-opacity, 0%);
|
||||
}
|
||||
|
||||
.color.swatch:hover {
|
||||
--tweak-icon-opacity: 40%;
|
||||
}
|
||||
|
||||
&.tweaked .core-column {
|
||||
--tweak-icon-opacity: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.tweaked-callout {
|
||||
padding: var(--wa-space-xs);
|
||||
padding-inline-start: var(--wa-space-m);
|
||||
margin-block: var(--wa-space-m);
|
||||
align-items: center;
|
||||
|
||||
&:not(.tweaked-any *) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&::part(message) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-xs);
|
||||
}
|
||||
|
||||
wa-button:first-of-type {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Better UI before Vue initializes */
|
||||
[v-if='saved'],
|
||||
[v-if^='tweaked'],
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.static-palette:has(+ .colors:not([v-cloak])) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.core-color {
|
||||
wa-radio-button::part(base) {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
padding: 0;
|
||||
border-radius: var(--wa-border-radius-circle);
|
||||
background: var(--color);
|
||||
background-clip: border-box;
|
||||
}
|
||||
|
||||
wa-radio-button:is([checked], :state(checked))::part(base) {
|
||||
box-shadow:
|
||||
inset 0 0 0 var(--indicator-width) var(--indicator-color),
|
||||
inset 0 0 0 calc(var(--indicator-width) + 1.5px) var(--wa-color-surface-default);
|
||||
}
|
||||
|
||||
&::part(form-control-input) {
|
||||
gap: var(--wa-space-xs);
|
||||
}
|
||||
}
|
||||
|
||||
[data-slug='custom'] > :not(.seeded) #seed-colors ~ :not(#saved),
|
||||
#outline:has(+ main > [data-slug='custom'] > :not(.seeded)) li:nth-child(n + 2) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
[id='palette-info'] {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-auto-flow: column;
|
||||
|
||||
> * {
|
||||
grid-column: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.hue-wheel {
|
||||
--r: clamp(2em, 6rem, 25vmin);
|
||||
grid-column: 2;
|
||||
grid-row: 1 / 5;
|
||||
position: relative;
|
||||
width: calc(var(--r) * 2);
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
--lc: var(--avg-l) var(--max-c);
|
||||
--lc2: var(--avg-l) calc(var(--max-c) / 2);
|
||||
margin-top: calc(var(--r) * -0.05);
|
||||
background: conic-gradient(
|
||||
in oklch,
|
||||
oklch(var(--lc) 0),
|
||||
oklch(var(--lc) 60),
|
||||
oklch(var(--lc) 120),
|
||||
oklch(var(--lc) 180),
|
||||
oklch(var(--lc) 240),
|
||||
oklch(var(--lc) 300),
|
||||
oklch(var(--lc) 360)
|
||||
);
|
||||
|
||||
&,
|
||||
&::before {
|
||||
--stops: oklch(var(--lc) 0), oklch(var(--lc) 60), oklch(var(--lc) 120), oklch(var(--lc) 180), oklch(var(--lc) 240),
|
||||
oklch(var(--lc) 300), oklch(var(--lc) 360);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
-webkit-mask: radial-gradient(white, transparent);
|
||||
background: radial-gradient(oklch(var(--avg-l) calc(var(--gray-chroma) * var(--max-c)) 0) 5%, transparent 30%),
|
||||
conic-gradient(
|
||||
in oklch,
|
||||
oklch(var(--lc2) 0),
|
||||
oklch(var(--lc2) 60),
|
||||
oklch(var(--lc2) 120),
|
||||
oklch(var(--lc2) 180),
|
||||
oklch(var(--lc2) 240),
|
||||
oklch(var(--lc2) 300),
|
||||
oklch(var(--lc2) 360)
|
||||
);
|
||||
}
|
||||
|
||||
.color {
|
||||
--scale-c: calc(var(--c) / var(--max-c));
|
||||
--distance: calc(var(--r) * var(--scale-c));
|
||||
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) rotate(calc(var(--h) * 1deg - 90deg)) translateX(var(--distance));
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: calc(1.2em + 0.3em * var(--scale-c));
|
||||
aspect-ratio: 1;
|
||||
|
||||
&:hover {
|
||||
--scale: 1.2;
|
||||
--line-color: white;
|
||||
--line-style: solid;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
border-top: 2px var(--line-style, dashed) var(--line-color, var(--wa-color-gray-80));
|
||||
padding-top: 100%;
|
||||
top: calc(50% - 1px);
|
||||
right: 50%;
|
||||
width: var(--distance);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
box-shadow: var(--wa-shadow-l);
|
||||
background: var(--color);
|
||||
transition: var(--wa-transition-fast);
|
||||
scale: var(--scale, 1);
|
||||
}
|
||||
}
|
||||
|
||||
wa-tooltip {
|
||||
/* Prevent flickering */
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.scale-filter {
|
||||
wa-tab wa-icon {
|
||||
margin-right: 0.4em;
|
||||
}
|
||||
}
|
||||
|
||||
.title wa-icon-button[name='pencil'] {
|
||||
margin-inline-start: var(--wa-space-xs);
|
||||
}
|
||||
|
||||
.seeded {
|
||||
wa-badge.status {
|
||||
display: none;
|
||||
}
|
||||
|
||||
wa-badge.pro {
|
||||
filter: grayscale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.selected-swatch,
|
||||
.color-select wa-option::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 1.2em;
|
||||
aspect-ratio: 1;
|
||||
flex: none;
|
||||
border-radius: var(--wa-border-radius-m);
|
||||
background: var(--color);
|
||||
border: 1px solid var(--wa-color-surface-default);
|
||||
}
|
||||
|
||||
.color-select wa-option {
|
||||
white-space: nowrap;
|
||||
|
||||
&::before {
|
||||
width: 1em;
|
||||
margin-inline: var(--wa-space-xs);
|
||||
}
|
||||
|
||||
&::part(checked-icon) {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
wa-icon[name='square-plus'] {
|
||||
vertical-align: -0.15em;
|
||||
color: var(--color-gray);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.color-popup {
|
||||
display: block;
|
||||
|
||||
.popup {
|
||||
min-width: 25ch;
|
||||
}
|
||||
}
|
||||
|
||||
wa-icon[name='thumbtack'],
|
||||
wa-icon-button[name='thumbtack']::part(icon) {
|
||||
rotate: 45deg;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,357 +0,0 @@
|
||||
const template = `
|
||||
<wa-card size="small" class="color" :class="{tweaked}"
|
||||
:style="{'--color': value, '--color-original': inputValue}">
|
||||
<div slot="image" :style="{ colorScheme: level <= 60 ? 'dark' : 'light'}">
|
||||
|
||||
<color-popup placement="top-start" class="seed-color-tweak" :pinned=true deletable @delete="$emit('delete')" title="Edit color">
|
||||
<wa-icon-button name="sliders-simple" class="tweak-icon"></wa-icon-button>
|
||||
<template #content>
|
||||
<color-slider label="Hue" label-default="Entered color"
|
||||
coord="h" :min="0" :max="359" :step="1"
|
||||
v-model:color="color" :default-value="inputLCH[2]" ></color-slider>
|
||||
<color-slider label="Colorfulness" label-default="Entered color"
|
||||
coord="c" :min="0" :max="maxChroma" :step="0.001"
|
||||
v-model:color="color" :default-value="inputLCH[1]" format-type="scale" :format-base-value="maxChroma" ></color-slider>
|
||||
<color-slider label="Lightness" label-default="Entered color"
|
||||
coord="l" :min="0" :max="1" :step="0.01"
|
||||
v-model:color="color" :default-value="inputLCH[0]" format-type="scale" :format-base-value="1" ></color-slider>
|
||||
</template>
|
||||
</color-popup>
|
||||
|
||||
<div class="name">
|
||||
<wa-dropdown class="pin-hue" :class="{pinned: pinnedHue}">
|
||||
<wa-button slot="trigger" appearance="outlined" caret>
|
||||
<wa-icon name="thumbtack" v-if="pinnedHue" variant="solid" slot="prefix"></wa-icon>
|
||||
{{ capitalize(hue) || 'New color' }}
|
||||
</wa-button>
|
||||
<wa-menu @wa-select="pinnedHue = $event.detail.item.value">
|
||||
<wa-menu-item type="checkbox" :checked="pinnedHue ? null : ''">Automatic <em>({{ capitalize(detectedColorInfo.hue) }})</em></wa-menu-item>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-menu-label>Pin to…</wa-menu-label>
|
||||
<wa-menu-item v-for="hue in allHues" type="checkbox" :value="hue" :checked="pinnedHue === hue ? '' : null">{{ capitalize(hue) }}</wa-menu-item>
|
||||
</wa-menu>
|
||||
</wa-dropdown>
|
||||
<span class="level">{{ level }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<wa-select class="color-to-role" multiple appearance="plain" placeholder="(No states)" max-options-visible="2"
|
||||
ref="roles" :value.attr="Object.keys(roles).join(' ')" :value="Object.keys(roles)"
|
||||
:getTag="getTag"
|
||||
@input="$emit('update:roles', $event.target.value)">
|
||||
<wa-option v-for="role in ROLES" :value="role" :class="{'default': !roles[role]}">{{ capitalize(role) }}</wa-option>
|
||||
</wa-select>
|
||||
|
||||
<wa-input :value="valueRaw" @input="handleInput" @focus="inputFocused = true" @blur="inputFocused = false" ref="input"></wa-input>
|
||||
</wa-card>
|
||||
`;
|
||||
|
||||
import Color from 'https://colorjs.io/dist/color.js';
|
||||
// import { nextTick } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
|
||||
import { nextTick } from 'https://cdn.jsdelivr.net/npm/vue@3/dist/vue.esm-browser.js';
|
||||
import getMaxChroma from '../color/get-max-chroma.js';
|
||||
import { identifyColor } from '../color/util.js';
|
||||
import ColorPopup from './color-popup.js';
|
||||
import ColorSlider from './color-slider.js';
|
||||
import InfoTip from './info-tip.js';
|
||||
import { ROLES, allHues } from '/assets/scripts/tweak/data.js';
|
||||
import { capitalize } from '/assets/scripts/tweak/util.js';
|
||||
|
||||
await customElements.whenDefined('wa-select');
|
||||
|
||||
let maxUid = 0;
|
||||
|
||||
const expose = [
|
||||
'valueRaw',
|
||||
'value',
|
||||
'inputValueRaw',
|
||||
'inputValue',
|
||||
'colorRaw',
|
||||
'color',
|
||||
'inputColorRaw',
|
||||
'inputColor',
|
||||
'hue',
|
||||
'pinnedHue',
|
||||
'level',
|
||||
'tweaked',
|
||||
];
|
||||
|
||||
export default {
|
||||
expose,
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default(rawProps) {
|
||||
return { value: '' };
|
||||
},
|
||||
},
|
||||
otherColors: {
|
||||
type: Array,
|
||||
},
|
||||
roles: {
|
||||
type: Object,
|
||||
default: {},
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'update:roles', 'delete'],
|
||||
data() {
|
||||
let uid = this.modelValue.uid ?? maxUid++;
|
||||
if (this.modelValue.uid) {
|
||||
maxUid = Math.max(maxUid, uid);
|
||||
}
|
||||
this.modelValue.uid = uid;
|
||||
|
||||
let valueRaw = this.modelValue.value;
|
||||
let inputValueRaw = this.modelValue.inputValue ?? valueRaw;
|
||||
let color = tryColor(this.modelValue.value);
|
||||
let inputColor = tryColor(inputValueRaw);
|
||||
|
||||
return {
|
||||
uid,
|
||||
initialProps: { ...this.modelValue },
|
||||
valueRaw,
|
||||
value: color ? valueRaw : undefined,
|
||||
color,
|
||||
inputValueRaw,
|
||||
inputValue: inputColor ? inputValueRaw : undefined,
|
||||
inputColor,
|
||||
pinnedHue: this.modelValue.pinnedHue,
|
||||
editing: 0,
|
||||
inputFocused: false,
|
||||
watching: {},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
// Non-reactive variables to expose
|
||||
Object.assign(this, { ROLES, allHues });
|
||||
},
|
||||
async mounted() {
|
||||
if (this.modelValue.editImmediately) {
|
||||
let input = this.$refs.input;
|
||||
await input.updateComplete;
|
||||
input.focus();
|
||||
input.select();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
inputLCH() {
|
||||
return this.inputColor?.oklch;
|
||||
},
|
||||
|
||||
currentLCH() {
|
||||
return this.color?.oklch;
|
||||
},
|
||||
|
||||
tweaked() {
|
||||
if (this.inputFocused || this.editing > 0 || !this.inputLCH || !this.currentLCH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.inputLCH.some((coord, i) => coord !== this.currentLCH[i]);
|
||||
},
|
||||
|
||||
computedValue() {
|
||||
let ret = {};
|
||||
for (let property of expose) {
|
||||
ret[property] = this[property];
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
colorRaw() {
|
||||
let ret = tryColor(this.modelValue.valueRaw);
|
||||
|
||||
if (ret) {
|
||||
this.value = this.modelValue.valueRaw;
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
colorInfo() {
|
||||
let ret = { ...this.detectedColorInfo };
|
||||
|
||||
if (this.pinnedHue) {
|
||||
ret.hue = this.pinnedHue;
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
detectedColorInfo() {
|
||||
if (!this.color) {
|
||||
return { hue: undefined, level: undefined };
|
||||
}
|
||||
|
||||
return identifyColor(this.color, this.otherColors);
|
||||
},
|
||||
|
||||
hue() {
|
||||
return this.colorInfo.hue;
|
||||
},
|
||||
|
||||
level() {
|
||||
return this.colorInfo.level;
|
||||
},
|
||||
|
||||
stringifiedColor() {
|
||||
// return stringifyColor(this.colorRaw);
|
||||
return this.color + '';
|
||||
},
|
||||
|
||||
inputColorRaw() {
|
||||
let ret = tryColor(this.inputValueRaw);
|
||||
|
||||
if (ret) {
|
||||
this.inputValue = this.inputValueRaw;
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
maxChroma() {
|
||||
if (!this.color) {
|
||||
return 0.4;
|
||||
}
|
||||
|
||||
return getMaxChroma(this.color.oklch.l, this.color.oklch.h);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
capitalize,
|
||||
|
||||
handleInput(event) {
|
||||
this.editing++;
|
||||
|
||||
let value = event.target.value;
|
||||
// Editing the input manually also incorporates any tweaks as part of the color itself
|
||||
// I.e. input color and color are now the same
|
||||
this.valueRaw = this.inputValueRaw = value;
|
||||
|
||||
nextTick().then(() => {
|
||||
if (this.colorRaw) {
|
||||
this.color = this.colorRaw;
|
||||
this.$refs.input.setCustomValidity('');
|
||||
} else {
|
||||
this.$refs.input.setCustomValidity('Invalid color');
|
||||
this.$refs.input.reportValidity();
|
||||
}
|
||||
|
||||
this.editing--;
|
||||
});
|
||||
},
|
||||
|
||||
mutateModelValue(mutator) {
|
||||
if (this.watching.modelValue === null) {
|
||||
// If we're not watching modelValue, it means we're reacting to a change to it
|
||||
// so no point in updating it again
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.watching.modelValue) {
|
||||
this.watching.modelValue();
|
||||
this.watching.modelValue = null;
|
||||
}
|
||||
|
||||
mutator();
|
||||
|
||||
this.watching.modelValue = this.$watch('modelValue', {
|
||||
deep: true,
|
||||
handler() {
|
||||
let computedValue = this.computedValue;
|
||||
// What changed?
|
||||
|
||||
if (this.modelValue.value !== computedValue.value) {
|
||||
this.valueRaw = this.modelValue.value;
|
||||
}
|
||||
|
||||
if (this.modelValue.color + '' !== computedValue.color + '') {
|
||||
this.color = this.modelValue.color;
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
getTag(option) {
|
||||
let isDefault = option.classList.contains('default');
|
||||
let tag = Object.assign(document.createElement('wa-tag'), {
|
||||
part: `tag${isDefault ? ' default' : ''}`,
|
||||
exportparts: `
|
||||
base:tag__base,
|
||||
content:tag__content,
|
||||
remove-button:tag__remove-button,
|
||||
remove-button__base:tag__remove-button__base`,
|
||||
size: 'small',
|
||||
removable: !isDefault,
|
||||
'data-value': option.value,
|
||||
id: 'tag-' + option.value,
|
||||
innerHTML: option.label + ` <wa-tooltip hoist for="tag-${option.value}">Default role</wa-tooltip>`,
|
||||
});
|
||||
|
||||
return tag;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
/** colorRaw -> color */
|
||||
colorRaw: {
|
||||
deep: true,
|
||||
handler() {
|
||||
if (this.colorRaw) {
|
||||
this.color = this.colorRaw;
|
||||
}
|
||||
},
|
||||
},
|
||||
/** inputColorRaw -> inputColor */
|
||||
inputColorRaw: {
|
||||
deep: true,
|
||||
handler() {
|
||||
if (this.inputColorRaw) {
|
||||
this.inputColor = this.inputColorRaw;
|
||||
}
|
||||
},
|
||||
},
|
||||
/** color -> value, valueRaw, modelValue.value */
|
||||
color: {
|
||||
deep: true,
|
||||
handler() {
|
||||
if (this.tweaked && this.color) {
|
||||
// If tweaked, color is the source of truth
|
||||
this.value = this.valueRaw = this.color + '';
|
||||
}
|
||||
},
|
||||
},
|
||||
/** computedValue -> modelValue */
|
||||
computedValue: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.mutateModelValue(() => {
|
||||
Object.assign(this.modelValue, this.computedValue);
|
||||
this.$emit('update:modelValue', this.modelValue);
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
template,
|
||||
components: { InfoTip, ColorSlider, ColorPopup },
|
||||
compilerOptions: {
|
||||
isCustomElement: tag => tag.startsWith('wa-'),
|
||||
},
|
||||
};
|
||||
|
||||
function tryColor(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value instanceof Color) {
|
||||
return value;
|
||||
}
|
||||
|
||||
try {
|
||||
return new Color(value);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import Color from 'https://colorjs.io/dist/color.js';
|
||||
import { stringifyColor } from '../color/util.js';
|
||||
import InfoTip from './info-tip.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
title: String,
|
||||
token: String,
|
||||
color: Color,
|
||||
deletable: Boolean,
|
||||
pinnable: Boolean,
|
||||
pinned: Boolean,
|
||||
placement: String,
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
emits: ['delete', 'pin'],
|
||||
mounted() {
|
||||
let popup = this.$refs.popup;
|
||||
|
||||
if (popup) {
|
||||
// Find trigger
|
||||
let trigger = popup.previousElementSibling;
|
||||
if (trigger) {
|
||||
trigger.slot ||= 'trigger';
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
stringifiedColor() {
|
||||
return stringifyColor(this.color);
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<wa-dropdown class="color-popup" :placement>
|
||||
<slot></slot>
|
||||
<component :is="title ? 'fieldset' : 'div'" class="popup" ref="popup">
|
||||
<component :is="title ? 'legend' : 'div'" class="wa-heading-s" v-if="title || token || deletable || pinnable">
|
||||
<span v-if="title">{{ title }}</span>
|
||||
<wa-copy-button v-if="title && token" :value="token" :copy-label="token"></wa-copy-button>
|
||||
|
||||
<info-tip v-if="deletable && pinned">
|
||||
<wa-button size="small" variant="danger" appearance="plain" class="delete-button align-end" @click="$emit('delete')">
|
||||
<wa-icon name="trash" variant="regular"></wa-icon>
|
||||
</wa-button>
|
||||
<template #content>
|
||||
Delete from my colors
|
||||
</template>
|
||||
</info-tip>
|
||||
<info-tip v-if="pinnable && !pinned">
|
||||
<wa-button appearance="outlined" size="small" class="pin-color align-end" @click="$emit('pin')">
|
||||
<wa-icon name="thumbtack" variant="regular" slot="prefix"></wa-icon>
|
||||
Pin
|
||||
</wa-button>
|
||||
<template #content>
|
||||
Prevent this color from changing as others are edited
|
||||
</template>
|
||||
</info-tip>
|
||||
</component>
|
||||
|
||||
<slot name="content"></slot>
|
||||
|
||||
<div class="wa-stack wa-gap-xs">
|
||||
<div class="copyable-code" v-if="token && !title">
|
||||
<code>{{ token }}</code>
|
||||
<wa-copy-button :value="token"></wa-copy-button>
|
||||
</div>
|
||||
<div class="copyable-code" v-if="color">
|
||||
<code>{{ stringifiedColor }}</code>
|
||||
<wa-copy-button :value="stringifiedColor"></wa-copy-button>
|
||||
</div>
|
||||
</div>
|
||||
</component>
|
||||
</wa-dropdown>`,
|
||||
compilerOptions: {
|
||||
isCustomElement: tag => tag.startsWith('wa-'),
|
||||
},
|
||||
components: {
|
||||
InfoTip,
|
||||
},
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
import { capitalize } from '/assets/scripts/tweak/util.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
modelValue: String,
|
||||
label: String,
|
||||
getLabel: {
|
||||
type: Function,
|
||||
default: capitalize,
|
||||
},
|
||||
getContent: {
|
||||
type: Function,
|
||||
},
|
||||
getColor: {
|
||||
type: Function,
|
||||
default: value => `var(--wa-color-${value})`,
|
||||
},
|
||||
values: {
|
||||
type: Array,
|
||||
default: [],
|
||||
},
|
||||
groups: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'input'],
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
computedGroups() {
|
||||
let ret = {};
|
||||
|
||||
if (this.values?.length) {
|
||||
ret[''] = this.values;
|
||||
}
|
||||
|
||||
if (this.groups) {
|
||||
for (let group in this.groups) {
|
||||
if (this.groups[group]?.length) {
|
||||
ret[group] = this.groups[group];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
firstGroup() {
|
||||
return Object.keys(this.computedGroups)[0];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
capitalize,
|
||||
handleInput(e) {
|
||||
this.$emit('input', this.modelValue);
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<wa-select class="color-select" name="brand" :label="label" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)"
|
||||
:style="{'--color': getColor(modelValue)}">
|
||||
<template v-for="values, group in computedGroups">
|
||||
<template v-if="group">
|
||||
<wa-divider v-if="group !== firstGroup"></wa-divider>
|
||||
<small>{{ group }}</small>
|
||||
</template>
|
||||
<wa-option v-if="values?.length" v-for="value of values" :label="getLabel(value)" :value="value" :style="{'--color': getColor(value)}" v-html="getContent?.(value) ?? getLabel(value)"></wa-option>
|
||||
</template>
|
||||
<slot></slot>
|
||||
</wa-select>
|
||||
`,
|
||||
};
|
||||
@@ -1,343 +0,0 @@
|
||||
const template = `
|
||||
<div class="color-slider" :style="{
|
||||
'--color': computedColor, '--color-1': colorMin, '--color-2': colorMax,
|
||||
'--default-value-progress': defaultProgress,
|
||||
}" :data-component="coord || null">
|
||||
<wa-slider ref="slider" :min="computedMin" :max="computedMax" :step="step" :value="value"
|
||||
@input="handleInput($event.target.value);" @change="inputEnd($event.target.value)">
|
||||
<div slot="label">
|
||||
{{ label }}
|
||||
<wa-icon-button v-if="value !== computedDefaultValue" @click="reset" class="clear-button" name="circle-xmark" library="system" variant="regular" label="Reset"></wa-icon-button>
|
||||
<info-tip>
|
||||
<div class="tick"></div>
|
||||
<template #content>{{ computedLabelDefault }}</template>
|
||||
</info-tip>
|
||||
</div>
|
||||
</wa-slider>
|
||||
<div class="label-min">{{ labelMin }}</div>
|
||||
<div class="label-max">{{ labelMax }}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
import Color from 'https://colorjs.io/dist/color.js';
|
||||
import InfoTip from './info-tip.js';
|
||||
import { capitalize, promise, roundTo } from '/assets/scripts/tweak/util.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
coord: {
|
||||
type: String,
|
||||
required: true,
|
||||
validator(value) {
|
||||
return ['l', 'c', 'h'].includes(value);
|
||||
},
|
||||
},
|
||||
color: Color,
|
||||
defaultColor: Color,
|
||||
|
||||
defaultValue: Number,
|
||||
defaultValueRelative: Number,
|
||||
|
||||
/** Used for relative types. Defaults to defaultValue if not provided. */
|
||||
baseValue: Number,
|
||||
|
||||
/** Used for formatting only. Only specify if different from base value. */
|
||||
formatBaseValue: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
|
||||
modelValue: {
|
||||
type: Number,
|
||||
},
|
||||
min: Number,
|
||||
max: Number,
|
||||
minRelative: Number,
|
||||
maxRelative: Number,
|
||||
step: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
|
||||
type: {
|
||||
type: String,
|
||||
default: 'raw',
|
||||
},
|
||||
formatType: {
|
||||
type: String,
|
||||
},
|
||||
|
||||
label: String,
|
||||
labelMin: String,
|
||||
labelMax: String,
|
||||
labelDefault: String,
|
||||
},
|
||||
emits: ['update:modelValue', 'update:color', 'input'],
|
||||
data() {
|
||||
return {
|
||||
mounted: promise(),
|
||||
initialColor: this.color,
|
||||
value: undefined,
|
||||
tweaking: false,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (!this.color && !this.defaultColor) {
|
||||
console.warn(
|
||||
`[${this.label}]`,
|
||||
'<color-slider> requires at least one of the following props: color, defaultColor',
|
||||
);
|
||||
}
|
||||
|
||||
if (this.modelValue !== undefined) {
|
||||
this.value = this.getAbsoluteValue(this.modelValue);
|
||||
} else if (this.color) {
|
||||
this.value = this.colorCoords[this.coordIndex];
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.$refs.slider) {
|
||||
this.$refs.slider.tooltipFormatter = value => this.formatValue(value);
|
||||
this.$refs.slider.colorSliderData = this; // for debugging
|
||||
}
|
||||
|
||||
this.mounted.resolve();
|
||||
},
|
||||
beforeUnmount() {
|
||||
delete this.$refs.slider?.colorSliderData;
|
||||
},
|
||||
computed: {
|
||||
computedMin() {
|
||||
if (this.minRelative !== undefined) {
|
||||
return getAbsoluteValue(this.minRelative);
|
||||
}
|
||||
|
||||
return this.min;
|
||||
},
|
||||
|
||||
computedMax() {
|
||||
if (this.maxRelative !== undefined) {
|
||||
return this.getAbsoluteValue(this.maxRelative);
|
||||
}
|
||||
|
||||
return this.max;
|
||||
},
|
||||
|
||||
computedColor() {
|
||||
return this.getColorAt(this.value);
|
||||
},
|
||||
|
||||
computedColorCoords() {
|
||||
return this.computedColor.oklch.slice();
|
||||
},
|
||||
|
||||
colorCoords() {
|
||||
let color = this.color ?? this.computedColor;
|
||||
return color?.oklch.slice();
|
||||
},
|
||||
|
||||
computedColorString() {
|
||||
return `oklch(${this.computedColorCoords.join(' ')})`;
|
||||
},
|
||||
|
||||
colorString() {
|
||||
return `oklch(${this.colorCoords.join(' ')})`;
|
||||
},
|
||||
|
||||
defaultCoords() {
|
||||
if (this.defaultColor) {
|
||||
return this.defaultColor.oklch.slice();
|
||||
}
|
||||
|
||||
let ret = this.color.oklch.slice();
|
||||
|
||||
if (this.defaultValue !== undefined) {
|
||||
ret[this.coordIndex] = this.defaultValue;
|
||||
}
|
||||
|
||||
return ret;
|
||||
},
|
||||
|
||||
coordIndex() {
|
||||
return ['l', 'c', 'h'].indexOf(this.coord);
|
||||
},
|
||||
|
||||
colorMin() {
|
||||
return this.getColorAt(this.computedMin);
|
||||
},
|
||||
|
||||
colorMax() {
|
||||
return this.getColorAt(this.computedMax);
|
||||
},
|
||||
|
||||
isRelative() {
|
||||
return this.type && this.type !== 'raw';
|
||||
},
|
||||
|
||||
computedBaseValue() {
|
||||
if (!this.isRelative) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.baseValue !== undefined) {
|
||||
return this.baseValue;
|
||||
}
|
||||
|
||||
return this.computedDefaultValue;
|
||||
},
|
||||
|
||||
computedDefaultValue() {
|
||||
if (this.defaultValue !== undefined) {
|
||||
return this.defaultValue;
|
||||
}
|
||||
|
||||
if (this.defaultValueRelative !== undefined) {
|
||||
return this.getAbsoluteValue(this.defaultValueRelative);
|
||||
}
|
||||
|
||||
if (this.baseValue !== undefined) {
|
||||
return this.baseValue;
|
||||
}
|
||||
|
||||
return this.defaultCoords[this.coordIndex];
|
||||
},
|
||||
|
||||
computedDefaultColor() {
|
||||
return this.defaultColor ?? this.getColorAt(this.computedDefaultValue);
|
||||
},
|
||||
|
||||
computedLabelDefault() {
|
||||
let labelDefault = this.labelDefault || 'Default value';
|
||||
let formattedDefaultValue = this.formatValue(this.computedDefaultValue);
|
||||
return `${labelDefault} (${formattedDefaultValue})`;
|
||||
},
|
||||
|
||||
defaultProgress() {
|
||||
return (this.computedDefaultValue - this.computedMin) / (this.computedMax - this.computedMin);
|
||||
},
|
||||
|
||||
relativeValue() {
|
||||
this.computedBaseValue;
|
||||
return this.getRelativeValue(this.value);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
capitalize,
|
||||
|
||||
getAbsoluteValue(relativeValue) {
|
||||
return getAbsoluteValue({
|
||||
type: this.type,
|
||||
relativeValue,
|
||||
baseValue: this.baseValue ?? this.computedBaseValue,
|
||||
});
|
||||
},
|
||||
|
||||
getRelativeValue(absoluteValue) {
|
||||
return getRelativeValue({
|
||||
type: this.type,
|
||||
absoluteValue,
|
||||
baseValue: this.baseValue ?? this.computedBaseValue,
|
||||
});
|
||||
},
|
||||
|
||||
formatValue(value = this.value) {
|
||||
let formatType = this.formatType ?? this.type;
|
||||
let style = formatType === 'scale' ? 'percent' : undefined;
|
||||
|
||||
if (formatType && formatType !== 'raw') {
|
||||
let baseValue = this.formatBaseValue ?? this.computedBaseValue;
|
||||
value = getRelativeValue({ type: formatType, absoluteValue: value, baseValue });
|
||||
}
|
||||
|
||||
value = roundTo(value, this.step);
|
||||
return value.toLocaleString(undefined, { style });
|
||||
},
|
||||
|
||||
getColorAt(value) {
|
||||
let coords = this.defaultCoords.slice();
|
||||
coords[this.coordIndex] = value;
|
||||
return new Color('oklch', coords);
|
||||
},
|
||||
|
||||
/** Called when value changes due to user interaction */
|
||||
handleInput(value) {
|
||||
this.value = value;
|
||||
this.tweaking = true;
|
||||
this.$emit('input', value);
|
||||
},
|
||||
|
||||
inputEnd() {
|
||||
this.tweaking = false;
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.handleInput(this.computedDefaultValue);
|
||||
this.inputEnd();
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
computedColorString() {
|
||||
if (this.color && this.colorString !== this.computedColorString) {
|
||||
// Color changed, communicate to the outside world
|
||||
this.$emit('update:color', this.computedColor);
|
||||
}
|
||||
},
|
||||
|
||||
colorString() {
|
||||
if (this.color && this.colorString !== this.computedColorString) {
|
||||
// Color changed in the outside world, update our internals
|
||||
if (this.colorCoords[this.coordIndex] !== this.value) {
|
||||
this.value = this.colorCoords[this.coordIndex];
|
||||
|
||||
let modelValue = this.getRelativeValue(this.value);
|
||||
this.$emit('update:modelValue', modelValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
relativeValue() {
|
||||
this.$emit('update:modelValue', this.relativeValue);
|
||||
},
|
||||
},
|
||||
template,
|
||||
components: { InfoTip },
|
||||
compilerOptions: {
|
||||
isCustomElement: tag => tag.startsWith('wa-'),
|
||||
},
|
||||
};
|
||||
|
||||
function getAbsoluteValue({ type, relativeValue, baseValue }) {
|
||||
if (baseValue === undefined) {
|
||||
type = 'raw';
|
||||
}
|
||||
|
||||
if (type === 'shift') {
|
||||
return relativeValue + baseValue;
|
||||
}
|
||||
|
||||
if (type === 'scale') {
|
||||
return relativeValue * baseValue;
|
||||
}
|
||||
|
||||
return relativeValue;
|
||||
}
|
||||
|
||||
function getRelativeValue({ type, absoluteValue, baseValue }) {
|
||||
if (baseValue === undefined) {
|
||||
type = 'raw';
|
||||
}
|
||||
|
||||
if (type === 'shift') {
|
||||
return absoluteValue - baseValue;
|
||||
}
|
||||
|
||||
if (type === 'scale') {
|
||||
if (!absoluteValue) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return absoluteValue / baseValue;
|
||||
}
|
||||
|
||||
return absoluteValue;
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
const template = `
|
||||
<wa-radio-group class="core-color" orientation="horizontal" :value="modelValue" @input="handleInput($event.target.value)">
|
||||
<template v-for="h in hues">
|
||||
<info-tip>
|
||||
<wa-radio-button :label="capitalize(h)" :value="h" :style="{'--color': colors[h]}"></wa-radio-button>
|
||||
<template #content>{{ capitalize(h) }}</template>
|
||||
</info-tip>
|
||||
</template>
|
||||
<div slot="label">{{ label }}</div>
|
||||
</wa-radio-group>
|
||||
`;
|
||||
|
||||
import Color from 'https://colorjs.io/dist/color.js';
|
||||
import InfoTip from './info-tip.js';
|
||||
import { hues } from '/assets/scripts/tweak/data.js';
|
||||
import { capitalize, promise, roundTo } from '/assets/scripts/tweak/util.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
modelValue: String,
|
||||
label: {
|
||||
type: String,
|
||||
default: 'Color',
|
||||
},
|
||||
colors: Object,
|
||||
},
|
||||
emits: ['update:modelValue', 'input'],
|
||||
data() {
|
||||
return {
|
||||
defaultValue: this.modelValue,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
Object.assign(this, { hues });
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
capitalize,
|
||||
|
||||
/** Called when value changes due to user interaction */
|
||||
handleInput(value) {
|
||||
this.value = value;
|
||||
this.$emit('input', value);
|
||||
this.$emit('update:modelValue', value);
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.handleInput(this.defaultValue);
|
||||
},
|
||||
},
|
||||
template,
|
||||
components: { InfoTip },
|
||||
compilerOptions: {
|
||||
isCustomElement: tag => tag.startsWith('wa-'),
|
||||
},
|
||||
};
|
||||
@@ -1,85 +0,0 @@
|
||||
const template = `
|
||||
<color-popup :title :token="token" :color="modelValue"
|
||||
:pinned :pinnable @pin="$emit('pin')" :deletable @delete="$emit('delete')">
|
||||
<div slot="trigger" class="color swatch" :style="{ '--color': modelValue, colorScheme: level > 60 ? 'light' : 'dark' }">
|
||||
<wa-icon class="pinned-icon" name="thumbtack" variant="regular" v-if="pinned"></wa-icon>
|
||||
<wa-icon name="sliders-simple" class="tweak-icon"></wa-icon>
|
||||
</div>
|
||||
<template #content>
|
||||
<color-slider v-if="(isEdge || pinned) && tweakBase && HUE_RANGES[hue]"
|
||||
:color="modelValue" @update:model-value="$emit('update:modelValue', $event)" :default-value="colors[hue][tweakBase].oklch.h"
|
||||
@input="!pinned ? $emit('pin') : null"
|
||||
coord="h" :min="HUE_RANGES[hue].min + 1" :max="HUE_RANGES[hue].max" :step="1"
|
||||
label="Hue shift" :label-min="moreHue[hueBefore[hue]]" :label-max="moreHue[hueAfter[hue]]"
|
||||
:label-default="\`\${capitalize(hue)} \${tweakBase}\`"
|
||||
></color-slider>
|
||||
</template>
|
||||
</color-popup>
|
||||
`;
|
||||
|
||||
import Color from 'https://colorjs.io/dist/color.js';
|
||||
import ColorPopup from './color-popup.js';
|
||||
import ColorSlider from './color-slider.js';
|
||||
import InfoTip from './info-tip.js';
|
||||
import { HUE_RANGES, hueAfter, hueBefore, hues, moreHue } from '/assets/scripts/tweak/data.js';
|
||||
import { capitalize, clamp, promise, roundTo } from '/assets/scripts/tweak/util.js';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
modelValue: Color,
|
||||
hue: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
level: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
coreLevel: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
pinned: Boolean,
|
||||
pinnable: Boolean,
|
||||
deletable: Boolean,
|
||||
colors: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
tweakBase: [String, Number],
|
||||
},
|
||||
emits: ['update:modelValue', 'pin', 'delete'],
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
created() {
|
||||
// Attach non-reactive data
|
||||
Object.assign(this, { moreHue, HUE_RANGES });
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return capitalize(this.hue) + ' ' + this.level;
|
||||
},
|
||||
hueBefore() {
|
||||
return hueBefore[this.hue];
|
||||
},
|
||||
hueAfter() {
|
||||
return hueAfter[this.hue];
|
||||
},
|
||||
token() {
|
||||
return `--wa-color-${this.hue}-${this.level}`;
|
||||
},
|
||||
isEdge() {
|
||||
return this.level == '95' || this.level == '05';
|
||||
},
|
||||
isCore() {
|
||||
return this.level == this.coreLevel;
|
||||
},
|
||||
},
|
||||
methods: { capitalize },
|
||||
template,
|
||||
components: { InfoTip, ColorSlider, ColorPopup },
|
||||
compilerOptions: {
|
||||
isCustomElement: tag => tag.startsWith('wa-'),
|
||||
},
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import Color from 'https://colorjs.io/dist/color.js';
|
||||
import { stringifyColor } from '../color/util.js';
|
||||
|
||||
let maxUid = 0;
|
||||
|
||||
export default {
|
||||
props: {},
|
||||
data() {
|
||||
let uid = ++maxUid;
|
||||
return { uid, id: 'info-tip-' + uid };
|
||||
},
|
||||
mounted() {
|
||||
let tooltip = this.$refs.tooltip;
|
||||
if (tooltip) {
|
||||
// Find trigger
|
||||
let trigger = tooltip.previousElementSibling;
|
||||
if (trigger) {
|
||||
if (trigger.id) {
|
||||
// Already has id
|
||||
this.id = trigger.id;
|
||||
} else {
|
||||
trigger.id = this.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
template: `
|
||||
<slot>
|
||||
<wa-icon-button :id="id" name="circle-question" variant="regular"></wa-icon-button>
|
||||
</slot>
|
||||
<wa-tooltip :for="id" ref="tooltip"><slot name="content"></slot></wa-tooltip>
|
||||
`,
|
||||
compilerOptions: {
|
||||
isCustomElement: tag => tag.startsWith('wa-'),
|
||||
},
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
---
|
||||
title: Custom
|
||||
isPro: true
|
||||
override:tags: [palettes, pro]
|
||||
order: 99
|
||||
description: Create your own color palette from scratch, from one or more seed colors.
|
||||
status: experimental
|
||||
---
|
||||
<link href="{{ page.url }}../app/custom.css" rel="stylesheet">
|
||||
|
||||
<h2 v-if="step > 0" v-cloak>My Colors</h2>
|
||||
|
||||
<p v-if="step > 0" v-cloak>
|
||||
Just add your colors, in any order. We’ll sort them out for you, generate tints, and suggest additional colors.
|
||||
</p>
|
||||
|
||||
<div id="seed-colors">
|
||||
<template v-for="color, i in seedColors" :key="color.uid ?? maxSeedUid">
|
||||
<color-input v-model="seedColors[i]"
|
||||
:other-colors="seedColors.filter((_, j) => j !== i).map(c => c.color)"
|
||||
:roles="seedColorRoles[i]"
|
||||
@update:roles="roles => setColorRole(i, roles)"
|
||||
@delete="deleteColor(i)"></color-input>
|
||||
</template>
|
||||
<wa-button class="add-button" appearance="outlined" @click="addColor(undefined, {editImmediately: true})">
|
||||
<wa-icon slot="prefix" name="plus" variant="regular"></wa-icon>
|
||||
<span v-content="step > 0 ? 'Add color' : 'New palette'">New palette</span>
|
||||
</wa-button>
|
||||
</div>
|
||||
|
||||
<wa-details id="suggested-colors" v-if="step > 0" v-cloak open>
|
||||
<h3 class="wa-heading-m" slot="summary">Suggestions</h3>
|
||||
|
||||
<p class="wa-caption-m">
|
||||
Generated by our fancy-schmancy algorithm to complement your colors.
|
||||
See a color you like? Grab it before it’s gone!
|
||||
</p>
|
||||
|
||||
<div class="suggestions wa-cluster wa-align-items-start wa-gap-s">
|
||||
<template v-for="color, hue in suggestedColors">
|
||||
<info-tip>
|
||||
<wa-button :style="{'--background-color': color}" @click="addColor({hue})">
|
||||
<wa-icon name="plus"></wa-icon>
|
||||
</wa-button>
|
||||
<template #content>{% raw %}{{ capitalize(hue) }}{% endraw %}</template>
|
||||
</info-tip>
|
||||
</template>
|
||||
</div>
|
||||
</wa-details>
|
||||
|
||||
<section id="roles" v-if="step > 0" v-cloak>
|
||||
<h2>Roles</h2>
|
||||
|
||||
<div>
|
||||
<color-select v-for="computedRole, role in computedRoles"
|
||||
:model-value="computedRoles[role]"
|
||||
@update:model-value="value => setRoleColor(role, value)"
|
||||
:class="{'default': !roles[role]}"
|
||||
:label="capitalize(role) + ':'"
|
||||
:groups="{
|
||||
Dynamic: !['brand', 'neutral'].includes(role) ? ['brand', 'neutral'] : undefined,
|
||||
Colors: Object.keys(paletteScales),
|
||||
Common: suggestedForRole[role]
|
||||
}"
|
||||
:get-label="capitalize"
|
||||
:get-content="value => capitalize(value) + (seedHues[value] || computedRoles[value] || value === 'gray' ? '' : ' <wa-icon name=square-plus variant=regular></wa-icon>')"
|
||||
:get-color="value => coreColors[computedRoles[value] ?? value]">
|
||||
{# <wa-badge class="default-badge" v-if="!roles[role]" slot="suffix" variant="neutral" appearance="outlined">Default</wa-badge> #}
|
||||
</color-select>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
layout: null
|
||||
permalink: "/docs/palettes/data.json"
|
||||
eleventyExcludeFromCollections: true
|
||||
---
|
||||
{
|
||||
{% for palette in collections.palettes %}
|
||||
{% set paletteId = palette.fileSlug -%}
|
||||
{% set colors = palettes[paletteId] -%}
|
||||
"{{ paletteId }}": {
|
||||
"title": "{{ palette.data.title }}",
|
||||
"colors": {
|
||||
{% for hue, tints in colors -%}
|
||||
"{{ hue }}": {
|
||||
{% for tint, value in tints -%}
|
||||
{% if tint != "05" -%}
|
||||
{% set value = value.coords or value -%}
|
||||
{% set key = "05" if tint == "5" else tint -%}
|
||||
"{{ key }}": {{ value | dump | safe }}{{ ', ' if not loop.last }}
|
||||
{%- endif %}
|
||||
{% endfor -%}
|
||||
}{{ ', ' if not loop.last }}
|
||||
{% endfor %} {# end of hue #}
|
||||
}
|
||||
}{{ ', ' if not loop.last }} {# end of palette #}
|
||||
{% endfor %}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
title: Default
|
||||
description: This is the palette used in the default theme.
|
||||
order: 0
|
||||
---
|
||||
|
||||
@@ -5,6 +5,6 @@ layout: overview
|
||||
override:tags: []
|
||||
forTag: palette
|
||||
categories:
|
||||
tags: [other, pro]
|
||||
other: Free
|
||||
pro: Pro
|
||||
---
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
{
|
||||
"layout": "palette.njk",
|
||||
"tags": ["palettes", "palette"],
|
||||
"wide": true,
|
||||
"eleventyComputed": {
|
||||
"snippet": ".wa-palette-{{ page.fileSlug }}",
|
||||
"icon": "palette",
|
||||
"file": "styles/color/{{ page.fileSlug }}.css"
|
||||
"icon": "palette"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,5 +2,7 @@
|
||||
title: Patterns
|
||||
description: Patterns are reusable solutions to common design problems.
|
||||
layout: overview
|
||||
categories: ["e-commerce"]
|
||||
listChildren: true
|
||||
override:tags: []
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Changelog
|
||||
description: Changes to each version of the project are documented here.
|
||||
layout: page-outline
|
||||
layout: page
|
||||
---
|
||||
|
||||
Web Awesome follows [Semantic Versioning](https://semver.org/). Breaking changes in components with the <wa-badge variant="brand" pill>Stable</wa-badge> badge will not be accepted until the next major version. As such, all contributions must consider the project's roadmap and take this into consideration. Features that are deemed no longer necessary will be deprecated but not removed.
|
||||
@@ -14,75 +14,11 @@ During the alpha period, things might break! We take breaking changes very serio
|
||||
|
||||
## Next
|
||||
|
||||
- Fixed `wa-pill` class for text fields
|
||||
- Fixed `pill` style for `<wa-input>` elements
|
||||
- Fixed a bug in `<wa-color-picker>` that prevented light dismiss from working when clicking immediately above the color picker dropdown
|
||||
- Fixed a bug in `<wa-select multiple>` that sometimes resulted in empty `<div>` elements being output
|
||||
|
||||
## 3.0.0-alpha.11
|
||||
|
||||
### Color Palettes
|
||||
|
||||
- Color palette tweaking UI. Tweak hue, grays, overall colorfulness, save or share the results.
|
||||
- Added a `pink` scale to all color palettes
|
||||
- Tweaked hues of all color palettes to make them more distinct and make their hues more intentional
|
||||
- Dropped `violet` and `teal`, instead using `purple` and `cyan` (this is not just a renaming, the colors have been adjusted too).
|
||||
- Fixed a bug in `<wa-switch>` that caused tooltips to work incorrectly when toggling the switch
|
||||
|
||||
### Design Tokens
|
||||
|
||||
- Added `--wa-color-[hue]` tokens with the "core" color of each scale, regardless of which tint it lives on.
|
||||
You can find them in the first column of each color palette.
|
||||
|
||||
### Themes
|
||||
|
||||
- Improved UI for theme remixing:
|
||||
- You can now override the brand color of any theme with any of the 9 hues supported.
|
||||
- Rich previews
|
||||
- Generated copyable code snippets.
|
||||
- Permalinks
|
||||
- Updated Active, Glossy, Playful, and Premium themes so that `--wa-color-brand-fill-loud` uses the core color of the chosen brand color, regardless of tint.
|
||||
|
||||
### Components
|
||||
|
||||
#### `<wa-radio>`
|
||||
|
||||
- Dropped the `base` part. It can now be styled by directly applying CSS to the element itself.
|
||||
- Added `hint` attribute and corresponding slot.
|
||||
|
||||
#### `<wa-select>`
|
||||
|
||||
- Added the `tag` part (and associated exported parts) to `<wa-select>` to allow targeting the tag that shows when more than the max number of visible items have been selected
|
||||
- Fixed a bug that prevented the placeholder color from being customized with the `--wa-form-control-placeholder-color` token
|
||||
- Fixed an incorrect CSS value in the expand icon
|
||||
- Fixed a bug that prevented the description from being read by screen readers
|
||||
|
||||
#### `<wa-option>`
|
||||
|
||||
- `label` attribute to override the generated label (useful for rich content)
|
||||
- `defaultLabel` property
|
||||
- Dropped `getTextLabel()` method (if you need dynamic labels, just set the `label` attribute dynamically)
|
||||
- Dropped `base` part for easier styling. CSS can now be applied directly to the element itself.
|
||||
|
||||
#### `<wa-menu-item>`
|
||||
|
||||
- `label` attribute to override the generated label (useful for rich content)
|
||||
- `defaultLabel` property
|
||||
- Dropped `getTextLabel()` method (if you need dynamic labels, just set the `label` attribute dynamically)
|
||||
|
||||
#### `<wa-card>`
|
||||
- Fixed a bug where child elements did not have correct rounding when headers and footers were absent.
|
||||
- Re-introduced `--border-color` so that the card itself can have a different border color than its inner borders.
|
||||
- Fixed a bug that prevented slots from showing automatically without `with-` attributes
|
||||
|
||||
#### `<wa-tab>`
|
||||
|
||||
- Fixed a bug that caused `document.createElement('wa-tab')` to fail (which also meant it could not be used in VueJS and other frameworks)
|
||||
|
||||
### Docs
|
||||
|
||||
- Added an orientation example to the native radio docs
|
||||
- Added the `tag` part (and associated exported parts) to `<wa-select>` to allow targeting the tag that shows when more than the max number of visible items have been selected
|
||||
- Fixed a number of broken event listeners throughout the docs
|
||||
- Fixed a bug in `<wa-card>` that prevented slots from showing automatically without `with-` attributes
|
||||
- Fixed a bug in `<wa-select>` that prevented the placeholder color from being customized with the `--wa-form-control-placeholder-color` token
|
||||
|
||||
## 3.0.0-alpha.10
|
||||
|
||||
|
||||
1
docs/docs/themes/active.md
vendored
1
docs/docs/themes/active.md
vendored
@@ -4,5 +4,4 @@ description: Energetic and tactile, always in motion.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: rudimentary
|
||||
brand: green
|
||||
---
|
||||
|
||||
1
docs/docs/themes/awesome.md
vendored
1
docs/docs/themes/awesome.md
vendored
@@ -3,5 +3,4 @@ title: Awesome
|
||||
description: Punchy and vibrant, the rockstar of themes.
|
||||
order: 0.2
|
||||
palette: bright
|
||||
brand: blue
|
||||
---
|
||||
|
||||
1
docs/docs/themes/brutalist.md
vendored
1
docs/docs/themes/brutalist.md
vendored
@@ -4,5 +4,4 @@ description: Sharp, square, and unapologetically bold.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: default
|
||||
brand: blue
|
||||
---
|
||||
|
||||
3
docs/docs/themes/creating.md
vendored
3
docs/docs/themes/creating.md
vendored
@@ -31,7 +31,8 @@ If you're customizing the default dark styles, scope your styles to the followin
|
||||
|
||||
```css
|
||||
.wa-dark,
|
||||
.wa-invert {
|
||||
.wa-invert,
|
||||
:is(:host-context(.wa-dark)) {
|
||||
/* your custom styles here */
|
||||
}
|
||||
```
|
||||
|
||||
1
docs/docs/themes/default.md
vendored
1
docs/docs/themes/default.md
vendored
@@ -3,5 +3,4 @@ title: Default
|
||||
description: Your trusty companion, like a perfectly broken-in pair of jeans.
|
||||
order: 0
|
||||
palette: default
|
||||
brand: blue
|
||||
---
|
||||
|
||||
69
docs/docs/themes/demo.njk
vendored
69
docs/docs/themes/demo.njk
vendored
@@ -10,17 +10,16 @@ override:tags: []
|
||||
eleventyComputed:
|
||||
forceTheme: "{{ theme.fileSlug }}"
|
||||
---
|
||||
{% set isPro = theme.data.isPro %}
|
||||
{% set status = theme.data.status %}
|
||||
{% set since = theme.data.since %}
|
||||
|
||||
<link rel="stylesheet" href="/dist/styles/themes/{{ theme.fileSlug }}.css" />
|
||||
<link rel="stylesheet" href="/docs/themes/showcase.css" />
|
||||
|
||||
{% set content %}
|
||||
<header>
|
||||
{% include 'breadcrumbs.njk' %}
|
||||
<h1 class="title">{{ theme.data.title }}</h1>
|
||||
<p id="mix_and_match" class="wa-size-s"></p>
|
||||
<p id="theme-status">{% include 'status.njk' %}</p>
|
||||
<p id="mix_and_match" hidden class="wa-size-s"></p>
|
||||
<p>{% include 'status.njk' %}</p>
|
||||
<p id="theme-showcase-description">{{ theme.data.description | inlineMarkdown | safe }}</p>
|
||||
</header>
|
||||
{% include 'theme-showcase.njk' %}
|
||||
@@ -35,56 +34,46 @@ eleventyComputed:
|
||||
</div>
|
||||
</wa-image-comparer>
|
||||
|
||||
<script type="module">
|
||||
import { urls as stylesheetURLs, docsURLs, icons } from "/assets/scripts/tweak/data.js";
|
||||
import { getThemeCode } from "/assets/scripts/tweak/code.js";
|
||||
|
||||
<script>
|
||||
function updateTheme() {
|
||||
let params = new URLSearchParams(window.location.search);
|
||||
params = Object.fromEntries(params.entries());
|
||||
let script = document.currentScript;
|
||||
const stylesheetURLs = {
|
||||
colors: id => `/dist/styles/themes/${ id }/color.css`,
|
||||
palette: id => `/dist/styles/color/${ id }.css`,
|
||||
typography: id => `/dist/styles/themes/${ id }/typography.css`
|
||||
};
|
||||
const icons = {
|
||||
colors: 'palette',
|
||||
palette: 'swatchbook',
|
||||
typography: 'font-case'
|
||||
}
|
||||
|
||||
for (let link of document.querySelectorAll('link.mix-and-match')) {
|
||||
link.remove();
|
||||
}
|
||||
|
||||
let tweaks = [];
|
||||
let code = getThemeCode("{{ theme.fileSlug }}", params, {attributes: 'class="mix-and-match"'});
|
||||
document.head.insertAdjacentHTML('beforeend', code);
|
||||
let msgs = [];
|
||||
|
||||
for (let name in stylesheetURLs) {
|
||||
let override = params[name];
|
||||
if (override) {
|
||||
let docsURL = docsURLs[name] ? docsURLs[name] + override + '/' : '';
|
||||
let title = override.replace(/^[a-z]/g, c => c.toUpperCase());
|
||||
|
||||
if (docsURL) {
|
||||
title = `<a href="${ docsURL }">${ title }</a>`;
|
||||
}
|
||||
|
||||
if (params.get(name)) {
|
||||
let url = stylesheetURLs[name](params.get(name));
|
||||
script.insertAdjacentHTML('afterend', `<link rel="stylesheet" href="${ url }" class="mix-and-match" />`);
|
||||
let docsURL = (name === 'palette' ? '/docs/palettes/' : '/docs/themes/') + params.get(name) + '/';
|
||||
let title = params.get(name).replace(/^[a-z]/g, c => c.toUpperCase());
|
||||
let icon = icons[name];
|
||||
tweaks.push(`<wa-icon name="${icon}" variant="regular"></wa-icon> ${ title }`);
|
||||
msgs.push(`<wa-icon name="${icon}" variant="regular"></wa-icon> <a href="${ docsURL }">${ title }</a>`);
|
||||
}
|
||||
}
|
||||
|
||||
let isRemixed = tweaks.length > 0;
|
||||
document.documentElement.classList.toggle('is-remixed', isRemixed);
|
||||
|
||||
if (isRemixed) {
|
||||
for (let p of document.querySelectorAll("#theme-status")) {
|
||||
let proBadge = p.querySelector(".pro");
|
||||
if (!proBadge) {
|
||||
p.insertAdjacentHTML('beforeend', '<wa-badge class="pro">PRO</wa-badge>');
|
||||
}
|
||||
}
|
||||
|
||||
for (let p of mix_and_match) {
|
||||
if (tweaks.length) {
|
||||
p.innerHTML = `<strong><wa-icon name="arrows-rotate"></wa-icon> Remixed</strong> ` + tweaks.map(msg => `<wa-badge appearance=outlined>
|
||||
${ msg }</wa-badge>`).join(' ');
|
||||
}
|
||||
for (let p of mix_and_match) {
|
||||
p.hidden = msgs.length === 0;
|
||||
if (msgs.length) {
|
||||
let icon =
|
||||
p.innerHTML = `<strong><wa-icon name="merge"></wa-icon> Remixed</strong> ` + msgs.map(msg => `<wa-badge appearance=outlined>
|
||||
${ msg }</wa-badge>`).join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
updateTheme();
|
||||
</script>
|
||||
|
||||
1
docs/docs/themes/glossy.md
vendored
1
docs/docs/themes/glossy.md
vendored
@@ -4,5 +4,4 @@ description: Bustling with plenty of luster and shine.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: elegant
|
||||
brand: indigo
|
||||
---
|
||||
|
||||
9
docs/docs/themes/index.njk
vendored
9
docs/docs/themes/index.njk
vendored
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: Themes
|
||||
description: Themes are collections of design tokens that thread through every Web Awesome component and pattern.
|
||||
description: Themes are collections of design tokens that thread through every Web Awesome component and pattern.
|
||||
Themes play a crucial role in [customizing Web Awesome](/docs/customizing).
|
||||
layout: overview
|
||||
override:tags: []
|
||||
forTag: theme
|
||||
categories:
|
||||
tags: [other, pro]
|
||||
other: Free
|
||||
pro: Pro
|
||||
---
|
||||
|
||||
<div class="max-line-length">
|
||||
@@ -30,7 +30,7 @@ In pre-made themes, we use a light color scheme by default.
|
||||
Additionally, styles may be scoped to the `:root` selector to be activated automatically.
|
||||
For pre-made themes, *all* custom properties are scoped to `:root`, the theme class, and `wa-light`.
|
||||
|
||||
Finally, we scope themes to `:host` to ensure the styles are applied to the shadow roots of custom elements.
|
||||
Finally, we scope themes to `:host` and `:host-context()` to ensure the styles are applied to the shadow roots of custom elements.
|
||||
|
||||
For example, the default theme is set up like this:
|
||||
|
||||
@@ -44,7 +44,8 @@ For example, the default theme is set up like this:
|
||||
}
|
||||
|
||||
.wa-dark,
|
||||
.wa-invert {
|
||||
.wa-invert,
|
||||
:host-context(.wa-dark) {
|
||||
/* subset of CSS custom properties for a dark color scheme */
|
||||
}
|
||||
```
|
||||
|
||||
1
docs/docs/themes/matter.md
vendored
1
docs/docs/themes/matter.md
vendored
@@ -4,7 +4,6 @@ description: Digital design inspired by the real world.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: mild
|
||||
brand: indigo
|
||||
---
|
||||
|
||||
Set the page theme to "{{ title }}" from the top right to preview the following examples.
|
||||
|
||||
1
docs/docs/themes/playful.md
vendored
1
docs/docs/themes/playful.md
vendored
@@ -4,5 +4,4 @@ description: Cheerful and engaging, like a playground on screen.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: rudimentary
|
||||
brand: purple
|
||||
---
|
||||
|
||||
1
docs/docs/themes/premium.md
vendored
1
docs/docs/themes/premium.md
vendored
@@ -4,5 +4,4 @@ description: The ultimate in sophistication and style.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: anodized
|
||||
brand: cyan
|
||||
---
|
||||
|
||||
102
docs/docs/themes/remix.css
vendored
102
docs/docs/themes/remix.css
vendored
@@ -1,102 +0,0 @@
|
||||
#mix_and_match {
|
||||
margin-block-end: var(--wa-space-2xl);
|
||||
|
||||
&::part(content) {
|
||||
display: flex;
|
||||
gap: var(--wa-space-xl);
|
||||
padding-block-start: var(--wa-space-m);
|
||||
}
|
||||
|
||||
> [slot='summary'] {
|
||||
margin: 0;
|
||||
|
||||
wa-icon:first-of-type {
|
||||
vertical-align: -0.15em;
|
||||
color: var(--wa-color-text-quiet);
|
||||
}
|
||||
|
||||
wa-icon#what-is-remixing {
|
||||
vertical-align: -0.1em;
|
||||
font-size: var(--wa-font-size-s);
|
||||
color: var(--wa-color-text-quiet);
|
||||
}
|
||||
}
|
||||
|
||||
wa-select {
|
||||
&::part(label) {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
}
|
||||
|
||||
wa-select:has(> wa-option > wa-card) {
|
||||
&::part(listbox) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
width: min(90vw, 800px);
|
||||
}
|
||||
|
||||
&:state(blank)::part(display-input) {
|
||||
font-style: italic;
|
||||
color: var(--wa-color-text-quiet);
|
||||
}
|
||||
}
|
||||
|
||||
wa-option:has(> wa-card) {
|
||||
position: relative;
|
||||
padding: var(--wa-space-smaller);
|
||||
|
||||
&::part(checked-icon) {
|
||||
position: absolute;
|
||||
inset-block-start: calc(var(--wa-space-smaller) - 0.5em);
|
||||
inset-inline-end: calc(var(--wa-space-smaller) - 0.5em);
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
line-height: 1em;
|
||||
padding: 0.4em;
|
||||
border-radius: var(--wa-border-radius-circle);
|
||||
text-align: center;
|
||||
background: var(--wa-color-brand-fill-loud);
|
||||
color: var(--wa-color-brand-on-loud);
|
||||
font-size: var(--wa-font-size-xs);
|
||||
}
|
||||
|
||||
wa-card {
|
||||
--spacing: var(--wa-space-smaller);
|
||||
|
||||
&::part(body) {
|
||||
background: var(--wa-color-neutral-fill-quiet);
|
||||
padding-block: var(--wa-space-s);
|
||||
}
|
||||
}
|
||||
|
||||
&:state(current),
|
||||
&:state(hover),
|
||||
&:state(selected),
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
wa-card {
|
||||
border-color: var(--wa-color-brand-border-loud);
|
||||
box-shadow: 0 0 0 var(--wa-border-width-m) var(--wa-color-brand-border-normal);
|
||||
}
|
||||
}
|
||||
|
||||
&[aria-selected='true'] {
|
||||
wa-card {
|
||||
--border-color: var(--wa-color-brand-border-loud);
|
||||
box-shadow: 0 0 0 var(--wa-border-width-m) var(--wa-color-brand-border-loud);
|
||||
|
||||
&::part(body) {
|
||||
background: var(--wa-color-brand-fill-quiet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#test_select wa-option:state(selected) {
|
||||
outline: 2px solid red;
|
||||
background: yellow;
|
||||
}
|
||||
166
docs/docs/themes/remix.js
vendored
166
docs/docs/themes/remix.js
vendored
@@ -1,166 +0,0 @@
|
||||
import Prism from '/assets/scripts/prism.js';
|
||||
import { cdnUrl, getThemeCode, Permalink } from '/assets/scripts/tweak.js';
|
||||
await Promise.all(['wa-select', 'wa-option', 'wa-details'].map(tag => customElements.whenDefined(tag)));
|
||||
|
||||
const domChange = document.startViewTransition ? document.startViewTransition.bind(document) : fn => fn();
|
||||
|
||||
let selects, data, codeSnippets;
|
||||
|
||||
let computed = {
|
||||
get isRemixed() {
|
||||
return Object.values(data.params).filter(Boolean).length > 0;
|
||||
},
|
||||
get palette() {
|
||||
return data.params.palette || data.defaultParams.palette;
|
||||
},
|
||||
get brand() {
|
||||
return data.params.brand || data.defaultParams.brand;
|
||||
},
|
||||
};
|
||||
|
||||
function selectsChanged(event) {
|
||||
data.params[event.target.name] = event.target.value;
|
||||
render(event.target.name);
|
||||
}
|
||||
|
||||
function init() {
|
||||
selects = Object.fromEntries(
|
||||
[...document.querySelectorAll('#mix_and_match wa-select')].map(select => [select.getAttribute('name'), select]),
|
||||
);
|
||||
|
||||
codeSnippets = document.querySelector('#usage ~ wa-tab-group.import-stylesheet-code:first-of-type');
|
||||
codeSnippets = {
|
||||
html: codeSnippets.querySelector('code.language-html'),
|
||||
css: codeSnippets.querySelector('code.language-css'),
|
||||
};
|
||||
|
||||
data = {
|
||||
baseTheme: wa_data.baseTheme,
|
||||
themes: wa_data.themes,
|
||||
palettes: wa_data.palettes,
|
||||
defaultParams: {
|
||||
colors: '',
|
||||
get palette() {
|
||||
let colors = data.params.colors || data.baseTheme;
|
||||
return data.themes[colors].palette;
|
||||
},
|
||||
get brand() {
|
||||
let colors = data.params.colors || data.baseTheme;
|
||||
return data.themes[colors].brand;
|
||||
},
|
||||
typography: '',
|
||||
},
|
||||
params: { colors: '', palette: '', brand: '', typography: '' },
|
||||
urlParams: new Permalink(),
|
||||
};
|
||||
|
||||
// Apply params from permalink
|
||||
for (let key in data.params) {
|
||||
if (data.urlParams.has(key)) {
|
||||
data.params[key] = data.urlParams.get(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (computed.isRemixed) {
|
||||
// Start with the remixing UI open if the theme has been remixed
|
||||
mix_and_match.setAttribute('open', '');
|
||||
mix_and_match.open = true;
|
||||
}
|
||||
|
||||
for (let name in selects) {
|
||||
selects[name].addEventListener('change', selectsChanged);
|
||||
}
|
||||
|
||||
Promise.all(Object.values(selects).map(select => select.updateComplete)).then(() => render());
|
||||
|
||||
return { selects, codeSnippets, data, computed, render };
|
||||
}
|
||||
|
||||
globalThis.remixApp = init();
|
||||
|
||||
// Async load CSS for other themes *before* current theme stylesheet
|
||||
let themeStylesheet = document.querySelector('#theme-stylesheet');
|
||||
|
||||
for (const theme in data.themes) {
|
||||
themeStylesheet.insertAdjacentHTML(
|
||||
'beforebegin',
|
||||
`<link rel="preload" as="style" href="/dist/styles/themes/${theme}/color.css" onload="this.rel = 'stylesheet'" />
|
||||
<link rel="preload" as="style" href="/dist/styles/themes/${theme}/typography.css" onload="this.rel = 'stylesheet'" />`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const palette in data.palettes) {
|
||||
themeStylesheet.insertAdjacentHTML(
|
||||
'beforebegin',
|
||||
`<link rel="preload" as="style" href="/dist/styles/color/${palette}.css" onload="this.rel = 'stylesheet'" />`,
|
||||
);
|
||||
}
|
||||
|
||||
function setDefault(select, value) {
|
||||
let oldDefaultOption = select.querySelector(`wa-option[value=""]:not([data-id="${value}"])`);
|
||||
let newDefaultOption = select.querySelector(`wa-option[value="${value}"]`);
|
||||
|
||||
if (oldDefaultOption) {
|
||||
oldDefaultOption.value = oldDefaultOption.dataset.id;
|
||||
}
|
||||
|
||||
if (newDefaultOption) {
|
||||
newDefaultOption.dataset.id ??= newDefaultOption.value;
|
||||
newDefaultOption.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function render(changedAspect) {
|
||||
if (!globalThis.demo) {
|
||||
return;
|
||||
}
|
||||
|
||||
let url = new URL(demo.src);
|
||||
|
||||
if (!changedAspect || changedAspect === 'colors') {
|
||||
// Update the default palette when the theme colors change to the default palette of that theme
|
||||
setDefault(selects.palette, data.defaultParams.palette);
|
||||
setDefault(selects.brand, data.defaultParams.brand);
|
||||
}
|
||||
|
||||
let brand = data.params.brand || data.defaultParams.brand;
|
||||
selects.brand.style.setProperty('--color', `var(--wa-color-${brand})`);
|
||||
|
||||
// Add current palette class and remove any other palette classes
|
||||
let paletteClass = `wa-palette-${computed.palette}`;
|
||||
selects.brand.className = selects.brand.className.replace(/\bwa-palette-[a-z]+\b/g, paletteClass);
|
||||
selects.brand.classList.add(paletteClass);
|
||||
|
||||
for (let aspect in data.params) {
|
||||
let value = data.params[aspect];
|
||||
selects[aspect].value = value;
|
||||
}
|
||||
|
||||
for (let key in data.params) {
|
||||
if (data.params[key]) {
|
||||
data.urlParams.set(key, data.params[key]);
|
||||
}
|
||||
}
|
||||
|
||||
// Update demo URL
|
||||
domChange(() => {
|
||||
url.search = data.urlParams;
|
||||
demo.src = url;
|
||||
return new Promise(resolve => (demo.onload = resolve));
|
||||
});
|
||||
|
||||
// Update page URL
|
||||
data.urlParams.updateLocation();
|
||||
|
||||
// Update code snippets
|
||||
for (let language in codeSnippets) {
|
||||
let codeSnippet = codeSnippets[language];
|
||||
let code = getThemeCode(data.baseTheme, data.params, { language, cdnUrl });
|
||||
codeSnippet.textContent = code;
|
||||
Prism.highlightElement(codeSnippet);
|
||||
}
|
||||
}
|
||||
|
||||
addEventListener('turbo:render', event => {
|
||||
remixApp = init();
|
||||
});
|
||||
18
docs/docs/themes/showcase.css
vendored
18
docs/docs/themes/showcase.css
vendored
@@ -12,25 +12,9 @@ body,
|
||||
#mix_and_match {
|
||||
font-weight: var(--wa-font-weight-semibold);
|
||||
color: var(--wa-color-text-quiet);
|
||||
margin-block-end: var(--wa-space-xs);
|
||||
|
||||
html:not(.is-remixed) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
wa-icon {
|
||||
vertical-align: -0.15em;
|
||||
}
|
||||
|
||||
> strong {
|
||||
margin-inline-end: var(--wa-space-2xs);
|
||||
}
|
||||
|
||||
wa-badge {
|
||||
> a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1
docs/docs/themes/tailspin.md
vendored
1
docs/docs/themes/tailspin.md
vendored
@@ -4,5 +4,4 @@ description: Like a bird in flight, guiding you from there to here.
|
||||
isPro: true
|
||||
tags: pro
|
||||
palette: vogue
|
||||
brand: indigo
|
||||
---
|
||||
|
||||
6
docs/docs/themes/themes.json
vendored
6
docs/docs/themes/themes.json
vendored
@@ -1,9 +1,5 @@
|
||||
{
|
||||
"layout": "theme.njk",
|
||||
"wide": true,
|
||||
"tags": ["themes", "theme"],
|
||||
"brand": "blue",
|
||||
"eleventyComputed": {
|
||||
"file": "styles/themes/{{ page.fileSlug }}.css"
|
||||
}
|
||||
"tags": ["themes", "theme"]
|
||||
}
|
||||
|
||||
@@ -3,5 +3,4 @@ title: Design Tokens
|
||||
description: A theme is a collection of predefined CSS custom properties that control global styles from color to shadows. These custom properties thread through all Web Awesome components for a consistent look and feel.
|
||||
layout: overview
|
||||
override:tags: []
|
||||
categories: {tags: true}
|
||||
---
|
||||
|
||||
@@ -4,6 +4,8 @@ description: Build better with Web Awesome, the open source library of web compo
|
||||
layout: page
|
||||
---
|
||||
|
||||
|
||||
|
||||
<style>
|
||||
.title,
|
||||
.anchor-heading a,
|
||||
@@ -385,4 +387,4 @@ layout: page
|
||||
© Fonticons, Inc.
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@shoelace-style/webawesome",
|
||||
"version": "3.0.0-alpha.11",
|
||||
"version": "3.0.0-alpha.10",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@shoelace-style/webawesome",
|
||||
"version": "3.0.0-alpha.11",
|
||||
"version": "3.0.0-alpha.10",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ctrl/tinycolor": "^4.1.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@shoelace-style/webawesome",
|
||||
"description": "A forward-thinking library of web components.",
|
||||
"version": "3.0.0-alpha.11",
|
||||
"version": "3.0.0-alpha.10",
|
||||
"homepage": "https://webawesome.com/",
|
||||
"author": "Web Awesome",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -4,7 +4,6 @@ import { execSync } from 'child_process';
|
||||
import { deleteAsync } from 'del';
|
||||
import esbuild from 'esbuild';
|
||||
import { replace } from 'esbuild-plugin-replace';
|
||||
|
||||
import { mkdir, readFile } from 'fs/promises';
|
||||
import getPort, { portNumbers } from 'get-port';
|
||||
import { globby } from 'globby';
|
||||
@@ -267,13 +266,6 @@ async function regenerateBundle() {
|
||||
* Generates the documentation site.
|
||||
*/
|
||||
async function generateDocs() {
|
||||
/**
|
||||
* Used by the webawesome-app to skip doc generation since it will do its own.
|
||||
*/
|
||||
if (process.env.SKIP_ELEVENTY === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
spinner.start('Writing the docs');
|
||||
|
||||
const args = [];
|
||||
|
||||
@@ -52,7 +52,6 @@ import styles from './button.css';
|
||||
@customElement('wa-button')
|
||||
export default class WaButton extends WebAwesomeFormAssociatedElement {
|
||||
static shadowStyle = [variantStyles, appearanceStyles, sizeStyles, nativeStyles, styles];
|
||||
static rectProxy = 'button';
|
||||
|
||||
static get validators() {
|
||||
return [...super.validators, MirrorValidator()];
|
||||
@@ -109,7 +108,7 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
|
||||
@property({ reflect: true }) value: string | null = null;
|
||||
|
||||
/** When set, the underlying button will be rendered as an `<a>` with this `href` instead of a `<button>`. */
|
||||
@property({ reflect: true }) href = null;
|
||||
@property() href = '';
|
||||
|
||||
/** Tells the browser where to open the link. Only used when `href` is present. */
|
||||
@property() target: '_blank' | '_parent' | '_self' | '_top';
|
||||
@@ -225,6 +224,17 @@ export default class WaButton extends WebAwesomeFormAssociatedElement {
|
||||
this.button.blur();
|
||||
}
|
||||
|
||||
getBoundingClientRect(): DOMRect {
|
||||
let rect = super.getBoundingClientRect();
|
||||
let buttonRect = this.button.getBoundingClientRect();
|
||||
|
||||
if (rect.width === 0 && buttonRect.width > 0) {
|
||||
return buttonRect;
|
||||
}
|
||||
|
||||
return rect;
|
||||
}
|
||||
|
||||
render() {
|
||||
const isLink = this.isLink();
|
||||
const tag = isLink ? literal`a` : literal`button`;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
--spacing: var(--wa-space);
|
||||
--border-width: var(--wa-panel-border-width);
|
||||
--border-color: var(--wa-color-surface-border);
|
||||
--border-radius: var(--wa-panel-border-radius);
|
||||
|
||||
--inner-border-radius: calc(var(--border-radius) - var(--border-width));
|
||||
@@ -13,7 +12,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--wa-color-surface-default);
|
||||
border-color: var(--border-color);
|
||||
border-color: var(--wa-color-surface-border);
|
||||
border-radius: var(--border-radius);
|
||||
border-style: var(--wa-panel-border-style);
|
||||
box-shadow: var(--wa-shadow-s);
|
||||
@@ -21,20 +20,12 @@
|
||||
color: var(--wa-color-text-normal);
|
||||
}
|
||||
|
||||
/* Take care of top and bottom radii */
|
||||
.image,
|
||||
:host(:not([with-image])) .header,
|
||||
:host(:not([with-image], [with-header])) .body {
|
||||
:host(:not([with-image])) .header {
|
||||
border-start-start-radius: var(--inner-border-radius);
|
||||
border-start-end-radius: var(--inner-border-radius);
|
||||
}
|
||||
|
||||
:host(:not([with-footer])) .body,
|
||||
.footer {
|
||||
border-end-start-radius: var(--inner-border-radius);
|
||||
border-end-end-radius: var(--inner-border-radius);
|
||||
}
|
||||
|
||||
.image {
|
||||
display: flex;
|
||||
|
||||
@@ -48,9 +39,7 @@
|
||||
|
||||
.header {
|
||||
display: block;
|
||||
border-block-end-style: inherit;
|
||||
border-block-end-color: var(--border-color);
|
||||
border-block-end-width: var(--border-width);
|
||||
border-bottom: inherit;
|
||||
padding: calc(var(--spacing) / 2) var(--spacing);
|
||||
}
|
||||
|
||||
@@ -61,9 +50,9 @@
|
||||
|
||||
.footer {
|
||||
display: block;
|
||||
border-block-start-style: inherit;
|
||||
border-block-start-color: var(--border-color);
|
||||
border-block-start-width: var(--border-width);
|
||||
border-top: inherit;
|
||||
border-end-start-radius: var(--inner-border-radius);
|
||||
border-end-end-radius: var(--inner-border-radius);
|
||||
padding: var(--spacing);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,9 @@ import styles from './card.css';
|
||||
* @csspart body - The container that wraps the card's main content.
|
||||
* @csspart footer - The container that wraps the card's footer.
|
||||
*
|
||||
* @cssproperty [--border-radius=var(--wa-panel-border-radius)] - The radius for the card's corners. Expects a single value.
|
||||
* @cssproperty [--border-color=var(--wa-color-surface-border)] - The color of the card's borders, including inner borders. Expects a single value.
|
||||
* @cssproperty [--border-width=var(--wa-panel-border-width)] - The width of the card's borders. Expects a single value.
|
||||
* @cssproperty [--spacing=var(--wa-space)] - The amount of space around and between sections of the card. Expects a single value.
|
||||
* @cssproperty --border-radius - The radius for the card's corners. Expects a single value. Defaults to `var(--wa-panel-border-radius)`.
|
||||
* @cssproperty --border-width - The width of the card's borders. Expects a single value. Defaults to `var(--wa-panel-border-width)`.
|
||||
* @cssproperty --spacing - The amount of space around and between sections of the card. Expects a single value. Defaults to `var(--wa-space)`.
|
||||
*/
|
||||
@customElement('wa-card')
|
||||
export default class WaCard extends WebAwesomeElement {
|
||||
|
||||
@@ -127,7 +127,7 @@ export default class WaCheckbox extends WebAwesomeFormAssociatedElement {
|
||||
@property({ type: Boolean, reflect: true }) required = false;
|
||||
|
||||
/** The checkbox's hint. If you need to display HTML, use the `hint` slot instead. */
|
||||
@property() hint = '';
|
||||
@property({ attribute: 'hint' }) hint = '';
|
||||
|
||||
private handleClick() {
|
||||
this.hasInteracted = true;
|
||||
|
||||
@@ -278,11 +278,11 @@
|
||||
}
|
||||
|
||||
/*
|
||||
* Color dropdown
|
||||
*/
|
||||
* Color dropdown
|
||||
*/
|
||||
|
||||
.color-dropdown {
|
||||
display: contents;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.color-dropdown::part(panel) {
|
||||
|
||||
@@ -55,6 +55,12 @@ details {
|
||||
padding-block-start: var(--spacing);
|
||||
}
|
||||
|
||||
.show {
|
||||
}
|
||||
|
||||
.hide {
|
||||
}
|
||||
|
||||
@keyframes show {
|
||||
from {
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user