diff --git a/_includes/feature-table.html b/_includes/feature-table.html
new file mode 100644
index 0000000..c18a95c
--- /dev/null
+++ b/_includes/feature-table.html
@@ -0,0 +1,198 @@
+
+
+
+
+
+The table below aims to track implemented features in popular engines and tools.
+You can click on a cell for more information.
+
+Please enable JavaScript for the table to load.
+
+
+ Loading table, please wait…
+ (report issues )
+
+
+
+
+
+ Currently showing categories:
+
+
+
+
+
+
+
+
+
+
+
+ Full Support
+
+ Full Support
+
+
+
+
+
+
+
+ No Support
+
+ No Support
+
+
+
+
+
+
+
+
+
+
+ Experimental Support
+
+ Experimental Support
+
+
+
+
+ Not Applicable
+
+ Not Applicable
+
+
+
+
+
+
+
+ Unknown
+
+ Unknown
+
+
+
+
+ footnote
+
+ More
+
+
+
+
+
+ Loading
+
+ Loading
+
+
+
diff --git a/_includes/header.html b/_includes/header.html
index a7ead2a..af931ed 100644
--- a/_includes/header.html
+++ b/_includes/header.html
@@ -16,8 +16,11 @@
-
+
+ {% if page.url == "/features/" %}
+
+ {% endif %}
diff --git a/css/custom.css b/css/custom.css
index 6178555..7818ed7 100644
--- a/css/custom.css
+++ b/css/custom.css
@@ -25,7 +25,7 @@ audio:not([controls]) {
height: 0;
}
[hidden] {
- display: none;
+ display: none !important;
}
html {
font-size: 100%;
@@ -2447,8 +2447,6 @@ dl dd {
}
table {
- /* https://bugzilla.mozilla.org/show_bug.cgi?id=1005271 */
- /* display: block; */
width: 100%;
overflow: auto;
}
@@ -2522,200 +2520,16 @@ pre code::after {
content: normal;
}
-#feature-support-scrollbox {
- width: min-content;
- max-width: 95vw;
- margin: 0 50%;
- margin-bottom: 2em;
- transform: translateX(-50%);
- overflow-x: auto;
-}
-
-#feature-support-scrollbox th[scope='row'] {
- position: sticky;
- left: -1px;
- background-color: inherit;
- z-index: 1;
-}
-
-#feature-support-scrollbox a::after {
- /* Hide external link symbols on the table, since they are all external. */
- display: none;
-}
-
-@media (min-width: 1400px) {
- #feature-support {
- white-space: nowrap;
- }
-}
-
-#feature-support {
- cursor: default;
-}
-
-#feature-support > caption {
- text-align: left;
-}
-
-#feature-support sup {
- padding-left: 1pt;
-}
-
-#feature-support tr > * {
- text-align: center;
-}
-
-#feature-support tr:first-child > th {
- vertical-align: bottom;
- white-space: normal;
-}
-
-#feature-support .img-container {
- width: 32px;
- height: 32px;
-}
-
-#feature-support td {
- position: relative; /* for tooltip */
-}
-
-#feature-support td:hover,
-#feature-support td:focus,
-#feature-support td:focus-within {
- background: rgba(0, 0, 0, 0.04);
-}
-
-#feature-support th img {
- max-height: 32px;
-}
-
-.feature-cell {
- position: relative;
- height: 24px; /* height of the icon inside */
- line-height: 24px;
-}
-
-.feature-cell > sup {
- font-size: 0.7em;
- position: absolute;
- top: 0.2em;
-}
-
-.feature-cell > svg {
- width: 24px;
- height: 24px;
-}
-
-.feature-cell.icon-yes {
- color: #1b5e20;
-}
-
-.feature-cell.icon-yes > svg .svg-stroke {
- fill: #1b5e20;
-}
-
-.feature-cell.icon-no {
- color: #a96e8e;
-}
-
-.feature-cell.icon-no > svg .svg-stroke {
- fill: #a96e8e;
-}
-
-.feature-cell.icon-flag {
- color: #575581;
-}
-
-.feature-cell.icon-flag > svg .svg-stroke {
- fill: #575581;
-}
-
-.feature-cell.icon-na {
- color: #78909c;
-}
-
-.feature-cell.icon-unknown > svg .svg-stroke {
- fill: #78909c;
-}
-
-#feature-support-scrollbox + ol {
- list-style: lower-alpha;
- font-size: 0.7em;
- margin: 0 0 1em 0;
- columns: 32em auto;
- column-gap: 2em;
-}
-
-#feature-support-scrollbox + ol > li {
- transition: background-color 0.08s ease-in-out;
-}
-
-#feature-support-scrollbox + ol .ref-highlight {
- background: #eceff1;
-}
-
-.feature-tooltip {
- text-align: left;
- text-align: start;
- white-space: normal;
- color: #000;
- background: #fefefe;
- font-size: 0.8em;
- border-radius: 2px;
- outline: none;
-
- top: 0;
- left: 0;
- z-index: 1;
- max-width: 16em;
- width: max-content;
- height: max-content;
- padding: 12px;
-}
-
-/* Only apply transition after the initial position was set */
-.feature-tooltip[data-placement] {
- transition: transform 0.2s ease-in-out;
-}
-
-.feature-tooltip,
-.feature-tooltip-arrow {
+.visually-hidden {
+ /* Visually hidden */
position: absolute;
- contain: layout style;
- --shadow-size: 3px;
- box-shadow: 0 0 var(--shadow-size) rgba(0, 0, 0, 0.3);
-}
-
-.feature-tooltip-arrow {
- --arrow-size: 8px;
- background: inherit;
- width: var(--arrow-size);
- height: var(--arrow-size);
-
- --c0: calc(var(--shadow-size) * -1);
- --c1: calc(100% + var(--shadow-size));
- clip-path: polygon(
- var(--c0) var(--c1),
- var(--c0) var(--c0),
- var(--c1) var(--c0)
- );
-}
-
-[data-placement='top'] > .feature-tooltip-arrow {
- bottom: 0;
- transform: translateY(50%) rotate(-135deg);
-}
-[data-placement='bottom'] > .feature-tooltip-arrow {
- top: 0;
- transform: translateY(-50%) rotate(45deg);
-}
-[data-placement='left'] > .feature-tooltip-arrow {
- right: 0;
- transform: translateX(50%) rotate(135deg);
-}
-[data-placement='right'] > .feature-tooltip-arrow {
- left: 0;
- transform: translateX(-50%) rotate(-45deg);
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ border: none;
+ white-space: nowrap;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
}
dark-mode-toggle {
diff --git a/css/dark.css b/css/dark.css
index f8a5218..4609b4d 100644
--- a/css/dark.css
+++ b/css/dark.css
@@ -1,13 +1,26 @@
:root {
color-scheme: dark;
+
+ --color-fg: #e5e5e5;
+ --color-bg: #1e1e1e;
+ --color-bg-highlight: rgb(255 255 255 / 4%);
+ --color-bg-secondary: #202020;
+ --color-link: #7cb1e2;
+ --color-link-visited: #ab8fee;
+ --color-border: #404549;
+ --color-border-primary: #2a4872;
+
+ color: var(--color-fg);
+ background-color: var(--color-bg);
}
+.invert-in-dark-theme,
:not(.flash) > a[href^='http']::after {
- filter: invert(1);
+ filter: invert(0.8);
}
.flash.flash-warn {
- color: CanvasText;
+ color: var(--color-fg);
background-color: #555;
}
@@ -20,30 +33,45 @@
}
.lead {
- color: #999;
+ color: #c7c7c7;
}
blockquote {
- color: #999;
+ color: #c7c7c7;
}
h6 {
- color: #999;
+ color: #c7c7c7;
}
-table th,
-table td {
- border: 1px solid #222;
+.text-secondary {
+ color: rgb(255 255 255 / 75%);
}
-table tr {
- border-top: 1px solid #333;
+#feature-table table {
+ --color-bg: #202020;
+ --color-status-dim: #444c50;
+ background-color: var(--color-bg);
}
-table tr:nth-child(2n) {
- background-color: #303030;
+#feature-table :is(.cell, .details-status) > * {
+ filter: brightness(2);
}
-.feature-cell {
- filter: brightness(2);
+#feature-table :is(th, .cell) {
+ border-left: none;
+}
+
+.status-yes {
+ color: #305934;
+ fill: #3d4e3e;
+}
+
+.status-no {
+ color: #785562;
+}
+
+.status-experimental {
+ color: #3e5997;
+ fill: #3f63b7;
}
diff --git a/css/feature-table.css b/css/feature-table.css
new file mode 100644
index 0000000..7787042
--- /dev/null
+++ b/css/feature-table.css
@@ -0,0 +1,429 @@
+[x-cloak] {
+ display: none !important;
+}
+
+@keyframes appear-suddenly {
+ 0% {
+ opacity: 0;
+ }
+ 95% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+#feature-table-loading {
+ animation: 1s linear forwards appear-suddenly;
+}
+
+#feature-table {
+ margin-bottom: 0.75rem;
+}
+
+#feature-table template {
+ /* Prevent templates from affecting layout. */
+ display: none !important;
+}
+
+#feature-table a {
+ text-decoration: none;
+ color: var(--color-link);
+}
+
+#feature-table a:visited {
+ color: var(--color-link-visited);
+}
+
+#feature-table a:hover {
+ text-decoration: underline;
+}
+
+#feature-table table a::after,
+#feature-table-loading a::after {
+ /* Hide external link symbols in the table, since they are all external. */
+ display: none;
+}
+
+#feature-table svg {
+ width: auto; /* Required for Safari */
+ height: 100%;
+}
+
+#feature-table fieldset {
+ border: none;
+ padding: 0;
+ margin: 0;
+}
+
+#table-actions {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ align-items: end;
+ gap: 1.5rem;
+ row-gap: 0.75rem;
+ font-size: .9rem;
+}
+
+#table-actions legend {
+ font-weight: 600;
+}
+
+#platform-filters {
+ display: flex; /* Flexbox does not work with due to a bug */
+ flex-direction: column;
+ gap: 0.2rem;
+ margin-top: 0.2rem;
+}
+
+.platform-filter {
+ flex: 1 1 auto;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ user-select: none;
+}
+
+.platform-filter > * {
+ cursor: pointer;
+}
+
+.platform-filter > input {
+ margin-top: 2px; /* Align checkbox with label */
+}
+
+/* Style as button groups on desktop. Threshold should be revised when categories change. */
+@media (min-width: 50rem) {
+ #table-actions legend {
+ /* Visually hidden */
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ border: none;
+ white-space: nowrap;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ }
+
+ #platform-filters {
+ flex-direction: row;
+ gap: 0;
+ max-width: 95vw;
+ overflow: auto visible;
+ background-color: var(--color-bg-secondary);
+ }
+
+ .platform-filter {
+ font-size: 0.95rem;
+ padding: 0.3rem 0.75rem;
+ padding-inline-start: 0.6rem;
+ gap: 0.6rem;
+ min-width: max-content;
+
+ --border: 1px solid var(--color-border);
+ border: var(--border);
+ }
+
+ .platform-filter:first-of-type {
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+ }
+
+ .platform-filter:last-of-type {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+ }
+
+ .platform-filter > input {
+ transform: scale(1.05);
+ }
+
+ .platform-filter:has(input:checked) {
+ --color-border: var(--color-border-primary);
+ }
+
+ /* Combo 1: checked|R| + |L|checked => hide |L| border */
+ /* Combo 2: any|R| + |L|unchecked => hide |L| border */
+ .platform-filter:has(input:checked) + .platform-filter:has(input:checked),
+ .platform-filter + .platform-filter:has(input:not(:checked)) {
+ border-left: none;
+ }
+
+ /* Combo 3: unchecked|R| + |L|checked => hide |R| border */
+ .platform-filter:has(input:not(:checked)):has(
+ + .platform-filter > input:checked
+ ) {
+ border-right: none;
+ }
+
+ .platform-filter:has(input:focus-visible) {
+ box-shadow: inset 0 0 0 0.25rem rgb(57 177 255 / 50%);
+ }
+}
+
+#feature-scroll {
+ width: max-content;
+ min-width: 100%;
+ max-width: 95vw;
+ margin: 0 50%;
+ margin-top: 0.85rem;
+ transform: translateX(-50%);
+ overflow-x: auto;
+}
+
+table {
+ width: min-content;
+ min-width: 100%;
+ margin: 0;
+ overflow: initial; /* Required for sticky headers */
+
+ display: grid;
+ grid-auto-flow: dense;
+ justify-content: center;
+ align-items: stretch;
+
+ --feature-column-min-width: 12rem;
+ grid-template-columns: minmax(var(--feature-column-min-width), 2fr) repeat(
+ calc(var(--num-columns) - 1),
+ 1fr
+ );
+
+ --border: 1px solid var(--color-border);
+ border: var(--border);
+ border-radius: 4px;
+ --cell-padding-block: 8px;
+ --cell-padding-inline: 10px;
+ --color-status-dim: #78909c;
+
+ font-size: 0.9rem;
+ line-height: 1;
+}
+
+thead,
+tbody,
+tr {
+ display: contents;
+}
+
+table th {
+ font-weight: 600;
+ padding: 8px 12px;
+}
+
+th,
+.cell {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ padding: var(--cell-padding-block) var(--cell-padding-inline);
+}
+
+.platform-header-row th {
+ display: grid;
+ grid-template-rows: subgrid;
+ grid-row: span 2;
+ justify-content: stretch;
+}
+
+.platform-header-row th > * {
+ display: inline-grid;
+ justify-items: center;
+ row-gap: 2px;
+ align-self: end;
+
+ /* Applies to cells without logo (e.g. Your browser) */
+ grid-row: span 2;
+}
+
+@supports not (grid-template-columns: subgrid) {
+ .platform-header-row th > * {
+ align-self: start;
+ }
+}
+
+.platform-header-row th > :has(img) {
+ /* Align logo and name using parent grid lines. */
+ grid-template-rows: inherit;
+ grid-row: inherit;
+}
+
+.platform-header-row th > :has(img) .platform-name {
+ white-space: nowrap;
+}
+
+.platform-header-row .rounded {
+ border-radius: 3px;
+}
+
+thead .category {
+ grid-column: span var(--num-columns-in-category);
+ font-size: 0.8rem;
+ opacity: 0.85;
+}
+
+tbody tr:first-of-type th {
+ grid-column: 1 / -1;
+}
+
+th[scope='row'] {
+ text-wrap: balance;
+ line-height: 1.4;
+ position: sticky;
+ left: -1px;
+ z-index: 1;
+ background-color: var(--color-bg);
+}
+
+th img {
+ max-width: 100%;
+ max-height: 1.65rem;
+ height: auto;
+ margin: 2px 0 4px;
+}
+
+.cell {
+ -webkit-appearance: none;
+ appearance: none;
+ background: none;
+ border: none;
+ justify-content: start;
+ gap: 5px;
+ touch-action: manipulation;
+}
+
+.cell .icon {
+ width: 1.25rem;
+ height: 1.25rem;
+}
+
+button.cell {
+ cursor: pointer;
+ border-bottom: var(--border);
+ border-bottom-width: 2px;
+ border-bottom-color: transparent; /* Prevent border from shifting cell height */
+}
+
+button.cell:hover {
+ background: var(--color-bg-highlight);
+}
+
+button.cell[aria-expanded='true'] {
+ border-bottom-color: var(--color-border);
+}
+
+td {
+ display: contents;
+}
+
+table > :first-child tr:not(:first-child) :is(th, .cell),
+table > :not(:first-child) tr :is(th, .cell),
+.details {
+ border-top: var(--border);
+}
+
+tr > th:not(:first-child),
+tr > :not(:first-child) .cell {
+ border-left: var(--border);
+}
+
+.text-secondary {
+ font-weight: normal;
+ color: rgb(0 0 0 / 75%);
+}
+
+.status-yes {
+ color: #0c3d10;
+ fill: #5c6b5d;
+}
+
+.status-no {
+ color: #83264a;
+ fill: #86556f;
+}
+
+.status-experimental {
+ color: #143a93;
+ fill: #3f63b7;
+}
+
+.status-not-applicable,
+.status-unknown,
+.details-note svg {
+ color: #52656f;
+ fill: var(--color-status-dim);
+}
+
+:is(.status-unknown, .status-not-applicable, .status-experimental) svg {
+ padding: 1.5px;
+}
+
+.icon-note svg {
+ padding: 1.5px;
+ fill: var(--color-status-dim);
+}
+
+.details {
+ grid-column: 1 / -1;
+ display: grid;
+ grid-template-columns: subgrid;
+}
+
+@supports not (grid-template-columns: subgrid) {
+ .details {
+ grid-template-columns: var(--feature-column-min-width, 12em) 1fr;
+ }
+}
+
+.details-inner {
+ grid-column: 2 / -1;
+ max-width: 95vw;
+ margin: 0;
+ padding: calc(var(--cell-padding-block) * 2.5) var(--cell-padding-inline);
+ display: grid;
+ grid-template-columns: max-content 1fr;
+ align-items: center;
+ row-gap: var(--cell-padding-inline);
+ column-gap: calc(var(--cell-padding-inline) * 0.85);
+ line-height: 1.35;
+}
+
+@media (width < 1000px) {
+ .details-inner {
+ /* Flush left on smaller screen */
+ grid-column: 1 / -1;
+ position: sticky;
+ left: -1px;
+ z-index: 1;
+ }
+}
+
+.details-inner > * {
+ display: contents;
+}
+
+.details-inner .icon {
+ height: 1.2rem;
+ height: 1lh;
+}
+
+.details-inner svg {
+ padding: 2px 0;
+}
+
+.details-status {
+ font-weight: 600;
+}
+
+.details-note svg {
+ opacity: 0.6;
+}
+
+.details-note-line {
+ grid-column: 2 / 2;
+ max-width: 90%;
+}
diff --git a/css/light.css b/css/light.css
index cded12b..7700546 100644
--- a/css/light.css
+++ b/css/light.css
@@ -1,5 +1,17 @@
:root {
color-scheme: light;
+
+ --color-fg: #151515;
+ --color-bg: #fefefe;
+ --color-bg-highlight: rgb(0 0 0 / 5%);
+ --color-bg-secondary: rgb(0 0 0 / 1.25%);
+ --color-link: #1751a7;
+ --color-link-visited: #5817a7;
+ --color-border: rgb(0 0 0 / 15%);
+ --color-border-primary: #7facec;
+
+ color: var(--color-fg);
+ background-color: var(--color-bg);
}
.flash.flash-warn {
@@ -12,7 +24,7 @@
}
.lead {
- color: #555;
+ color: #3a3a3a;
}
blockquote {
@@ -23,15 +35,6 @@ h6 {
color: #777;
}
-table th,
-table td {
- border: 1px solid #ddd;
-}
-
-table tr {
- border-top: 1px solid #ccc;
-}
-
-table tr:nth-child(2n) {
- background-color: #f8f8f8;
+#feature-table .top-corner {
+ border-top: none !important;
}
diff --git a/features.js b/features.js
index 5184672..516b3a0 100644
--- a/features.js
+++ b/features.js
@@ -1,484 +1,525 @@
-(async () => {
- 'use strict';
+'use strict';
+
+/*! groupby-polyfill. MIT License. Jimmy Wärting */
+/**
+ * Groups elements from an iterable into an object based on a callback function.
+ *
+ * @template T, K
+ * @param {Iterable} iterable - The iterable to group.
+ * @param {function(T, number): K} callbackfn - The callback function to
+ * determine the grouping key.
+ * @returns {Object.} An object where keys are the grouping keys
+ * and values are arrays of grouped elements.
+ *
+ * This was introduced because of https://github.com/GoogleChromeLabs/wasm-feature-detect/issues/82.
+ */
+Object.groupBy ??= function groupBy(iterable, callbackfn) {
+ const obj = Object.create(null);
+ let i = 0;
+ for (const value of iterable) {
+ const key = callbackfn(value, i++);
+ key in obj ? obj[key].push(value) : (obj[key] = [value]);
+ }
+ return obj;
+};
+
+/**
+ * `Array.map` but for object values.
+ *
+ * @template {object} T
+ * @template R
+ * @param {T} obj
+ * @param {(value: T[keyof T], key: keyof T) => R} mapper
+ * @returns {{ [K in keyof T]: R }}
+ */
+function mapValues(obj, mapper) {
+ return Object.fromEntries(
+ Object.entries(obj).map(([key, value]) => [key, mapper(value, key)])
+ );
+}
+
+/**
+ * Break a string into three parts using the given delimiter.
+ * @param {string} str
+ * @param {string} delim
+ * @returns {[string, string, string]}
+ */
+function splitParts(str, delim) {
+ const start = str.indexOf(delim);
+ const end = str.indexOf(delim, start + 1);
+ if (start >= 0 && end > start) {
+ const head = str.substring(0, start);
+ const body = str.substring(start + 1, end);
+ const tail = str.substring(end + 1);
+ return [head, body, tail];
+ }
+ return [str, '', ''];
+}
+
+function loadFeatureDetection() {
+ // Please cache bust by bumping the `v` parameter whenever `feature.json` is
+ // updated to depend on a new version of the library. See #353 for discussion.
+ // Make sure to also match the preload link in `feature-table.html`.
+ const module =
+ import('https://unpkg.com/wasm-feature-detect@1/dist/esm/index.js?v=1');
+ return (featureName) =>
+ module.then((wasmFeatureDetect) => wasmFeatureDetect[featureName]());
+}
+
+const container = document.getElementById('feature-table');
+
+/**
+ * @typedef {{
+ * type: 'yes' | 'no' | 'not-applicable' | 'experimental' | 'unknown';
+ * version?: string;
+ * note?: string;
+ * expanded?: boolean;
+ * }} DecodedStatus
+ */
+/** @typedef {null | boolean | 'flag' | string} RawState */ /**
+ * @param {RawState | [RawState, string]} status
+ */
+function decodeSupportStatus(status) {
+ // Meaning of each entry:
+ // * null => not applicable to this browser
+ // * true/false => supported/unsupported
+ // * "version" => supported since "version"
+ // * "flag" => flag required (must be lowercase)
+ // * [true, "footnotes"] => supported, with "footnotes"
+ // * ["version", "footnotes"] => supported since "version", with "footnotes"
+ // …and any combination thereof
+
+ /** @type {RawState} */
+ let state, note;
+ if (Array.isArray(status)) {
+ if (status.length !== 2) throw new TypeError();
+ [state, note] = status;
+ } else {
+ state = status;
+ }
- function h(name, props = {}, children = []) {
- const node = Object.assign(document.createElement(name), props);
- node.append(...children);
- return node;
+ /** @type {DecodedStatus['type']} */
+ let type, version;
+ if (typeof state === 'string') {
+ type = state === 'flag' ? 'experimental' : 'yes';
+ version = state !== 'flag' ? state : undefined;
+ } else if (!state) {
+ type = state === null ? 'not-applicable' : 'no';
+ } else {
+ if (state !== true)
+ throw new TypeError(
+ `unexpected supported status ${JSON.stringify(state)}`
+ );
+ type = 'yes';
}
- // Convert number to lowercase hexavigesimal like "a, b, c, .., x, y, z, aa, ab, ..", starting from zero.
- // This is the same format as CSS `list-style: lower-alpha`, which is used for our footnote lists.
- function toAlphabet(num) {
- const digit = num % 26,
- char = String.fromCharCode(97 + digit),
- rem = num - digit;
- return rem ? toAlphabet(Math.floor(rem / 26) - 1) + char : char;
+ return { type, version, note, expanded: false };
+}
+
+/** @typedef {{ name: string, queryKey: string, default?: boolean }} Category */
+
+/** @param {Category[]} allCategories */
+function loadSelectedCategories(allCategories) {
+ const names = new URLSearchParams(location.search)
+ .getAll('categories')
+ .flatMap((values) => values.split(','))
+ .flatMap((param) => {
+ const category = allCategories.find(({ queryKey }) => queryKey === param);
+ return category ? [category.name] : [];
+ });
+
+ return names.length
+ ? names
+ : allCategories
+ .filter((category) => category.default)
+ .map((category) => category.name);
+}
+
+/**
+ * @param {Category[]} allCategories
+ * @param {string[]} selected
+ */
+function saveSelectedCategories(allCategories, selected) {
+ const defaultSelection = allCategories
+ .filter((category) => category.default)
+ .map((category) => category.name);
+
+ if (
+ selected.length === defaultSelection.length &&
+ selected.every((name) => defaultSelection.includes(name))
+ ) {
+ selected = [];
}
- // Map names to HTML ids. For example, idMap['table-col']('Chrome') will return 'table-col-chrome'.
- // This is to satisfy the need for unique ids in `headers` attributes.
- // Hardcoded array makes it easier to find typos, since it would throw an error if the namespace is mistyped.
- const idMap = ['table-group', 'table-col', 'table-row'].reduce(
- (map, namespace) => {
- map[namespace] = (str) =>
- namespace + '-' + str.toLowerCase().replace(/[^\w\d-_]+/g, '-');
- return map;
- },
- {}
- );
+ // Keep the same order as in `allCategories`
+ const queryKeys = allCategories
+ .filter(({ name }) => selected.includes(name))
+ .map(({ queryKey }) => queryKey);
- // Get a copy of the requested SVG icon. Those are defined in the markdown as templates.
- function icon(key) {
- return document
- .getElementById(`support-symbol-${key}`)
- .content.firstElementChild.cloneNode(true);
+ const url = new URL(location.href);
+ if (queryKeys.length) {
+ url.searchParams.set('categories', queryKeys.join(','));
+ } else {
+ url.searchParams.delete('categories');
}
- const scrollbox = document.getElementById('feature-support-scrollbox');
- const table = document.getElementById('feature-support');
-
- const detectWasmFeature = _loadFeatureDetectModule();
- const addTooltip = _loadTooltipModule();
-
- const { features, browsers } = await fetch('/features.json', {
- credentials: 'include', // https://stackoverflow.com/a/63814972
- mode: 'no-cors',
- }).then((res) => res.json());
-
- const tBody = document.createElement('tbody');
- table.append(
- h('thead', {}, [
- h('tr', {}, [
- h('th', { id: 'table-blank' }),
- h('th', { scope: 'col', id: idMap['table-col']('Your browser') }, [
- 'Your browser',
- ]),
- ...Object.entries(browsers).map(([name, { url, logo }]) =>
- h('th', { scope: 'col', id: idMap['table-col'](name) }, [
- h('a', { href: url, target: '_blank' }, [
- // Empty alt trick: https://www.w3.org/WAI/WCAG22/Techniques/html/H2
- h('img', { src: logo, alt: '' }),
- h('br'),
- name,
- ]),
- ])
- ),
- ]),
- ]),
- tBody
- );
+ history.replaceState(null, '', url);
+}
+
+// TODO: think of a cleaner way to store icons
+const statusIcons = mapValues(
+ {
+ yes: 'icon-check',
+ no: 'icon-close',
+ 'not-applicable': 'icon-forbid-2',
+ experimental: 'icon-flask',
+ unknown: 'icon-question-mark',
+ asterisk: 'icon-asterisk',
+ more: 'icon-more',
+ loading: 'icon-loading',
+ },
+ (id) => /** @type {DocumentFragment} */ (document.getElementById(id).content)
+);
+
+const noteIcons = mapValues(
+ {
+ yes: 'icon-checkbox-circle',
+ no: 'icon-close-circle',
+ 'not-applicable': 'icon-forbid-2',
+ experimental: 'icon-flask',
+ unknown: 'icon-checkbox-blank-circle',
+ },
+ (id) => /** @type {DocumentFragment} */ (document.getElementById(id).content)
+);
+
+/**
+ * @typedef {{
+ * name: string;
+ * url: string;
+ * logo: string;
+ * logoClassName?: string;
+ * category: string;
+ * categories: string[];
+ * features: Record
+ * }} Platform
+ */
+
+const state = () => ({
+ /** @type {Platform[]} */
+ platforms: [],
+
+ /** @type {Record} */
+ yourBrowser: {},
+
+ /** @type {{ name: string; features: object[] }[]} */
+ featureGroups: [],
+
+ /** @type {Category[]} */
+ categories: [],
+
+ get categoryNames() {
+ return this.categories.map(({ name }) => name);
+ },
+
+ /** @type {string[]} */
+ selectedCategories: [],
+
+ async init() {
+ const {
+ features,
+ categories,
+ browsers: platforms,
+ } = await fetch('/features.json', {
+ credentials: 'include', // https://stackoverflow.com/a/63814972
+ mode: 'no-cors',
+ }).then((res) => res.json());
+
+ const categoriesInUse = new Set(
+ Object.values(platforms).flatMap(({ category }) => category)
+ );
- /*! groupby-polyfill. MIT License. Jimmy Wärting */
+ // Hide empty categories.
+ this.categories = categories.filter(({ name }) =>
+ categoriesInUse.has(name)
+ );
+ this.selectedCategories = loadSelectedCategories(categories);
+
+ this.platforms = Object.entries(platforms).map(
+ ([name, { category, ...platform }]) => {
+ // Determine the primary category.
+ let categories = [];
+ if (Array.isArray(category)) {
+ categories = category;
+ category = category[0];
+ }
- /**
- * Groups elements from an iterable into an object based on a callback function.
- *
- * @template T, K
- * @param {Iterable} iterable - The iterable to group.
- * @param {function(T, number): K} callbackfn - The callback function to
- * determine the grouping key.
- * @returns {Object.} An object where keys are the grouping keys
- * and values are arrays of grouped elements.
- *
- * This was introduced because of https://github.com/GoogleChromeLabs/wasm-feature-detect/issues/82.
- */
- Object.groupBy ??= function groupBy(iterable, callbackfn) {
- const obj = Object.create(null);
- let i = 0;
- for (const value of iterable) {
- const key = callbackfn(value, i++);
- key in obj ? obj[key].push(value) : (obj[key] = [value]);
- }
- return obj;
- };
-
- let featureGroups = Object.groupBy(
- Object.entries(features).map(([name, feature]) =>
- Object.assign(feature, { name })
- ),
- (f) => f.phase
- );
+ // Decode the compact status format for easier future processing.
+ const platformFeatures = mapValues(features, (_, featName) => {
+ const raw = platform.features[featName];
+ return typeof raw === 'undefined'
+ ? { type: 'no' } // Missing values default to 'no'
+ : decodeSupportStatus(raw);
+ });
- featureGroups = [
- {
- name: 'Phase 5 - The Feature is Standardized',
- features: featureGroups[5],
- },
- { name: 'Phase 4 - Standardize the Feature', features: featureGroups[4] },
- { name: 'Phase 3 - Implementation Phase', features: featureGroups[3] },
- {
- name: 'Phase 2 - Proposed Spec Text Available',
- features: featureGroups[2],
- },
- { name: 'Phase 1 - Feature Proposal', features: featureGroups[1] },
- { name: 'Inactive', features: featureGroups['inactive'] },
- ];
-
- // Collect all notes and assign an index to each unique item
- // { "First unique note": 0, "Second unique note": 1, ...}
- const notes = Object.values(browsers).flatMap((b) =>
- Object.values(b.features)
- .filter((s) => Array.isArray(s))
- .map((s) => s[1])
- );
- const note2index = new Map();
- let noteIndex = 0;
- for (const note of notes) {
- if (!note2index.has(note)) {
- note2index.set(note, noteIndex++);
- }
- }
+ return {
+ name,
+ ...platform,
+ category,
+ categories,
+ features: platformFeatures,
+ };
+ }
+ );
- // Generate the footnote list. They are later referenced in the actual table.
- const noteList = document.createElement('ol');
- // Place footnote list outside of the scolling area
- scrollbox.parentNode.insertBefore(noteList, scrollbox.nextSibling);
- for (const [note, index] of note2index) {
- const item = h('li', { id: `feature-note-${index}` });
- noteList.appendChild(item).appendChild(renderNote(note));
- }
+ let featureByGroup = Object.groupBy(
+ Object.entries(features).map(([id, feature]) =>
+ Object.assign(feature, { id })
+ ),
+ (f) => f.phase
+ );
- // Create an element that links to the specified footnote.
- // Also returns the HTML id of the footnote it refers to.
- function createNoteRef(index) {
- const id = `feature-note-${index}`;
- return [id, h('a', { href: `#${id}` }, [`[${toAlphabet(index)}]`])];
- }
+ this.featureGroups = [
+ {
+ name: 'Phase 5 – The Feature is Standardized',
+ features: featureByGroup[5],
+ },
+ {
+ name: 'Phase 4 – Standardize the Feature',
+ features: featureByGroup[4],
+ },
+ { name: 'Phase 3 – Implementation Phase', features: featureByGroup[3] },
+ {
+ name: 'Phase 2 – Proposed Spec Text Available',
+ features: featureByGroup[2],
+ },
+ { name: 'Phase 1 – Feature Proposal', features: featureByGroup[1] },
+ { name: 'Inactive', features: featureByGroup['inactive'] },
+ ];
+
+ const featureDetect = loadFeatureDetection();
+ for (const id of Object.keys(features)) {
+ featureDetect(id)
+ .then((supported) => {
+ this.yourBrowser[id] = {
+ type: supported ? 'yes' : 'no',
+ version: supported ? 'Yes' : undefined,
+ };
+ })
+ .catch(() => {
+ this.yourBrowser[id] = { type: 'unknown' };
+ });
+ }
- const columnCount = 2 + Object.keys(browsers).length;
+ document.getElementById('feature-table-loading')?.remove();
+ },
- for (const { name: groupName, features } of featureGroups) {
- if (!features) {
- continue;
+ onSelectedCategoryChange(value, oldValue) {
+ if (!value.length && this.categories.length) {
+ // Prevent user from deselecting all categories.
+ this.selectedCategories = this.categoryNames.filter(
+ (name) => !oldValue.includes(name)
+ );
}
- tBody.append(
- h('tr', {}, [
- h(
- 'th',
- {
- scope: 'colgroup',
- colSpan: columnCount,
- id: idMap['table-group'](groupName),
- headers: 'table-blank',
- // Chrome doesn't handle `headers` attribute correctly.
- // Just hide the group headers for now…
- // https://bugs.chromium.org/p/chromium/issues/detail?id=1081201
- //
- // Actually Firefox doesn't support `ariaHidden` attribute.
- // This is a happy coincidence, since `headers` works fine on Firefox anyway.
- ariaHidden: true,
- },
- [groupName]
- ),
- ])
- );
- for (const { name: featName, description, url } of features) {
- const detectResult = h(
- 'td',
+ saveSelectedCategories(this.categories, this.selectedCategories);
+ },
+
+ /**
+ * Returns the cells to be rendered in a specific feature row
+ * (or null for the header row), excluding the row header.
+ *
+ * @param {string | null} featureId
+ * @returns {(Omit, 'features'> & { name: string; category: string; status?: DecodedStatus | undefined; })[]}
+ */
+ cellsForRow(featureId) {
+ const selected = new Set(this.selectedCategories);
+ const cells = [
+ {
+ name: 'Your browser',
+ category: 'Web Browsers',
+ features: this.yourBrowser,
+ },
+ ...this.platforms,
+ ].flatMap(({ category, features, ...platform }) => {
+ if (!selected.has(category)) {
+ // Look for the next available option if the primary category is not selected.
+ category = platform.categories?.find((category) =>
+ selected.has(category)
+ );
+
+ // Skip the platform if none of its categories are selected,.
+ if (!category) return [];
+ }
+
+ return [
{
- headers: [
- idMap['table-col']('Your browser'),
- idMap['table-row'](featName),
- ].join(' '),
+ ...platform,
+ category,
+ status: featureId ? features[featureId] : undefined,
},
- [buildCellInner('loading')]
- );
+ ];
+ });
- detectWasmFeature(featName).then(
- (supported) => {
- detectResult.textContent = '';
- detectResult.appendChild(buildCellInner(supported ? 'yes' : 'no'));
- addTooltip(
- detectResult,
- supported ? '✓ Supported' : '✗ Not supported',
- [tBody, scrollbox]
- );
- },
- (_err) => {
- detectResult.textContent = '';
- detectResult.appendChild(buildCellInner('unknown'));
- addTooltip(detectResult, 'Detection unavailable for this feature', [
- tBody,
- scrollbox,
- ]);
- }
- );
-
- tBody.append(
- h('tr', {}, [
- h(
- 'th',
- {
- scope: 'row',
- id: idMap['table-row'](featName),
- headers: idMap['table-group'](groupName),
- },
- [h('a', { href: url, target: '_blank' }, [description])]
- ),
- detectResult,
- ...Object.entries(browsers).map(([browserName, { features }]) => {
- // Meaning of each entry:
- // * null => not applicable for this browser
- // * true/false => supported/unsupported
- // * "version" => supported since "version"
- // * "flag" => flag required (must be lowercase)
- // * [true, "footnotes"] => supported, with "footnotes"
- // * ["version", "footnotes"] => supported since "version", with "footnotes"
- // …and any combination thereof
-
- /** @type {null|boolean|string|[boolean|string,string]} */
- let support = features[featName];
- let box, note;
-
- // First extract the footnote part if it's an array
- if (Array.isArray(support)) {
- if (support.length !== 2) throw new TypeError();
- note = support[1];
- support = support[0];
- }
-
- if (typeof support === 'string') {
- if (support === 'flag') {
- // 'flag' is treated specially as the "requires flag" icon
- box = buildCellInner('flag');
- } else {
- // Otherwise it's a version number, like "95"
- box = buildCellInner('yes', support);
- note ||= `✓ Supported since version ${support}`;
- }
- } else if (!support) {
- if (support === null) {
- box = buildCellInner('na', 'N/A');
- note ||= '✗ Not applicable for this browser/engine';
- } else {
- box = buildCellInner('no');
- note ||= '✗ Not supported';
- }
- } else {
- if (support !== true) throw new TypeError();
- box = buildCellInner('yes');
- // Magic value, keep in sync with `renderNote`
- note ||= '✓ Supported, introduced in unknown version';
- }
-
- const cell = h(
- 'td',
- {
- headers: [
- idMap['table-col'](browserName),
- idMap['table-row'](featName),
- ].join(' '),
- },
- [box]
- );
+ const { categoryNames } = this;
+ return cells.sort(
+ (a, b) =>
+ categoryNames.indexOf(a.category) - categoryNames.indexOf(b.category)
+ );
+ },
- // Give the cell itself an `aria-lebel` to avoid screen readers calling it "empty cell".
- const icon = box.firstElementChild;
- if (icon?.hasAttribute('aria-label')) {
- cell.setAttribute('aria-label', icon.getAttribute('aria-label'));
- icon.removeAttribute('aria-label');
- }
-
- if (note && note2index.has(note)) {
- cell.tabIndex = 0; // focusable
- const index = note2index.get(note);
- const [noteId, refLink] = createNoteRef(index);
- box.appendChild(h('sup', {}, [refLink]));
-
- // Accommodate the width of elements, which are absolutely positioned
- box.style.paddingInline = `${toAlphabet(index).length}ch`;
-
- const noteItem = document.getElementById(noteId);
- if (noteItem) {
- cell.addEventListener('mouseenter', () =>
- noteItem.classList.add('ref-highlight')
- );
- cell.addEventListener('mouseleave', () =>
- noteItem.classList.remove('ref-highlight')
- );
- }
- }
-
- // Clip to both and the scrollbox.
- // the former is to avoid blocking out the headers;
- // the latter is to keep the tooltip inside the scrollable area
- addTooltip(cell, note, [tBody, scrollbox]);
- return cell;
- }),
- ])
- );
- tBody.lastElementChild.setAttribute(
- 'aria-describedby',
- idMap['table-row'](featName)
- );
+ /** The categories currently displayed and their number of columns. */
+ get cellGroupsForRow() {
+ return mapValues(
+ Object.groupBy(this.cellsForRow(null), ({ category }) => category),
+ (platforms) => platforms.length
+ );
+ },
+
+ get numColumns() {
+ return 1 + this.cellsForRow(null).length;
+ },
+
+ /** @param {DecodedStatus} selected */
+ toggleFeatureDetails(selected) {
+ if (selected.expanded) {
+ selected.expanded = false;
+ } else {
+ // Only one should be open at a time, close everything else first.
+ for (const platform of this.platforms)
+ for (const feat of Object.values(platform.features))
+ feat?.expanded && (feat.expanded = false);
+
+ for (const feat of Object.values(this.yourBrowser))
+ feat?.expanded && (feat.expanded = false);
+
+ selected.expanded = true;
}
- }
+ },
+
+ /** @param {DecodedStatus | undefined} status */
+ classForStatus(status) {
+ if (!status?.type) return null;
+ return `status-${status.type}`;
+ },
+
+ /** @param {DecodedStatus | undefined} status */
+ iconForStatus(status) {
+ if (!status?.type) return statusIcons['loading'];
+ return statusIcons[status.type];
+ },
+
+ get iconMoreDetails() {
+ return statusIcons['more'];
+ },
+
+ get iconNote() {
+ return statusIcons['asterisk'];
+ },
+
+ /** @param {DecodedStatus | undefined} status */
+ iconForNote(status) {
+ if (!status?.type) return noteIcons['unknown'];
+ return noteIcons[status.type] ?? noteIcons['unknown'];
+ },
+
+ /** @param {DecodedStatus | undefined} status */
+ labelForStatus(status) {
+ if (!status) return null;
+ if (status.version) return status.version;
+ switch (
+ status.type
+ // case 'no':
+ // return 'No';
+ // case 'not-applicable':
+ // return 'N/A';
+ ) {
+ }
+ return null;
+ },
- function buildCellInner(type, text) {
- const content = text || icon(type);
- return h('div', { className: `feature-cell icon-${type}` }, [content]);
- }
+ /**
+ * @param {DecodedStatus | undefined} status
+ * @param {string | null} platformName
+ */
+ detailsLabelForStatus(status, platformName) {
+ if (!status?.type) return null;
+ switch (status.type) {
+ case 'yes':
+ if (platformName === 'Your browser') return 'Supported in your browser';
+ if (status.version) {
+ return `Supported in ${platformName} ${status.version}`;
+ } else {
+ const fragment = document.createDocumentFragment(),
+ note = document.createElement('span');
+ note.className = 'text-secondary';
+ note.textContent = '(version unknown)';
+ fragment.append(`Supported in ${platformName} `, note);
+ return fragment;
+ }
+ case 'no':
+ if (platformName === 'Your browser')
+ return 'Not supported in your browser';
+ return `Not supported in ${platformName}`;
+ case 'experimental':
+ return `Experimental support in ${platformName}`;
+ case 'not-applicable':
+ return `This feature is not applicable to ${platformName}`;
+ case 'unknown':
+ return 'Detection unavailable for this feature';
+ }
+ throw new TypeError();
+ },
- function renderNote(note) {
- const fragment = document.createDocumentFragment();
- const isMissingData = note.includes('introduced in unknown version');
+ /** @param {string} note */
+ renderNote(note) {
+ if (!note) return note;
// Transform markdown-like backticks into html
+ const fragment = document.createDocumentFragment();
while (note) {
const [head, body, tail] = splitParts(note, '`');
head && fragment.append(head);
- body && fragment.appendChild(h('code', {}, [body]));
- note = tail;
- }
-
- const firstNode = fragment.firstChild;
- if (firstNode.nodeType === Node.TEXT_NODE) {
- // No point for screen readers to pronounce those symbols out loud.
- for (const symbol of ['✓', '✗']) {
- if (firstNode.nodeValue?.startsWith(symbol)) {
- // Before: <#text>✓ Supported#text>
- // After: ✓ <#text> Supported#text>
- firstNode.splitText(1);
- const symbolNode = h('span', {}, firstNode.nodeValue);
- symbolNode.setAttribute('aria-hidden', '');
- fragment.replaceChild(symbolNode, firstNode);
- break;
- }
+ if (body) {
+ const el = document.createElement('code');
+ el.textContent = body;
+ fragment.appendChild(el);
}
+ note = tail;
}
-
- if (isMissingData) {
- fragment.appendChild(
- h(
- 'a',
- {
- href: 'https://github.com/WebAssembly/website/blob/master/features.json',
- target: '_blank',
- },
- [' (contribute data)']
- )
- );
- }
-
return fragment;
- }
-
- // Break a string into three parts using the given delimiter.
- function splitParts(str, delim) {
- const start = str.indexOf(delim);
- const end = str.indexOf(delim, start + 1);
- if (start >= 0 && end > start) {
- const head = str.substring(0, start);
- const body = str.substring(start + 1, end);
- const tail = str.substring(end + 1);
- return [head, body, tail];
+ },
+
+ /** @param {string} s */
+ str2id(s) {
+ return s.replaceAll(/\W+/g, '-').toLowerCase();
+ },
+});
+
+document.addEventListener('alpine:init', () => {
+ // A custom direction `x-replace` to directly insert DOM nodes into the document.
+ // This avoids HTML parsing and is much more performant than `x-html`.
+ Alpine.directive(
+ 'replace',
+ (
+ /** @type {Element} */ el,
+ { expression, modifiers },
+ { evaluateLater, effect }
+ ) => {
+ const clone = modifiers.includes('clone');
+ const getChild = evaluateLater(expression);
+ effect(() =>
+ getChild((child) => {
+ if (Array.isArray(child))
+ throw new TypeError(
+ 'x-replace cannot operate on arrays, use DocumentFragment instead'
+ );
+ if (clone && child instanceof Node)
+ child = document.importNode(child, true);
+ el.replaceChildren(child);
+ })
+ );
}
- return [str, '', ''];
- }
-
- // Lazy-loading
- function _loadTooltipModule() {
- // Be sure to change the preloads in markdown when updating url.
- // The ESM bundle of this package doesn't work with unpkg.com.
- const module =
- import('https://cdn.jsdelivr.net/npm/@floating-ui/dom@1/+esm');
-
- const subscribers = new Set();
- const updateAll = () => {
- for (const fn of subscribers) fn();
- };
-
- document.addEventListener('scroll', updateAll, { passive: true });
- scrollbox.addEventListener('scroll', updateAll, { passive: true });
- window.addEventListener('resize', updateAll, { passive: true });
-
- let counter = 0;
- return (reference, note, boundary) =>
- module.then(({ computePosition, offset, flip, shift, arrow }) => {
- const tooltipId = `tooltip-${counter++}`;
- const tooltip = h('div', {
- id: tooltipId,
- className: 'feature-tooltip',
- role: 'tooltip',
- });
- tooltip.appendChild(renderNote(note));
-
- const arrowElement = h('div', { className: 'feature-tooltip-arrow' });
- tooltip.appendChild(arrowElement);
-
- const update = () =>
- computePosition(reference, tooltip, {
- placement: 'top',
- middleware: [
- offset(6),
- flip({ boundary }),
- shift({ padding: 6, boundary }),
- arrow({ element: arrowElement, padding: 3, boundary }),
- ],
- }).then(({ x, y, placement, middlewareData }) => {
- const { x: arrowX, y: arrowY } = middlewareData.arrow;
- Object.assign(arrowElement.style, {
- left: arrowX !== null ? `${arrowX}px` : '',
- top: arrowY !== null ? `${arrowY}px` : '',
- });
-
- tooltip.style.transform = `translate(${x}px, ${y}px)`;
- // Force the browser to apply CSS changes first
- if (tooltip.dataset.placement !== placement) tooltip.offsetHeight;
- // This will then enable the transition effect
- tooltip.dataset.placement = placement;
- });
-
- const setVisible = (visible) => {
- if (visible) {
- tooltip.style.removeProperty('display');
- update();
- subscribers.add(update);
- } else {
- subscribers.delete(update);
- tooltip.style.display = 'none';
- delete tooltip.dataset.placement; // disable the transition effect
- }
- };
-
- setVisible(false);
-
- const monitor = (name, state, listener = () => setVisible(state)) =>
- reference.addEventListener(name, listener);
- monitor('focusin', true);
- monitor('focusout', false);
-
- // Add a bit of delay to mouse events
- let timeout = null;
- monitor('mouseenter', true, () => {
- clearTimeout(timeout);
- if (subscribers.size) {
- timeout = setTimeout(() => setVisible(true), 80);
- } else {
- // Immediately show if there aren't other tooltips visible
- setVisible(true);
- }
- });
- monitor('mouseleave', false, () => {
- clearTimeout(timeout);
- timeout = setTimeout(() => setVisible(false), 80);
- });
-
- reference.appendChild(tooltip);
- reference.setAttribute('aria-describedby', tooltipId);
- return tooltip;
- });
- }
+ );
- function _loadFeatureDetectModule() {
- // Please cache bust by bumping the `v` parameter whenever `feature.json` is
- // updated to depend on a new version of the library. See #353 for discussion.
- // Make sure to also match the preload link in `features.md`.
- const module =
- import('https://unpkg.com/wasm-feature-detect@1/dist/esm/index.js?v=1');
- return (featureName) =>
- module.then((wasmFeatureDetect) => wasmFeatureDetect[featureName]());
- }
-})();
+ Alpine.data('data', state);
+});
diff --git a/features.json b/features.json
index c0c9166..4a295f4 100644
--- a/features.json
+++ b/features.json
@@ -252,10 +252,17 @@
"phase": 3
}
},
+ "categories": [
+ { "name": "Web Browsers", "queryKey": "browsers", "default": true },
+ { "name": "Standalone Runtimes", "queryKey": "standalones", "default": true },
+ { "name": "Embedded Runtimes", "queryKey": "embeddeds" },
+ { "name": "Tools", "queryKey": "tools" }
+ ],
"browsers": {
"Chrome": {
"url": "https://www.google.com/chrome/",
"logo": "/images/chrome.svg",
+ "category": "Web Browsers",
"features": {
"bigInt": "85",
"branchHinting": "137",
@@ -298,6 +305,7 @@
"Firefox": {
"url": "https://www.mozilla.org/firefox/",
"logo": "/images/firefox.svg",
+ "category": "Web Browsers",
"features": {
"bigInt": "78",
"branchHinting": [
@@ -341,10 +349,11 @@
"Safari": {
"url": "https://www.apple.com/safari/",
"logo": "/images/safari.svg",
+ "category": "Web Browsers",
"features": {
"bigInt": [
"15",
- "wasm-bigint is supported in desktop Safari since 14.1 and iOS Safari since 14.5; however BigInt64Array, which is needed by Emscripten, was released in 15"
+ "`wasm-bigint` is supported in desktop Safari since 14.1 and iOS Safari since 14.5; however `BigInt64Array`, which is needed by Emscripten, was released in 15"
],
"branchHinting": "16",
"bulkMemory": "15",
@@ -383,6 +392,7 @@
"Node.js": {
"url": "https://nodejs.org/",
"logo": "/images/nodejs.svg",
+ "category": "Standalone Runtimes",
"features": {
"bigInt": "15.0",
"branchHinting": [
@@ -433,6 +443,7 @@
"Deno": {
"url": "https://deno.com/",
"logo": "/images/deno.svg",
+ "category": "Standalone Runtimes",
"features": {
"bigInt": "1.1.2",
"branchHinting": "2.3.2",
@@ -475,6 +486,7 @@
"GraalWasm": {
"url": "https://www.graalvm.org/webassembly/",
"logo": "/images/graalvm.svg",
+ "category": "Standalone Runtimes",
"features": {
"bigInt": "21.3",
"bulkMemory": "23.0",
@@ -499,6 +511,7 @@
"Chicory": {
"url": "https://chicory.dev/",
"logo": "/images/chicory.svg",
+ "category": "Standalone Runtimes",
"features": {
"bigInt": null,
"bulkMemory": "1.0.0",
@@ -523,6 +536,7 @@
"Wasmtime": {
"url": "https://wasmtime.dev/",
"logo": "/images/bca.svg",
+ "category": "Standalone Runtimes",
"features": {
"bigInt": null,
"bulkMemory": "0.20",
@@ -561,6 +575,7 @@
"Wasmer": {
"url": "https://wasmer.io/",
"logo": "/images/wasmer.svg",
+ "category": "Standalone Runtimes",
"features": {
"bigInt": null,
"bulkMemory": "1.0",
@@ -580,39 +595,10 @@
"webContentSecurityPolicy": null
}
},
- "wasm2c": {
- "url": "https://github.com/WebAssembly/wabt",
- "logo": "/images/wasm2c.svg",
- "features": {
- "bigInt": null,
- "bulkMemory": "1.0.30",
- "customAnnotationSyntaxInTheTextFormat": null,
- "exceptionsFinal": ["flag", "Requires flag `--enable-exceptions`"],
- "exceptions": ["flag", "Requires flag `--enable-exceptions`"],
- "extendedConst": ["flag", "Requires flag `--enable-extended-const`"],
- "esmIntegration": null,
- "jspi": null,
- "jsStringBuiltins": null,
- "memory64": ["flag", "Requires flag `--enable-memory64`"],
- "multiMemory": ["flag", "Requires flag `--enable-multi-memory`"],
- "tailCall": ["flag", "Requires flag `--enable-tail-call`"],
- "customPageSizes": [
- "flag",
- "Requires flag `--enable-custom-page-sizes`"
- ],
- "multiValue": "1.0.24",
- "mutableGlobals": "1.0.1",
- "referenceTypes": "1.0.31",
- "saturatedFloatToInt": "1.0.24",
- "signExtensions": "1.0.24",
- "simd": "1.0.33",
- "typeReflection": null,
- "webContentSecurityPolicy": null
- }
- },
"wizard": {
"url": "https://github.com/titzer/wizard-engine",
"logo": "/images/wizard.svg",
+ "category": "Standalone Runtimes",
"features": {
"bigInt": null,
"bulkMemory": "25",
@@ -625,7 +611,6 @@
"jspi": null,
"jsStringBuiltins": null,
"memory64": "25",
- "multiMemory": "25",
"tailCall": "25",
"customPageSizes": ["flag", "Requires flag `--ext:custom-page-sizes`"],
"multiValue": "21",
@@ -646,6 +631,8 @@
"wazero": {
"url": "https://wazero.io",
"logo": "/images/wazero.svg",
+ "logoClassName": "invert-in-dark-theme",
+ "category": "Standalone Runtimes",
"features": {
"bigInt": null,
"bulkMemory": true,
@@ -658,8 +645,7 @@
"jspi": null,
"jsStringBuiltins": null,
"memory64": false,
- "multiMemory": false,
- "tailCall": ["flag", "experimental feature"],
+ "tailCall": "flag",
"customPageSizes": null,
"multiValue": true,
"multiMemory": false,
@@ -670,15 +656,48 @@
"signExtensions": true,
"simd": true,
"stackSwitching": false,
- "threads": ["flag", "experimental feature"],
+ "threads": "flag",
"typedFunctionReferences": false,
"typeReflection": null,
"webContentSecurityPolicy": null
}
},
+ "wasm2c": {
+ "url": "https://github.com/WebAssembly/wabt",
+ "logo": "/images/wasm2c.svg",
+ "category": "Standalone Runtimes",
+ "features": {
+ "bigInt": null,
+ "bulkMemory": "1.0.30",
+ "customAnnotationSyntaxInTheTextFormat": null,
+ "exceptionsFinal": ["flag", "Requires flag `--enable-exceptions`"],
+ "exceptions": ["flag", "Requires flag `--enable-exceptions`"],
+ "extendedConst": ["flag", "Requires flag `--enable-extended-const`"],
+ "esmIntegration": null,
+ "jspi": null,
+ "jsStringBuiltins": null,
+ "memory64": ["flag", "Requires flag `--enable-memory64`"],
+ "multiMemory": ["flag", "Requires flag `--enable-multi-memory`"],
+ "tailCall": ["flag", "Requires flag `--enable-tail-call`"],
+ "customPageSizes": [
+ "flag",
+ "Requires flag `--enable-custom-page-sizes`"
+ ],
+ "multiValue": "1.0.24",
+ "mutableGlobals": "1.0.1",
+ "referenceTypes": "1.0.31",
+ "saturatedFloatToInt": "1.0.24",
+ "signExtensions": "1.0.24",
+ "simd": "1.0.33",
+ "typeReflection": null,
+ "webContentSecurityPolicy": null
+ }
+ },
"Owi": {
"url": "https://github.com/ocamlpro/owi",
- "logo": "/images/owi.png",
+ "logo": "/images/owi.webp",
+ "logoClassName": "rounded",
+ "category": ["Tools", "Standalone Runtimes"],
"features": {
"bigInt": null,
"bulkMemory": true,
@@ -691,7 +710,6 @@
"jspi": null,
"jsStringBuiltins": null,
"memory64": false,
- "multiMemory": false,
"tailCall": true,
"customPageSizes": false,
"multiValue": true,
@@ -712,6 +730,7 @@
"Binaryen": {
"url": "https://github.com/WebAssembly/binaryen",
"logo": "/images/binaryen.svg",
+ "category": "Tools",
"features": {
"bigInt": true,
"branchHinting": true,
@@ -726,7 +745,6 @@
"jspi": true,
"jsStringBuiltins": true,
"memory64": true,
- "multiMemory": true,
"tailCall": true,
"customPageSizes": false,
"multiValue": true,
@@ -747,6 +765,7 @@
"wasm-language-tools": {
"url": "https://github.com/g-plane/wasm-language-tools",
"logo": "/images/wasm-language-tools.svg",
+ "category": "Tools",
"features": {
"bigInt": null,
"branchHinting": null,
diff --git a/features.md b/features.md
index 51e8767..7a3920b 100644
--- a/features.md
+++ b/features.md
@@ -16,33 +16,7 @@ For the complete list of current proposals and their respective stages, check
out the
[`WebAssembly/proposals` repo](https://github.com/WebAssembly/proposals).
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-The table below aims to track implemented features in popular engines:
-
-
-
-
-
-
+{% include feature-table.html %}
To detect supported features at runtime from JavaScript, check out the
[`wasm-feature-detect` library](https://github.com/GoogleChromeLabs/wasm-feature-detect),
diff --git a/features.schema.json b/features.schema.json
index f70ec19..cb5c965 100644
--- a/features.schema.json
+++ b/features.schema.json
@@ -24,13 +24,34 @@
"properties": {
"url": { "type": "string", "format": "uri" },
"logo": { "type": "string", "format": "uri-reference" },
+ "logoClassName": { "type": "string" },
+ "category": {
+ "title": "Category this platform belongs to. If multiple, the first one will be considered primary",
+ "anyOf": [
+ { "$ref": "#/definitions/category" },
+ {
+ "type": "array",
+ "items": { "$ref": "#/definitions/category" },
+ "minItems": 2,
+ "uniqueItems": true
+ }
+ ]
+ },
"features": {
"type": "object",
"additionalProperties": { "$ref": "#/definitions/status" }
}
},
"additionalProperties": false,
- "required": ["url", "logo", "features"]
+ "required": ["url", "category", "features"]
+ },
+ "category": {
+ "enum": [
+ "Web Browsers",
+ "Standalone Runtimes",
+ "Embedded Runtimes",
+ "Tools"
+ ]
},
"status": {
"title": "Status of this feature",
@@ -80,6 +101,18 @@
"type": "object",
"additionalProperties": { "$ref": "#/definitions/feature-info" }
},
+ "categories": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": { "$ref": "#/definitions/category" },
+ "queryKey": { "type": "string", "pattern": "^[\\w\\-\\.~]+$" },
+ "default": { "const": true }
+ },
+ "required": ["name", "queryKey"]
+ }
+ },
"browsers": {
"type": "object",
"additionalProperties": { "$ref": "#/definitions/browser-features" }
diff --git a/images/binaryen.svg b/images/binaryen.svg
index bb089ea..3b788f2 100644
--- a/images/binaryen.svg
+++ b/images/binaryen.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/images/owi.png b/images/owi.png
deleted file mode 100644
index 41082a1..0000000
Binary files a/images/owi.png and /dev/null differ
diff --git a/images/owi.webp b/images/owi.webp
new file mode 100644
index 0000000..475005c
Binary files /dev/null and b/images/owi.webp differ
diff --git a/js/dark-mode-toggle.min.mjs b/js/dark-mode-toggle.min.mjs
index 0b3ed70..82be723 100644
--- a/js/dark-mode-toggle.min.mjs
+++ b/js/dark-mode-toggle.min.mjs
@@ -1,2 +1,7 @@
// @license © 2019 Google LLC. Licensed under the Apache License, Version 2.0.
-const e=document;let t={};try{t=localStorage}catch(e){}const i="prefers-color-scheme";const a="media";const s="light";const r="dark";const h="system";const o=`(${i}:${r})`;const l=`(${i}:${s})`;const n="link[rel=stylesheet]";const c="style";const d="remember";const p="legend";const b="toggle";const g="switch";const m="three-way";const u="appearance";const f="permanent";const k="mode";const y="colorschemechange";const $="permanentcolorscheme";const v="all";const L="not all";const T="dark-mode-toggle";const W="https://googlechromelabs.github.io/dark-mode-toggle/demo/";const w=(e,t,i=t)=>{Object.defineProperty(e,i,{enumerable:true,get(){const e=this.getAttribute(t);return e===null?"":e},set(e){this.setAttribute(t,e)}})};const x=(e,t,i=t)=>{Object.defineProperty(e,i,{enumerable:true,get(){return this.hasAttribute(t)},set(e){if(e){this.setAttribute(t,"")}else{this.removeAttribute(t)}}})};const R=e.createElement("template");R.innerHTML=``;export class DarkModeToggle extends HTMLElement{static get observedAttributes(){return[k,u,f,p,s,r,d]}constructor(){super();w(this,k);w(this,u);w(this,p);w(this,s);w(this,r);w(this,h);w(this,d);x(this,f);this.t=null;this.i=null;e.addEventListener(y,e=>{this.mode=e.detail.colorScheme;this.h();this.o();this.l()});e.addEventListener($,e=>{this.permanent=e.detail.permanent;this.p.checked=this.permanent;this.l()});this.m()}m(){const t=this.attachShadow({mode:"open"});t.append(R.content.cloneNode(true));this.t=e.querySelectorAll(`${n}[${a}*=${i}][${a}*="${r}"],\n ${c}[${a}*=${i}][${a}*="${r}"]`);this.i=e.querySelectorAll(`${n}[${a}*=${i}][${a}*="${s}"], \n ${c}[${a}*=${i}][${a}*="${s}"]`);this.u=t.querySelector("[part=lightRadio]");this.k=t.querySelector("[part=lightLabel]");this.$=t.querySelector("[part=darkRadio]");this.v=t.querySelector("[part=darkLabel]");this.L=t.querySelector("[part=toggleCheckbox]");this.T=t.querySelector("[part=toggleLabel]");this.W=t.querySelector("[part=lightThreeWayRadio]");this.R=t.querySelector("[part=lightThreeWayLabel]");this.C=t.querySelector("[part=systemThreeWayRadio]");this.M=t.querySelector("[part=systemThreeWayLabel]");this.S=t.querySelector("[part=darkThreeWayRadio]");this._=t.querySelector("[part=darkThreeWayLabel]");this.A=t.querySelector("legend");this.D=t.querySelector("aside");this.p=t.querySelector("[part=permanentCheckbox]");this.O=t.querySelector("[part=permanentLabel]")}connectedCallback(){const e=matchMedia(o).media!==L;if(e){matchMedia(o).addListener(({matches:e})=>{if(this.permanent){return}this.mode=e?r:s;this.j(y,{colorScheme:this.mode})})}let i=false;try{i=t.getItem(T)}catch(e){}if(i&&[r,s].includes(i)){this.mode=i;this.p.checked=true;this.permanent=true}else if(e){this.mode=matchMedia(l).matches?s:r}if(!this.mode){this.mode=s}if(this.permanent&&!i){try{t.setItem(T,this.mode)}catch(e){}}if(!this.appearance){this.appearance=b}this.P();this.h();this.o();this.l();[this.u,this.$].forEach(e=>{e.addEventListener("change",()=>{this.mode=this.u.checked?s:r;this.o();this.l();this.j(y,{colorScheme:this.mode})})});this.L.addEventListener("change",()=>{this.mode=this.L.checked?r:s;this.h();this.l();this.j(y,{colorScheme:this.mode})});this.W.addEventListener("change",()=>{this.mode=s;this.permanent=true;this.o();this.h();this.l();this.j(y,{colorScheme:this.mode});this.j($,{permanent:this.permanent})});this.S.addEventListener("change",()=>{this.mode=r;this.permanent=true;this.o();this.h();this.l();this.j(y,{colorScheme:this.mode});this.j($,{permanent:this.permanent})});this.C.addEventListener("change",()=>{this.mode=this.H();this.permanent=false;this.o();this.h();this.l();this.j(y,{colorScheme:this.mode});this.j($,{permanent:this.permanent})});this.p.addEventListener("change",()=>{this.permanent=this.p.checked;this.l();this.j($,{permanent:this.permanent})});this.q();this.j(y,{colorScheme:this.mode});this.j($,{permanent:this.permanent})}attributeChangedCallback(e,i,a){if(e===k){const e=[s,h,r];if(!e.includes(a)){throw new RangeError(`Allowed values are: "${e.join(`", "`)}".`)}if(matchMedia("(hover:none)").matches&&this.remember){this.B()}if(this.permanent){try{t.setItem(T,this.mode)}catch(e){}}this.h();this.o();this.l();this.q()}else if(e===u){const e=[b,g,m];if(!e.includes(a)){throw new RangeError(`Allowed values are: "${e.join(`", "`)}".`)}this.P()}else if(e===f){if(this.permanent){if(this.mode){try{t.setItem(T,this.mode)}catch(e){}}}else{try{t.removeItem(T)}catch(e){}}this.p.checked=this.permanent}else if(e===p){this.A.textContent=a}else if(e===d){this.O.textContent=a}else if(e===s){this.k.textContent=a;if(this.mode===s){this.T.textContent=a}}else if(e===r){this.v.textContent=a;if(this.mode===r){this.T.textContent=a}}}H(){return matchMedia(l).matches?s:r}j(e,t){this.dispatchEvent(new CustomEvent(e,{bubbles:true,composed:true,detail:t}))}P(){this.u.hidden=this.k.hidden=this.$.hidden=this.v.hidden=this.L.hidden=this.T.hidden=this.W.hidden=this.R.hidden=this.C.hidden=this.M.hidden=this.S.hidden=this._.hidden=true;switch(this.appearance){case g:this.u.hidden=this.k.hidden=this.$.hidden=this.v.hidden=false;break;case m:this.W.hidden=this.R.hidden=this.C.hidden=this.M.hidden=this.S.hidden=this._.hidden=false;break;case b:default:this.L.hidden=this.T.hidden=false;break}}h(){if(this.mode===s){this.u.checked=true}else{this.$.checked=true}}o(){if(this.mode===s){this.T.style.setProperty(`--${T}-checkbox-icon`,`var(--${T}-light-icon,url("${W}moon.png"))`);this.T.textContent=this.light;if(!this.light){this.T.ariaLabel=r}this.L.checked=false}else{this.T.style.setProperty(`--${T}-checkbox-icon`,`var(--${T}-dark-icon,url("${W}sun.png"))`);this.T.textContent=this.dark;if(!this.dark){this.T.ariaLabel=s}this.L.checked=true}}l(){this.R.ariaLabel=s;this.M.ariaLabel=h;this._.ariaLabel=r;this.R.textContent=this.light;this.M.textContent=this.system;this._.textContent=this.dark;if(this.permanent){if(this.mode===s){this.W.checked=true}else{this.S.checked=true}}else{this.C.checked=true}}q(){if(this.mode===s){this.i.forEach(e=>{e.media=v;e.disabled=false});this.t.forEach(e=>{e.media=L;e.disabled=true})}else{this.t.forEach(e=>{e.media=v;e.disabled=false});this.i.forEach(e=>{e.media=L;e.disabled=true})}}B(){this.D.style.visibility="visible";setTimeout(()=>{this.D.style.visibility="hidden"},3e3)}}customElements.define(T,DarkModeToggle);
\ No newline at end of file
+const e=document;let t={};try{t=localStorage}catch(e){}const i="prefers-color-scheme";const a="media";const s="light";const r="dark";const h="system";const o=`(${i}:${r})`;const l=`(${i}:${s})`;const n="link[rel=stylesheet]";const c="style";const d="remember";const p="legend";const b="toggle";const g="switch";const m="three-way";const u="appearance";const f="permanent";const k="mode";const y="colorschemechange";const $="permanentcolorscheme";const v="all";const L="not all";const T="dark-mode-toggle";const W="https://googlechromelabs.github.io/dark-mode-toggle/demo/";const w=(e,t,i=t)=>{Object.defineProperty(e,i,{enumerable:true,get(){const e=this.getAttribute(t);return e===null?"":e},set(e){this.setAttribute(t,e)}})};const x=(e,t,i=t)=>{Object.defineProperty(e,i,{enumerable:true,get(){return this.hasAttribute(t)},set(e){if(e){this.setAttribute(t,"")}else{this.removeAttribute(t)}}})};const R=e.createElement("template");R.innerHTML=``;export class DarkModeToggle extends HTMLElement{static get observedAttributes(){return[k,u,f,p,s,r,d]}constructor(){super();w(this,k);w(this,u);w(this,p);w(this,s);w(this,r);w(this,h);w(this,d);x(this,f);this.t=null;this.i=null;e.addEventListener(y,e=>{this.mode=e.detail.colorScheme;this.h();this.o();this.l()});e.addEventListener($,e=>{this.permanent=e.detail.permanent;this.p.checked=this.permanent;this.l()});this.m()}m(){const t=this.attachShadow({mode:"open"});t.append(R.content.cloneNode(true));this.t=e.querySelectorAll(`${n}[${a}*=${i}][${a}*="${r}"],\n ${c}[${a}*=${i}][${a}*="${r}"]`);this.i=e.querySelectorAll(`${n}[${a}*=${i}][${a}*="${s}"], \n ${c}[${a}*=${i}][${a}*="${s}"]`);this.u=t.querySelector("[part=lightRadio]");this.k=t.querySelector("[part=lightLabel]");this.$=t.querySelector("[part=darkRadio]");this.v=t.querySelector("[part=darkLabel]");this.L=t.querySelector("[part=toggleCheckbox]");this.T=t.querySelector("[part=toggleLabel]");this.W=t.querySelector("[part=lightThreeWayRadio]");this.R=t.querySelector("[part=lightThreeWayLabel]");this.C=t.querySelector("[part=systemThreeWayRadio]");this.M=t.querySelector("[part=systemThreeWayLabel]");this.S=t.querySelector("[part=darkThreeWayRadio]");this._=t.querySelector("[part=darkThreeWayLabel]");this.A=t.querySelector("legend");this.D=t.querySelector("aside");this.p=t.querySelector("[part=permanentCheckbox]");this.O=t.querySelector("[part=permanentLabel]")}connectedCallback(){const e=matchMedia(o).media!==L;if(e){matchMedia(o).addListener(({matches:e})=>{if(this.permanent){return}this.mode=e?r:s;this.j(y,{colorScheme:this.mode})})}let i=false;try{i=t.getItem(T)}catch(e){}if(i&&[r,s].includes(i)){this.mode=i;this.p.checked=true;this.permanent=true}else if(e){this.mode=matchMedia(l).matches?s:r}if(!this.mode){this.mode=s}if(this.permanent&&!i){try{t.setItem(T,this.mode)}catch(e){}}if(!this.appearance){this.appearance=b}this.P();this.h();this.o();this.l();[this.u,this.$].forEach(e=>{e.addEventListener("change",()=>{this.mode=this.u.checked?s:r;this.o();this.l();this.j(y,{colorScheme:this.mode})})});this.L.addEventListener("change",()=>{this.mode=this.L.checked?r:s;this.h();this.l();this.j(y,{colorScheme:this.mode})});this.W.addEventListener("change",()=>{this.mode=s;this.permanent=true;this.o();this.h();this.l();this.j(y,{colorScheme:this.mode});this.j($,{permanent:this.permanent})});this.S.addEventListener("change",()=>{this.mode=r;this.permanent=true;this.o();this.h();this.l();this.j(y,{colorScheme:this.mode});this.j($,{permanent:this.permanent})});this.C.addEventListener("change",()=>{this.mode=this.H();this.permanent=false;this.o();this.h();this.l();this.j(y,{colorScheme:this.mode});this.j($,{permanent:this.permanent})});this.p.addEventListener("change",()=>{this.permanent=this.p.checked;this.l();this.j($,{permanent:this.permanent})});this.q();this.j(y,{colorScheme:this.mode});this.j($,{permanent:this.permanent})}attributeChangedCallback(e,i,a){if(e===k){const e=[s,h,r];if(!e.includes(a)){throw new RangeError(`Allowed values are: "${e.join(`", "`)}".`)}if(matchMedia("(hover:none)").matches&&this.remember){this.B()}if(this.permanent){try{t.setItem(T,this.mode)}catch(e){}}this.h();this.o();this.l();this.q()}else if(e===u){const e=[b,g,m];if(!e.includes(a)){throw new RangeError(`Allowed values are: "${e.join(`", "`)}".`)}this.P()}else if(e===f){if(this.permanent){if(this.mode){try{t.setItem(T,this.mode)}catch(e){}}}else{try{t.removeItem(T)}catch(e){}}this.p.checked=this.permanent}else if(e===p){this.A.textContent=a}else if(e===d){this.O.textContent=a}else if(e===s){this.k.textContent=a;if(this.mode===s){this.T.textContent=a}}else if(e===r){this.v.textContent=a;if(this.mode===r){this.T.textContent=a}}}H(){return matchMedia(l).matches?s:r}j(e,t){this.dispatchEvent(new CustomEvent(e,{bubbles:true,composed:true,detail:t}))}P(){this.u.hidden=this.k.hidden=this.$.hidden=this.v.hidden=this.L.hidden=this.T.hidden=this.W.hidden=this.R.hidden=this.C.hidden=this.M.hidden=this.S.hidden=this._.hidden=true;switch(this.appearance){case g:this.u.hidden=this.k.hidden=this.$.hidden=this.v.hidden=false;break;case m:this.W.hidden=this.R.hidden=this.C.hidden=this.M.hidden=this.S.hidden=this._.hidden=false;break;case b:default:this.L.hidden=this.T.hidden=false;break}}h(){if(this.mode===s){this.u.checked=true}else{this.$.checked=true}}o(){if(this.mode===s){this.T.style.setProperty(`--${T}-checkbox-icon`,`var(--${T}-light-icon,url("${W}moon.png"))`);this.T.textContent=this.light;if(!this.light){this.T.ariaLabel=r}this.L.checked=false}else{this.T.style.setProperty(`--${T}-checkbox-icon`,`var(--${T}-dark-icon,url("${W}sun.png"))`);this.T.textContent=this.dark;if(!this.dark){this.T.ariaLabel=s}this.L.checked=true}}l(){this.R.ariaLabel=s;this.M.ariaLabel=h;this._.ariaLabel=r;this.R.textContent=this.light;this.M.textContent=this.system;this._.textContent=this.dark;if(this.permanent){if(this.mode===s){this.W.checked=true}else{this.S.checked=true}}else{this.C.checked=true}}q(){if(this.mode===s){this.i.forEach(e=>{e.media=v;e.disabled=false});this.t.forEach(e=>{e.media=L;e.disabled=true})}else{this.t.forEach(e=>{e.media=v;e.disabled=false});this.i.forEach(e=>{e.media=L;e.disabled=true})}}B(){this.D.style.visibility="visible";setTimeout(()=>{this.D.style.visibility="hidden"},3e3)}}customElements.define(T,DarkModeToggle);
+
+// Keep the in sync with user selection.
+document.addEventListener('colorschemechange', (e) => {
+ document.querySelectorAll('meta[name=color-scheme]').forEach(el => el.content = e.detail.colorScheme);
+});