Refactor routing in App component to enhance navigation and improve error handling by integrating dynamic routes and updating the NotFound route.

This commit is contained in:
becarta
2025-05-23 12:43:00 +02:00
parent f40db0f5c9
commit a544759a3b
11127 changed files with 1647032 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
export interface Integration {
name: string;
title: string;
description: string;
image?: string;
categories: string[];
repoUrl: string;
npmUrl: string;
homepageUrl: string;
official: boolean;
featured: number;
downloads: number;
}
declare const _default: {
id: string;
name: string;
icon: "astro:logo";
init(canvas: ShadowRoot, eventTarget: import("../helpers.js").ToolbarAppEventTarget): Promise<void>;
};
export default _default;

View File

@@ -0,0 +1,394 @@
import { isDefinedIcon } from "../ui-library/icons.js";
import { colorForIntegration, iconForIntegration } from "./utils/icons.js";
import {
closeOnOutsideClick,
createWindowElement,
synchronizePlacementOnUpdate
} from "./utils/window.js";
const astroLogo = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 99 26" width="100"><path fill="#fff" d="M6.70402 22.1453c-1.17459-1.0737-1.51748-3.3297-1.02811-4.9641.84853 1.0304 2.02424 1.3569 3.24204 1.5411 1.88005.2844 3.72635.178 5.47285-.6813.1998-.0984.3844-.2292.6027-.3617.1639.4755.2065.9554.1493 1.4439-.1392 1.1898-.7313 2.1088-1.673 2.8054-.3765.2787-.775.5278-1.1639.7905-1.1948.8075-1.518 1.7544-1.0691 3.1318.0107.0336.0202.0671.0444.149-.6101-.273-1.0557-.6705-1.39518-1.1931-.3586-.5517-.52921-1.1619-.53819-1.8221-.00449-.3213-.00449-.6455-.0477-.9623-.10551-.7722-.46804-1.118-1.15102-1.1379-.70094-.0205-1.2554.4129-1.40244 1.0953-.01122.0523-.02749.1041-.04377.1649l.00112.0006Z"/><path fill="url(#paint0_linear_386_2739)" d="M6.70402 22.1453c-1.17459-1.0737-1.51748-3.3297-1.02811-4.9641.84853 1.0304 2.02424 1.3569 3.24204 1.5411 1.88005.2844 3.72635.178 5.47285-.6813.1998-.0984.3844-.2292.6027-.3617.1639.4755.2065.9554.1493 1.4439-.1392 1.1898-.7313 2.1088-1.673 2.8054-.3765.2787-.775.5278-1.1639.7905-1.1948.8075-1.518 1.7544-1.0691 3.1318.0107.0336.0202.0671.0444.149-.6101-.273-1.0557-.6705-1.39518-1.1931-.3586-.5517-.52921-1.1619-.53819-1.8221-.00449-.3213-.00449-.6455-.0477-.9623-.10551-.7722-.46804-1.118-1.15102-1.1379-.70094-.0205-1.2554.4129-1.40244 1.0953-.01122.0523-.02749.1041-.04377.1649l.00112.0006Z"/><path fill="#fff" d="M0 16.909s3.47815-1.6944 6.96603-1.6944l2.62973-8.13858c.09846-.39359.38592-.66106.71044-.66106.3246 0 .612.26747.7105.66106l2.6297 8.13858c4.1309 0 6.966 1.6944 6.966 1.6944S14.7045.814589 14.693.782298C14.5234.306461 14.2371 0 13.8512 0H6.76183c-.38593 0-.66063.306461-.84174.782298C5.90733.81398 0 16.909 0 16.909ZM36.671 11.7318c0 1.4262-1.7739 2.2779-4.2302 2.2779-1.5985 0-2.1638-.3962-2.1638-1.2281 0-.8715.7018-1.2875 2.3003-1.2875 1.4426 0 2.6707.0198 4.0937.1981v.0396Zm.0195-1.7629c-.8772-.19808-2.2028-.31693-3.7818-.31693-4.6006 0-6.7644 1.08943-6.7644 3.62483 0 2.6344 1.4815 3.6446 4.9125 3.6446 2.9046 0 4.8735-.7328 5.5947-2.5354h.117c-.0195.4358-.039.8716-.039 1.2083 0 .931.156 1.0102.9162 1.0102h3.5869c-.1949-.5546-.3119-2.1194-.3119-3.4663 0-1.446.0585-2.5355.0585-4.00123 0-2.99098-1.7934-4.89253-7.4077-4.89253-2.4173 0-5.1074.41596-7.1543 1.03.1949.81213.4679 2.45617.6043 3.5258 1.774-.83193 4.2887-1.18847 6.2381-1.18847 2.6902 0 3.4309.61404 3.4309 1.86193v.4952ZM46.5325 12.5637c-.4874.0594-1.1502.0594-1.8325.0594-.7213 0-1.3841-.0198-1.8324-.0792 0 .1585-.0195.3367-.0195.4952 0 2.476 1.618 3.922 7.3102 3.922 5.3609 0 7.0958-1.4262 7.0958-3.9418 0-2.3769-1.1501-3.5456-6.238-3.8031-3.9573-.17827-4.3082-.61404-4.3082-1.10924 0-.57442.5068-.87154 3.158-.87154 2.7487 0 3.4894.37635 3.4894 1.16866v.17827c.3899-.01981 1.0917-.03961 1.813-.03961.6823 0 1.423.0198 1.8519.05942 0-.17827.0195-.33674.0195-.47539 0-2.91175-2.4172-3.86252-7.0958-3.86252-5.2634 0-7.0373 1.2875-7.0373 3.8031 0 2.25805 1.423 3.66445 6.472 3.88235 3.7233.1188 4.1327.5348 4.1327 1.1092 0 .6141-.6043.8914-3.2165.8914-3.0021 0-3.7623-.416-3.7623-1.2677v-.1189ZM63.6883 2.125c-1.423 1.32712-3.9768 2.65425-5.3998 3.01079.0195.73289.0195 2.07982.0195 2.81271l1.3061.01981c-.0195 1.40635-.039 3.10979-.039 4.23889 0 2.6344 1.3841 4.6152 5.6922 4.6152 1.813 0 3.0216-.1981 4.5226-.515-.1559-.9706-.3314-2.4562-.3898-3.5852-.8968.2971-2.0274.4556-3.275.4556-1.735 0-2.4368-.4754-2.4368-1.8422 0-1.1884 0-2.29767.0195-3.32768 2.2223.01981 4.4446.05943 5.7507.09904-.0195-1.03.0195-2.51559.078-3.50598-1.8909.03961-4.0157.05942-5.7702.05942.0195-.87154.039-1.70347.0585-2.5354h-.1365ZM75.3313 7.35427c.0195-1.03001.039-1.90156.0585-2.75329h-3.9183c.0585 1.70347.0585 3.44656.0585 6.00172 0 2.5553-.0195 4.3182-.0585 6.0018h4.4836c-.078-1.1885-.0975-3.189-.0975-4.8925 0-2.69388 1.0917-3.46638 3.5674-3.46638 1.1502 0 1.9689.13865 2.6902.39615.0195-1.01019.2144-2.97117.3314-3.84271-.7408-.21789-1.5595-.35655-2.5537-.35655-2.1249-.0198-3.6844.85174-4.4056 2.93156l-.156-.0198ZM94.8501 10.5235c0 2.1591-1.5595 3.1693-4.0157 3.1693-2.4368 0-3.9963-.9508-3.9963-3.1693 0-2.21846 1.579-3.05039 3.9963-3.05039 2.4367 0 4.0157.89135 4.0157 3.05039Zm4.0743-.099c0-4.29832-3.353-6.21968-8.09-6.21968-4.7566 0-7.9926 1.92136-7.9926 6.21968 0 4.2785 3.0216 6.5762 7.9731 6.5762 4.9904 0 8.1095-2.2977 8.1095-6.5762Z"/><defs><linearGradient id="paint0_linear_386_2739" x1="5.46011" x2="16.8017" y1="25.9999" y2="20.6412" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>';
let integrationData;
var astro_default = {
id: "astro:home",
name: "Menu",
icon: "astro:logo",
async init(canvas, eventTarget) {
createCanvas();
document.addEventListener("astro:after-swap", createCanvas);
eventTarget.addEventListener("app-toggled", async (event) => {
resetDebugButton();
if (!(event instanceof CustomEvent)) return;
if (event.detail.state === true) {
if (!integrationData) fetchIntegrationData();
}
});
closeOnOutsideClick(eventTarget);
synchronizePlacementOnUpdate(eventTarget, canvas);
function fetchIntegrationData() {
fetch("https://astro.build/api/v1/dev-overlay/", {
cache: "no-cache"
}).then((res) => res.json()).then((data) => {
integrationData = data;
integrationData.data = integrationData.data.map((integration) => {
return integration;
});
refreshIntegrationList();
});
}
function createCanvas() {
const links = [
{
icon: "bug",
name: "Report a Bug",
link: "https://github.com/withastro/astro/issues/new/choose"
},
{
icon: "lightbulb",
name: "Feedback",
link: "https://github.com/withastro/roadmap/discussions/new/choose"
},
{
icon: "file-search",
name: "Documentation",
link: "https://docs.astro.build"
},
{
icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17 14"><path fill="currentColor" d="M14.3451 1.9072c-1.0375-.47613-2.1323-.81595-3.257-1.010998-.0102-.001716-.0207-.000234-.03.004243s-.017.011728-.022.020757c-.141.249998-.297.576998-.406.832998-1.2124-.18399-2.44561-.18399-3.658 0-.12159-.28518-.25914-.56328-.412-.832998-.00513-.00893-.01285-.016098-.02213-.02056-.00928-.004462-.0197-.00601-.02987-.00444-1.125.193998-2.22.533998-3.257 1.010998-.00888.00339-.0163.00975-.021.018-2.074 3.099-2.643004 6.122-2.364004 9.107.001.014.01.028.021.037 1.207724.8946 2.558594 1.5777 3.995004 2.02.01014.0032.02103.0031.03111-.0003.01007-.0034.01878-.01.02489-.0187.308-.42.582-.863.818-1.329.00491-.0096.0066-.0205.0048-.0312-.00181-.0106-.007-.0204-.0148-.0278-.00517-.0049-.0113-.0086-.018-.011-.43084-.1656-.84811-.3645-1.248-.595-.01117-.0063-.01948-.0167-.0232-.029-.00373-.0123-.00258-.0255.0032-.037.0034-.0074.00854-.014.015-.019.084-.063.168-.129.248-.195.00706-.0057.01554-.0093.02453-.0106.00898-.0012.01813 0 .02647.0036 2.619 1.196 5.454 1.196 8.041 0 .0086-.0037.0181-.0051.0275-.0038.0093.0012.0181.0049.0255.0108.08.066.164.132.248.195.0068.005.0123.0116.0159.0192.0036.0076.0053.016.0049.0244-.0003.0084-.0028.0166-.0072.0238-.0043.0072-.0104.0133-.0176.0176-.399.2326-.8168.4313-1.249.594-.0069.0025-.0132.0065-.0183.0117-.0052.0051-.0092.0114-.0117.0183-.0023.0067-.0032.0138-.0027.0208.0005.0071.0024.0139.0057.0202.24.465.515.909.817 1.329.0061.0087.0148.0153.0249.0187.0101.0034.021.0035.0311.0003 1.4388-.441 2.7919-1.1241 4.001-2.02.0061-.0042.0111-.0097.0147-.0161.0037-.0064.0058-.0135.0063-.0209.334-3.451-.559-6.449-2.366-9.106-.0018-.00439-.0045-.00834-.008-.01162-.0034-.00327-.0075-.00578-.012-.00738Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612Zm5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612Z"/></svg>',
name: "Community",
link: "https://astro.build/chat"
}
];
const hasNewerVersion = window.__astro_dev_toolbar__.latestAstroVersion;
const windowComponent = createWindowElement(
`<style>
#buttons-container {
display: flex;
gap: 16px;
justify-content: center;
}
#buttons-container astro-dev-toolbar-card {
flex: 1;
}
footer {
display: flex;
justify-content: center;
gap: 24px;
}
footer a {
color: rgba(145, 152, 173, 1);
}
footer a:hover {
color: rgba(204, 206, 216, 1);
}
#main-container {
display: flex;
flex-direction: column;
height: 100%;
gap: 24px;
}
p {
margin-top: 0;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
}
header section {
display: flex;
gap: 0.8em;
}
h2 {
color: white;
margin: 0;
font-size: 18px;
}
a {
color: rgba(224, 204, 250, 1);
}
a:hover {
color: #f4ecfd;
}
#integration-list-wrapper {
position: relative;
--offset: 24px;
overflow-x: auto;
overflow-y: hidden;
margin-left: calc(var(--offset) * -1);
margin-right: calc(var(--offset) * -1);
padding-left: var(--offset);
padding-right: var(--offset);
height: 210px;
}
/* Pseudo-elements to fade cards as they scroll out of viewport */
#integration-list-wrapper::before,
#integration-list-wrapper::after {
content: '';
height: 192px;
display: block;
position: fixed;
width: var(--offset);
top: 106px;
background: red;
}
#integration-list-wrapper::before {
left: -1px;
border-left: 1px solid rgba(52, 56, 65, 1);
background: linear-gradient(to right, rgba(19, 21, 26, 1), rgba(19, 21, 26, 0));
}
#integration-list-wrapper::after {
right: -1px;
border-right: 1px solid rgba(52, 56, 65, 1);
background: linear-gradient(to left, rgba(19, 21, 26, 1), rgba(19, 21, 26, 0));
}
#integration-list-wrapper::-webkit-scrollbar {
width: 5px;
height: 8px;
background-color: rgba(255, 255, 255, 0.08); /* or add it to the track */
border-radius: 4px;
}
/* This is wild but gives us a gap on either side of the container */
#integration-list-wrapper::-webkit-scrollbar-button:start:decrement,
#integration-list-wrapper::-webkit-scrollbar-button:end:increment {
display: block;
width: 24px;
background-color: #13151A;
}
/* Removes arrows on both sides */
#integration-list-wrapper::-webkit-scrollbar-button:horizontal:start:increment,
#integration-list-wrapper::-webkit-scrollbar-button:horizontal:end:decrement {
display: none;
}
#integration-list-wrapper::-webkit-scrollbar-track-piece {
border-radius: 4px;
}
#integration-list-wrapper::-webkit-scrollbar-thumb {
background-color: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
#integration-list {
margin-top: 1em;
display: flex;
gap: 16px;
padding-bottom: 1em;
}
#integration-list::after {
content: " ";
display: inline-block;
white-space: pre;
width: 1px;
height: 1px;
}
#integration-list astro-dev-toolbar-card, .integration-skeleton {
min-width: 240px;
height: 160px;
}
.integration-skeleton {
animation: pulse 2s calc(var(--i, 0) * 250ms) cubic-bezier(0.4, 0, 0.6, 1) infinite;
background-color: rgba(35, 38, 45, 1);
border-radius: 8px;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: .5;
}
}
#integration-list astro-dev-toolbar-card .integration-image {
width: 40px;
height: 40px;
background-color: var(--integration-image-background, white);
border-radius: 9999px;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 8px;
}
#integration-list astro-dev-toolbar-card img {
width: 24px;
height: 24px;
}
#integration-list astro-dev-toolbar-card astro-dev-toolbar-icon {
width: 24px;
height: 24px;
color: #fff;
}
#links {
margin: auto 0;
display: flex;
justify-content: center;
gap: 24px;
}
#links a {
text-decoration: none;
align-items: center;
display: flex;
flex-direction: column;
gap: 0.7em;
flex: 1;
white-space: nowrap;
font-weight: 600;
color: white;
}
#links a:hover {
color: rgba(145, 152, 173, 1);
}
#links astro-dev-toolbar-icon {
width: 1.5em;
height: 1.5em;
display: block;
}
#integration-list astro-dev-toolbar-card svg {
width: 24px;
height: 24px;
vertical-align: bottom;
}
#integration-list astro-dev-toolbar-card h3 {
margin: 0;
margin-bottom: 8px;
color: white;
white-space: nowrap;
}
#integration-list astro-dev-toolbar-card p {
font-size: 14px;
}
@media (forced-colors: active) {
svg path[fill="#fff"] {
fill: black;
}
}
</style>
<header>
<section>
${astroLogo}
<astro-dev-toolbar-badge badge-style="gray" size="large">${window.__astro_dev_toolbar__.version}</astro-dev-toolbar-badge>
${hasNewerVersion ? `<astro-dev-toolbar-badge badge-style="green" size="large">${window.__astro_dev_toolbar__.latestAstroVersion} available!</astro-dev-toolbar-badge>
` : ""}
</section>
<astro-dev-toolbar-button id="copy-debug-button">Copy debug info <astro-dev-toolbar-icon icon="copy" /></astro-dev-toolbar-button>
</header>
<hr />
<div id="main-container">
<div>
<header><h2>Featured integrations</h2><a href="https://astro.build/integrations/" target="_blank">View all</a></header>
<div id="integration-list-wrapper">
<section id="integration-list">
<div class="integration-skeleton" style="--i:0;"></div>
<div class="integration-skeleton" style="--i:1;"></div>
<div class="integration-skeleton" style="--i:2;"></div>
<div class="integration-skeleton" style="--i:3;"></div>
<div class="integration-skeleton" style="--i:4;"></div>
</section>
</div>
</div>
<section id="links">
${links.map(
(link) => `<a href="${link.link}" target="_blank"><astro-dev-toolbar-icon ${isDefinedIcon(link.icon) ? `icon="${link.icon}">` : `>${link.icon}`}</astro-dev-toolbar-icon>${link.name}</a>`
).join("")}
</section>
</div>
`
);
const copyDebugButton = windowComponent.querySelector("#copy-debug-button");
copyDebugButton?.addEventListener("click", () => {
navigator.clipboard.writeText(
"```\n" + window.__astro_dev_toolbar__.debugInfo + "\n```"
);
copyDebugButton.textContent = "Copied to clipboard!";
setTimeout(() => {
resetDebugButton();
}, 3500);
});
canvas.append(windowComponent);
if (integrationData) refreshIntegrationList();
}
function resetDebugButton() {
const copyDebugButton = canvas.querySelector("#copy-debug-button");
if (!copyDebugButton) return;
copyDebugButton.innerHTML = 'Copy debug info <astro-dev-toolbar-icon icon="copy" />';
}
function refreshIntegrationList() {
const integrationList = canvas.querySelector("#integration-list");
if (!integrationList) return;
integrationList.innerHTML = "";
const fragment = document.createDocumentFragment();
for (const integration of integrationData.data) {
const integrationComponent = document.createElement("astro-dev-toolbar-card");
integrationComponent.link = integration.homepageUrl;
const integrationContainer = document.createElement("div");
integrationContainer.className = "integration-container";
const integrationImage = document.createElement("div");
integrationImage.className = "integration-image";
if (integration.image) {
const img = document.createElement("img");
img.src = integration.image;
img.alt = integration.title;
integrationImage.append(img);
} else {
const icon = document.createElement("astro-dev-toolbar-icon");
icon.icon = iconForIntegration(integration);
integrationImage.append(icon);
integrationImage.style.setProperty(
"--integration-image-background",
colorForIntegration()
);
}
integrationContainer.append(integrationImage);
let integrationTitle = document.createElement("h3");
integrationTitle.textContent = integration.title;
if (integration.official || integration.categories.includes("official")) {
integrationTitle.innerHTML += ' <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 21 20"><rect width="19" height="19" x="1.16602" y=".5" fill="url(#paint0_linear_917_1096)" fill-opacity=".33" rx="9.5"/><path fill="#fff" d="M15.139 6.80657c-.062-.06248-.1357-.11208-.217-.14592-.0812-.03385-.1683-.05127-.2563-.05127-.0881 0-.1752.01742-.2564.05127-.0813.03384-.155.08344-.217.14592L9.22566 11.7799 7.13899 9.68657c-.06435-.06216-.14031-.11103-.22355-.14383-.08323-.03281-.17211-.04889-.26157-.04735-.08945.00155-.17773.0207-.25978.05637a.68120694.68120694 0 0 0-.21843.15148c-.06216.06435-.11104.14031-.14384.22355-.0328.08321-.04889.17211-.04734.26161.00154.0894.0207.1777.05636.2597.03566.0821.08714.1563.15148.2185l2.56 2.56c.06198.0625.13571.1121.21695.1459s.16838.0513.25639.0513c.088 0 .17514-.0175.25638-.0513s.15497-.0834.21695-.1459L15.139 7.78657c.0677-.06242.1217-.13819.1586-.22253.0369-.08433.056-.1754.056-.26747 0-.09206-.0191-.18313-.056-.26747-.0369-.08433-.0909-.1601-.1586-.22253Z"/><rect width="19" height="19" x="1.16602" y=".5" stroke="url(#paint1_linear_917_1096)" rx="9.5"/><defs><linearGradient id="paint0_linear_917_1096" x1="20.666" x2="-3.47548" y1=".00000136" y2="10.1345" gradientUnits="userSpaceOnUse"><stop stop-color="#4AF2C8"/><stop offset="1" stop-color="#2F4CB3"/></linearGradient><linearGradient id="paint1_linear_917_1096" x1="20.666" x2="-3.47548" y1=".00000136" y2="10.1345" gradientUnits="userSpaceOnUse"><stop stop-color="#4AF2C8"/><stop offset="1" stop-color="#2F4CB3"/></linearGradient></defs></svg>';
}
integrationContainer.append(integrationTitle);
const integrationDescription = document.createElement("p");
integrationDescription.textContent = integration.description.length > 90 ? integration.description.slice(0, 90) + "\u2026" : integration.description;
integrationContainer.append(integrationDescription);
integrationComponent.append(integrationContainer);
fragment.append(integrationComponent);
}
integrationList.append(fragment);
}
}
};
export {
astro_default as default
};

View File

@@ -0,0 +1,15 @@
import type { DevToolbarHighlight } from '../../ui-library/highlight.js';
import { type AuditRule } from './rules/index.js';
export type Audit = {
auditedElement: HTMLElement;
rule: AuditRule;
highlight: DevToolbarHighlight | null;
card: HTMLElement | null;
};
declare const _default: {
id: string;
name: string;
icon: "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 1 20 16\"><path fill=\"#fff\" d=\"M.6 2A1.1 1.1 0 0 1 1.7.9h16.6a1.1 1.1 0 1 1 0 2.2H1.6A1.1 1.1 0 0 1 .8 2Zm1.1 7.1h6a1.1 1.1 0 0 0 0-2.2h-6a1.1 1.1 0 0 0 0 2.2ZM9.3 13H1.8a1.1 1.1 0 1 0 0 2.2h7.5a1.1 1.1 0 1 0 0-2.2Zm11.3 1.9a1.1 1.1 0 0 1-1.5 0l-1.7-1.7a4.1 4.1 0 1 1 1.6-1.6l1.6 1.7a1.1 1.1 0 0 1 0 1.6Zm-5.3-3.4a1.9 1.9 0 1 0 0-3.8 1.9 1.9 0 0 0 0 3.8Z\"/></svg>";
init(canvas: ShadowRoot, eventTarget: import("../../helpers.js").ToolbarAppEventTarget): Promise<void>;
};
export default _default;

View File

@@ -0,0 +1,172 @@
import { settings } from "../../settings.js";
import { positionHighlight } from "../utils/highlight.js";
import { closeOnOutsideClick } from "../utils/window.js";
import { rulesCategories } from "./rules/index.js";
import { DevToolbarAuditListItem } from "./ui/audit-list-item.js";
import { DevToolbarAuditListWindow } from "./ui/audit-list-window.js";
import { createAuditUI } from "./ui/audit-ui.js";
const icon = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 1 20 16"><path fill="#fff" d="M.6 2A1.1 1.1 0 0 1 1.7.9h16.6a1.1 1.1 0 1 1 0 2.2H1.6A1.1 1.1 0 0 1 .8 2Zm1.1 7.1h6a1.1 1.1 0 0 0 0-2.2h-6a1.1 1.1 0 0 0 0 2.2ZM9.3 13H1.8a1.1 1.1 0 1 0 0 2.2h7.5a1.1 1.1 0 1 0 0-2.2Zm11.3 1.9a1.1 1.1 0 0 1-1.5 0l-1.7-1.7a4.1 4.1 0 1 1 1.6-1.6l1.6 1.7a1.1 1.1 0 0 1 0 1.6Zm-5.3-3.4a1.9 1.9 0 1 0 0-3.8 1.9 1.9 0 0 0 0 3.8Z"/></svg>';
try {
customElements.define("astro-dev-toolbar-audit-window", DevToolbarAuditListWindow);
customElements.define("astro-dev-toolbar-audit-list-item", DevToolbarAuditListItem);
} catch {
}
let showState = false;
var audit_default = {
id: "astro:audit",
name: "Audit",
icon,
async init(canvas, eventTarget) {
let audits = [];
let auditWindow = document.createElement(
"astro-dev-toolbar-audit-window"
);
let hasCreatedUI = false;
canvas.appendChild(auditWindow);
await lint();
let mutationDebounce;
const observer = new MutationObserver(() => {
if (mutationDebounce) {
clearTimeout(mutationDebounce);
}
mutationDebounce = setTimeout(() => {
settings.logger.verboseLog("Rerunning audit lints because the DOM has been updated.");
if ("requestIdleCallback" in window) {
window.requestIdleCallback(
async () => {
lint().then(() => {
if (showState) createAuditsUI();
});
},
{ timeout: 300 }
);
} else {
setTimeout(async () => {
lint().then(() => {
if (showState) createAuditsUI();
});
}, 150);
}
}, 250);
});
setupObserver();
document.addEventListener("astro:before-preparation", () => {
observer.disconnect();
});
document.addEventListener("astro:after-swap", async () => {
lint();
});
document.addEventListener("astro:page-load", async () => {
refreshLintPositions();
setTimeout(() => {
setupObserver();
}, 100);
});
eventTarget.addEventListener("app-toggled", (event) => {
if (event.detail.state === true) {
showState = true;
createAuditsUI();
} else {
showState = false;
}
});
closeOnOutsideClick(eventTarget, () => {
const activeAudits = audits.filter((audit) => audit.card?.hasAttribute("active"));
if (activeAudits.length > 0) {
activeAudits.forEach((audit) => {
audit.card?.toggleAttribute("active", false);
});
return true;
}
return false;
});
async function createAuditsUI() {
if (hasCreatedUI) return;
const fragment = document.createDocumentFragment();
for (const audit of audits) {
const { card, highlight } = createAuditUI(audit, audits);
audit.card = card;
audit.highlight = highlight;
fragment.appendChild(highlight);
}
auditWindow.audits = audits;
canvas.appendChild(fragment);
hasCreatedUI = true;
}
async function lint() {
if (audits.length > 0) {
audits.forEach((audit) => {
audit.highlight?.remove();
audit.card?.remove();
});
audits = [];
hasCreatedUI = false;
}
const selectorCache = /* @__PURE__ */ new Map();
for (const ruleCategory of rulesCategories) {
for (const rule of ruleCategory.rules) {
const elements = selectorCache.get(rule.selector) ?? document.querySelectorAll(rule.selector);
let matches = [];
if (typeof rule.match === "undefined") {
matches = Array.from(elements);
} else {
for (const element of elements) {
try {
if (await rule.match(element)) {
matches.push(element);
}
} catch (e) {
settings.logger.error(`Error while running audit's match function: ${e}`);
}
}
}
for (const element of matches) {
if (audits.some((audit) => audit.auditedElement === element)) continue;
await createAuditProblem(rule, element);
}
}
}
eventTarget.dispatchEvent(
new CustomEvent("toggle-notification", {
detail: {
state: audits.length > 0
}
})
);
}
async function createAuditProblem(rule, originalElement) {
const computedStyle = window.getComputedStyle(originalElement);
const targetedElement = originalElement.children[0] || originalElement;
if (targetedElement.offsetParent === null || computedStyle.display === "none") {
return;
}
if (originalElement.nodeName === "IMG" && !originalElement.complete) {
return;
}
audits.push({
auditedElement: originalElement,
rule,
card: null,
highlight: null
});
}
function refreshLintPositions() {
audits.forEach(({ highlight, auditedElement }) => {
const rect = auditedElement.getBoundingClientRect();
if (highlight) positionHighlight(highlight, rect);
});
}
["scroll", "resize"].forEach((event) => {
window.addEventListener(event, refreshLintPositions);
});
function setupObserver() {
observer.observe(document.body, {
childList: true,
subtree: true
});
}
}
};
export {
audit_default as default
};

View File

@@ -0,0 +1,26 @@
/**
* https://github.com/sveltejs/svelte/blob/61e5e53eee82e895c1a5b4fd36efb87eafa1fc2d/LICENSE.md
* @license MIT
*
* Copyright (c) 2016-23 [these people](https://github.com/sveltejs/svelte/graphs/contributors)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import type { AuditRuleWithSelector } from './index.js';
export declare const a11y: AuditRuleWithSelector[];

View File

@@ -0,0 +1,592 @@
/**
* https://github.com/sveltejs/svelte/blob/61e5e53eee82e895c1a5b4fd36efb87eafa1fc2d/LICENSE.md
* @license MIT
*
* Copyright (c) 2016-23 [these people](https://github.com/sveltejs/svelte/graphs/contributors)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { aria, roles } from "aria-query";
import { AXObjectRoles, elementAXObjects } from "axobject-query";
const WHITESPACE_REGEX = /\s+/;
const a11y_required_attributes = {
a: ["href"],
area: ["alt", "aria-label", "aria-labelledby"],
// html-has-lang
html: ["lang"],
// iframe-has-title
iframe: ["title"],
img: ["alt"],
object: ["title", "aria-label", "aria-labelledby"]
};
const MAYBE_INTERACTIVE = /* @__PURE__ */ new Map([
["a", "href"],
["input", "type"],
["audio", "controls"],
["img", "usemap"],
["object", "usemap"],
["video", "controls"]
]);
const interactiveElements = [
"button",
"details",
"embed",
"iframe",
"label",
"select",
"textarea",
...MAYBE_INTERACTIVE.keys()
];
const labellableElements = ["button", "input", "meter", "output", "progress", "select", "textarea"];
const aria_non_interactive_roles = [
"alert",
"alertdialog",
"application",
"article",
"banner",
"cell",
"columnheader",
"complementary",
"contentinfo",
"definition",
"dialog",
"directory",
"document",
"feed",
"figure",
"form",
"group",
"heading",
"img",
"list",
"listitem",
"log",
"main",
"marquee",
"math",
"menuitemradio",
"navigation",
"none",
"note",
"presentation",
"region",
"row",
"rowgroup",
"rowheader",
"search",
"status",
"tabpanel",
"term",
"timer",
"toolbar",
"tooltip"
];
const roleless_elements = ["div", "span"];
const a11y_required_content = [
// anchor-has-content
"a",
// heading-has-content
"h1",
"h2",
"h3",
"h4",
"h5",
"h6"
];
const a11y_distracting_elements = ["blink", "marquee"];
const a11y_implicit_semantics = /* @__PURE__ */ new Map([
["a", "link"],
["area", "link"],
["article", "article"],
["aside", "complementary"],
["body", "document"],
["button", "button"],
["datalist", "listbox"],
["dd", "definition"],
["dfn", "term"],
["dialog", "dialog"],
["details", "group"],
["dt", "term"],
["fieldset", "group"],
["figure", "figure"],
["form", "form"],
["h1", "heading"],
["h2", "heading"],
["h3", "heading"],
["h4", "heading"],
["h5", "heading"],
["h6", "heading"],
["hr", "separator"],
["img", "img"],
["li", "listitem"],
["link", "link"],
["main", "main"],
["menu", "list"],
["meter", "progressbar"],
["nav", "navigation"],
["ol", "list"],
["option", "option"],
["optgroup", "group"],
["output", "status"],
["progress", "progressbar"],
["section", "region"],
["summary", "button"],
["table", "table"],
["tbody", "rowgroup"],
["textarea", "textbox"],
["tfoot", "rowgroup"],
["thead", "rowgroup"],
["tr", "row"],
["ul", "list"]
]);
const menuitem_type_to_implicit_role = /* @__PURE__ */ new Map([
["command", "menuitem"],
["checkbox", "menuitemcheckbox"],
["radio", "menuitemradio"]
]);
const input_type_to_implicit_role = /* @__PURE__ */ new Map([
["button", "button"],
["image", "button"],
["reset", "button"],
["submit", "button"],
["checkbox", "checkbox"],
["radio", "radio"],
["range", "slider"],
["number", "spinbutton"],
["email", "textbox"],
["search", "searchbox"],
["tel", "textbox"],
["text", "textbox"],
["url", "textbox"]
]);
const ariaAttributes = new Set(
"activedescendant atomic autocomplete busy checked colcount colindex colspan controls current describedby description details disabled dropeffect errormessage expanded flowto grabbed haspopup hidden invalid keyshortcuts label labelledby level live modal multiline multiselectable orientation owns placeholder posinset pressed readonly relevant required roledescription rowcount rowindex rowspan selected setsize sort valuemax valuemin valuenow valuetext".split(
" "
)
);
const ariaRoles = new Set(
"alert alertdialog application article banner button cell checkbox columnheader combobox complementary contentinfo definition dialog directory document feed figure form grid gridcell group heading img link list listbox listitem log main marquee math menu menubar menuitem menuitemcheckbox menuitemradio navigation none note option presentation progressbar radio radiogroup region row rowgroup rowheader scrollbar search searchbox separator slider spinbutton status switch tab tablist tabpanel textbox timer toolbar tooltip tree treegrid treeitem".split(
" "
)
);
function isInteractive(element) {
const attribute = MAYBE_INTERACTIVE.get(element.localName);
if (attribute) {
return element.hasAttribute(attribute);
}
return true;
}
const a11y = [
{
code: "a11y-accesskey",
title: "Avoid using `accesskey`",
message: "The `accesskey` attribute can cause accessibility issues. The shortcuts can conflict with the browser's or operating system's shortcuts, and they are difficult for users to discover and use.",
selector: "[accesskey]"
},
{
code: "a11y-aria-activedescendant-has-tabindex",
title: "Elements with attribute `aria-activedescendant` must be tabbable",
message: "Element with the `aria-activedescendant` attribute must either have an inherent `tabindex` or declare `tabindex` as an attribute.",
selector: "[aria-activedescendant]",
match(element) {
if (!element.tabIndex && !element.hasAttribute("tabindex")) return true;
}
},
{
code: "a11y-aria-attributes",
title: "Element does not support ARIA roles.",
message: "Elements like `meta`, `html`, `script`, `style` do not support having ARIA roles.",
selector: ":is(meta, html, script, style)[role]",
match(element) {
for (const attribute of element.attributes) {
if (attribute.name.startsWith("aria-")) return true;
}
}
},
{
code: "a11y-autofocus",
title: "Avoid using `autofocus`",
message: "The `autofocus` attribute can cause accessibility issues, as it can cause the focus to move around unexpectedly for screen reader users.",
selector: "[autofocus]"
},
{
code: "a11y-distracting-elements",
title: "Distracting elements should not be used",
message: "Elements that can be visually distracting like `<marquee>` or `<blink>` can cause accessibility issues for visually impaired users and should be avoided.",
selector: `:is(${a11y_distracting_elements.join(",")})`
},
{
code: "a11y-hidden",
title: "Certain DOM elements are useful for screen reader navigation and should not be hidden",
message: (element) => `${element.localName} element should not be hidden.`,
selector: "[aria-hidden]:is(h1,h2,h3,h4,h5,h6)"
},
{
code: "a11y-img-redundant-alt",
title: "Redundant text in alt attribute",
message: 'Screen readers already announce `img` elements as an image. There is no need to use words such as "image", "photo", and/or "picture".',
selector: "img[alt]:not([aria-hidden])",
match: (img) => /\b(?:image|picture|photo)\b/i.test(img.alt)
},
{
code: "a11y-incorrect-aria-attribute-type",
title: "Incorrect value for ARIA attribute.",
message: "`aria-hidden` should only receive a boolean.",
selector: "[aria-hidden]",
match(element) {
const value = element.getAttribute("aria-hidden");
if (!value) return true;
if (!["true", "false"].includes(value)) return true;
}
},
{
code: "a11y-invalid-href",
title: "Invalid `href` attribute",
message: "`href` should not be empty, `'#'`, or `javascript:`.",
selector: 'a[href]:is([href=""], [href="#"], [href^="javascript:" i])'
},
{
code: "a11y-invalid-label",
title: "`label` element should have an associated control and a text content.",
message: "The `label` element must be associated with a control either by using the `for` attribute or by containing a nested form element. Additionally, the `label` element must have text content.",
selector: "label",
match(element) {
const hasFor = element.hasAttribute("for");
const nestedLabellableElement = element.querySelector(`${labellableElements.join(", ")}`);
if (!hasFor && !nestedLabellableElement) return true;
const innerText = element.innerText.trim();
if (innerText === "") return true;
}
},
{
code: "a11y-media-has-caption",
title: "Unmuted video elements should have captions",
message: "Videos without captions can be difficult for deaf and hard-of-hearing users to follow along with. If the video does not need captions, add the `muted` attribute.",
selector: "video:not([muted])",
match(element) {
const tracks = element.querySelectorAll("track");
if (!tracks.length) return true;
const hasCaptionTrack = Array.from(tracks).some(
(track) => track.getAttribute("kind") === "captions"
);
return !hasCaptionTrack;
}
},
{
code: "a11y-misplaced-scope",
title: "The `scope` attribute should only be used on `<th>` elements",
message: "The `scope` attribute tells the browser and screen readers how to navigate tables. In HTML5, it should only be used on `<th>` elements.",
selector: ":not(th)[scope]"
},
{
code: "a11y-missing-attribute",
title: "Required attributes missing.",
description: "Some HTML elements require additional attributes for accessibility. For example, an `img` element requires an `alt` attribute, this attribute is used to describe the content of the image for screen readers.",
message: (element) => {
const requiredAttributes = a11y_required_attributes[element.localName];
const missingAttributes = requiredAttributes.filter(
(attribute) => !element.hasAttribute(attribute)
);
return `${element.localName} element is missing required attributes for accessibility: ${missingAttributes.join(", ")} `;
},
selector: Object.keys(a11y_required_attributes).join(","),
match(element) {
const requiredAttributes = a11y_required_attributes[element.localName];
if (!requiredAttributes) return true;
for (const attribute of requiredAttributes) {
if (!element.hasAttribute(attribute)) return true;
}
return false;
}
},
{
code: "a11y-missing-content",
title: "Missing content",
message: "Headings and anchors must have an accessible name, which can come from: inner text, aria-label, aria-labelledby, an img with alt property, or an svg with a tag <title></title>.",
selector: a11y_required_content.join(","),
match(element) {
const innerText = element.innerText?.trim();
if (innerText && innerText !== "") return false;
const ariaLabel = element.getAttribute("aria-label")?.trim();
if (ariaLabel && ariaLabel !== "") return false;
const ariaLabelledby = element.getAttribute("aria-labelledby")?.trim();
if (ariaLabelledby) {
const ids = ariaLabelledby.split(" ");
for (const id of ids) {
const referencedElement = document.getElementById(id);
if (referencedElement && referencedElement.innerText.trim() !== "") return false;
}
}
const imgElements = element.querySelectorAll("img");
for (const img of imgElements) {
const altAttribute = img.getAttribute("alt");
if (altAttribute && altAttribute.trim() !== "") return false;
}
const svgElements = element.querySelectorAll("svg");
for (const svg of svgElements) {
const titleText = svg.querySelector("title");
if (titleText && titleText.textContent && titleText.textContent.trim() !== "") return false;
}
const inputElements = element.querySelectorAll("input");
for (const input of inputElements) {
if (input.type === "image") {
const altAttribute = input.getAttribute("alt");
if (altAttribute && altAttribute.trim() !== "") return false;
}
const inputAriaLabel = input.getAttribute("aria-label")?.trim();
if (inputAriaLabel && inputAriaLabel !== "") return false;
const inputAriaLabelledby = input.getAttribute("aria-labelledby")?.trim();
if (inputAriaLabelledby) {
const ids = inputAriaLabelledby.split(" ");
for (const id of ids) {
const referencedElement = document.getElementById(id);
if (referencedElement && referencedElement.innerText.trim() !== "") return false;
}
}
const title = input.getAttribute("title")?.trim();
if (title && title !== "") return false;
}
return true;
}
},
{
code: "a11y-no-redundant-roles",
title: "HTML element has redundant ARIA roles",
message: "Giving these elements an ARIA role that is already set by the browser has no effect and is redundant.",
selector: [...a11y_implicit_semantics.keys()].join(","),
match(element) {
const role = element.getAttribute("role");
if (element.localName === "input") {
const type = element.getAttribute("type");
if (!type) return true;
const implicitRoleForType = input_type_to_implicit_role.get(type);
if (!implicitRoleForType) return true;
if (role === implicitRoleForType) return false;
}
const implicitRole = a11y_implicit_semantics.get(element.localName);
if (!implicitRole) return true;
if (role === implicitRole) return false;
}
},
{
code: "a11y-no-interactive-element-to-noninteractive-role",
title: "Non-interactive ARIA role used on interactive HTML element.",
message: "Interactive HTML elements like `<a>` and `<button>` cannot use non-interactive roles like `heading`, `list`, `menu`, and `toolbar`.",
selector: `[role]:is(${interactiveElements.join(",")})`,
match(element) {
if (!isInteractive(element)) return false;
const role = element.getAttribute("role");
if (!role) return false;
if (!ariaRoles.has(role)) return false;
if (roleless_elements.includes(element.localName)) return false;
if (aria_non_interactive_roles.includes(role)) return true;
}
},
{
code: "a11y-no-noninteractive-element-to-interactive-role",
title: "Interactive ARIA role used on non-interactive HTML element.",
message: "Interactive roles should not be used to convert a non-interactive element to an interactive element",
selector: `[role]:not(${interactiveElements.join(",")})`,
match(element) {
if (!isInteractive(element)) return false;
const role = element.getAttribute("role");
if (!role) return false;
if (!ariaRoles.has(role)) return false;
const exceptions = a11y_non_interactive_element_to_interactive_role_exceptions[element.localName];
if (exceptions?.includes(role)) return false;
if (roleless_elements.includes(element.localName)) return false;
if (!aria_non_interactive_roles.includes(role)) return true;
}
},
{
code: "a11y-no-noninteractive-tabindex",
title: "Invalid `tabindex` on non-interactive element",
description: 'The `tabindex` attribute should only be used on interactive elements, as it can be confusing for keyboard-only users to navigate through non-interactive elements. If your element is only conditionally interactive, consider using `tabindex="-1"` to make it focusable only when it is actually interactive.',
message: (element) => `${element.localName} elements should not have \`tabindex\` attribute`,
selector: '[tabindex]:not([role="tabpanel"])',
match(element) {
const isScrollable = element.scrollHeight > element.clientHeight || element.scrollWidth > element.clientWidth;
if (isScrollable) return false;
if (!isInteractive(element)) return false;
if (!interactiveElements.includes(element.localName) && !roleless_elements.includes(element.localName))
return true;
}
},
{
code: "a11y-positive-tabindex",
title: "Avoid positive `tabindex` property values",
message: "This will move elements out of the expected tab order, creating a confusing experience for keyboard users.",
selector: '[tabindex]:not([tabindex="-1"]):not([tabindex="0"])'
},
{
code: "a11y-role-has-required-aria-props",
title: "Missing attributes required for ARIA role",
message: (element) => {
const { __astro_role: role, __astro_missing_attributes: required } = element;
return `${element.localName} element is missing required attributes for its role (${role}): ${required.join(", ")}`;
},
selector: "*",
match(element) {
const role = getRole(element);
if (!role) return false;
if (is_semantic_role_element(role, element.localName, getAttributeObject(element))) {
return;
}
const elementRoles = role.split(WHITESPACE_REGEX);
for (const elementRole of elementRoles) {
const { requiredProps } = roles.get(elementRole);
const required_role_props = Object.keys(requiredProps);
const missingProps = required_role_props.filter((prop) => !element.hasAttribute(prop));
if (missingProps.length > 0) {
element.__astro_role = elementRole;
element.__astro_missing_attributes = missingProps;
return true;
}
}
}
},
{
code: "a11y-role-supports-aria-props",
title: "Unsupported ARIA attribute",
message: (element) => {
const { __astro_role: role, __astro_unsupported_attributes: unsupported } = element;
return `${element.localName} element has ARIA attributes that are not supported by its role (${role}): ${unsupported.join(
", "
)}`;
},
selector: "*",
match(element) {
const role = getRole(element);
if (!role) return false;
const elementRoles = role.split(WHITESPACE_REGEX);
for (const elementRole of elementRoles) {
const { props } = roles.get(elementRole);
const attributes = getAttributeObject(element);
const unsupportedAttributes = aria.keys().filter((attribute) => !(attribute in props));
const invalidAttributes = Object.keys(attributes).filter(
(key) => key.startsWith("aria-") && unsupportedAttributes.includes(key)
);
if (invalidAttributes.length > 0) {
element.__astro_role = elementRole;
element.__astro_unsupported_attributes = invalidAttributes;
return true;
}
}
}
},
{
code: "a11y-structure",
title: "Invalid DOM structure",
message: "The DOM structure must be valid for accessibility of the page, for example `figcaption` must be a direct child of `figure`.",
selector: "figcaption:not(figure > figcaption)"
},
{
code: "a11y-unknown-aria-attribute",
title: "Unknown ARIA attribute",
message: "ARIA attributes prefixed with `aria-` must be valid, non-abstract ARIA attributes.",
selector: "*",
match(element) {
for (const attribute of element.attributes) {
if (attribute.name.startsWith("aria-")) {
if (!ariaAttributes.has(attribute.name.slice("aria-".length))) return true;
}
}
}
},
{
code: "a11y-unknown-role",
title: "Unknown ARIA role",
message: "ARIA roles must be valid, non-abstract ARIA roles.",
selector: "[role]",
match(element) {
const role = element.getAttribute("role");
if (!role) return true;
if (!ariaRoles.has(role)) return true;
}
}
];
const a11y_non_interactive_element_to_interactive_role_exceptions = {
ul: ["listbox", "menu", "menubar", "radiogroup", "tablist", "tree", "treegrid"],
ol: ["listbox", "menu", "menubar", "radiogroup", "tablist", "tree", "treegrid"],
li: ["menuitem", "option", "row", "tab", "treeitem"],
table: ["grid"],
td: ["gridcell"],
fieldset: ["radiogroup", "presentation"]
};
const combobox_if_list = ["email", "search", "tel", "text", "url"];
function input_implicit_role(attributes) {
if (!("type" in attributes)) return;
const { type, list } = attributes;
if (!type) return;
if (list && combobox_if_list.includes(type)) {
return "combobox";
}
return input_type_to_implicit_role.get(type);
}
function menuitem_implicit_role(attributes) {
if (!("type" in attributes)) return;
const { type } = attributes;
if (!type) return;
return menuitem_type_to_implicit_role.get(type);
}
function getRole(element) {
if (element.hasAttribute("role")) {
return element.getAttribute("role");
}
return getImplicitRole(element);
}
function getImplicitRole(element) {
const name = element.localName;
const attrs = getAttributeObject(element);
if (name === "menuitem") {
return menuitem_implicit_role(attrs);
} else if (name === "input") {
return input_implicit_role(attrs);
} else {
return a11y_implicit_semantics.get(name);
}
}
function getAttributeObject(element) {
let obj = {};
for (let i = 0; i < element.attributes.length; i++) {
const attribute = element.attributes.item(i);
obj[attribute.name] = attribute.value;
}
return obj;
}
function is_semantic_role_element(role, tag_name, attributes) {
for (const [schema, ax_object] of elementAXObjects.entries()) {
if (schema.name === tag_name && (!schema.attributes || schema.attributes.every((attr) => attributes[attr.name] === attr.value))) {
for (const name of ax_object) {
const axRoles = AXObjectRoles.get(name);
if (axRoles) {
for (const { name: _name } of axRoles) {
if (_name === role) {
return true;
}
}
}
}
}
}
return false;
}
export {
a11y
};

View File

@@ -0,0 +1,35 @@
type DynamicString = string | ((element: Element) => string);
export interface AuditRule {
code: string;
title: DynamicString;
message: DynamicString;
description?: DynamicString;
}
export interface ResolvedAuditRule {
code: string;
title: string;
message: string;
description?: string;
}
export interface AuditRuleWithSelector extends AuditRule {
selector: string;
match?: (element: Element) => boolean | null | undefined | void | Promise<boolean> | Promise<void> | Promise<null> | Promise<undefined>;
}
export declare const rulesCategories: ({
code: string;
name: string;
icon: "person-arms-spread";
rules: AuditRuleWithSelector[];
} | {
code: string;
name: string;
icon: "gauge";
rules: AuditRuleWithSelector[];
})[];
export declare function resolveAuditRule(rule: AuditRule, element: Element): ResolvedAuditRule;
export declare function getAuditCategory(rule: AuditRule): 'perf' | 'a11y';
export declare const categoryLabel: {
perf: string;
a11y: string;
};
export {};

View File

@@ -0,0 +1,39 @@
import { settings } from "../../../settings.js";
import { a11y } from "./a11y.js";
import { perf } from "./perf.js";
const rulesCategories = [
{ code: "a11y", name: "Accessibility", icon: "person-arms-spread", rules: a11y },
{ code: "perf", name: "Performance", icon: "gauge", rules: perf }
];
const dynamicAuditRuleKeys = ["title", "message", "description"];
function resolveAuditRule(rule, element) {
let resolved = { ...rule };
for (const key of dynamicAuditRuleKeys) {
const value = rule[key];
if (typeof value === "string") continue;
try {
if (!value) {
resolved[key] = "";
continue;
}
resolved[key] = value(element);
} catch (err) {
settings.logger.error(`Error resolving dynamic audit rule ${rule.code}'s ${key}: ${err}`);
resolved[key] = "Error resolving dynamic rule";
}
}
return resolved;
}
function getAuditCategory(rule) {
return rule.code.split("-")[0];
}
const categoryLabel = {
perf: "performance",
a11y: "accessibility"
};
export {
categoryLabel,
getAuditCategory,
resolveAuditRule,
rulesCategories
};

View File

@@ -0,0 +1,2 @@
import type { AuditRuleWithSelector } from './index.js';
export declare const perf: AuditRuleWithSelector[];

View File

@@ -0,0 +1,100 @@
const EXTERNAL_URL_REGEX = /^(?:[a-z+]+:)?\/\//i;
const perf = [
{
code: "perf-use-image-component",
title: "Use the Image component",
message: "This image could be replaced with the Image component to improve performance.",
selector: "img:not([data-image-component])",
async match(element) {
const src = element.getAttribute("src");
if (!src) return false;
if (src.startsWith("data:")) return false;
if (!EXTERNAL_URL_REGEX.test(src)) {
const imageData = await fetch(src).then((response) => response.blob());
if (imageData.size < 20480) return false;
}
return true;
}
},
{
code: "perf-use-loading-lazy",
title: "Unoptimized loading attribute",
message: (element) => `This ${element.nodeName} tag is below the fold and could be lazy-loaded to improve performance.`,
selector: 'img:not([loading]), img[loading="eager"], iframe:not([loading]), iframe[loading="eager"]',
match(element) {
const htmlElement = element;
const elementYPosition = htmlElement.getBoundingClientRect().y + window.scrollY;
if (elementYPosition < window.innerHeight) return false;
if (htmlElement.src.startsWith("data:")) return false;
return true;
}
},
{
code: "perf-use-loading-eager",
title: "Unoptimized loading attribute",
message: (element) => `This ${element.nodeName} tag is above the fold and could be eagerly-loaded to improve performance.`,
selector: 'img[loading="lazy"], iframe[loading="lazy"]',
match(element) {
const htmlElement = element;
const elementYPosition = htmlElement.getBoundingClientRect().y + window.scrollY;
if (elementYPosition > window.innerHeight) return false;
if (htmlElement.src.startsWith("data:")) return false;
return true;
}
},
{
code: "perf-use-videos",
title: "Use videos instead of GIFs for large animations",
message: "This GIF could be replaced with a video to reduce its file size and improve performance.",
selector: 'img[src$=".gif"]',
async match(element) {
const src = element.getAttribute("src");
if (!src) return false;
if (EXTERNAL_URL_REGEX.test(src)) return false;
if (!EXTERNAL_URL_REGEX.test(src)) {
const imageData = await fetch(src).then((response) => response.blob());
if (imageData.size < 102400) return false;
}
return true;
}
},
{
code: "perf-slow-component-server-render",
title: "Server-rendered component took a long time to render",
message: (element) => `This component took an unusually long time to render on the server (${getCleanRenderingTime(
element.getAttribute("server-render-time")
)}). This might be a sign that it's doing too much work on the server, or something is blocking rendering.`,
selector: "astro-island[server-render-time]",
match(element) {
const serverRenderTime = element.getAttribute("server-render-time");
if (!serverRenderTime) return false;
const renderingTime = parseFloat(serverRenderTime);
if (Number.isNaN(renderingTime)) return false;
return renderingTime > 500;
}
},
{
code: "perf-slow-component-client-hydration",
title: "Client-rendered component took a long time to hydrate",
message: (element) => `This component took an unusually long time to render on the server (${getCleanRenderingTime(
element.getAttribute("client-render-time")
)}). This could be a sign that something is blocking the main thread and preventing the component from hydrating quickly.`,
selector: "astro-island[client-render-time]",
match(element) {
const clientRenderTime = element.getAttribute("client-render-time");
if (!clientRenderTime) return false;
const renderingTime = parseFloat(clientRenderTime);
if (Number.isNaN(renderingTime)) return false;
return renderingTime > 500;
}
}
];
function getCleanRenderingTime(time) {
if (!time) return "unknown";
const renderingTime = parseFloat(time);
if (Number.isNaN(renderingTime)) return "unknown";
return renderingTime.toFixed(2) + "s";
}
export {
perf
};

View File

@@ -0,0 +1,7 @@
export declare class DevToolbarAuditListItem extends HTMLElement {
clickAction?: () => void | (() => Promise<void>);
shadowRoot: ShadowRoot;
isManualFocus: boolean;
constructor();
connectedCallback(): void;
}

View File

@@ -0,0 +1,144 @@
class DevToolbarAuditListItem extends HTMLElement {
clickAction;
shadowRoot;
isManualFocus;
constructor() {
super();
this.shadowRoot = this.attachShadow({ mode: "open" });
this.isManualFocus = false;
this.shadowRoot.innerHTML = `
<style>
:host>button, :host>div {
box-sizing: border-box;
padding: 16px;
background: transparent;
border: none;
border-bottom: 1px solid #1F2433;
text-decoration: none;
width: 100%;
height: 100%;
}
h1, h2, h3, h4, h5, h6 {
color: #fff;
font-weight: 600;
}
:host>button:hover, :host([hovered])>button {
background: #FFFFFF20;
}
svg {
display: block;
margin: 0 auto;
}
:host>button#astro-overlay-card {
text-align: left;
box-shadow: none;
display: flex;
align-items: center;
overflow: hidden;
gap: 8px;
}
:host(:not([active]))>button:hover {
cursor: pointer;
}
.extended-info {
display: none;
color: white;
font-size: 14px;
}
.extended-info hr {
border: 1px solid rgba(27, 30, 36, 1);
}
:host([active]) .extended-info {
display: block;
position: absolute;
height: 100%;
top: 98px;
height: calc(100% - 98px);
background: #0d0e12;
user-select: text;
overflow: auto;
border: none;
z-index: 1000000000;
flex-direction: column;
line-height: 1.25rem;
}
:host([active])>button#astro-overlay-card {
display: none;
}
.audit-title {
margin: 0;
margin-bottom: 4px;
}
.extended-info .audit-selector {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
display: flex;
align-items: center;
border-bottom: 1px solid transparent;
user-select: none;
color: rgba(191, 193, 201, 1);
}
.extended-info .audit-selector:hover {
border-bottom: 1px solid rgba(255, 255, 255);
cursor: pointer;
color: #fff;
}
.audit-selector svg {
width: 16px;
height: 16px;
display: inline;
}
.extended-info .audit-description {
color: rgba(191, 193, 201, 1);
}
.extended-info code {
padding: 1px 3px;
border-radius: 3px;
background: #1F2433;
}
.reset-button {
text-align: left;
border: none;
margin: 0;
width: auto;
overflow: visible;
background: transparent;
font: inherit;
line-height: normal;
-webkit-font-smoothing: inherit;
-moz-osx-font-smoothing: inherit;
-webkit-appearance: none;
padding: 0;
color: white;
}
</style>
<button id="astro-overlay-card">
<slot />
</button>
`;
}
connectedCallback() {
if (this.clickAction) {
this.shadowRoot.getElementById("astro-overlay-card")?.addEventListener("click", this.clickAction);
}
}
}
export {
DevToolbarAuditListItem
};

View File

@@ -0,0 +1,23 @@
import type { Icon } from '../../../ui-library/icons.js';
import type { Audit } from '../index.js';
export declare function createRoundedBadge(icon: Icon): {
badge: import("../../../ui-library/badge.js").DevToolbarBadge;
updateCount: (count: number) => void;
};
export declare class DevToolbarAuditListWindow extends HTMLElement {
_audits: Audit[];
shadowRoot: ShadowRoot;
badges: {
[key: string]: {
badge: HTMLElement;
updateCount: (count: number) => void;
};
};
get audits(): Audit[];
set audits(value: Audit[]);
constructor();
connectedCallback(): void;
updateAuditList(): void;
updateBadgeCounts(): void;
render(): void;
}

View File

@@ -0,0 +1,382 @@
import { getAuditCategory, rulesCategories } from "../rules/index.js";
function createRoundedBadge(icon) {
const badge = document.createElement("astro-dev-toolbar-badge");
badge.shadowRoot.innerHTML += `
<style>
:host>div {
padding: 12px 8px;
font-size: 14px;
display: flex;
gap: 4px;
}
</style>
`;
badge.innerHTML = `<astro-dev-toolbar-icon icon="${icon}"></astro-dev-toolbar-icon>0`;
return {
badge,
updateCount: (count) => {
if (count === 0) {
badge.badgeStyle = "green";
} else {
badge.badgeStyle = "purple";
}
badge.innerHTML = `<astro-dev-toolbar-icon icon="${icon}"></astro-dev-toolbar-icon>${count}`;
}
};
}
class DevToolbarAuditListWindow extends HTMLElement {
_audits = [];
shadowRoot;
badges = {};
get audits() {
return this._audits;
}
set audits(value) {
this._audits = value;
this.render();
}
constructor() {
super();
this.shadowRoot = this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `<style>
:host {
box-sizing: border-box;
display: flex;
flex-direction: column;
background: linear-gradient(0deg, #13151a, #13151a), linear-gradient(0deg, #343841, #343841);
border: 1px solid rgba(52, 56, 65, 1);
width: min(640px, 100%);
max-height: 480px;
border-radius: 12px;
padding: 24px;
font-family:
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
"Helvetica Neue",
Arial,
"Noto Sans",
sans-serif,
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol",
"Noto Color Emoji";
color: rgba(191, 193, 201, 1);
position: fixed;
z-index: 999999999;
bottom: 72px;
left: 50%;
transform: translateX(-50%);
box-shadow:
0px 0px 0px 0px rgba(19, 21, 26, 0.3),
0px 1px 2px 0px rgba(19, 21, 26, 0.29),
0px 4px 4px 0px rgba(19, 21, 26, 0.26),
0px 10px 6px 0px rgba(19, 21, 26, 0.15),
0px 17px 7px 0px rgba(19, 21, 26, 0.04),
0px 26px 7px 0px rgba(19, 21, 26, 0.01);
}
@media (forced-colors: active) {
:host {
background: white;
}
}
@media (max-width: 640px) {
:host {
border-radius: 0;
}
}
hr,
::slotted(hr) {
border: 1px solid rgba(27, 30, 36, 1);
margin: 1em 0;
}
.reset-button {
text-align: left;
border: none;
margin: 0;
width: auto;
overflow: visible;
background: transparent;
font: inherit;
line-height: normal;
-webkit-font-smoothing: inherit;
-moz-osx-font-smoothing: inherit;
-webkit-appearance: none;
padding: 0;
}
:host {
left: initial;
top: 8px;
right: 8px;
transform: none;
width: 350px;
min-height: 350px;
max-height: 420px;
padding: 0;
overflow: hidden;
}
hr {
margin: 0;
}
header {
display: flex;
align-items: center;
gap: 4px;
}
header > section {
display: flex;
align-items: center;
gap: 1em;
padding: 18px;
}
header.category-header {
background: rgba(27, 30, 36, 1);
padding: 10px 16px;
position: sticky;
top: 0;
}
header.category-header astro-dev-toolbar-icon {
opacity: 0.6;
}
#audit-counts {
display: flex;
gap: 0.5em;
}
#audit-counts > div {
display: flex;
gap: 8px;
align-items: center;
}
ul,
li {
margin: 0;
padding: 0;
list-style: none;
}
h1 {
font-size: 24px;
font-weight: 600;
color: #fff;
margin: 0;
}
h2 {
font-weight: 600;
margin: 0;
color: white;
font-size: 14px;
}
h3 {
font-weight: normal;
margin: 0;
color: white;
font-size: 14px;
}
.audit-header {
display: flex;
gap: 8px;
align-items: center;
}
.audit-selector {
color: white;
font-size: 12px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px;
padding: 4px 6px;
}
[active] .audit-selector:hover {
text-decoration: underline;
cursor: pointer;
}
.selector-title-container {
display: flex;
align-items: center;
gap: 8px;
}
astro-dev-toolbar-icon {
color: white;
fill: white;
display: inline-block;
height: 16px;
width: 16px;
}
#audit-list {
display: flex;
flex-direction: column;
overflow: auto;
overscroll-behavior: contain;
height: 100%;
}
#back-to-list {
display: none;
align-items: center;
justify-content: center;
background: rgba(27, 30, 36, 1);
gap: 8px;
padding: 8px;
color: white;
font-size: 14px;
padding-right: 24px;
}
#back-to-list:hover {
cursor: pointer;
background: #313236;
}
#back-to-list:has(+ #audit-list astro-dev-toolbar-audit-list-item[active]) {
display: flex;
}
.no-audit-container {
display: flex;
flex-direction: column;
padding: 24px;
}
.no-audit-container h1 {
font-size: 20px;
}
.no-audit-container astro-dev-toolbar-icon {
width: auto;
height: auto;
margin: 0 auto;
}
</style>
<template id="category-template">
<div>
<header class="category-header">
</header>
<div class="category-content"></div>
</div>
</template>
<header>
<section id="header-left">
<h1>Audit</h1>
<section id="audit-counts"></section>
</section>
</header>
<hr />
<button id="back-to-list" class="reset-button">
<astro-dev-toolbar-icon icon="arrow-left"></astro-dev-toolbar-icon>
Back to list
</button>
<div id="audit-list"></div>
`;
const auditCounts = this.shadowRoot.getElementById("audit-counts");
if (auditCounts) {
rulesCategories.forEach((category) => {
const headerEntryContainer = document.createElement("div");
const auditCount = this.audits.filter(
(audit) => getAuditCategory(audit.rule) === category.code
).length;
const categoryBadge = createRoundedBadge(category.icon);
categoryBadge.updateCount(auditCount);
headerEntryContainer.append(categoryBadge.badge);
auditCounts.append(headerEntryContainer);
this.badges[category.code] = categoryBadge;
});
}
const backToListButton = this.shadowRoot.getElementById("back-to-list");
if (backToListButton) {
backToListButton.addEventListener("click", () => {
const activeAudit = this.shadowRoot.querySelector(
"astro-dev-toolbar-audit-list-item[active]"
);
if (activeAudit) {
activeAudit.toggleAttribute("active", false);
}
});
}
}
connectedCallback() {
this.render();
}
updateAuditList() {
const auditListContainer = this.shadowRoot.getElementById("audit-list");
if (auditListContainer) {
auditListContainer.innerHTML = "";
if (this.audits.length > 0) {
for (const category of rulesCategories) {
const template = this.shadowRoot.getElementById(
"category-template"
);
if (!template) return;
const clone = document.importNode(template.content, true);
const categoryContainer = clone.querySelector("div");
const categoryHeader = clone.querySelector(".category-header");
categoryHeader.innerHTML = `<astro-dev-toolbar-icon icon="${category.icon}"></astro-dev-toolbar-icon><h2>${category.name}</h2>`;
categoryContainer.append(categoryHeader);
const categoryContent = clone.querySelector(".category-content");
const categoryAudits = this.audits.filter(
(audit) => getAuditCategory(audit.rule) === category.code
);
for (const audit of categoryAudits) {
if (audit.card) categoryContent.append(audit.card);
}
categoryContainer.append(categoryContent);
auditListContainer.append(categoryContainer);
}
} else {
const noAuditContainer = document.createElement("div");
noAuditContainer.classList.add("no-audit-container");
noAuditContainer.innerHTML = `
<header>
<h1></astro-dev-toolbar-icon>No accessibility or performance issues detected.</h1>
</header>
<p>
Nice work! This app scans the page and highlights common accessibility and performance issues for you, like a missing "alt" attribute on an image, or a image not using performant attributes.
</p>
<astro-dev-toolbar-icon icon="houston-detective"></astro-dev-toolbar-icon>
`;
auditListContainer.append(noAuditContainer);
}
}
}
updateBadgeCounts() {
for (const category of rulesCategories) {
const auditCount = this.audits.filter(
(audit) => getAuditCategory(audit.rule) === category.code
).length;
this.badges[category.code].updateCount(auditCount);
}
}
render() {
this.updateAuditList();
this.updateBadgeCounts();
}
}
export {
DevToolbarAuditListWindow,
createRoundedBadge
};

View File

@@ -0,0 +1,6 @@
import type { Audit } from '../index.js';
import type { DevToolbarAuditListItem } from './audit-list-item.js';
export declare function createAuditUI(audit: Audit, audits: Audit[]): {
highlight: import("../../../ui-library/highlight.js").DevToolbarHighlight;
card: DevToolbarAuditListItem;
};

View File

@@ -0,0 +1,129 @@
import { escape as escapeHTML } from "html-escaper";
import {
attachTooltipToHighlight,
createHighlight,
getElementsPositionInDocument
} from "../../utils/highlight.js";
import { resolveAuditRule } from "../rules/index.js";
function truncate(val, maxLength) {
return val.length > maxLength ? val.slice(0, maxLength - 1) + "&hellip;" : val;
}
function createAuditUI(audit, audits) {
const rect = audit.auditedElement.getBoundingClientRect();
const highlight = createHighlight(rect, "warning", { "data-audit-code": audit.rule.code });
const resolvedAuditRule = resolveAuditRule(audit.rule, audit.auditedElement);
const tooltip = buildAuditTooltip(resolvedAuditRule, audit.auditedElement);
const card = buildAuditCard(resolvedAuditRule, highlight, audit.auditedElement, audits);
["focus", "mouseover"].forEach((event) => {
const attribute = event === "focus" ? "active" : "hovered";
highlight.addEventListener(event, () => {
if (event === "focus") {
audits.forEach((adt) => {
if (adt.card) adt.card.toggleAttribute("active", false);
});
if (!card.isManualFocus) card.scrollIntoView();
card.toggleAttribute("active", true);
} else {
card.toggleAttribute(attribute, true);
}
});
});
highlight.addEventListener("mouseout", () => {
card.toggleAttribute("hovered", false);
});
const { isFixed } = getElementsPositionInDocument(audit.auditedElement);
if (isFixed) {
tooltip.style.position = highlight.style.position = "fixed";
}
attachTooltipToHighlight(highlight, tooltip, audit.auditedElement);
return { highlight, card };
}
function buildAuditTooltip(rule, element) {
const tooltip = document.createElement("astro-dev-toolbar-tooltip");
const { title, message } = rule;
tooltip.sections = [
{
icon: "warning",
title: escapeHTML(title)
},
{
content: escapeHTML(message)
}
];
const elementFile = element.getAttribute("data-astro-source-file");
const elementPosition = element.getAttribute("data-astro-source-loc");
if (elementFile) {
const elementFileWithPosition = elementFile + (elementPosition ? ":" + elementPosition : "");
tooltip.sections.push({
content: elementFileWithPosition.slice(
window.__astro_dev_toolbar__.root.length - 1
// We want to keep the final slash, so minus one.
),
clickDescription: "Click to go to file",
async clickAction() {
await fetch("/__open-in-editor?file=" + encodeURIComponent(elementFileWithPosition));
}
});
}
return tooltip;
}
function buildAuditCard(rule, highlightElement, auditedElement, audits) {
const card = document.createElement(
"astro-dev-toolbar-audit-list-item"
);
card.clickAction = () => {
if (card.hasAttribute("active")) return;
audits.forEach((audit) => {
audit.card?.toggleAttribute("active", false);
});
highlightElement.scrollIntoView();
card.isManualFocus = true;
highlightElement.focus();
card.isManualFocus = false;
};
const selectorTitleContainer = document.createElement("section");
selectorTitleContainer.classList.add("selector-title-container");
const selector = document.createElement("span");
const selectorName = truncate(auditedElement.tagName.toLowerCase(), 8);
selector.classList.add("audit-selector");
selector.innerHTML = escapeHTML(selectorName);
const title = document.createElement("h3");
title.classList.add("audit-title");
title.innerText = rule.title;
selectorTitleContainer.append(selector, title);
card.append(selectorTitleContainer);
const extendedInfo = document.createElement("div");
extendedInfo.classList.add("extended-info");
const selectorButton = document.createElement("button");
selectorButton.className = "audit-selector reset-button";
selectorButton.innerHTML = `${selectorName} <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="currentColor" viewBox="0 0 256 256"><path d="M128,136v64a8,8,0,0,1-16,0V155.32L45.66,221.66a8,8,0,0,1-11.32-11.32L100.68,144H56a8,8,0,0,1,0-16h64A8,8,0,0,1,128,136ZM208,32H80A16,16,0,0,0,64,48V96a8,8,0,0,0,16,0V48H208V176H160a8,8,0,0,0,0,16h48a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32Z"></path></svg>`;
selectorButton.addEventListener("click", () => {
highlightElement.scrollIntoView();
highlightElement.focus();
});
extendedInfo.append(title.cloneNode(true));
extendedInfo.append(selectorButton);
extendedInfo.append(document.createElement("hr"));
const message = document.createElement("p");
message.classList.add("audit-message");
message.innerHTML = simpleRenderMarkdown(rule.message);
extendedInfo.appendChild(message);
const description = rule.description;
if (description) {
const descriptionElement = document.createElement("p");
descriptionElement.classList.add("audit-description");
descriptionElement.innerHTML = simpleRenderMarkdown(description);
extendedInfo.appendChild(descriptionElement);
}
card.shadowRoot.appendChild(extendedInfo);
return card;
}
const linkRegex = /\[([^[]+)\]\((.*)\)/g;
const boldRegex = /\*\*(.+)\*\*/g;
const codeRegex = /`([^`]+)`/g;
function simpleRenderMarkdown(markdown) {
return escapeHTML(markdown).replace(linkRegex, `<a href="$2" target="_blank">$1</a>`).replace(boldRegex, "<b>$1</b>").replace(codeRegex, "<code>$1</code>");
}
export {
createAuditUI
};

View File

@@ -0,0 +1,7 @@
declare const _default: {
id: string;
name: string;
icon: "gear";
init(canvas: ShadowRoot, eventTarget: import("../helpers.js").ToolbarAppEventTarget): void;
};
export default _default;

View File

@@ -0,0 +1,201 @@
import { settings } from "../settings.js";
import { isValidPlacement, placements } from "../ui-library/window.js";
import {
closeOnOutsideClick,
createWindowElement,
synchronizePlacementOnUpdate
} from "./utils/window.js";
const settingsRows = [
{
name: "Disable notifications",
description: "Hide notification badges in the toolbar.",
input: "checkbox",
settingKey: "disableAppNotification",
changeEvent: (evt) => {
if (evt.currentTarget instanceof HTMLInputElement) {
const devToolbar = document.querySelector("astro-dev-toolbar");
if (devToolbar) {
devToolbar.setNotificationVisible(!evt.currentTarget.checked);
}
settings.updateSetting("disableAppNotification", evt.currentTarget.checked);
const action = evt.currentTarget.checked ? "disabled" : "enabled";
settings.logger.verboseLog(`App notification badges ${action}`);
}
}
},
{
name: "Verbose logging",
description: "Logs dev toolbar events in the browser console.",
input: "checkbox",
settingKey: "verbose",
changeEvent: (evt) => {
if (evt.currentTarget instanceof HTMLInputElement) {
settings.updateSetting("verbose", evt.currentTarget.checked);
const action = evt.currentTarget.checked ? "enabled" : "disabled";
settings.logger.verboseLog(`Verbose logging ${action}`);
}
}
},
{
name: "Placement",
description: "Adjust the placement of the dev toolbar.",
input: "select",
settingKey: "placement",
changeEvent: (evt) => {
if (evt.currentTarget instanceof HTMLSelectElement) {
const placement = evt.currentTarget.value;
if (isValidPlacement(placement)) {
document.querySelector("astro-dev-toolbar")?.setToolbarPlacement(placement);
settings.updateSetting("placement", placement);
settings.logger.verboseLog(`Placement set to ${placement}`);
}
}
}
}
];
var settings_default = {
id: "astro:settings",
name: "Settings",
icon: "gear",
init(canvas, eventTarget) {
createSettingsWindow();
document.addEventListener("astro:after-swap", createSettingsWindow);
closeOnOutsideClick(eventTarget);
synchronizePlacementOnUpdate(eventTarget, canvas);
function createSettingsWindow() {
const windowElement = createWindowElement(
`<style>
:host astro-dev-toolbar-window {
height: 480px;
--color-purple: rgba(224, 204, 250, 1);
}
header {
display: flex;
}
h2, h3 {
margin-top: 0;
}
.setting-row {
display: flex;
justify-content: space-between;
align-items: center;
}
h3 {
font-size: 16px;
font-weight: 400;
color: white;
margin-bottom: 4px;
}
label {
font-size: 14px;
line-height: 1.5rem;
}
h1 {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #fff;
margin: 0;
font-size: 22px;
}
astro-dev-toolbar-icon {
width: 1em;
height: 1em;
display: block;
}
code {
color: var(--color-purple);
border-color: #343841;
border-style: solid;
border-width: 1px;
border-radius: .4em;
background-color: #24262D;
padding: .3em;
}
label > section {
max-width: 67%;
}
p {
line-height: 1.5em;
}
a, a:visited {
color: var(--color-purple);
}
a:hover {
color: #f4ecfd;
}
</style>
<header>
<h1><astro-dev-toolbar-icon icon="gear"></astro-dev-toolbar-icon> Settings</h1>
</header>
<hr id="general"/>
<label class="setting-row">
<section>
<h3>Hide toolbar</h3>
Run <code>astro preferences disable devToolbar</code> in your terminal to disable the toolbar. <a href="https://docs.astro.build/en/reference/cli-reference/#astro-preferences" target="_blank">Learn more</a>.
</section>
</label>
`
);
const general = windowElement.querySelector("#general");
for (const settingsRow of settingsRows) {
general.after(document.createElement("hr"));
general.after(getElementForSettingAsString(settingsRow));
}
canvas.append(windowElement);
function getElementForSettingAsString(setting) {
const label = document.createElement("label");
label.classList.add("setting-row");
const section = document.createElement("section");
section.innerHTML = `<h3>${setting.name}</h3>${setting.description}`;
label.append(section);
switch (setting.input) {
case "checkbox": {
const astroToggle = document.createElement("astro-dev-toolbar-toggle");
astroToggle.input.addEventListener("change", setting.changeEvent);
astroToggle.input.checked = settings.config[setting.settingKey];
label.append(astroToggle);
break;
}
case "select": {
const astroSelect = document.createElement("astro-dev-toolbar-select");
placements.forEach((placement) => {
const option = document.createElement("option");
option.setAttribute("value", placement);
if (placement === settings.config[setting.settingKey]) {
option.selected = true;
}
option.textContent = `${placement.slice(0, 1).toUpperCase()}${placement.slice(
1
)}`.replace("-", " ");
astroSelect.append(option);
});
astroSelect.element.addEventListener("change", setting.changeEvent);
label.append(astroSelect);
break;
}
case "number":
case "text":
default:
break;
}
return label;
}
}
}
};
export {
settings_default as default
};

View File

@@ -0,0 +1,8 @@
import type { DevToolbarHighlight } from '../../ui-library/highlight.js';
import type { Icon } from '../../ui-library/icons.js';
export declare function createHighlight(rect: DOMRect, icon?: Icon, additionalAttributes?: Record<string, string>): DevToolbarHighlight;
export declare function getElementsPositionInDocument(el: Element): {
isFixed: boolean;
};
export declare function positionHighlight(highlight: DevToolbarHighlight, rect: DOMRect): void;
export declare function attachTooltipToHighlight(highlight: DevToolbarHighlight, tooltip: HTMLElement, originalElement: Element): void;

View File

@@ -0,0 +1,69 @@
function createHighlight(rect, icon, additionalAttributes) {
const highlight = document.createElement("astro-dev-toolbar-highlight");
if (icon) highlight.icon = icon;
if (additionalAttributes) {
for (const [key, value] of Object.entries(additionalAttributes)) {
highlight.setAttribute(key, value);
}
}
highlight.tabIndex = 0;
if (rect.width === 0 || rect.height === 0) {
highlight.style.display = "none";
} else {
positionHighlight(highlight, rect);
}
return highlight;
}
function getElementsPositionInDocument(el) {
let isFixed = false;
let current = el;
while (current instanceof Element) {
let style = getComputedStyle(current);
if (style.position === "fixed") {
isFixed = true;
}
current = current.parentNode;
}
return {
isFixed
};
}
function positionHighlight(highlight, rect) {
highlight.style.display = "block";
const scrollY = highlight.style.position === "fixed" ? 0 : window.scrollY;
highlight.style.top = `${Math.max(rect.top + scrollY - 10, 0)}px`;
highlight.style.left = `${Math.max(rect.left + window.scrollX - 10, 0)}px`;
highlight.style.width = `${rect.width + 15}px`;
highlight.style.height = `${rect.height + 15}px`;
}
function attachTooltipToHighlight(highlight, tooltip, originalElement) {
highlight.shadowRoot.append(tooltip);
["mouseover", "focus"].forEach((event) => {
highlight.addEventListener(event, () => {
tooltip.dataset.show = "true";
const originalRect = originalElement.getBoundingClientRect();
const dialogRect = tooltip.getBoundingClientRect();
if (originalRect.top < dialogRect.height) {
tooltip.style.top = `${originalRect.height + 15}px`;
} else {
tooltip.style.top = `-${tooltip.offsetHeight}px`;
}
if (dialogRect.right > document.documentElement.clientWidth) {
tooltip.style.right = "0px";
} else if (dialogRect.left < 0) {
tooltip.style.left = "0px";
}
});
});
["mouseout", "blur"].forEach((event) => {
highlight.addEventListener(event, () => {
tooltip.dataset.show = "false";
});
});
}
export {
attachTooltipToHighlight,
createHighlight,
getElementsPositionInDocument,
positionHighlight
};

View File

@@ -0,0 +1,3 @@
import type { Integration } from '../astro.js';
export declare function iconForIntegration(integration: Integration): string;
export declare function colorForIntegration(): string;

View File

@@ -0,0 +1,37 @@
function randomFromArray(list) {
return list[Math.floor(Math.random() * list.length)];
}
const categoryIcons = new Map(
Object.entries({
frameworks: ["puzzle", "grid"],
adapters: ["puzzle", "grid", "compress"],
"css+ui": ["compress", "grid", "image", "resizeImage", "puzzle"],
"performance+seo": ["approveUser", "checkCircle", "compress", "robot", "searchFile", "sitemap"],
analytics: ["checkCircle", "compress", "searchFile"],
accessibility: ["approveUser", "checkCircle"],
other: ["checkCircle", "grid", "puzzle", "sitemap"]
})
);
function iconForIntegration(integration) {
const icons = integration.categories.filter((category) => categoryIcons.has(category)).map((category) => categoryIcons.get(category)).flat();
return randomFromArray(icons);
}
const iconColors = [
"#BC52EE",
"#6D6AF0",
"#52EEBD",
"#52B7EE",
"#52EE55",
"#B7EE52",
"#EEBD52",
"#EE5552",
"#EE52B7",
"#858B98"
];
function colorForIntegration() {
return randomFromArray(iconColors);
}
export {
colorForIntegration,
iconForIntegration
};

View File

@@ -0,0 +1,3 @@
export declare function createWindowElement(content: string, placement?: "bottom-left" | "bottom-center" | "bottom-right"): import("../../ui-library/window.js").DevToolbarWindow;
export declare function closeOnOutsideClick(eventTarget: EventTarget, additionalCheck?: (target: Element) => boolean): void;
export declare function synchronizePlacementOnUpdate(eventTarget: EventTarget, canvas: ShadowRoot): void;

View File

@@ -0,0 +1,48 @@
import { settings } from "../../settings.js";
function createWindowElement(content, placement = settings.config.placement) {
const windowElement = document.createElement("astro-dev-toolbar-window");
windowElement.innerHTML = content;
windowElement.placement = placement;
return windowElement;
}
function closeOnOutsideClick(eventTarget, additionalCheck) {
function onPageClick(event) {
const target = event.target;
if (!target) return;
if (!target.closest) return;
if (target.closest("astro-dev-toolbar")) return;
if (additionalCheck && additionalCheck(target)) return;
eventTarget.dispatchEvent(
new CustomEvent("toggle-app", {
detail: {
state: false
}
})
);
}
eventTarget.addEventListener("app-toggled", (event) => {
if (event.detail.state === true) {
document.addEventListener("click", onPageClick, true);
} else {
document.removeEventListener("click", onPageClick, true);
}
});
}
function synchronizePlacementOnUpdate(eventTarget, canvas) {
eventTarget.addEventListener("placement-updated", (evt) => {
if (!(evt instanceof CustomEvent)) {
return;
}
const windowElement = canvas.querySelector("astro-dev-toolbar-window");
if (!windowElement) {
return;
}
const event = evt;
windowElement.placement = event.detail.placement;
});
}
export {
closeOnOutsideClick,
createWindowElement,
synchronizePlacementOnUpdate
};

View File

@@ -0,0 +1,7 @@
declare const _default: {
id: string;
name: string;
icon: "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><path fill=\"#fff\" d=\"M7.9 1.5v-.4a1.1 1.1 0 0 1 2.2 0v.4a1.1 1.1 0 1 1-2.2 0Zm-6.4 8.6a1.1 1.1 0 1 0 0-2.2h-.4a1.1 1.1 0 0 0 0 2.2h.4ZM12 3.7a1.1 1.1 0 0 0 1.4-.7l.4-1.1a1.1 1.1 0 0 0-2.1-.8l-.4 1.2a1.1 1.1 0 0 0 .7 1.4Zm-9.7 7.6-1.2.4a1.1 1.1 0 1 0 .8 2.1l1-.4a1.1 1.1 0 1 0-.6-2ZM20.8 17a1.9 1.9 0 0 1 0 2.6l-1.2 1.2a1.9 1.9 0 0 1-2.6 0l-4.3-4.2-1.6 3.6a1.9 1.9 0 0 1-1.7 1.2A1.9 1.9 0 0 1 7.5 20L2.7 5a1.9 1.9 0 0 1 2.4-2.4l15 5a1.9 1.9 0 0 1 .2 3.4l-3.7 1.6 4.2 4.3ZM19 18.3 14.6 14a1.9 1.9 0 0 1 .6-3l3.2-1.5L5.1 5.1l4.3 13.3 1.5-3.2a1.9 1.9 0 0 1 3-.6l4.4 4.4.7-.7Z\"/></svg>";
init(canvas: ShadowRoot, eventTarget: import("../helpers.js").ToolbarAppEventTarget): void;
};
export default _default;

View File

@@ -0,0 +1,140 @@
import { escape as escapeHTML } from "html-escaper";
import {
attachTooltipToHighlight,
createHighlight,
getElementsPositionInDocument,
positionHighlight
} from "./utils/highlight.js";
import {
closeOnOutsideClick,
createWindowElement,
synchronizePlacementOnUpdate
} from "./utils/window.js";
const icon = '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#fff" d="M7.9 1.5v-.4a1.1 1.1 0 0 1 2.2 0v.4a1.1 1.1 0 1 1-2.2 0Zm-6.4 8.6a1.1 1.1 0 1 0 0-2.2h-.4a1.1 1.1 0 0 0 0 2.2h.4ZM12 3.7a1.1 1.1 0 0 0 1.4-.7l.4-1.1a1.1 1.1 0 0 0-2.1-.8l-.4 1.2a1.1 1.1 0 0 0 .7 1.4Zm-9.7 7.6-1.2.4a1.1 1.1 0 1 0 .8 2.1l1-.4a1.1 1.1 0 1 0-.6-2ZM20.8 17a1.9 1.9 0 0 1 0 2.6l-1.2 1.2a1.9 1.9 0 0 1-2.6 0l-4.3-4.2-1.6 3.6a1.9 1.9 0 0 1-1.7 1.2A1.9 1.9 0 0 1 7.5 20L2.7 5a1.9 1.9 0 0 1 2.4-2.4l15 5a1.9 1.9 0 0 1 .2 3.4l-3.7 1.6 4.2 4.3ZM19 18.3 14.6 14a1.9 1.9 0 0 1 .6-3l3.2-1.5L5.1 5.1l4.3 13.3 1.5-3.2a1.9 1.9 0 0 1 3-.6l4.4 4.4.7-.7Z"/></svg>';
var xray_default = {
id: "astro:xray",
name: "Inspect",
icon,
init(canvas, eventTarget) {
let islandsOverlays = [];
addIslandsOverlay();
document.addEventListener("astro:after-swap", addIslandsOverlay);
document.addEventListener("astro:page-load", refreshIslandsOverlayPositions);
closeOnOutsideClick(eventTarget);
synchronizePlacementOnUpdate(eventTarget, canvas);
function addIslandsOverlay() {
islandsOverlays.forEach(({ highlightElement }) => {
highlightElement.remove();
});
islandsOverlays = [];
const islands = document.querySelectorAll("astro-island");
if (islands.length === 0) {
const window2 = createWindowElement(
`<style>
header {
display: flex;
}
h1 {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #fff;
margin: 0;
font-size: 22px;
}
astro-dev-toolbar-icon {
width: 1em;
height: 1em;
padding: 8px;
display: block;
background: #5f9ea0;
border-radius: 9999px;
}
</style>
<header>
<h1><astro-dev-toolbar-icon icon="lightbulb"></astro-dev-toolbar-icon>No islands detected.</h1>
</header>
<p>
It looks like there are no interactive component islands on this page. Did you forget to add a client directive to your interactive UI component?
</p>
`
);
canvas.append(window2);
return;
}
islands.forEach((island) => {
const computedStyle = window.getComputedStyle(island);
const islandElement = island.children[0] || island;
if (islandElement.offsetParent === null || computedStyle.display === "none") {
return;
}
const rect = islandElement.getBoundingClientRect();
const highlight = createHighlight(rect);
const tooltip = buildIslandTooltip(island);
const { isFixed } = getElementsPositionInDocument(islandElement);
if (isFixed) {
tooltip.style.position = highlight.style.position = "fixed";
}
attachTooltipToHighlight(highlight, tooltip, islandElement);
canvas.append(highlight);
islandsOverlays.push({ highlightElement: highlight, island: islandElement });
});
["scroll", "resize"].forEach((event) => {
window.addEventListener(event, refreshIslandsOverlayPositions);
});
}
function refreshIslandsOverlayPositions() {
islandsOverlays.forEach(({ highlightElement, island: islandElement }) => {
const rect = islandElement.getBoundingClientRect();
positionHighlight(highlightElement, rect);
});
}
function buildIslandTooltip(island) {
const tooltip = document.createElement("astro-dev-toolbar-tooltip");
tooltip.sections = [];
const islandProps = island.getAttribute("props") ? JSON.parse(island.getAttribute("props")) : {};
const islandClientDirective = island.getAttribute("client");
if (islandClientDirective) {
tooltip.sections.push({
title: "Client directive",
inlineTitle: `<code>client:${islandClientDirective}</code>`
});
}
const islandPropsEntries = Object.entries(islandProps).filter(
(prop) => !prop[0].startsWith("data-astro-cid-")
);
if (islandPropsEntries.length > 0) {
const stringifiedProps = JSON.stringify(
Object.fromEntries(islandPropsEntries.map((prop) => [prop[0], prop[1][1]])),
void 0,
2
);
tooltip.sections.push({
title: "Props",
content: `<pre><code>${escapeHTML(stringifiedProps)}</code></pre>`
});
}
const islandComponentPath = island.getAttribute("component-url");
if (islandComponentPath) {
tooltip.sections.push({
content: islandComponentPath,
clickDescription: "Click to go to file",
async clickAction() {
await fetch(
"/__open-in-editor?file=" + encodeURIComponent(
window.__astro_dev_toolbar__.root + islandComponentPath.slice(1)
)
);
}
});
}
return tooltip;
}
}
};
export {
xray_default as default
};