excel2json init

This commit is contained in:
lirui
2026-02-09 20:08:26 +08:00
commit 36f5d247b1
37 changed files with 4046 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
{
"permissions": {
"allow": [
"Bash(npm install:*)",
"mcp__svelte__svelte-autofixer",
"Bash(npm run check:*)",
"Bash(npx vitest:*)",
"Bash(node -e:*)",
"Bash(npm run build:*)"
]
},
"enableAllProjectMcpServers": true,
"enabledMcpjsonServers": [
"svelte"
]
}

7
.cursor/mcp.json Normal file
View File

@@ -0,0 +1,7 @@
{
"mcpServers": {
"svelte": {
"url": "https://mcp.svelte.dev/mcp"
}
}
}

8
.gemini/settings.json Normal file
View File

@@ -0,0 +1,8 @@
{
"$schema": "https://raw.githubusercontent.com/google-gemini/gemini-cli/main/schemas/settings.schema.json",
"mcpServers": {
"svelte": {
"url": "https://mcp.svelte.dev/mcp"
}
}
}

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

8
.mcp.json Normal file
View File

@@ -0,0 +1,8 @@
{
"mcpServers": {
"svelte": {
"type": "http",
"url": "https://mcp.svelte.dev/mcp"
}
}
}

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=true

7
.vscode/mcp.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"servers": {
"svelte": {
"url": "https://mcp.svelte.dev/mcp"
}
}
}

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"files.associations": {
"*.css": "tailwindcss"
}
}

23
AGENTS.md Normal file
View File

@@ -0,0 +1,23 @@
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
## Available MCP Tools:
### 1. list-sections
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
### 2. get-documentation
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
### 3. svelte-autofixer
Analyzes Svelte code and returns issues and suggestions.
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
### 4. playground-link
Generates a Svelte Playground link with the provided code.
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.

48
CLAUDE.md Normal file
View File

@@ -0,0 +1,48 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
**excel2json** — a SvelteKit web application for converting Excel files to JSON. Built with Svelte 5, SvelteKit 2, TypeScript, and Tailwind CSS 4.
## Commands
- `npm run dev` — start dev server
- `npm run build` — production build
- `npm run preview` — preview production build
- `npm run check` — type-check with svelte-check
- `npm run test` — run all tests once
- `npm run test:unit` — run tests in watch mode
- `npm run test:unit -- --run --testNamePattern="pattern"` — run a single test by name
## Architecture
- **Framework**: SvelteKit 2 with Svelte 5 (runes API: `$state`, `$props`, `$derived`, etc.)
- **Styling**: Tailwind CSS 4 with `@tailwindcss/forms` and `@tailwindcss/typography` plugins, configured via `src/routes/layout.css`
- **Adapter**: `@sveltejs/adapter-auto`
- **TypeScript**: strict mode enabled
### Testing
Two Vitest project configurations in `vite.config.ts`:
- **`client`** — browser tests using Playwright (headless Chromium). Files matching `src/**/*.svelte.{test,spec}.{js,ts}`. Uses `vitest-browser-svelte` for component rendering.
- **`server`** — Node.js unit tests. Files matching `src/**/*.{test,spec}.{js,ts}` (excluding `.svelte.` test files).
All tests require assertions (`expect.requireAssertions: true`).
### Key Conventions
- Svelte components use `lang="ts"` in script tags
- Shared library code goes in `src/lib/` (aliased as `$lib`)
- Use Svelte 5 runes syntax, not legacy Svelte 4 patterns
## Svelte MCP Server
A Svelte MCP server is available for Svelte 5 / SvelteKit documentation lookup and code validation. When writing Svelte code:
1. Use `list-sections` first to discover relevant documentation
2. Use `get-documentation` to fetch needed sections
3. Use `svelte-autofixer` to validate Svelte code before finalizing — keep calling until no issues remain
4. Offer `playground-link` only if code was NOT written to project files

126
DEMAND.md Normal file
View File

@@ -0,0 +1,126 @@
项目需求文档 (PRD): Excel 转 JSON 可视化映射工具
1. 项目概述
我们需要开发一个基于 Svelte 的单页应用。该应用允许用户上传 Excel 文件,在左侧预览数据,在右侧实时预览转换后的 JSON 数据。核心功能是用户可以自定义“Excel列”到“JSON字段”的映射规则、处理空值逻辑以及格式化特定数据类型如日期并支持将这些配置导出为模板以便下次复用。
2. 技术栈要求
框架: Svelte (推荐使用 Svelte 5 或 SvelteKit) + TypeScript
样式: TailwindCSS (用于快速构建左右分栏布局)
Excel 处理: xlsx (SheetJS) 或类似的轻量级库
图标库: Lucide-svelte (可选)
3. 界面布局 (UI Layout)
页面主要分为 顶部工具栏 和 主体内容区。
顶部工具栏 (Header):
文件上传按钮 (支持拖拽上传 Excel)。
模板操作区:[导入配置模板]、[导出当前配置]。
全局操作:[下载 JSON]、[复制 JSON]。
主体内容区 (Main Split View):
左侧 (Source Panel - 50%): 显示 Excel 解析后的表格数据。
关键交互: 表头应包含“设置”功能。用户点击表头或表头旁边的图标,可以弹出/展开该列的映射配置面板。
右侧 (Target Panel - 50%): 显示转换后的 JSON 代码预览(支持语法高亮)。
4. 核心功能细节
4.1 Excel 导入与展示
支持 .xlsx, .xls, .csv 格式。
读取 Excel 的第一行作为默认表头Key
数据以表格形式展示在左侧。
4.2 字段映射配置 (Mapping Logic)
这是本应用的核心。每一列都需要一个配置对象,包含以下属性:
Original Header (源字段): Excel 原始表头名称 (只读)。
Target Key (目标字段): 映射到 JSON 中的 Key 名称 (用户可修改)。
示例: Excel 中是 "姓名",用户输入 "userName",生成的 JSON 为 {"userName": "..."}。
Data Type (数据类型):
String (默认)
Number
Boolean
Date
Format Rules (格式化规则 - 仅针对特定类型):
如果是 Date 类型,提供格式化选项 (如 YYYY-MM-DD, YYYY/MM/DD HH:mm, Unix Timestamp)。需要引入 dayjs 或类似库处理。
Null Handling (空值处理):
开关选项Exclude if Empty (如果该单元格为空,则在生成的 JSON 对象中完全移除该 Key)。
默认值:如果未勾选“移除”,可设置一个默认值 (Default Value)。
4.3 JSON 实时预览
当用户修改映射配置(如修改 Key 名称、切换日期格式、改变空值策略)时,右侧的 JSON 预览应 实时 (Reactive) 更新。
4.4 模板系统 (Configuration Persistence)
导出模板: 将当前的映射规则数组导出为 .json 文件。
数据结构示例:
JSON
[
{ "source": "A", "target": "AAA", "type": "string", "excludeIfEmpty": false },
{ "source": "入职日期", "target": "joinDate", "type": "date", "format": "YYYY-MM-DD" }
]
导入模板: 上传上述格式的 JSON 文件,应用自动匹配当前 Excel 的表头。如果 Excel 包含模板中定义的 source 字段,则自动应用对应的规则。
5. 交互流程 (User Story)
用户打开页面,拖入 staff_data.xlsx。
左侧显示表格。用户发现“出生日期”这一列解析出来是数字Excel 时间戳)。
用户点击“出生日期”列的设置,将类型改为 Date格式选择 YYYY-MM-DD。
用户发现有一列“备注”很多是空的,点击设置,勾选 Exclude if Empty。
用户将“姓名”列的 Target Key 改为 full_name。
右侧 JSON 实时变成了期望的格式。
用户点击“导出配置”,保存为 staff_mapping.json。
下周,用户上传新的 Excel并点击“导入配置”选择 staff_mapping.json所有规则自动应用直接复制右侧 JSON。
请执行以下任务:
数据结构设计: 定义 MappingConfig 和 RowData 的 TypeScript 接口。
核心逻辑实现: 编写一个 convertData 函数,根据映射配置将 Excel 原始数据转换为最终 JSON。
组件编写:
App.svelte: 主布局和状态管理。
ExcelTable.svelte: 左侧表格,包含列头配置 UI。
JsonPreview.svelte: 右侧展示。
请确保代码不仅能运行,而且具有良好的错误处理(例如文件解析失败)。
支持嵌套对象: 如果 Target Key 包含点号(例如 user.address.city生成的 JSON 应当自动构建对应的嵌套对象结构,而不是生成一个带点的字符串键名。

23
GEMINI.md Normal file
View File

@@ -0,0 +1,23 @@
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
## Available MCP Tools:
### 1. list-sections
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
### 2. get-documentation
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
### 3. svelte-autofixer
Analyzes Svelte code and returns issues and suggestions.
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
### 4. playground-link
Generates a Svelte Playground link with the provided code.
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.

42
README.md Normal file
View File

@@ -0,0 +1,42 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```sh
# create a new project
npx sv create my-app
```
To recreate this project with the same configuration:
```sh
# recreate this project
npx sv create --template minimal --types ts --add vitest="usages:unit,component" tailwindcss="plugins:typography,forms" devtools-json mcp="ide:claude-code,vscode,other,gemini,cursor,opencode+setup:remote" --install yarn .
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```sh
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```sh
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

9
opencode.json Normal file
View File

@@ -0,0 +1,9 @@
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"svelte": {
"type": "remote",
"url": "https://mcp.svelte.dev/mcp"
}
}
}

1738
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "excel2json",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"test:unit": "vitest",
"test": "npm run test:unit -- --run"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/kit": "^2.50.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18",
"@vitest/browser-playwright": "^4.0.18",
"playwright": "^1.58.1",
"svelte": "^5.49.2",
"svelte-check": "^4.3.6",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite-plugin-devtools-json": "^1.0.0",
"vitest": "^4.0.18",
"vitest-browser-svelte": "^2.0.2"
},
"dependencies": {
"dayjs": "^1.11.19",
"xlsx": "^0.18.5"
}
}

13
src/app.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

11
src/app.html Normal file
View File

@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

7
src/demo.spec.ts Normal file
View File

@@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,107 @@
<script lang="ts">
import type { MappingConfig, DataType, DateFormat } from '$lib/types.js';
let { config = $bindable(), onclose }: { config: MappingConfig; onclose: () => void } = $props();
const dataTypes: { value: DataType; label: string }[] = [
{ value: 'string', label: '字符串 (String)' },
{ value: 'number', label: '数字 (Number)' },
{ value: 'boolean', label: '布尔 (Boolean)' },
{ value: 'date', label: '日期 (Date)' }
];
const dateFormats: { value: DateFormat; label: string }[] = [
{ value: 'YYYY-MM-DD', label: 'YYYY-MM-DD' },
{ value: 'YYYY/MM/DD', label: 'YYYY/MM/DD' },
{ value: 'YYYY-MM-DD HH:mm', label: 'YYYY-MM-DD HH:mm' },
{ value: 'YYYY/MM/DD HH:mm', label: 'YYYY/MM/DD HH:mm' },
{ value: 'YYYY-MM-DD HH:mm:ss', label: 'YYYY-MM-DD HH:mm:ss' },
{ value: 'timestamp', label: 'Unix Timestamp' }
];
function onTypeChange(e: Event) {
const select = e.target as HTMLSelectElement;
config.type = select.value as DataType;
if (config.type === 'date' && !config.format) {
config.format = 'YYYY-MM-DD';
}
if (config.type !== 'date') {
config.format = undefined;
}
}
</script>
<div class="absolute top-full left-0 z-50 mt-1 w-72 rounded-lg border border-gray-200 bg-white p-4 shadow-xl">
<div class="mb-3 flex items-center justify-between">
<h4 class="text-sm font-semibold text-gray-700">列配置</h4>
<button onclick={onclose} aria-label="关闭配置" class="text-gray-400 hover:text-gray-600 cursor-pointer">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div class="space-y-3">
<!-- Source header (read-only) -->
<div>
<label for="source-{config.source}" class="mb-1 block text-xs font-medium text-gray-500">源字段</label>
<input id="source-{config.source}" type="text" value={config.source} disabled
class="w-full rounded border border-gray-200 bg-gray-50 px-2 py-1.5 text-sm text-gray-500" />
</div>
<!-- Target key -->
<div>
<label for="target-{config.source}" class="mb-1 block text-xs font-medium text-gray-500">目标字段 (JSON Key)</label>
<input id="target-{config.source}" type="text" bind:value={config.target}
class="w-full rounded border border-gray-300 px-2 py-1.5 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500" />
</div>
<!-- Data type -->
<div>
<label for="type-{config.source}" class="mb-1 block text-xs font-medium text-gray-500">数据类型</label>
<select id="type-{config.source}" value={config.type} onchange={onTypeChange}
class="w-full rounded border border-gray-300 px-2 py-1.5 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500">
{#each dataTypes as dt (dt.value)}
<option value={dt.value}>{dt.label}</option>
{/each}
</select>
</div>
<!-- Date format (only for date type) -->
{#if config.type === 'date'}
<div>
<label for="format-{config.source}" class="mb-1 block text-xs font-medium text-gray-500">日期格式</label>
<select id="format-{config.source}" bind:value={config.format}
class="w-full rounded border border-gray-300 px-2 py-1.5 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500">
{#each dateFormats as df (df.value)}
<option value={df.value}>{df.label}</option>
{/each}
</select>
</div>
{/if}
<!-- Exclude if empty -->
<div class="flex items-center gap-2">
<input type="checkbox" id="exclude-empty-{config.source}" bind:checked={config.excludeIfEmpty}
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
<label for="exclude-empty-{config.source}" class="text-sm text-gray-600">空值时移除该字段</label>
</div>
<!-- Default value -->
{#if !config.excludeIfEmpty}
<div>
<label for="default-{config.source}" class="mb-1 block text-xs font-medium text-gray-500">默认值 (空值时)</label>
<input id="default-{config.source}" type="text" bind:value={config.defaultValue}
class="w-full rounded border border-gray-300 px-2 py-1.5 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500"
placeholder="留空则为 null" />
</div>
{/if}
<!-- Enable/disable column -->
<div class="flex items-center gap-2 border-t border-gray-100 pt-3">
<input type="checkbox" id="enabled-{config.source}" bind:checked={config.enabled}
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500" />
<label for="enabled-{config.source}" class="text-sm text-gray-600">包含此列到输出</label>
</div>
</div>
</div>

View File

@@ -0,0 +1,94 @@
<script lang="ts">
import type { MappingConfig, RowData } from '$lib/types.js';
import ColumnConfig from './ColumnConfig.svelte';
let {
headers,
rows,
mappings = $bindable()
}: {
headers: string[];
rows: RowData[];
mappings: MappingConfig[];
} = $props();
let activeConfigIndex = $state<number | null>(null);
const maxPreviewRows = 100;
function toggleConfig(index: number) {
activeConfigIndex = activeConfigIndex === index ? null : index;
}
function displayValue(value: unknown): string {
if (value === undefined || value === null || value === '') return '';
if (value instanceof Date) {
const y = value.getFullYear();
const m = String(value.getMonth() + 1).padStart(2, '0');
const d = String(value.getDate()).padStart(2, '0');
const H = String(value.getHours()).padStart(2, '0');
const M = String(value.getMinutes()).padStart(2, '0');
const S = String(value.getSeconds()).padStart(2, '0');
if (H === '00' && M === '00' && S === '00') return `${y}-${m}-${d}`;
return `${y}-${m}-${d} ${H}:${M}:${S}`;
}
return String(value);
}
</script>
<div class="flex h-full flex-col overflow-hidden">
<div class="flex-shrink-0 border-b border-gray-200 bg-gray-50 px-4 py-2">
<h3 class="text-sm font-semibold text-gray-700">
Excel 数据
<span class="ml-2 text-xs font-normal text-gray-400">
{rows.length}× {headers.length}
{#if rows.length > maxPreviewRows}
(显示前 {maxPreviewRows} 行)
{/if}
</span>
</h3>
</div>
<div class="flex-1 overflow-auto">
<table class="w-full text-sm">
<thead class="sticky top-0 z-10 bg-gray-50">
<tr>
{#each headers as header, i (header)}
<th class="relative border-b border-r border-gray-200 px-3 py-2 text-left font-medium text-gray-600">
<div class="flex items-center gap-1">
<span class="truncate" title={header}>{header}</span>
<button
onclick={() => toggleConfig(i)}
class="ml-auto flex-shrink-0 rounded p-0.5 text-gray-400 hover:bg-gray-200 hover:text-gray-600 cursor-pointer"
title="配置此列"
>
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
{#if mappings[i] && mappings[i].target !== mappings[i].source}
<span class="text-xs text-blue-500" title="映射为: {mappings[i].target}">{mappings[i].target}</span>
{/if}
</div>
{#if activeConfigIndex === i && mappings[i]}
<ColumnConfig bind:config={mappings[i]} onclose={() => (activeConfigIndex = null)} />
{/if}
</th>
{/each}
</tr>
</thead>
<tbody>
{#each rows.slice(0, maxPreviewRows) as row, rowIdx (rowIdx)}
<tr class={rowIdx % 2 === 0 ? 'bg-white' : 'bg-gray-50/50'}>
{#each headers as header (header)}
<td class="border-b border-r border-gray-100 px-3 py-1.5 text-gray-700" title={displayValue(row[header])}>
<span class="block max-w-[200px] truncate">{displayValue(row[header])}</span>
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
</div>

View File

@@ -0,0 +1,70 @@
<script lang="ts">
let { json }: { json: Record<string, unknown>[] } = $props();
let collapsed = $state(false);
const jsonString = $derived(JSON.stringify(json, null, 2));
/**
* Simple JSON syntax highlighting — light theme.
*/
function highlight(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"([^"]*)"(?=\s*:)/g, '<span class="json-key">"$1"</span>')
.replace(/:\s*"([^"]*)"/g, ': <span class="json-string">"$1"</span>')
.replace(/:\s*(\d+\.?\d*)/g, ': <span class="json-number">$1</span>')
.replace(/:\s*(true|false)/g, ': <span class="json-bool">$1</span>')
.replace(/:\s*(null)/g, ': <span class="json-null">$1</span>');
}
</script>
<div class="flex h-full flex-col overflow-hidden">
<div class="flex flex-shrink-0 items-center justify-between border-b border-gray-200 bg-gray-50 px-4 py-2">
<h3 class="text-sm font-semibold text-gray-700">
JSON 预览
<span class="ml-2 text-xs font-normal text-gray-400">{json.length} 条记录</span>
</h3>
<div class="flex items-center gap-2">
<button
onclick={() => (collapsed = !collapsed)}
class="rounded px-2 py-1 text-xs text-gray-500 hover:bg-gray-200 cursor-pointer"
>
{collapsed ? '展开' : '折叠'}
</button>
</div>
</div>
<div class="json-preview flex-1 overflow-auto p-4">
{#if collapsed}
<pre class="font-mono text-xs leading-relaxed text-gray-600">{JSON.stringify(json)}</pre>
{:else}
<pre class="font-mono text-xs leading-relaxed text-gray-500">{@html highlight(jsonString)}</pre>
{/if}
</div>
</div>
<style>
.json-preview {
background: #fafbfc;
}
:global(.json-key) {
color: #24292e;
font-weight: 500;
}
:global(.json-string) {
color: #22863a;
}
:global(.json-number) {
color: #005cc5;
}
:global(.json-bool) {
color: #d73a49;
}
:global(.json-null) {
color: #9ca3af;
font-style: italic;
}
</style>

145
src/lib/converter.spec.ts Normal file
View File

@@ -0,0 +1,145 @@
import { describe, it, expect } from 'vitest';
import { convertData, createDefaultMappings, applyTemplate, exportTemplate } from '$lib/converter.js';
import type { MappingConfig, RowData } from '$lib/types.js';
describe('createDefaultMappings', () => {
it('creates mappings from headers with correct defaults', () => {
const mappings = createDefaultMappings(['姓名', '年龄']);
expect(mappings).toHaveLength(2);
expect(mappings[0]).toEqual({
source: '姓名',
target: '姓名',
type: 'string',
format: undefined,
excludeIfEmpty: false,
defaultValue: '',
enabled: true
});
});
it('auto-detects date columns from row data', () => {
const rows: RowData[] = [
{ name: 'Alice', birthday: new Date('2000-01-01') },
{ name: 'Bob', birthday: new Date('1995-06-15') }
];
const mappings = createDefaultMappings(['name', 'birthday'], rows);
expect(mappings[0].type).toBe('string');
expect(mappings[1].type).toBe('date');
expect(mappings[1].format).toBe('YYYY-MM-DD');
});
});
describe('convertData', () => {
const rows: RowData[] = [
{ name: 'Alice', age: 30, active: 'true', notes: '' },
{ name: 'Bob', age: 25, active: 'false', notes: 'some note' }
];
it('converts with default string mappings', () => {
const mappings = createDefaultMappings(['name', 'age']);
const result = convertData(rows, mappings);
expect(result).toHaveLength(2);
expect(result[0]).toEqual({ name: 'Alice', age: '30' });
});
it('converts number type', () => {
const mappings: MappingConfig[] = [
{ source: 'age', target: 'age', type: 'number', excludeIfEmpty: false, defaultValue: '', enabled: true }
];
const result = convertData(rows, mappings);
expect(result[0]).toEqual({ age: 30 });
});
it('converts boolean type', () => {
const mappings: MappingConfig[] = [
{ source: 'active', target: 'isActive', type: 'boolean', excludeIfEmpty: false, defaultValue: '', enabled: true }
];
const result = convertData(rows, mappings);
expect(result[0]).toEqual({ isActive: true });
expect(result[1]).toEqual({ isActive: false });
});
it('excludes empty fields when excludeIfEmpty is true', () => {
const mappings: MappingConfig[] = [
{ source: 'notes', target: 'notes', type: 'string', excludeIfEmpty: true, defaultValue: '', enabled: true }
];
const result = convertData(rows, mappings);
expect(result[0]).toEqual({});
expect(result[1]).toEqual({ notes: 'some note' });
});
it('uses default value for empty fields', () => {
const mappings: MappingConfig[] = [
{ source: 'notes', target: 'notes', type: 'string', excludeIfEmpty: false, defaultValue: 'N/A', enabled: true }
];
const result = convertData(rows, mappings);
expect(result[0]).toEqual({ notes: 'N/A' });
});
it('returns null for empty fields with no default', () => {
const mappings: MappingConfig[] = [
{ source: 'notes', target: 'notes', type: 'string', excludeIfEmpty: false, defaultValue: '', enabled: true }
];
const result = convertData(rows, mappings);
expect(result[0]).toEqual({ notes: null });
});
it('skips disabled columns', () => {
const mappings: MappingConfig[] = [
{ source: 'name', target: 'name', type: 'string', excludeIfEmpty: false, defaultValue: '', enabled: false },
{ source: 'age', target: 'age', type: 'number', excludeIfEmpty: false, defaultValue: '', enabled: true }
];
const result = convertData(rows, mappings);
expect(result[0]).toEqual({ age: 30 });
});
it('renames target keys', () => {
const mappings: MappingConfig[] = [
{ source: 'name', target: 'full_name', type: 'string', excludeIfEmpty: false, defaultValue: '', enabled: true }
];
const result = convertData(rows, mappings);
expect(result[0]).toEqual({ full_name: 'Alice' });
});
});
describe('date conversion', () => {
it('formats Excel serial date numbers', () => {
const rows: RowData[] = [{ date: 44927 }]; // 2023-01-01
const mappings: MappingConfig[] = [
{ source: 'date', target: 'date', type: 'date', format: 'YYYY-MM-DD', excludeIfEmpty: false, defaultValue: '', enabled: true }
];
const result = convertData(rows, mappings);
expect(result[0].date).toBe('2023-01-01');
});
it('returns unix timestamp when format is timestamp', () => {
const rows: RowData[] = [{ date: '2023-01-01' }];
const mappings: MappingConfig[] = [
{ source: 'date', target: 'date', type: 'date', format: 'timestamp', excludeIfEmpty: false, defaultValue: '', enabled: true }
];
const result = convertData(rows, mappings);
expect(typeof result[0].date).toBe('number');
});
});
describe('template system', () => {
it('exports and applies templates', () => {
const mappings = createDefaultMappings(['姓名', '入职日期']);
mappings[0].target = 'userName';
mappings[1].type = 'date';
mappings[1].format = 'YYYY-MM-DD';
mappings[1].target = 'joinDate';
const template = exportTemplate(mappings);
expect(template).toHaveLength(2);
expect(template[0].target).toBe('userName');
expect(template[1].format).toBe('YYYY-MM-DD');
// Apply to fresh mappings
const freshMappings = createDefaultMappings(['姓名', '入职日期', '备注']);
const applied = applyTemplate(freshMappings, template);
expect(applied[0].target).toBe('userName');
expect(applied[1].type).toBe('date');
expect(applied[2].target).toBe('备注'); // not in template, unchanged
});
});

172
src/lib/converter.ts Normal file
View File

@@ -0,0 +1,172 @@
import dayjs from 'dayjs';
import type { MappingConfig, RowData, DataType, MappingTemplate } from './types.js';
/**
* Check if a value is considered empty.
*/
function isEmpty(value: unknown): boolean {
return value === undefined || value === null || value === '';
}
/**
* Convert a raw cell value to the specified data type.
*/
function convertValue(value: unknown, type: DataType, format?: string): unknown {
if (isEmpty(value)) return undefined;
switch (type) {
case 'number': {
const num = Number(value);
return isNaN(num) ? value : num;
}
case 'boolean': {
if (typeof value === 'boolean') return value;
const str = String(value).toLowerCase().trim();
if (['true', '1', 'yes', '是'].includes(str)) return true;
if (['false', '0', 'no', '否'].includes(str)) return false;
return Boolean(value);
}
case 'date': {
return formatDate(value, format);
}
case 'string':
default:
if (value instanceof Date) return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
return String(value);
}
}
/**
* Format a value as a date string. Handles Excel serial date numbers.
*/
function formatDate(value: unknown, format?: string): string | number {
let date: dayjs.Dayjs;
if (value instanceof Date) {
date = dayjs(value);
} else if (typeof value === 'number') {
// Excel serial date: days since 1900-01-01 (with the 1900 leap year bug)
// Use UTC to avoid timezone issues
const excelEpochMs = Date.UTC(1899, 11, 30);
const ms = excelEpochMs + value * 86400000;
date = dayjs(new Date(ms));
} else {
date = dayjs(value as string);
}
if (!date.isValid()) return String(value);
if (format === 'timestamp') {
return date.unix();
}
return date.format(format || 'YYYY-MM-DD');
}
/**
* Convert raw Excel rows to JSON objects based on mapping configs.
*/
export function convertData(
rows: RowData[],
mappings: MappingConfig[]
): Record<string, unknown>[] {
const enabledMappings = mappings.filter((m) => m.enabled);
return rows.map((row) => {
const obj: Record<string, unknown> = {};
for (const mapping of enabledMappings) {
const rawValue = row[mapping.source];
const isEmptyVal = isEmpty(rawValue);
if (isEmptyVal && mapping.excludeIfEmpty) {
continue;
}
if (isEmptyVal) {
obj[mapping.target] = mapping.defaultValue !== undefined && mapping.defaultValue !== ''
? convertValue(mapping.defaultValue, mapping.type, mapping.format)
: null;
} else {
obj[mapping.target] = convertValue(rawValue, mapping.type, mapping.format);
}
}
return obj;
});
}
/**
* Create default mapping configs from Excel headers.
* Auto-detects date columns by sampling row data.
*/
export function createDefaultMappings(headers: string[], rows?: RowData[]): MappingConfig[] {
return headers.map((header) => {
const detectedType = rows ? detectColumnType(header, rows) : 'string';
return {
source: header,
target: header,
type: detectedType,
format: detectedType === 'date' ? 'YYYY-MM-DD' : undefined,
excludeIfEmpty: false,
defaultValue: '',
enabled: true
};
});
}
/**
* Detect the data type of a column by sampling its values.
*/
function detectColumnType(header: string, rows: RowData[]): DataType {
const sample = rows.slice(0, 20);
let dateCount = 0;
let totalNonEmpty = 0;
for (const row of sample) {
const val = row[header];
if (val === undefined || val === null || val === '') continue;
totalNonEmpty++;
if (val instanceof Date) dateCount++;
}
if (totalNonEmpty > 0 && dateCount / totalNonEmpty >= 0.5) return 'date';
return 'string';
}
/**
* Apply a template to existing mappings. Matches by source header name.
*/
export function applyTemplate(
currentMappings: MappingConfig[],
template: MappingTemplate
): MappingConfig[] {
const templateMap = new Map(template.map((t) => [t.source, t]));
return currentMappings.map((mapping) => {
const tmpl = templateMap.get(mapping.source);
if (tmpl) {
return {
...mapping,
target: tmpl.target,
type: tmpl.type,
format: tmpl.format,
excludeIfEmpty: tmpl.excludeIfEmpty,
defaultValue: tmpl.defaultValue ?? ''
};
}
return mapping;
});
}
/**
* Export current mappings as a template.
*/
export function exportTemplate(mappings: MappingConfig[]): MappingTemplate {
return mappings.map(({ source, target, type, format, excludeIfEmpty, defaultValue }) => {
const entry: MappingTemplate[number] = { source, target, type, excludeIfEmpty };
if (type === 'date' && format) entry.format = format;
if (defaultValue) entry.defaultValue = defaultValue;
return entry;
});
}

44
src/lib/excel.ts Normal file
View File

@@ -0,0 +1,44 @@
import * as XLSX from 'xlsx';
import type { RowData } from './types.js';
export interface ParsedExcel {
headers: string[];
rows: RowData[];
sheetNames: string[];
}
/**
* Parse an Excel/CSV file into headers and rows.
*/
export async function parseExcelFile(file: File): Promise<ParsedExcel> {
const buffer = await file.arrayBuffer();
const workbook = XLSX.read(buffer, { type: 'array', cellDates: true });
const sheetNames = workbook.SheetNames;
if (sheetNames.length === 0) {
throw new Error('文件中没有找到任何工作表');
}
return parseSheet(workbook, sheetNames[0]);
}
/**
* Parse a specific sheet from a workbook.
*/
export function parseSheet(workbook: XLSX.WorkBook, sheetName: string): ParsedExcel {
const sheet = workbook.Sheets[sheetName];
if (!sheet) {
throw new Error(`工作表 "${sheetName}" 不存在`);
}
const jsonData = XLSX.utils.sheet_to_json<Record<string, unknown>>(sheet, {
defval: ''
});
if (jsonData.length === 0) {
throw new Error('工作表中没有数据');
}
const headers = Object.keys(jsonData[0]);
return { headers, rows: jsonData, sheetNames: workbook.SheetNames };
}

1
src/lib/index.ts Normal file
View File

@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

35
src/lib/types.ts Normal file
View File

@@ -0,0 +1,35 @@
export type DataType = 'string' | 'number' | 'boolean' | 'date';
export type DateFormat =
| 'YYYY-MM-DD'
| 'YYYY/MM/DD'
| 'YYYY-MM-DD HH:mm'
| 'YYYY/MM/DD HH:mm'
| 'YYYY-MM-DD HH:mm:ss'
| 'timestamp';
export interface MappingConfig {
/** Excel original header name (read-only) */
source: string;
/** Target key name in JSON output */
target: string;
/** Data type for conversion */
type: DataType;
/** Date format string (only used when type is 'date') */
format?: DateFormat;
/** If true, exclude this key from JSON when cell is empty */
excludeIfEmpty: boolean;
/** Default value when cell is empty and excludeIfEmpty is false */
defaultValue?: string;
/** Whether this column is included in output */
enabled: boolean;
}
/** A single row of raw Excel data, keyed by original header */
export type RowData = Record<string, unknown>;
/** Template file structure for import/export */
export type MappingTemplate = Pick<
MappingConfig,
'source' | 'target' | 'type' | 'format' | 'excludeIfEmpty' | 'defaultValue'
>[];

View File

@@ -0,0 +1,9 @@
<script lang="ts">
import './layout.css';
import favicon from '$lib/assets/favicon.svg';
let { children } = $props();
</script>
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
{@render children()}

259
src/routes/+page.svelte Normal file
View File

@@ -0,0 +1,259 @@
<script lang="ts">
import type { MappingConfig, RowData, MappingTemplate } from '$lib/types.js';
import { parseExcelFile } from '$lib/excel.js';
import { convertData, createDefaultMappings, applyTemplate, exportTemplate } from '$lib/converter.js';
import ExcelTable from '$lib/components/ExcelTable.svelte';
import JsonPreview from '$lib/components/JsonPreview.svelte';
// State
let headers = $state<string[]>([]);
let rows = $state<RowData[]>([]);
let mappings = $state<MappingConfig[]>([]);
let errorMessage = $state('');
let fileName = $state('');
let isDragOver = $state(false);
let copySuccess = $state(false);
// Derived
const hasData = $derived(headers.length > 0 && rows.length > 0);
const convertedJson = $derived(hasData ? convertData(rows, mappings) : []);
const jsonString = $derived(JSON.stringify(convertedJson, null, 2));
// File handling
async function handleFile(file: File) {
const validExtensions = ['.xlsx', '.xls', '.csv'];
const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
if (!validExtensions.includes(ext)) {
errorMessage = `不支持的文件格式: ${ext}。请上传 .xlsx, .xls 或 .csv 文件。`;
return;
}
try {
errorMessage = '';
const result = await parseExcelFile(file);
headers = result.headers;
rows = result.rows;
mappings = createDefaultMappings(result.headers, result.rows);
fileName = file.name;
} catch (e) {
errorMessage = e instanceof Error ? e.message : '文件解析失败';
headers = [];
rows = [];
mappings = [];
}
}
function onFileInput(e: Event) {
const input = e.target as HTMLInputElement;
const file = input.files?.[0];
if (file) handleFile(file);
input.value = '';
}
function onDrop(e: DragEvent) {
e.preventDefault();
isDragOver = false;
const file = e.dataTransfer?.files[0];
if (file) handleFile(file);
}
function onDragOver(e: DragEvent) {
e.preventDefault();
isDragOver = true;
}
function onDragLeave() {
isDragOver = false;
}
// Template handling
function handleExportTemplate() {
const template = exportTemplate(mappings);
const blob = new Blob([JSON.stringify(template, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName ? fileName.replace(/\.[^.]+$/, '_mapping.json') : 'mapping_template.json';
a.click();
URL.revokeObjectURL(url);
}
function handleImportTemplate() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = async () => {
const file = input.files?.[0];
if (!file) return;
try {
const text = await file.text();
const template: MappingTemplate = JSON.parse(text);
if (!Array.isArray(template)) {
errorMessage = '无效的模板格式';
return;
}
mappings = applyTemplate(mappings, template);
errorMessage = '';
} catch {
errorMessage = '模板文件解析失败';
}
};
input.click();
}
// JSON operations
function handleDownloadJson() {
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName ? fileName.replace(/\.[^.]+$/, '.json') : 'output.json';
a.click();
URL.revokeObjectURL(url);
}
async function handleCopyJson() {
try {
await navigator.clipboard.writeText(jsonString);
copySuccess = true;
setTimeout(() => (copySuccess = false), 2000);
} catch {
errorMessage = '复制失败,请手动复制';
}
}
</script>
<div
class="flex h-screen flex-col bg-gray-100"
ondrop={onDrop}
ondragover={onDragOver}
ondragleave={onDragLeave}
role="application"
>
<!-- Header Toolbar -->
<header class="flex flex-shrink-0 items-center gap-3 border-b border-gray-200 bg-white px-4 py-3 shadow-sm">
<h1 class="text-lg font-bold text-gray-800">Excel → JSON</h1>
<div class="mx-2 h-6 w-px bg-gray-200"></div>
<!-- File upload -->
<label class="inline-flex cursor-pointer items-center gap-1.5 rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
上传文件
<input type="file" accept=".xlsx,.xls,.csv" onchange={onFileInput} class="hidden" />
</label>
{#if fileName}
<span class="text-sm text-gray-500">{fileName}</span>
{/if}
<div class="mx-2 h-6 w-px bg-gray-200"></div>
<!-- Template operations -->
<button
onclick={handleImportTemplate}
disabled={!hasData}
class="inline-flex items-center gap-1 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
导入配置
</button>
<button
onclick={handleExportTemplate}
disabled={!hasData}
class="inline-flex items-center gap-1 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
导出配置
</button>
<div class="flex-1"></div>
<!-- JSON operations -->
<button
onclick={handleDownloadJson}
disabled={!hasData}
class="inline-flex items-center gap-1 rounded-md bg-green-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-green-700 disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
下载 JSON
</button>
<button
onclick={handleCopyJson}
disabled={!hasData}
class="inline-flex items-center gap-1 rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer"
>
{#if copySuccess}
<svg class="h-4 w-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
已复制
{:else}
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
复制 JSON
{/if}
</button>
</header>
<!-- Error message -->
{#if errorMessage}
<div class="flex items-center gap-2 bg-red-50 px-4 py-2 text-sm text-red-700">
<svg class="h-4 w-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{errorMessage}
<button onclick={() => (errorMessage = '')} aria-label="关闭错误" class="ml-auto text-red-500 hover:text-red-700 cursor-pointer">
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/if}
<!-- Main content -->
{#if hasData}
<div class="flex flex-1 overflow-hidden">
<!-- Left: Excel Table -->
<div class="w-1/2 overflow-hidden border-r border-gray-300">
<ExcelTable {headers} {rows} bind:mappings />
</div>
<!-- Right: JSON Preview -->
<div class="w-1/2 overflow-hidden">
<JsonPreview json={convertedJson} />
</div>
</div>
{:else}
<!-- Empty state / drop zone -->
<div class="flex flex-1 items-center justify-center p-8">
<div
class="flex max-w-md flex-col items-center rounded-2xl border-2 border-dashed p-12 text-center transition-colors {isDragOver
? 'border-blue-500 bg-blue-50'
: 'border-gray-300 bg-white'}"
>
<svg class="mb-4 h-16 w-16 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<h2 class="mb-2 text-xl font-semibold text-gray-700">拖拽 Excel 文件到此处</h2>
<p class="mb-4 text-sm text-gray-500">支持 .xlsx, .xls, .csv 格式</p>
<label class="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-blue-600 px-5 py-2.5 text-sm font-medium text-white hover:bg-blue-700">
<svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
选择文件
<input type="file" accept=".xlsx,.xls,.csv" onchange={onFileInput} class="hidden" />
</label>
</div>
</div>
{/if}
</div>

3
src/routes/layout.css Normal file
View File

@@ -0,0 +1,3 @@
@import 'tailwindcss';
@plugin '@tailwindcss/forms';
@plugin '@tailwindcss/typography';

View File

@@ -0,0 +1,21 @@
import { page } from 'vitest/browser';
import { describe, expect, it } from 'vitest';
import { render } from 'vitest-browser-svelte';
import Page from './+page.svelte';
describe('/+page.svelte', () => {
it('should render the app title', async () => {
render(Page);
const heading = page.getByRole('heading', { level: 1 });
await expect.element(heading).toBeInTheDocument();
await expect.element(heading).toHaveTextContent('Excel → JSON');
});
it('should show the drop zone when no file is loaded', async () => {
render(Page);
const dropText = page.getByText('拖拽 Excel 文件到此处');
await expect.element(dropText).toBeInTheDocument();
});
});

3
static/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
# allow crawling everything by default
User-agent: *
Disallow:

13
svelte.config.js Normal file
View File

@@ -0,0 +1,13 @@
import adapter from '@sveltejs/adapter-auto';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

20
tsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"rewriteRelativeImportExtensions": true,
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// To make changes to top-level options such as include and exclude, we recommend extending
// the generated config; see https://svelte.dev/docs/kit/configuration#typescript
}

37
vite.config.ts Normal file
View File

@@ -0,0 +1,37 @@
import devtoolsJson from 'vite-plugin-devtools-json';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vitest/config';
import { playwright } from '@vitest/browser-playwright';
import { sveltekit } from '@sveltejs/kit/vite';
export default defineConfig({
plugins: [tailwindcss(), sveltekit(), devtoolsJson()],
test: {
expect: { requireAssertions: true },
projects: [
{
extends: './vite.config.ts',
test: {
name: 'client',
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium', headless: true }]
},
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
exclude: ['src/lib/server/**']
}
},
{
extends: './vite.config.ts',
test: {
name: 'server',
environment: 'node',
include: ['src/**/*.{test,spec}.{js,ts}'],
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}']
}
}
]
}
});

859
yarn.lock Normal file
View File

@@ -0,0 +1,859 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@esbuild/win32-x64@0.27.3":
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==
"@jridgewell/gen-mapping@^0.3.5":
version "0.3.13"
resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz"
integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.0"
"@jridgewell/trace-mapping" "^0.3.24"
"@jridgewell/remapping@^2.3.4":
version "2.3.5"
resolved "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz"
integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==
dependencies:
"@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24"
"@jridgewell/resolve-uri@^3.1.0":
version "3.1.2"
resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz"
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5":
version "1.5.5"
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz"
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25":
version "0.3.31"
resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz"
integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==
dependencies:
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@polka/url@^1.0.0-next.24":
version "1.0.0-next.29"
resolved "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz"
integrity sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==
"@rollup/rollup-win32-x64-gnu@4.57.1":
version "4.57.1"
resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz"
integrity sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==
"@rollup/rollup-win32-x64-msvc@4.57.1":
version "4.57.1"
resolved "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz"
integrity sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==
"@standard-schema/spec@^1.0.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz"
integrity sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==
"@sveltejs/acorn-typescript@^1.0.5":
version "1.0.8"
resolved "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz"
integrity sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==
"@sveltejs/adapter-auto@^7.0.0":
version "7.0.0"
resolved "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-7.0.0.tgz"
integrity sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw==
"@sveltejs/kit@^2.0.0", "@sveltejs/kit@^2.50.2":
version "2.50.2"
resolved "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.50.2.tgz"
integrity sha512-875hTUkEbz+MyJIxWbQjfMaekqdmEKUUfR7JyKcpfMRZqcGyrO9Gd+iS1D/Dx8LpE5FEtutWGOtlAh4ReSAiOA==
dependencies:
"@standard-schema/spec" "^1.0.0"
"@sveltejs/acorn-typescript" "^1.0.5"
"@types/cookie" "^0.6.0"
acorn "^8.14.1"
cookie "^0.6.0"
devalue "^5.6.2"
esm-env "^1.2.2"
kleur "^4.1.5"
magic-string "^0.30.5"
mrmime "^2.0.0"
sade "^1.8.1"
set-cookie-parser "^3.0.0"
sirv "^3.0.0"
"@sveltejs/vite-plugin-svelte-inspector@^5.0.0":
version "5.0.2"
resolved "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz"
integrity sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==
dependencies:
obug "^2.1.0"
"@sveltejs/vite-plugin-svelte@^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "@sveltejs/vite-plugin-svelte@^6.0.0-next.0", "@sveltejs/vite-plugin-svelte@^6.2.4":
version "6.2.4"
resolved "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz"
integrity sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==
dependencies:
"@sveltejs/vite-plugin-svelte-inspector" "^5.0.0"
deepmerge "^4.3.1"
magic-string "^0.30.21"
obug "^2.1.0"
vitefu "^1.1.1"
"@tailwindcss/forms@^0.5.11":
version "0.5.11"
resolved "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.11.tgz"
integrity sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==
dependencies:
mini-svg-data-uri "^1.2.3"
"@tailwindcss/node@4.1.18":
version "4.1.18"
resolved "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz"
integrity sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==
dependencies:
"@jridgewell/remapping" "^2.3.4"
enhanced-resolve "^5.18.3"
jiti "^2.6.1"
lightningcss "1.30.2"
magic-string "^0.30.21"
source-map-js "^1.2.1"
tailwindcss "4.1.18"
"@tailwindcss/oxide-win32-x64-msvc@4.1.18":
version "4.1.18"
resolved "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz"
integrity sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==
"@tailwindcss/oxide@4.1.18":
version "4.1.18"
resolved "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz"
integrity sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==
optionalDependencies:
"@tailwindcss/oxide-android-arm64" "4.1.18"
"@tailwindcss/oxide-darwin-arm64" "4.1.18"
"@tailwindcss/oxide-darwin-x64" "4.1.18"
"@tailwindcss/oxide-freebsd-x64" "4.1.18"
"@tailwindcss/oxide-linux-arm-gnueabihf" "4.1.18"
"@tailwindcss/oxide-linux-arm64-gnu" "4.1.18"
"@tailwindcss/oxide-linux-arm64-musl" "4.1.18"
"@tailwindcss/oxide-linux-x64-gnu" "4.1.18"
"@tailwindcss/oxide-linux-x64-musl" "4.1.18"
"@tailwindcss/oxide-wasm32-wasi" "4.1.18"
"@tailwindcss/oxide-win32-arm64-msvc" "4.1.18"
"@tailwindcss/oxide-win32-x64-msvc" "4.1.18"
"@tailwindcss/typography@^0.5.19":
version "0.5.19"
resolved "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz"
integrity sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==
dependencies:
postcss-selector-parser "6.0.10"
"@tailwindcss/vite@^4.1.18":
version "4.1.18"
resolved "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz"
integrity sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==
dependencies:
"@tailwindcss/node" "4.1.18"
"@tailwindcss/oxide" "4.1.18"
tailwindcss "4.1.18"
"@testing-library/svelte-core@^1.0.0":
version "1.0.0"
resolved "https://registry.npmjs.org/@testing-library/svelte-core/-/svelte-core-1.0.0.tgz"
integrity sha512-VkUePoLV6oOYwSUvX6ShA8KLnJqZiYMIbP2JW2t0GLWLkJxKGvuH5qrrZBV/X7cXFnLGuFQEC7RheYiZOW68KQ==
"@types/chai@^5.2.2":
version "5.2.3"
resolved "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz"
integrity sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==
dependencies:
"@types/deep-eql" "*"
assertion-error "^2.0.1"
"@types/cookie@^0.6.0":
version "0.6.0"
resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz"
integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==
"@types/deep-eql@*":
version "4.0.2"
resolved "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz"
integrity sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==
"@types/estree@^1.0.0", "@types/estree@^1.0.5", "@types/estree@^1.0.6", "@types/estree@1.0.8":
version "1.0.8"
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@vitest/browser-playwright@^4.0.18", "@vitest/browser-playwright@4.0.18":
version "4.0.18"
resolved "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.0.18.tgz"
integrity sha512-gfajTHVCiwpxRj1qh0Sh/5bbGLG4F/ZH/V9xvFVoFddpITfMta9YGow0W6ZpTTORv2vdJuz9TnrNSmjKvpOf4g==
dependencies:
"@vitest/browser" "4.0.18"
"@vitest/mocker" "4.0.18"
tinyrainbow "^3.0.3"
"@vitest/browser@4.0.18":
version "4.0.18"
resolved "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.18.tgz"
integrity sha512-gVQqh7paBz3gC+ZdcCmNSWJMk70IUjDeVqi+5m5vYpEHsIwRgw3Y545jljtajhkekIpIp5Gg8oK7bctgY0E2Ng==
dependencies:
"@vitest/mocker" "4.0.18"
"@vitest/utils" "4.0.18"
magic-string "^0.30.21"
pixelmatch "7.1.0"
pngjs "^7.0.0"
sirv "^3.0.2"
tinyrainbow "^3.0.3"
ws "^8.18.3"
"@vitest/expect@4.0.18":
version "4.0.18"
resolved "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz"
integrity sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==
dependencies:
"@standard-schema/spec" "^1.0.0"
"@types/chai" "^5.2.2"
"@vitest/spy" "4.0.18"
"@vitest/utils" "4.0.18"
chai "^6.2.1"
tinyrainbow "^3.0.3"
"@vitest/mocker@4.0.18":
version "4.0.18"
resolved "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz"
integrity sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==
dependencies:
"@vitest/spy" "4.0.18"
estree-walker "^3.0.3"
magic-string "^0.30.21"
"@vitest/pretty-format@4.0.18":
version "4.0.18"
resolved "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz"
integrity sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==
dependencies:
tinyrainbow "^3.0.3"
"@vitest/runner@4.0.18":
version "4.0.18"
resolved "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz"
integrity sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==
dependencies:
"@vitest/utils" "4.0.18"
pathe "^2.0.3"
"@vitest/snapshot@4.0.18":
version "4.0.18"
resolved "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz"
integrity sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==
dependencies:
"@vitest/pretty-format" "4.0.18"
magic-string "^0.30.21"
pathe "^2.0.3"
"@vitest/spy@4.0.18":
version "4.0.18"
resolved "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz"
integrity sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==
"@vitest/utils@4.0.18":
version "4.0.18"
resolved "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz"
integrity sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==
dependencies:
"@vitest/pretty-format" "4.0.18"
tinyrainbow "^3.0.3"
acorn@^8.12.1, acorn@^8.14.1, acorn@^8.9.0:
version "8.15.0"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
adler-32@~1.3.0:
version "1.3.1"
resolved "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz"
integrity sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==
aria-query@^5.3.1:
version "5.3.2"
resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz"
integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==
assertion-error@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz"
integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==
axobject-query@^4.1.0:
version "4.1.0"
resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz"
integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==
cfb@~1.2.1:
version "1.2.2"
resolved "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz"
integrity sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==
dependencies:
adler-32 "~1.3.0"
crc-32 "~1.2.0"
chai@^6.2.1:
version "6.2.2"
resolved "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz"
integrity sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==
chokidar@^4.0.1:
version "4.0.3"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz"
integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
dependencies:
readdirp "^4.0.1"
clsx@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
codepage@~1.15.0:
version "1.15.0"
resolved "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz"
integrity sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==
cookie@^0.6.0:
version "0.6.0"
resolved "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz"
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
crc-32@~1.2.0, crc-32@~1.2.1:
version "1.2.2"
resolved "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz"
integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
dayjs@^1.11.19:
version "1.11.19"
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz"
integrity sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==
deepmerge@^4.3.1:
version "4.3.1"
resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz"
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
detect-libc@^2.0.3:
version "2.1.2"
resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz"
integrity sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==
devalue@^5.6.2:
version "5.6.2"
resolved "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz"
integrity sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==
enhanced-resolve@^5.18.3:
version "5.19.0"
resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz"
integrity sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.3.0"
es-module-lexer@^1.7.0:
version "1.7.0"
resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz"
integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==
esbuild@^0.27.0:
version "0.27.3"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz"
integrity sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==
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"
esm-env@^1.2.1, esm-env@^1.2.2:
version "1.2.2"
resolved "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz"
integrity sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==
esrap@^2.2.2:
version "2.2.3"
resolved "https://registry.npmjs.org/esrap/-/esrap-2.2.3.tgz"
integrity sha512-8fOS+GIGCQZl/ZIlhl59htOlms6U8NvX6ZYgYHpRU/b6tVSh3uHkOHZikl3D4cMbYM0JlpBe+p/BkZEi8J9XIQ==
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
estree-walker@^3.0.3:
version "3.0.3"
resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz"
integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==
dependencies:
"@types/estree" "^1.0.0"
expect-type@^1.2.2:
version "1.3.0"
resolved "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz"
integrity sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==
fdir@^6.2.0, fdir@^6.5.0:
version "6.5.0"
resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz"
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
frac@~1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz"
integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==
graceful-fs@^4.2.4:
version "4.2.11"
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
is-reference@^3.0.3:
version "3.0.3"
resolved "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz"
integrity sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==
dependencies:
"@types/estree" "^1.0.6"
jiti@^2.6.1, jiti@>=1.21.0:
version "2.6.1"
resolved "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz"
integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==
kleur@^4.1.5:
version "4.1.5"
resolved "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz"
integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
lightningcss-win32-x64-msvc@1.30.2:
version "1.30.2"
resolved "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz"
integrity sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==
lightningcss@^1.21.0, lightningcss@1.30.2:
version "1.30.2"
resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz"
integrity sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==
dependencies:
detect-libc "^2.0.3"
optionalDependencies:
lightningcss-android-arm64 "1.30.2"
lightningcss-darwin-arm64 "1.30.2"
lightningcss-darwin-x64 "1.30.2"
lightningcss-freebsd-x64 "1.30.2"
lightningcss-linux-arm-gnueabihf "1.30.2"
lightningcss-linux-arm64-gnu "1.30.2"
lightningcss-linux-arm64-musl "1.30.2"
lightningcss-linux-x64-gnu "1.30.2"
lightningcss-linux-x64-musl "1.30.2"
lightningcss-win32-arm64-msvc "1.30.2"
lightningcss-win32-x64-msvc "1.30.2"
locate-character@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz"
integrity sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==
magic-string@^0.30.11, magic-string@^0.30.21, magic-string@^0.30.5:
version "0.30.21"
resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz"
integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==
dependencies:
"@jridgewell/sourcemap-codec" "^1.5.5"
mini-svg-data-uri@^1.2.3:
version "1.4.4"
resolved "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz"
integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==
mri@^1.1.0:
version "1.2.0"
resolved "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz"
integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
mrmime@^2.0.0:
version "2.0.1"
resolved "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz"
integrity sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==
nanoid@^3.3.11:
version "3.3.11"
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
obug@^2.1.0, obug@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz"
integrity sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==
pathe@^2.0.3:
version "2.0.3"
resolved "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz"
integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
picocolors@^1.0.0, picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
"picomatch@^3 || ^4", picomatch@^4.0.3:
version "4.0.3"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
pixelmatch@7.1.0:
version "7.1.0"
resolved "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.1.0.tgz"
integrity sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==
dependencies:
pngjs "^7.0.0"
playwright-core@1.58.2:
version "1.58.2"
resolved "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz"
integrity sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==
playwright@*, playwright@^1.58.1:
version "1.58.2"
resolved "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz"
integrity sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==
dependencies:
playwright-core "1.58.2"
optionalDependencies:
fsevents "2.3.2"
pngjs@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz"
integrity sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==
postcss-selector-parser@6.0.10:
version "6.0.10"
resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz"
integrity sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==
dependencies:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
postcss@^8.5.6:
version "8.5.6"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz"
integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==
dependencies:
nanoid "^3.3.11"
picocolors "^1.1.1"
source-map-js "^1.2.1"
readdirp@^4.0.1:
version "4.1.2"
resolved "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz"
integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
rollup@^4.43.0:
version "4.57.1"
resolved "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz"
integrity sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==
dependencies:
"@types/estree" "1.0.8"
optionalDependencies:
"@rollup/rollup-android-arm-eabi" "4.57.1"
"@rollup/rollup-android-arm64" "4.57.1"
"@rollup/rollup-darwin-arm64" "4.57.1"
"@rollup/rollup-darwin-x64" "4.57.1"
"@rollup/rollup-freebsd-arm64" "4.57.1"
"@rollup/rollup-freebsd-x64" "4.57.1"
"@rollup/rollup-linux-arm-gnueabihf" "4.57.1"
"@rollup/rollup-linux-arm-musleabihf" "4.57.1"
"@rollup/rollup-linux-arm64-gnu" "4.57.1"
"@rollup/rollup-linux-arm64-musl" "4.57.1"
"@rollup/rollup-linux-loong64-gnu" "4.57.1"
"@rollup/rollup-linux-loong64-musl" "4.57.1"
"@rollup/rollup-linux-ppc64-gnu" "4.57.1"
"@rollup/rollup-linux-ppc64-musl" "4.57.1"
"@rollup/rollup-linux-riscv64-gnu" "4.57.1"
"@rollup/rollup-linux-riscv64-musl" "4.57.1"
"@rollup/rollup-linux-s390x-gnu" "4.57.1"
"@rollup/rollup-linux-x64-gnu" "4.57.1"
"@rollup/rollup-linux-x64-musl" "4.57.1"
"@rollup/rollup-openbsd-x64" "4.57.1"
"@rollup/rollup-openharmony-arm64" "4.57.1"
"@rollup/rollup-win32-arm64-msvc" "4.57.1"
"@rollup/rollup-win32-ia32-msvc" "4.57.1"
"@rollup/rollup-win32-x64-gnu" "4.57.1"
"@rollup/rollup-win32-x64-msvc" "4.57.1"
fsevents "~2.3.2"
sade@^1.7.4, sade@^1.8.1:
version "1.8.1"
resolved "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz"
integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==
dependencies:
mri "^1.1.0"
set-cookie-parser@^3.0.0:
version "3.0.1"
resolved "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.0.1.tgz"
integrity sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q==
siginfo@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz"
integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==
sirv@^3.0.0, sirv@^3.0.2:
version "3.0.2"
resolved "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz"
integrity sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==
dependencies:
"@polka/url" "^1.0.0-next.24"
mrmime "^2.0.0"
totalist "^3.0.0"
source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
ssf@~0.11.2:
version "0.11.2"
resolved "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz"
integrity sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==
dependencies:
frac "~1.1.2"
stackback@0.0.2:
version "0.0.2"
resolved "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz"
integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==
std-env@^3.10.0:
version "3.10.0"
resolved "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz"
integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==
svelte-check@^4.3.6:
version "4.3.6"
resolved "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.6.tgz"
integrity sha512-uBkz96ElE3G4pt9E1Tw0xvBfIUQkeH794kDQZdAUk795UVMr+NJZpuFSS62vcmO/DuSalK83LyOwhgWq8YGU1Q==
dependencies:
"@jridgewell/trace-mapping" "^0.3.25"
chokidar "^4.0.1"
fdir "^6.2.0"
picocolors "^1.0.0"
sade "^1.7.4"
"svelte@^3 || ^4 || ^5 || ^5.0.0-next.0", "svelte@^4.0.0 || ^5.0.0-next.0", svelte@^5.0.0, svelte@^5.49.2:
version "5.50.0"
resolved "https://registry.npmjs.org/svelte/-/svelte-5.50.0.tgz"
integrity sha512-FR9kTLmX5i0oyeQ5j/+w8DuagIkQ7MWMuPpPVioW2zx9Dw77q+1ufLzF1IqNtcTXPRnIIio4PlasliVn43OnbQ==
dependencies:
"@jridgewell/remapping" "^2.3.4"
"@jridgewell/sourcemap-codec" "^1.5.0"
"@sveltejs/acorn-typescript" "^1.0.5"
"@types/estree" "^1.0.5"
acorn "^8.12.1"
aria-query "^5.3.1"
axobject-query "^4.1.0"
clsx "^2.1.1"
devalue "^5.6.2"
esm-env "^1.2.1"
esrap "^2.2.2"
is-reference "^3.0.3"
locate-character "^3.0.0"
magic-string "^0.30.11"
zimmerframe "^1.1.2"
tailwindcss@^4.1.18, "tailwindcss@>=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1", "tailwindcss@>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1", tailwindcss@4.1.18:
version "4.1.18"
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz"
integrity sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==
tapable@^2.3.0:
version "2.3.0"
resolved "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz"
integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==
tinybench@^2.9.0:
version "2.9.0"
resolved "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz"
integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==
tinyexec@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz"
integrity sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==
tinyglobby@^0.2.15:
version "0.2.15"
resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz"
integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==
dependencies:
fdir "^6.5.0"
picomatch "^4.0.3"
tinyrainbow@^3.0.3:
version "3.0.3"
resolved "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz"
integrity sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==
totalist@^3.0.0:
version "3.0.1"
resolved "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz"
integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==
typescript@^5.3.3, typescript@^5.9.3, typescript@>=5.0.0:
version "5.9.3"
resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz"
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
util-deprecate@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
uuid@^11.1.0:
version "11.1.0"
resolved "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz"
integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==
vite-plugin-devtools-json@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/vite-plugin-devtools-json/-/vite-plugin-devtools-json-1.0.0.tgz"
integrity sha512-MobvwqX76Vqt/O4AbnNMNWoXWGrKUqZbphCUle/J2KXH82yKQiunOeKnz/nqEPosPsoWWPP9FtNuPBSYpiiwkw==
dependencies:
uuid "^11.1.0"
"vite@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0", "vite@^5.0.0 || ^6.0.0 || ^7.0.0", "vite@^5.0.3 || ^6.0.0 || ^7.0.0-beta.0", "vite@^5.2.0 || ^6 || ^7", "vite@^6.0.0 || ^7.0.0", "vite@^6.0.0 || ^7.0.0-0", "vite@^6.3.0 || ^7.0.0", vite@^7.3.1:
version "7.3.1"
resolved "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz"
integrity sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==
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"
optionalDependencies:
fsevents "~2.3.3"
vitefu@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz"
integrity sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==
vitest-browser-svelte@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/vitest-browser-svelte/-/vitest-browser-svelte-2.0.2.tgz"
integrity sha512-OLJVYoIYflwToFIy3s41pZ9mVp6dwXfYd8IIsWoc57g8DyN3SxsNJ5GB1xWFPxLFlKM+1MPExjPxLaqdELrfRQ==
dependencies:
"@testing-library/svelte-core" "^1.0.0"
vitest@^4.0.0, vitest@^4.0.18, vitest@4.0.18:
version "4.0.18"
resolved "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz"
integrity sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==
dependencies:
"@vitest/expect" "4.0.18"
"@vitest/mocker" "4.0.18"
"@vitest/pretty-format" "4.0.18"
"@vitest/runner" "4.0.18"
"@vitest/snapshot" "4.0.18"
"@vitest/spy" "4.0.18"
"@vitest/utils" "4.0.18"
es-module-lexer "^1.7.0"
expect-type "^1.2.2"
magic-string "^0.30.21"
obug "^2.1.1"
pathe "^2.0.3"
picomatch "^4.0.3"
std-env "^3.10.0"
tinybench "^2.9.0"
tinyexec "^1.0.2"
tinyglobby "^0.2.15"
tinyrainbow "^3.0.3"
vite "^6.0.0 || ^7.0.0"
why-is-node-running "^2.3.0"
why-is-node-running@^2.3.0:
version "2.3.0"
resolved "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz"
integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==
dependencies:
siginfo "^2.0.0"
stackback "0.0.2"
wmf@~1.0.1:
version "1.0.2"
resolved "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz"
integrity sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==
word@~0.3.0:
version "0.3.0"
resolved "https://registry.npmjs.org/word/-/word-0.3.0.tgz"
integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==
ws@^8.18.3:
version "8.19.0"
resolved "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz"
integrity sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==
xlsx@^0.18.5:
version "0.18.5"
resolved "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz"
integrity sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==
dependencies:
adler-32 "~1.3.0"
cfb "~1.2.1"
codepage "~1.15.0"
crc-32 "~1.2.1"
ssf "~0.11.2"
wmf "~1.0.1"
word "~0.3.0"
zimmerframe@^1.1.2:
version "1.1.4"
resolved "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz"
integrity sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==