Compare commits

..
10 Commits
Author SHA1 Message Date
DenozordecandCursor 87309cfcec web(ui): feedback — confirm dialog, skeletons, README
CI / changes (push) Successful in 7s
CI / openapi (push) Has been skipped
CI / go (push) Has been skipped
CI / docker-web (push) Failing after 1m21s
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / docker-go (push) Has been skipped
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 12:01:15 +07:00
DenozordecandCursor 1f9930d467 web(ui): migrate operations and module detail to ui patterns
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 12:01:15 +07:00
DenozordecandCursor 9808aca44f web(ui): migrate settings to ui patterns
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 12:01:15 +07:00
DenozordecandCursor 3a3056ba76 web(ui): migrate network to ui patterns
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 12:01:15 +07:00
DenozordecandCursor 57dcf306f3 web(ui): migrate dashboard, schedule, monitoring to ui patterns
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 12:01:11 +07:00
DenozordecandCursor b992301c99 web(ui): AppDataTable pattern and modules pilot
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 11:59:34 +07:00
DenozordecandCursor 77a2cfd426 web(ui): form patterns — FormField, formsnap, directories pilot
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 11:59:25 +07:00
DenozordecandCursor ac635e91eb web(ui): design tokens — spacing and component scale
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 11:56:38 +07:00
DenozordecandCursor 2d78c8156f web(ui): app layout — sidebar, mobile sheet, topbar, PageHeader
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 11:56:24 +07:00
DenozordecandCursor 00a2a44630 web(ui): foundation — ui/core layout, shadcn primitives, notify
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 11:54:29 +07:00
200 changed files with 2541 additions and 913 deletions

No files matched your search

+15 -31
View File
@@ -1,44 +1,28 @@
# sv
# EvoBGP WebUI
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
SvelteKit-приложение панели управления EvoBGP. Запуск — [docs/quickstart.md](../docs/quickstart.md).
Это веб-интерфейс **EvoBGP**. Запуск вместе с API, CORS и портами — в [../docs/quickstart.md](../docs/quickstart.md); доступ и ключи — [../docs/access.md](../docs/access.md).
## UI-архитектура
## Creating a project
| Путь | Назначение |
|------|------------|
| `src/lib/ui/core/` | shadcn-svelte (только через `npx shadcn-svelte@latest add … -y -o`) |
| `src/lib/ui/app/` | Layout, PageHeader, toast (`notify`) |
| `src/lib/ui/patterns/` | FormField, AppDataTable, ConfirmDialog, EmptyState |
| `src/lib/components/ui/` | Re-export на `ui/core` (совместимость) |
If you're seeing this, you've probably already done this step. Congrats!
Токены темы: `src/routes/layout.css`. Справочник: `src/lib/ui/app/tokens.md`.
```sh
# create a new project
npx sv create my-app
```
To recreate this project with the same configuration:
```sh
# recreate this project
npx sv@0.14.0 create --template minimal --types ts --add tailwindcss="plugins:none" sveltekit-adapter="adapter:static" prettier --no-download-check --install npm web
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
## Разработка
```sh
npm install
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
npm run check
```
## Building
To create a production version of your app:
Добавление компонентов shadcn (из каталога `web/`):
```sh
npm run build
npx shadcn-svelte@latest add <component> -y -o
```
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.
+1 -1
View File
@@ -7,7 +7,7 @@
"aliases": {
"components": "$lib/components",
"utils": "$lib/utils",
"ui": "$lib/components/ui",
"ui": "$lib/ui/core",
"hooks": "$lib/hooks",
"lib": "$lib"
},
+641 -4
View File
@@ -18,22 +18,54 @@
},
"devDependencies": {
"@internationalized/date": "^3.12.0",
"@lucide/svelte": "^1.7.0",
"@lucide/svelte": "^1.16.0",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.50.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/vite": "^4.1.18",
"@types/node": "^22.15.0",
"formsnap": "^2.0.1",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.4.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"svelte": "^5.54.0",
"svelte-check": "^4.4.2",
"sveltekit-superforms": "^2.30.1",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vite": "^7.3.1"
}
},
"node_modules/@ark/schema": {
"version": "0.56.0",
"resolved": "https://registry.npmjs.org/@ark/schema/-/schema-0.56.0.tgz",
"integrity": "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@ark/util": "0.56.0"
}
},
"node_modules/@ark/util": {
"version": "0.56.0",
"resolved": "https://registry.npmjs.org/@ark/util/-/util-0.56.0.tgz",
"integrity": "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/@babel/runtime": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
@@ -450,6 +482,14 @@
"node": ">=18"
}
},
"node_modules/@exodus/schemasafe": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz",
"integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/@floating-ui/core": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
@@ -475,6 +515,25 @@
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
"license": "MIT"
},
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
"integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true
},
"node_modules/@hapi/topo": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
"integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@internationalized/date": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.0.tgz",
@@ -530,9 +589,9 @@
}
},
"node_modules/@lucide/svelte": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.7.0.tgz",
"integrity": "sha512-YytBKOUBGox7yWcykZnYxOkn5WpR5G1qYXLYXV/j1B79SOTTEKzB+s5yF5Rq9l9OkweDStNH2b4yTqfvhEhV8g==",
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.16.0.tgz",
"integrity": "sha512-AvvPJnaWxeiNkAljI5MsSEc84yHPLMaWQIAJOcbX7k9au/f9ITS7cxTTQiautDiOFKVOXiYdZ+d6mtl88J+Kbg==",
"dev": true,
"license": "ISC",
"peerDependencies": {
@@ -546,6 +605,14 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/@poppinss/macroable": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@poppinss/macroable/-/macroable-1.1.2.tgz",
"integrity": "sha512-FAVBRzzWhYP5mA3lCwLH1A0fKBqq5anyjGet90Z81aRK5c/+LTGUE1zJhZrErjaenBSOOI9BVUs3WVmotneFQA==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
@@ -871,6 +938,33 @@
"win32"
]
},
"node_modules/@sideway/address": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
"integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"@hapi/hoek": "^9.0.0"
}
},
"node_modules/@sideway/formula": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true
},
"node_modules/@sideway/pinpoint": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
@@ -1301,6 +1395,49 @@
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT"
},
"node_modules/@types/validator": {
"version": "13.15.10",
"resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
"integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/@typeschema/class-validator": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@typeschema/class-validator/-/class-validator-0.3.0.tgz",
"integrity": "sha512-OJSFeZDIQ8EK1HTljKLT5CItM2wsbgczLN8tMEfz3I1Lmhc5TBfkZ0eikFzUC16tI3d1Nag7um6TfCgp2I2Bww==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@typeschema/core": "0.14.0"
},
"peerDependencies": {
"class-validator": "^0.14.1"
},
"peerDependenciesMeta": {
"class-validator": {
"optional": true
}
}
},
"node_modules/@typeschema/core": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@typeschema/core/-/core-0.14.0.tgz",
"integrity": "sha512-Ia6PtZHcL3KqsAWXjMi5xIyZ7XMH4aSnOQes8mfMLx+wGFGtGRNlwe6Y7cYvX+WfNK67OL0/HSe9t8QDygV0/w==",
"dev": true,
"license": "MIT",
"optional": true,
"peerDependencies": {
"@types/json-schema": "^7.0.15"
},
"peerDependenciesMeta": {
"@types/json-schema": {
"optional": true
}
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.58.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz",
@@ -1314,6 +1451,49 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@valibot/to-json-schema": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@valibot/to-json-schema/-/to-json-schema-1.7.0.tgz",
"integrity": "sha512-Y3pPVibbIOHzohrlxSINvO7w/bvXkoYS3BQHoImV9ynE+bXKf171bdMucPurV2zp7gdmt0L1HCcNAsbo7cFRQw==",
"dev": true,
"license": "MIT",
"optional": true,
"peerDependencies": {
"valibot": "^1.4.0"
}
},
"node_modules/@vinejs/compiler": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@vinejs/compiler/-/compiler-3.0.0.tgz",
"integrity": "sha512-v9Lsv59nR56+bmy2p0+czjZxsLHwaibJ+SV5iK9JJfehlJMa501jUJQqqz4X/OqKXrxtE3uTQmSqjUqzF3B2mw==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@vinejs/vine": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@vinejs/vine/-/vine-3.0.1.tgz",
"integrity": "sha512-ZtvYkYpZOYdvbws3uaOAvTFuvFXoQGAtmzeiXu+XSMGxi5GVsODpoI9Xu9TplEMuD/5fmAtBbKb9cQHkWkLXDQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@poppinss/macroable": "^1.0.4",
"@types/validator": "^13.12.2",
"@vinejs/compiler": "^3.0.0",
"camelcase": "^8.0.0",
"dayjs": "^1.11.13",
"dlv": "^1.1.3",
"normalize-url": "^8.0.1",
"validator": "^13.12.0"
},
"engines": {
"node": ">=18.16.0"
}
},
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
@@ -1335,6 +1515,30 @@
"node": ">= 0.4"
}
},
"node_modules/arkregex": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/arkregex/-/arkregex-0.0.5.tgz",
"integrity": "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@ark/util": "0.56.0"
}
},
"node_modules/arktype": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/arktype/-/arktype-2.2.0.tgz",
"integrity": "sha512-t54MZ7ti5BhOEvzEkgKnWvqj+UbDfWig+DHr5I34xatymPusKLS0lQpNJd8M6DzmIto2QGszHfNKoFIT8tMCZQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@ark/schema": "0.56.0",
"@ark/util": "0.56.0",
"arkregex": "0.0.5"
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -1368,6 +1572,20 @@
"svelte": "^5.33.0"
}
},
"node_modules/camelcase": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz",
"integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -1384,6 +1602,19 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/class-validator": {
"version": "0.14.4",
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.4.tgz",
"integrity": "sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@types/validator": "^13.15.3",
"libphonenumber-js": "^1.11.1",
"validator": "^13.15.22"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -1403,6 +1634,14 @@
"node": ">= 0.6"
}
},
"node_modules/dayjs": {
"version": "1.11.20",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
"integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
@@ -1438,6 +1677,26 @@
"integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==",
"license": "MIT"
},
"node_modules/dlv": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/effect": {
"version": "3.21.2",
"resolved": "https://registry.npmjs.org/effect/-/effect-3.21.2.tgz",
"integrity": "sha512-rXd2FGDM8KdjSIrc+mqEELo7ScW7xTVxEf1iInmPSpIde9/nyGuFM710cjTo7/EreGXiUX2MOonPpprbz2XHCg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"fast-check": "^3.23.1"
}
},
"node_modules/enhanced-resolve": {
"version": "5.20.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz",
@@ -1510,6 +1769,30 @@
"@typescript-eslint/types": "^8.2.0"
}
},
"node_modules/fast-check": {
"version": "3.23.2",
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
"integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT",
"optional": true,
"dependencies": {
"pure-rand": "^6.1.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -1528,6 +1811,47 @@
}
}
},
"node_modules/formsnap": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/formsnap/-/formsnap-2.0.1.tgz",
"integrity": "sha512-iJSe4YKd/W6WhLwKDVJU9FQeaJRpEFuolhju7ZXlRpUVyDdqFdMP8AUBICgnVvQPyP41IPAlBa/v0Eo35iE6wQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"svelte-toolbelt": "^0.5.0"
},
"engines": {
"node": ">=18",
"pnpm": ">=8.7.0"
},
"funding": {
"url": "https://github.com/sponsors/huntabyte"
},
"peerDependencies": {
"svelte": "^5.0.0",
"sveltekit-superforms": "^2.19.0"
}
},
"node_modules/formsnap/node_modules/svelte-toolbelt": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.5.0.tgz",
"integrity": "sha512-t3tenZcnfQoIeRuQf/jBU7bvTeT3TGkcEE+1EUr5orp0lR7NEpprflpuie3x9Dn0W9nOKqs3HwKGJeeN5Ok1sQ==",
"dev": true,
"funding": [
"https://github.com/sponsors/huntabyte"
],
"dependencies": {
"clsx": "^2.1.1",
"style-to-object": "^1.0.8"
},
"engines": {
"node": ">=18",
"pnpm": ">=8.7.0"
},
"peerDependencies": {
"svelte": "^5.0.0-next.126"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1574,6 +1898,36 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/joi": {
"version": "17.13.3",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
"integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"dependencies": {
"@hapi/hoek": "^9.3.0",
"@hapi/topo": "^5.1.0",
"@sideway/address": "^4.1.5",
"@sideway/formula": "^3.0.1",
"@sideway/pinpoint": "^2.0.0"
}
},
"node_modules/json-schema-to-ts": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"ts-algebra": "^2.0.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/kleur": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@@ -1584,6 +1938,14 @@
"node": ">=6"
}
},
"node_modules/libphonenumber-js": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.2.tgz",
"integrity": "sha512-S3kmBrptp3yRTm83NUcHy9g1vbwiWMzI8WvY22+koBJ6zkRteLnedBL2VX0MIAGwx2yiyxX4J85pceZyQ6ffgg==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
@@ -1858,6 +2220,13 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/memoize-weak": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/memoize-weak/-/memoize-weak-1.0.2.tgz",
"integrity": "sha512-gj39xkrjEw7nCn4nJ1M5ms6+MyMlyiGmttzsqAUsAKn6bYKwuTHh/AO3cKPF8IBrTIYTxb0wWXFs3E//Y8VoWQ==",
"dev": true,
"license": "ISC"
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@@ -1897,6 +2266,20 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/normalize-url": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz",
"integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/obug": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
@@ -2063,6 +2446,32 @@
}
}
},
"node_modules/property-expr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/pure-rand": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT",
"optional": true
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
@@ -2200,6 +2609,17 @@
"inline-style-parser": "0.2.7"
}
},
"node_modules/superstruct": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz",
"integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/svelte": {
"version": "5.55.1",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.1.tgz",
@@ -2299,6 +2719,104 @@
"svelte": "^5.30.2"
}
},
"node_modules/sveltekit-superforms": {
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/sveltekit-superforms/-/sveltekit-superforms-2.30.1.tgz",
"integrity": "sha512-wBzyqsE0idvEJWuNJ+HCiAtdxa7Z55GZ8jmtlVHJfonrk9bRYC49MoPaloYyFoYuU3QPy6Omna/Qzn1kaIkgew==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ciscoheat"
},
{
"type": "ko-fi",
"url": "https://ko-fi.com/ciscoheat"
},
{
"type": "paypal",
"url": "https://www.paypal.com/donate/?hosted_button_id=NY7F5ALHHSVQS"
}
],
"license": "MIT",
"dependencies": {
"devalue": "^5.6.4",
"memoize-weak": "^1.0.2",
"ts-deepmerge": "^7.0.3"
},
"optionalDependencies": {
"@exodus/schemasafe": "^1.3.0",
"@standard-schema/spec": "^1.1.0",
"@typeschema/class-validator": "^0.3.0",
"@valibot/to-json-schema": "^1.6.0",
"@vinejs/vine": "^3.0.1",
"arktype": "^2.2.0",
"class-validator": "^0.14.4",
"effect": "^3.21.0",
"joi": "^17.13.3",
"json-schema-to-ts": "^3.1.1",
"superstruct": "^2.0.2",
"typebox": "^1.1.6",
"valibot": "^1.3.1",
"yup": "^1.7.1",
"zod": "^4.3.6",
"zod-v3-to-json-schema": "^4.0.0"
},
"peerDependencies": {
"@exodus/schemasafe": "^1.3.0",
"@sveltejs/kit": "1.x || 2.x",
"@typeschema/class-validator": "^0.3.0",
"@vinejs/vine": "^1.8.0 || ^2.0.0 || ^3.0.0",
"arktype": ">=2.0.0-rc.23",
"class-validator": "^0.14.1",
"effect": "^3.21.0",
"joi": "^17.13.1",
"superstruct": "^2.0.2",
"svelte": "3.x || 4.x || >=5.0.0-next.51",
"typebox": "^1.0.36",
"valibot": "^1.2.0",
"yup": "^1.4.0",
"zod": "^3.25.0 || ^4.0.0"
},
"peerDependenciesMeta": {
"@exodus/schemasafe": {
"optional": true
},
"@typeschema/class-validator": {
"optional": true
},
"@vinejs/vine": {
"optional": true
},
"arktype": {
"optional": true
},
"class-validator": {
"optional": true
},
"effect": {
"optional": true
},
"joi": {
"optional": true
},
"superstruct": {
"optional": true
},
"typebox": {
"optional": true
},
"valibot": {
"optional": true
},
"yup": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/tabbable": {
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz",
@@ -2354,6 +2872,14 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/tiny-case": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -2371,6 +2897,14 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/toposort": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/totalist": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
@@ -2381,6 +2915,24 @@
"node": ">=6"
}
},
"node_modules/ts-algebra": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/ts-deepmerge": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-7.0.3.tgz",
"integrity": "sha512-Du/ZW2RfwV/D4cmA5rXafYjBQVuvu4qGiEEla4EmEHVHgRdx68Gftx7i66jn2bzHPwSVZY36Ae6OuDn9el4ZKA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14.13.1"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -2396,6 +2948,28 @@
"url": "https://github.com/sponsors/Wombosvideo"
}
},
"node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"dev": true,
"license": "(MIT OR CC0-1.0)",
"optional": true,
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typebox": {
"version": "1.1.38",
"resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz",
"integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -2417,6 +2991,33 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/valibot": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.0.tgz",
"integrity": "sha512-iC/x7fVcSyOwlm/VSt7RlHnzNGLGvR9GnxdifUeWoCJo0q4ZZvrVkIHC6faTlkxG47I2Y4UrFquPuVHCrOnrLg==",
"dev": true,
"license": "MIT",
"optional": true,
"peerDependencies": {
"typescript": ">=5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/validator": {
"version": "13.15.35",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz",
"integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">= 0.10"
}
},
"node_modules/vite": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
@@ -2512,11 +3113,47 @@
}
}
},
"node_modules/yup": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz",
"integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"property-expr": "^2.0.5",
"tiny-case": "^1.0.3",
"toposort": "^2.0.2",
"type-fest": "^2.19.0"
}
},
"node_modules/zimmerframe": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
"license": "MIT"
},
"node_modules/zod": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"dev": true,
"license": "MIT",
"optional": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-v3-to-json-schema": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/zod-v3-to-json-schema/-/zod-v3-to-json-schema-4.0.0.tgz",
"integrity": "sha512-KixLrhX/uPmRFnDgsZrzrk4x5SSJA+PmaE5adbfID9+3KPJcdxqRobaHU397EfWBqfQircrjKqvEqZ/mW5QH6w==",
"dev": true,
"license": "ISC",
"optional": true,
"peerDependencies": {
"zod": "^3.25 || ^4.0.14"
}
}
}
}
+3 -1
View File
@@ -15,17 +15,19 @@
},
"devDependencies": {
"@internationalized/date": "^3.12.0",
"@lucide/svelte": "^1.7.0",
"@lucide/svelte": "^1.16.0",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.50.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@tailwindcss/vite": "^4.1.18",
"@types/node": "^22.15.0",
"formsnap": "^2.0.1",
"prettier": "^3.8.1",
"prettier-plugin-svelte": "^3.4.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"svelte": "^5.54.0",
"svelte-check": "^4.4.2",
"sveltekit-superforms": "^2.30.1",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3",
"vite": "^7.3.1"
-208
View File
@@ -1,208 +0,0 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { cn } from '$lib/utils.js';
import { Button } from '$lib/components/ui/button/index.js';
import { buttonVariants } from '$lib/components/ui/button/button.svelte';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger
} from '$lib/components/ui/dropdown-menu/index.js';
import { Separator } from '$lib/components/ui/separator/index.js';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '$lib/components/ui/tooltip/index.js';
import type { ThemePreference } from '$lib/theme.js';
import Activity from '@lucide/svelte/icons/activity';
import Boxes from '@lucide/svelte/icons/boxes';
import BookOpen from '@lucide/svelte/icons/book-open';
import LayoutDashboard from '@lucide/svelte/icons/layout-dashboard';
import Network from '@lucide/svelte/icons/network';
import Settings from '@lucide/svelte/icons/settings';
import Gauge from '@lucide/svelte/icons/gauge';
import Zap from '@lucide/svelte/icons/zap';
import CalendarClock from '@lucide/svelte/icons/calendar-clock';
import PanelLeftClose from '@lucide/svelte/icons/panel-left-close';
import PanelLeftOpen from '@lucide/svelte/icons/panel-left-open';
import Sun from '@lucide/svelte/icons/sun';
import Moon from '@lucide/svelte/icons/moon';
import Monitor from '@lucide/svelte/icons/monitor';
const nav = [
{ href: '/', label: 'Обзор', icon: LayoutDashboard },
{ href: '/modules', label: 'Модули', icon: Boxes },
{ href: '/directories', label: 'Справочники', icon: BookOpen },
{ href: '/network', label: 'Сеть', icon: Network },
{ href: '/revisions', label: 'Ревизии', icon: Activity },
{ href: '/operations', label: 'Операции', icon: Zap },
{ href: '/schedule', label: 'Расписание', icon: CalendarClock },
{ href: '/monitoring', label: 'Мониторинг', icon: Gauge },
] as const;
const navBottom = [
{ href: '/settings', label: 'Настройки', icon: Settings },
] as const;
let collapsed = $state(false);
let { children, theme = $bindable<ThemePreference>('system') } = $props();
function isActive(href: string) {
const pathname = page.url.pathname;
if (href === '/') return pathname === '/';
return pathname === href || pathname.startsWith(href + '/');
}
</script>
<TooltipProvider delayDuration={0}>
<div class="bg-background flex min-h-screen">
<aside
class={cn(
'border-border bg-sidebar sticky top-0 flex h-screen shrink-0 flex-col overflow-hidden border-r transition-all duration-200',
collapsed ? 'w-14' : 'w-56'
)}
aria-label="Основная навигация"
>
<!-- Header -->
<div class={cn('flex items-center gap-2 p-3', collapsed && 'justify-center')}>
{#if !collapsed}
<div class="min-w-0 flex-1">
<a href={resolve('/')} class="text-sidebar-foreground block truncate font-semibold tracking-tight">EvoBGP</a>
<p class="text-sidebar-foreground/50 truncate text-xs">Панель управления</p>
</div>
{/if}
<DropdownMenu>
<DropdownMenuTrigger
class={cn(
buttonVariants({ variant: 'ghost', size: 'icon-sm' }),
'text-sidebar-foreground/60 hover:text-sidebar-foreground shrink-0'
)}
aria-label="Тема оформления"
>
<span class="flex size-4 items-center justify-center">
<Sun class="size-4 dark:hidden" />
<Moon class="hidden size-4 dark:block" />
</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-44">
<DropdownMenuLabel>Тема</DropdownMenuLabel>
<DropdownMenuRadioGroup bind:value={theme}>
<DropdownMenuRadioItem value="light" class="gap-2">
<Sun class="size-4" />
Светлая
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark" class="gap-2">
<Moon class="size-4" />
Тёмная
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system" class="gap-2">
<Monitor class="size-4" />
Как в системе
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<button
onclick={() => (collapsed = !collapsed)}
class={cn(
buttonVariants({ variant: 'ghost', size: 'icon-sm' }),
'text-sidebar-foreground/60 hover:text-sidebar-foreground shrink-0'
)}
aria-label={collapsed ? 'Развернуть меню' : 'Свернуть меню'}
>
{#if collapsed}
<PanelLeftOpen class="size-4" />
{:else}
<PanelLeftClose class="size-4" />
{/if}
</button>
</div>
<Separator />
<!-- Main nav -->
<nav class="flex flex-1 flex-col gap-0.5 overflow-y-auto p-2">
{#each nav as item (item.href)}
{@const Icon = item.icon}
{@const active = isActive(item.href)}
{#if collapsed}
<Tooltip>
<TooltipTrigger>
<a
href={resolve(item.href)}
class={cn(
buttonVariants({ variant: active ? 'secondary' : 'ghost', size: 'icon-sm' }),
'w-full no-underline flex items-center justify-center',
active && 'ring-sidebar-primary/50 bg-sidebar-accent/90 text-sidebar-accent-foreground ring-1'
)}
aria-current={active ? 'page' : undefined}
>
<Icon class="size-4" />
</a>
</TooltipTrigger>
<TooltipContent side="right">{item.label}</TooltipContent>
</Tooltip>
{:else}
<a
href={resolve(item.href)}
class={cn(
buttonVariants({ variant: active ? 'secondary' : 'ghost', size: 'sm' }),
'w-full justify-start gap-2 no-underline',
active &&
'border-l-sidebar-primary bg-sidebar-accent/80 text-sidebar-accent-foreground border-l-2 shadow-sm'
)}
aria-current={active ? 'page' : undefined}
>
<Icon class="size-4" />
{item.label}
</a>
{/if}
{/each}
</nav>
<!-- Bottom nav -->
<div class="flex flex-col gap-0.5 p-2">
<Separator class="mb-2" />
{#each navBottom as item (item.href)}
{@const Icon = item.icon}
{@const active = isActive(item.href)}
{#if collapsed}
<Tooltip>
<TooltipTrigger>
<a
href={resolve(item.href)}
class={cn(
buttonVariants({ variant: active ? 'secondary' : 'ghost', size: 'icon-sm' }),
'w-full no-underline flex items-center justify-center',
active && 'ring-sidebar-primary/50 bg-sidebar-accent/90 text-sidebar-accent-foreground ring-1'
)}
>
<Icon class="size-4" />
</a>
</TooltipTrigger>
<TooltipContent side="right">{item.label}</TooltipContent>
</Tooltip>
{:else}
<a
href={resolve(item.href)}
class={cn(
buttonVariants({ variant: active ? 'secondary' : 'ghost', size: 'sm' }),
'w-full justify-start gap-2 no-underline',
active &&
'border-l-sidebar-primary bg-sidebar-accent/80 text-sidebar-accent-foreground border-l-2 shadow-sm'
)}
>
<Icon class="size-4" />
{item.label}
</a>
{/if}
{/each}
</div>
</aside>
<main class="min-w-0 flex-1 p-6">
{@render children?.()}
</main>
</div>
</TooltipProvider>
@@ -1,40 +1 @@
import Root from "./alert-dialog.svelte";
import Portal from "./alert-dialog-portal.svelte";
import Trigger from "./alert-dialog-trigger.svelte";
import Title from "./alert-dialog-title.svelte";
import Action from "./alert-dialog-action.svelte";
import Cancel from "./alert-dialog-cancel.svelte";
import Footer from "./alert-dialog-footer.svelte";
import Header from "./alert-dialog-header.svelte";
import Overlay from "./alert-dialog-overlay.svelte";
import Content from "./alert-dialog-content.svelte";
import Description from "./alert-dialog-description.svelte";
import Media from "./alert-dialog-media.svelte";
export {
Root,
Title,
Action,
Cancel,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
Media,
//
Root as AlertDialog,
Title as AlertDialogTitle,
Action as AlertDialogAction,
Cancel as AlertDialogCancel,
Portal as AlertDialogPortal,
Footer as AlertDialogFooter,
Header as AlertDialogHeader,
Trigger as AlertDialogTrigger,
Overlay as AlertDialogOverlay,
Content as AlertDialogContent,
Description as AlertDialogDescription,
Media as AlertDialogMedia,
};
export * from '$lib/ui/core/alert-dialog/index.js';
+1 -17
View File
@@ -1,17 +1 @@
import Root from "./alert.svelte";
import Description from "./alert-description.svelte";
import Title from "./alert-title.svelte";
import Action from "./alert-action.svelte";
export { alertVariants, type AlertVariant } from "./alert.svelte";
export {
Root,
Description,
Title,
Action,
//
Root as Alert,
Description as AlertDescription,
Title as AlertTitle,
Action as AlertAction,
};
export * from '$lib/ui/core/alert/index.js';
+1 -2
View File
@@ -1,2 +1 @@
export { default as Badge } from "./badge.svelte";
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
export * from '$lib/ui/core/badge/index.js';
+1 -17
View File
@@ -1,17 +1 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};
export * from '$lib/ui/core/button/index.js';
+1 -25
View File
@@ -1,25 +1 @@
import Root from "./card.svelte";
import Content from "./card-content.svelte";
import Description from "./card-description.svelte";
import Footer from "./card-footer.svelte";
import Header from "./card-header.svelte";
import Title from "./card-title.svelte";
import Action from "./card-action.svelte";
export {
Root,
Content,
Description,
Footer,
Header,
Title,
Action,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
Action as CardAction,
};
export * from '$lib/ui/core/card/index.js';
@@ -0,0 +1 @@
export * from '$lib/ui/core/checkbox/index.js';
@@ -1,3 +1 @@
export { default as FlexRender } from "./flex-render.svelte";
export { renderComponent, renderSnippet } from "./render-helpers.js";
export { createSvelteTable } from "./data-table.svelte.js";
export * from '$lib/ui/core/data-table/index.js';
+1 -34
View File
@@ -1,34 +1 @@
import Root from "./dialog.svelte";
import Portal from "./dialog-portal.svelte";
import Title from "./dialog-title.svelte";
import Footer from "./dialog-footer.svelte";
import Header from "./dialog-header.svelte";
import Overlay from "./dialog-overlay.svelte";
import Content from "./dialog-content.svelte";
import Description from "./dialog-description.svelte";
import Trigger from "./dialog-trigger.svelte";
import Close from "./dialog-close.svelte";
export {
Root,
Title,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
Close,
//
Root as Dialog,
Title as DialogTitle,
Portal as DialogPortal,
Footer as DialogFooter,
Header as DialogHeader,
Trigger as DialogTrigger,
Overlay as DialogOverlay,
Content as DialogContent,
Description as DialogDescription,
Close as DialogClose,
};
export * from '$lib/ui/core/dialog/index.js';
@@ -1,54 +1 @@
import Root from "./dropdown-menu.svelte";
import Sub from "./dropdown-menu-sub.svelte";
import CheckboxGroup from "./dropdown-menu-checkbox-group.svelte";
import CheckboxItem from "./dropdown-menu-checkbox-item.svelte";
import Content from "./dropdown-menu-content.svelte";
import Group from "./dropdown-menu-group.svelte";
import Item from "./dropdown-menu-item.svelte";
import Label from "./dropdown-menu-label.svelte";
import RadioGroup from "./dropdown-menu-radio-group.svelte";
import RadioItem from "./dropdown-menu-radio-item.svelte";
import Separator from "./dropdown-menu-separator.svelte";
import Shortcut from "./dropdown-menu-shortcut.svelte";
import Trigger from "./dropdown-menu-trigger.svelte";
import SubContent from "./dropdown-menu-sub-content.svelte";
import SubTrigger from "./dropdown-menu-sub-trigger.svelte";
import GroupHeading from "./dropdown-menu-group-heading.svelte";
import Portal from "./dropdown-menu-portal.svelte";
export {
CheckboxGroup,
CheckboxItem,
Content,
Portal,
Root as DropdownMenu,
CheckboxGroup as DropdownMenuCheckboxGroup,
CheckboxItem as DropdownMenuCheckboxItem,
Content as DropdownMenuContent,
Portal as DropdownMenuPortal,
Group as DropdownMenuGroup,
Item as DropdownMenuItem,
Label as DropdownMenuLabel,
RadioGroup as DropdownMenuRadioGroup,
RadioItem as DropdownMenuRadioItem,
Separator as DropdownMenuSeparator,
Shortcut as DropdownMenuShortcut,
Sub as DropdownMenuSub,
SubContent as DropdownMenuSubContent,
SubTrigger as DropdownMenuSubTrigger,
Trigger as DropdownMenuTrigger,
GroupHeading as DropdownMenuGroupHeading,
Group,
GroupHeading,
Item,
Label,
RadioGroup,
RadioItem,
Root,
Separator,
Shortcut,
Sub,
SubContent,
SubTrigger,
Trigger,
};
export * from '$lib/ui/core/dropdown-menu/index.js';
+1
View File
@@ -0,0 +1 @@
export * from '$lib/ui/core/form/index.js';
+1 -7
View File
@@ -1,7 +1 @@
import Root from "./input.svelte";
export {
Root,
//
Root as Input,
};
export * from '$lib/ui/core/input/index.js';
+1 -7
View File
@@ -1,7 +1 @@
import Root from "./label.svelte";
export {
Root,
//
Root as Label,
};
export * from '$lib/ui/core/label/index.js';
+1 -28
View File
@@ -1,28 +1 @@
import Root from "./popover.svelte";
import Close from "./popover-close.svelte";
import Content from "./popover-content.svelte";
import Description from "./popover-description.svelte";
import Header from "./popover-header.svelte";
import Title from "./popover-title.svelte";
import Trigger from "./popover-trigger.svelte";
import Portal from "./popover-portal.svelte";
export {
Root,
Content,
Description,
Header,
Title,
Trigger,
Close,
Portal,
//
Root as Popover,
Content as PopoverContent,
Description as PopoverDescription,
Header as PopoverHeader,
Title as PopoverTitle,
Trigger as PopoverTrigger,
Close as PopoverClose,
Portal as PopoverPortal,
};
export * from '$lib/ui/core/popover/index.js';
+1 -10
View File
@@ -1,10 +1 @@
import Scrollbar from "./scroll-area-scrollbar.svelte";
import Root from "./scroll-area.svelte";
export {
Root,
Scrollbar,
//,
Root as ScrollArea,
Scrollbar as ScrollAreaScrollbar,
};
export * from '$lib/ui/core/scroll-area/index.js';
+1 -37
View File
@@ -1,37 +1 @@
import Root from "./select.svelte";
import Group from "./select-group.svelte";
import Label from "./select-label.svelte";
import Item from "./select-item.svelte";
import Content from "./select-content.svelte";
import Trigger from "./select-trigger.svelte";
import Separator from "./select-separator.svelte";
import ScrollDownButton from "./select-scroll-down-button.svelte";
import ScrollUpButton from "./select-scroll-up-button.svelte";
import GroupHeading from "./select-group-heading.svelte";
import Portal from "./select-portal.svelte";
export {
Root,
Group,
Label,
Item,
Content,
Trigger,
Separator,
ScrollDownButton,
ScrollUpButton,
GroupHeading,
Portal,
//
Root as Select,
Group as SelectGroup,
Label as SelectLabel,
Item as SelectItem,
Content as SelectContent,
Trigger as SelectTrigger,
Separator as SelectSeparator,
ScrollDownButton as SelectScrollDownButton,
ScrollUpButton as SelectScrollUpButton,
GroupHeading as SelectGroupHeading,
Portal as SelectPortal,
};
export * from '$lib/ui/core/select/index.js';
+1 -7
View File
@@ -1,7 +1 @@
import Root from "./separator.svelte";
export {
Root,
//
Root as Separator,
};
export * from '$lib/ui/core/separator/index.js';
+1
View File
@@ -0,0 +1 @@
export * from '$lib/ui/core/sheet/index.js';
@@ -0,0 +1 @@
export * from '$lib/ui/core/skeleton/index.js';
+1 -7
View File
@@ -1,7 +1 @@
import Root from "./switch.svelte";
export {
Root,
//
Root as Switch,
};
export * from '$lib/ui/core/switch/index.js';
+1 -28
View File
@@ -1,28 +1 @@
import Root from "./table.svelte";
import Body from "./table-body.svelte";
import Caption from "./table-caption.svelte";
import Cell from "./table-cell.svelte";
import Footer from "./table-footer.svelte";
import Head from "./table-head.svelte";
import Header from "./table-header.svelte";
import Row from "./table-row.svelte";
export {
Root,
Body,
Caption,
Cell,
Footer,
Head,
Header,
Row,
//
Root as Table,
Body as TableBody,
Caption as TableCaption,
Cell as TableCell,
Footer as TableFooter,
Head as TableHead,
Header as TableHeader,
Row as TableRow,
};
export * from '$lib/ui/core/table/index.js';
+1 -18
View File
@@ -1,18 +1 @@
import Root from "./tabs.svelte";
import Content from "./tabs-content.svelte";
import List, { tabsListVariants, type TabsListVariant } from "./tabs-list.svelte";
import Trigger from "./tabs-trigger.svelte";
export {
Root,
Content,
List,
Trigger,
tabsListVariants,
type TabsListVariant,
//
Root as Tabs,
Content as TabsContent,
List as TabsList,
Trigger as TabsTrigger,
};
export * from '$lib/ui/core/tabs/index.js';
+1 -7
View File
@@ -1,7 +1 @@
import Root from "./textarea.svelte";
export {
Root,
//
Root as Textarea,
};
export * from '$lib/ui/core/textarea/index.js';
+1 -19
View File
@@ -1,19 +1 @@
import Root from "./tooltip.svelte";
import Trigger from "./tooltip-trigger.svelte";
import Content from "./tooltip-content.svelte";
import Provider from "./tooltip-provider.svelte";
import Portal from "./tooltip-portal.svelte";
export {
Root,
Trigger,
Content,
Provider,
Portal,
//
Root as Tooltip,
Content as TooltipContent,
Trigger as TooltipTrigger,
Provider as TooltipProvider,
Portal as TooltipPortal,
};
export * from '$lib/ui/core/tooltip/index.js';
@@ -0,0 +1,33 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { TooltipProvider } from '$lib/ui/core/tooltip/index.js';
import type { ThemePreference } from '$lib/theme.js';
import AppSidebar from './app-sidebar.svelte';
import AppMobileNav from './app-mobile-nav.svelte';
let {
children,
theme = $bindable<ThemePreference>('system')
}: {
children: Snippet;
theme?: ThemePreference;
} = $props();
let collapsed = $state(false);
let mobileNavOpen = $state(false);
</script>
<TooltipProvider delayDuration={0}>
<div class="bg-background flex min-h-screen">
<AppSidebar bind:collapsed bind:theme />
<div class="flex min-w-0 flex-1 flex-col">
<div class="border-border flex items-center gap-2 border-b px-4 py-2 md:hidden">
<AppMobileNav bind:open={mobileNavOpen} bind:theme />
<span class="font-semibold tracking-tight">EvoBGP</span>
</div>
<main class="min-w-0 flex-1 p-4 md:p-6">
{@render children()}
</main>
</div>
</div>
</TooltipProvider>
@@ -0,0 +1,45 @@
<script lang="ts">
import { resolve } from '$app/paths';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger
} from '$lib/ui/core/sheet/index.js';
import { Button } from '$lib/ui/core/button/index.js';
import type { ThemePreference } from '$lib/theme.js';
import Menu from '@lucide/svelte/icons/menu';
import AppNavLinks from './app-nav-links.svelte';
import ThemeMenu from './theme-menu.svelte';
let {
open = $bindable(false),
theme = $bindable<ThemePreference>('system')
}: {
open?: boolean;
theme?: ThemePreference;
} = $props();
</script>
<Sheet bind:open>
<SheetTrigger>
<Button variant="ghost" size="icon-sm" class="md:hidden" aria-label="Открыть меню">
<Menu class="size-5" />
</Button>
</SheetTrigger>
<SheetContent side="left" class="bg-sidebar text-sidebar-foreground flex w-72 flex-col gap-0 p-0">
<SheetHeader class="border-border border-b p-4 text-left">
<SheetTitle>
<a href={resolve('/')} class="font-semibold tracking-tight no-underline">EvoBGP</a>
</SheetTitle>
<p class="text-sidebar-foreground/50 text-xs font-normal">Панель управления</p>
</SheetHeader>
<div class="flex items-center gap-2 px-4 py-2">
<ThemeMenu bind:theme />
</div>
<div class="flex min-h-0 flex-1 flex-col overflow-hidden">
<AppNavLinks onNavigate={() => (open = false)} />
</div>
</SheetContent>
</Sheet>
@@ -0,0 +1,91 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { cn } from '$lib/utils.js';
import { buttonVariants } from '$lib/ui/core/button/index.js';
import { Separator } from '$lib/ui/core/separator/index.js';
import { Tooltip, TooltipContent, TooltipTrigger } from '$lib/ui/core/tooltip/index.js';
import { bottomNav, mainNav } from './nav.js';
type Props = {
collapsed?: boolean;
onNavigate?: () => void;
};
let { collapsed = false, onNavigate }: Props = $props();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const navHref = (href: string) => resolve(href as any);
function isActive(href: string) {
const pathname = page.url.pathname;
if (href === '/') return pathname === '/';
return pathname === href || pathname.startsWith(href + '/');
}
function linkClass(active: boolean, iconOnly: boolean) {
return cn(
buttonVariants({ variant: active ? 'secondary' : 'ghost', size: iconOnly ? 'icon-sm' : 'sm' }),
iconOnly ? 'flex w-full items-center justify-center no-underline' : 'flex w-full items-center justify-start gap-2 no-underline',
active &&
(iconOnly
? 'ring-sidebar-primary/50 bg-sidebar-accent/90 text-sidebar-accent-foreground ring-1'
: 'border-l-sidebar-primary bg-sidebar-accent/80 text-sidebar-accent-foreground border-l-2 shadow-sm')
);
}
</script>
<nav class="flex flex-1 flex-col gap-0.5 overflow-y-auto p-2">
{#each mainNav as item (item.href)}
{@const Icon = item.icon}
{@const active = isActive(item.href)}
{#if collapsed}
<Tooltip>
<TooltipTrigger>
<a
href={navHref(item.href)}
class={linkClass(active, true)}
aria-current={active ? 'page' : undefined}
onclick={onNavigate}
>
<Icon class="size-4" />
</a>
</TooltipTrigger>
<TooltipContent side="right">{item.label}</TooltipContent>
</Tooltip>
{:else}
<a
href={navHref(item.href)}
class={linkClass(active, false)}
aria-current={active ? 'page' : undefined}
onclick={onNavigate}
>
<Icon class="size-4" />
{item.label}
</a>
{/if}
{/each}
</nav>
<div class="flex flex-col gap-0.5 p-2">
<Separator class="mb-2" />
{#each bottomNav as item (item.href)}
{@const Icon = item.icon}
{@const active = isActive(item.href)}
{#if collapsed}
<Tooltip>
<TooltipTrigger>
<a href={navHref(item.href)} class={linkClass(active, true)} onclick={onNavigate}>
<Icon class="size-4" />
</a>
</TooltipTrigger>
<TooltipContent side="right">{item.label}</TooltipContent>
</Tooltip>
{:else}
<a href={navHref(item.href)} class={linkClass(active, false)} onclick={onNavigate}>
<Icon class="size-4" />
{item.label}
</a>
{/if}
{/each}
</div>
@@ -0,0 +1,56 @@
<script lang="ts">
import { resolve } from '$app/paths';
import { cn } from '$lib/utils.js';
import { buttonVariants } from '$lib/ui/core/button/index.js';
import { Separator } from '$lib/ui/core/separator/index.js';
import type { ThemePreference } from '$lib/theme.js';
import PanelLeftClose from '@lucide/svelte/icons/panel-left-close';
import PanelLeftOpen from '@lucide/svelte/icons/panel-left-open';
import AppNavLinks from './app-nav-links.svelte';
import ThemeMenu from './theme-menu.svelte';
let {
collapsed = $bindable(false),
theme = $bindable<ThemePreference>('system')
}: {
collapsed?: boolean;
theme?: ThemePreference;
} = $props();
</script>
<aside
class={cn(
'border-border bg-sidebar sticky top-0 hidden h-screen shrink-0 flex-col overflow-hidden border-r transition-all duration-200 md:flex',
collapsed ? 'w-14' : 'w-56'
)}
aria-label="Основная навигация"
>
<div class={cn('flex items-center gap-2 p-3', collapsed && 'justify-center')}>
{#if !collapsed}
<div class="min-w-0 flex-1">
<a href={resolve('/')} class="text-sidebar-foreground block truncate font-semibold tracking-tight"
>EvoBGP</a
>
<p class="text-sidebar-foreground/50 truncate text-xs">Панель управления</p>
</div>
{/if}
<ThemeMenu bind:theme />
<button
type="button"
onclick={() => (collapsed = !collapsed)}
class={cn(
buttonVariants({ variant: 'ghost', size: 'icon-sm' }),
'text-sidebar-foreground/60 hover:text-sidebar-foreground shrink-0'
)}
aria-label={collapsed ? 'Развернуть меню' : 'Свернуть меню'}
>
{#if collapsed}
<PanelLeftOpen class="size-4" />
{:else}
<PanelLeftClose class="size-4" />
{/if}
</button>
</div>
<Separator />
<AppNavLinks {collapsed} />
</aside>
@@ -0,0 +1,38 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { cn } from '$lib/utils.js';
type Props = {
title?: string;
description?: string;
actions?: Snippet;
mobileNav?: Snippet;
class?: string;
};
let { title, description, actions, mobileNav, class: className }: Props = $props();
</script>
<header
class={cn(
'border-border bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-30 flex flex-col gap-3 border-b px-4 py-3 backdrop-blur md:px-6',
className
)}
>
<div class="flex items-start gap-3">
{#if mobileNav}
<div class="shrink-0 pt-0.5 md:hidden">{@render mobileNav()}</div>
{/if}
<div class="min-w-0 flex-1">
{#if title}
<h1 class="text-lg font-semibold tracking-tight md:text-xl">{title}</h1>
{/if}
{#if description}
<p class="text-muted-foreground mt-0.5 text-sm">{description}</p>
{/if}
</div>
{#if actions}
<div class="flex shrink-0 flex-wrap items-center gap-2">{@render actions()}</div>
{/if}
</div>
</header>
+29
View File
@@ -0,0 +1,29 @@
import type { Component } from 'svelte';
import Activity from '@lucide/svelte/icons/activity';
import Boxes from '@lucide/svelte/icons/boxes';
import BookOpen from '@lucide/svelte/icons/book-open';
import CalendarClock from '@lucide/svelte/icons/calendar-clock';
import Gauge from '@lucide/svelte/icons/gauge';
import LayoutDashboard from '@lucide/svelte/icons/layout-dashboard';
import Network from '@lucide/svelte/icons/network';
import Settings from '@lucide/svelte/icons/settings';
import Zap from '@lucide/svelte/icons/zap';
export type NavItem = {
href: string;
label: string;
icon: Component;
};
export const mainNav: NavItem[] = [
{ href: '/', label: 'Обзор', icon: LayoutDashboard },
{ href: '/modules', label: 'Модули', icon: Boxes },
{ href: '/directories', label: 'Справочники', icon: BookOpen },
{ href: '/network', label: 'Сеть', icon: Network },
{ href: '/revisions', label: 'Ревизии', icon: Activity },
{ href: '/operations', label: 'Операции', icon: Zap },
{ href: '/schedule', label: 'Расписание', icon: CalendarClock },
{ href: '/monitoring', label: 'Мониторинг', icon: Gauge }
];
export const bottomNav: NavItem[] = [{ href: '/settings', label: 'Настройки', icon: Settings }];
@@ -0,0 +1,51 @@
<script lang="ts">
import { cn } from '$lib/utils.js';
import { buttonVariants } from '$lib/ui/core/button/index.js';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger
} from '$lib/ui/core/dropdown-menu/index.js';
import type { ThemePreference } from '$lib/theme.js';
import Sun from '@lucide/svelte/icons/sun';
import Moon from '@lucide/svelte/icons/moon';
import Monitor from '@lucide/svelte/icons/monitor';
let { theme = $bindable<ThemePreference>('system'), class: className }: { theme?: ThemePreference; class?: string } = $props();
</script>
<DropdownMenu>
<DropdownMenuTrigger
class={cn(
buttonVariants({ variant: 'ghost', size: 'icon-sm' }),
'text-sidebar-foreground/60 hover:text-sidebar-foreground shrink-0',
className
)}
aria-label="Тема оформления"
>
<span class="flex size-4 items-center justify-center">
<Sun class="size-4 dark:hidden" />
<Moon class="hidden size-4 dark:block" />
</span>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" class="w-44">
<DropdownMenuLabel>Тема</DropdownMenuLabel>
<DropdownMenuRadioGroup bind:value={theme}>
<DropdownMenuRadioItem value="light" class="gap-2">
<Sun class="size-4" />
Светлая
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark" class="gap-2">
<Moon class="size-4" />
Тёмная
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system" class="gap-2">
<Monitor class="size-4" />
Как в системе
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
@@ -0,0 +1,40 @@
<script lang="ts">
import type { Component, Snippet } from 'svelte';
import { cn } from '$lib/utils.js';
type Props = {
title: string;
description?: string;
icon?: Component;
iconClass?: string;
actions?: Snippet;
class?: string;
};
let { title, description, icon: Icon, iconClass, actions, class: className }: Props = $props();
</script>
<div class={cn('flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between', className)}>
<div class="flex min-w-0 items-start gap-3">
{#if Icon}
<div
class={cn(
'flex size-11 shrink-0 items-center justify-center rounded-xl',
iconClass ?? 'bg-muted text-muted-foreground'
)}
aria-hidden="true"
>
<Icon class="size-6" />
</div>
{/if}
<div class="min-w-0">
<h1 class="text-2xl font-semibold tracking-tight">{title}</h1>
{#if description}
<p class="text-muted-foreground mt-1 text-sm">{description}</p>
{/if}
</div>
</div>
{#if actions}
<div class="flex shrink-0 flex-wrap items-center gap-2">{@render actions()}</div>
{/if}
</div>
+20
View File
@@ -0,0 +1,20 @@
import { toast } from 'svelte-sonner';
export const notify = {
success: (message: string, description?: string) =>
toast.success(message, description ? { description } : undefined),
error: (message: string, description?: string) =>
toast.error(message, description ? { description } : undefined),
info: (message: string, description?: string) =>
toast.info(message, description ? { description } : undefined),
warning: (message: string, description?: string) =>
toast.warning(message, description ? { description } : undefined),
promise: toast.promise,
dismiss: toast.dismiss,
message: toast.message
};
export function notifyApiError(e: unknown, fallback = 'Ошибка запроса') {
const message = e instanceof Error ? e.message : String(e);
notify.error(message || fallback);
}
+22
View File
@@ -0,0 +1,22 @@
# Design tokens (EvoBGP WebUI)
Базовая тема — [shadcn-svelte](https://www.shadcn-svelte.com), переменные в [`src/routes/layout.css`](../../routes/layout.css).
## Layout
| Token | Значение | Tailwind |
|-------|----------|----------|
| `--page-padding` | 1.5rem | `p-page` через `p-[var(--page-padding)]` |
| `--section-gap` | 1.5rem | `gap-section` |
## Радиусы
`--radius-sm` … `--radius-xl` — из `--radius` (shadcn).
## Семантика
`success`, `warning`, `info`, `destructive` — классы `text-success`, `bg-warning/10`, `border-info` и т.д.
## Размеры компонентов
Button/Input: `sm` | `default` | `lg` — через `buttonVariants` и классы input в `ui/core`.
@@ -4,7 +4,7 @@
buttonVariants,
type ButtonVariant,
type ButtonSize,
} from "$lib/components/ui/button/index.js";
} from "$lib/ui/core/button/index.js";
import { cn } from "$lib/utils.js";
let {
@@ -4,7 +4,7 @@
buttonVariants,
type ButtonVariant,
type ButtonSize,
} from "$lib/components/ui/button/index.js";
} from "$lib/ui/core/button/index.js";
import { cn } from "$lib/utils.js";
let {
+40
View File
@@ -0,0 +1,40 @@
import Root from "./alert-dialog.svelte";
import Portal from "./alert-dialog-portal.svelte";
import Trigger from "./alert-dialog-trigger.svelte";
import Title from "./alert-dialog-title.svelte";
import Action from "./alert-dialog-action.svelte";
import Cancel from "./alert-dialog-cancel.svelte";
import Footer from "./alert-dialog-footer.svelte";
import Header from "./alert-dialog-header.svelte";
import Overlay from "./alert-dialog-overlay.svelte";
import Content from "./alert-dialog-content.svelte";
import Description from "./alert-dialog-description.svelte";
import Media from "./alert-dialog-media.svelte";
export {
Root,
Title,
Action,
Cancel,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
Media,
//
Root as AlertDialog,
Title as AlertDialogTitle,
Action as AlertDialogAction,
Cancel as AlertDialogCancel,
Portal as AlertDialogPortal,
Footer as AlertDialogFooter,
Header as AlertDialogHeader,
Trigger as AlertDialogTrigger,
Overlay as AlertDialogOverlay,
Content as AlertDialogContent,
Description as AlertDialogDescription,
Media as AlertDialogMedia,
};
+17
View File
@@ -0,0 +1,17 @@
import Root from "./alert.svelte";
import Description from "./alert-description.svelte";
import Title from "./alert-title.svelte";
import Action from "./alert-action.svelte";
export { alertVariants, type AlertVariant } from "./alert.svelte";
export {
Root,
Description,
Title,
Action,
//
Root as Alert,
Description as AlertDescription,
Title as AlertTitle,
Action as AlertAction,
};
+2
View File
@@ -0,0 +1,2 @@
export { default as Badge } from "./badge.svelte";
export { badgeVariants, type BadgeVariant } from "./badge.svelte";
+17
View File
@@ -0,0 +1,17 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};
+25
View File
@@ -0,0 +1,25 @@
import Root from "./card.svelte";
import Content from "./card-content.svelte";
import Description from "./card-description.svelte";
import Footer from "./card-footer.svelte";
import Header from "./card-header.svelte";
import Title from "./card-title.svelte";
import Action from "./card-action.svelte";
export {
Root,
Content,
Description,
Footer,
Header,
Title,
Action,
//
Root as Card,
Content as CardContent,
Description as CardDescription,
Footer as CardFooter,
Header as CardHeader,
Title as CardTitle,
Action as CardAction,
};
@@ -0,0 +1,39 @@
<script lang="ts">
import { Checkbox as CheckboxPrimitive } from "bits-ui";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import CheckIcon from '@lucide/svelte/icons/check';
import MinusIcon from '@lucide/svelte/icons/minus';
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
...restProps
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
</script>
<CheckboxPrimitive.Root
bind:ref
data-slot="checkbox"
class={cn(
"border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-[4px] border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-3 aria-invalid:ring-3 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:checked
bind:indeterminate
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<div
data-slot="checkbox-indicator"
class="[&>svg]:size-3.5 grid place-content-center text-current transition-none"
>
{#if checked}
<CheckIcon />
{:else if indeterminate}
<MinusIcon />
{/if}
</div>
{/snippet}
</CheckboxPrimitive.Root>
+6
View File
@@ -0,0 +1,6 @@
import Root from "./checkbox.svelte";
export {
Root,
//
Root as Checkbox,
};
+3
View File
@@ -0,0 +1,3 @@
export { default as FlexRender } from "./flex-render.svelte";
export { renderComponent, renderSnippet } from "./render-helpers.js";
export { createSvelteTable } from "./data-table.svelte.js";
@@ -5,7 +5,7 @@
import * as Dialog from "./index.js";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import type { ComponentProps } from "svelte";
import { Button } from "$lib/components/ui/button/index.js";
import { Button } from "$lib/ui/core/button/index.js";
import XIcon from '@lucide/svelte/icons/x';
let {
@@ -2,7 +2,7 @@
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import { Dialog as DialogPrimitive } from "bits-ui";
import { Button } from "$lib/components/ui/button/index.js";
import { Button } from "$lib/ui/core/button/index.js";
let {
ref = $bindable(null),
+34
View File
@@ -0,0 +1,34 @@
import Root from "./dialog.svelte";
import Portal from "./dialog-portal.svelte";
import Title from "./dialog-title.svelte";
import Footer from "./dialog-footer.svelte";
import Header from "./dialog-header.svelte";
import Overlay from "./dialog-overlay.svelte";
import Content from "./dialog-content.svelte";
import Description from "./dialog-description.svelte";
import Trigger from "./dialog-trigger.svelte";
import Close from "./dialog-close.svelte";
export {
Root,
Title,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
Close,
//
Root as Dialog,
Title as DialogTitle,
Portal as DialogPortal,
Footer as DialogFooter,
Header as DialogHeader,
Trigger as DialogTrigger,
Overlay as DialogOverlay,
Content as DialogContent,
Description as DialogDescription,
Close as DialogClose,
};
Loaded 100 of 200 files, more files were not shown because too many files have changed in this diff. Show more