Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fcd2dca8dc | |||
| db8ed2a868 | |||
| 522d31dc6e | |||
| 36ba210323 | |||
| 8ccc515a7b | |||
| d8ae756948 | |||
| f3b34df5bd | |||
| 3b910850cb | |||
| b9101a4582 | |||
| 58744e46b6 |
@@ -69,6 +69,7 @@ nunit-*.xml
|
|||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
dist-ssr/
|
dist-ssr/
|
||||||
|
API/wwwroot/
|
||||||
.vite/
|
.vite/
|
||||||
.npm/
|
.npm/
|
||||||
.pnpm-store/
|
.pnpm-store/
|
||||||
@@ -84,6 +85,8 @@ __screenshots__/
|
|||||||
.env.local
|
.env.local
|
||||||
.env.*.local
|
.env.*.local
|
||||||
*.local
|
*.local
|
||||||
|
API/appsettings.custom.json
|
||||||
|
API/appsettings.custom.*.json
|
||||||
|
|
||||||
# Temporary files
|
# Temporary files
|
||||||
~$*
|
~$*
|
||||||
|
|||||||
+6
-1
@@ -7,7 +7,12 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.4">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageReference>
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace API.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/health")]
|
||||||
|
public class HealthController : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult Get()
|
||||||
|
{
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace API.Controllers
|
|
||||||
{
|
|
||||||
[ApiController]
|
|
||||||
[Route("[controller]")]
|
|
||||||
public class WeatherForecastController : ControllerBase
|
|
||||||
{
|
|
||||||
private static readonly string[] Summaries =
|
|
||||||
[
|
|
||||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
|
||||||
];
|
|
||||||
|
|
||||||
[HttpGet(Name = "GetWeatherForecast")]
|
|
||||||
public IEnumerable<WeatherForecast> Get()
|
|
||||||
{
|
|
||||||
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
|
||||||
{
|
|
||||||
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
|
||||||
TemperatureC = Random.Shared.Next(-20, 55),
|
|
||||||
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
|
|
||||||
})
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace API.Database;
|
||||||
|
|
||||||
|
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : DbContext(options);
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: hoard-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: hoard
|
||||||
|
POSTGRES_USER: hoard
|
||||||
|
POSTGRES_PASSWORD: hoard_dev_password
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- hoard_postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U hoard -d hoard"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
pgadmin:
|
||||||
|
image: dpage/pgadmin4:9.3
|
||||||
|
container_name: hoard-pgadmin
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
PGADMIN_DEFAULT_EMAIL: familiehimmelberg@gmail.com
|
||||||
|
PGADMIN_DEFAULT_PASSWORD: admin
|
||||||
|
PGADMIN_CONFIG_SERVER_MODE: "False"
|
||||||
|
ports:
|
||||||
|
- "5050:80"
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
- hoard_pgadmin_data:/var/lib/pgadmin
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
hoard_postgres_data:
|
||||||
|
hoard_pgadmin_data:
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using API.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260418134949_InitialPostgres")]
|
||||||
|
partial class InitialPostgres
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.4")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("API.Models.Test.TestItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAtUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(2000)
|
||||||
|
.HasColumnType("character varying(2000)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAtUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("test_items", (string)null);
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InitialPostgres : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "test_items",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
|
||||||
|
CreatedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
UpdatedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_test_items", x => x.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "test_items");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using API.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260418153650_RemoveTestItems")]
|
||||||
|
partial class RemoveTestItems
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.4")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class RemoveTestItems : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "test_items");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "test_items",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
CreatedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
|
||||||
|
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||||
|
UpdatedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_test_items", x => x.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using API.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace API.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.4")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+43
-9
@@ -1,23 +1,57 @@
|
|||||||
var builder = WebApplication.CreateBuilder(args);
|
using API.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
// Add services to the container.
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration.AddJsonFile("appsettings.custom.json", optional: true, reloadOnChange: true);
|
||||||
|
|
||||||
|
var connectionString = builder.Configuration.GetConnectionString("Postgres")
|
||||||
|
?? throw new InvalidOperationException("Connection string 'Postgres' wurde nicht gefunden.");
|
||||||
|
|
||||||
|
builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
||||||
|
{
|
||||||
|
options.UseNpgsql(connectionString);
|
||||||
|
});
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddOpenApi();
|
builder.Services.AddSwaggerGen();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
using (var scope = app.Services.CreateScope())
|
||||||
|
{
|
||||||
|
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
|
||||||
|
dbContext.Database.Migrate();
|
||||||
|
}
|
||||||
|
var webRootPath = app.Environment.WebRootPath ?? Path.Combine(app.Environment.ContentRootPath, "wwwroot");
|
||||||
|
var indexFilePath = Path.Combine(webRootPath, "index.html");
|
||||||
|
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.MapOpenApi();
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseDefaultFiles();
|
||||||
|
app.UseStaticFiles();
|
||||||
app.UseAuthorization();
|
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
app.MapFallback(async context =>
|
||||||
|
{
|
||||||
|
if (context.Request.Path.StartsWithSegments("/api"))
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(indexFilePath))
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = StatusCodes.Status404NotFound;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Response.ContentType = "text/html; charset=utf-8";
|
||||||
|
await context.Response.SendFileAsync(indexFilePath);
|
||||||
|
});
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
namespace API
|
|
||||||
{
|
|
||||||
public class WeatherForecast
|
|
||||||
{
|
|
||||||
public DateOnly Date { get; set; }
|
|
||||||
|
|
||||||
public int TemperatureC { get; set; }
|
|
||||||
|
|
||||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
|
||||||
|
|
||||||
public string? Summary { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"Postgres": "Host=localhost;Port=5432;Database=hoard;Username=hoard;Password=hoard_dev_password"
|
||||||
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"Postgres": ""
|
||||||
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
|
|||||||
Generated
+47
-8
@@ -8,9 +8,12 @@
|
|||||||
"name": "-",
|
"name": "-",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource/roboto": "^5.2.10",
|
||||||
|
"@mdi/font": "^7.4.47",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.31",
|
"vue": "^3.5.31",
|
||||||
"vue-router": "^5.0.4"
|
"vue-router": "^5.0.4",
|
||||||
|
"vuetify": "^4.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node24": "^24.0.4",
|
"@tsconfig/node24": "^24.0.4",
|
||||||
@@ -59,7 +62,6 @@
|
|||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -515,10 +517,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fontsource/roboto": {
|
||||||
|
"version": "5.2.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.10.tgz",
|
||||||
|
"integrity": "sha512-8HlA5FtSfz//oFSr2eL7GFXAiE7eIkcGOtx7tjsLKq+as702x9+GU7K95iDeWFapHC4M2hv9RrpXKRTGGBI8Zg==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
@@ -564,6 +576,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@mdi/font": {
|
||||||
|
"version": "7.4.47",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz",
|
||||||
|
"integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/wasm-runtime": {
|
"node_modules/@napi-rs/wasm-runtime": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
|
||||||
@@ -886,7 +904,6 @@
|
|||||||
"integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
|
"integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
@@ -1342,7 +1359,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.10.12",
|
"baseline-browser-mapping": "^2.10.12",
|
||||||
"caniuse-lite": "^1.0.30001782",
|
"caniuse-lite": "^1.0.30001782",
|
||||||
@@ -2304,7 +2320,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
||||||
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/devtools-api": "^7.7.7"
|
"@vue/devtools-api": "^7.7.7"
|
||||||
},
|
},
|
||||||
@@ -2616,7 +2631,6 @@
|
|||||||
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
|
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -2705,7 +2719,6 @@
|
|||||||
"integrity": "sha512-baBr4jUVSLJ0RPyZ2nK0zS2+W8hNHbM4hEzfvllukmRPVS3xDG5ATTNtbRXrKIOE2b8/FsPWJAOnuIxcs7g3cw==",
|
"integrity": "sha512-baBr4jUVSLJ0RPyZ2nK0zS2+W8hNHbM4hEzfvllukmRPVS3xDG5ATTNtbRXrKIOE2b8/FsPWJAOnuIxcs7g3cw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.4",
|
||||||
@@ -2921,7 +2934,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz",
|
||||||
"integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==",
|
"integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.32",
|
"@vue/compiler-dom": "3.5.32",
|
||||||
"@vue/compiler-sfc": "3.5.32",
|
"@vue/compiler-sfc": "3.5.32",
|
||||||
@@ -3033,6 +3045,33 @@
|
|||||||
"typescript": ">=5.0.0"
|
"typescript": ">=5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vuetify": {
|
||||||
|
"version": "4.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-4.0.5.tgz",
|
||||||
|
"integrity": "sha512-pFysKOHuY3dROTVh9PdlhVz50ZR0E5/goY5ecTXc8F8tajUA2ee3xZ8Lqs1WtEw/X3w93wx/LogyjgaQCAL/Ig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/johnleider"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=4.7",
|
||||||
|
"vite-plugin-vuetify": ">=2.1.0",
|
||||||
|
"vue": "^3.5.0",
|
||||||
|
"webpack-plugin-vuetify": ">=3.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite-plugin-vuetify": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"webpack-plugin-vuetify": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/webpack-virtual-modules": {
|
"node_modules/webpack-virtual-modules": {
|
||||||
"version": "0.6.2",
|
"version": "0.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
|
||||||
|
|||||||
+4
-1
@@ -12,9 +12,12 @@
|
|||||||
"format": "prettier --write --experimental-cli src/"
|
"format": "prettier --write --experimental-cli src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource/roboto": "^5.2.10",
|
||||||
|
"@mdi/font": "^7.4.47",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.31",
|
"vue": "^3.5.31",
|
||||||
"vue-router": "^5.0.4"
|
"vue-router": "^5.0.4",
|
||||||
|
"vuetify": "^4.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node24": "^24.0.4",
|
"@tsconfig/node24": "^24.0.4",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 34 KiB |
@@ -1,11 +0,0 @@
|
|||||||
<script setup lang="ts"></script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<h1>You did it!</h1>
|
|
||||||
<p>
|
|
||||||
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the
|
|
||||||
documentation
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
@@ -0,0 +1,521 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
import { useDisplay, useTheme } from 'vuetify'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import iconImage from '@/assets/images/icon.svg'
|
||||||
|
import { Visibility, routes } from '@/plugins/routesLayout'
|
||||||
|
|
||||||
|
const display = useDisplay()
|
||||||
|
const theme = useTheme()
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const showDrawer = ref(true)
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
const themeStorageKey = 'theme'
|
||||||
|
|
||||||
|
function normalizeRoutePath(path: string) {
|
||||||
|
if (!path || path === '/') {
|
||||||
|
return '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.endsWith('/') ? path.slice(0, -1) : path
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWithinRoutePath(currentPath: string, targetPath: string) {
|
||||||
|
const normalizedCurrentPath = normalizeRoutePath(currentPath)
|
||||||
|
const normalizedTargetPath = normalizeRoutePath(targetPath)
|
||||||
|
|
||||||
|
if (normalizedTargetPath === '/') {
|
||||||
|
return normalizedCurrentPath === '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
normalizedCurrentPath === normalizedTargetPath ||
|
||||||
|
normalizedCurrentPath.startsWith(`${normalizedTargetPath}/`)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveVisibilityRoutes(path: string, visibilityRoute?: string | string[]) {
|
||||||
|
if (Array.isArray(visibilityRoute)) {
|
||||||
|
return visibilityRoute.map((entry) => entry.trim()).filter((entry) => entry.length > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedVisibilityRoute = visibilityRoute?.trim()
|
||||||
|
if (normalizedVisibilityRoute && normalizedVisibilityRoute.length > 0) {
|
||||||
|
return [normalizedVisibilityRoute]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [path]
|
||||||
|
}
|
||||||
|
|
||||||
|
const sidebarRoutes = computed(() =>
|
||||||
|
routes.filter((item) => {
|
||||||
|
if (item.visible === Visibility.Public) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.visible !== Visibility.Route) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveVisibilityRoutes(item.path, item.visibilityRoute).some((targetPath) =>
|
||||||
|
isWithinRoutePath(route.path, targetPath),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const footerRoutes = computed(() => routes.filter((x) => x.visible === Visibility.Footer))
|
||||||
|
|
||||||
|
const activeRoute = computed(() => {
|
||||||
|
const byName = routes.find((x) => x.meta?.name === route.name)
|
||||||
|
if (byName) {
|
||||||
|
return byName
|
||||||
|
}
|
||||||
|
|
||||||
|
return routes.find((x) => x.path === route.path)
|
||||||
|
})
|
||||||
|
|
||||||
|
const pageName = computed(() => activeRoute.value?.name ?? 'Hoard')
|
||||||
|
const pageDescription = computed(
|
||||||
|
() => activeRoute.value?.description ?? 'Self-hosted Dateiablage im Browser',
|
||||||
|
)
|
||||||
|
const shouldShowFooter = computed(() => activeRoute.value?.disableFooter !== true)
|
||||||
|
const isDarkTheme = computed(() => theme.global.name.value === 'dark')
|
||||||
|
const themeIcon = computed(() => (isDarkTheme.value ? 'mdi-white-balance-sunny' : 'mdi-weather-night'))
|
||||||
|
const themeLabel = computed(() =>
|
||||||
|
isDarkTheme.value ? 'Hellen Modus aktivieren' : 'Dunklen Modus aktivieren',
|
||||||
|
)
|
||||||
|
|
||||||
|
function applyTheme(nextTheme: 'light' | 'dark', persist = true) {
|
||||||
|
theme.global.name.value = nextTheme
|
||||||
|
document.documentElement.setAttribute('data-theme', nextTheme)
|
||||||
|
if (persist) {
|
||||||
|
localStorage.setItem(themeStorageKey, nextTheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleDrawer() {
|
||||||
|
showDrawer.value = !showDrawer.value
|
||||||
|
localStorage.setItem('drawer', showDrawer.value ? 'Y' : 'N')
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
applyTheme(isDarkTheme.value ? 'light' : 'dark')
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeWebsiteTitle() {
|
||||||
|
if (activeRoute.value) {
|
||||||
|
document.title = `Hoard | ${activeRoute.value.name}`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
document.title = 'Hoard'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const storedTheme = localStorage.getItem(themeStorageKey)
|
||||||
|
const validTheme = storedTheme === 'dark' || storedTheme === 'light' ? storedTheme : 'light'
|
||||||
|
applyTheme(validTheme, false)
|
||||||
|
|
||||||
|
const storedDrawer = localStorage.getItem('drawer')
|
||||||
|
showDrawer.value = storedDrawer ? storedDrawer.startsWith('Y') : !display.mobile.value
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.fullPath,
|
||||||
|
() => {
|
||||||
|
changeWebsiteTitle()
|
||||||
|
if (display.mobile.value) {
|
||||||
|
showDrawer.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-app class="hoard-shell">
|
||||||
|
<v-app-bar class="hoard-app-bar" elevation="0" height="64">
|
||||||
|
<template #prepend>
|
||||||
|
<v-app-bar-nav-icon
|
||||||
|
v-tooltip="!showDrawer ? 'Menü öffnen' : 'Menü schließen'"
|
||||||
|
@click="toggleDrawer()"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<button type="button" class="brand-button" @click="router.push({ name: 'Home' })">
|
||||||
|
<span class="brand-mark">
|
||||||
|
<img :src="iconImage" alt="Hoard Icon" class="brand-logo" />
|
||||||
|
</span>
|
||||||
|
<span class="brand-text">
|
||||||
|
<span class="brand-title">Hoard</span>
|
||||||
|
<span class="brand-subtitle">Dateien zuerst</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<v-divider vertical class="mx-3 topbar-context-divider" />
|
||||||
|
|
||||||
|
<div class="page-context">
|
||||||
|
<p class="page-name">{{ pageName }}</p>
|
||||||
|
<p class="page-description">{{ pageDescription }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-spacer />
|
||||||
|
|
||||||
|
<div class="topbar-actions">
|
||||||
|
<v-tooltip location="bottom">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
icon
|
||||||
|
:aria-label="themeLabel"
|
||||||
|
v-bind="props"
|
||||||
|
@click="toggleTheme"
|
||||||
|
>
|
||||||
|
<v-icon>{{ themeIcon }}</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
{{ themeLabel }}
|
||||||
|
</v-tooltip>
|
||||||
|
|
||||||
|
<v-tooltip v-if="!display.smAndDown.value" location="bottom">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn variant="outlined" prepend-icon="mdi-account-circle-outline" to="/login" v-bind="props">
|
||||||
|
Konto
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
Zum Login
|
||||||
|
</v-tooltip>
|
||||||
|
|
||||||
|
<v-tooltip v-else location="bottom">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn icon to="/login" v-bind="props" aria-label="Zum Login">
|
||||||
|
<v-icon>mdi-account-circle-outline</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
Zum Login
|
||||||
|
</v-tooltip>
|
||||||
|
</div>
|
||||||
|
</v-app-bar>
|
||||||
|
|
||||||
|
<v-navigation-drawer
|
||||||
|
v-model="showDrawer"
|
||||||
|
:class="['hoard-drawer', { 'hoard-drawer--mobile': display.mobile.value }]"
|
||||||
|
:location="display.mobile.value ? 'bottom' : 'left'"
|
||||||
|
:temporary="display.mobile.value"
|
||||||
|
:permanent="!display.mobile.value"
|
||||||
|
:width="268"
|
||||||
|
:elevation="display.mobile.value ? 6 : 0"
|
||||||
|
>
|
||||||
|
<div class="drawer-top">
|
||||||
|
<p class="drawer-kicker hoard-kicker hoard-kicker--xs">Navigation</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-list nav :density="display.mobile.value ? 'default' : 'comfortable'" class="px-1">
|
||||||
|
<v-list-item
|
||||||
|
v-for="item in sidebarRoutes"
|
||||||
|
:key="item.path"
|
||||||
|
:to="item.path"
|
||||||
|
:active="route.path === item.path"
|
||||||
|
:prepend-icon="item.icon"
|
||||||
|
:title="item.name"
|
||||||
|
class="hoard-nav-item"
|
||||||
|
rounded="lg"
|
||||||
|
link
|
||||||
|
/>
|
||||||
|
</v-list>
|
||||||
|
|
||||||
|
<template #append>
|
||||||
|
<div class="drawer-bottom">
|
||||||
|
<v-btn variant="text" prepend-icon="mdi-home" to="/" :block="display.mobile.value">
|
||||||
|
Zur Startseite
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-navigation-drawer>
|
||||||
|
|
||||||
|
<v-main class="hoard-main">
|
||||||
|
<div class="main-shell">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-footer
|
||||||
|
v-if="shouldShowFooter"
|
||||||
|
class="hoard-footer d-flex align-center justify-space-between ga-2 flex-wrap py-3"
|
||||||
|
>
|
||||||
|
<div class="footer-links">
|
||||||
|
<v-btn
|
||||||
|
v-for="link in footerRoutes"
|
||||||
|
:key="link.path"
|
||||||
|
:to="link.path"
|
||||||
|
:text="link.name"
|
||||||
|
variant="text"
|
||||||
|
rounded
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="footer-copy">{{ currentYear }} - <strong>Hoard</strong></p>
|
||||||
|
</v-footer>
|
||||||
|
</v-main>
|
||||||
|
</v-app>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hoard-shell {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-app-bar {
|
||||||
|
padding-inline: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
color: inherit;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-mark {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo {
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-title {
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-subtitle {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-context {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-context-divider {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-name,
|
||||||
|
.page-description,
|
||||||
|
.drawer-kicker,
|
||||||
|
.drawer-text,
|
||||||
|
.footer-copy {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-name {
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-description {
|
||||||
|
max-width: min(360px, 32vw);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 1360px) {
|
||||||
|
.page-description {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-context {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 1180px) {
|
||||||
|
.topbar-context-divider {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-drawer {
|
||||||
|
padding-top: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-drawer--mobile {
|
||||||
|
border-top-left-radius: var(--radius-lg);
|
||||||
|
border-top-right-radius: var(--radius-lg);
|
||||||
|
max-height: min(72vh, 560px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-top {
|
||||||
|
padding: var(--space-2) var(--space-4) var(--space-4);
|
||||||
|
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 85%, white 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-title {
|
||||||
|
margin: var(--space-2) 0 var(--space-1);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: var(--font-size-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-text {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-bottom {
|
||||||
|
padding: var(--space-3) var(--space-2) var(--space-4);
|
||||||
|
border-top: 1px solid color-mix(in srgb, var(--color-border) 90%, white 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.hoard-nav-item) {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.hoard-nav-item .v-list-item-title) {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-shell {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-footer {
|
||||||
|
padding-inline: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-copy {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 960px) {
|
||||||
|
.hoard-app-bar {
|
||||||
|
padding-inline:
|
||||||
|
max(var(--space-1), env(safe-area-inset-left))
|
||||||
|
max(var(--space-1), env(safe-area-inset-right));
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-button {
|
||||||
|
min-width: 0;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-title {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-actions {
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbar-context-divider,
|
||||||
|
.page-context {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-drawer {
|
||||||
|
padding-top: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-top {
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-bottom {
|
||||||
|
padding: var(--space-2) var(--space-3) calc(var(--space-3) + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.drawer-bottom .v-btn) {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-shell {
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-subtitle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-footer {
|
||||||
|
justify-content: center !important;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding-inline:
|
||||||
|
max(var(--space-2), env(safe-area-inset-left))
|
||||||
|
max(var(--space-2), env(safe-area-inset-right));
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 600px) {
|
||||||
|
.main-shell {
|
||||||
|
padding: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-title {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.footer-links .v-btn) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@@ -0,0 +1,74 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" width="2048" height="2048">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="borderGrad" x1="560" y1="520" x2="1500" y2="1455" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#061117"/>
|
||||||
|
<stop offset="0.24" stop-color="#0a2530"/>
|
||||||
|
<stop offset="0.48" stop-color="#0a3946"/>
|
||||||
|
<stop offset="0.74" stop-color="#081f28"/>
|
||||||
|
<stop offset="1" stop-color="#050e13"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="borderGlow" cx="1210" cy="930" r="720" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#1ea97e" stop-opacity="0.06"/>
|
||||||
|
<stop offset="0.45" stop-color="#0d4e4d" stop-opacity="0.05"/>
|
||||||
|
<stop offset="1" stop-color="#000000" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
|
||||||
|
<linearGradient id="topGrad" x1="590" y1="720" x2="1355" y2="540" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#9bdb61"/>
|
||||||
|
<stop offset="0.56" stop-color="#b9ee76"/>
|
||||||
|
<stop offset="1" stop-color="#a6df67"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="leftGrad" x1="588" y1="670" x2="726" y2="1305" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#0a8b4d"/>
|
||||||
|
<stop offset="0.52" stop-color="#067841"/>
|
||||||
|
<stop offset="1" stop-color="#005a33"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="frontGrad" x1="1120" y1="745" x2="1030" y2="1385" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#2dbf47"/>
|
||||||
|
<stop offset="0.42" stop-color="#24a847"/>
|
||||||
|
<stop offset="1" stop-color="#006b3b"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="frontHighlight" cx="1110" cy="850" r="500" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#d6ff9c" stop-opacity="0.035"/>
|
||||||
|
<stop offset="0.6" stop-color="#7ad26f" stop-opacity="0.01"/>
|
||||||
|
<stop offset="1" stop-color="#000000" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
|
||||||
|
<linearGradient id="stripeGrad" x1="740" y1="560" x2="1335" y2="695" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#0b7d42"/>
|
||||||
|
<stop offset="1" stop-color="#165f37"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="panelShadowGrad" x1="950" y1="1110" x2="1310" y2="1295" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#0a7a41" stop-opacity="0.45"/>
|
||||||
|
<stop offset="0.45" stop-color="#086a39" stop-opacity="0.58"/>
|
||||||
|
<stop offset="1" stop-color="#044f2c" stop-opacity="0.70"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<clipPath id="borderRingClip" clipPathUnits="userSpaceOnUse">
|
||||||
|
<path d="M 888.0 1477.5 L 867.0 1477.5 L 865.0 1475.5 L 854.0 1473.5 L 812.0 1451.5 L 811.0 1449.5 L 798.0 1443.5 L 720.0 1397.5 L 644.0 1347.5 L 576.0 1299.5 L 563.0 1289.5 L 545.5 1269.0 L 541.5 1257.0 L 540.5 1245.0 L 541.5 669.0 L 545.5 658.0 L 559.0 641.5 L 572.0 633.5 L 589.0 629.5 L 591.0 627.5 L 604.0 625.5 L 606.0 623.5 L 619.0 621.5 L 646.0 613.5 L 652.0 613.5 L 654.0 611.5 L 676.0 607.5 L 678.0 605.5 L 684.0 605.5 L 694.0 601.5 L 700.0 601.5 L 751.0 587.5 L 757.0 587.5 L 759.0 585.5 L 765.0 585.5 L 767.0 583.5 L 773.0 583.5 L 783.0 579.5 L 789.0 579.5 L 791.0 577.5 L 805.0 575.5 L 807.0 573.5 L 813.0 573.5 L 880.0 555.5 L 886.0 555.5 L 888.0 553.5 L 894.0 553.5 L 962.0 535.5 L 968.0 535.5 L 970.0 533.5 L 984.0 531.5 L 987.0 529.5 L 1000.0 527.5 L 1002.0 525.5 L 1008.0 525.5 L 1010.0 523.5 L 1032.0 519.5 L 1034.0 517.5 L 1040.0 517.5 L 1042.0 515.5 L 1064.0 511.5 L 1066.0 509.5 L 1072.0 509.5 L 1090.0 503.5 L 1096.0 503.5 L 1098.0 501.5 L 1104.0 501.5 L 1107.0 499.5 L 1120.0 497.5 L 1122.0 495.5 L 1136.0 493.5 L 1138.0 491.5 L 1144.0 491.5 L 1147.0 489.5 L 1166.0 489.5 L 1175.0 491.5 L 1222.0 513.5 L 1227.0 517.5 L 1245.0 525.5 L 1246.0 527.5 L 1253.0 529.5 L 1273.0 541.5 L 1287.0 547.5 L 1288.0 549.5 L 1291.0 549.5 L 1292.0 551.5 L 1329.0 569.5 L 1330.0 571.5 L 1441.0 629.5 L 1482.0 655.5 L 1497.5 675.0 L 1503.5 692.0 L 1503.5 1264.0 L 1495.5 1281.0 L 1486.0 1291.5 L 1475.0 1299.5 L 1455.0 1307.5 L 1437.0 1311.5 L 1435.0 1313.5 L 1405.0 1321.5 L 1403.0 1323.5 L 1398.0 1323.5 L 1396.0 1325.5 L 1365.0 1333.5 L 1363.0 1335.5 L 1351.0 1337.5 L 1330.0 1345.5 L 1318.0 1347.5 L 1303.0 1353.5 L 1278.0 1359.5 L 1263.0 1365.5 L 1258.0 1365.5 L 1243.0 1371.5 L 1225.0 1375.5 L 1203.0 1383.5 L 1198.0 1383.5 L 1196.0 1385.5 L 1172.0 1391.5 L 1170.0 1393.5 L 1165.0 1393.5 L 1143.0 1401.5 L 1138.0 1401.5 L 1130.0 1405.5 L 1118.0 1407.5 L 1116.0 1409.5 L 1046.0 1429.5 L 1031.0 1435.5 L 1006.0 1441.5 L 991.0 1447.5 L 986.0 1447.5 L 971.0 1453.5 L 966.0 1453.5 L 964.0 1455.5 L 939.0 1461.5 L 937.0 1463.5 L 932.0 1463.5 L 930.0 1465.5 Z M 892.5 1411.0 L 923.0 1403.5 L 925.0 1401.5 L 930.0 1401.5 L 932.0 1399.5 L 937.0 1399.5 L 939.0 1397.5 L 999.0 1381.5 L 1001.0 1379.5 L 1006.0 1379.5 L 1008.0 1377.5 L 1020.0 1375.5 L 1022.0 1373.5 L 1027.0 1373.5 L 1029.0 1371.5 L 1034.0 1371.5 L 1050.0 1365.5 L 1055.0 1365.5 L 1063.0 1361.5 L 1089.0 1355.5 L 1104.0 1349.5 L 1109.0 1349.5 L 1111.0 1347.5 L 1136.0 1341.5 L 1138.0 1339.5 L 1157.0 1335.5 L 1159.0 1333.5 L 1238.0 1311.5 L 1240.0 1309.5 L 1245.0 1309.5 L 1254.0 1305.5 L 1326.0 1285.5 L 1328.0 1283.5 L 1408.0 1261.5 L 1425.0 1255.5 L 1437.5 1245.0 L 1441.5 1239.0 L 1442.5 1232.0 L 1442.5 760.0 L 1441.5 706.0 L 1433.0 693.5 L 1422.0 689.5 L 1403.0 691.5 L 1388.0 695.5 L 1368.0 697.5 L 1365.0 699.5 L 1344.0 701.5 L 1342.0 703.5 L 1308.0 707.5 L 1294.0 711.5 L 1285.0 711.5 L 1282.0 713.5 L 1262.0 715.5 L 1248.0 719.5 L 1215.0 723.5 L 1212.0 725.5 L 1170.0 731.5 L 1156.0 735.5 L 1147.0 735.5 L 1144.0 737.5 L 1135.0 737.5 L 1132.0 739.5 L 1124.0 739.5 L 1098.0 745.5 L 1055.0 751.5 L 1041.0 755.5 L 1021.0 757.5 L 1007.0 761.5 L 1000.0 761.5 L 997.0 763.5 L 977.0 765.5 L 974.0 767.5 L 953.0 769.5 L 950.0 771.5 L 942.0 771.5 L 939.0 773.5 L 931.0 773.5 L 905.0 779.5 L 896.0 779.5 L 881.0 783.5 L 849.0 787.5 L 846.0 789.5 L 805.0 795.5 L 791.0 799.5 L 771.0 801.5 L 761.0 805.5 L 751.5 814.0 L 745.5 827.0 L 745.5 1333.0 L 786.0 1359.5 L 809.0 1371.5 L 861.0 1403.5 L 879.0 1411.5 Z" clip-rule="evenodd"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="panelClip" clipPathUnits="userSpaceOnUse">
|
||||||
|
<path d="M 901.0 1371.5 L 893.0 1371.5 L 877.0 1365.5 L 808.0 1329.5 L 791.5 1318.0 L 791.5 847.0 L 793.5 842.0 L 803.0 833.5 L 818.0 829.5 L 827.0 829.5 L 841.0 825.5 L 850.0 825.5 L 853.0 823.5 L 874.0 821.5 L 877.0 819.5 L 909.0 815.5 L 912.0 813.5 L 932.0 811.5 L 946.0 807.5 L 955.0 807.5 L 969.0 803.5 L 977.0 803.5 L 980.0 801.5 L 1012.0 797.5 L 1015.0 795.5 L 1047.0 791.5 L 1050.0 789.5 L 1059.0 789.5 L 1073.0 785.5 L 1082.0 785.5 L 1085.0 783.5 L 1128.0 777.5 L 1131.0 775.5 L 1140.0 775.5 L 1143.0 773.5 L 1175.0 769.5 L 1189.0 765.5 L 1198.0 765.5 L 1201.0 763.5 L 1234.0 759.5 L 1237.0 757.5 L 1279.0 751.5 L 1282.0 749.5 L 1291.0 749.5 L 1294.0 747.5 L 1302.0 747.5 L 1352.0 737.5 L 1401.0 730.5 L 1401.5 1213.0 L 1397.5 1223.0 L 1389.0 1231.5 L 1373.0 1237.5 L 1368.0 1237.5 L 1366.0 1239.5 L 1361.0 1239.5 L 1359.0 1241.5 L 1333.0 1247.5 L 1324.0 1251.5 L 1319.0 1251.5 L 1317.0 1253.5 L 1312.0 1253.5 L 1310.0 1255.5 L 1285.0 1261.5 L 1283.0 1263.5 L 1277.0 1263.5 L 1275.0 1265.5 L 1256.0 1269.5 L 1241.0 1275.5 L 1236.0 1275.5 L 1187.0 1289.5 L 1185.0 1291.5 L 1165.0 1295.5 L 1163.0 1297.5 L 1158.0 1297.5 L 1149.0 1301.5 L 1144.0 1301.5 L 1142.0 1303.5 L 1109.0 1311.5 L 1107.0 1313.5 L 1088.0 1317.5 L 1086.0 1319.5 L 1067.0 1323.5 L 1058.0 1327.5 L 1052.0 1327.5 L 1050.0 1329.5 L 1015.0 1339.5 L 1010.0 1339.5 L 1008.0 1341.5 L 975.0 1349.5 L 973.0 1351.5 L 968.0 1351.5 L 959.0 1355.5 L 954.0 1355.5 L 952.0 1357.5 L 947.0 1357.5 Z M 906.5 1279.0 L 957.0 1265.5 L 979.0 1261.5 L 981.0 1259.5 L 986.0 1259.5 L 997.0 1255.5 L 1027.0 1249.5 L 1029.5 1247.0 L 1029.5 1100.0 L 1032.0 1097.5 L 1037.0 1097.5 L 1040.0 1095.5 L 1056.0 1093.5 L 1058.0 1091.5 L 1064.0 1091.5 L 1076.0 1087.5 L 1083.0 1087.5 L 1111.0 1079.5 L 1145.0 1073.5 L 1147.0 1071.5 L 1165.0 1068.5 L 1166.5 1074.0 L 1165.5 1226.0 L 1167.0 1227.5 L 1232.0 1211.5 L 1234.0 1209.5 L 1240.0 1209.5 L 1250.0 1205.5 L 1264.0 1203.5 L 1266.0 1201.5 L 1272.0 1201.5 L 1282.0 1197.5 L 1288.0 1197.5 L 1291.5 1195.0 L 1291.5 925.0 L 1290.5 812.0 L 1289.0 810.5 L 1279.0 813.5 L 1271.0 813.5 L 1269.0 815.5 L 1240.0 819.5 L 1237.0 821.5 L 1218.0 823.5 L 1205.0 827.5 L 1187.0 829.5 L 1184.0 831.5 L 1176.0 831.5 L 1165.5 835.0 L 1165.5 964.0 L 1164.0 965.5 L 1111.0 975.5 L 1108.0 977.5 L 1101.0 977.5 L 1098.0 979.5 L 1091.0 979.5 L 1088.0 981.5 L 1081.0 981.5 L 1078.0 983.5 L 1071.0 983.5 L 1068.0 985.5 L 1061.0 985.5 L 1058.0 987.5 L 1039.0 989.5 L 1036.0 991.5 L 1028.5 990.0 L 1029.5 857.0 L 1028.0 855.5 L 1001.0 859.5 L 998.0 861.5 L 990.0 861.5 L 987.0 863.5 L 978.0 863.5 L 965.0 867.5 L 956.0 867.5 L 943.0 871.5 L 934.0 871.5 L 921.0 875.5 L 900.0 877.5 L 897.5 880.0 L 899.5 1279.0 Z" clip-rule="evenodd"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<path d="M 888.0 1477.5 L 867.0 1477.5 L 865.0 1475.5 L 854.0 1473.5 L 812.0 1451.5 L 811.0 1449.5 L 798.0 1443.5 L 720.0 1397.5 L 644.0 1347.5 L 576.0 1299.5 L 563.0 1289.5 L 545.5 1269.0 L 541.5 1257.0 L 540.5 1245.0 L 541.5 669.0 L 545.5 658.0 L 559.0 641.5 L 572.0 633.5 L 589.0 629.5 L 591.0 627.5 L 604.0 625.5 L 606.0 623.5 L 619.0 621.5 L 646.0 613.5 L 652.0 613.5 L 654.0 611.5 L 676.0 607.5 L 678.0 605.5 L 684.0 605.5 L 694.0 601.5 L 700.0 601.5 L 751.0 587.5 L 757.0 587.5 L 759.0 585.5 L 765.0 585.5 L 767.0 583.5 L 773.0 583.5 L 783.0 579.5 L 789.0 579.5 L 791.0 577.5 L 805.0 575.5 L 807.0 573.5 L 813.0 573.5 L 880.0 555.5 L 886.0 555.5 L 888.0 553.5 L 894.0 553.5 L 962.0 535.5 L 968.0 535.5 L 970.0 533.5 L 984.0 531.5 L 987.0 529.5 L 1000.0 527.5 L 1002.0 525.5 L 1008.0 525.5 L 1010.0 523.5 L 1032.0 519.5 L 1034.0 517.5 L 1040.0 517.5 L 1042.0 515.5 L 1064.0 511.5 L 1066.0 509.5 L 1072.0 509.5 L 1090.0 503.5 L 1096.0 503.5 L 1098.0 501.5 L 1104.0 501.5 L 1107.0 499.5 L 1120.0 497.5 L 1122.0 495.5 L 1136.0 493.5 L 1138.0 491.5 L 1144.0 491.5 L 1147.0 489.5 L 1166.0 489.5 L 1175.0 491.5 L 1222.0 513.5 L 1227.0 517.5 L 1245.0 525.5 L 1246.0 527.5 L 1253.0 529.5 L 1273.0 541.5 L 1287.0 547.5 L 1288.0 549.5 L 1291.0 549.5 L 1292.0 551.5 L 1329.0 569.5 L 1330.0 571.5 L 1441.0 629.5 L 1482.0 655.5 L 1497.5 675.0 L 1503.5 692.0 L 1503.5 1264.0 L 1495.5 1281.0 L 1486.0 1291.5 L 1475.0 1299.5 L 1455.0 1307.5 L 1437.0 1311.5 L 1435.0 1313.5 L 1405.0 1321.5 L 1403.0 1323.5 L 1398.0 1323.5 L 1396.0 1325.5 L 1365.0 1333.5 L 1363.0 1335.5 L 1351.0 1337.5 L 1330.0 1345.5 L 1318.0 1347.5 L 1303.0 1353.5 L 1278.0 1359.5 L 1263.0 1365.5 L 1258.0 1365.5 L 1243.0 1371.5 L 1225.0 1375.5 L 1203.0 1383.5 L 1198.0 1383.5 L 1196.0 1385.5 L 1172.0 1391.5 L 1170.0 1393.5 L 1165.0 1393.5 L 1143.0 1401.5 L 1138.0 1401.5 L 1130.0 1405.5 L 1118.0 1407.5 L 1116.0 1409.5 L 1046.0 1429.5 L 1031.0 1435.5 L 1006.0 1441.5 L 991.0 1447.5 L 986.0 1447.5 L 971.0 1453.5 L 966.0 1453.5 L 964.0 1455.5 L 939.0 1461.5 L 937.0 1463.5 L 932.0 1463.5 L 930.0 1465.5 Z M 892.5 1411.0 L 923.0 1403.5 L 925.0 1401.5 L 930.0 1401.5 L 932.0 1399.5 L 937.0 1399.5 L 939.0 1397.5 L 999.0 1381.5 L 1001.0 1379.5 L 1006.0 1379.5 L 1008.0 1377.5 L 1020.0 1375.5 L 1022.0 1373.5 L 1027.0 1373.5 L 1029.0 1371.5 L 1034.0 1371.5 L 1050.0 1365.5 L 1055.0 1365.5 L 1063.0 1361.5 L 1089.0 1355.5 L 1104.0 1349.5 L 1109.0 1349.5 L 1111.0 1347.5 L 1136.0 1341.5 L 1138.0 1339.5 L 1157.0 1335.5 L 1159.0 1333.5 L 1238.0 1311.5 L 1240.0 1309.5 L 1245.0 1309.5 L 1254.0 1305.5 L 1326.0 1285.5 L 1328.0 1283.5 L 1408.0 1261.5 L 1425.0 1255.5 L 1437.5 1245.0 L 1441.5 1239.0 L 1442.5 1232.0 L 1442.5 760.0 L 1441.5 706.0 L 1433.0 693.5 L 1422.0 689.5 L 1403.0 691.5 L 1388.0 695.5 L 1368.0 697.5 L 1365.0 699.5 L 1344.0 701.5 L 1342.0 703.5 L 1308.0 707.5 L 1294.0 711.5 L 1285.0 711.5 L 1282.0 713.5 L 1262.0 715.5 L 1248.0 719.5 L 1215.0 723.5 L 1212.0 725.5 L 1170.0 731.5 L 1156.0 735.5 L 1147.0 735.5 L 1144.0 737.5 L 1135.0 737.5 L 1132.0 739.5 L 1124.0 739.5 L 1098.0 745.5 L 1055.0 751.5 L 1041.0 755.5 L 1021.0 757.5 L 1007.0 761.5 L 1000.0 761.5 L 997.0 763.5 L 977.0 765.5 L 974.0 767.5 L 953.0 769.5 L 950.0 771.5 L 942.0 771.5 L 939.0 773.5 L 931.0 773.5 L 905.0 779.5 L 896.0 779.5 L 881.0 783.5 L 849.0 787.5 L 846.0 789.5 L 805.0 795.5 L 791.0 799.5 L 771.0 801.5 L 761.0 805.5 L 751.5 814.0 L 745.5 827.0 L 745.5 1333.0 L 786.0 1359.5 L 809.0 1371.5 L 861.0 1403.5 L 879.0 1411.5 Z" fill="url(#borderGrad)" fill-rule="evenodd"/>
|
||||||
|
<rect width="2048" height="2048" fill="url(#borderGlow)" clip-path="url(#borderRingClip)"/>
|
||||||
|
|
||||||
|
<path d="M 740.0 749.5 L 729.0 749.5 L 717.0 743.5 L 596.0 675.5 L 591.0 672.5 L 589.5 669.0 L 688.0 643.5 L 694.0 643.5 L 697.0 641.5 L 785.0 621.5 L 796.0 617.5 L 802.0 617.5 L 812.0 613.5 L 818.0 613.5 L 877.0 597.5 L 883.0 597.5 L 910.0 589.5 L 916.0 589.5 L 975.0 573.5 L 981.0 573.5 L 1008.0 565.5 L 1014.0 565.5 L 1057.0 553.5 L 1063.0 553.5 L 1098.0 543.5 L 1145.0 533.5 L 1148.0 531.5 L 1156.0 531.5 L 1195.5 554.0 L 1119.0 573.5 L 1079.0 581.5 L 1052.0 589.5 L 1029.0 593.5 L 1026.0 595.5 L 995.0 601.5 L 950.0 613.5 L 944.0 613.5 L 941.0 615.5 L 909.0 621.5 L 906.0 623.5 L 773.0 653.5 L 751.0 659.5 L 743.5 666.0 L 746.0 669.5 L 757.0 673.5 L 777.0 673.5 L 1227.0 572.5 L 1233.0 573.5 L 1252.0 585.5 L 1269.0 593.5 L 1269.5 596.0 L 1052.0 639.5 L 1039.0 643.5 L 1022.0 645.5 L 1019.0 647.5 L 883.0 673.5 L 880.0 675.5 L 834.0 683.5 L 831.0 685.5 L 810.0 689.5 L 803.5 694.0 L 803.5 698.0 L 811.0 703.5 L 839.0 703.5 L 955.0 679.5 L 1302.0 614.5 L 1305.0 614.5 L 1353.5 640.0 L 1353.0 642.5 L 1340.0 643.5 L 1337.0 645.5 L 1329.0 645.5 L 1325.0 647.5 L 1282.0 653.5 L 1278.0 655.5 L 1269.0 655.5 L 1266.0 657.5 L 1235.0 661.5 L 1232.0 663.5 L 1224.0 663.5 L 1209.0 667.5 L 1177.0 671.5 L 1174.0 673.5 L 1166.0 673.5 L 1094.0 687.5 L 1063.0 691.5 L 1060.0 693.5 L 1052.0 693.5 L 1049.0 695.5 L 1041.0 695.5 L 1037.0 697.5 L 1029.0 697.5 L 1025.0 699.5 L 1017.0 699.5 L 1002.0 703.5 L 960.0 709.5 L 956.0 711.5 L 948.0 711.5 L 945.0 713.5 L 879.0 723.5 L 875.0 725.5 L 834.0 731.5 L 819.0 735.5 L 811.0 735.5 L 785.0 741.5 L 777.0 741.5 Z" fill="url(#topGrad)"/>
|
||||||
|
<path d="M 740.0 749.5 L 729.0 749.5 L 717.0 743.5 L 596.0 675.5 L 591.0 672.5 L 589.5 669.0 L 688.0 643.5 L 694.0 643.5 L 697.0 641.5 L 785.0 621.5 L 796.0 617.5 L 802.0 617.5 L 812.0 613.5 L 818.0 613.5 L 877.0 597.5 L 883.0 597.5 L 910.0 589.5 L 916.0 589.5 L 975.0 573.5 L 981.0 573.5 L 1008.0 565.5 L 1014.0 565.5 L 1057.0 553.5 L 1063.0 553.5 L 1098.0 543.5 L 1145.0 533.5 L 1148.0 531.5 L 1156.0 531.5 L 1195.5 554.0 L 1119.0 573.5 L 1079.0 581.5 L 1052.0 589.5 L 1029.0 593.5 L 1026.0 595.5 L 995.0 601.5 L 950.0 613.5 L 944.0 613.5 L 941.0 615.5 L 909.0 621.5 L 906.0 623.5 L 773.0 653.5 L 751.0 659.5 L 743.5 666.0 L 746.0 669.5 L 757.0 673.5 L 777.0 673.5 L 1227.0 572.5 L 1233.0 573.5 L 1252.0 585.5 L 1269.0 593.5 L 1269.5 596.0 L 1052.0 639.5 L 1039.0 643.5 L 1022.0 645.5 L 1019.0 647.5 L 883.0 673.5 L 880.0 675.5 L 834.0 683.5 L 831.0 685.5 L 810.0 689.5 L 803.5 694.0 L 803.5 698.0 L 811.0 703.5 L 839.0 703.5 L 955.0 679.5 L 1302.0 614.5 L 1305.0 614.5 L 1353.5 640.0 L 1353.0 642.5 L 1340.0 643.5 L 1337.0 645.5 L 1329.0 645.5 L 1325.0 647.5 L 1282.0 653.5 L 1278.0 655.5 L 1269.0 655.5 L 1266.0 657.5 L 1235.0 661.5 L 1232.0 663.5 L 1224.0 663.5 L 1209.0 667.5 L 1177.0 671.5 L 1174.0 673.5 L 1166.0 673.5 L 1094.0 687.5 L 1063.0 691.5 L 1060.0 693.5 L 1052.0 693.5 L 1049.0 695.5 L 1041.0 695.5 L 1037.0 697.5 L 1029.0 697.5 L 1025.0 699.5 L 1017.0 699.5 L 1002.0 703.5 L 960.0 709.5 L 956.0 711.5 L 948.0 711.5 L 945.0 713.5 L 879.0 723.5 L 875.0 725.5 L 834.0 731.5 L 819.0 735.5 L 811.0 735.5 L 785.0 741.5 L 777.0 741.5 Z" fill="none" stroke="#d6ff90" stroke-opacity="0.08" stroke-width="2" stroke-linejoin="round"/>
|
||||||
|
<path d="M 778.0 673.5 L 756.0 673.5 L 752.5 672.0 L 772.5 672.0 L 752.0 669.5 L 748.0 667.5 L 747.5 665.0 L 759.0 659.5 L 773.0 657.5 L 773.0 655.5 L 769.5 655.0 L 773.0 653.5 L 777.5 654.0 L 775.0 655.5 L 782.0 655.5 L 784.0 653.5 L 807.0 649.5 L 809.0 647.5 L 815.0 647.5 L 824.5 644.0 L 791.0 650.5 L 789.0 652.5 L 778.5 653.0 L 781.0 651.5 L 932.0 617.5 L 952.0 611.5 L 967.0 609.5 L 978.0 605.5 L 1168.0 561.5 L 1179.0 557.5 L 1184.5 558.0 L 1182.0 559.5 L 1188.0 559.5 L 1195.0 557.5 L 1196.0 553.5 L 1222.0 567.5 L 1223.5 571.0 L 1219.0 572.5 L 1230.5 572.0 Z" fill="url(#stripeGrad)" opacity="0.96"/>
|
||||||
|
<path d="M 833.0 704.5 L 811.0 703.5 L 808.5 702.0 L 811.5 700.0 L 807.5 699.0 L 806.5 695.0 L 811.0 691.5 L 834.0 687.5 L 836.0 685.5 L 853.0 683.5 L 854.0 681.5 L 807.5 691.0 L 824.0 685.5 L 1048.0 641.5 L 1051.0 639.5 L 1058.0 639.5 L 1071.0 635.5 L 1087.0 633.5 L 1090.0 631.5 L 1127.0 625.5 L 1130.0 623.5 L 1176.0 615.5 L 1258.0 597.5 L 1264.5 598.0 L 1250.5 601.0 L 1270.0 599.5 L 1269.0 597.5 L 1264.5 597.0 L 1271.0 594.5 L 1273.0 597.5 L 1299.5 612.0 L 1289.0 615.5 L 1269.5 618.0 L 1295.5 616.0 L 1292.0 617.5 L 1230.0 627.5 L 1162.0 641.5 L 1155.0 641.5 L 1141.0 645.5 L 1134.0 645.5 L 1068.0 659.5 L 976.0 675.5 L 973.0 677.5 L 966.0 677.5 Z" fill="url(#stripeGrad)" opacity="0.96"/>
|
||||||
|
|
||||||
|
<path d="M 680.0 1300.5 L 634.0 1265.5 L 599.0 1241.5 L 588.5 1232.0 L 587.5 1228.0 L 588.0 670.5 L 592.5 674.0 L 592.0 675.5 L 594.0 674.5 L 596.5 676.0 L 596.0 677.5 L 598.0 676.5 L 599.5 678.0 L 598.5 679.0 L 603.0 679.5 L 603.0 681.5 L 605.0 680.5 L 606.0 683.5 L 608.0 682.5 L 614.0 685.5 L 691.0 728.5 L 696.0 731.5 L 696.0 733.5 L 698.0 732.5 L 710.0 740.5 L 721.0 745.5 L 721.0 747.5 L 723.0 746.5 L 725.0 749.5 L 726.0 748.5 L 728.5 750.0 L 723.0 753.5 L 711.0 757.5 L 694.5 772.0 L 685.5 787.0 L 681.5 800.0 L 681.5 1299.0 Z" fill="url(#leftGrad)"/>
|
||||||
|
|
||||||
|
<path d="M 901.0 1371.5 L 893.0 1371.5 L 877.0 1365.5 L 808.0 1329.5 L 791.5 1318.0 L 791.5 847.0 L 793.5 842.0 L 803.0 833.5 L 818.0 829.5 L 827.0 829.5 L 841.0 825.5 L 850.0 825.5 L 853.0 823.5 L 874.0 821.5 L 877.0 819.5 L 909.0 815.5 L 912.0 813.5 L 932.0 811.5 L 946.0 807.5 L 955.0 807.5 L 969.0 803.5 L 977.0 803.5 L 980.0 801.5 L 1012.0 797.5 L 1015.0 795.5 L 1047.0 791.5 L 1050.0 789.5 L 1059.0 789.5 L 1073.0 785.5 L 1082.0 785.5 L 1085.0 783.5 L 1128.0 777.5 L 1131.0 775.5 L 1140.0 775.5 L 1143.0 773.5 L 1175.0 769.5 L 1189.0 765.5 L 1198.0 765.5 L 1201.0 763.5 L 1234.0 759.5 L 1237.0 757.5 L 1279.0 751.5 L 1282.0 749.5 L 1291.0 749.5 L 1294.0 747.5 L 1302.0 747.5 L 1352.0 737.5 L 1401.0 730.5 L 1401.5 1213.0 L 1397.5 1223.0 L 1389.0 1231.5 L 1373.0 1237.5 L 1368.0 1237.5 L 1366.0 1239.5 L 1361.0 1239.5 L 1359.0 1241.5 L 1333.0 1247.5 L 1324.0 1251.5 L 1319.0 1251.5 L 1317.0 1253.5 L 1312.0 1253.5 L 1310.0 1255.5 L 1285.0 1261.5 L 1283.0 1263.5 L 1277.0 1263.5 L 1275.0 1265.5 L 1256.0 1269.5 L 1241.0 1275.5 L 1236.0 1275.5 L 1187.0 1289.5 L 1185.0 1291.5 L 1165.0 1295.5 L 1163.0 1297.5 L 1158.0 1297.5 L 1149.0 1301.5 L 1144.0 1301.5 L 1142.0 1303.5 L 1109.0 1311.5 L 1107.0 1313.5 L 1088.0 1317.5 L 1086.0 1319.5 L 1067.0 1323.5 L 1058.0 1327.5 L 1052.0 1327.5 L 1050.0 1329.5 L 1015.0 1339.5 L 1010.0 1339.5 L 1008.0 1341.5 L 975.0 1349.5 L 973.0 1351.5 L 968.0 1351.5 L 959.0 1355.5 L 954.0 1355.5 L 952.0 1357.5 L 947.0 1357.5 Z M 906.5 1279.0 L 957.0 1265.5 L 979.0 1261.5 L 981.0 1259.5 L 986.0 1259.5 L 997.0 1255.5 L 1027.0 1249.5 L 1029.5 1247.0 L 1029.5 1100.0 L 1032.0 1097.5 L 1037.0 1097.5 L 1040.0 1095.5 L 1056.0 1093.5 L 1058.0 1091.5 L 1064.0 1091.5 L 1076.0 1087.5 L 1083.0 1087.5 L 1111.0 1079.5 L 1145.0 1073.5 L 1147.0 1071.5 L 1165.0 1068.5 L 1166.5 1074.0 L 1165.5 1226.0 L 1167.0 1227.5 L 1232.0 1211.5 L 1234.0 1209.5 L 1240.0 1209.5 L 1250.0 1205.5 L 1264.0 1203.5 L 1266.0 1201.5 L 1272.0 1201.5 L 1282.0 1197.5 L 1288.0 1197.5 L 1291.5 1195.0 L 1291.5 925.0 L 1290.5 812.0 L 1289.0 810.5 L 1279.0 813.5 L 1271.0 813.5 L 1269.0 815.5 L 1240.0 819.5 L 1237.0 821.5 L 1218.0 823.5 L 1205.0 827.5 L 1187.0 829.5 L 1184.0 831.5 L 1176.0 831.5 L 1165.5 835.0 L 1165.5 964.0 L 1164.0 965.5 L 1111.0 975.5 L 1108.0 977.5 L 1101.0 977.5 L 1098.0 979.5 L 1091.0 979.5 L 1088.0 981.5 L 1081.0 981.5 L 1078.0 983.5 L 1071.0 983.5 L 1068.0 985.5 L 1061.0 985.5 L 1058.0 987.5 L 1039.0 989.5 L 1036.0 991.5 L 1028.5 990.0 L 1029.5 857.0 L 1028.0 855.5 L 1001.0 859.5 L 998.0 861.5 L 990.0 861.5 L 987.0 863.5 L 978.0 863.5 L 965.0 867.5 L 956.0 867.5 L 943.0 871.5 L 934.0 871.5 L 921.0 875.5 L 900.0 877.5 L 897.5 880.0 L 899.5 1279.0 Z" fill="url(#frontGrad)" fill-rule="evenodd"/>
|
||||||
|
<rect width="2048" height="2048" fill="url(#frontHighlight)" clip-path="url(#panelClip)"/>
|
||||||
|
<path d="M 900.0 1278.0 L 1030.0 1247.0 L 1030.0 1100.0 L 1165.0 1071.0 L 1165.0 1227.0 L 1292.0 1195.0 L 1292.0 1250.0 L 980.0 1348.0 L 900.0 1316.0 Z" fill="url(#panelShadowGrad)"/>
|
||||||
|
<path d="M 901.0 1371.5 L 893.0 1371.5 L 877.0 1365.5 L 808.0 1329.5 L 791.5 1318.0 L 791.5 847.0 L 793.5 842.0 L 803.0 833.5 L 818.0 829.5 L 827.0 829.5 L 841.0 825.5 L 850.0 825.5 L 853.0 823.5 L 874.0 821.5 L 877.0 819.5 L 909.0 815.5 L 912.0 813.5 L 932.0 811.5 L 946.0 807.5 L 955.0 807.5 L 969.0 803.5 L 977.0 803.5 L 980.0 801.5 L 1012.0 797.5 L 1015.0 795.5 L 1047.0 791.5 L 1050.0 789.5 L 1059.0 789.5 L 1073.0 785.5 L 1082.0 785.5 L 1085.0 783.5 L 1128.0 777.5 L 1131.0 775.5 L 1140.0 775.5 L 1143.0 773.5 L 1175.0 769.5 L 1189.0 765.5 L 1198.0 765.5 L 1201.0 763.5 L 1234.0 759.5 L 1237.0 757.5 L 1279.0 751.5 L 1282.0 749.5 L 1291.0 749.5 L 1294.0 747.5 L 1302.0 747.5 L 1352.0 737.5 L 1401.0 730.5 L 1401.5 1213.0 L 1397.5 1223.0 L 1389.0 1231.5 L 1373.0 1237.5 L 1368.0 1237.5 L 1366.0 1239.5 L 1361.0 1239.5 L 1359.0 1241.5 L 1333.0 1247.5 L 1324.0 1251.5 L 1319.0 1251.5 L 1317.0 1253.5 L 1312.0 1253.5 L 1310.0 1255.5 L 1285.0 1261.5 L 1283.0 1263.5 L 1277.0 1263.5 L 1275.0 1265.5 L 1256.0 1269.5 L 1241.0 1275.5 L 1236.0 1275.5 L 1187.0 1289.5 L 1185.0 1291.5 L 1165.0 1295.5 L 1163.0 1297.5 L 1158.0 1297.5 L 1149.0 1301.5 L 1144.0 1301.5 L 1142.0 1303.5 L 1109.0 1311.5 L 1107.0 1313.5 L 1088.0 1317.5 L 1086.0 1319.5 L 1067.0 1323.5 L 1058.0 1327.5 L 1052.0 1327.5 L 1050.0 1329.5 L 1015.0 1339.5 L 1010.0 1339.5 L 1008.0 1341.5 L 975.0 1349.5 L 973.0 1351.5 L 968.0 1351.5 L 959.0 1355.5 L 954.0 1355.5 L 952.0 1357.5 L 947.0 1357.5 Z M 906.5 1279.0 L 957.0 1265.5 L 979.0 1261.5 L 981.0 1259.5 L 986.0 1259.5 L 997.0 1255.5 L 1027.0 1249.5 L 1029.5 1247.0 L 1029.5 1100.0 L 1032.0 1097.5 L 1037.0 1097.5 L 1040.0 1095.5 L 1056.0 1093.5 L 1058.0 1091.5 L 1064.0 1091.5 L 1076.0 1087.5 L 1083.0 1087.5 L 1111.0 1079.5 L 1145.0 1073.5 L 1147.0 1071.5 L 1165.0 1068.5 L 1166.5 1074.0 L 1165.5 1226.0 L 1167.0 1227.5 L 1232.0 1211.5 L 1234.0 1209.5 L 1240.0 1209.5 L 1250.0 1205.5 L 1264.0 1203.5 L 1266.0 1201.5 L 1272.0 1201.5 L 1282.0 1197.5 L 1288.0 1197.5 L 1291.5 1195.0 L 1291.5 925.0 L 1290.5 812.0 L 1289.0 810.5 L 1279.0 813.5 L 1271.0 813.5 L 1269.0 815.5 L 1240.0 819.5 L 1237.0 821.5 L 1218.0 823.5 L 1205.0 827.5 L 1187.0 829.5 L 1184.0 831.5 L 1176.0 831.5 L 1165.5 835.0 L 1165.5 964.0 L 1164.0 965.5 L 1111.0 975.5 L 1108.0 977.5 L 1101.0 977.5 L 1098.0 979.5 L 1091.0 979.5 L 1088.0 981.5 L 1081.0 981.5 L 1078.0 983.5 L 1071.0 983.5 L 1068.0 985.5 L 1061.0 985.5 L 1058.0 987.5 L 1039.0 989.5 L 1036.0 991.5 L 1028.5 990.0 L 1029.5 857.0 L 1028.0 855.5 L 1001.0 859.5 L 998.0 861.5 L 990.0 861.5 L 987.0 863.5 L 978.0 863.5 L 965.0 867.5 L 956.0 867.5 L 943.0 871.5 L 934.0 871.5 L 921.0 875.5 L 900.0 877.5 L 897.5 880.0 L 899.5 1279.0 Z" fill="none" stroke="#2fe56b" stroke-opacity="0.05" stroke-width="3" stroke-linejoin="round" fill-rule="evenodd"/>
|
||||||
|
|
||||||
|
<path d="M 888.0 1477.5 L 867.0 1477.5 L 865.0 1475.5 L 854.0 1473.5 L 812.0 1451.5 L 811.0 1449.5 L 798.0 1443.5 L 720.0 1397.5 L 644.0 1347.5 L 576.0 1299.5 L 563.0 1289.5 L 545.5 1269.0 L 541.5 1257.0 L 540.5 1245.0 L 541.5 669.0 L 545.5 658.0 L 559.0 641.5 L 572.0 633.5 L 589.0 629.5 L 591.0 627.5 L 604.0 625.5 L 606.0 623.5 L 619.0 621.5 L 646.0 613.5 L 652.0 613.5 L 654.0 611.5 L 676.0 607.5 L 678.0 605.5 L 684.0 605.5 L 694.0 601.5 L 700.0 601.5 L 751.0 587.5 L 757.0 587.5 L 759.0 585.5 L 765.0 585.5 L 767.0 583.5 L 773.0 583.5 L 783.0 579.5 L 789.0 579.5 L 791.0 577.5 L 805.0 575.5 L 807.0 573.5 L 813.0 573.5 L 880.0 555.5 L 886.0 555.5 L 888.0 553.5 L 894.0 553.5 L 962.0 535.5 L 968.0 535.5 L 970.0 533.5 L 984.0 531.5 L 987.0 529.5 L 1000.0 527.5 L 1002.0 525.5 L 1008.0 525.5 L 1010.0 523.5 L 1032.0 519.5 L 1034.0 517.5 L 1040.0 517.5 L 1042.0 515.5 L 1064.0 511.5 L 1066.0 509.5 L 1072.0 509.5 L 1090.0 503.5 L 1096.0 503.5 L 1098.0 501.5 L 1104.0 501.5 L 1107.0 499.5 L 1120.0 497.5 L 1122.0 495.5 L 1136.0 493.5 L 1138.0 491.5 L 1144.0 491.5 L 1147.0 489.5 L 1166.0 489.5 L 1175.0 491.5 L 1222.0 513.5 L 1227.0 517.5 L 1245.0 525.5 L 1246.0 527.5 L 1253.0 529.5 L 1273.0 541.5 L 1287.0 547.5 L 1288.0 549.5 L 1291.0 549.5 L 1292.0 551.5 L 1329.0 569.5 L 1330.0 571.5 L 1441.0 629.5 L 1482.0 655.5 L 1497.5 675.0 L 1503.5 692.0 L 1503.5 1264.0 L 1495.5 1281.0 L 1486.0 1291.5 L 1475.0 1299.5 L 1455.0 1307.5 L 1437.0 1311.5 L 1435.0 1313.5 L 1405.0 1321.5 L 1403.0 1323.5 L 1398.0 1323.5 L 1396.0 1325.5 L 1365.0 1333.5 L 1363.0 1335.5 L 1351.0 1337.5 L 1330.0 1345.5 L 1318.0 1347.5 L 1303.0 1353.5 L 1278.0 1359.5 L 1263.0 1365.5 L 1258.0 1365.5 L 1243.0 1371.5 L 1225.0 1375.5 L 1203.0 1383.5 L 1198.0 1383.5 L 1196.0 1385.5 L 1172.0 1391.5 L 1170.0 1393.5 L 1165.0 1393.5 L 1143.0 1401.5 L 1138.0 1401.5 L 1130.0 1405.5 L 1118.0 1407.5 L 1116.0 1409.5 L 1046.0 1429.5 L 1031.0 1435.5 L 1006.0 1441.5 L 991.0 1447.5 L 986.0 1447.5 L 971.0 1453.5 L 966.0 1453.5 L 964.0 1455.5 L 939.0 1461.5 L 937.0 1463.5 L 932.0 1463.5 L 930.0 1465.5 Z" fill="none" stroke="#00efc3" stroke-opacity="0.06" stroke-width="1.5" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,465 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
|
||||||
|
--color-bg: #f6f8f5;
|
||||||
|
--color-surface: #ffffff;
|
||||||
|
--color-surface-alt: #f1f4ef;
|
||||||
|
--color-border: #dce4d8;
|
||||||
|
--color-border-strong: #c7d2c2;
|
||||||
|
|
||||||
|
--color-text: #1f2a21;
|
||||||
|
--color-text-secondary: #5f6e62;
|
||||||
|
--color-text-muted: #7d8a80;
|
||||||
|
--color-text-on-primary: #ffffff;
|
||||||
|
|
||||||
|
--color-primary-700: #1c652f;
|
||||||
|
--color-primary-600: #2e7d32;
|
||||||
|
--color-primary-500: #3c8f42;
|
||||||
|
--color-primary-300: #a8d5a2;
|
||||||
|
--color-primary-100: #eaf5e8;
|
||||||
|
--color-accent-lime: #b7e36b;
|
||||||
|
|
||||||
|
--color-success: #2e7d32;
|
||||||
|
--color-warning: #b7791f;
|
||||||
|
--color-danger: #c0392b;
|
||||||
|
--color-info: #2f6fb3;
|
||||||
|
|
||||||
|
--radius-sm: 8px;
|
||||||
|
--radius-md: 10px;
|
||||||
|
--radius-lg: 14px;
|
||||||
|
|
||||||
|
--shadow-sm: 0 1px 2px rgb(16 24 18 / 6%);
|
||||||
|
--shadow-md: 0 6px 18px rgb(16 24 18 / 8%);
|
||||||
|
|
||||||
|
--space-1: 4px;
|
||||||
|
--space-2: 8px;
|
||||||
|
--space-3: 12px;
|
||||||
|
--space-4: 16px;
|
||||||
|
--space-5: 20px;
|
||||||
|
--space-6: 24px;
|
||||||
|
--space-8: 32px;
|
||||||
|
--space-10: 40px;
|
||||||
|
|
||||||
|
--font-family-sans:
|
||||||
|
Inter, 'Segoe UI', Roboto, system-ui, -apple-system, BlinkMacSystemFont, 'Helvetica Neue',
|
||||||
|
Arial, sans-serif;
|
||||||
|
--font-size-xs: 12px;
|
||||||
|
--font-size-sm: 13px;
|
||||||
|
--font-size-md: 14px;
|
||||||
|
--font-size-lg: 18px;
|
||||||
|
--font-size-xl: 24px;
|
||||||
|
|
||||||
|
--line-height-tight: 1.3;
|
||||||
|
--line-height-normal: 1.5;
|
||||||
|
--line-height-loose: 1.65;
|
||||||
|
|
||||||
|
--transition-fast: 160ms ease;
|
||||||
|
--page-bg-glow: rgb(183 227 107 / 14%);
|
||||||
|
--scrollbar-thumb: #a6b3a2;
|
||||||
|
--scrollbar-track: #e8ede5;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme='dark'] {
|
||||||
|
color-scheme: dark;
|
||||||
|
|
||||||
|
--color-bg: #101215;
|
||||||
|
--color-surface: #171a1f;
|
||||||
|
--color-surface-alt: #1d2229;
|
||||||
|
--color-border: #2c333d;
|
||||||
|
--color-border-strong: #3a4350;
|
||||||
|
|
||||||
|
--color-text: #ebeff3;
|
||||||
|
--color-text-secondary: #c5ccd4;
|
||||||
|
--color-text-muted: #95a0ad;
|
||||||
|
--color-text-on-primary: #08120a;
|
||||||
|
|
||||||
|
--color-primary-700: #5fb968;
|
||||||
|
--color-primary-600: #4ea758;
|
||||||
|
--color-primary-500: #3f9148;
|
||||||
|
--color-primary-300: #2e6a37;
|
||||||
|
--color-primary-100: #202822;
|
||||||
|
--color-accent-lime: #b7e36b;
|
||||||
|
|
||||||
|
--color-success: #5fb968;
|
||||||
|
--color-warning: #d0a34e;
|
||||||
|
--color-danger: #e07a7a;
|
||||||
|
--color-info: #6aa8de;
|
||||||
|
|
||||||
|
--shadow-sm: 0 1px 2px rgb(0 0 0 / 35%);
|
||||||
|
--shadow-md: 0 6px 18px rgb(0 0 0 / 40%);
|
||||||
|
|
||||||
|
--page-bg-glow: rgb(79 145 72 / 7%);
|
||||||
|
--scrollbar-thumb: #4f5763;
|
||||||
|
--scrollbar-track: #171b22;
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#app {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: var(--line-height-normal);
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-family-sans);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
color: var(--color-text);
|
||||||
|
background: radial-gradient(circle at top right, var(--page-bg-glow), transparent 36%), var(--color-bg);
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary-300) 38%, transparent);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-primary-700);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--color-primary-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
margin: 0 0 var(--space-4);
|
||||||
|
line-height: var(--line-height-tight);
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 var(--space-4);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
line-height: var(--line-height-loose);
|
||||||
|
}
|
||||||
|
|
||||||
|
:where(a, button, input, textarea, select, [tabindex]):focus-visible {
|
||||||
|
outline: 2px solid var(--color-primary-500);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* App shell */
|
||||||
|
.v-application {
|
||||||
|
font-family: var(--font-family-sans) !important;
|
||||||
|
color: var(--color-text) !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-main {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-app-bar {
|
||||||
|
background-color: var(--color-surface) !important;
|
||||||
|
border-bottom: 1px solid var(--color-border) !important;
|
||||||
|
box-shadow: var(--shadow-sm) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-navigation-drawer {
|
||||||
|
background-color: var(--color-surface-alt) !important;
|
||||||
|
border-right: 1px solid var(--color-border) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-navigation-drawer .v-list-item {
|
||||||
|
border-radius: var(--radius-md) !important;
|
||||||
|
margin: 2px var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-navigation-drawer .v-list-item:hover {
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary-100) 45%, var(--color-surface-alt) 55%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-navigation-drawer .v-list-item--active {
|
||||||
|
color: var(--color-primary-700) !important;
|
||||||
|
background-color: var(--color-primary-100) !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Surface components */
|
||||||
|
.v-card {
|
||||||
|
border: 1px solid var(--color-border) !important;
|
||||||
|
border-radius: var(--radius-lg) !important;
|
||||||
|
background-color: var(--color-surface) !important;
|
||||||
|
box-shadow: var(--shadow-sm) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-card-title {
|
||||||
|
color: var(--color-text) !important;
|
||||||
|
font-size: var(--font-size-lg) !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-card-text {
|
||||||
|
color: var(--color-text-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-footer {
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
background-color: color-mix(in srgb, var(--color-surface) 90%, var(--color-bg) 10%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.v-btn {
|
||||||
|
letter-spacing: 0 !important;
|
||||||
|
text-transform: none !important;
|
||||||
|
border-radius: var(--radius-md) !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-btn--variant-elevated,
|
||||||
|
.v-btn--variant-flat {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-btn--variant-elevated:not(.v-btn--disabled):hover,
|
||||||
|
.v-btn--variant-flat:not(.v-btn--disabled):hover {
|
||||||
|
box-shadow: var(--shadow-sm) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-btn.v-btn--variant-elevated:not(.v-btn--disabled) {
|
||||||
|
color: var(--color-text-on-primary) !important;
|
||||||
|
background-color: var(--color-primary-700) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-btn.v-btn--variant-elevated:not(.v-btn--disabled):hover {
|
||||||
|
background-color: var(--color-primary-600) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-btn--variant-outlined {
|
||||||
|
border-color: var(--color-border-strong) !important;
|
||||||
|
color: var(--color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inputs */
|
||||||
|
.v-input .v-field {
|
||||||
|
border-radius: var(--radius-md) !important;
|
||||||
|
background-color: var(--color-surface) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-input .v-field__outline {
|
||||||
|
--v-field-border-opacity: 1;
|
||||||
|
color: var(--color-border-strong) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-input.v-input--focused .v-field__outline {
|
||||||
|
color: var(--color-primary-600) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vuetify steuert den Fokuszustand bereits am Feld; verhindert doppelten Fokusrahmen im Input */
|
||||||
|
.v-input .v-field :is(input, textarea, select):focus-visible {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-label {
|
||||||
|
color: var(--color-text-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables and list-like content */
|
||||||
|
.v-table {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background-color: var(--color-surface) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-table thead th {
|
||||||
|
color: var(--color-text-secondary) !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
background-color: var(--color-surface-alt) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-table tbody tr:hover td {
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary-100) 35%, var(--color-surface) 65%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status helpers */
|
||||||
|
.hoard-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: 2px var(--space-2);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-status--success {
|
||||||
|
color: var(--color-success);
|
||||||
|
background-color: color-mix(in srgb, var(--color-success) 14%, var(--color-surface) 86%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-status--warning {
|
||||||
|
color: var(--color-warning);
|
||||||
|
background-color: color-mix(in srgb, var(--color-warning) 15%, var(--color-surface) 85%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-status--danger {
|
||||||
|
color: var(--color-danger);
|
||||||
|
background-color: color-mix(in srgb, var(--color-danger) 12%, var(--color-surface) 88%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-status--info {
|
||||||
|
color: var(--color-info);
|
||||||
|
background-color: color-mix(in srgb, var(--color-info) 12%, var(--color-surface) 88%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reusable layout helpers for file/productivity pages */
|
||||||
|
.hoard-panel {
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
background-color: var(--color-surface-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-list-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(220px, 2fr) minmax(120px, 1fr) minmax(100px, 1fr) minmax(120px, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 85%, white 15%);
|
||||||
|
transition: background-color var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-list-row:hover {
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary-100) 35%, var(--color-surface) 65%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-list-row.is-selected {
|
||||||
|
color: var(--color-primary-700);
|
||||||
|
background-color: var(--color-primary-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-meta {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-empty-state {
|
||||||
|
padding: var(--space-8) var(--space-6);
|
||||||
|
text-align: center;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-empty-state h2 {
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar refinement */
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar-track {
|
||||||
|
background: var(--scrollbar-track);
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--scrollbar-thumb);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
*::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: color-mix(in srgb, var(--scrollbar-thumb) 85%, black 15%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 960px) {
|
||||||
|
:root {
|
||||||
|
--hoard-mobile-safe-left: max(var(--space-2), env(safe-area-inset-left));
|
||||||
|
--hoard-mobile-safe-right: max(var(--space-2), env(safe-area-inset-right));
|
||||||
|
--hoard-mobile-safe-bottom: max(var(--space-3), env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-main {
|
||||||
|
padding-bottom: var(--hoard-mobile-safe-bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-btn {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-btn--icon.v-btn {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-navigation-drawer .v-list-item {
|
||||||
|
min-height: 48px;
|
||||||
|
margin: 4px var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-list-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--space-2);
|
||||||
|
align-items: start;
|
||||||
|
padding-block: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-toolbar {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: stretch;
|
||||||
|
padding: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-empty-state {
|
||||||
|
padding: var(--space-6) var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-navigation-drawer {
|
||||||
|
border-right: none !important;
|
||||||
|
border-top: 1px solid var(--color-border) !important;
|
||||||
|
padding-bottom: var(--hoard-mobile-safe-bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 600px) {
|
||||||
|
.hoard-toolbar {
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-list-row {
|
||||||
|
padding-inline: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-meta {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-1
@@ -1,12 +1,17 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
|
import './global.css'
|
||||||
|
import './styles/global/page-layouts.css'
|
||||||
|
import './styles/global/surface-patterns.css'
|
||||||
|
|
||||||
import App from './App.vue'
|
import App from './Layout.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
import vuetify from './plugins/vuetify'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
app.use(vuetify)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
import Home from '@/routes/Home.vue'
|
||||||
|
import NotFound from '@/routes/404NotFound.vue'
|
||||||
|
import Login from '@/routes/authentication/Login.vue'
|
||||||
|
import Impressum from '@/routes/Impressum.vue'
|
||||||
|
|
||||||
|
export enum Visibility {
|
||||||
|
Hidden,
|
||||||
|
Authenticated,
|
||||||
|
Unauthenticated,
|
||||||
|
Authorized,
|
||||||
|
Public,
|
||||||
|
Footer,
|
||||||
|
Route,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LayoutRoute {
|
||||||
|
path: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
icon: string
|
||||||
|
disableFooter?: boolean
|
||||||
|
visible: Visibility
|
||||||
|
visibilityRoute?: string | string[]
|
||||||
|
meta?: RouteRecordRaw
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kurzanleitung fuer Sidebar-Sichtbarkeit:
|
||||||
|
* - `Visibility.Public`: Eintrag ist immer in der Sidebar sichtbar.
|
||||||
|
* - `Visibility.Route`: Eintrag ist nur sichtbar, wenn die aktuelle URL im angegebenen Bereich liegt.
|
||||||
|
* - Ohne `visibilityRoute` wird automatisch `path` als Bereich verwendet.
|
||||||
|
* - Szenario 1: `path: '/dash'` -> sichtbar bei `/dash` und `/dash/*`.
|
||||||
|
* - Szenario 2: `visibilityRoute: '/admin'` -> Eintrag wird nur im Admin-Bereich gezeigt.
|
||||||
|
* - Szenario 3: `visibilityRoute: ['/dash', '/projects']` -> sichtbar in beiden Bereichen.
|
||||||
|
* - `Visibility.Footer` und `Visibility.Hidden`: nicht in der Sidebar sichtbar.
|
||||||
|
*/
|
||||||
|
export const routes: LayoutRoute[] = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Startseite',
|
||||||
|
description: 'Self-hosted Datei-Workspace für Hoard',
|
||||||
|
icon: 'mdi-home',
|
||||||
|
visible: Visibility.Public,
|
||||||
|
meta: {
|
||||||
|
name: 'Home',
|
||||||
|
path: '/',
|
||||||
|
component: Home,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
description: 'Logge dich ein',
|
||||||
|
icon: 'mdi-login',
|
||||||
|
visible: Visibility.Hidden,
|
||||||
|
meta: {
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: Login,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/impressum',
|
||||||
|
name: 'Impressum',
|
||||||
|
description: 'Impressum der Anwendung',
|
||||||
|
icon: 'mdi-file-document',
|
||||||
|
visible: Visibility.Footer,
|
||||||
|
meta: {
|
||||||
|
path: '/impressum',
|
||||||
|
name: 'Impressum',
|
||||||
|
component: Impressum,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/notFound',
|
||||||
|
name: 'Nicht gefunden',
|
||||||
|
description: 'Diese Seite wurde nicht gefunden',
|
||||||
|
icon: 'mdi-information-outline',
|
||||||
|
visible: Visibility.Hidden,
|
||||||
|
meta: { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
|
||||||
|
},
|
||||||
|
]
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import 'vuetify/styles'
|
||||||
|
import '@fontsource/roboto/100.css'
|
||||||
|
import '@fontsource/roboto/300.css'
|
||||||
|
import '@fontsource/roboto/400.css'
|
||||||
|
import '@fontsource/roboto/500.css'
|
||||||
|
import '@fontsource/roboto/700.css'
|
||||||
|
import '@fontsource/roboto/900.css'
|
||||||
|
import { createVuetify } from 'vuetify'
|
||||||
|
import * as components from 'vuetify/components'
|
||||||
|
import * as directives from 'vuetify/directives'
|
||||||
|
import '@mdi/font/css/materialdesignicons.css'
|
||||||
|
import { aliases, mdi } from 'vuetify/iconsets/mdi'
|
||||||
|
|
||||||
|
export default createVuetify({
|
||||||
|
components,
|
||||||
|
directives,
|
||||||
|
theme: {
|
||||||
|
defaultTheme: 'light',
|
||||||
|
themes: {
|
||||||
|
light: {
|
||||||
|
dark: false,
|
||||||
|
colors: {
|
||||||
|
primary: '#1C652F',
|
||||||
|
secondary: '#5F6E62',
|
||||||
|
background: '#F6F8F5',
|
||||||
|
surface: '#FFFFFF',
|
||||||
|
success: '#2E7D32',
|
||||||
|
warning: '#B7791F',
|
||||||
|
error: '#C0392B',
|
||||||
|
info: '#2F6FB3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
dark: true,
|
||||||
|
colors: {
|
||||||
|
primary: '#4EA758',
|
||||||
|
secondary: '#A7B0BC',
|
||||||
|
background: '#101215',
|
||||||
|
surface: '#171A1F',
|
||||||
|
success: '#5FB968',
|
||||||
|
warning: '#D0A34E',
|
||||||
|
error: '#E07A7A',
|
||||||
|
info: '#6AA8DE',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
defaultSet: 'mdi',
|
||||||
|
aliases,
|
||||||
|
sets: {
|
||||||
|
mdi,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
|
||||||
|
import { routes } from '@/plugins/routesLayout'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [],
|
routes: routes.filter((x) => x.meta !== undefined).map((x) => x.meta) as RouteRecordRaw[],
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export const
|
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import notFoundImage from '@/assets/images/404NotFound.png'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
function navigateBack() {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
router.back()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid class="not-found-page hoard-page hoard-page--centered">
|
||||||
|
<section class="not-found-shell hoard-panel hoard-shell-grid hoard-panel-gradient">
|
||||||
|
<div class="not-found-visual">
|
||||||
|
<div class="image-frame">
|
||||||
|
<img :src="notFoundImage" alt="Illustration für eine nicht gefundene Seite" class="not-found-image" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="not-found-content">
|
||||||
|
<p class="not-found-kicker hoard-kicker hoard-kicker--wide">Fehler 404</p>
|
||||||
|
<h1>Seite nicht gefunden</h1>
|
||||||
|
<p class="not-found-text">
|
||||||
|
Der Link ist ungültig oder die Seite wurde verschoben. Du kannst direkt zur
|
||||||
|
Startseite zurück oder die vorherige Ansicht öffnen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="not-found-actions hoard-action-row">
|
||||||
|
<v-btn color="primary" prepend-icon="mdi-home" to="/">Zur Startseite</v-btn>
|
||||||
|
<v-btn variant="outlined" prepend-icon="mdi-arrow-left" @click="navigateBack">Zurück</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.not-found-shell {
|
||||||
|
--hoard-shell-width: 980px;
|
||||||
|
--hoard-gradient-angle: 180deg;
|
||||||
|
--hoard-gradient-start: color-mix(in srgb, var(--color-surface) 94%, var(--color-primary-100) 6%);
|
||||||
|
--hoard-gradient-end: color-mix(in srgb, var(--color-surface) 82%, var(--color-surface-alt) 18%);
|
||||||
|
--hoard-gradient-end-stop: 100%;
|
||||||
|
|
||||||
|
grid-template-columns: minmax(260px, 1fr) minmax(320px, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-visual {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-frame {
|
||||||
|
width: min(100%, 360px);
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
padding: var(--space-4);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background-color: color-mix(in srgb, var(--color-surface-alt) 84%, var(--color-surface) 16%);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
font-size: clamp(1.8rem, 2vw + 1rem, 2.4rem);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-text {
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
max-width: 44ch;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-actions {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 960px) {
|
||||||
|
.not-found-shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-frame {
|
||||||
|
width: min(100%, 320px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-content {
|
||||||
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-actions {
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.not-found-actions .v-btn) {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 600px) {
|
||||||
|
.not-found-shell {
|
||||||
|
--hoard-shell-padding-block-mobile-xs: var(--space-4);
|
||||||
|
--hoard-shell-padding-inline-mobile-xs: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(1.5rem, 7vw, 1.9rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-text {
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-frame {
|
||||||
|
width: min(100%, 260px);
|
||||||
|
padding: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-actions {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.not-found-actions .v-btn) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,470 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const valueProps = [
|
||||||
|
{
|
||||||
|
icon: 'mdi-folder-multiple-outline',
|
||||||
|
title: 'Dateien zuerst',
|
||||||
|
text: 'Ordner, Dateiliste und Vorschau sind der Kern. Kein überladenes Dashboard-Gefühl.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'mdi-file-document-edit-outline',
|
||||||
|
title: 'Markdown direkt im Browser',
|
||||||
|
text: 'Dokumente bearbeiten, lesen und strukturieren ohne Tool-Wechsel.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'mdi-server-outline',
|
||||||
|
title: 'Self-hosted Kontrolle',
|
||||||
|
text: 'Dateimetadaten in PostgreSQL, Dateien in MinIO, alles auf deinem Server.',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const coreFeatures = [
|
||||||
|
{
|
||||||
|
icon: 'mdi-folder-open-outline',
|
||||||
|
title: 'Ordnernavigation wie gewohnt',
|
||||||
|
text: 'Schnell durch Verzeichnisse klicken, Inhalte erfassen und sauber organisieren.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'mdi-image-outline',
|
||||||
|
title: 'PDF- und Bildvorschau',
|
||||||
|
text: 'Dateien öffnen und direkt einsehen, ohne externe Viewer oder Downloads.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'mdi-account-lock-outline',
|
||||||
|
title: 'Klare Benutzerlogik',
|
||||||
|
text: 'Keine offene Registrierung. Accounts werden bewusst und kontrolliert verwaltet.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'mdi-lightning-bolt-outline',
|
||||||
|
title: 'Schlankes MVP-Setup',
|
||||||
|
text: 'Fokus auf das Wesentliche: stabil, wartbar und realistisch für Solo-Entwicklung.',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const workflowSteps = [
|
||||||
|
{
|
||||||
|
number: '01',
|
||||||
|
title: 'Anmelden',
|
||||||
|
text: 'Melde dich mit einem vorhandenen Konto an und starte direkt in deiner Dateiablage.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: '02',
|
||||||
|
title: 'Dateien strukturieren',
|
||||||
|
text: 'Lege Ordner an, lade Dateien hoch und halte deine Arbeitsbereiche aufgeräumt.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: '03',
|
||||||
|
title: 'Inhalte bearbeiten',
|
||||||
|
text: 'Markdown, Bilder und PDFs direkt in der App ansehen und bearbeiten.',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const techStack = ['Vue 3', 'ASP.NET Core', 'PostgreSQL', 'MinIO', 'md-editor-v3', 'Cookie Auth']
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid class="landing-page hoard-page">
|
||||||
|
<section class="hero hoard-panel hoard-panel-gradient">
|
||||||
|
<div class="hero-copy">
|
||||||
|
<p class="hero-kicker hoard-kicker">Self-hosted Datei-Workspace</p>
|
||||||
|
<h1>Hoard ist deine ruhige Startseite für Dateien, Ordner und Markdown.</h1>
|
||||||
|
<p class="hero-lead">
|
||||||
|
Eine einfache, Google-Drive-inspirierte Web-App für Teams, die volle Kontrolle über
|
||||||
|
Daten, Struktur und Workflow behalten wollen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="hero-actions hoard-action-row">
|
||||||
|
<v-btn color="primary" size="large" prepend-icon="mdi-login" to="/login">
|
||||||
|
Zum Login
|
||||||
|
</v-btn>
|
||||||
|
<v-btn variant="outlined" size="large" prepend-icon="mdi-file-document-outline" to="/impressum">
|
||||||
|
Mehr erfahren
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hero-tags">
|
||||||
|
<span class="hero-tag">Light-first UX</span>
|
||||||
|
<span class="hero-tag">Mehrbenutzerfähig</span>
|
||||||
|
<span class="hero-tag">Ohne SaaS-Abhängigkeit</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hero-preview hoard-panel">
|
||||||
|
<header class="preview-head">
|
||||||
|
<p class="preview-title">Beispielansicht</p>
|
||||||
|
<span class="preview-pill">Workspace</span>
|
||||||
|
</header>
|
||||||
|
<div class="preview-list">
|
||||||
|
<article class="preview-row">
|
||||||
|
<v-icon icon="mdi-folder-outline" size="18" />
|
||||||
|
<div>
|
||||||
|
<p class="row-title">Dokumentation</p>
|
||||||
|
<p class="row-meta">Ordner · vor 3 Tagen aktualisiert</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="preview-row">
|
||||||
|
<v-icon icon="mdi-file-document-outline" size="18" />
|
||||||
|
<div>
|
||||||
|
<p class="row-title">roadmap.md</p>
|
||||||
|
<p class="row-meta">Markdown · 18 KB</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="preview-row">
|
||||||
|
<v-icon icon="mdi-file-pdf-box" size="18" />
|
||||||
|
<div>
|
||||||
|
<p class="row-title">api-reference.pdf</p>
|
||||||
|
<p class="row-meta">PDF · 1.2 MB</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="value-grid">
|
||||||
|
<article v-for="item in valueProps" :key="item.title" class="value-card hoard-panel">
|
||||||
|
<v-icon :icon="item.icon" size="22" />
|
||||||
|
<h2>{{ item.title }}</h2>
|
||||||
|
<p>{{ item.text }}</p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="feature-section hoard-panel">
|
||||||
|
<header class="section-head">
|
||||||
|
<p class="section-kicker hoard-kicker">Für den Produktivalltag</p>
|
||||||
|
<h2>Weniger Tool-Chaos, mehr Fokus auf Inhalte</h2>
|
||||||
|
</header>
|
||||||
|
<div class="feature-grid">
|
||||||
|
<article v-for="feature in coreFeatures" :key="feature.title" class="feature-card">
|
||||||
|
<v-icon :icon="feature.icon" size="20" />
|
||||||
|
<h3>{{ feature.title }}</h3>
|
||||||
|
<p>{{ feature.text }}</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="workflow-section">
|
||||||
|
<header class="section-head">
|
||||||
|
<p class="section-kicker hoard-kicker">So funktioniert Hoard</p>
|
||||||
|
<h2>In drei klaren Schritten produktiv starten</h2>
|
||||||
|
</header>
|
||||||
|
<div class="workflow-grid">
|
||||||
|
<article v-for="step in workflowSteps" :key="step.number" class="workflow-card hoard-panel">
|
||||||
|
<p class="workflow-number">{{ step.number }}</p>
|
||||||
|
<h3>{{ step.title }}</h3>
|
||||||
|
<p>{{ step.text }}</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="stack-section hoard-panel">
|
||||||
|
<div class="stack-copy">
|
||||||
|
<p class="section-kicker hoard-kicker">Technische Basis</p>
|
||||||
|
<h2>Schlank gebaut für ein realistisches MVP</h2>
|
||||||
|
<p class="stack-text">
|
||||||
|
Hoard kombiniert einen modernen Frontend-Stack mit einem pragmatischen Backend-Setup,
|
||||||
|
damit Weiterentwicklung und Betrieb auch solo gut machbar bleiben.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="stack-list">
|
||||||
|
<span v-for="item in techStack" :key="item" class="stack-pill">{{ item }}</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.landing-page {
|
||||||
|
--hoard-page-width: 1180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
--hoard-gradient-angle: 120deg;
|
||||||
|
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 34%, var(--color-surface) 66%);
|
||||||
|
--hoard-gradient-end: var(--color-surface);
|
||||||
|
--hoard-gradient-end-stop: 52%;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
|
||||||
|
gap: var(--space-6);
|
||||||
|
padding: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-number,
|
||||||
|
.preview-title,
|
||||||
|
.row-title,
|
||||||
|
.row-meta,
|
||||||
|
.stack-text {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
max-width: 20ch;
|
||||||
|
font-size: clamp(2rem, 2.5vw + 1rem, 3rem);
|
||||||
|
line-height: 1.08;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-lead {
|
||||||
|
margin-bottom: var(--space-5);
|
||||||
|
max-width: 50ch;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 5px var(--space-3);
|
||||||
|
border: 1px solid var(--color-border-strong);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-preview {
|
||||||
|
align-self: center;
|
||||||
|
padding: var(--space-5);
|
||||||
|
width: 100%;
|
||||||
|
background-color: color-mix(in srgb, var(--color-surface-alt) 86%, var(--color-surface) 14%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-title {
|
||||||
|
color: var(--color-text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px var(--space-2);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--color-primary-700);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: var(--color-primary-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--space-3);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--color-border) 75%, var(--color-surface) 25%);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background-color: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-title {
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-meta {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-card {
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-card h2,
|
||||||
|
.section-head h2 {
|
||||||
|
margin: var(--space-3) 0 var(--space-2);
|
||||||
|
font-size: clamp(1.25rem, 1.2vw + 0.8rem, 1.8rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-card p,
|
||||||
|
.feature-card p,
|
||||||
|
.workflow-card p,
|
||||||
|
.stack-text {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-section,
|
||||||
|
.stack-section {
|
||||||
|
padding: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head {
|
||||||
|
margin-bottom: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
padding: var(--space-4);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--color-border) 75%, var(--color-surface) 25%);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background-color: color-mix(in srgb, var(--color-surface) 80%, var(--color-surface-alt) 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h3,
|
||||||
|
.workflow-card h3,
|
||||||
|
.stack-copy h2 {
|
||||||
|
margin: var(--space-2) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-card {
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.workflow-number {
|
||||||
|
color: var(--color-primary-700);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-section {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
|
||||||
|
gap: var(--space-6);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-pill {
|
||||||
|
padding: 6px var(--space-3);
|
||||||
|
border: 1px solid var(--color-border-strong);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
background-color: color-mix(in srgb, var(--color-surface-alt) 75%, var(--color-surface) 25%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 1100px) {
|
||||||
|
.hero,
|
||||||
|
.stack-section {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-preview {
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 960px) {
|
||||||
|
.hero,
|
||||||
|
.feature-section,
|
||||||
|
.stack-section {
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-preview {
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-row {
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-grid,
|
||||||
|
.feature-grid,
|
||||||
|
.workflow-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 600px) {
|
||||||
|
.hero,
|
||||||
|
.feature-section,
|
||||||
|
.stack-section {
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(1.6rem, 8vw, 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-lead {
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.hero-actions .v-btn) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-tags {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-tag {
|
||||||
|
padding: 4px var(--space-2);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-card,
|
||||||
|
.workflow-card,
|
||||||
|
.feature-card {
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-pill {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const companyDetails = [
|
||||||
|
{ label: 'Anbieter', value: 'Hoard Labs GmbH (Testdaten)' },
|
||||||
|
{ label: 'Vertreten durch', value: 'Max Mustermann, Geschäftsführer' },
|
||||||
|
{ label: 'Adresse', value: 'Musterstraße 42, 12345 Musterstadt, Deutschland' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const registerDetails = [
|
||||||
|
{ label: 'Handelsregister', value: 'HRB 123456' },
|
||||||
|
{ label: 'Registergericht', value: 'Amtsgericht Musterstadt' },
|
||||||
|
{ label: 'Umsatzsteuer-ID', value: 'DE123456789' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const contactDetails = [
|
||||||
|
{ label: 'Telefon', value: '+49 30 1234567-0', href: 'tel:+493012345670' },
|
||||||
|
{ label: 'E-Mail', value: 'kontakt@hoard-demo.de', href: 'mailto:kontakt@hoard-demo.de' },
|
||||||
|
{ label: 'Support', value: 'support@hoard-demo.de', href: 'mailto:support@hoard-demo.de' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const legalNotes = [
|
||||||
|
{
|
||||||
|
title: 'Verantwortlich für den Inhalt',
|
||||||
|
text: 'Julia Beispiel, Musterstraße 42, 12345 Musterstadt, Deutschland (Testdaten).',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'EU-Streitbeilegung',
|
||||||
|
text: 'Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung bereit: https://ec.europa.eu/consumers/odr/. Wir sind nicht verpflichtet und nicht bereit, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Haftung für Inhalte',
|
||||||
|
text: 'Als Diensteanbieter sind wir für eigene Inhalte nach den allgemeinen Gesetzen verantwortlich. Für fremde Inhalte, auf die wir verweisen, übernehmen wir keine Gewähr. Dieses Impressum enthält ausschließlich Demo-Angaben.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Haftung für Links',
|
||||||
|
text: 'Unsere Seiten enthalten Links zu externen Webseiten Dritter. Für deren Inhalte ist stets der jeweilige Anbieter verantwortlich. Bei Bekanntwerden von Rechtsverletzungen werden derartige Links umgehend entfernt.',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid class="impressum-page hoard-page">
|
||||||
|
<section class="impressum-hero hoard-panel hoard-panel-gradient">
|
||||||
|
<div class="hero-copy">
|
||||||
|
<p class="hero-kicker hoard-kicker">Rechtliche Angaben</p>
|
||||||
|
<h1>Impressum</h1>
|
||||||
|
<p class="hero-lead">
|
||||||
|
Diese Seite ist im Hoard-Design aufgebaut und mit Testdaten gefüllt. Ersetze die Angaben
|
||||||
|
vor einem produktiven Einsatz mit deinen echten Unternehmensdaten.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="hero-meta">
|
||||||
|
<span class="meta-pill">Testdaten</span>
|
||||||
|
<span class="meta-text">Stand: 17. April 2026</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="hero-actions hoard-action-row">
|
||||||
|
<v-btn color="primary" prepend-icon="mdi-home" to="/">Zur Startseite</v-btn>
|
||||||
|
<v-btn variant="outlined" prepend-icon="mdi-login" to="/login">Zum Login</v-btn>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="details-grid">
|
||||||
|
<article class="detail-card hoard-panel">
|
||||||
|
<h2>Anbieterangaben</h2>
|
||||||
|
<dl class="detail-list">
|
||||||
|
<div v-for="entry in companyDetails" :key="entry.label" class="detail-item">
|
||||||
|
<dt>{{ entry.label }}</dt>
|
||||||
|
<dd>{{ entry.value }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="detail-card hoard-panel">
|
||||||
|
<h2>Kontakt</h2>
|
||||||
|
<dl class="detail-list">
|
||||||
|
<div v-for="entry in contactDetails" :key="entry.label" class="detail-item">
|
||||||
|
<dt>{{ entry.label }}</dt>
|
||||||
|
<dd>
|
||||||
|
<a :href="entry.href">{{ entry.value }}</a>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="detail-card hoard-panel">
|
||||||
|
<h2>Register und Steuer</h2>
|
||||||
|
<dl class="detail-list">
|
||||||
|
<div v-for="entry in registerDetails" :key="entry.label" class="detail-item">
|
||||||
|
<dt>{{ entry.label }}</dt>
|
||||||
|
<dd>{{ entry.value }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="notes-section hoard-panel">
|
||||||
|
<header class="notes-head">
|
||||||
|
<p class="notes-kicker hoard-kicker">Rechtliche Hinweise</p>
|
||||||
|
<h2>Wichtige Zusatzinformationen</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="notes-grid">
|
||||||
|
<article v-for="note in legalNotes" :key="note.title" class="note-card">
|
||||||
|
<h3>{{ note.title }}</h3>
|
||||||
|
<p>{{ note.text }}</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.impressum-page {
|
||||||
|
--hoard-page-width: 1120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.impressum-hero {
|
||||||
|
--hoard-gradient-angle: 125deg;
|
||||||
|
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 40%, var(--color-surface) 60%);
|
||||||
|
--hoard-gradient-end: var(--color-surface);
|
||||||
|
--hoard-gradient-end-stop: 56%;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
gap: var(--space-6);
|
||||||
|
align-items: end;
|
||||||
|
padding: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-lead,
|
||||||
|
.meta-text {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
font-size: clamp(1.9rem, 2.2vw + 1rem, 2.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-lead {
|
||||||
|
max-width: 64ch;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin-top: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px var(--space-3);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--color-primary-300) 60%, var(--color-border) 40%);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--color-primary-700);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: color-mix(in srgb, var(--color-primary-100) 82%, var(--color-surface) 18%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-text {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(1.2rem, 1.2vw + 0.85rem, 1.6rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
padding-bottom: var(--space-3);
|
||||||
|
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 75%, var(--color-surface) 25%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-section {
|
||||||
|
padding: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-head {
|
||||||
|
margin-bottom: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-card {
|
||||||
|
padding: var(--space-4);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--color-border) 76%, var(--color-surface) 24%);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background-color: color-mix(in srgb, var(--color-surface-alt) 72%, var(--color-surface) 28%);
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 var(--space-2);
|
||||||
|
font-size: 1.03rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-card p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 1080px) {
|
||||||
|
.details-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 960px) {
|
||||||
|
.impressum-hero,
|
||||||
|
.notes-section {
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.impressum-hero {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-grid,
|
||||||
|
.notes-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.hero-actions .v-btn) {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 600px) {
|
||||||
|
.impressum-hero,
|
||||||
|
.notes-section {
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(1.55rem, 7vw, 1.95rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-meta {
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.hero-actions .v-btn) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card {
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-card {
|
||||||
|
padding: var(--space-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const showPassword = ref(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<v-container fluid class="login-page hoard-page hoard-page--centered">
|
||||||
|
<section class="login-shell hoard-panel hoard-shell-grid hoard-panel-gradient">
|
||||||
|
<aside class="login-brand">
|
||||||
|
<p class="login-kicker hoard-kicker hoard-kicker--wide">Willkommen bei Hoard</p>
|
||||||
|
<h1>Anmelden und weiterarbeiten</h1>
|
||||||
|
<p class="login-intro">
|
||||||
|
Deine Dateiablage bleibt aufgeräumt, schnell und direkt im Browser bedienbar.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul class="login-points">
|
||||||
|
<li>
|
||||||
|
<v-icon icon="mdi-folder-outline" size="18" />
|
||||||
|
Ordner und Dateien zentral verwalten
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<v-icon icon="mdi-file-document-edit-outline" size="18" />
|
||||||
|
Markdown-Dateien sofort bearbeiten
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<v-icon icon="mdi-image-outline" size="18" />
|
||||||
|
Bilder und PDFs direkt als Vorschau ansehen
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<v-form class="login-form hoard-panel" @submit.prevent>
|
||||||
|
<div class="form-head">
|
||||||
|
<h2>Login</h2>
|
||||||
|
<p>Melde dich mit deinem bestehenden Konto an.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
label="E-Mail"
|
||||||
|
type="email"
|
||||||
|
variant="outlined"
|
||||||
|
prepend-inner-icon="mdi-email-outline"
|
||||||
|
autocomplete="email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<v-text-field
|
||||||
|
label="Passwort"
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
variant="outlined"
|
||||||
|
prepend-inner-icon="mdi-lock-outline"
|
||||||
|
:append-inner-icon="showPassword ? 'mdi-eye-off-outline' : 'mdi-eye-outline'"
|
||||||
|
autocomplete="current-password"
|
||||||
|
required
|
||||||
|
@click:append-inner="showPassword = !showPassword"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="form-meta">
|
||||||
|
<v-checkbox hide-details color="primary" density="compact" label="Angemeldet bleiben" />
|
||||||
|
<v-btn variant="text" size="small">Passwort vergessen?</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-btn type="submit" color="primary" block size="large" prepend-icon="mdi-login">Anmelden</v-btn>
|
||||||
|
|
||||||
|
<v-btn variant="outlined" block to="/" prepend-icon="mdi-home">Zur Startseite</v-btn>
|
||||||
|
</v-form>
|
||||||
|
</section>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-shell {
|
||||||
|
--hoard-shell-width: 1040px;
|
||||||
|
--hoard-gradient-angle: 115deg;
|
||||||
|
--hoard-gradient-start: color-mix(in srgb, var(--color-primary-100) 45%, var(--color-surface) 55%);
|
||||||
|
--hoard-gradient-end: var(--color-surface);
|
||||||
|
--hoard-gradient-end-stop: 52%;
|
||||||
|
|
||||||
|
grid-template-columns: minmax(280px, 1fr) minmax(320px, 430px);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: var(--space-3);
|
||||||
|
max-width: 18ch;
|
||||||
|
font-size: clamp(1.9rem, 2vw + 1rem, 2.6rem);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-intro {
|
||||||
|
margin-bottom: var(--space-5);
|
||||||
|
max-width: 44ch;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-points {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-points li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-3);
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding: var(--space-6);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-head h2 {
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
font-size: 1.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-head p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 960px) {
|
||||||
|
.login-shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-meta {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 600px) {
|
||||||
|
h1 {
|
||||||
|
max-width: none;
|
||||||
|
font-size: clamp(1.55rem, 7vw, 1.95rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-intro {
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-points {
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-points li {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-meta {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.form-meta .v-btn) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.login-form .v-btn) {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/* Shared layout primitives for route-level page shells */
|
||||||
|
.hoard-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--hoard-page-gap, var(--space-6));
|
||||||
|
margin-inline: auto;
|
||||||
|
width: min(100%, var(--hoard-page-width, 1120px));
|
||||||
|
padding-block:
|
||||||
|
var(--hoard-page-padding-start, var(--space-4))
|
||||||
|
var(--hoard-page-padding-end, var(--space-8));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-page--centered {
|
||||||
|
width: 100%;
|
||||||
|
margin-inline: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: calc(100vh - var(--hoard-centered-offset, 210px));
|
||||||
|
padding: var(--hoard-centered-padding, var(--space-8) var(--space-4));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-shell-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--hoard-shell-gap, var(--space-8));
|
||||||
|
width: min(100%, var(--hoard-shell-width, 1040px));
|
||||||
|
padding: var(--hoard-shell-padding, var(--space-8));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 960px) {
|
||||||
|
.hoard-page {
|
||||||
|
width: 100%;
|
||||||
|
gap: var(--hoard-page-gap-mobile, var(--space-5));
|
||||||
|
padding-inline:
|
||||||
|
var(--hoard-page-padding-inline-start-mobile, max(var(--space-2), env(safe-area-inset-left)))
|
||||||
|
var(--hoard-page-padding-inline-end-mobile, max(var(--space-2), env(safe-area-inset-right)));
|
||||||
|
padding-block:
|
||||||
|
var(--hoard-page-padding-start-mobile, var(--space-2))
|
||||||
|
var(--hoard-page-padding-end-mobile, var(--space-6));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-page--centered {
|
||||||
|
width: 100%;
|
||||||
|
min-height: calc(100vh - var(--hoard-centered-offset-mobile, 180px));
|
||||||
|
padding: var(--hoard-centered-padding-mobile, var(--space-5) var(--space-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-shell-grid {
|
||||||
|
width: 100%;
|
||||||
|
gap: var(--hoard-shell-gap-mobile, var(--space-5));
|
||||||
|
padding:
|
||||||
|
var(--hoard-shell-padding-block-mobile, var(--space-5))
|
||||||
|
var(--hoard-shell-padding-inline-mobile, var(--space-4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 600px) {
|
||||||
|
.hoard-page {
|
||||||
|
gap: var(--hoard-page-gap-mobile-xs, var(--space-4));
|
||||||
|
padding-block:
|
||||||
|
var(--hoard-page-padding-start-mobile-xs, var(--space-2))
|
||||||
|
var(--hoard-page-padding-end-mobile-xs, var(--space-5));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-page--centered {
|
||||||
|
min-height: calc(100vh - var(--hoard-centered-offset-mobile-xs, 164px));
|
||||||
|
padding: var(--hoard-centered-padding-mobile-xs, var(--space-4) var(--space-2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-shell-grid {
|
||||||
|
gap: var(--hoard-shell-gap-mobile-xs, var(--space-4));
|
||||||
|
padding:
|
||||||
|
var(--hoard-shell-padding-block-mobile-xs, var(--space-4))
|
||||||
|
var(--hoard-shell-padding-inline-mobile-xs, var(--space-3));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/* Shared surface and content patterns */
|
||||||
|
.hoard-kicker {
|
||||||
|
margin: 0 0 var(--space-2);
|
||||||
|
color: var(--color-primary-700);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-kicker--wide {
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-kicker--xs {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-action-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-panel-gradient {
|
||||||
|
background: linear-gradient(
|
||||||
|
var(--hoard-gradient-angle, 120deg),
|
||||||
|
var(--hoard-gradient-start, color-mix(in srgb, var(--color-primary-100) 34%, var(--color-surface) 66%))
|
||||||
|
0%,
|
||||||
|
var(--hoard-gradient-end, var(--color-surface)) var(--hoard-gradient-end-stop, 52%)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 960px) {
|
||||||
|
.hoard-action-row {
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 600px) {
|
||||||
|
.hoard-kicker {
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-action-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hoard-action-row > * {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
+460
@@ -0,0 +1,460 @@
|
|||||||
|
# Hoard – Style Guide
|
||||||
|
|
||||||
|
## Zielbild
|
||||||
|
Hoard soll wirken wie eine ruhige, moderne Dateiverwaltung im Browser: klar, aufgeräumt, produktiv und leicht verständlich. Nicht verspielt, nicht luxuriös, nicht wie ein komplexes Notion-Klon-System. Die Oberfläche soll in erster Linie Ordnung vermitteln und den Fokus auf Dateien, Ordner, Vorschau und Markdown-Bearbeitung legen.
|
||||||
|
|
||||||
|
Die Gestaltung orientiert sich an drei Prinzipien:
|
||||||
|
|
||||||
|
1. **Dateien zuerst** – Inhalte, Dateinamen, Pfade und Aktionen stehen optisch im Vordergrund.
|
||||||
|
2. **Ruhige Oberfläche** – wenig visuelle Unruhe, viel Weißraum, zurückhaltende Farben.
|
||||||
|
3. **Grün als Identität, nicht als Dauerfeuer** – die Markenfarbe wird gezielt für Auswahl, Primäraktionen und Status genutzt, nicht flächendeckend.
|
||||||
|
|
||||||
|
## Stilrichtung
|
||||||
|
Die Seite soll sich optisch zwischen Google Drive und einer modernen self-hosted Admin-Oberfläche bewegen.
|
||||||
|
|
||||||
|
**So soll es wirken:**
|
||||||
|
- sachlich und sauber
|
||||||
|
- freundlich, aber nicht verspielt
|
||||||
|
- modern, aber bewusst einfach
|
||||||
|
- produktiv statt marketing-lastig
|
||||||
|
- leicht technisch, ohne kalt zu sein
|
||||||
|
|
||||||
|
**So soll es nicht wirken:**
|
||||||
|
- kein Neon- oder Gaming-Look
|
||||||
|
- kein Glassmorphism
|
||||||
|
- keine harten Kontraste überall
|
||||||
|
- keine überladenen Kartenlayouts
|
||||||
|
- keine bunte Mischung vieler Akzentfarben
|
||||||
|
|
||||||
|
## Visuelle Identität
|
||||||
|
Die Markenwirkung basiert auf neutralen Flächen mit einem kontrollierten Grün als Wiedererkennungsmerkmal. Das Grün kommt aus dem Logo und steht für Ablage, Struktur, Ruhe und „self-hosted tool“ statt „Social App“.
|
||||||
|
|
||||||
|
Die App soll **light-first** gestaltet werden. Ein Dark Mode kann später kommen, aber das Grunddesign wird zuerst für helle Oberflächen optimiert. Das spart Aufwand und hält die UI konsistenter.
|
||||||
|
|
||||||
|
## Farbpalette
|
||||||
|
|
||||||
|
### Primärfarben
|
||||||
|
- **Primary 700:** `#1C652F`
|
||||||
|
Für Primärbuttons, aktive Icons, Fokusrahmen, Links in aktiven Zuständen.
|
||||||
|
- **Primary 600:** `#2E7D32`
|
||||||
|
Für Hover-Zustände und aktive Navigation.
|
||||||
|
- **Primary 500:** `#3C8F42`
|
||||||
|
Für ausgewählte Einträge, Badges, bestätigende States.
|
||||||
|
- **Primary 300:** `#A8D5A2`
|
||||||
|
Für weiche Hintergründe von Auswahlflächen.
|
||||||
|
- **Primary 100:** `#EAF5E8`
|
||||||
|
Für sehr subtile Hervorhebungen.
|
||||||
|
|
||||||
|
### Akzentfarbe
|
||||||
|
- **Accent Lime:** `#B7E36B`
|
||||||
|
Nur sehr sparsam einsetzen, z. B. kleiner Glow im Logo-Bereich, leichtere Highlights, Upload-Fortschritt oder ausgewählte Illustrationsdetails. Nicht für normalen Text.
|
||||||
|
|
||||||
|
### Neutrale Farben
|
||||||
|
- **Background:** `#F6F8F5`
|
||||||
|
Hauptseitenhintergrund.
|
||||||
|
- **Surface 1:** `#FFFFFF`
|
||||||
|
Karten, Panels, Modals, Dialoge.
|
||||||
|
- **Surface 2:** `#F1F4EF`
|
||||||
|
Sekundäre Flächen, Toolbar-Hintergründe, Tabellenkopf.
|
||||||
|
- **Border:** `#DCE4D8`
|
||||||
|
Standard-Border.
|
||||||
|
- **Border Strong:** `#C7D2C2`
|
||||||
|
Stärkere Abgrenzung bei Panels und Inputs.
|
||||||
|
|
||||||
|
### Textfarben
|
||||||
|
- **Text Primary:** `#1F2A21`
|
||||||
|
- **Text Secondary:** `#5F6E62`
|
||||||
|
- **Text Muted:** `#7D8A80`
|
||||||
|
- **Text On Primary:** `#FFFFFF`
|
||||||
|
|
||||||
|
### Statusfarben
|
||||||
|
Schlicht halten, nicht zu bunt.
|
||||||
|
- **Success:** `#2E7D32`
|
||||||
|
- **Warning:** `#B7791F`
|
||||||
|
- **Danger:** `#C0392B`
|
||||||
|
- **Info:** `#2F6FB3`
|
||||||
|
|
||||||
|
## Typografie
|
||||||
|
Die Typografie soll neutral, gut lesbar und unauffällig modern sein. Keine dekorativen Schriften.
|
||||||
|
|
||||||
|
**Empfohlene Schriftfamilie:**
|
||||||
|
- `Inter`
|
||||||
|
- Fallback: `system-ui, sans-serif`
|
||||||
|
|
||||||
|
**Typografische Regeln:**
|
||||||
|
- normale Lesetexte: 14–15 px
|
||||||
|
- UI-Haupttext in Listen und Tabellen: 14 px
|
||||||
|
- Seitenüberschriften: 24–28 px
|
||||||
|
- Bereichsüberschriften: 18–20 px
|
||||||
|
- kleine Meta-Infos: 12–13 px
|
||||||
|
- Zeilenhöhe großzügig halten, besonders in Dateilisten und Formularen
|
||||||
|
|
||||||
|
**Schriftgewicht:**
|
||||||
|
- 400 für normalen Fließtext
|
||||||
|
- 500 für UI-Text und Labels
|
||||||
|
- 600 für Titel und aktive Elemente
|
||||||
|
- 700 nur sehr gezielt
|
||||||
|
|
||||||
|
## Layoutprinzip
|
||||||
|
Das Layout soll stark an eine Dateiverwaltung erinnern.
|
||||||
|
|
||||||
|
### Grundaufbau
|
||||||
|
- **Topbar** für Logo, Breadcrumbs, Kontextaktionen, Benutzer-Menü
|
||||||
|
- **linke Sidebar** für Navigation
|
||||||
|
- **Hauptbereich** für Dateiliste oder Grid
|
||||||
|
- **rechte Vorschau / Detailansicht** optional als Panel oder getrennte Ansicht
|
||||||
|
|
||||||
|
### Seitenbreite und Abstand
|
||||||
|
- großzügige horizontale Abstände
|
||||||
|
- Hauptinhalte nicht zu schmal machen
|
||||||
|
- Panels mit genug Luft, aber ohne Dashboard-Overdesign
|
||||||
|
- Standard-Abstandssystem in 4er- oder 8er-Schritten
|
||||||
|
|
||||||
|
**Spacing-Skala:**
|
||||||
|
- 4 px
|
||||||
|
- 8 px
|
||||||
|
- 12 px
|
||||||
|
- 16 px
|
||||||
|
- 20 px
|
||||||
|
- 24 px
|
||||||
|
- 32 px
|
||||||
|
- 40 px
|
||||||
|
|
||||||
|
## Formensprache
|
||||||
|
Die Formensprache soll weich, aber nicht rundgelutscht sein.
|
||||||
|
|
||||||
|
- kleine Controls: `8px` Radius
|
||||||
|
- Panels, Inputs, Dropdowns: `10px`
|
||||||
|
- Modals und größere Karten: `14px`
|
||||||
|
- keine pillenförmigen Vollflächen als Grundstil
|
||||||
|
|
||||||
|
## Schatten und Tiefe
|
||||||
|
Sehr zurückhaltend einsetzen. Die App soll stabil und ruhig wirken, nicht schwebend.
|
||||||
|
|
||||||
|
**Standard-Schatten:**
|
||||||
|
- Panels: leichter, weicher Schatten
|
||||||
|
- Dropdowns / Modals: etwas stärker, aber nie dramatisch
|
||||||
|
- keine starken farbigen Schatten im Produktivbereich
|
||||||
|
- grüner Glow nur höchstens im Branding oder auf Marketing-/Login-Flächen
|
||||||
|
|
||||||
|
## Komponentenstil
|
||||||
|
|
||||||
|
### Topbar
|
||||||
|
Die Topbar ist ruhig und funktional.
|
||||||
|
- Höhe ca. 60–64 px
|
||||||
|
- heller Hintergrund oder leicht abgesetzte Surface-Farbe
|
||||||
|
- Logo links
|
||||||
|
- Breadcrumbs klar lesbar, nicht zu klein
|
||||||
|
- Kontextaktionen rechts davon oder am rechten Rand
|
||||||
|
- dünne Unterkante oder subtile Shadow-Abgrenzung
|
||||||
|
|
||||||
|
### Sidebar
|
||||||
|
Die Sidebar ist funktional, nicht dominant.
|
||||||
|
- feste Breite, ca. 240–280 px
|
||||||
|
- leicht abgesetzte Hintergrundfläche
|
||||||
|
- aktive Einträge mit heller grüner Fläche und dunklerem Text
|
||||||
|
- Icons schlicht und einheitlich
|
||||||
|
- Navigation in logische Gruppen, aber ohne zu viele Sektionen
|
||||||
|
|
||||||
|
### Dateiliste
|
||||||
|
Die Dateiliste ist das Herzstück der App.
|
||||||
|
|
||||||
|
**Darstellung:**
|
||||||
|
- standardmäßig Listenansicht
|
||||||
|
- klare Spalten für Name, Typ, Größe, geändert am
|
||||||
|
- Zeilenhöhe eher luftig statt kompakt gepresst
|
||||||
|
- Hover nur leicht hervorheben
|
||||||
|
- ausgewählte Zeile mit heller grüner Tönung
|
||||||
|
- Doppelklick oder klarer Primärklick zum Öffnen
|
||||||
|
- Dateisymbole farblich dezent
|
||||||
|
|
||||||
|
**Wichtig:**
|
||||||
|
Die Liste soll strukturierter und ruhiger wirken als ein typisches Admin-Grid. Nicht wie eine Datenbanktabelle, sondern wie eine echte Dateiverwaltung.
|
||||||
|
|
||||||
|
### Karten / Panels
|
||||||
|
Karten nur dort einsetzen, wo es fachlich Sinn ergibt:
|
||||||
|
- Vorschau-Panel
|
||||||
|
- Datei-Infos
|
||||||
|
- Upload-Status
|
||||||
|
- Modals
|
||||||
|
|
||||||
|
Nicht jede Seite künstlich in zehn Karten aufteilen.
|
||||||
|
|
||||||
|
### Buttons
|
||||||
|
Buttons sollen schlicht und klar sein.
|
||||||
|
|
||||||
|
**Primärbutton:**
|
||||||
|
- grüner Hintergrund
|
||||||
|
- weiße Schrift
|
||||||
|
- mittlere Höhe
|
||||||
|
- klar erkennbare Hover- und Disabled-Zustände
|
||||||
|
|
||||||
|
**Sekundärbutton:**
|
||||||
|
- helle Fläche mit Border
|
||||||
|
- dunkler Text
|
||||||
|
- kein zu aggressiver Kontrast
|
||||||
|
|
||||||
|
**Tertiärbutton / Icon-Button:**
|
||||||
|
- für Zeilenaktionen, Toolbar, Preview-Aktionen
|
||||||
|
- Hover mit leichter Surface-Abhebung
|
||||||
|
|
||||||
|
**Regel:**
|
||||||
|
Es sollte pro Bereich meist genau eine klare Primäraktion geben.
|
||||||
|
|
||||||
|
### Inputs
|
||||||
|
- weiße Fläche
|
||||||
|
- klarer Border
|
||||||
|
- Fokuszustand mit grünem Ring oder grün betonter Border
|
||||||
|
- keine dunklen, schweren Inputs
|
||||||
|
- Labels oberhalb statt Placeholder-only
|
||||||
|
|
||||||
|
### Modals und Dialoge
|
||||||
|
- kompakt und funktional
|
||||||
|
- deutlicher Titel
|
||||||
|
- klare Primär- und Sekundäraktion
|
||||||
|
- nicht zu breit
|
||||||
|
- Löschen-Aktionen visuell bewusst neutral mit Danger-Akzent nur am Button
|
||||||
|
|
||||||
|
### Dropdowns und Kontextmenüs
|
||||||
|
- schlicht, hell, sauber getrennte Einträge
|
||||||
|
- Icons optional, aber konsistent
|
||||||
|
- Hover klar sichtbar
|
||||||
|
- kritische Aktionen unten gruppieren
|
||||||
|
|
||||||
|
## Vorschau-Bereich
|
||||||
|
Der Vorschau-Bereich ist ein zentraler Teil von Hoard und soll hochwertig, aber ruhig wirken.
|
||||||
|
|
||||||
|
### PDF-Vorschau
|
||||||
|
- heller neutraler Hintergrund
|
||||||
|
- PDF sitzt auf einer weißen „Papier“-Fläche
|
||||||
|
- genug Rand um die Seite herum
|
||||||
|
- Controls minimal und funktional
|
||||||
|
|
||||||
|
### Bildvorschau
|
||||||
|
- dunklerer neutraler Viewer-Hintergrund ist okay, wenn das Bild dadurch besser wirkt
|
||||||
|
- umgebende UI trotzdem konsistent mit dem restlichen Produkt halten
|
||||||
|
- keine übertriebene Galerie-Optik
|
||||||
|
|
||||||
|
### Markdown-Ansicht / Editor
|
||||||
|
Markdown-Dateien sollen wie Arbeitsdokumente wirken, nicht wie Blogposts.
|
||||||
|
|
||||||
|
**Vorgaben:**
|
||||||
|
- gute Textbreite
|
||||||
|
- klare Hierarchie bei Überschriften
|
||||||
|
- dezente Codeblock-Gestaltung
|
||||||
|
- sehr gute Lesbarkeit
|
||||||
|
- Editor und Preview optisch zur restlichen App passend, auch wenn `md-editor-v3` eigene Defaults mitbringt
|
||||||
|
|
||||||
|
## Tabellen- und Listenverhalten
|
||||||
|
Da der Kern deiner App Listen, Dateiansichten und Metadaten sind, muss das Verhalten konsistent sein.
|
||||||
|
|
||||||
|
- Hover ist immer subtil
|
||||||
|
- Auswahl ist immer über denselben Grünton markiert
|
||||||
|
- aktive Navigation und aktive Listelemente nutzen dieselbe semantische Farbe
|
||||||
|
- Sortierung, falls später vorhanden, visuell zurückhaltend markieren
|
||||||
|
- Bulk-Actions nur anzeigen, wenn wirklich etwas ausgewählt ist
|
||||||
|
|
||||||
|
## Icons
|
||||||
|
Empfohlen ist ein schlanker, moderner Icon-Stil mit einheitlicher Konturstärke.
|
||||||
|
|
||||||
|
Geeignet wären z. B.:
|
||||||
|
- Lucide
|
||||||
|
- Heroicons
|
||||||
|
|
||||||
|
**Regeln:**
|
||||||
|
- möglichst Outline-Icons
|
||||||
|
- nur wenige gefüllte Icons
|
||||||
|
- Dateityp-Icons dürfen leicht differenziert sein, aber nicht bunt explodieren
|
||||||
|
- Ordner-Icon in gedecktem Grün oder neutralem Grau
|
||||||
|
|
||||||
|
## Status und Feedback
|
||||||
|
Feedback soll klar sein, aber nicht laut.
|
||||||
|
|
||||||
|
### Toasts
|
||||||
|
- kurze Texte
|
||||||
|
- kein unnötiger Fließtext
|
||||||
|
- success, error, info klar unterscheidbar
|
||||||
|
- am besten oben rechts oder unten rechts, aber konsistent
|
||||||
|
|
||||||
|
### Ladezustände
|
||||||
|
- Skeletons oder sehr schlichte Loader
|
||||||
|
- lieber ruhige Platzhalter statt hektische Spinner überall
|
||||||
|
|
||||||
|
### Leere Zustände
|
||||||
|
Leere Zustände sollen freundlich, aber nüchtern sein.
|
||||||
|
- kleines Icon oder einfache Illustration
|
||||||
|
- ein klarer Satz
|
||||||
|
- eine eindeutige Folgeaktion
|
||||||
|
|
||||||
|
## Login-Seite
|
||||||
|
Die Login-Seite darf minimal etwas mehr Branding zeigen als die Haupt-App.
|
||||||
|
|
||||||
|
**Empfehlung:**
|
||||||
|
- zentrierte Login-Card
|
||||||
|
- Logo sichtbar
|
||||||
|
- neutraler Hintergrund mit sehr leichter grüner Stimmung
|
||||||
|
- keine starke Hero-Sektion nötig
|
||||||
|
- Fokus auf schnellem Einstieg
|
||||||
|
|
||||||
|
## Responsive Verhalten
|
||||||
|
Desktop ist der Hauptfokus. Mobile muss funktionieren, aber nicht die Priorität des MVP sein.
|
||||||
|
|
||||||
|
### Desktop
|
||||||
|
- volle Sidebar
|
||||||
|
- großzügige Dateiliste
|
||||||
|
- Preview neben Liste möglich
|
||||||
|
|
||||||
|
### Tablet
|
||||||
|
- Sidebar einklappbar
|
||||||
|
- Preview eher als Overlay oder eigene Ansicht
|
||||||
|
|
||||||
|
### Mobile
|
||||||
|
- eher einfache Stapelansicht
|
||||||
|
- Fokus auf Navigation und Öffnen
|
||||||
|
- Bearbeitung von Markdown darf reduziert sein, solange Lesen und einfache Bedienung sauber funktionieren
|
||||||
|
|
||||||
|
### Umsetzungsstandard Responsivität (verbindlich)
|
||||||
|
Die folgenden Regeln bilden den aktuellen Responsive-Standard von Hoard und sollen bei allen kommenden UI-Aufgaben eingehalten werden.
|
||||||
|
|
||||||
|
1. **Desktop-first, Mobile-only Overrides**
|
||||||
|
- Desktop-Styles bleiben Basis.
|
||||||
|
- Mobile-Anpassungen ausschließlich in Media Queries, keine Änderungen an Desktop-Baseregeln.
|
||||||
|
|
||||||
|
2. **Breakpoints**
|
||||||
|
- `@media (width <= 960px)` für Tablet/Mobile-Umbruch (Layout-Stacks, Spacing-Reduktion, Drawer-/Footer-Verhalten).
|
||||||
|
- `@media (width <= 600px)` für Phone-Feinschliff (volle Breite für CTAs, kompaktere Typo/Abstände).
|
||||||
|
|
||||||
|
3. **Globale Responsive-Patterns zuerst nutzen**
|
||||||
|
- Wiederverwendbare Anpassungen immer zuerst in den globalen Dateien pflegen:
|
||||||
|
- `GUI/src/global.css`
|
||||||
|
- `GUI/src/styles/global/page-layouts.css`
|
||||||
|
- `GUI/src/styles/global/surface-patterns.css`
|
||||||
|
- Seiten-spezifisches `scoped` CSS nur für wirklich lokale Sonderfälle.
|
||||||
|
|
||||||
|
4. **Touch-Zielgrößen und Bedienbarkeit**
|
||||||
|
- Interaktive Elemente mobil mit klarer Daumen-Bedienbarkeit:
|
||||||
|
- Buttons mindestens `44px` Höhe.
|
||||||
|
- Icon-Buttons mindestens `44x44px`.
|
||||||
|
- Navigations-Listeneinträge mindestens `48px` Höhe.
|
||||||
|
- Aktionszeilen (`hoard-action-row`) auf kleinen Geräten vertikal stapeln, damit Primäraktionen gut erreichbar sind.
|
||||||
|
|
||||||
|
5. **Safe-Area-Unterstützung**
|
||||||
|
- Bei mobilen Außenabständen `env(safe-area-inset-*)` berücksichtigen (iOS/Android).
|
||||||
|
- Muster: `max(var(--space-x), env(safe-area-inset-...))` als Fallback-sicherer Abstand.
|
||||||
|
- Besonders relevant für App-Bar-Ränder, Seiten-Padding und Bottom-Bereiche (Drawer/Footer).
|
||||||
|
|
||||||
|
6. **App-Shell-Muster**
|
||||||
|
- Mobile Navigation bleibt als Bottom-Sheet-Drawer.
|
||||||
|
- Desktop-Navigation bleibt unverändert (keine visuelle Regression auf großen Viewports).
|
||||||
|
- Footer-Links auf kleinen Screens umbauen (Wrap/Grid), mit ausreichend großen Tap-Flächen.
|
||||||
|
|
||||||
|
7. **Seiten-Muster**
|
||||||
|
- Content-Änderungen vermeiden; nur Layout/Bedienung anpassen.
|
||||||
|
- Karten-/Grid-Bereiche bei `<= 960px` in Einspalten-Layout überführen.
|
||||||
|
- Primäre CTAs bei `<= 600px` in voller Breite anzeigen.
|
||||||
|
|
||||||
|
8. **Responsive QA vor Abschluss**
|
||||||
|
- Pflicht-Viewports: `360x800`, `390x844`, `768x1024`, `1024x768`, `>=1280`.
|
||||||
|
- Prüfen: Navigation, Scroll-Verhalten, CTA-Erreichbarkeit, Formular-Bedienbarkeit.
|
||||||
|
- Desktop-Regression-Check: bei `>=1024` darf sich das gewollte Desktop-Erscheinungsbild nicht ändern.
|
||||||
|
|
||||||
|
## Interaktionsprinzipien
|
||||||
|
- Primäraktionen immer klar sichtbar
|
||||||
|
- destruktive Aktionen nie zu nah an Standardaktionen
|
||||||
|
- Dateizeilen sollen sich klickbar anfühlen, ohne wie Buttons auszusehen
|
||||||
|
- Hover, Active und Selected Zustände deutlich unterscheiden
|
||||||
|
- Fokuszustände für Tastaturbedienung sauber sichtbar machen
|
||||||
|
|
||||||
|
## Stil für konkrete Bereiche
|
||||||
|
|
||||||
|
### Ordnernavigation
|
||||||
|
- Breadcrumbs schlicht, klickbar, gut lesbar
|
||||||
|
- aktueller Ordner klar markiert
|
||||||
|
- Pfad nie visuell dominanter als der Inhalt
|
||||||
|
|
||||||
|
### Upload-Bereich
|
||||||
|
- Uploads funktional anzeigen, nicht dramatisch
|
||||||
|
- Fortschritt mit ruhiger grüner Progressbar
|
||||||
|
- Fehlerfälle klar lesbar
|
||||||
|
- Upload-Liste eher kompakt halten
|
||||||
|
|
||||||
|
### Datei-Details
|
||||||
|
- Metadaten in sauberem Zwei-Spalten-Raster oder kompakter Liste
|
||||||
|
- Labels und Werte klar unterscheidbar
|
||||||
|
- Aktionen wie Download, Umbenennen, Löschen klar getrennt
|
||||||
|
|
||||||
|
## Designregeln für die Umsetzung
|
||||||
|
|
||||||
|
### Immer tun
|
||||||
|
- viel Weißraum lassen
|
||||||
|
- Grün nur gezielt einsetzen
|
||||||
|
- Borders und Surface-Unterschiede subtil halten
|
||||||
|
- Listen und Dateiansichten priorisieren
|
||||||
|
- Text gut lesbar und eher neutral halten
|
||||||
|
|
||||||
|
### Vermeiden
|
||||||
|
- zu viele Karten
|
||||||
|
- zu viele Farbflächen
|
||||||
|
- übertriebene Animationen
|
||||||
|
- mehrere konkurrierende Akzentfarben
|
||||||
|
- rein dekorative UI-Elemente ohne Nutzen
|
||||||
|
|
||||||
|
## Beispiel für Design Tokens
|
||||||
|
Diese Tokens können später direkt in CSS-Variablen oder ein Theme übernommen werden.
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--color-bg: #F6F8F5;
|
||||||
|
--color-surface: #FFFFFF;
|
||||||
|
--color-surface-alt: #F1F4EF;
|
||||||
|
--color-border: #DCE4D8;
|
||||||
|
--color-border-strong: #C7D2C2;
|
||||||
|
|
||||||
|
--color-text: #1F2A21;
|
||||||
|
--color-text-secondary: #5F6E62;
|
||||||
|
--color-text-muted: #7D8A80;
|
||||||
|
|
||||||
|
--color-primary-700: #1C652F;
|
||||||
|
--color-primary-600: #2E7D32;
|
||||||
|
--color-primary-500: #3C8F42;
|
||||||
|
--color-primary-300: #A8D5A2;
|
||||||
|
--color-primary-100: #EAF5E8;
|
||||||
|
--color-accent-lime: #B7E36B;
|
||||||
|
|
||||||
|
--color-success: #2E7D32;
|
||||||
|
--color-warning: #B7791F;
|
||||||
|
--color-danger: #C0392B;
|
||||||
|
--color-info: #2F6FB3;
|
||||||
|
|
||||||
|
--radius-sm: 8px;
|
||||||
|
--radius-md: 10px;
|
||||||
|
--radius-lg: 14px;
|
||||||
|
|
||||||
|
--shadow-sm: 0 1px 2px rgba(16, 24, 18, 0.06);
|
||||||
|
--shadow-md: 0 6px 18px rgba(16, 24, 18, 0.08);
|
||||||
|
|
||||||
|
--space-1: 4px;
|
||||||
|
--space-2: 8px;
|
||||||
|
--space-3: 12px;
|
||||||
|
--space-4: 16px;
|
||||||
|
--space-5: 20px;
|
||||||
|
--space-6: 24px;
|
||||||
|
--space-8: 32px;
|
||||||
|
--space-10: 40px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Abschlussentscheidung für Hoard
|
||||||
|
Für Hoard ist ein **ruhiger, light-first, dateiorientierter Produktivstil mit neutralen Flächen und kontrolliertem Grün als Markenfarbe** die passendste Richtung.
|
||||||
|
|
||||||
|
Das passt zur Produktidee, weil:
|
||||||
|
- die App primär eine Dateiverwaltung ist
|
||||||
|
- Markdown-Bearbeitung und Vorschau im Vordergrund stehen
|
||||||
|
- die Oberfläche einfach und wartbar bleiben soll
|
||||||
|
- der Stil gut allein umsetzbar ist
|
||||||
|
- die UI professionell wirkt, ohne nach großem SaaS-Produkt aussehen zu müssen
|
||||||
|
|
||||||
|
## Kurzfassung als Design-Leitlinie
|
||||||
|
Wenn du bei einer UI-Entscheidung unsicher bist, gilt:
|
||||||
|
|
||||||
|
**Lieber schlichter als spektakulär. Lieber Google-Drive-artig als Dashboard-artig. Lieber ruhige Flächen und klare Listen als visuelle Effekte. Grün ist Identität, nicht Dekoration.**
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
"include": ["env.d.ts", "src/**/*.vue", "src/**/*.ts"],
|
||||||
"exclude": ["src/**/__tests__/*"],
|
"exclude": ["src/**/__tests__/*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Extra safety for array and object lookups, but may have false positives.
|
// Extra safety for array and object lookups, but may have false positives.
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ export default defineConfig({
|
|||||||
vue(),
|
vue(),
|
||||||
vueDevTools(),
|
vueDevTools(),
|
||||||
],
|
],
|
||||||
|
build: {
|
||||||
|
outDir: fileURLToPath(new URL('../API/wwwroot', import.meta.url)),
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
|||||||
@@ -1,2 +1,33 @@
|
|||||||
# Hoard
|
# Hoard
|
||||||
File and Knowledge Management
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="./GUI/src/assets/images/icon.png" width="120" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
Hoard ist eine einfache, selbst gehostete Web-App zur Verwaltung von Dateien und Ordnern – mit integrierter Markdown-Bearbeitung direkt im Browser.
|
||||||
|
|
||||||
|
Die Anwendung bewegt sich funktional zwischen Google Drive, Notion und Obsidian, bleibt dabei aber bewusst schlank und pragmatisch umgesetzt. Fokus liegt auf klarer Navigation, einfacher Bedienung und einem realistischen Umfang für ein Solo-Projekt.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 📁 Dateien und Ordner verwalten
|
||||||
|
- 🧭 Durch Ordnerstrukturen navigieren
|
||||||
|
- 📝 Markdown-Dateien direkt im Browser bearbeiten
|
||||||
|
- 🖼️ Vorschau für Bilder und PDFs
|
||||||
|
- 📦 Andere Dateien speichern und herunterladen
|
||||||
|
- 👥 Mehrbenutzerfähig (Accounts werden manuell erstellt)
|
||||||
|
- 🔐 Klassische Login-Session mit Cookies
|
||||||
|
|
||||||
|
## Tech-Stack
|
||||||
|
|
||||||
|
- **Frontend:** Vue 3
|
||||||
|
- **Markdown-Editor:** md-editor-v3
|
||||||
|
- **Backend:** ASP.NET Core (C#)
|
||||||
|
- **Datenbank:** PostgreSQL
|
||||||
|
- **Dateispeicher:** MinIO (S3-kompatibel)
|
||||||
|
- **Authentifizierung:** Cookie-basiert
|
||||||
|
- **Deployment:** Self-hosted auf eigenem Server
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Ein bewusst einfach gehaltenes System, das sich wie eine klassische Dateiverwaltung im Browser anfühlt – ohne unnötige Komplexität, aber mit sauberer Grundlage für spätere Erweiterungen.
|
||||||
|
|||||||
+127
@@ -0,0 +1,127 @@
|
|||||||
|
# Projektübersicht – self-hosted Datei- und Markdown-App
|
||||||
|
|
||||||
|
## Projektidee
|
||||||
|
Ich baue eine einfache self-hosted Web-App, die sich funktional zwischen Google Drive, Notion und Obsidian einordnet. Der Schwerpunkt liegt aber klar auf einer Google-Drive-artigen Oberfläche mit Dateien und Ordnern. Markdown-Dateien sollen direkt im Browser bearbeitet werden können, andere Dateien sollen gespeichert und – wenn möglich – als Vorschau angezeigt werden.
|
||||||
|
|
||||||
|
## Ziel des Projekts
|
||||||
|
Das Projekt ist ein kleines, bewusst einfach gehaltenes Solo-Projekt neben meiner Ausbildung. Es soll mehrere Benutzer unterstützen, aber technisch und funktional schlank bleiben. Wichtig ist ein realistisches MVP, das sauber läuft und später erweitert werden kann.
|
||||||
|
|
||||||
|
## Geplanter Tech-Stack
|
||||||
|
- Frontend: Vue 3
|
||||||
|
- Markdown-Editor: md-editor-v3
|
||||||
|
- Backend: ASP.NET Core mit C#
|
||||||
|
- Datenbank: PostgreSQL
|
||||||
|
- Dateispeicher: MinIO als S3-kompatibler Storage
|
||||||
|
- Authentifizierung: klassische Cookie-basierte Authentifizierung, keine JWTs
|
||||||
|
- Deployment: self-hosted Web-App auf meinem eigenen Server
|
||||||
|
|
||||||
|
## Kernfunktionen für das MVP
|
||||||
|
- Login mit bestehenden Accounts
|
||||||
|
- Kein öffentliches Registrieren
|
||||||
|
- Ein initialer Admin-Account wird zuerst erstellt
|
||||||
|
- Weitere Benutzer werden später nur manuell durch den Admin angelegt
|
||||||
|
- Dateien und Ordner anlegen, hochladen und öffnen
|
||||||
|
- Durch Ordnerstrukturen navigieren
|
||||||
|
- Google-Drive-artige Hauptansicht mit Dateiliste und Vorschau
|
||||||
|
- Markdown-Dateien direkt im Browser bearbeiten
|
||||||
|
- PDFs und Bilder als Vorschau anzeigen
|
||||||
|
- Andere Dateien einfach speichern und bei Bedarf herunterladen oder öffnen
|
||||||
|
|
||||||
|
## Was bewusst nicht Teil des MVP ist
|
||||||
|
- Keine Registrierung für normale Nutzer
|
||||||
|
- Kein Teilen oder Freigeben von Dateien
|
||||||
|
- Keine Suche
|
||||||
|
- Keine Versionierung oder Dateihistorie
|
||||||
|
- Keine Echtzeit-Zusammenarbeit
|
||||||
|
- Keine Desktop-App oder Mobile-App
|
||||||
|
- Keine komplizierte Rechteverwaltung
|
||||||
|
- Keine JWT-, OAuth- oder SSO-Lösung
|
||||||
|
|
||||||
|
## Gewünschter Stil der Anwendung
|
||||||
|
Die Oberfläche soll sich eher an Google Drive orientieren als an Notion oder Obsidian. Wichtig sind Übersicht, einfache Navigation und ein klarer Fokus auf Dateien, Ordner, Vorschau und Bearbeitung. Die App soll schlicht, pragmatisch und gut allein umsetzbar sein.
|
||||||
|
|
||||||
|
## Sprachregel für UI-Texte
|
||||||
|
- Umlaute sind ausdrücklich erwünscht (`ä`, `ö`, `ü`, `Ä`, `Ö`, `Ü`).
|
||||||
|
- Keine Umschreibungen mit `ae`, `oe`, `ue` in sichtbaren deutschen Texten.
|
||||||
|
|
||||||
|
## Wie die App später wirken soll
|
||||||
|
Die Anwendung soll wie eine einfache Dateiverwaltung im Browser wirken. Man meldet sich an, sieht seine Ordner und Dateien, kann sich durch die Struktur klicken, PDFs und Bilder direkt ansehen und Markdown-Dateien öffnen und bearbeiten. Der Fokus liegt auf Einfachheit statt auf vielen Sonderfunktionen.
|
||||||
|
|
||||||
|
## Technische Leitidee
|
||||||
|
Das Projekt soll möglichst einfach aufgebaut werden. Dateimetadaten liegen in PostgreSQL, die eigentlichen Dateien in MinIO. Das Backend verwaltet Login, Benutzer, Ordner, Dateien und Vorschau-Informationen. Das Frontend bildet hauptsächlich die Dateiverwaltung, Vorschau und Markdown-Bearbeitung ab. Die gesamte Architektur soll bewusst schlank bleiben, damit sie für ein Solo-Projekt realistisch ist.
|
||||||
|
|
||||||
|
## Projektbeschreibung für eine KI
|
||||||
|
Ich baue alleine neben meiner Ausbildung eine einfache self-hosted Web-App für mehrere Benutzer. Die App kombiniert eine Google-Drive-artige Dateiverwaltung mit einfacher Markdown-Bearbeitung. Benutzer sollen durch Ordner und Dateien navigieren können, Bilder und PDFs in einer Vorschau sehen und Markdown-Dateien direkt im Browser bearbeiten. Es gibt keine öffentliche Registrierung, kein Teilen, keine Suche und keine Versionierung. Benutzerkonten werden manuell angelegt, beginnend mit einem initialen Admin-Account. Der Tech-Stack besteht aus Vue 3 im Frontend, md-editor-v3 als Markdown-Editor, ASP.NET Core mit C# im Backend, PostgreSQL für Metadaten, MinIO als S3-kompatiblen Dateispeicher und Cookie-basierter Authentifizierung.
|
||||||
|
|
||||||
|
## Frontend-Designquelle (Style Guide)
|
||||||
|
- Es gibt einen zentralen Design-Guide unter `GUI/style.md`.
|
||||||
|
- Dieser Guide definiert die visuelle Richtung für Hoard: light-first, dateiorientiert, ruhige Flächen, gezielte Verwendung von Grün als Markenfarbe.
|
||||||
|
- Enthalten sind Farbpalette, Typografie, Spacing, Border-Radien, Schatten, Komponentenregeln und Interaktionsprinzipien.
|
||||||
|
|
||||||
|
## Angelegte globale CSS-Basis
|
||||||
|
- Statt `app.css` wurde eine zentrale globale Datei `GUI/src/global.css` angelegt und verwendet.
|
||||||
|
- Diese Datei wird in `GUI/src/main.ts` über `import './global.css'` eingebunden.
|
||||||
|
- Zusätzlich wurden modulare globale CSS-Dateien angelegt: `GUI/src/styles/global/page-layouts.css` und `GUI/src/styles/global/surface-patterns.css`.
|
||||||
|
- Beide Module werden ebenfalls zentral in `GUI/src/main.ts` eingebunden und bündeln wiederkehrende Layout-/Surface-Patterns.
|
||||||
|
- Inhaltlich stellt `global.css` bereit:
|
||||||
|
- Design-Tokens als CSS-Variablen (`:root`) für Farben, Spacing, Radius, Schatten, Typografie und Statusfarben.
|
||||||
|
- Globale Basisstile für `html`, `body`, Links, Überschriften, Fokuszustände und Scrollbars.
|
||||||
|
- Vuetify-nahe globale Anpassungen für App-Shell und Standardkomponenten (Topbar, Sidebar, Cards, Buttons, Inputs, Tabellen).
|
||||||
|
- Wiederverwendbare Utility-/Pattern-Klassen für Hoard-Seiten, z. B. `hoard-panel`, `hoard-toolbar`, `hoard-list-row`, `hoard-empty-state`, `hoard-status`.
|
||||||
|
- Responsive Verhalten für kleinere Viewports per Media Query.
|
||||||
|
|
||||||
|
## Anleitung: CSS-Patterns verwenden
|
||||||
|
- Neue Seiten standardmäßig mit `hoard-page` aufbauen; für zentrierte Vollhöhen-Ansichten zusätzlich `hoard-page--centered`.
|
||||||
|
- Karten-/Shell-Container als `hoard-shell-grid hoard-panel` verwenden; Breite/Abstände pro Seite über CSS-Variablen setzen (`--hoard-shell-width`, `--hoard-shell-gap`, `--hoard-shell-padding`).
|
||||||
|
- Wiederkehrende Headlines/Kicker mit `hoard-kicker` nutzen, Varianten bei Bedarf mit `hoard-kicker--wide` oder `hoard-kicker--xs`.
|
||||||
|
- Button-/Link-Aktionszeilen mit `hoard-action-row` bauen statt pro Seite eigene Flex-Definitionen zu duplizieren.
|
||||||
|
- Gradient-Flächen über `hoard-panel-gradient` + Variablen steuern (`--hoard-gradient-angle`, `--hoard-gradient-start`, `--hoard-gradient-end`, `--hoard-gradient-end-stop`), nicht pro Seite komplett neu definieren.
|
||||||
|
- Lokales `scoped` CSS nur für wirklich seitenspezifische Styles verwenden; alles Wiederverwendbare zuerst in `GUI/src/styles/global/page-layouts.css` oder `GUI/src/styles/global/surface-patterns.css` ergänzen.
|
||||||
|
|
||||||
|
## Aktueller Stand
|
||||||
|
- `GUI/src/Layout.vue` bildet die zentrale App-Shell mit Topbar, Sidebar, Footer, Routen-Kontext und responsivem Drawer-Verhalten.
|
||||||
|
- Darkmode (`light`/`dark`) ist global integriert (Toggle in der Topbar, Persistenz in `localStorage`, Theme-Tokens in CSS/Vuetify).
|
||||||
|
- Öffentliche Kernseiten sind im einheitlichen Hoard-Stil umgesetzt: `Home.vue` (Landingpage), `Login.vue`, `404NotFound.vue`, `Impressum.vue`.
|
||||||
|
- Das Topbar-Branding nutzt das App-Icon aus `GUI/src/assets/images/icon.svg`.
|
||||||
|
- Globale CSS-Struktur ist aktiv: `GUI/src/global.css` (Tokens/Basis) sowie `GUI/src/styles/global/page-layouts.css` und `GUI/src/styles/global/surface-patterns.css` für wiederverwendbare Patterns.
|
||||||
|
- Sidebar-Sichtbarkeit unterstützt `Visibility.Route` mit optionalem `visibilityRoute` in `GUI/src/plugins/routesLayout.ts`.
|
||||||
|
- Mobile-Touch-Optimierung ist für alle aktuellen öffentlichen Oberflächen aktiv (Shell, Home, Login, Impressum, 404), inklusive Safe-Area-Unterstützung.
|
||||||
|
- Desktop-Ansicht bleibt unverändert, da alle neuen Anpassungen ausschließlich in mobilen Breakpoints (`<= 960px`, Feinschliff `<= 600px`) umgesetzt sind.
|
||||||
|
- Backend-API ist auf ein Minimal-Setup reduziert und stellt aktuell den Test-Endpunkt `GET /api/health` bereit.
|
||||||
|
- Swagger/OpenAPI ist im Backend nur im Development-Modus aktiv (`/swagger`).
|
||||||
|
- Frontend-Build (`npm run build` im `GUI`-Projekt) schreibt direkt nach `API/wwwroot`; das Backend liefert die SPA und statische Assets aus.
|
||||||
|
- Backend nutzt jetzt PostgreSQL über `ConnectionStrings:Postgres` mit EF Core (`ApplicationDbContext`) und führt Migrationen beim Start automatisch aus.
|
||||||
|
- Temporäre Test-Entity und Test-CRUD-Endpunkt (`api/test-items`) wurden wieder entfernt; aktuell bleibt der minimale Health-Endpunkt `GET /api/health`.
|
||||||
|
- Für lokale Entwicklung liegt unter `API/Dev/docker-compose.yml` ein Stack mit PostgreSQL (`localhost:5432`) und pgAdmin (`localhost:5050`).
|
||||||
|
- API lädt optional `API/appsettings.custom.json`; wenn vorhanden, überschreibt sie Werte aus `appsettings.json`.
|
||||||
|
- `API/appsettings.custom.json` ist in `.gitignore` hinterlegt, damit lokale Konfigurationswerte nicht versehentlich committed werden.
|
||||||
|
|
||||||
|
## Änderungen durch Codex
|
||||||
|
- Grundlegender UI-Neuaufbau der App-Shell (`GUI/src/Layout.vue`) inklusive Navigation, Footer und Seitenkontext.
|
||||||
|
- Einführung eines globalen Theme-Managements (`light`/`dark`) über `GUI/src/plugins/vuetify.ts`, `GUI/src/global.css` und `localStorage`.
|
||||||
|
- Überarbeitung der zentralen öffentlichen Seiten (`Home.vue`, `Login.vue`, `404NotFound.vue`, `Impressum.vue`) auf ein einheitliches Hoard-Design.
|
||||||
|
- Erweiterung von `GUI/src/plugins/routesLayout.ts` um routeabhängige Sidebar-Sichtbarkeit (`Visibility.Route`, `visibilityRoute`).
|
||||||
|
- Konsolidierung der UI-Texte auf deutsche Umlaute gemäß Sprachregel.
|
||||||
|
- Aufbau und fortlaufende Konsolidierung der globalen CSS-Basis (`global.css`) inkl. Fokus-/Auswahl-Polish.
|
||||||
|
- CSS-Debloat-Refactor: gemeinsame Oberflächen-Patterns in `GUI/src/styles/global/page-layouts.css` und `GUI/src/styles/global/surface-patterns.css` ausgelagert und zentral in `GUI/src/main.ts` eingebunden.
|
||||||
|
- `codexInfo.md` um eine kompakte Nutzunganleitung für die globalen CSS-Patterns ergänzt.
|
||||||
|
- Mobile-Usability über globale Styles erweitert: größere Touch-Ziele (`v-btn`, Navigationspunkte), Safe-Area-Paddings und mobile Spacing-Feinschliff in `GUI/src/global.css` sowie den globalen Pattern-Dateien.
|
||||||
|
- `GUI/src/Layout.vue` für Mobile optimiert: entzerrte App-Bar-Abstände, touchfreundlicher Bottom-Sheet-Drawer und besser bedienbarer Footer auf kleinen Viewports.
|
||||||
|
- Mobile-spezifische Detailoptimierungen in `Home.vue`, `Login.vue`, `Impressum.vue` und `404NotFound.vue` ergänzt (Actions, Card-/Form-Spacing, CTA-Stacking), ohne Desktop-Basislayout zu verändern.
|
||||||
|
- `GUI/style.md` um einen verbindlichen Abschnitt „Umsetzungsstandard Responsivität“ ergänzt (Breakpoints, Touch-Zielgrößen, Safe-Area, globale Pattern-Nutzung, QA-Checkliste), damit Folgeaufgaben denselben Stil beibehalten.
|
||||||
|
- Topbar-Kontext in `GUI/src/Layout.vue` für schmalere Breiten beruhigt: auf Mobile wird der Seitenkontext komplett ausgeblendet, auf mittleren Breiten bleibt nur der Seitentitel (ohne Unterzeile), damit das Header-Layout sauber und nicht gequetscht wirkt.
|
||||||
|
- Backend-Template-Code bereinigt: `WeatherForecastController` und `WeatherForecast` entfernt, OpenAPI-Templatepaket aus `API/API.csproj` entfernt.
|
||||||
|
- Neuen Test-Controller `API/Controllers/HealthController.cs` angelegt (`GET /api/health`), der `200 OK` zurückgibt.
|
||||||
|
- `GUI/vite.config.ts` Build-Ausgabe auf `API/wwwroot` umgestellt (`outDir`) und Bereinigung des Zielordners beim Build aktiviert (`emptyOutDir: true`).
|
||||||
|
- `API/Program.cs` erweitert, damit statische Dateien aus `wwwroot` inkl. SPA-Fallback (`index.html`) ausgeliefert werden.
|
||||||
|
- SPA-Fallback im Backend aufgeteilt: Frontend-Routen liefern `index.html`, unbekannte `/api/*`-Routen bleiben korrekt `404` statt auf die SPA zu fallen.
|
||||||
|
- Swagger im Backend ergänzt: `Swashbuckle.AspNetCore` eingebunden, Services registriert und UI nur in `Development` aktiviert.
|
||||||
|
- PostgreSQL-Integration im Backend umgesetzt: `Npgsql.EntityFrameworkCore.PostgreSQL` + `Microsoft.EntityFrameworkCore.Design` in `API/API.csproj` ergänzt und Connection String `ConnectionStrings:Postgres` in den Settings hinterlegt.
|
||||||
|
- `API/Database/ApplicationDbContext.cs` mit Test-Entity `API/Models/Test/TestItem.cs` angelegt; erste Migrationen in `API/Migrations` erstellt.
|
||||||
|
- `API/Program.cs` um `AddDbContext` (Postgres) und `Database.Migrate()` beim Start erweitert, damit Migrationen automatisch angewendet werden.
|
||||||
|
- `API/Controllers/TestItemsController.cs` als einfacher CRUD-Testcontroller (`GET/POST/PUT/DELETE`) unter `api/test-items` ergänzt.
|
||||||
|
- Dev-Stack für lokale Datenbankarbeit ergänzt: `API/Dev/docker-compose.yml` startet PostgreSQL + pgAdmin.
|
||||||
|
- `API/Program.cs` so erweitert, dass optional `appsettings.custom.json` geladen wird und bei vorhandener Datei bevorzugte lokale Overrides möglich sind.
|
||||||
|
- Neue lokale Konfigurationsdatei `API/appsettings.custom.json` aus der bisherigen `appsettings.json` angelegt und in `.gitignore` ergänzt.
|
||||||
|
- Test-Datentyp und Test-API wieder entfernt: `API/Models/Test/TestItem.cs` und `API/Controllers/TestItemsController.cs` gelöscht, `ApplicationDbContext` bereinigt.
|
||||||
|
- Neue Migration `RemoveTestItems` erstellt (`API/Migrations/20260418153650_RemoveTestItems.cs`), die Tabelle `test_items` entfernt und den Snapshot aktualisiert.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"dotnet-ef": {
|
||||||
|
"version": "10.0.6",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-ef"
|
||||||
|
],
|
||||||
|
"rollForward": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user