-
-
-
-
-
-
+
+
@@ -121,7 +124,7 @@
Code
Datasets
Models
-
Spaces
+
Demos
diff --git a/main.js b/main.js
index bf6c297..d47902b 100644
--- a/main.js
+++ b/main.js
@@ -6,12 +6,15 @@
// SECTION 1: CONFIGURATION AND STATE MANAGEMENT
//
-import jsYaml from 'js-yaml';
-import { normalizeTag } from './src/normalizeTag.js';
-import { getPlatformApiUrls } from './src/defineApiUrls.js';
-import { getPlatformDisplay } from './src/defineRibbonVals.js';
-import { filterItems, sortItems } from './src/filterAndSort.js';
-import { filterNewAdditionalEntries } from './src/filterNewAdditionalEntries.js';
+import { load } from 'js-yaml';
+import { initializeUIFromConfig, setThemeToggle } from './src/ui/initUserInterface.js';
+import { parseUrlParams, updateUrlParams, getCurrentState } from './src/ui/urlManager.js';
+import { renderItemList } from './src/ui/render.js';
+import { getPlatformApiUrls } from './src/utils/definePlatformVals.js';
+import { filterItems, sortItems } from './src/utils/filterAndSort.js';
+import { fetchCodeRepos } from './src/api/fetchCodeRepos.js';
+import { fetchHfRepos } from './src/api/fetchHfRepos.js';
+import { fetchCatalogStats } from './src/api/fetchStats.js';
// Start fetching config immediately when the module loads (before DOMContentLoaded)
// so the fetch is in-flight while the DOM is being parsed.
@@ -20,29 +23,12 @@ const configPromise = fetch('config.yaml')
if (!r.ok) throw new Error(`Failed to load config.yaml: HTTP ${r.status}`);
return r.text();
})
- .then(text => jsYaml.load(text));
+ .then(text => load(text));
// Module-scope lets ā assigned after config loads, used by all functions below
let CONFIG;
-let ORGANIZATION_NAME, CATALOG_REPO_NAME, PLATFORM, API_BASE_URL, REFRESH_INTERVAL_DAYS, ADDITIONAL_REPOS, ADDITIONAL_HF_REPOS;
-let ORG_API_URL, REPO_API_URL;
-
-// Build a reverse lookup from TAG_GROUPS (defined in tag-groups.js): raw tag ā [canonical tags]
-// A raw tag may appear in multiple groups, so the value is an array.
-const tagLookup = Object.create(null);
-if (typeof TAG_GROUPS !== 'undefined') {
- for (const [canonical, aliases] of Object.entries(TAG_GROUPS)) {
- for (const alias of aliases) {
- const key = alias.toLowerCase();
- if (tagLookup[key]) {
- tagLookup[key].push(canonical);
- } else {
- tagLookup[key] = [canonical];
- }
- }
- }
-}
-
+let ORGANIZATION_NAME, HF_ORGANIZATION_NAME, CATALOG_REPO_NAME, PLATFORM, API_BASE_URL, REFRESH_INTERVAL_DAYS, ADDITIONAL_REPOS, ADDITIONAL_HF_REPOS;
+let ORG_API_URL, REPO_API_URL, RELEASE_SUFFIX;
let releasesMap = {};
@@ -65,103 +51,8 @@ let fetchedData = {
spaces: false
};
-// Utility function to handle errors and display a user-friendly message
-const handleError = (error, message) => {
- console.error(message, error);
- const itemList = document.getElementById('itemList');
- itemList.innerHTML = `
-
Error loading items.
-
${message}
-
`;
-};
-
-//
-// SECTION 1B: URL PARAMETER HANDLING
-//
-
-/**
- * Parses URL parameters from both query string (?key=value) and hash (#key=value).
- * Query parameters take precedence over hash parameters.
- * @returns {Object} An object containing the parsed parameters.
- */
-const parseUrlParams = () => {
- const params = {};
-
- // Parse query string parameters (e.g., ?type=datasets&q=fish)
- const queryString = window.location.search;
- const urlParams = new URLSearchParams(queryString);
-
- // Parse hash parameters (e.g., #type=datasets&q=fish)
- const hash = window.location.hash.slice(1); // Remove the leading '#'
- const hashParams = new URLSearchParams(hash);
-
- // Hash parameters first (lower precedence)
- for (const [key, value] of hashParams) {
- params[key] = value;
- }
-
- // Query parameters override hash parameters (higher precedence)
- for (const [key, value] of urlParams) {
- params[key] = value;
- }
-
- return params;
-};
-
-/**
- * Updates the URL hash with the current filter state without triggering a page reload.
- * @param {Object} state - The current state object with type, q, sort, tag properties.
- */
-const updateUrlParams = (state) => {
- const params = new URLSearchParams();
-
- // Only add non-default values to the URL
- if (state.type && state.type !== 'all') {
- params.set('type', state.type);
- }
- if (state.q && state.q.trim() !== '') {
- params.set('q', state.q);
- }
- if (state.sort && state.sort !== 'lastModified') {
- params.set('sort', state.sort);
- }
- if (state.tag && state.tag !== '') {
- params.set('tag', state.tag);
- }
- if (state.archived && state.archived !== 'active') {
- params.set('archived', state.archived);
- }
-
- const paramString = params.toString();
- const newHash = paramString ? `#${paramString}` : '';
-
- // Build the new URL preserving the pathname and any query parameters when clearing hash
- const baseUrl = window.location.pathname + window.location.search;
- const newUrl = newHash ? baseUrl + newHash : baseUrl;
-
- // Update the URL without triggering a page reload
- const currentUrl = window.location.pathname + window.location.search + window.location.hash;
- if (currentUrl !== newUrl) {
- history.replaceState(null, '', newUrl);
- }
-};
-
-/**
- * Gets the current filter state from form elements.
- * @returns {Object} The current state object.
- */
-const getCurrentState = () => {
- return {
- type: document.getElementById('repoType')?.value || 'all',
- q: document.getElementById('searchInput')?.value || '',
- sort: document.getElementById('sortBy')?.value || 'lastModified',
- tag: document.getElementById('tagFilter')?.value || '',
- archived: document.getElementById('archiveFilter')?.value || 'active'
- };
-};
-
//
-// SECTION 2: DATA FETCHING LOGIC
+// SECTION 2: DATA FETCHING
//
/**
@@ -178,388 +69,47 @@ const fetchHubItems = async (repoType) => {
const skeletons = document.querySelectorAll('.skeleton-card');
skeletons.forEach(s => s.classList.remove('hidden'));
- try {
- let items = [];
-
- // Platform-specific api requests for code
- if (repoType === "code") {
- if (fetchedData.code) return allItems.code // reuse if already fetched
-
- // Paginate through all public repos (Platform determines API max returns per page)
- let allRepos = [];
- let nextUrl = `${ORG_API_URL}`;
- while (nextUrl) {
- const ghResponse = await fetch(nextUrl);
-
- if (!ghResponse.ok) {
- const platformDisplay = getPlatformDisplay(PLATFORM);
- throw new Error(`${platformDisplay.displayName || PLATFORM} error: ${ghResponse.status}`);
- }
-
- const page = await ghResponse.json();
- allRepos = allRepos.concat(page);
-
- // Parse the Link header to find the next page URL, if any
- const linkHeader = ghResponse.headers.get('Link');
- const match = linkHeader && linkHeader.match(/<([^>]+)>;\s*rel="next"/);
- nextUrl = match ? match[1] : null;
- }
-
- // For org-owned entries in ADDITIONAL_REPOS, reuse data already in allRepos to avoid redundant API calls.
- // Only fetch entries that belong to a different org (external repos).
- const allReposByFullName = new Map(allRepos.map(r => [r.full_name, r]));
- const toFetch = ADDITIONAL_REPOS.filter(ownerRepo => !allReposByFullName.has(ownerRepo));
- const fromAllRepos = ADDITIONAL_REPOS.map(ownerRepo => allReposByFullName.get(ownerRepo)).filter(Boolean);
-
- const fetchedExternalData = await Promise.all(
- toFetch.map(ownerRepo =>
- fetch(`${REPO_API_URL}${ownerRepo}`)
- .then(r => {
- if (!r.ok) {
- console.warn(`Failed to fetch additional repo "${ownerRepo}": HTTP ${r.status}`);
- return null;
- }
- return r.json();
- })
- .catch(err => {
- console.warn(`Network error fetching additional repo "${ownerRepo}":`, err);
- return null;
- })
- )
- );
- const additionalRepos = [...fromAllRepos, ...fetchedExternalData.filter(Boolean)];
-
- // Keep only non-forks from org; deduplicate against additional repos by full_name
- const orgRepoNames = new Set(additionalRepos.map(r => r.full_name));
- const orgNonForks = allRepos.filter(repo => repo.name !== ".github" && !repo.fork && !orgRepoNames.has(repo.full_name));
-
- // Process additional repos and all remaining org non-forks to include metadata and 'new' flag as appropriate
- items = [...additionalRepos, ...orgNonForks]
- .map(repo => {
- const createdAt = new Date(repo.created_at);
- const lastModified = new Date(repo.updated_at);
- const isNew = (new Date() - createdAt) / (1000 * 60 * 60 * 24) < REFRESH_INTERVAL_DAYS;
-
- const rawTags = (repo.topics || []).map(t => t.toLowerCase());
- const tags = [...new Set(rawTags.flatMap(t => normalizeTag(t, tagLookup)).filter(Boolean))];
- const displayTags = rawTags.filter(t => !t.includes(':'));
- tags.forEach(tag => tagsMap.code.add(tag));
-
- const release = releasesMap[repo.full_name] ?? null;
-
- return {
- id: repo.full_name, // "Imageomics/
", used as backup if can't get repo.name
- repoType: "code",
- createdAt,
- lastModified,
- isNew,
- archived: repo.archived || false,
- tags,
- rawTags,
- displayTags,
- description: repo.description || "No description provided.",
- html_url: repo.html_url,
- hasNewRelease: release?.isNew ?? false,
- latestReleaseUrl: release?.url ?? null,
- latestReleaseTag: release?.tag ?? null,
- cardData: {
- pretty_name: repo.name, // , the one used for card title display
- description: repo.description,
- stars: repo.stargazers_count
- }
- };
- });
-
- allItems.code = items;
- fetchedData.code = true;
-
- skeletons.forEach(s => s.classList.add('hidden'));
-
- return items;
- }
+ let items = []
- // hugging face api requests for datasets/models/spaces
- const response = await fetch(`${API_BASE_URL}${repoType}?author=${ORGANIZATION_NAME}&full=true`);
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- let hfItems = await response.json();
-
- // Fetch additional HF repos of this type from outside the org
- const additionalForType = ADDITIONAL_HF_REPOS.filter(entry => entry.type === repoType);
- if (additionalForType.length) {
- const existingIds = new Set(hfItems.map(item => item.id));
- const toFetch = filterNewAdditionalEntries(existingIds, additionalForType);
-
- const fetched = await Promise.all(
- toFetch.map(entry =>
- fetch(`${API_BASE_URL}${repoType}/${entry.repo}`)
- .then(r => {
- if (!r.ok) {
- console.warn(`Failed to fetch additional HF repo "${entry.repo}": HTTP ${r.status}`);
- return null;
- }
- return r.json();
- })
- .catch(err => {
- console.warn(`Network error fetching additional HF repo "${entry.repo}":`, err);
- return null;
- })
- )
- );
- hfItems = [...hfItems, ...fetched.filter(item => item && !existingIds.has(item.id))];
+ if (repoType === 'code') {
+ // Check that releasesMap is populated; if not, fetch it from releases.json
+ if (Object.keys(releasesMap).length === 0) {
+ releasesMap = await fetch('./releases.json')
+ .then(res => res.ok ? res.json() : {})
+ .catch(() => ({}));
}
-
- // Step 2: If we are fetching models, get the full details for each one.
- if (repoType === 'models') {
- const detailPromises = hfItems.map(item =>
- fetch(`${API_BASE_URL}models/${item.id}`).then(res => {
- if (!res.ok) {
- console.error(`Failed to fetch details for ${item.id}`);
- return null; // Return null for failed requests
- }
- return res.json();
- })
- );
-
- // Wait for all detail requests to complete in parallel.
- const detailedItems = await Promise.all(detailPromises);
-
- // Filter out any models that failed to fetch and assign the detailed list.
- hfItems = detailedItems.filter(Boolean);
- }
-
- // Process the data to include metadata and a 'new' flag
- const processedItems = hfItems.map(item => {
- const createdAt = new Date(item.createdAt);
- const lastModified = new Date(item.lastModified);
- const isNew = (new Date() - createdAt) / (1000 * 60 * 60 * 24) < REFRESH_INTERVAL_DAYS;
-
- // Extract tags from the YAML metadata (handling different structures)
- const rawTags = (item.cardData?.tags || item.tags || []).map(t => String(t).toLowerCase());
- const tags = [...new Set(rawTags.flatMap(t => normalizeTag(t, tagLookup)).filter(Boolean))];
- const displayTags = rawTags.filter(t => !t.includes(':'));
- tags.forEach(tag => tagsMap[repoType].add(tag));
-
- return {
- ...item,
- repoType,
- createdAt,
- lastModified,
- isNew,
- archived: false,
- likes: item.likes || 0,
- tags,
- rawTags,
- displayTags
- };
- });
-
- allItems[repoType] = processedItems;
- fetchedData[repoType] = true;
-
- skeletons.forEach(s => s.classList.add('hidden'));
-
- return processedItems;
- } catch (error) {
- handleError(error, `Failed to fetch ${repoType}. Please check your network connection or the API.`);
- return [];
- }
-};
-
-/**
- * Fetches statistics for the Catalog repository itself (Stars, Forks, Version)
- * populates the badge in the top right corner.
- */
-const fetchCatalogStats = async () => {
- // Helper: Updates text, shows the specific stat, and ensures the divider is visible
- const update = (textId, containerId, value) => {
- const el = document.getElementById(textId);
- const container = document.getElementById(containerId);
- if (el && container && value !== undefined) {
- el.innerText = value;
- if (value != 0) {
- container.classList.remove('hidden');
- container.classList.add('flex');
- }
- }
- };
-
- try {
- //TODO: Update stars and forks to support other platforms (GitLab, Codeberg) once implemented
- // 1. Get Stars & Forks
- const repo = await fetch(`${REPO_API_URL}${ORGANIZATION_NAME}/${CATALOG_REPO_NAME}`).then(r => r.ok ? r.json() : {});
- if (repo.stargazers_count !== undefined) update('gh-stars', 'gh-star-container', repo.stargazers_count);
- if (repo.forks_count !== undefined) update('gh-forks', 'gh-fork-container', repo.forks_count);
-
- // 2. Get Version (Tag)
- // TODO: Import from package.json
- const release = await fetch(`${REPO_API_URL}${ORGANIZATION_NAME}/${CATALOG_REPO_NAME}/releases/latest`).then(r => r.ok ? r.json() : {});
- if (release.tag_name) update('gh-tag', 'gh-version-container', release.tag_name);
-
- } catch (e) {
- console.warn("Could not fetch Code Repo stats", e);
- }
-};
-
-//
-// SECTION 3: RENDERING LOGIC
-//
-
-/**
- * Escapes HTML special characters in a string to prevent XSS.
- * @param {string} str - The input string.
- * @returns {string} The escaped string.
- */
-const escapeHTML = (str) => {
- if (!str) return "";
- return str.replace(/[&<>"']/g, (m) => ({
- '&': '&',
- '<': '<',
- '>': '>',
- '"': '"',
- "'": '''
- }[m]));
-};
-
-/**
- * Adds word break opportunities after underscores to allow proper text wrapping.
- * @param {string} str - The input string.
- * @returns {string} The string with tags inserted after underscores.
- */
-const addWordBreakOpportunities = (str) => {
- if (!str) return "";
- // Replace underscores with underscore + word break opportunity
- return str.replace(/_/g, '_');
-};
-
-/**
- * Renders a single item card (code, dataset, model, or space) to HTML.
- * @param {Object} item - The item object to render.
- * @param {string} repoType - The type of repository.
- * @returns {string} The HTML string for the item card.
- */
-const renderHubItemCard = (item, repoType) => {
- const lastUpdatedDate = new Date(item.lastModified).toLocaleDateString();
- const tagsHtml = (item.displayTags || item.rawTags || []).map(tag =>
- `${tag} `
- ).join('');
-
- // Use pretty_name for the heading, with a fallback
- // HF API keys for CardData: https://huggingface.co/docs/huggingface_hub/main/en/package_reference/cards#huggingface_hub.CardData
- // datasets have pretty_name, models have model_name, spaces have title
- const prettyName = item.cardData?.pretty_name || item.cardData?.model_name || item.cardData?.title || item.id.split('/')[1];
-
- // Use the description from cardData, with fallbacks
- const displayDescription = item.cardData?.description || item.cardData?.model_description || item.description || 'No description provided.';
-
- // Construct the correct URL based on the repository type
- let itemUrl;
-
- switch (item.repoType) {
- case "code":
- itemUrl = item.html_url;
- break;
- case "datasets":
- itemUrl = `https://huggingface.co/datasets/${item.id}`;
- break;
- case "spaces":
- itemUrl = `https://huggingface.co/spaces/${item.id}`;
- break;
- case "models":
- itemUrl = `https://huggingface.co/${item.id}`;
- break;
- default:
- // fallback for "all"
- itemUrl = `https://huggingface.co/${item.id}`;
- break;
+ items = await fetchCodeRepos(
+ PLATFORM,
+ ADDITIONAL_REPOS,
+ ORG_API_URL,
+ REPO_API_URL,
+ REFRESH_INTERVAL_DAYS,
+ releasesMap
+ );
+ } else {
+ items = await fetchHfRepos(
+ repoType,
+ ADDITIONAL_HF_REPOS,
+ API_BASE_URL,
+ HF_ORGANIZATION_NAME,
+ REFRESH_INTERVAL_DAYS
+ );
}
- // stars for code repos
- const badgeHtml = (() => {
- if (item.isNew) {
- return `
- New!
- `;
- }
-
- if (typeof item.cardData.stars === "number" && item.cardData.stars > 0) {
- return `
- ā ${item.cardData.stars}
- `;
- }
-
- if (typeof item.likes === "number" && item.likes > 0) {
- return `
-
- ā¤ļø ${item.likes}
- `;
- }
- return "";
- })();
-
- const escapedTitle = escapeHTML(prettyName);
- const displayTitle = addWordBreakOpportunities(escapedTitle);
-
- return `
-
-
-
-
- ${displayDescription}
-
-
-
-
- ${tagsHtml}
-
-
- Updated: ${lastUpdatedDate}
- ${item.archived ? `Archived ` : ''}
-
-
-
- `;
-};
+ // Store fetched items and mark as fetched
+ allItems[repoType] = items;
+ fetchedData[repoType] = true;
+ items.forEach(item => {
+ item.tags.forEach(tag => tagsMap[repoType].add(tag));
+ });
-/**
- * Renders the list of items to the DOM.
- * @param {Array} items - The array of item objects to render.
- * @param {string} repoType - The type of repository.
- */
-const renderItemList = (items, repoType) => {
- const itemListElement = document.getElementById('itemList');
- const emptyStateElement = document.getElementById('emptyState');
+ skeletons.forEach(s => s.classList.add('hidden'));
- if (items.length === 0) {
- itemListElement.innerHTML = '';
- emptyStateElement.classList.remove('hidden');
- } else {
- itemListElement.innerHTML = items.map(item => renderHubItemCard(item, repoType)).join('');
- emptyStateElement.classList.add('hidden');
- }
+ return items;
};
//
-// SECTION 4: SEARCH, FILTER, AND SORT LOGIC
+// SECTION 3: SEARCH, FILTER, AND SORT LOGIC
//
/**
@@ -592,7 +142,7 @@ const applyFiltersAndSort = async (updateUrl = true) => {
const filtered = filterItems(currentItems, { searchTerm, tagFilter, archiveFilter });
const sorted = sortItems(filtered, sortBy);
- renderItemList(sorted, repoType);
+ renderItemList(sorted);
};
/**
@@ -629,66 +179,8 @@ const populateTagFilter = (repoType) => {
};
//
-// SECTION 5: EVENT LISTENERS AND INITIALIZATION
+// SECTION 4: EVENT LISTENERS AND INITIALIZATION
//
-
-/**
- * Initializes UI elements from configuration values.
- * This sets up the header, logo, repo ribbon, and dynamic styles.
- */
-const initializeUIFromConfig = () => {
- // Set header logo
- const logoImg = document.getElementById('logo-img');
- if (logoImg) {
- logoImg.src = CONFIG.LOGO_URL;
- logoImg.alt = CONFIG.ORG_NAME + ' Logo';
-
- logoImg.onload = () => {
- logoImg.classList.remove('opacity-0');
- };
- }
-
- // Set header title and description
- const headerTitle = document.getElementById('header-title');
- if (headerTitle) {
- headerTitle.textContent = CONFIG.CATALOG_TITLE;
- headerTitle.style.color = CONFIG.COLORS.primary;
- }
-
- const headerDesc = document.getElementById('header-description');
- if (headerDesc) {
- headerDesc.textContent = CONFIG.CATALOG_DESCRIPTION;
- }
-
- // Set Code Repo ribbon link, SVG path, display name, and colors
- const repoRibbon = document.getElementById('repo-ribbon');
- const platformDisplay = getPlatformDisplay(PLATFORM);
- if (repoRibbon && platformDisplay) {
- repoRibbon.href = `${platformDisplay.ribbonUrl}${ORGANIZATION_NAME}/${CATALOG_REPO_NAME}`;
- const pathElement = document.getElementById('repo-ribbon-icon');
- pathElement.setAttribute('d', platformDisplay.path);
- const platformDisplayName = document.getElementById('platform-display-name');
- platformDisplayName.textContent = platformDisplay.displayName || PLATFORM;
- repoRibbon.style.backgroundColor = CONFIG.COLORS.secondary;
- repoRibbon.style.setProperty('--hover-color', CONFIG.COLORS.primary);
- repoRibbon.addEventListener('mouseenter', function () {
- this.style.backgroundColor = CONFIG.COLORS.primary;
- });
- repoRibbon.addEventListener('mouseleave', function () {
- this.style.backgroundColor = CONFIG.COLORS.secondary;
- });
- }
-
- // Set focus ring colors for form inputs and link hover colors
- const style = document.createElement('style');
- style.textContent = `
- .focus\\:ring-2:focus { --tw-ring-color: var(--color-accent) !important; }
- .item-link:hover { color: var(--color-accent) !important; }
- .dark .item-link:hover { color: var(--color-accent-dark) !important; }
- `;
- document.head.appendChild(style);
-};
-
document.addEventListener('DOMContentLoaded', async () => {
// Load config before anything else
try {
@@ -703,7 +195,7 @@ document.addEventListener('DOMContentLoaded', async () => {
setTimeout(() => errorDiv.remove(), 10000);
// Fall back to defaults so the page isn't completely broken
CONFIG = {
- ORGANIZATION_NAME: '', CATALOG_REPO_NAME: '', ORG_NAME: '',
+ ORGANIZATION_NAME: '', HF_ORGANIZATION_NAME: '', CATALOG_REPO_NAME: '', ORG_NAME: '',
CATALOG_TITLE: 'Catalog', CATALOG_DESCRIPTION: '',
LOGO_URL: '', FAVICON_URL: '',
COLORS: { primary: '#92991c', secondary: '#5d8095', accent: '#0097b2', accentDark: '#4fd1eb', tag: '#9bcb5e' },
@@ -714,39 +206,35 @@ document.addEventListener('DOMContentLoaded', async () => {
}
// Assign module-scope variables used by all functions
- ORGANIZATION_NAME = CONFIG.ORGANIZATION_NAME;
- CATALOG_REPO_NAME = CONFIG.CATALOG_REPO_NAME;
- PLATFORM = CONFIG.PLATFORM;
- ORG_API_URL = getPlatformApiUrls(PLATFORM, ORGANIZATION_NAME).org;
- REPO_API_URL = getPlatformApiUrls(PLATFORM, ORGANIZATION_NAME).repo;
- API_BASE_URL = CONFIG.API_BASE_URL;
- REFRESH_INTERVAL_DAYS = CONFIG.REFRESH_INTERVAL_DAYS;
- ADDITIONAL_REPOS = CONFIG.ADDITIONAL_REPOS;
- ADDITIONAL_HF_REPOS = CONFIG.ADDITIONAL_HF_REPOS;
-
- // Apply CSS custom properties and document metadata
- document.title = CONFIG.CATALOG_TITLE || `${CONFIG.ORG_NAME} Catalog`;
- document.documentElement.style.setProperty('--color-primary', CONFIG.COLORS?.primary || '#92991c');
- document.documentElement.style.setProperty('--color-secondary', CONFIG.COLORS?.secondary || '#5d8095');
- document.documentElement.style.setProperty('--color-accent', CONFIG.COLORS?.accent || '#0097b2');
- document.documentElement.style.setProperty('--color-accent-dark', CONFIG.COLORS?.accentDark || '#4fd1eb');
- document.documentElement.style.setProperty('--color-tag', CONFIG.COLORS?.tag || '#9bcb5e');
- const fontFamily = CONFIG.FONT_FAMILY || 'Inter';
- document.documentElement.style.setProperty('--font-family', fontFamily.includes(' ') ? `"${fontFamily}"` : fontFamily);
-
- // Update Google Fonts link
- if (CONFIG.FONT_FAMILY) {
- const fontFamily = CONFIG.FONT_FAMILY.replace(/\s+/g, '+');
- const fontLink = document.getElementById('font-link');
- if (fontLink) fontLink.href = `https://fonts.googleapis.com/css2?family=${fontFamily}:wght@400;500;600;700&display=swap`;
+ // Destructure CONFIG into individual variables for easier access
+ ({
+ ORGANIZATION_NAME,
+ HF_ORGANIZATION_NAME,
+ CATALOG_REPO_NAME,
+ PLATFORM,
+ API_BASE_URL,
+ REFRESH_INTERVAL_DAYS,
+ ADDITIONAL_REPOS,
+ ADDITIONAL_HF_REPOS
+ } = CONFIG);
+ // Destructure platform-specific API URLs from getPlatformApiUrls
+ ({
+ org: ORG_API_URL,
+ repo: REPO_API_URL,
+ releaseSuffix: RELEASE_SUFFIX
+ } = getPlatformApiUrls(PLATFORM, ORGANIZATION_NAME));
+
+ // Guard: if ORGANIZATION_NAME or HF_ORGANIZATION_NAME is missing (e.g. config.yaml failed to load),
+ // stop here ā proceeding would fire requests like ?author=&full=true which
+ // could return unbounded results from the Hugging Face API.
+ if (!ORGANIZATION_NAME || !HF_ORGANIZATION_NAME){
+ console.error("Organization name is missing for one or both APIs. Halting initialization.");
+ return;
}
- // Set favicon
- const faviconLink = document.getElementById('favicon-link');
- if (faviconLink && CONFIG.FAVICON_URL) faviconLink.href = CONFIG.FAVICON_URL;
-
// Initialize UI from config
- initializeUIFromConfig();
+ initializeUIFromConfig(CONFIG);
+ setThemeToggle();
const searchInput = document.getElementById('searchInput');
const sortBySelect = document.getElementById('sortBy');
@@ -808,12 +296,7 @@ document.addEventListener('DOMContentLoaded', async () => {
});
// Initialize the Catalog Badge (Stars/Forks/Version)
- // Guard: if ORGANIZATION_NAME is missing (e.g. config.yaml failed to load),
- // stop here ā proceeding would fire requests like ?author=&full=true which
- // could return unbounded results from the Hugging Face API.
- if (!ORGANIZATION_NAME) return;
-
- fetchCatalogStats();
+ fetchCatalogStats(REPO_API_URL, ORGANIZATION_NAME, CATALOG_REPO_NAME, RELEASE_SUFFIX)
// Load pre-built release data (written by scripts/fetch-releases.js at build time)
releasesMap = await fetch('./releases.json')
@@ -854,18 +337,3 @@ document.addEventListener('DOMContentLoaded', async () => {
await applyFiltersAndSort(false);
updateUrlParams(getCurrentState());
});
-
-//
-// THEME TOGGLE LOGIC
-//
-const themeToggleBtn = document.getElementById('themeToggleBtn');
-
-themeToggleBtn.addEventListener('click', () => {
- if (document.documentElement.classList.contains('dark')) {
- document.documentElement.classList.remove('dark');
- localStorage.theme = 'light';
- } else {
- document.documentElement.classList.add('dark');
- localStorage.theme = 'dark';
- }
-});
diff --git a/package-lock.json b/package-lock.json
index a5c8051..0be73d3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,458 +9,273 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
- "js-yaml": "^4.1.0"
+ "js-yaml": "^5.2.1"
},
"devDependencies": {
- "@tailwindcss/vite": "^4.2.1",
+ "@tailwindcss/vite": "^4.3.2",
+ "jsdom": "^29.1.1",
"tailwindcss": "^4.2.1",
- "vite": "^7.3.2",
+ "vite": "^8.1.3",
"vitest": "^4.1.5"
},
"engines": {
"node": ">=24"
}
},
- "node_modules/@esbuild/aix-ppc64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
- "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
- "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
- "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/android-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
- "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
- "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/darwin-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
- "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
- "cpu": [
- "x64"
- ],
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "5.1.11",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
+ "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
- "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/freebsd-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
- "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
+ "dependencies": {
+ "@asamuzakjp/generational-cache": "^1.0.1",
+ "@csstools/css-calc": "^3.2.0",
+ "@csstools/css-color-parser": "^4.1.0",
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
+ },
"engines": {
- "node": ">=18"
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
- "node_modules/@esbuild/linux-arm": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
- "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
- "cpu": [
- "arm"
- ],
+ "node_modules/@asamuzakjp/dom-selector": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
+ "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
+ "dependencies": {
+ "@asamuzakjp/generational-cache": "^1.0.1",
+ "@asamuzakjp/nwsapi": "^2.3.9",
+ "bidi-js": "^1.0.3",
+ "css-tree": "^3.2.1",
+ "is-potential-custom-element-name": "^1.0.1"
+ },
"engines": {
- "node": ">=18"
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
- "node_modules/@esbuild/linux-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
- "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
- "cpu": [
- "arm64"
- ],
+ "node_modules/@asamuzakjp/generational-cache": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
+ "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
"engines": {
- "node": ">=18"
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
- "node_modules/@esbuild/linux-ia32": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
- "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
- "cpu": [
- "ia32"
- ],
+ "node_modules/@asamuzakjp/nwsapi": {
+ "version": "2.3.9",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
+ "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
+ "license": "MIT"
},
- "node_modules/@esbuild/linux-loong64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
- "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
- "cpu": [
- "loong64"
- ],
+ "node_modules/@bramus/specificity": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
+ "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "css-tree": "^3.0.0"
+ },
+ "bin": {
+ "specificity": "bin/cli.js"
}
},
- "node_modules/@esbuild/linux-mips64el": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
- "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
- "cpu": [
- "mips64el"
- ],
+ "node_modules/@csstools/color-helpers": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz",
+ "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
],
+ "license": "MIT-0",
"engines": {
- "node": ">=18"
+ "node": ">=20.19.0"
}
},
- "node_modules/@esbuild/linux-ppc64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
- "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
- "cpu": [
- "ppc64"
- ],
+ "node_modules/@csstools/css-calc": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz",
+ "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-riscv64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
- "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
- "cpu": [
- "riscv64"
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
],
- "dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
"engines": {
- "node": ">=18"
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
}
},
- "node_modules/@esbuild/linux-s390x": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
- "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
- "cpu": [
- "s390x"
- ],
+ "node_modules/@csstools/css-color-parser": {
+ "version": "4.1.9",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz",
+ "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/linux-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
- "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
- "cpu": [
- "x64"
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
],
- "dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
+ "dependencies": {
+ "@csstools/color-helpers": "^6.1.0",
+ "@csstools/css-calc": "^3.2.1"
+ },
"engines": {
- "node": ">=18"
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^4.0.0",
+ "@csstools/css-tokenizer": "^4.0.0"
}
},
- "node_modules/@esbuild/netbsd-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
- "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
- "cpu": [
- "arm64"
- ],
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
+ "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/netbsd-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
- "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
- "cpu": [
- "x64"
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
],
- "dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "netbsd"
- ],
"engines": {
- "node": ">=18"
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^4.0.0"
}
},
- "node_modules/@esbuild/openbsd-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
- "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
- "cpu": [
- "arm64"
- ],
+ "node_modules/@csstools/css-syntax-patches-for-csstree": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz",
+ "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
],
- "engines": {
- "node": ">=18"
+ "license": "MIT-0",
+ "peerDependencies": {
+ "css-tree": "^3.2.1"
+ },
+ "peerDependenciesMeta": {
+ "css-tree": {
+ "optional": true
+ }
}
},
- "node_modules/@esbuild/openbsd-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
- "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
- "cpu": [
- "x64"
- ],
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
+ "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ],
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@esbuild/openharmony-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
- "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
- "cpu": [
- "arm64"
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
],
- "dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
"engines": {
- "node": ">=18"
+ "node": ">=20.19.0"
}
},
- "node_modules/@esbuild/sunos-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
- "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
- "cpu": [
- "x64"
- ],
+ "node_modules/@emnapi/core": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz",
+ "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==",
"dev": true,
"license": "MIT",
"optional": true,
- "os": [
- "sunos"
- ],
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.2",
+ "tslib": "^2.4.0"
}
},
- "node_modules/@esbuild/win32-arm64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
- "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
- "cpu": [
- "arm64"
- ],
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz",
+ "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==",
"dev": true,
"license": "MIT",
"optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "tslib": "^2.4.0"
}
},
- "node_modules/@esbuild/win32-ia32": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
- "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
- "cpu": [
- "ia32"
- ],
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
+ "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
"dev": true,
"license": "MIT",
"optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "tslib": "^2.4.0"
}
},
- "node_modules/@esbuild/win32-x64": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
- "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
- "cpu": [
- "x64"
- ],
+ "node_modules/@exodus/bytes": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
+ "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
"dev": true,
"license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
"engines": {
- "node": ">=18"
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@noble/hashes": "^1.8.0 || ^2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@noble/hashes": {
+ "optional": true
+ }
}
},
"node_modules/@jridgewell/gen-mapping": {
@@ -513,24 +328,39 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
- "node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
- "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
- "cpu": [
- "arm"
- ],
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
+ "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
"dev": true,
"license": "MIT",
"optional": true,
- "os": [
- "android"
- ]
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.3"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
},
- "node_modules/@rollup/rollup-android-arm64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
- "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
+ "node_modules/@oxc-project/types": {
+ "version": "0.138.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz",
+ "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz",
+ "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==",
"cpu": [
"arm64"
],
@@ -539,12 +369,15 @@
"optional": true,
"os": [
"android"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
- "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz",
+ "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==",
"cpu": [
"arm64"
],
@@ -553,12 +386,15 @@
"optional": true,
"os": [
"darwin"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
- "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz",
+ "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==",
"cpu": [
"x64"
],
@@ -567,26 +403,15 @@
"optional": true,
"os": [
"darwin"
- ]
- },
- "node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
- "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
- "cpu": [
- "arm64"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
- "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz",
+ "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==",
"cpu": [
"x64"
],
@@ -595,26 +420,15 @@
"optional": true,
"os": [
"freebsd"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
- "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
- "cpu": [
- "arm"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
- "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz",
+ "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==",
"cpu": [
"arm"
],
@@ -623,26 +437,15 @@
"optional": true,
"os": [
"linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
- "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
- "cpu": [
- "arm64"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
- "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz",
+ "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==",
"cpu": [
"arm64"
],
@@ -651,54 +454,32 @@
"optional": true,
"os": [
"linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
- "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
- "cpu": [
- "loong64"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
- "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz",
+ "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==",
"cpu": [
- "loong64"
+ "arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
- "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
- "cpu": [
- "ppc64"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
- "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz",
+ "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==",
"cpu": [
"ppc64"
],
@@ -707,40 +488,15 @@
"optional": true,
"os": [
"linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
- "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
- "cpu": [
- "riscv64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
- "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
- "cpu": [
- "riscv64"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
- "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz",
+ "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==",
"cpu": [
"s390x"
],
@@ -749,12 +505,15 @@
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
- "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz",
+ "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==",
"cpu": [
"x64"
],
@@ -763,12 +522,15 @@
"optional": true,
"os": [
"linux"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
- "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz",
+ "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==",
"cpu": [
"x64"
],
@@ -777,26 +539,15 @@
"optional": true,
"os": [
"linux"
- ]
- },
- "node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
- "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
- "cpu": [
- "x64"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openbsd"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
- "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz",
+ "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==",
"cpu": [
"arm64"
],
@@ -805,54 +556,51 @@
"optional": true,
"os": [
"openharmony"
- ]
- },
- "node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
- "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
- "cpu": [
- "arm64"
],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
- "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz",
+ "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==",
"cpu": [
- "ia32"
+ "wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
- "os": [
- "win32"
- ]
+ "dependencies": {
+ "@emnapi/core": "1.11.1",
+ "@emnapi/runtime": "1.11.1",
+ "@napi-rs/wasm-runtime": "^1.1.6"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
- "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz",
+ "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==",
"cpu": [
- "x64"
+ "arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
},
- "node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
- "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz",
+ "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==",
"cpu": [
"x64"
],
@@ -861,7 +609,17 @@
"optional": true,
"os": [
"win32"
- ]
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
@@ -871,49 +629,49 @@
"license": "MIT"
},
"node_modules/@tailwindcss/node": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz",
- "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz",
+ "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.5",
- "enhanced-resolve": "^5.19.0",
- "jiti": "^2.6.1",
- "lightningcss": "1.31.1",
+ "enhanced-resolve": "5.21.6",
+ "jiti": "^2.7.0",
+ "lightningcss": "1.32.0",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
- "tailwindcss": "4.2.1"
+ "tailwindcss": "4.3.2"
}
},
"node_modules/@tailwindcss/oxide": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz",
- "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz",
+ "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 20"
},
"optionalDependencies": {
- "@tailwindcss/oxide-android-arm64": "4.2.1",
- "@tailwindcss/oxide-darwin-arm64": "4.2.1",
- "@tailwindcss/oxide-darwin-x64": "4.2.1",
- "@tailwindcss/oxide-freebsd-x64": "4.2.1",
- "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1",
- "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1",
- "@tailwindcss/oxide-linux-arm64-musl": "4.2.1",
- "@tailwindcss/oxide-linux-x64-gnu": "4.2.1",
- "@tailwindcss/oxide-linux-x64-musl": "4.2.1",
- "@tailwindcss/oxide-wasm32-wasi": "4.2.1",
- "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1",
- "@tailwindcss/oxide-win32-x64-msvc": "4.2.1"
+ "@tailwindcss/oxide-android-arm64": "4.3.2",
+ "@tailwindcss/oxide-darwin-arm64": "4.3.2",
+ "@tailwindcss/oxide-darwin-x64": "4.3.2",
+ "@tailwindcss/oxide-freebsd-x64": "4.3.2",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.3.2",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.3.2",
+ "@tailwindcss/oxide-linux-x64-musl": "4.3.2",
+ "@tailwindcss/oxide-wasm32-wasi": "4.3.2",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.3.2"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz",
- "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz",
+ "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==",
"cpu": [
"arm64"
],
@@ -928,9 +686,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz",
- "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz",
+ "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==",
"cpu": [
"arm64"
],
@@ -945,9 +703,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz",
- "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz",
+ "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==",
"cpu": [
"x64"
],
@@ -962,9 +720,9 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz",
- "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz",
+ "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==",
"cpu": [
"x64"
],
@@ -979,9 +737,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz",
- "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz",
+ "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==",
"cpu": [
"arm"
],
@@ -996,9 +754,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz",
- "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz",
+ "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==",
"cpu": [
"arm64"
],
@@ -1013,9 +771,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz",
- "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz",
+ "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==",
"cpu": [
"arm64"
],
@@ -1030,9 +788,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz",
- "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz",
+ "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==",
"cpu": [
"x64"
],
@@ -1047,9 +805,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz",
- "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz",
+ "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==",
"cpu": [
"x64"
],
@@ -1064,9 +822,9 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz",
- "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz",
+ "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
@@ -1082,85 +840,21 @@
"license": "MIT",
"optional": true,
"dependencies": {
- "@emnapi/core": "^1.8.1",
- "@emnapi/runtime": "^1.8.1",
- "@emnapi/wasi-threads": "^1.1.0",
- "@napi-rs/wasm-runtime": "^1.1.1",
- "@tybys/wasm-util": "^0.10.1",
+ "@emnapi/core": "^1.11.1",
+ "@emnapi/runtime": "^1.11.1",
+ "@emnapi/wasi-threads": "^1.2.2",
+ "@napi-rs/wasm-runtime": "^1.1.4",
+ "@tybys/wasm-util": "^0.10.2",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14.0.0"
}
},
- "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
- "version": "1.8.1",
- "dev": true,
- "inBundle": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/wasi-threads": "1.1.0",
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
- "version": "1.8.1",
- "dev": true,
- "inBundle": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
- "version": "1.1.0",
- "dev": true,
- "inBundle": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
- "version": "1.1.1",
- "dev": true,
- "inBundle": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/core": "^1.7.1",
- "@emnapi/runtime": "^1.7.1",
- "@tybys/wasm-util": "^0.10.1"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
- "version": "0.10.1",
- "dev": true,
- "inBundle": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
- "version": "2.8.1",
- "dev": true,
- "inBundle": true,
- "license": "0BSD",
- "optional": true
- },
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz",
- "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz",
+ "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==",
"cpu": [
"arm64"
],
@@ -1175,9 +869,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz",
- "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz",
+ "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==",
"cpu": [
"x64"
],
@@ -1192,18 +886,29 @@
}
},
"node_modules/@tailwindcss/vite": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz",
- "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz",
+ "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@tailwindcss/node": "4.2.1",
- "@tailwindcss/oxide": "4.2.1",
- "tailwindcss": "4.2.1"
+ "@tailwindcss/node": "4.3.2",
+ "@tailwindcss/oxide": "4.3.2",
+ "tailwindcss": "4.3.2"
},
"peerDependencies": {
- "vite": "^5.2.0 || ^6 || ^7"
+ "vite": "^5.2.0 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
+ "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
}
},
"node_modules/@types/chai": {
@@ -1360,6 +1065,16 @@
"node": ">=12"
}
},
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
@@ -1377,6 +1092,41 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/css-tree": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
+ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.27.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/data-urls": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
+ "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -1388,19 +1138,32 @@
}
},
"node_modules/enhanced-resolve": {
- "version": "5.20.0",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
- "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==",
+ "version": "5.21.6",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz",
+ "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
- "tapable": "^2.3.0"
+ "tapable": "^2.3.3"
},
"engines": {
"node": ">=10.13.0"
}
},
+ "node_modules/entities": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
+ "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/es-module-lexer": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz",
@@ -1408,48 +1171,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/esbuild": {
- "version": "0.27.3",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
- "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "bin": {
- "esbuild": "bin/esbuild"
- },
- "engines": {
- "node": ">=18"
- },
- "optionalDependencies": {
- "@esbuild/aix-ppc64": "0.27.3",
- "@esbuild/android-arm": "0.27.3",
- "@esbuild/android-arm64": "0.27.3",
- "@esbuild/android-x64": "0.27.3",
- "@esbuild/darwin-arm64": "0.27.3",
- "@esbuild/darwin-x64": "0.27.3",
- "@esbuild/freebsd-arm64": "0.27.3",
- "@esbuild/freebsd-x64": "0.27.3",
- "@esbuild/linux-arm": "0.27.3",
- "@esbuild/linux-arm64": "0.27.3",
- "@esbuild/linux-ia32": "0.27.3",
- "@esbuild/linux-loong64": "0.27.3",
- "@esbuild/linux-mips64el": "0.27.3",
- "@esbuild/linux-ppc64": "0.27.3",
- "@esbuild/linux-riscv64": "0.27.3",
- "@esbuild/linux-s390x": "0.27.3",
- "@esbuild/linux-x64": "0.27.3",
- "@esbuild/netbsd-arm64": "0.27.3",
- "@esbuild/netbsd-x64": "0.27.3",
- "@esbuild/openbsd-arm64": "0.27.3",
- "@esbuild/openbsd-x64": "0.27.3",
- "@esbuild/openharmony-arm64": "0.27.3",
- "@esbuild/sunos-x64": "0.27.3",
- "@esbuild/win32-arm64": "0.27.3",
- "@esbuild/win32-ia32": "0.27.3",
- "@esbuild/win32-x64": "0.27.3"
- }
- },
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@@ -1510,10 +1231,30 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/html-encoding-sniffer": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
+ "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.6.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/jiti": {
- "version": "2.6.1",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
- "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
"dev": true,
"license": "MIT",
"bin": {
@@ -1521,21 +1262,72 @@
}
},
"node_modules/js-yaml": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
- "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.2.1.tgz",
+ "integrity": "sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
- "js-yaml": "bin/js-yaml.js"
+ "js-yaml": "bin/js-yaml.mjs"
+ }
+ },
+ "node_modules/jsdom": {
+ "version": "29.1.1",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
+ "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^5.1.11",
+ "@asamuzakjp/dom-selector": "^7.1.1",
+ "@bramus/specificity": "^2.4.2",
+ "@csstools/css-syntax-patches-for-csstree": "^1.1.3",
+ "@exodus/bytes": "^1.15.0",
+ "css-tree": "^3.2.1",
+ "data-urls": "^7.0.0",
+ "decimal.js": "^10.6.0",
+ "html-encoding-sniffer": "^6.0.0",
+ "is-potential-custom-element-name": "^1.0.1",
+ "lru-cache": "^11.3.5",
+ "parse5": "^8.0.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^6.0.1",
+ "undici": "^7.25.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^8.0.1",
+ "whatwg-mimetype": "^5.0.0",
+ "whatwg-url": "^16.0.1",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
}
},
"node_modules/lightningcss": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
- "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
"dev": true,
"license": "MPL-2.0",
"dependencies": {
@@ -1549,23 +1341,23 @@
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
- "lightningcss-android-arm64": "1.31.1",
- "lightningcss-darwin-arm64": "1.31.1",
- "lightningcss-darwin-x64": "1.31.1",
- "lightningcss-freebsd-x64": "1.31.1",
- "lightningcss-linux-arm-gnueabihf": "1.31.1",
- "lightningcss-linux-arm64-gnu": "1.31.1",
- "lightningcss-linux-arm64-musl": "1.31.1",
- "lightningcss-linux-x64-gnu": "1.31.1",
- "lightningcss-linux-x64-musl": "1.31.1",
- "lightningcss-win32-arm64-msvc": "1.31.1",
- "lightningcss-win32-x64-msvc": "1.31.1"
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
}
},
"node_modules/lightningcss-android-arm64": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz",
- "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
"cpu": [
"arm64"
],
@@ -1584,9 +1376,9 @@
}
},
"node_modules/lightningcss-darwin-arm64": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz",
- "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
"cpu": [
"arm64"
],
@@ -1605,9 +1397,9 @@
}
},
"node_modules/lightningcss-darwin-x64": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz",
- "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
"cpu": [
"x64"
],
@@ -1626,9 +1418,9 @@
}
},
"node_modules/lightningcss-freebsd-x64": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz",
- "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
"cpu": [
"x64"
],
@@ -1647,9 +1439,9 @@
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz",
- "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
"cpu": [
"arm"
],
@@ -1668,9 +1460,9 @@
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz",
- "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
"cpu": [
"arm64"
],
@@ -1689,9 +1481,9 @@
}
},
"node_modules/lightningcss-linux-arm64-musl": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz",
- "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
"cpu": [
"arm64"
],
@@ -1710,9 +1502,9 @@
}
},
"node_modules/lightningcss-linux-x64-gnu": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz",
- "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
"cpu": [
"x64"
],
@@ -1731,9 +1523,9 @@
}
},
"node_modules/lightningcss-linux-x64-musl": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz",
- "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
"cpu": [
"x64"
],
@@ -1752,9 +1544,9 @@
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz",
- "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
"cpu": [
"arm64"
],
@@ -1773,9 +1565,9 @@
}
},
"node_modules/lightningcss-win32-x64-msvc": {
- "version": "1.31.1",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz",
- "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==",
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
"cpu": [
"x64"
],
@@ -1793,6 +1585,16 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/lru-cache": {
+ "version": "11.5.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
+ "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -1803,10 +1605,17 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/mdn-data": {
+ "version": "2.27.1",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
+ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
"node_modules/nanoid": {
- "version": "3.3.11",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
- "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "version": "3.3.15",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
+ "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
"dev": true,
"funding": [
{
@@ -1833,6 +1642,19 @@
],
"license": "MIT"
},
+ "node_modules/parse5": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
+ "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^8.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -1861,9 +1683,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.12",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
- "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
+ "version": "8.5.16",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
+ "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
"dev": true,
"funding": [
{
@@ -1881,7 +1703,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.11",
+ "nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -1889,49 +1711,71 @@
"node": "^10 || ^12 || >=14"
}
},
- "node_modules/rollup": {
- "version": "4.59.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
- "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz",
+ "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/estree": "1.0.8"
+ "@oxc-project/types": "=0.138.0",
+ "@rolldown/pluginutils": "^1.0.0"
},
"bin": {
- "rollup": "dist/bin/rollup"
+ "rolldown": "bin/cli.mjs"
},
"engines": {
- "node": ">=18.0.0",
- "npm": ">=8.0.0"
+ "node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.59.0",
- "@rollup/rollup-android-arm64": "4.59.0",
- "@rollup/rollup-darwin-arm64": "4.59.0",
- "@rollup/rollup-darwin-x64": "4.59.0",
- "@rollup/rollup-freebsd-arm64": "4.59.0",
- "@rollup/rollup-freebsd-x64": "4.59.0",
- "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
- "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
- "@rollup/rollup-linux-arm64-gnu": "4.59.0",
- "@rollup/rollup-linux-arm64-musl": "4.59.0",
- "@rollup/rollup-linux-loong64-gnu": "4.59.0",
- "@rollup/rollup-linux-loong64-musl": "4.59.0",
- "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
- "@rollup/rollup-linux-ppc64-musl": "4.59.0",
- "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
- "@rollup/rollup-linux-riscv64-musl": "4.59.0",
- "@rollup/rollup-linux-s390x-gnu": "4.59.0",
- "@rollup/rollup-linux-x64-gnu": "4.59.0",
- "@rollup/rollup-linux-x64-musl": "4.59.0",
- "@rollup/rollup-openbsd-x64": "4.59.0",
- "@rollup/rollup-openharmony-arm64": "4.59.0",
- "@rollup/rollup-win32-arm64-msvc": "4.59.0",
- "@rollup/rollup-win32-ia32-msvc": "4.59.0",
- "@rollup/rollup-win32-x64-gnu": "4.59.0",
- "@rollup/rollup-win32-x64-msvc": "4.59.0",
- "fsevents": "~2.3.2"
+ "@rolldown/binding-android-arm64": "1.1.4",
+ "@rolldown/binding-darwin-arm64": "1.1.4",
+ "@rolldown/binding-darwin-x64": "1.1.4",
+ "@rolldown/binding-freebsd-x64": "1.1.4",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.1.4",
+ "@rolldown/binding-linux-arm64-gnu": "1.1.4",
+ "@rolldown/binding-linux-arm64-musl": "1.1.4",
+ "@rolldown/binding-linux-ppc64-gnu": "1.1.4",
+ "@rolldown/binding-linux-s390x-gnu": "1.1.4",
+ "@rolldown/binding-linux-x64-gnu": "1.1.4",
+ "@rolldown/binding-linux-x64-musl": "1.1.4",
+ "@rolldown/binding-openharmony-arm64": "1.1.4",
+ "@rolldown/binding-wasm32-wasi": "1.1.4",
+ "@rolldown/binding-win32-arm64-msvc": "1.1.4",
+ "@rolldown/binding-win32-x64-msvc": "1.1.4"
+ }
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
}
},
"node_modules/siginfo": {
@@ -1965,17 +1809,24 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tailwindcss": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
- "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz",
+ "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==",
"dev": true,
"license": "MIT"
},
"node_modules/tapable": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
- "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2004,14 +1855,14 @@
}
},
"node_modules/tinyglobby": {
- "version": "0.2.15",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
- "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
- "picomatch": "^4.0.3"
+ "picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
@@ -2030,19 +1881,82 @@
"node": ">=14.0.0"
}
},
+ "node_modules/tldts": {
+ "version": "7.4.6",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.6.tgz",
+ "integrity": "sha512-rbP0Gyx8b3Ae9yO//CU2wbSnQNoQ66m1nJdSbSHmnwKwzkkz/u8mERYU8T2rmlmy+bJvRNn84yNCW8gYqox44Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.4.6"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "7.4.6",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.6.tgz",
+ "integrity": "sha512-TkQNGJIhlEphpHCjKodMTSe23egUZr/g+flI2qkLgiJ/maAzSgXypSLRTNH3nCmqgayEmtcJBiLcfODSAr1xoA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tough-cookie": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
+ "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^7.0.5"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
+ "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/undici": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
+ "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
"node_modules/vite": {
- "version": "7.3.2",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
- "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
+ "version": "8.1.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz",
+ "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "esbuild": "^0.27.0",
- "fdir": "^6.5.0",
- "picomatch": "^4.0.3",
- "postcss": "^8.5.6",
- "rollup": "^4.43.0",
- "tinyglobby": "^0.2.15"
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.16",
+ "rolldown": "~1.1.3",
+ "tinyglobby": "^0.2.17"
},
"bin": {
"vite": "bin/vite.js"
@@ -2058,9 +1972,10 @@
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.3.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
- "lightningcss": "^1.21.0",
"sass": "^1.70.0",
"sass-embedded": "^1.70.0",
"stylus": ">=0.54.8",
@@ -2073,13 +1988,16 @@
"@types/node": {
"optional": true
},
- "jiti": {
+ "@vitejs/devtools": {
"optional": true
},
- "less": {
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
"optional": true
},
- "lightningcss": {
+ "less": {
"optional": true
},
"sass": {
@@ -2195,6 +2113,54 @@
}
}
},
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
+ "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
+ "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "16.0.1",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
+ "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@exodus/bytes": "^1.11.0",
+ "tr46": "^6.0.0",
+ "webidl-conversions": "^8.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ }
+ },
"node_modules/why-is-node-running": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
@@ -2211,6 +2177,23 @@
"engines": {
"node": ">=8"
}
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
}
}
}
diff --git a/package.json b/package.json
index a05160c..0141e12 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "catalog",
"version": "1.0.0",
- "description": "Repository for web-based ABC-Center code, data, model, and spaces catalog. This catalog is designed to use the GitHub API for searching all code repositories created under the [ABC-Center GitHub Organization](https://github.com/ABC-Center) and the Hugging Face API for searching all dataset, model, and spaces repositories created under the [ABC-Center Hugging Face Organization](https://huggingface.co/ABC-Center).",
+ "description": "Repository for web-based ABC-Center code, data, model, and demos catalog. This catalog is designed to use the GitHub API for searching all code repositories created under the [ABC-Center GitHub Organization](https://github.com/ABC-Center) and the Hugging Face API for searching all dataset, model, and spaces repositories created under the [ABC-Center Hugging Face Organization](https://huggingface.co/ABC-Center).",
"main": "main.js",
"scripts": {
"dev": "vite",
@@ -39,12 +39,13 @@
},
"homepage": "https://github.com/ABC-Center/catalog#readme",
"dependencies": {
- "js-yaml": "^4.1.0"
+ "js-yaml": "^5.2.1"
},
"devDependencies": {
- "@tailwindcss/vite": "^4.2.1",
+ "@tailwindcss/vite": "^4.3.2",
"tailwindcss": "^4.2.1",
- "vite": "^7.3.2",
- "vitest": "^4.1.5"
+ "vite": "^8.1.3",
+ "vitest": "^4.1.5",
+ "jsdom": "^29.1.1"
}
}
diff --git a/public/config.yaml b/public/config.yaml
index d15d0ee..2f05b58 100644
--- a/public/config.yaml
+++ b/public/config.yaml
@@ -2,7 +2,8 @@
# Customize these values to personalize the catalog for your organization
# Organization & Repository Settings
-ORGANIZATION_NAME: ABC-Center # GitHub/Hugging Face organization name for API calls (case-sensitive for HF)
+ORGANIZATION_NAME: ABC-Center # Codebase platform organization name (for API calls)
+HF_ORGANIZATION_NAME: ABC-Center # Hugging Face organization name (case-sensitive, for API calls)
ORG_NAME: ABC-Center # Display name for GitHub organization (can differ from API name)
CATALOG_REPO_NAME: catalog # Repository name for the catalog itself (used for stats badge)
@@ -21,9 +22,9 @@ COLORS:
tag: "#4fb797a1" # Tag background color (ABC turquoise 63% alpha)
# API & Behavior Settings
-# Codebase platform for API calls and link generation. Supported values: "github", pending: "codeberg" or "gitlab".
+# Codebase platform for API calls and link generation. Supported values: "github", "codeberg", or "gitlab".
PLATFORM: github
-# Dataset, model, and space (demo) default: Hugging Face, other platforms would require a custom module
+# Dataset, model, and demo default: Hugging Face, other platforms would require a custom module
API_BASE_URL: "https://huggingface.co/api/"
# Define "new" repository criteria
REFRESH_INTERVAL_DAYS: 30
@@ -41,7 +42,8 @@ ADDITIONAL_REPOS:
- "kitzeslab/Kitzes_MethodsEcolEvo_manuscript_2025"
# Array of Hugging Face repos from outside the org to include.
-# Each entry must specify "repo" (owner/name) and "type" (datasets, models, or spaces).
+# Each entry must specify "repo" (owner/name) and "type" (datasets, models, or spaces/demos).
+# Organization names are case-sensitive in the Hugging Face API.
# ADDITIONAL_HF_REPOS:
# - repo: "user/dataset-name"
# type: "datasets"
diff --git a/scripts/export-tags.js b/scripts/export-tags.js
index f3ec2ba..7e762f2 100644
--- a/scripts/export-tags.js
+++ b/scripts/export-tags.js
@@ -12,14 +12,14 @@
// public/config.yaml
//
-import fs from 'fs';
+import { readFileSync, writeFileSync } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
-import jsYaml from 'js-yaml';
+import { load } from 'js-yaml';
import { validateConfig } from '../src/validateConfig.js';
-import { getPlatformApiUrls } from '../src/defineApiUrls.js';
-import { getPlatformDisplay } from '../src/defineRibbonVals.js';
-import { filterNewAdditionalEntries } from '../src/filterNewAdditionalEntries.js';
+import { filterNewAdditionalEntries } from '../src/utils/filterNewAdditionalEntries.js';
+import { getPlatformVals, getPlatformApiUrls } from '../src/utils/definePlatformVals.js';
+import { getPlatformHeaders } from './platformScriptHelpers.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -27,7 +27,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Parse config.yaml to extract the CONFIG object values we need
// ---------------------------------------------------------------------------
const configPath = path.resolve(__dirname, '../public/config.yaml');
-const rawConfig = jsYaml.load(fs.readFileSync(configPath, 'utf8'));
+const rawConfig = load(readFileSync(configPath, 'utf8'));
const errors = validateConfig(rawConfig);
if (errors.length) {
@@ -35,21 +35,21 @@ if (errors.length) {
}
const CONFIG = rawConfig;
-const { ORGANIZATION_NAME, PLATFORM, API_BASE_URL, ADDITIONAL_REPOS } = CONFIG;
+const { ORGANIZATION_NAME, HF_ORGANIZATION_NAME, API_BASE_URL, ADDITIONAL_REPOS } = CONFIG;
const ADDITIONAL_HF_REPOS = CONFIG.ADDITIONAL_HF_REPOS;
// ---------------------------------------------------------------------------
// Fetch helpers
// ---------------------------------------------------------------------------
-// Update this section as needed for non-GitHub code platforms (e.g., Codeberg or GitLab)
-const GITHUB_TOKEN = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
+/* Update the corresponding workflow and token as needed for non-GitHub code platforms (e.g., Codeberg or GitLab)
+* `headers['Accept']` is only needed for GitHub to avoid 403 errors on some endpoints.
+*/
+const platform = (CONFIG.PLATFORM || 'github').toLowerCase();
+const { org: ORG_API_URL, repo: REPO_API_URL } = getPlatformApiUrls(platform, ORGANIZATION_NAME);
+const { profileRepo, fullNameKey, forkKey, encodeRepoId } = getPlatformVals(platform);
const get = async (url) => {
- const headers = {};
- if (GITHUB_TOKEN && url.includes('api.github.com')) {
- headers['Authorization'] = `Bearer ${GITHUB_TOKEN}`;
- headers['Accept'] = 'application/vnd.github+json';
- }
+ const headers = getPlatformHeaders(url, platform, ORGANIZATION_NAME);
const res = await fetch(url, { headers });
if (!res.ok) throw new Error(`HTTP ${res.status} ā ${url}`);
return { json: await res.json(), headers: res.headers };
@@ -59,11 +59,9 @@ const get = async (url) => {
// Tag collection
// ---------------------------------------------------------------------------
const allTags = new Set();
-const { org: ORG_API_URL, repo: REPO_API_URL } = getPlatformApiUrls(PLATFORM, ORGANIZATION_NAME);
const collectCodePlatformTags = async () => {
- const platformDisplay = getPlatformDisplay(PLATFORM);
- console.log(`Fetching ${platformDisplay.displayName || PLATFORM} repos...`);
+ console.log(`Fetching ${platform} repos...`);
let allRepos = [];
let nextUrl = `${ORG_API_URL}`;
@@ -78,26 +76,26 @@ const collectCodePlatformTags = async () => {
// Additional repos
const additionalData = await Promise.all(
ADDITIONAL_REPOS.map(ownerRepo =>
- get(`${REPO_API_URL}${ownerRepo}`)
+ get(`${REPO_API_URL}${encodeRepoId(ownerRepo)}`)
.then(({ json }) => json)
.catch(() => null)
)
);
const additionalRepos = additionalData.filter(Boolean);
- const additionalNames = new Set(additionalRepos.map(r => r.full_name));
- const orgNonForks = allRepos.filter(r => r.name !== '.github' && !r.fork && !additionalNames.has(r.full_name));
+ const additionalNames = new Set(additionalRepos.map(r => r[fullNameKey]));
+ const orgNonForks = allRepos.filter(r => r.name !== profileRepo && !r[forkKey] && !additionalNames.has(r[fullNameKey]));
[...additionalRepos, ...orgNonForks].forEach(repo => {
(repo.topics || []).forEach(t => allTags.add(t.toLowerCase()));
});
- console.log(` ${platformDisplay.displayName || PLATFORM}: processed ${orgNonForks.length} org repos + ${additionalRepos.length} additional repos`);
+ console.log(` ${platform}: processed ${orgNonForks.length} org repos + ${additionalRepos.length} additional repos`);
};
const collectHFTags = async (repoType) => {
console.log(`Fetching HF ${repoType}...`);
- let items = (await get(`${API_BASE_URL}${repoType}?author=${ORGANIZATION_NAME}&full=true`)).json;
+ let items = (await get(`${API_BASE_URL}${repoType}?author=${HF_ORGANIZATION_NAME}&full=true`)).json;
// Fetch additional HF repos of this type
const additionalForType = ADDITIONAL_HF_REPOS.filter(entry => entry.type === repoType);
@@ -145,7 +143,7 @@ const collectHFTags = async (repoType) => {
const sorted = Array.from(allTags).sort();
const outPath = path.resolve(__dirname, 'tag-export.txt');
- fs.writeFileSync(outPath, sorted.join('\n') + '\n', 'utf8');
+ writeFileSync(outPath, sorted.join('\n') + '\n', 'utf8');
console.log(`\nDone. ${sorted.length} unique tags written to scripts/tag-export.txt`);
} catch (err) {
diff --git a/scripts/fetch-releases.js b/scripts/fetch-releases.js
index e868f25..ca68010 100644
--- a/scripts/fetch-releases.js
+++ b/scripts/fetch-releases.js
@@ -1,34 +1,40 @@
// scripts/fetch-releases.js
-// Build-time script: fetches the latest GitHub release for each code repo
+// Build-time script: fetches the latest release for each code repo
// and writes public/releases.json. Runs as part of `npm run build`.
import { readFileSync, writeFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
-import jsYaml from 'js-yaml';
+import { load } from 'js-yaml';
import { validateConfig } from '../src/validateConfig.js';
-import { getPlatformApiUrls } from '../src/defineApiUrls.js';
+import { getPlatformVals, getPlatformApiUrls } from '../src/utils/definePlatformVals.js';
+import { getPlatformReleaseVals, getPlatformHeaders } from './platformScriptHelpers.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Load CONFIG from public/config.yaml
const configPath = join(__dirname, '../public/config.yaml');
-const CONFIG = jsYaml.load(readFileSync(configPath, 'utf8'));
+const CONFIG = load(readFileSync(configPath, 'utf8'));
const errors = validateConfig(CONFIG);
if (errors.length) {
throw new Error(`Invalid config at ${configPath}: ${errors.join('; ')}`);
}
-const TOKEN = process.env.GITHUB_TOKEN;
-const headers = TOKEN
- ? { Authorization: `Bearer ${TOKEN}`, 'User-Agent': 'catalog-build-script' }
- : { 'User-Agent': 'catalog-build-script' };
+/**
+ * Define platform-specific variables needed for script to fetch releases, including API URLs, headers, and repo data keys.
+ * Update the corresponding workflow and token as needed for non-GitHub code platforms (e.g., Codeberg or GitLab).
+*/
+const platform = (CONFIG.PLATFORM || 'github').toLowerCase();
+
+const { org: ORG_API_URL, repo: REPO_API_URL, releaseSuffix: RELEASE_SUFFIX } = getPlatformApiUrls(platform, CONFIG.ORGANIZATION_NAME);
+const { releasePublishedAtKey, getReleaseUrl } = getPlatformReleaseVals(platform);
+const { profileRepo, fullNameKey, forkKey, encodeRepoId } = getPlatformVals(platform);
+const headers = getPlatformHeaders(ORG_API_URL, platform, CONFIG.ORGANIZATION_NAME);
const TWO_WEEKS_MS = 14 * 24 * 60 * 60 * 1000;
// Step 1: Fetch all public org repos (paginated, same logic as main.js)
-const { org: ORG_API_URL, repo: REPO_API_URL } = getPlatformApiUrls(CONFIG.PLATFORM, CONFIG.ORGANIZATION_NAME);
let allOrgRepos = [];
let nextUrl = `${ORG_API_URL}`;
while (nextUrl) {
@@ -47,29 +53,64 @@ while (nextUrl) {
// Step 2: Collect all code repo IDs (mirrors main.js deduplication logic)
const additionalRepoIds = CONFIG.ADDITIONAL_REPOS || [];
const additionalRepoSet = new Set(additionalRepoIds);
-const orgNonForks = allOrgRepos.filter(r => r.name !== '.github' && !r.fork && !additionalRepoSet.has(r.full_name));
+const orgNonForks = allOrgRepos.filter(r => r.name !== profileRepo && !r[forkKey] && !additionalRepoSet.has(r[fullNameKey]));
const repoIds = [
...additionalRepoIds,
- ...orgNonForks.map(r => r.full_name),
+ ...orgNonForks.map(r => r[fullNameKey]),
];
-// Step 3: Fetch latest release for each repo
-const releases = {};
-for (const id of repoIds) {
+// Step 3: Fetch latest releases in parallel with bounded concurrency (max 5 concurrent requests), key-value mapping for each repo ID
+
+/**
+ * Zero-dependency bounded concurrency worker pool
+ * @param {*} items - object to fetch
+ * @param {int} concurrency - maximum number of concurrent requests
+ * @param {*} fn - function to apply to each item
+ * @returns {Promise} - A promise resolving to an array of results
+ */
+async function mapConcurrent(items, concurrency, fn) {
+ const results = new Array(items.length);
+ let index = 0;
+ const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
+ while (index < items.length) {
+ const i = index++;
+ results[i] = await fn(items[i]);
+ }
+ });
+ await Promise.all(workers);
+ return results;
+}
+
+/**
+ * Fetch the latest release for a given repo ID.
+ * @param {String} id - repo ID ('owner/repo' format)
+ * @returns {Promise<[String, Object|null]>} - A promise resolving to a tuple of [repo ID, release data or null if not found]
+ */
+async function fetchRepoRelease(id) {
try {
- const res = await fetch(`${REPO_API_URL}${id}/releases/latest`, { headers });
- if (!res.ok) { releases[id] = null; continue; }
+ const res = await fetch(`${REPO_API_URL}${encodeRepoId(id)}/${RELEASE_SUFFIX}`, { headers });
+ if (!res.ok) return [id, null];
+
const data = await res.json();
- releases[id] = {
+ const publishedAt = data[releasePublishedAtKey];
+ // gitlab's release API returns a different structure than GitHub/Codeberg
+ return [id, {
tag: data.tag_name,
- url: data.html_url,
- publishedAt: data.published_at,
- isNew: (Date.now() - new Date(data.published_at)) < TWO_WEEKS_MS,
- };
- } catch {
- releases[id] = null;
+ url: getReleaseUrl(data, id),
+ publishedAt: publishedAt,
+ isNew: (Date.now() - new Date(publishedAt)) < TWO_WEEKS_MS,
+ }];
+ } catch (err) {
+ console.warn(`[fetch-releases] Network error for repo "${id}":`, err.message);
+ return [id, null];
}
-}
+};
+
+// fetch the repo releases
+const releaseEntries = await mapConcurrent(repoIds, 5, fetchRepoRelease);
+
+// Convert [id, releaseData] pairs into an object for JSON output
+const releases = Object.fromEntries(releaseEntries);
writeFileSync(join(__dirname, '../public/releases.json'), JSON.stringify(releases));
console.log(`Wrote releases.json (${Object.keys(releases).length} repos)`);
diff --git a/scripts/platformScriptHelpers.js b/scripts/platformScriptHelpers.js
new file mode 100644
index 0000000..9354f32
--- /dev/null
+++ b/scripts/platformScriptHelpers.js
@@ -0,0 +1,73 @@
+/**
+ * Script utilities for platform-specific API calls and headers when exporting tags or fetching repo release data.
+ * Collection of functions to define platform-specific values needed only in build-time/workflow scripts.
+ * Each function returns dictionaries with the platform values that have been keyed by platform name (e.g., 'github', 'gitlab', 'codeberg') and potentially other instance-specific values (e.g., organizationName, repoApiUrl).
+ * Platform and organization name are defined from config.yaml and passed to the functions requiring them.
+*/
+
+
+/**
+ * Utility function to get platform-specific values for repo data keys (e.g., star count and profile repo name).
+ * @param {string} platform - 'github', 'gitlab', or 'codeberg'
+ * @returns {Object} platformVals - An object containing platform-specific values for repo data keys (e.g., release URL and published date key)
+ */
+export function getPlatformReleaseVals(platform) {
+ const platformVals = {
+ github: {
+ getReleaseUrl: (data) => data.html_url,
+ releasePublishedAtKey: 'published_at'
+ },
+ gitlab: {
+ getReleaseUrl: (data, repoId) => {
+ const tag = encodeURIComponent(data.tag_name);
+ return `https://gitlab.com/${repoId}/-/releases/${tag}`;
+ },
+ releasePublishedAtKey: 'released_at'
+ },
+ codeberg: {
+ getReleaseUrl: (data) => data.html_url,
+ releasePublishedAtKey: 'published_at'
+ }
+};
+ return platformVals[platform.toLowerCase()];
+}
+
+// Define platform-specific token authentication and API host information for scripts to use when making API calls.
+const tokenAuthByPlatform = {
+ github: {
+ token: process.env.GITHUB_TOKEN || process.env.GH_TOKEN,
+ apiHost : 'api.github.com',
+ authScheme: 'Bearer',
+ },
+ gitlab: {
+ token: process.env.GITLAB_TOKEN,
+ apiHost : 'gitlab.com/api/v4',
+ authScheme: 'Bearer',
+ },
+ codeberg: {
+ token: process.env.CODEBERG_TOKEN,
+ apiHost : 'codeberg.org/api/v1',
+ authScheme: 'token'
+ },
+};
+/**
+ * Utility function for scripts to get platform-specific headers
+* Update the corresponding workflow as needed for non-GitHub code platforms (e.g., Codeberg or GitLab)
+* `headers['Accept']` is only needed for GitHub to avoid 403 errors on some endpoints.
+* @param {string} url - URL to check for the platform API host
+* @param {string} platform - 'github', 'gitlab', or 'codeberg'
+* @param {string} orgName - The name of the organization as used in API calls
+* @returns {object} headers - headers object to use for fetch requests
+*/
+export function getPlatformHeaders(url, platform, orgName) {
+ const { token, apiHost, authScheme } = tokenAuthByPlatform[platform];
+ const cleanedOrgName = orgName.replace(/[^a-zA-Z0-9_-]/g, '');
+ const headers = { 'User-Agent': `${cleanedOrgName}-catalog-build-script` };
+ if (token && apiHost && url.includes(apiHost)) {
+ headers['Authorization'] = `${authScheme} ${token}`;
+ if (platform === 'github') {
+ headers['Accept'] = 'application/vnd.github+json';
+ }
+ }
+ return headers;
+}
diff --git a/src/api/fetchCodeRepos.js b/src/api/fetchCodeRepos.js
new file mode 100644
index 0000000..c918ac4
--- /dev/null
+++ b/src/api/fetchCodeRepos.js
@@ -0,0 +1,123 @@
+import { handleError } from '../ui/render.js';
+import { getPlatformVals } from '../utils/definePlatformVals.js';
+import { getPlatformDisplay } from '../utils/defineRibbonVals.js';
+import { normalizeTag, filterDisplayTags } from '../utils/normalizeTag.js';
+
+/**
+ * Function for fetching code repositories from the specified platform (GitHub, GitLab, or Codeberg).
+ * Both org-owned (non-forks) and additional repos are fetched; metadata is processed for each repo.
+ * It also determines if a repo is "new" based on the provided refresh interval.
+ * @async
+ * @param {string} platform - 'github', 'gitlab', or 'codeberg'
+ * @param {Array} additionalRepos - An array of additional "owner/repo" strings to include in addition to non-forked
+ * org repos.
+ * @param {string} orgApiUrl - The API URL for fetching organization repos
+ * @param {string} repoApiUrl - The API URL for fetching individual repo details
+ * @param {number} refreshIntervalDays - The cutoff in days for determining if a repo is "new"
+ * @param {Record} releasesMap -
+ * Release information for repos, keyed by full_name
+ * @returns {Promise} processedItems - A promise resolving to an array of code repositories
+ */
+export async function fetchCodeRepos(
+ platform,
+ additionalRepos,
+ orgApiUrl,
+ repoApiUrl,
+ refreshIntervalDays,
+ releasesMap
+) {
+
+ let allRepos = [];
+ let nextUrl = `${orgApiUrl}`;
+ // get platform-specific keys
+ const { starsKey, profileRepo, fullNameKey, updatedAtKey, forkKey, urlKey, encodeRepoId } = getPlatformVals(platform);
+ try {
+ while (nextUrl) {
+ const ghResponse = await fetch(nextUrl);
+
+ if (!ghResponse.ok) {
+ const platformDisplay = getPlatformDisplay(platform);
+ throw new Error(`${platformDisplay.displayName || platform} error: ${ghResponse.status}`);
+ }
+
+ const page = await ghResponse.json();
+ allRepos = allRepos.concat(page);
+
+ // Parse the Link header to find the next page URL, if any
+ const linkHeader = ghResponse.headers.get('Link');
+ const match = linkHeader && linkHeader.match(/<([^>]+)>;\s*rel="next"/);
+ nextUrl = match ? match[1] : null;
+ }
+
+ // For org-owned entries in additionalRepos, reuse data already in allRepos to avoid redundant API calls.
+ // Only fetch entries that belong to a different org (external repos).
+ const allReposByFullName = new Map(allRepos.map(r => [r[fullNameKey], r]));
+ const toFetch = additionalRepos.filter(ownerRepo => !allReposByFullName.has(ownerRepo));
+ const fromAllRepos = additionalRepos.map(ownerRepo => allReposByFullName.get(ownerRepo)).filter(Boolean);
+
+ const fetchedExternalData = await Promise.all(
+ toFetch.map(ownerRepo =>
+ fetch(`${repoApiUrl}${encodeRepoId(ownerRepo)}`)
+ .then(r => {
+ if (!r.ok) {
+ console.warn(`Failed to fetch additional repo "${ownerRepo}": HTTP ${r.status}`);
+ return null;
+ }
+ return r.json();
+ })
+ .catch(err => {
+ console.warn(`Network error fetching additional repo "${ownerRepo}":`, err);
+ return null;
+ })
+ )
+ );
+ const filteredAdditionalRepos = [...fromAllRepos, ...fetchedExternalData.filter(Boolean)];
+
+ // Keep only non-forks from org; deduplicate against additional repos by full_name
+ const orgRepoNames = new Set(filteredAdditionalRepos.map(r => r[fullNameKey]));
+ const orgNonForks = allRepos.filter(repo =>
+ repo.name !== profileRepo &&
+ !repo[forkKey] &&
+ !orgRepoNames.has(repo[fullNameKey]));
+
+ // Process additional repos and all remaining org non-forks to include metadata and 'new' flag as appropriate
+ let processedItems = [...filteredAdditionalRepos, ...orgNonForks]
+ .map(repo => {
+ const createdAt = new Date(repo.created_at);
+ const lastModified = new Date(repo[updatedAtKey]);
+ const isNew = (new Date() - createdAt) / (1000 * 60 * 60 * 24) < refreshIntervalDays;
+
+ const rawTags = (repo.topics || []).map(t => t.toLowerCase());
+ const tags = [...new Set(rawTags.flatMap(t => normalizeTag(t)).filter(Boolean))];
+ const displayTags = filterDisplayTags(rawTags);
+
+ const release = releasesMap[repo[fullNameKey]] ?? null;
+
+ return {
+ id: repo[fullNameKey], // "Imageomics/", used as backup if can't get repo.name
+ repoType: "code",
+ createdAt,
+ lastModified,
+ isNew,
+ archived: repo.archived || false,
+ tags,
+ rawTags,
+ displayTags,
+ description: repo.description, // fallback in display (render.js)
+ html_url: repo[urlKey],
+ hasNewRelease: release?.isNew ?? false,
+ latestReleaseUrl: release?.url ?? null,
+ latestReleaseTag: release?.tag ?? null,
+ cardData: {
+ pretty_name: repo.name, // , the one used for card title display
+ stars: repo[starsKey] ?? 0
+ }
+ };
+ });
+
+ return processedItems;
+ } catch (error) {
+ handleError(error, `Failed to fetch code from ${platform}. Please check your network connection or the API.`);
+ return [];
+ }
+}
diff --git a/src/api/fetchHfRepos.js b/src/api/fetchHfRepos.js
new file mode 100644
index 0000000..45fb7da
--- /dev/null
+++ b/src/api/fetchHfRepos.js
@@ -0,0 +1,105 @@
+import { normalizeTag, filterDisplayTags } from '../utils/normalizeTag.js';
+import { handleError } from '../ui/render.js';
+import { filterNewAdditionalEntries } from '../utils/filterNewAdditionalEntries.js';
+
+/**
+ * Function for fetching Hugging Face repositories (models, datasets, or spaces) from the specified organization.
+ * It fetches both org-owned and additional repos, processes metadata, and determines if a repo is "new" based on the provided refresh interval.
+ * @async
+ * @param {string} repoType - The type of repository to fetch ("datasets", "models", or "spaces").
+ * @param {Array} additionalHfRepos - Array of Hugging Face repos, by type to include in addition to org-owned repos
+ * @param {string} apiBaseUrl - The base URL for the Hugging Face API
+ * @param {string} hfOrgName - The Hugging Face organization name
+ * @param {number} refreshIntervalDays - The cutoff in days for determining if a repo is "new"
+ * @returns {Promise} processedItems - A promise resolving to an array of Hugging Face repositories with metadata
+ */
+export async function fetchHfRepos(
+ repoType,
+ additionalHfRepos,
+ apiBaseUrl,
+ hfOrgName,
+ refreshIntervalDays
+) {
+
+ try {
+ // hugging face api requests for datasets/models/spaces
+ const response = await fetch(`${apiBaseUrl}${repoType}?author=${hfOrgName}&full=true`);
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ let hfItems = await response.json();
+
+ // Fetch additional HF repos of this type from outside the org
+ const additionalForType = additionalHfRepos.filter(entry => entry.type === repoType);
+ if (additionalForType.length) {
+ const existingIds = new Set(hfItems.map(item => item.id));
+ const toFetch = filterNewAdditionalEntries(existingIds, additionalForType);
+
+ const fetched = await Promise.all(
+ toFetch.map(entry =>
+ fetch(`${apiBaseUrl}${repoType}/${entry.repo}`)
+ .then(r => {
+ if (!r.ok) {
+ console.warn(`Failed to fetch additional HF repo "${entry.repo}": HTTP ${r.status}`);
+ return null;
+ }
+ return r.json();
+ })
+ .catch(err => {
+ console.warn(`Network error fetching additional HF repo "${entry.repo}":`, err);
+ return null;
+ })
+ )
+ );
+ hfItems = [...hfItems, ...fetched.filter(item => item && !existingIds.has(item.id))];
+ }
+
+ // Step 2: If we are fetching models, get the full details for each one.
+ if (repoType === 'models') {
+ const detailPromises = hfItems.map(item =>
+ fetch(`${apiBaseUrl}models/${item.id}`).then(res => {
+ if (!res.ok) {
+ console.error(`Failed to fetch details for ${item.id}`);
+ return null; // Return null for failed requests
+ }
+ return res.json();
+ })
+ );
+
+ // Wait for all detail requests to complete in parallel.
+ const detailedItems = await Promise.all(detailPromises);
+
+ // Filter out any models that failed to fetch and assign the detailed list.
+ hfItems = detailedItems.filter(Boolean);
+ }
+ // Process the data to include metadata and a 'new' flag
+ const processedItems = hfItems.map(item => {
+ const createdAt = new Date(item.createdAt);
+ const lastModified = new Date(item.lastModified);
+ const isNew = (new Date() - createdAt) / (1000 * 60 * 60 * 24) < refreshIntervalDays;
+
+ // Extract tags from the YAML metadata (handling different structures)
+ const rawTags = (item.cardData?.tags || item.tags || []).map(t => String(t).toLowerCase());
+ const tags = [...new Set(rawTags.flatMap(t => normalizeTag(t)).filter(Boolean))];
+ const displayTags = filterDisplayTags(rawTags);
+
+ return {
+ ...item,
+ repoType,
+ createdAt,
+ lastModified,
+ isNew,
+ archived: false,
+ likes: item.likes || 0,
+ tags,
+ rawTags,
+ displayTags
+ };
+ });
+
+ return processedItems;
+ } catch (error) {
+ handleError(error, `Failed to fetch ${repoType} from Hugging Face. Please check your network connection or the API.`);
+ return [];
+ }
+}
diff --git a/src/api/fetchStats.js b/src/api/fetchStats.js
new file mode 100644
index 0000000..c43c47d
--- /dev/null
+++ b/src/api/fetchStats.js
@@ -0,0 +1,42 @@
+/**
+ * Fetches statistics for the Catalog repository itself (Stars, Forks, Version)
+ * populates the badge in the top right corner.
+ * @param {string} repoApiUrl - The base API URL for the code platform (e.g., GitHub API URL)
+ * @param {string} organizationName - The organization name for the catalog repository
+ * @param {string} catalogRepoName - The repository name for the catalog itself
+ * @param {string} releaseSuffix - The suffix used for fetching the latest release information (e.g., 'releases/latest')
+ * @returns {Promise} - A promise that resolves when the stats have been fetched and displayed
+ */
+export const fetchCatalogStats = async (repoApiUrl, organizationName, catalogRepoName, releaseSuffix) => {
+ // Helper: Updates text, shows the specific stat, and ensures the divider is visible
+ const update = (textId, containerId, value) => {
+ const el = document.getElementById(textId);
+ const container = document.getElementById(containerId);
+ if (el && container && value !== undefined) {
+ el.innerText = value;
+ if (value != 0) {
+ container.classList.remove('hidden');
+ container.classList.add('flex');
+ }
+ }
+ };
+
+ // Need to URL-encode the owner/repo for GitLab API ('%2F' instead of '/'), but need '/' for GitHub and Codeberg.
+ const ownerRepo = repoApiUrl.includes("gitlab") ? `${organizationName}%2F${catalogRepoName}` : `${organizationName}/${catalogRepoName}`;
+
+ try {
+ // platform isn't passed, forks_count is shared
+ // 1. Get Stars & Forks
+ const repo = await fetch(`${repoApiUrl}${ownerRepo}`).then(r => r.ok ? r.json() : {});
+ const star_count = repo.stargazers_count ?? repo.stars_count ?? repo.star_count;
+ if (star_count !== undefined) update('gh-stars', 'gh-star-container', star_count);
+ if (repo.forks_count !== undefined) update('gh-forks', 'gh-fork-container', repo.forks_count);
+
+ // 2. Get Version (Tag)
+ const release = await fetch(`${repoApiUrl}${ownerRepo}/${releaseSuffix}`).then(r => r.ok ? r.json() : {});
+ if (release.tag_name !== undefined) update('gh-tag', 'gh-version-container', release.tag_name);
+
+ } catch (e) {
+ console.warn("Could not fetch Code Repo stats", e);
+ }
+};
diff --git a/src/assets/GitHub_Invertocat.svg b/src/assets/GitHub_Invertocat.svg
new file mode 100755
index 0000000..5300969
--- /dev/null
+++ b/src/assets/GitHub_Invertocat.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/codeberg-logo_icon_white.svg b/src/assets/codeberg-logo_icon_white.svg
new file mode 100644
index 0000000..74a0fa9
--- /dev/null
+++ b/src/assets/codeberg-logo_icon_white.svg
@@ -0,0 +1,164 @@
+
+
+ Codeberg logo
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+ Codeberg logo
+
+
+
+ Robert Martinez
+
+
+
+
+ Codeberg and the Codeberg Logo are trademarks of Codeberg e.V.
+
+
+ 2020-04-09
+
+
+ Codeberg e.V.
+
+
+ codeberg.org
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/assets/gitlab-logo-700-rgb.svg b/src/assets/gitlab-logo-700-rgb.svg
new file mode 100644
index 0000000..72e3232
--- /dev/null
+++ b/src/assets/gitlab-logo-700-rgb.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/defineApiUrls.js b/src/defineApiUrls.js
deleted file mode 100644
index 5b20e43..0000000
--- a/src/defineApiUrls.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Defines API URLs based on the selected platform (GitHub, note: GitLab and Codeberg support under development).
- * This allows the rest of the codebase to use these constants when making API calls,
- * abstracting away platform-specific URL structures.
- *
- * Usage: import { getPlatformApiUrls } from './defineApiUrls.js';
- *
- * Input: platform and organizationName (e.g., 'github' and 'imageomics'), defined from config.yaml and passed to this function.
- * Output: platformApiUrls[platform] = { org: ORG_API_URL, repo: REPO_API_URL }
- */
-
-/**
- * Utility function to get the platform-specific API URLs for organization repos and individual repo details.
- * @param {string} platform - 'github', pending: 'gitlab', or 'codeberg'
- * @param {string} organizationName - The name of the organization (used in URL construction)
- * @returns {object} An object containing ORG_API_URL and REPO_API_URL
- */
-export function getPlatformApiUrls(platform, organizationName) {
- const platformApiUrls = {
- github: {
- org: `https://api.github.com/orgs/${organizationName}/repos?type=public&per_page=100`,
- repo: "https://api.github.com/repos/"
- },
- // gitlab: {
- // org: `https://gitlab.com/api/v4/groups/${organizationName}/projects?per_page=100`,
- // repo: "https://gitlab.com/api/v4/projects/"
- // },
- // codeberg: {
- // org: `https://codeberg.org/api/v1/orgs/${organizationName}/repos?limit=50`,
- // repo: "https://codeberg.org/api/v1/repos/"
- // }
- };
- return platformApiUrls[platform.toLowerCase()];
-}
diff --git a/src/defineRibbonVals.js b/src/defineRibbonVals.js
deleted file mode 100644
index 9ab64a0..0000000
--- a/src/defineRibbonVals.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * A collection of platform-specific display information (e.g., SVG path data)
- * for code developer platforms including GitHub, pending: GitLab and Codeberg.
- * Used for linking to repo and rendering icon and platform name in the ribbon component of the UI.
- * All paths are optimized for a 0 0 24 24 viewBox.
- *
- * Usage: import { getPlatformDisplay } from './defineRibbonVals.js';
- *
- * Input: platform (e.g., 'github'), defined from config.yaml and passed to this function.
- * Output: platformDisplays[platform] = { path: SVG_PATH_DATA, viewBox: VIEWBOX_DATA,
- * displayName: DISPLAY_NAME, ribbonUrl: RIBBON_URL }
- */
-
-/**
- * Utility function to get the full platform display information
- * @param {string} platform - 'github', 'gitlab', or 'codeberg'
- * @returns {object|null}
- */
-export function getPlatformDisplay(platform) {
- const platformDisplays = {
- github: {
- path: "M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z",
- viewBox: "0 0 24 24",
- displayName: "GitHub",
- ribbonUrl: "https://github.com/"
- },
- // gitlab: {
- // path: "m23.71 11.83-2.13-6.52a.89.89 0 0 0-1.7 0l-2.13 6.52H6.25L4.12 5.31a.89.89 0 0 0-1.7 0L.29 11.83a1 1 0 0 0 .37 1.13l11.34 8.24 11.34-8.24a1 1 0 0 0 .37-1.13Z",
- // viewBox: "0 0 24 24",
- // color: "#FC6D26",
- // displayName: "GitLab",
- // ribbonUrl: "https://gitlab.com/"
- //},
- // codeberg: {
- // path: "M12.004 2.378a.636.636 0 0 0-.547.31L1.258 19.874a.636.636 0 0 0 .547.954h1.94c.143 0 .278-.074.354-.197L12 6.544l7.89 14.087a.406.406 0 0 0 .354.197h2.012a.636.636 0 0 0 .546-.954L12.551 2.688a.636.636 0 0 0-.547-.31Z",
- // viewBox: "0 0 24 24",
- // color: "#2185d0",
- // displayName: "Codeberg",
- // ribbonUrl: "https://codeberg.org/"
- //}
- };
- return platformDisplays[platform.toLowerCase()];
-}
diff --git a/src/normalizeTag.js b/src/normalizeTag.js
deleted file mode 100644
index 56d7ba0..0000000
--- a/src/normalizeTag.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Normalizes a raw tag string.
- * - Returns null for Hugging Face system metadata tags (tags containing a colon).
- * - Maps known aliases to their canonical tag(s) via tagLookup.
- * - Falls back to the lowercased original if no mapping exists.
- * @param {string} tag
- * @param {Object} tagLookup - Reverse lookup map: lowercased alias ā [canonical tags]
- * @returns {string[]|null}
- */
-export function normalizeTag(tag, tagLookup) {
- const lower = String(tag).toLowerCase();
- // OPTION LINE -- REMOVE IF UNWANTED
- // Removes Hugging Face auto-generated system tags (e.g. "license:mit", "format:parquet").
- // These are identified by the presence of a colon. To include auto-generated tags in the
- // catalog, remove the following line.
- if (lower.includes(':')) return null;
- return tagLookup[lower] ?? [lower];
-}
diff --git a/src/ui/initUserInterface.js b/src/ui/initUserInterface.js
new file mode 100644
index 0000000..c8c250d
--- /dev/null
+++ b/src/ui/initUserInterface.js
@@ -0,0 +1,106 @@
+import { getPlatformDisplay } from '../utils/defineRibbonVals.js';
+
+/**
+ * Initializes UI elements from configuration values.
+ * This sets up the header, logo, repo ribbon, and dynamic styles.
+ * @param {Object} config - The loaded configuration object.
+ */
+export const initializeUIFromConfig = (config) => {
+ // Apply CSS custom properties and document metadata
+ document.title = config.CATALOG_TITLE || `${config.ORG_NAME} Catalog`;
+ document.documentElement.style.setProperty('--color-primary', config.COLORS?.primary || '#92991c');
+ document.documentElement.style.setProperty('--color-secondary', config.COLORS?.secondary || '#5d8095');
+ document.documentElement.style.setProperty('--color-accent', config.COLORS?.accent || '#0097b2');
+ document.documentElement.style.setProperty('--color-accent-dark', config.COLORS?.accentDark || '#4fd1eb');
+ document.documentElement.style.setProperty('--color-tag', config.COLORS?.tag || '#9bcb5e');
+ const fontFamily = config.FONT_FAMILY || 'Inter';
+ document.documentElement.style.setProperty('--font-family', fontFamily.includes(' ') ? `"${fontFamily}"` : fontFamily);
+
+ // Update Google Fonts link
+ if (config.FONT_FAMILY) {
+ const fontFamily = config.FONT_FAMILY.replace(/\s+/g, '+');
+ const fontLink = document.getElementById('font-link');
+ if (fontLink) fontLink.href = `https://fonts.googleapis.com/css2?family=${fontFamily}:wght@400;500;600;700&display=swap`;
+ }
+
+ // Set favicon
+ const faviconLink = document.getElementById('favicon-link');
+ if (faviconLink && config.FAVICON_URL) faviconLink.href = config.FAVICON_URL;
+
+ // Set header logo
+ const logoImg = document.getElementById('logo-img');
+ if (logoImg) {
+ logoImg.src = config.LOGO_URL;
+ logoImg.alt = config.ORG_NAME + ' Logo';
+
+ logoImg.onload = () => {
+ logoImg.classList.remove('opacity-0');
+ };
+ }
+
+ // Set header title and description
+ const headerTitle = document.getElementById('header-title');
+ if (headerTitle) {
+ headerTitle.textContent = config.CATALOG_TITLE || `${config.ORG_NAME} Catalog`;
+ headerTitle.style.color = config.COLORS.primary;
+ }
+
+ const headerDesc = document.getElementById('header-description');
+ if (headerDesc) {
+ headerDesc.textContent = config.CATALOG_DESCRIPTION;
+ }
+
+ // Set Code Repo ribbon link, SVG path, display name, and colors
+ const repoRibbon = document.getElementById('repo-ribbon');
+ const platformDisplay = getPlatformDisplay(config.PLATFORM);
+ if (repoRibbon && platformDisplay) {
+ repoRibbon.href = `${platformDisplay.ribbonUrl}${config.ORGANIZATION_NAME}/${config.CATALOG_REPO_NAME}`;
+ const svgElement = document.getElementById('repo-ribbon-icon-container');
+ svgElement.innerHTML = platformDisplay.svg;
+ const platformDisplayName = document.getElementById('platform-display-name');
+ platformDisplayName.textContent = platformDisplay.displayName || config.PLATFORM;
+ repoRibbon.style.backgroundColor = config.COLORS.secondary;
+ repoRibbon.style.setProperty('--hover-color', config.COLORS.primary);
+ repoRibbon.addEventListener('mouseenter', function () {
+ this.style.backgroundColor = config.COLORS.primary;
+ });
+ repoRibbon.addEventListener('mouseleave', function () {
+ this.style.backgroundColor = config.COLORS.secondary;
+ });
+ }
+
+ // Set focus ring colors for form inputs and link hover colors
+ const style = document.createElement('style');
+ style.textContent = `
+ .focus\\:ring-2:focus { --tw-ring-color: var(--color-accent) !important; }
+ .item-link:hover { color: var(--color-accent) !important; }
+ .dark .item-link:hover { color: var(--color-accent-dark) !important; }
+ `;
+ document.head.appendChild(style);
+};
+
+//
+// THEME TOGGLE LOGIC
+// Establishes a button to toggle between light and dark themes, storing the preference in localStorage.
+export const setThemeToggle = () => {
+ const themeToggleBtn = document.getElementById('themeToggleBtn');
+
+ if (
+ localStorage.theme === 'dark' ||
+ (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
+ ) {
+ document.documentElement.classList.add('dark');
+ } else {
+ document.documentElement.classList.remove('dark');
+ }
+
+ themeToggleBtn.addEventListener('click', () => {
+ if (document.documentElement.classList.contains('dark')) {
+ document.documentElement.classList.remove('dark');
+ localStorage.theme = 'light';
+ } else {
+ document.documentElement.classList.add('dark');
+ localStorage.theme = 'dark';
+ }
+ });
+};
diff --git a/src/ui/render.js b/src/ui/render.js
new file mode 100644
index 0000000..6ef015d
--- /dev/null
+++ b/src/ui/render.js
@@ -0,0 +1,169 @@
+/**
+ * This module contains functions for presentation of code repositories, datasets, models, or spaces in HTML.
+ * It includes functions for escaping HTML, adding word break opportunities, and rendering item cards.
+ */
+
+/**
+ * Escapes HTML special characters in a string to prevent XSS.
+ * @param {string} str - The input string.
+ * @returns {string} The escaped string.
+ */
+const escapeHTML = (str) => {
+ if (!str) return "";
+ return str.replace(/[&<>"']/g, (m) => ({
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"',
+ "'": '''
+ }[m]));
+};
+
+/**
+ * Adds word break opportunities after underscores to allow proper text wrapping.
+ * @param {string} str - The input string.
+ * @returns {string} The string with tags inserted after underscores.
+ */
+const addWordBreakOpportunities = (str) => {
+ if (!str) return "";
+ // Replace underscores with underscore + word break opportunity
+ return str.replace(/_/g, '_');
+};
+
+/**
+ * Renders a single item card (code, dataset, model, or space) to HTML.
+ * @param {Object} item - The item object to render.
+ * @returns {string} The HTML string for the item card.
+ */
+const renderHubItemCard = (item) => {
+ const lastUpdatedDate = new Date(item.lastModified).toLocaleDateString();
+ const tagsHtml = (item.displayTags || item.rawTags || []).map(tag =>
+ `${escapeHTML(tag)} `
+ ).join('');
+
+ // Use pretty_name for the heading, with a fallback
+ // HF API keys for CardData: https://huggingface.co/docs/huggingface_hub/main/en/package_reference/cards#huggingface_hub.CardData
+ // datasets have pretty_name, models have model_name, spaces have title
+ const prettyName = item.cardData?.pretty_name || item.cardData?.model_name || item.cardData?.title || item.id.split('/')[1];
+
+ // Use the description from cardData, with fallbacks
+ const displayDescription = item.cardData?.description || item.cardData?.model_description || item.description || 'No description provided.';
+
+ // Construct the correct URL based on the repository type
+ let itemUrl;
+
+ switch (item.repoType) {
+ case "code":
+ itemUrl = item.html_url;
+ break;
+ case "datasets":
+ itemUrl = `https://huggingface.co/datasets/${item.id}`;
+ break;
+ case "spaces":
+ itemUrl = `https://huggingface.co/spaces/${item.id}`;
+ break;
+ case "models":
+ itemUrl = `https://huggingface.co/${item.id}`;
+ break;
+ default:
+ // fallback for "all"
+ itemUrl = `https://huggingface.co/${item.id}`;
+ break;
+ }
+
+ // stars for code repos
+ const badgeHtml = (() => {
+ if (item.isNew) {
+ return `
+ New!
+ `;
+ }
+
+ if (typeof item.cardData?.stars === "number" && item.cardData.stars > 0) {
+ return `
+ ā ${item.cardData.stars}
+ `;
+ }
+
+ if (typeof item.likes === "number" && item.likes > 0) {
+ return `
+
+ ā¤ļø ${item.likes}
+ `;
+ }
+ return "";
+ })();
+
+ const escapedTitle = escapeHTML(prettyName);
+ const displayTitle = addWordBreakOpportunities(escapedTitle);
+
+ return `
+
+
+
+
+ ${escapeHTML(displayDescription)}
+
+
+
+
+ ${tagsHtml}
+
+
+ Updated: ${lastUpdatedDate}
+ ${item.archived ? `Archived ` : ''}
+
+
+
+ `;
+};
+
+/**
+ * Renders the list of items to the DOM.
+ * @param {Array} items - The array of item objects to render.
+ */
+export const renderItemList = (items) => {
+ const itemListElement = document.getElementById('itemList');
+ const emptyStateElement = document.getElementById('emptyState');
+ const resultCountElement = document.getElementById('resultCount');
+
+ if (resultCountElement) {
+ resultCountElement.textContent = `${items.length} result${items.length !== 1 ? 's' : ''}`;
+ }
+
+ if (items.length === 0) {
+ itemListElement.innerHTML = '';
+ emptyStateElement.classList.remove('hidden');
+ } else {
+ itemListElement.innerHTML = items.map(item => renderHubItemCard(item)).join('');
+ emptyStateElement.classList.add('hidden');
+ }
+};
+
+// Utility function to handle errors and display a user-friendly message
+export const handleError = (error, message) => {
+ console.error(message, error);
+ const itemList = document.getElementById('itemList');
+ itemList.innerHTML = `
+
Error loading items.
+
${escapeHTML(message)}
+
`;
+};
diff --git a/src/ui/urlManager.js b/src/ui/urlManager.js
new file mode 100644
index 0000000..0c38335
--- /dev/null
+++ b/src/ui/urlManager.js
@@ -0,0 +1,85 @@
+/**
+ * This module manages URL parameters for the catalog application: parsing, updating, and reflecting the
+ * current filter state in the URL for shareable searches.
+ */
+
+/**
+ * Parses URL parameters from both query string (?key=value) and hash (#key=value).
+ * Query parameters take precedence over hash parameters.
+ * @returns {Object} An object containing the parsed parameters.
+ */
+export const parseUrlParams = () => {
+ const params = {};
+
+ // Parse query string parameters (e.g., ?type=datasets&q=fish)
+ const queryString = window.location.search;
+ const urlParams = new URLSearchParams(queryString);
+
+ // Parse hash parameters (e.g., #type=datasets&q=fish)
+ const hash = window.location.hash.slice(1); // Remove the leading '#'
+ const hashParams = new URLSearchParams(hash);
+
+ // Hash parameters first (lower precedence)
+ for (const [key, value] of hashParams) {
+ params[key] = value;
+ }
+
+ // Query parameters override hash parameters (higher precedence)
+ for (const [key, value] of urlParams) {
+ params[key] = value;
+ }
+
+ return params;
+};
+
+/**
+ * Updates the URL hash with the current filter state without triggering a page reload.
+ * @param {Object} state - The current state object with type, q, sort, tag, archived properties.
+ */
+export const updateUrlParams = (state) => {
+ const params = new URLSearchParams();
+
+ // Only add non-default values to the URL
+ if (state.type && state.type !== 'all') {
+ params.set('type', state.type);
+ }
+ if (state.q && state.q.trim() !== '') {
+ params.set('q', state.q);
+ }
+ if (state.sort && state.sort !== 'lastModified') {
+ params.set('sort', state.sort);
+ }
+ if (state.tag && state.tag !== '') {
+ params.set('tag', state.tag);
+ }
+ if (state.archived && state.archived !== 'active') {
+ params.set('archived', state.archived);
+ }
+
+ const paramString = params.toString();
+ const newHash = paramString ? `#${paramString}` : '';
+
+ // Build the new URL preserving the pathname and any query parameters when clearing hash
+ const baseUrl = window.location.pathname + window.location.search;
+ const newUrl = newHash ? baseUrl + newHash : baseUrl;
+
+ // Update the URL without triggering a page reload
+ const currentUrl = window.location.pathname + window.location.search + window.location.hash;
+ if (currentUrl !== newUrl) {
+ history.replaceState(null, '', newUrl);
+ }
+};
+
+/**
+ * Gets the current filter state from form elements.
+ * @returns {Object} The current state object.
+ */
+export const getCurrentState = () => {
+ return {
+ type: document.getElementById('repoType')?.value || 'all',
+ q: document.getElementById('searchInput')?.value || '',
+ sort: document.getElementById('sortBy')?.value || 'lastModified',
+ tag: document.getElementById('tagFilter')?.value || '',
+ archived: document.getElementById('archiveFilter')?.value || 'active'
+ };
+};
diff --git a/src/utils/definePlatformVals.js b/src/utils/definePlatformVals.js
new file mode 100644
index 0000000..07dd579
--- /dev/null
+++ b/src/utils/definePlatformVals.js
@@ -0,0 +1,81 @@
+/**
+ * Collection of functions to define platform-specific values for the catalog, such as API URLs, display values, and repo data keys.
+ * Each function returns dictionaries with the platform values that have been keyed by platform name (e.g., 'github', 'gitlab', 'codeberg') and potentially other instance-specific values (e.g., organizationName, repoApiUrl).
+ * Platform and organization name are defined from config.yaml and passed to the functions requiring them.
+*/
+
+
+/**
+ * Utility function to get platform-specific values for repo data keys (e.g., star count and profile repo name).
+ * @param {string} platform
+ * @returns {Object} platformVals - An object containing platform-specific values for repo data keys (e.g., star count and profile repo name)
+ */
+export function getPlatformVals(platform) {
+ const platformVals = {
+ github: {
+ starsKey: 'stargazers_count',
+ profileRepo: '.github',
+ fullNameKey: 'full_name',
+ updatedAtKey: 'updated_at',
+ forkKey: 'fork', //forks_count is shared
+ urlKey: 'html_url',
+ encodeRepoId: (ownerRepo) => ownerRepo
+ },
+ gitlab: {
+ starsKey: 'star_count',
+ profileRepo: 'gitlab-profile',
+ fullNameKey: 'path_with_namespace',
+ updatedAtKey: 'last_activity_at', // key used by GitLab for project update ordering, may include broader group-based activity
+ forkKey: 'forked_from_project',
+ urlKey: 'web_url',
+ encodeRepoId: (ownerRepo) => encodeURIComponent(ownerRepo) // GitLab requires URL-encoded 'owner/repo' format
+ },
+ codeberg: {
+ starsKey: 'stars_count',
+ profileRepo: '.profile',
+ fullNameKey: 'full_name',
+ updatedAtKey: 'updated_at',
+ forkKey: 'fork',
+ urlKey: 'html_url',
+ encodeRepoId: (ownerRepo) => ownerRepo
+ }
+};
+ return platformVals[platform.toLowerCase()];
+}
+
+
+/**
+ * Utility function to get the platform-specific API URLs for organization repos and individual repo details.
+ * Defines API URLs based on the selected platform (GitHub, GitLab, or Codeberg).
+ * This allows the rest of the codebase to use these constants when making API calls,
+ * abstracting away platform-specific URL structures.
+ *
+ * Usage: import { getPlatformApiUrls } from './definePlatformVals.js';
+ *
+ * Input: platform and organizationName (e.g., 'github' and 'imageomics'), defined from config.yaml and passed to this function.
+ * Output: platformApiUrls[platform] = { org: ORG_API_URL, repo: REPO_API_URL, releaseSuffix: RELEASE_SUFFIX }
+ *
+ * @param {string} platform - 'github', 'gitlab', or 'codeberg'
+ * @param {string} organizationName - The name of the organization (used in URL construction)
+ * @returns {object} An object containing ORG_API_URL, REPO_API_URL, and RELEASE_SUFFIX
+ */
+export function getPlatformApiUrls(platform, organizationName) {
+ const platformApiUrls = {
+ github: {
+ org: `https://api.github.com/orgs/${organizationName}/repos?type=public&per_page=100`,
+ repo: "https://api.github.com/repos/",
+ releaseSuffix: "releases/latest"
+ },
+ gitlab: {
+ org: `https://gitlab.com/api/v4/groups/${organizationName}/projects?visibility=public&include_subgroups=true&with_shared=false&per_page=100`,
+ repo: "https://gitlab.com/api/v4/projects/",
+ releaseSuffix: "releases/permalink/latest"
+ },
+ codeberg: {
+ org: `https://codeberg.org/api/v1/orgs/${organizationName}/repos?type=public&limit=50`,
+ repo: "https://codeberg.org/api/v1/repos/",
+ releaseSuffix: "releases/latest"
+ }
+ };
+ return platformApiUrls[platform.toLowerCase()];
+}
diff --git a/src/utils/defineRibbonVals.js b/src/utils/defineRibbonVals.js
new file mode 100644
index 0000000..a03249c
--- /dev/null
+++ b/src/utils/defineRibbonVals.js
@@ -0,0 +1,41 @@
+/**
+ * A collection of platform-specific display information (e.g., SVGs and URLs)
+ * for code developer platforms including GitHub, GitLab, and Codeberg.
+ * Used for linking to repo and rendering icon and platform name in the ribbon component of the UI.
+ *
+ * Usage: import { getPlatformDisplay } from './defineRibbonVals.js';
+ *
+ * Input: platform (e.g., 'github'), defined from config.yaml and passed to this function.
+ * Output: platformDisplays[platform] = { svg: CODE_PLATFORM_SVG,
+ * displayName: DISPLAY_NAME, ribbonUrl: RIBBON_URL }
+ */
+
+import githubSvg from '../assets/GitHub_Invertocat.svg?raw';
+import gitlabSvg from '../assets/gitlab-logo-700-rgb.svg?raw';
+import codebergSvg from '../assets/codeberg-logo_icon_white.svg?raw';
+
+/**
+ * Utility function to get the full platform display information
+ * @param {string} platform - 'github', 'gitlab', or 'codeberg'
+ * @returns {object|null}
+ */
+export function getPlatformDisplay(platform) {
+ const platformDisplays = {
+ github: {
+ svg: githubSvg,
+ displayName: "GitHub",
+ ribbonUrl: "https://github.com/"
+ },
+ gitlab: {
+ svg: gitlabSvg,
+ displayName: "GitLab",
+ ribbonUrl: "https://gitlab.com/"
+ },
+ codeberg: {
+ svg: codebergSvg,
+ displayName: "Codeberg",
+ ribbonUrl: "https://codeberg.org/"
+ }
+ };
+ return platformDisplays[platform.toLowerCase()];
+}
diff --git a/src/filterAndSort.js b/src/utils/filterAndSort.js
similarity index 100%
rename from src/filterAndSort.js
rename to src/utils/filterAndSort.js
diff --git a/src/filterNewAdditionalEntries.js b/src/utils/filterNewAdditionalEntries.js
similarity index 100%
rename from src/filterNewAdditionalEntries.js
rename to src/utils/filterNewAdditionalEntries.js
diff --git a/src/utils/normalizeTag.js b/src/utils/normalizeTag.js
new file mode 100644
index 0000000..6b89177
--- /dev/null
+++ b/src/utils/normalizeTag.js
@@ -0,0 +1,64 @@
+/**
+ * Tag (keyword) processing, including normalization and tag-group lookup.
+ * These functions are used to standardize tags across different repositories and platforms.
+ */
+
+/**
+ * Builds a reverse lookup map from TAG_GROUPS (defined in tag-groups.js): raw tag ā [canonical tags]
+ * A raw tag may appear in multiple groups, so the value is an array.
+ * @returns {Object} The reverse lookup map.
+ */
+export const buildTagLookup = () => {
+ const lookup = Object.create(null);
+ if (typeof TAG_GROUPS !== 'undefined') {
+ for (const [canonical, aliases] of Object.entries(TAG_GROUPS)) {
+ for (const alias of aliases) {
+ const key = alias.toLowerCase();
+ if (lookup[key]) {
+ lookup[key].push(canonical);
+ } else {
+ lookup[key] = [canonical];
+ }
+ }
+ }
+ }
+ return lookup;
+};
+
+const tagLookup = buildTagLookup();
+
+/**
+ * Normalizes a raw tag string for the tag selection dropdown.
+ * - Returns null for Hugging Face system metadata tags (tags containing a colon).
+ * - Maps known aliases to their canonical tag(s) via tagLookup.
+ * - Falls back to the lowercased original if no mapping exists.
+ * @param {string} tag
+ * @param {Object} [lookup=tagLookup] - Optional reverse lookup map (alias ā canonical tags).
+ * Defaults to the module's prebuilt lookup from TAG_GROUPS. Pass a custom lookup in tests.
+ * @returns {string[]|null}
+ */
+export function normalizeTag(tag, lookup = tagLookup) {
+ const lower = String(tag).toLowerCase();
+ // OPTION LINE -- REMOVE IF UNWANTED
+ // Removes Hugging Face auto-generated system tags (e.g. "license:mit", "format:parquet") from dropdown filter.
+ // These are identified by the presence of a colon. To include auto-generated tags in the
+ // catalog, remove the following line and `.filter` line from the filterDisplayTags function.
+ if (lower.includes(':')) return null;
+ return lookup[lower] ?? [lower];
+}
+
+/**
+ * Sorts and filters tags for display in the repo cards.
+ * Currently excludes tags containing a colon (Hugging Face system metadata tags).
+ * @param {string[]} rawTags - Array of tags to sort and filter.
+ * @returns {string[]} Filtered and sorted array of tags for display.
+ */
+export function filterDisplayTags(rawTags) {
+ let displayTags = rawTags.sort() || [];
+ // OPTION LINE -- REMOVE IF UNWANTED
+ // Removes Hugging Face auto-generated system tags (e.g. "license:mit", "format:parquet") from display.
+ // These are identified by the presence of a colon. To include auto-generated tags in the repo cards,
+ // remove the following line.
+ displayTags = displayTags.filter(t => !t.includes(':'));
+ return displayTags;
+}
diff --git a/src/validateConfig.js b/src/validateConfig.js
index 7cac8f3..6c861ce 100644
--- a/src/validateConfig.js
+++ b/src/validateConfig.js
@@ -14,15 +14,57 @@ export function validateConfig(config) {
return errors;
}
- if (!config.ORGANIZATION_NAME) errors.push('ORGANIZATION_NAME');
- if (!config.ORG_NAME) errors.push('ORG_NAME');
- if (!config.CATALOG_REPO_NAME) errors.push('CATALOG_REPO_NAME');
- if (!config.PLATFORM) errors.push('PLATFORM');
- /** Update to include 'codeberg' and 'gitlab' once supported */
- const supportedPlatforms = ['github'];
- if (config.PLATFORM && !supportedPlatforms.includes(config.PLATFORM.toLowerCase())) {
+ /**
+ * PLATFORM will be parsed without whitespace, but must be string to avoid undefined errors
+ */
+ const supportedPlatforms = ['github', 'gitlab', 'codeberg'];
+ const platform = config.PLATFORM;
+ var validPlatform = '';
+ if (!platform || typeof platform !== 'string') {
+ errors.push('PLATFORM');
+ } else if (!supportedPlatforms.includes(platform.toLowerCase())) {
errors.push(`PLATFORM must be one of: ${supportedPlatforms.join(', ')}`);
+ } else {
+ validPlatform = platform.toLowerCase();
}
+
+ /**
+ * Validate organization names, existence and allowed characters
+ *
+ * GitHub org names allow only letters, numbers, and hyphens; GitLab and Codeberg also allow underscores.
+ * All require alphanumeric characters at start and end.
+ * See: https://docs.gitlab.com/user/reserved_names/
+ * See: https://codeberg.org/forgejo-contrib/forgejo-cli/wiki/Organizations
+ */
+ const orgName = config.ORGANIZATION_NAME;
+ const ghOrgRegex = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
+ const glcbOrgRegex = /^[a-zA-Z0-9](?:[a-zA-Z0-9_\-]*[a-zA-Z0-9])?$/;
+ if (typeof orgName !== 'string' || !orgName.trim()) {
+ errors.push('ORGANIZATION_NAME');
+ } else if (validPlatform != '') {
+ // If PLATFORM is invalid, we cannot determine which regex to use for ORGANIZATION_NAME validation.
+ if (validPlatform == 'github') {
+ var codeRegex = ghOrgRegex;
+ var codeRegexError = 'only letters, numbers, and hyphens';
+ } else {
+ var codeRegex = glcbOrgRegex;
+ var codeRegexError = 'only letters, numbers, hyphens, and underscores';
+ }
+ if (!codeRegex.test(orgName)) {
+ errors.push(`ORGANIZATION_NAME (${orgName}) is invalid for ${validPlatform} API calls, ${codeRegexError} are allowed`);
+ }
+ }
+ // Hugging Face org names allow letters, numbers, hyphens, and underscores.
+ const hfOrgRegex = /^[a-zA-Z0-9](?:[a-zA-Z0-9_\-]*[a-zA-Z0-9])?$/;
+ const hfOrgName = config.HF_ORGANIZATION_NAME;
+ if (typeof hfOrgName !== 'string' || !hfOrgName.trim()) {
+ errors.push('HF_ORGANIZATION_NAME');
+ } else if (!hfOrgRegex.test(hfOrgName)) {
+ errors.push(`HF_ORGANIZATION_NAME (${hfOrgName}) is invalid, only letters, numbers, hyphens, and underscores are allowed`);
+ }
+ if (!config.ORG_NAME) errors.push('ORG_NAME');
+ if (!config.CATALOG_REPO_NAME) errors.push('CATALOG_REPO_NAME');
+
if (!config.API_BASE_URL) errors.push('API_BASE_URL');
if (config.REFRESH_INTERVAL_DAYS == null) errors.push('REFRESH_INTERVAL_DAYS');
diff --git a/tests/api/fetchCodeRepos.test.js b/tests/api/fetchCodeRepos.test.js
new file mode 100644
index 0000000..ece9c1c
--- /dev/null
+++ b/tests/api/fetchCodeRepos.test.js
@@ -0,0 +1,367 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { fetchCodeRepos } from '../../src/api/fetchCodeRepos.js';
+import { handleError } from '../../src/ui/render.js';
+import { getPlatformVals, getPlatformApiUrls } from '../../src/utils/definePlatformVals.js';
+
+// Mock the internal dependencies
+vi.mock('../../src/utils/normalizeTag.js', () => ({
+ normalizeTag: vi.fn((tag) => [tag.toLowerCase()]),
+ filterDisplayTags: vi.fn((rawTags) => (rawTags || []).filter(t => !t.includes(':')))
+}));
+vi.mock('../../src/ui/render.js', () => ({
+ handleError: vi.fn()
+}));
+
+const SUPPORTED_PLATFORMS = ['github', 'gitlab', 'codeberg'];
+const TEST_ORG = 'test-org';
+
+const platformConfigs = SUPPORTED_PLATFORMS.map(platform => {
+ const platformVals = getPlatformVals(platform);
+ const urls = getPlatformApiUrls(platform, TEST_ORG);
+
+ return {
+ name: platform,
+ orgApiUrl: urls.org,
+ repoApiUrl: urls.repo,
+ starsKey: platformVals.starsKey,
+ platformProfileRepo: platformVals.profileRepo,
+ forkKey: platformVals.forkKey,
+ fullNameKey: platformVals.fullNameKey,
+ updatedAtKey: platformVals.updatedAtKey,
+ urlKey: platformVals.urlKey,
+ encodeRepoId: platformVals.encodeRepoId // used only for GitLab Additional Repos
+ };
+});
+
+const getMockForkValue = (isFork, platform) => {
+ if (!isFork) {
+ // GitLab leaves non-forks as undefined; GitHub/Codeberg return false
+ return platform === 'gitlab' ? undefined : false;
+ }
+ // GitLab returns parent project object; GitHub/Codeberg return true
+ return platform === 'gitlab' ? { id: 999 } : true;
+};
+
+describe.each(platformConfigs)('fetchCodeRepos - $name', (platformConfig) => {
+ const originalFetch = global.fetch;
+
+ beforeEach(() => {
+ global.fetch = vi.fn();
+ });
+ afterEach(() => {
+ global.fetch = originalFetch;
+ vi.clearAllMocks();
+ });
+
+ const orgApiUrl = platformConfig.orgApiUrl;
+ const repoApiUrl = platformConfig.repoApiUrl;
+ const platform = platformConfig.name;
+ const starsKey = platformConfig.starsKey;
+ const platformProfileRepo = platformConfig.platformProfileRepo;
+ const forkKey = platformConfig.forkKey;
+ const fullNameKey = platformConfig.fullNameKey;
+ const updatedAtKey = platformConfig.updatedAtKey;
+ const urlKey = platformConfig.urlKey;
+ const encodeRepoId = platformConfig.encodeRepoId;
+ const refreshIntervalDays = 30;
+ const releasesMap = {};
+
+ // Generate reliable relative timestamps so tests don't break as time passes
+ const now = new Date();
+ const recentDateISO = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(); // 2 days ago
+ const oldDateISO = new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(); // 45 days ago
+ const oldestDateISO = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString(); // 90 days ago
+
+ it('maps raw repo data, resolves platform keys, and attaches release data', async () => {
+ // Mock a single page platform response
+ global.fetch.mockResolvedValueOnce({
+ ok: true,
+ headers: { get: () => null }, // No 'link' header means no pagination
+ json: () => Promise.resolve([{
+ [fullNameKey]: 'test-org/code-repo',
+ name: 'code-repo',
+ description: 'A test repository',
+ [forkKey]: getMockForkValue(false, platform),
+ [updatedAtKey]: now.toISOString(),
+ created_at: recentDateISO,
+ topics: ['python'],
+ [starsKey]: 42,
+ [urlKey]: 'http://example.com/test-org/code-repo'
+ }])
+ });
+
+ // Mock releases map generated by scripts/fetch-releases.js
+ const mockReleasesMap = {
+ 'test-org/code-repo': { tag: 'v1.0', url: 'http://...', isNew: true }
+ };
+
+ const items = await fetchCodeRepos(
+ platform,
+ [], // No additional repos
+ orgApiUrl,
+ repoApiUrl,
+ refreshIntervalDays,
+ mockReleasesMap
+ );
+
+ expect(items).toHaveLength(1);
+ expect(items[0].id).toBe('test-org/code-repo');
+ expect(items[0].repoType).toBe('code');
+ expect(items[0].description).toBe('A test repository');
+ // fork is used for inclusion filtering only and is not returned by fetchCodeRepos
+ expect(items[0].lastModified).toStrictEqual(now);
+ expect(items[0].hasNewRelease).toBe(true);
+ expect(items[0].latestReleaseTag).toBe('v1.0');
+ expect(items[0].tags).toContain('python');
+ expect(items[0].html_url).toBe('http://example.com/test-org/code-repo');
+
+ expect(items[0].cardData.pretty_name).toBe('code-repo');
+ expect(items[0].cardData.stars).toBe(42);
+
+ expect(global.fetch).toHaveBeenCalledTimes(1);
+ expect(global.fetch).toHaveBeenCalledWith(orgApiUrl);
+ });
+
+ // Pagination test: simulate multiple pages of API results using the Link header
+ it('handles multi-page pagination', async () => {
+ global.fetch.mockImplementation((url) => {
+ if (url === orgApiUrl) {
+ // Page 1 setup with Link header pointing to Page 2
+ return Promise.resolve({
+ ok: true,
+ headers: {
+ get: (name) => name.toLowerCase() === 'link'
+ ? `<${orgApiUrl}?page=2>; rel="next"`
+ : null
+ },
+ json: () => Promise.resolve([{
+ [fullNameKey]: 'test-org/repo-page1',
+ name: 'repo-page1',
+ created_at: recentDateISO,
+ [updatedAtKey]: recentDateISO
+ }])
+ });
+ } else if (url === `${orgApiUrl}?page=2`) {
+ // Page 2 setup with no Link header (breaks loop)
+ return Promise.resolve({
+ ok: true,
+ headers: { get: () => null },
+ json: () => Promise.resolve([{
+ [fullNameKey]: 'test-org/repo-page2',
+ name: 'repo-page2',
+ created_at: recentDateISO,
+ [updatedAtKey]: recentDateISO }])
+ });
+ }
+ });
+
+ const items = await fetchCodeRepos(
+ platform,
+ [],
+ orgApiUrl,
+ repoApiUrl,
+ refreshIntervalDays,
+ releasesMap);
+
+ expect(global.fetch).toHaveBeenCalledTimes(2);
+ expect(global.fetch).toHaveBeenNthCalledWith(1, orgApiUrl);
+ expect(global.fetch).toHaveBeenNthCalledWith(2, `${orgApiUrl}?page=2`);
+ expect(items).toHaveLength(2);
+ expect(items.map(i => i.id)).toEqual(['test-org/repo-page1', 'test-org/repo-page2']);
+ expect(items[0].cardData.pretty_name).toBe('repo-page1');
+ expect(items[1].cardData.pretty_name).toBe('repo-page2');
+ });
+
+ // Test additional repos fetching and merging with org repos
+ it('fetches additional org repos once and fetches external repos', async () => {
+ // Base organization discovery lists 'test-org/internal-additional'
+ const mockOrgRepo = {
+ [fullNameKey]: 'test-org/internal-additional',
+ name: 'internal-additional',
+ created_at: recentDateISO,
+ [updatedAtKey]: recentDateISO
+ };
+
+ const externalRepoId = 'external-org/external-additional';
+ const expectedExternalRepoUrl = `${repoApiUrl}${encodeRepoId(externalRepoId)}`;
+
+ const mockExternalRepo = {
+ [fullNameKey]: externalRepoId,
+ name: 'external-additional',
+ created_at: recentDateISO,
+ [updatedAtKey]: recentDateISO
+ };
+
+ global.fetch.mockImplementation((url) => {
+ if (url === orgApiUrl) {
+ return Promise.resolve({
+ ok: true,
+ headers: { get: () => null },
+ json: () => Promise.resolve([mockOrgRepo])
+ });
+ }
+ if (url === expectedExternalRepoUrl) {
+ return Promise.resolve({
+ ok: true,
+ headers: { get: () => null },
+ json: () => Promise.resolve(mockExternalRepo)
+ });
+ }
+ });
+
+ const additionalRepos = [
+ 'test-org/internal-additional',
+ externalRepoId
+ ];
+
+ const items = await fetchCodeRepos(
+ platform,
+ additionalRepos,
+ orgApiUrl,
+ repoApiUrl,
+ refreshIntervalDays,
+ releasesMap
+ );
+
+ /** Fetch should only be triggered twice: Once for org list, once for external repo.
+ * The internal-additional repo is found inside the cache,
+ * should not hit the network a second time.
+ */
+ expect(global.fetch).toHaveBeenCalledTimes(2);
+ expect(global.fetch).toHaveBeenCalledWith(
+ `${expectedExternalRepoUrl}`
+ );
+ expect(global.fetch).not.toHaveBeenCalledWith(
+ `${repoApiUrl}test-org/internal-additional`
+ );
+
+ /** Assert deduplication: 'test-org/internal-additional' in both core lists,
+ * shouldn't show up twice in processed outputs.
+ */
+ expect(items).toHaveLength(2);
+ expect(items.map(i => i.id)).toContain('test-org/internal-additional');
+ expect(items.map(i => i.id)).toContain(externalRepoId);
+ });
+
+ // Test that forks and platform profile repos are excluded from the final list
+ it('excludes repository targets designated as forks and platform profile repos', async () => {
+ global.fetch.mockResolvedValueOnce({
+ ok: true,
+ headers: { get: () => null },
+ json: () => Promise.resolve([
+ {
+ [fullNameKey]: 'test-org/valid-repo',
+ name: 'valid-repo',
+ [forkKey]: getMockForkValue(false, platform),
+ created_at: recentDateISO,
+ [updatedAtKey]: recentDateISO
+ },
+ {
+ [fullNameKey]: 'test-org/forked-repo',
+ name: 'forked-repo',
+ [forkKey]: getMockForkValue(true, platform),
+ created_at: recentDateISO,
+ [updatedAtKey]: recentDateISO
+ },
+ {
+ [fullNameKey]: 'test-org/forked-in-list',
+ name: 'forked-in-list',
+ [forkKey]: getMockForkValue(true, platform),
+ created_at: recentDateISO,
+ [updatedAtKey]: recentDateISO
+ },
+ {
+ [fullNameKey]: `test-org/${platformProfileRepo}`,
+ name: platformProfileRepo,
+ [forkKey]: getMockForkValue(false, platform),
+ created_at: recentDateISO,
+ [updatedAtKey]: recentDateISO
+ }
+ ])
+ });
+
+ const additionalRepos = [
+ 'test-org/forked-in-list' // This is a fork, but listed to include
+ ];
+
+ const items = await fetchCodeRepos(
+ platform,
+ additionalRepos,
+ orgApiUrl,
+ repoApiUrl,
+ refreshIntervalDays,
+ releasesMap);
+
+ expect(items).toHaveLength(2); // Should only include valid-repo and forked-in-list
+ expect(items.map(i => i.id)).toContain('test-org/valid-repo');
+ expect(items.map(i => i.id)).toContain('test-org/forked-in-list');
+ expect(items.map(i => i.id)).not.toContain('test-org/forked-repo');
+ expect(items.map(i => i.id)).not.toContain(`test-org/${platformProfileRepo}`);
+ });
+
+ // Test that isNew flag is set correctly based on creation date and refresh interval
+ it('flags items as new based on evaluation thresholds', async () => {
+ global.fetch.mockResolvedValueOnce({
+ ok: true,
+ headers: { get: () => null },
+ json: () => Promise.resolve([
+ {
+ [fullNameKey]: 'test-org/new-repo',
+ name: 'new-repo',
+ created_at: recentDateISO,
+ [updatedAtKey]: now.toISOString()
+ },
+ {
+ [fullNameKey]: 'test-org/old-repo',
+ name: 'old-repo',
+ created_at: oldestDateISO,
+ [updatedAtKey]: oldDateISO
+ }
+ ])
+ });
+
+ const items = await fetchCodeRepos(
+ platform,
+ [],
+ orgApiUrl,
+ repoApiUrl,
+ refreshIntervalDays,
+ releasesMap
+ );
+
+ const newRepo = items.find(i => i.id === 'test-org/new-repo');
+ const oldRepo = items.find(i => i.id === 'test-org/old-repo');
+
+ // check that the lastModified dates are set correctly based on the platform-specific updatedAtKey
+ expect(items[0].lastModified).toStrictEqual(now);
+ expect(items[1].lastModified).toStrictEqual(new Date(oldDateISO));
+ expect(newRepo.isNew).toBe(true);
+ expect(oldRepo.isNew).toBe(false);
+ });
+
+ // Test that network errors are handled gracefully and trigger UI error handling
+ it('handles network errors gracefully', async () => {
+ // Force the main fetch to fail
+ global.fetch.mockResolvedValueOnce({
+ ok: false,
+ status: 404,
+ headers: { get: () => null }
+ });
+
+ const items = await fetchCodeRepos(
+ platform,
+ [],
+ orgApiUrl,
+ repoApiUrl,
+ refreshIntervalDays,
+ releasesMap
+ );
+
+ expect(handleError).toHaveBeenCalledWith(
+ expect.any(Error),
+ expect.stringContaining(`Failed to fetch code from ${platform}`)
+ );
+ expect(items).toEqual([]);
+ });
+
+});
diff --git a/tests/api/fetchHfRepos.test.js b/tests/api/fetchHfRepos.test.js
new file mode 100644
index 0000000..ef6248a
--- /dev/null
+++ b/tests/api/fetchHfRepos.test.js
@@ -0,0 +1,341 @@
+// tests/fetchHfRepos.test.js
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { fetchHfRepos } from '../../src/api/fetchHfRepos.js';
+import { handleError } from '../../src/ui/render.js';
+
+// Mock the internal dependencies
+vi.mock('../../src/utils/normalizeTag.js', () => ({
+ normalizeTag: vi.fn((tag) => [tag.toLowerCase()]),
+ filterDisplayTags: vi.fn((rawTags) => (rawTags || []).filter(t => !t.includes(':')))
+}));
+
+vi.mock('../../src/ui/render.js', () => ({
+ handleError: vi.fn()
+}));
+
+vi.mock('../../src/utils/filterNewAdditionalEntries.js', () => ({
+ // Pass the additional entries straight through
+ filterNewAdditionalEntries: vi.fn((existingIds, additional) =>
+ additional.filter(entry => !existingIds.has(entry.repo)))
+}));
+
+describe('fetchHfRepos', () => {
+ const originalFetch = global.fetch;
+
+ beforeEach(() => {
+ // Reset the fetch mock before each test
+ global.fetch = vi.fn();
+ });
+
+ afterEach(() => {
+ global.fetch = originalFetch;
+ vi.clearAllMocks();
+ });
+
+ const apiBaseUrl = 'https://huggingface.co/api/';
+ const hfOrgName = 'test-org';
+ const refreshIntervalDays = 30;
+
+ // Generate reliable relative timestamps so tests don't break as time passes
+ const now = new Date();
+ const recentDateISO = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(); // 2 days ago
+ const oldDateISO = new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(); // 45 days ago
+ const oldestDateISO = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000).toISOString(); // 90 days ago
+
+ // Test fetching datasets and checking the "new" flag based on creation date
+ it('fetches datasets and flags new repositories as expected', async () => {
+ global.fetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve([
+ {
+ id: 'test-org/new-dataset',
+ createdAt: recentDateISO,
+ lastModified: recentDateISO,
+ likes: 10,
+ tags: ['nlp']
+ },
+ {
+ id: 'test-org/old-dataset',
+ createdAt: oldestDateISO,
+ lastModified: oldDateISO,
+ likes: 5,
+ tags: ['vision']
+ }
+ ])
+ });
+
+ const items = await fetchHfRepos(
+ 'datasets',
+ [],
+ apiBaseUrl,
+ hfOrgName,
+ refreshIntervalDays
+ );
+
+ expect(items).toHaveLength(2);
+ // new dataset expected values
+ expect(items[0].id).toBe('test-org/new-dataset');
+ expect(items[0].repoType).toBe('datasets');
+ expect(items[0].likes).toBe(10);
+ expect(items[0].tags).toContain('nlp');
+ expect(items[0].isNew).toBe(true);
+ // old dataset expected values
+ expect(items[1].id).toBe('test-org/old-dataset');
+ expect(items[1].repoType).toBe('datasets');
+ expect(items[1].likes).toBe(5);
+ expect(items[1].tags).toContain('vision');
+ expect(items[1].isNew).toBe(false);
+ });
+
+ it('fetches datasets as expected without secondary detail fetch', async () => {
+ // Setup mock response for the main org datasets
+ global.fetch.mockResolvedValueOnce({
+ ok: true,
+ json: () => Promise.resolve([{
+ id: 'test-org/dataset-1',
+ createdAt: recentDateISO, // 1 month ago
+ lastModified: now.toISOString(),
+ tags: ['nlp', 'task:text-classification'],
+ cardData:
+ {
+ pretty_name: 'Dataset 1',
+ description: 'Test dataset'
+ }
+ }])
+ });
+
+ const items = await fetchHfRepos(
+ 'datasets',
+ [], // No additional repos
+ apiBaseUrl,
+ hfOrgName,
+ refreshIntervalDays
+ );
+
+ expect(global.fetch).toHaveBeenCalledTimes(1);
+ expect(global.fetch).toHaveBeenCalledWith(`${apiBaseUrl}datasets?author=${hfOrgName}&full=true`);
+ expect(items).toHaveLength(1);
+ expect(items[0].repoType).toBe('datasets');
+ expect(items[0].tags).toContain('nlp');
+ expect(items[0].cardData.pretty_name).toBe('Dataset 1');
+ expect(items[0].cardData.description).toBe('Test dataset');
+
+ // Assert dates are correctly converted to Date objects
+ expect(items[0].createdAt).toBeInstanceOf(Date);
+ expect(items[0].lastModified).toBeInstanceOf(Date);
+ });
+
+ it('makes secondary fetch calls when fetching models to get full details', async () => {
+ // We need a custom mock implementation because models trigger sequential fetch calls
+ global.fetch.mockImplementation((url) => {
+ if (url.includes('models?author=')) {
+ // Return the initial summary list
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve([{ id: 'test-org/model-1' }])
+ });
+ }
+ if (url.includes('models/test-org/model-1')) {
+ // Return the detailed model data
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({
+ id: 'test-org/model-1',
+ createdAt: now.toISOString(), // Simulating a brand new model
+ lastModified: now.toISOString(),
+ cardData:
+ {
+ model_name: 'Model 1',
+ model_description: 'Test model',
+ tags: ['vision']
+ }
+ })
+ });
+ }
+ });
+
+ const items = await fetchHfRepos(
+ 'models',
+ [],
+ apiBaseUrl,
+ hfOrgName,
+ refreshIntervalDays
+ );
+
+ // It should have called the main endpoint and the specific model endpoint
+ expect(global.fetch).toHaveBeenCalledTimes(2);
+ expect(global.fetch).toHaveBeenCalledWith(`${apiBaseUrl}models/test-org/model-1`);
+
+ expect(items).toHaveLength(1);
+ expect(items[0].isNew).toBe(true);
+ expect(items[0].tags).toContain('vision');
+ expect(items[0].cardData.model_name).toBe('Model 1');
+ expect(items[0].cardData.model_description).toBe('Test model');
+ });
+
+ // Test that the function handles partial failures gracefully
+ it('survives partial failures if one model secondary fetch fails but another succeeds', async () => {
+ global.fetch.mockImplementation((url) => {
+ if (url.includes('models?author=')) {
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve([
+ { id: 'test-org/good-model' },
+ { id: 'test-org/broken-model' }
+ ])
+ });
+ }
+ if (url.includes('models/test-org/good-model')) {
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({
+ id: 'test-org/good-model',
+ createdAt: recentDateISO,
+ lastModified: recentDateISO
+ })
+ });
+ }
+ if (url.includes('models/test-org/broken-model')) {
+ // Simulate a broken/missing resource detail
+ return Promise.resolve({ ok: false, status: 404 });
+ }
+ });
+
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
+
+ const items = await fetchHfRepos(
+ 'models',
+ [],
+ apiBaseUrl,
+ hfOrgName,
+ refreshIntervalDays
+ );
+
+ // The broken model should be cleanly filtered out by detailedItems.filter(Boolean)
+ expect(items).toHaveLength(1);
+ expect(items[0].id).toBe('test-org/good-model');
+ expect(items[0].isNew).toBe(true);
+ expect(items[0].repoType).toBe('models');
+
+ // Ensure the error was logged for the broken model
+ expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining(
+ 'Failed to fetch details for test-org/broken-model'
+ ));
+
+ consoleSpy.mockRestore();
+ });
+
+ // Test that additional repos are fetched and merged correctly
+ it('fetches additional Hugging Face repos and merges them', async () => {
+ const additionalHfRepos = [
+ { type: 'spaces', repo: 'external-user/cool-space' },
+ { type: 'spaces', repo: 'test-org/space-1' } // Already in org, should be ignored
+ ];
+
+ global.fetch.mockImplementation((url) => {
+ if (url.includes('spaces?author=')) {
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve([{
+ id: 'test-org/space-1',
+ createdAt: recentDateISO,
+ lastModified: recentDateISO
+ }])
+ });
+ }
+ if (url.includes('spaces/external-user/cool-space')) {
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({
+ id: 'external-user/cool-space',
+ createdAt: oldDateISO,
+ lastModified: recentDateISO
+ })
+ });
+ }
+ });
+
+ const items = await fetchHfRepos(
+ 'spaces',
+ additionalHfRepos,
+ apiBaseUrl,
+ hfOrgName,
+ refreshIntervalDays
+ );
+
+ // 1 for org spaces, 1 for additional space, duplicate not fetched again
+ expect(items).toHaveLength(2);
+ expect(global.fetch).toHaveBeenCalledTimes(2);
+ expect(global.fetch).toHaveBeenCalledWith(`${apiBaseUrl}spaces/external-user/cool-space`);
+ expect(global.fetch).toHaveBeenCalledWith(`${apiBaseUrl}spaces?author=${hfOrgName}&full=true`);
+
+ expect(items[0].id).toBe('test-org/space-1');
+ expect(items[0].repoType).toBe('spaces');
+ expect(items[0].isNew).toBe(true);
+
+ expect(items[1].id).toBe('external-user/cool-space');
+ expect(items[1].repoType).toBe('spaces');
+ expect(items[1].isNew).toBe(false);
+ });
+
+ // Test that network errors in fetching additional repos are handled gracefully
+ it('logs warnings and ignores external additional repo if its fetch throws a network error', async () => {
+ const additionalHfRepos = [{ type: 'datasets', repo: 'external-user/broken-additional' }];
+
+ global.fetch.mockImplementation((url) => {
+ if (url.includes('datasets?author=')) {
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve([{
+ id: 'test-org/dataset-1',
+ createdAt: recentDateISO,
+ lastModified: recentDateISO
+ }])
+ });
+ }
+ if (url.includes('datasets/external-user/broken-additional')) {
+ // Hard crash fetch
+ return Promise.reject(new Error('Network disconnected'));
+ }
+ });
+
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+
+ const items = await fetchHfRepos(
+ 'datasets',
+ additionalHfRepos,
+ apiBaseUrl,
+ hfOrgName,
+ refreshIntervalDays
+ );
+
+ // System should catch the error safely, remain un-crashed, and return just the internal dataset
+ expect(items).toHaveLength(1);
+ expect(items[0].id).toBe('test-org/dataset-1');
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
+ expect.stringContaining('Network error fetching additional HF repo "external-user/broken-additional":'),
+ expect.any(Error)
+ );
+
+ consoleWarnSpy.mockRestore();
+ });
+
+ it('handles network errors gracefully', async () => {
+ // Force the main fetch to fail
+ global.fetch.mockResolvedValueOnce({
+ ok: false,
+ status: 500
+ });
+
+ const items = await fetchHfRepos(
+ 'datasets',
+ [],
+ apiBaseUrl,
+ hfOrgName,
+ refreshIntervalDays
+ );
+
+ expect(items).toEqual([]);
+ expect(handleError).toHaveBeenCalled();
+ });
+});
diff --git a/tests/config.integration.test.js b/tests/config.integration.test.js
index e68abcc..25585de 100644
--- a/tests/config.integration.test.js
+++ b/tests/config.integration.test.js
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
-import jsYaml from 'js-yaml';
+import { load } from 'js-yaml';
import { validateConfig } from '../src/validateConfig.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -11,14 +11,14 @@ const configPath = resolve(__dirname, '../public/config.yaml');
describe('public/config.yaml', () => {
it('parses as a valid YAML object', () => {
const raw = readFileSync(configPath, 'utf8');
- const config = jsYaml.load(raw);
+ const config = load(raw);
expect(config).toBeTruthy();
expect(typeof config).toBe('object');
expect(Array.isArray(config)).toBe(false);
});
it('passes all validation checks', () => {
- const config = jsYaml.load(readFileSync(configPath, 'utf8'));
+ const config = load(readFileSync(configPath, 'utf8'));
const errors = validateConfig(config);
expect(errors).toEqual([]);
});
diff --git a/tests/ui/urlManager.test.js b/tests/ui/urlManager.test.js
new file mode 100644
index 0000000..c37b996
--- /dev/null
+++ b/tests/ui/urlManager.test.js
@@ -0,0 +1,105 @@
+// @vitest-environment jsdom
+import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
+import { parseUrlParams, updateUrlParams, getCurrentState } from '../../src/ui/urlManager.js';
+
+describe('urlManager with jsdom environment', () => {
+ // Reset the DOM and URL state before each test to ensure isolation
+ beforeEach(() => {
+ document.body.innerHTML = '';
+ window.history.replaceState(null, '', '/');
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ vi.restoreAllMocks();
+ });
+
+ // Test URL parsing with both query and hash parameters, ensuring query takes precedence
+ it('parses URL params, prioritizing search query over hash', () => {
+ window.history.replaceState(null, '', '/?type=models&q=fish#type=datasets');
+
+ const params = parseUrlParams();
+
+ // ?type=models should override #type=datasets
+ expect(params).toEqual({ type: 'models', q: 'fish' });
+ });
+
+ // Test URL state sync
+ it('updates URL params without refreshing the page', () => {
+ const mockState = {
+ type: 'datasets',
+ q: 'nlp',
+ sort: 'lastModified',
+ tag: '',
+ archived: 'active'
+ };
+
+ // Spy on replaceState to verify the function call signature
+ const replaceStateSpy = vi.spyOn(window.history, 'replaceState');
+
+ updateUrlParams(mockState);
+
+ // Only non-default values should end up in the hash/search string
+ expect(replaceStateSpy).toHaveBeenCalledWith(
+ null,
+ '',
+ '/#type=datasets&q=nlp'
+ );
+ expect(window.location.hash).toBe('#type=datasets&q=nlp');
+ });
+
+ it('clears or avoids adding default values to the URL hash completely', () => {
+ // Start with a populated URL string
+ window.history.replaceState(null, '', '/#type=datasets&q=nlp');
+
+ const defaultState = {
+ type: 'all',
+ q: '',
+ sort: 'lastModified',
+ tag: '',
+ archived: 'active'
+ };
+
+ updateUrlParams(defaultState);
+
+ // All defaults means hash string gets completely wiped out
+ expect(window.location.hash).toBe('');
+ });
+
+ // Test dynamic state retrieval from DOM elements, simulating user input
+ it('maps active DOM values correctly into a state matrix via getCurrentState', () => {
+ // Inject real HTML nodes into JSDOM's virtual container layout
+ document.body.innerHTML = `
+ Spaces
+
+ Stars
+ PyTorch
+ All
+ `;
+
+ const activeState = getCurrentState();
+
+ expect(activeState).toEqual({
+ type: 'spaces',
+ q: 'image segmentation',
+ sort: 'stars',
+ tag: 'pytorch',
+ archived: 'all'
+ });
+ });
+
+ it('safely handles missing or empty DOM elements by applying built-in defaults', () => {
+ // Leaving document.body empty tests fallback properties
+ document.body.innerHTML = '';
+
+ const defaultState = getCurrentState();
+
+ expect(defaultState).toEqual({
+ type: 'all',
+ q: '',
+ sort: 'lastModified',
+ tag: '',
+ archived: 'active'
+ });
+ });
+});
diff --git a/tests/filterAndSort.test.js b/tests/utils/filterAndSort.test.js
similarity index 99%
rename from tests/filterAndSort.test.js
rename to tests/utils/filterAndSort.test.js
index f4ca2e3..d4fb2ae 100644
--- a/tests/filterAndSort.test.js
+++ b/tests/utils/filterAndSort.test.js
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
-import { filterItems, sortItems } from '../src/filterAndSort.js';
+import { filterItems, sortItems } from '../../src/utils/filterAndSort.js';
// Minimal item factory ā only the fields filterItems/sortItems actually read.
const makeItem = (overrides = {}) => ({
diff --git a/tests/filterNewAdditionalEntries.test.js b/tests/utils/filterNewAdditionalEntries.test.js
similarity index 96%
rename from tests/filterNewAdditionalEntries.test.js
rename to tests/utils/filterNewAdditionalEntries.test.js
index 4ccc0f0..3a2a7c7 100644
--- a/tests/filterNewAdditionalEntries.test.js
+++ b/tests/utils/filterNewAdditionalEntries.test.js
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
-import { filterNewAdditionalEntries } from '../src/filterNewAdditionalEntries.js';
+import { filterNewAdditionalEntries } from '../../src/utils/filterNewAdditionalEntries.js';
describe('filterNewAdditionalEntries', () => {
it('returns all entries when existingIds is empty', () => {
diff --git a/tests/normalizeTag.test.js b/tests/utils/normalizeTag.test.js
similarity index 96%
rename from tests/normalizeTag.test.js
rename to tests/utils/normalizeTag.test.js
index adbd15f..46ef0ed 100644
--- a/tests/normalizeTag.test.js
+++ b/tests/utils/normalizeTag.test.js
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
-import { normalizeTag } from '../src/normalizeTag.js';
+import { normalizeTag } from '../../src/utils/normalizeTag.js';
// tagLookup format: lowercased alias ā [canonical tags]
const LOOKUP = {
diff --git a/tests/validateConfig.test.js b/tests/validateConfig.test.js
index a9ee8c1..7354429 100644
--- a/tests/validateConfig.test.js
+++ b/tests/validateConfig.test.js
@@ -3,6 +3,7 @@ import { validateConfig } from '../src/validateConfig.js';
const VALID_CONFIG = {
ORGANIZATION_NAME: 'imageomics',
+ HF_ORGANIZATION_NAME: 'imageomics',
ORG_NAME: 'Imageomics',
CATALOG_REPO_NAME: 'catalog',
PLATFORM: 'github',
@@ -53,6 +54,37 @@ describe('validateConfig', () => {
expect(validateConfig({ ...VALID_CONFIG, ORGANIZATION_NAME: '' })).toContain('ORGANIZATION_NAME');
});
+ it('errors when ORGANIZATION_NAME is not in accepted format for selected platform: GitHub, but okay for other platforms', () => {
+ expect(
+ validateConfig({ ...VALID_CONFIG, ORGANIZATION_NAME: 'abc_center' })
+ ).toContain('ORGANIZATION_NAME (abc_center) is invalid for github API calls, only letters, numbers, and hyphens are allowed');
+ });
+
+ it('does not error when ORGANIZATION_NAME is in accepted format for selected platform: GitLab, but not for GitHub', () => {
+ const config = { ...VALID_CONFIG, PLATFORM: 'gitlab', ORGANIZATION_NAME: 'abc_center' };
+ expect(validateConfig(config)).toEqual([]);
+ });
+
+ it('errors when ORGANIZATION_NAME is not in accepted format for selected platform: Codeberg, and other platforms', () => {
+ const config = { ...VALID_CONFIG, PLATFORM: 'codeberg', ORGANIZATION_NAME: 'abc center' };
+ expect(validateConfig(config)).toContain('ORGANIZATION_NAME (abc center) is invalid for codeberg API calls, only letters, numbers, hyphens, and underscores are allowed');
+ });
+
+ it('errors when HF_ORGANIZATION_NAME is missing', () => {
+ const { HF_ORGANIZATION_NAME: _, ...config } = VALID_CONFIG;
+ expect(validateConfig(config)).toContain('HF_ORGANIZATION_NAME');
+ });
+
+ it('errors when HF_ORGANIZATION_NAME is empty string', () => {
+ expect(validateConfig({ ...VALID_CONFIG, HF_ORGANIZATION_NAME: '' })).toContain('HF_ORGANIZATION_NAME');
+ });
+
+ it('errors when HF_ORGANIZATION_NAME is not in accepted format', () => {
+ expect(
+ validateConfig({ ...VALID_CONFIG, HF_ORGANIZATION_NAME: 'abc center' })
+ ).toContain('HF_ORGANIZATION_NAME (abc center) is invalid, only letters, numbers, hyphens, and underscores are allowed');
+ });
+
it('errors when ORG_NAME is missing', () => {
const { ORG_NAME: _, ...config } = VALID_CONFIG;
expect(validateConfig(config)).toContain('ORG_NAME');
@@ -75,7 +107,11 @@ describe('validateConfig', () => {
const { PLATFORM: _, ...config } = VALID_CONFIG;
expect(validateConfig(config)).toContain('PLATFORM');
});
-
+
+ it('errors when PLATFORM is not a string', () => {
+ expect(validateConfig({ ...VALID_CONFIG, PLATFORM: 123 })).toContain('PLATFORM');
+ });
+
it('errors when PLATFORM is unrecognized', () => {
const errors = validateConfig({ ...VALID_CONFIG, PLATFORM: 'blah' });
expect(errors.some(e => e.includes('PLATFORM'))).toBe(true);
@@ -141,6 +177,7 @@ describe('validateConfig', () => {
};
const errors = validateConfig(config);
expect(errors).toContain('ORGANIZATION_NAME');
+ expect(errors).toContain('HF_ORGANIZATION_NAME');
expect(errors).toContain('ORG_NAME');
expect(errors).toContain('CATALOG_REPO_NAME');
expect(errors).toContain('PLATFORM');
diff --git a/vite.config.js b/vite.config.js
index c216b73..34d5d49 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,5 +1,33 @@
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
+import { readFileSync } from 'fs';
+import { load } from 'js-yaml';
+import { validateConfig } from './src/validateConfig.js';
+
+const CONFIG_PATH = './public/config.yaml';
+let parsedConfig;
+
+// Run config validation before Vite starts (dev/build/preview)
+try {
+ const fileContents = readFileSync(CONFIG_PATH, 'utf8');
+ parsedConfig = load(fileContents);
+} catch (error) {
+ console.error(`\nā [CONFIG ERROR] Failed to read or parse ${CONFIG_PATH}: ${error.message}\n`);
+ process.exit(1); // Stop Vite immediately if the file is completely unreadable or broken YAML
+}
+
+// Any errors will be collected in the 'errors' array, to be printed out and cause a safe crash if not empty.
+const errors = validateConfig(parsedConfig);
+if (errors && errors.length > 0) {
+ console.error('\nā [CONFIG ERROR] Vite failed to start due to the following configuration errors:\n');
+
+ errors.forEach((error, index) => {
+ console.error(` ${index + 1}. ${error}`);
+ });
+
+ console.error(`\n Please fix the listed issue(s) in ${CONFIG_PATH} and try again.\n`);
+ process.exit(1); // Safely kills the npm run dev terminal process
+}
const repoName = process.env.GITHUB_REPOSITORY?.split('/')[1]