mirror of
https://github.com/skoelle/PeopleCostCounter.git
synced 2026-09-18 12:20:24 +00:00
Compare commits
13
Commits
84d4b41ae3
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36847cc63a | ||
|
|
0f23a04592 | ||
|
|
3c90346982 | ||
|
|
055e81b8e7 | ||
|
|
cfd1ffdb29 | ||
|
|
258fb956a0 | ||
|
|
87ed08c458 | ||
|
|
9fc8d3663f | ||
|
|
e5608a35e3 | ||
|
|
16cb63981a | ||
|
|
ace92a048c | ||
|
|
0c60c5b982 | ||
|
|
045215a760 |
@@ -10,11 +10,11 @@ jobs:
|
|||||||
html-lint:
|
html-lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v7
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '24'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
- name: Run html-validate
|
- name: Run html-validate
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
title: "PeopleCostCounter"
|
||||||
|
emoji: "💰"
|
||||||
|
category: code
|
||||||
|
subcategory: "Dev Tools"
|
||||||
|
status: active
|
||||||
|
stack: [HTML, CSS, JavaScript]
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
# AGENTS.md — PeopleCostCounter
|
||||||
|
|
||||||
|
This file guides future AI sessions and contributors working on this project.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**PeopleCostCounter** ("Meeting Cost Tracker") is a tiny, self-contained, single-page HTML
|
||||||
|
app that calculates and displays — in real time — how much a meeting is costing while it
|
||||||
|
runs. It takes the number of developers present and the average monthly gross salary,
|
||||||
|
then counts up a running total cost once the user presses **Start**.
|
||||||
|
|
||||||
|
- **No build step.** Each `.html` file is a complete, standalone page.
|
||||||
|
- **No runtime dependencies.** Only vanilla HTML, CSS, and JavaScript.
|
||||||
|
- **No external JS libraries.** Google Fonts (Inter / JetBrains Mono) are loaded via CDN;
|
||||||
|
pages work fine if fonts are blocked.
|
||||||
|
- **No persistence.** No localStorage, cookies, or server-side code.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
PeopleCostCounter/
|
||||||
|
├── LICENSE # MIT License
|
||||||
|
├── README.md # User-facing documentation
|
||||||
|
├── package.json # Dev tooling config (html-validate only)
|
||||||
|
├── .htmlvalidate.json # HTML validation config/rules
|
||||||
|
├── screenshot.png # Promotional screenshot
|
||||||
|
├── meeting-cost-prompt.md # Original development prompt / notes
|
||||||
|
├── meeting-tracker-de.html # Canonical build — German UI
|
||||||
|
├── meeting-tracker-en.html # Canonical build — English UI
|
||||||
|
├── .github/
|
||||||
|
│ └── workflows/
|
||||||
|
│ └── html-lint.yml # CI: runs html-validate on push/PR
|
||||||
|
└── variants/
|
||||||
|
├── meeting-tracker-variant1.html # Experimental — dark theme
|
||||||
|
├── meeting-tracker-variant2.html # Experimental — dark gradient theme
|
||||||
|
└── meeting-tracker-variant3.html # Experimental — alternate styling
|
||||||
|
```
|
||||||
|
|
||||||
|
### Canonical vs. Variant Files
|
||||||
|
|
||||||
|
- `meeting-tracker-de.html` and `meeting-tracker-en.html` at the repository root are the
|
||||||
|
**canonical builds**. Changes to the tracker should generally be applied to both.
|
||||||
|
- Files in `variants/` are **experimental or alternative designs** and may diverge from
|
||||||
|
the canonical builds. Use them as a reference for styling ideas, but prefer editing the
|
||||||
|
canonical files for functional changes.
|
||||||
|
- `meeting-cost-prompt.md` is an internal development artifact (the original prompt used to
|
||||||
|
generate the tracker). It is not part of the public UI and can be referenced for
|
||||||
|
context on design decisions and the cost formula.
|
||||||
|
|
||||||
|
## Cost Formula
|
||||||
|
|
||||||
|
```
|
||||||
|
AnnualSalary = AvgMonthlySalary × 12
|
||||||
|
AnnualSalaryWithEmployer = AnnualSalary × 1.2 # 20% employer overhead
|
||||||
|
TotalAnnualCost = AnnualSalaryWithEmployer × DevelopersPresent
|
||||||
|
CostPerSecond = TotalAnnualCost ÷ 220 ÷ 8 ÷ 60 ÷ 60
|
||||||
|
```
|
||||||
|
|
||||||
|
- **220 days** = approximate working days per year (accounts for weekends and holidays).
|
||||||
|
- **8 hours** = hours per working day.
|
||||||
|
- **1.2 multiplier** = simplified German employer contribution factor (~20%). This is an
|
||||||
|
approximation, not payroll-grade accounting.
|
||||||
|
- See the README "What it calculates" section for full context and references.
|
||||||
|
|
||||||
|
### Test Values
|
||||||
|
|
||||||
|
| Developers | Monthly Salary | Cost/sec | Cost after 60s |
|
||||||
|
|------------|----------------|---------------|-----------------|
|
||||||
|
| 8 | 5,000 € | ~0.0909 € | ~5.45 € |
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Linting
|
||||||
|
|
||||||
|
HTML validation is performed with [html-validate](https://html-validate.org/).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install (creates node_modules)
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Lint all HTML files
|
||||||
|
npx html-validate "**/*.html"
|
||||||
|
|
||||||
|
# Via npm script
|
||||||
|
npm run lint:html
|
||||||
|
```
|
||||||
|
|
||||||
|
The CI workflow (`.github/workflows/html-lint.yml`) runs `npx html-validate "**/*.html"`
|
||||||
|
on every push and pull request targeting `main`. Ensure linting passes before committing.
|
||||||
|
|
||||||
|
### Local Preview
|
||||||
|
|
||||||
|
There is no dev server. Simply open any HTML file directly in a browser:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Or use any static file server / Live Server extension
|
||||||
|
open meeting-tracker-en.html
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding Features or Fixing Bugs
|
||||||
|
|
||||||
|
1. Apply changes to **both** canonical files (`meeting-tracker-de.html` and
|
||||||
|
`meeting-tracker-en.html`) to keep them in sync.
|
||||||
|
2. If the change is language-specific (e.g., German number formatting vs. English),
|
||||||
|
update the relevant locale string only.
|
||||||
|
3. Run `npx html-validate "**/*.html"` to verify the HTML is still valid.
|
||||||
|
4. Test the cost calculation with the test values above.
|
||||||
|
|
||||||
|
### Branching Conventions
|
||||||
|
|
||||||
|
- Create branches named `feature/...` or `fix/...` for changes.
|
||||||
|
- Prefer small, focused pull requests.
|
||||||
|
|
||||||
|
## Code Conventions
|
||||||
|
|
||||||
|
### HTML Files
|
||||||
|
|
||||||
|
- Each source file begins with two license header comment lines:
|
||||||
|
```html
|
||||||
|
<!-- Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de) -->
|
||||||
|
<!-- Licensed under the MIT License. See LICENSE file in project root for details. -->
|
||||||
|
```
|
||||||
|
- Inline CSS and JavaScript are used (no external `.css` or `.js` files) to keep each page
|
||||||
|
fully self-contained.
|
||||||
|
- Inline styles are permitted (the `.htmlvalidate.json` config disables the
|
||||||
|
`no-inline-style` rule).
|
||||||
|
- Semantic HTML is preferred: `<header>`, `<main>`, `<section>`, `<form>`, `<label>`,
|
||||||
|
`<button>`, etc.
|
||||||
|
- Accessibility: buttons have `aria-label` attributes where the visual label is icon-only.
|
||||||
|
SVG icons use `aria-hidden="true"`.
|
||||||
|
|
||||||
|
### JavaScript
|
||||||
|
|
||||||
|
- Vanilla JavaScript (no frameworks or libraries).
|
||||||
|
- The English (`meeting-tracker-en.html`) version includes a **theme toggle** (light/dark)
|
||||||
|
and a **pause/resume** button.
|
||||||
|
- The German (`meeting-tracker-de.html`) version uses **Start/Reset** (no pause feature).
|
||||||
|
- Event handlers are attached via `onclick` in the HTML for buttons (e.g.,
|
||||||
|
`onclick="handleBtn()"`).
|
||||||
|
- Number formatting uses `Intl.NumberFormat` with `de-DE` locale (German) or `en-GB` locale
|
||||||
|
(English), both with `currency: 'EUR'`.
|
||||||
|
- The cost counter ticks via `setInterval` at 100ms for the running display and updates
|
||||||
|
the per-second display on input change.
|
||||||
|
|
||||||
|
### CSS
|
||||||
|
|
||||||
|
- CSS custom properties (variables) are defined in `:root` for color palette, typography,
|
||||||
|
spacing, and shadows.
|
||||||
|
- Both light and dark themes are supported via the `data-theme` attribute on `<html>`.
|
||||||
|
- Use `clamp()` for responsive font sizes where appropriate (e.g., the main cost display).
|
||||||
|
|
||||||
|
### `.htmlvalidate.json`
|
||||||
|
|
||||||
|
This file configures html-validate. The following rules are relaxed for this project:
|
||||||
|
|
||||||
|
| Rule | Setting | Reason |
|
||||||
|
|-----------------------|:-------:|----------------------------------|
|
||||||
|
| `no-implicit-button-type` | off | Buttons use default styling |
|
||||||
|
| `no-inline-style` | off | Inline styles are used throughout |
|
||||||
|
| `void-style` | off | Mixed HTML5 void element styles |
|
||||||
|
| `doctype-style` | off | Mixed `<!DOCTYPE html>` and `<!doctype html>` |
|
||||||
|
| `aria-label-misuse` | off | Lenient ARIA checking |
|
||||||
|
| `no-redundant-aria-label` | off | Lenient ARIA checking |
|
||||||
|
|
||||||
|
## Adding New Files
|
||||||
|
|
||||||
|
- Source code files should include the appropriate license header comment at the top.
|
||||||
|
- For HTML files:
|
||||||
|
```html
|
||||||
|
<!-- Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de) -->
|
||||||
|
<!-- Licensed under the MIT License. See LICENSE file in project root for details. -->
|
||||||
|
```
|
||||||
|
- For files with a shebang line (e.g., Python `.py`), insert the license header **after**
|
||||||
|
the shebang.
|
||||||
|
- Skip files that already contain the header.
|
||||||
|
- Markdown, JSON, package config files, and workflow files typically do not require headers.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
- Full text in `LICENSE`
|
||||||
|
- License headers in all source code files
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2026 S. Koelle
|
Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,91 +1,133 @@
|
|||||||
# Meeting Cost Tracker
|
# 💰 Meeting Cost Tracker
|
||||||
|
|
||||||
A small single-page tool that shows, in real time, how much a meeting is costing while it runs.
|
> A small single-page tool that shows, **in real time**, how much a meeting is costing while it runs. 💸
|
||||||
|
|
||||||
It is intentionally simple: enter how many developers are present, enter the average monthly gross salary, press **Start**, and the page begins counting the cost every second.
|
It is intentionally simple: enter how many developers are present, enter the average monthly gross salary, press **Start**, and the page begins counting the cost every second. ⏱️
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Quick start
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick start
|
||||||
|
|
||||||
Open the tracker in your browser. The project is published via GitHub Pages — live demo URLs:
|
Open the tracker in your browser. The project is published via GitHub Pages — live demo URLs:
|
||||||
|
|
||||||
- English UI: https://skoelle.github.io/PeopleCostCounter/meeting-tracker-en.html
|
- 🇬🇧 English UI: https://skoelle.github.io/PeopleCostCounter/meeting-tracker-en.html
|
||||||
- German UI: https://skoelle.github.io/PeopleCostCounter/meeting-tracker-de.html
|
- 🇩🇪 German UI: https://skoelle.github.io/PeopleCostCounter/meeting-tracker-de.html
|
||||||
|
|
||||||
You can also open the local HTML files directly in a browser (no build step required). Recommended browsers: Chrome, Edge, Firefox.
|
You can also open the local HTML files directly in a browser (no build step required). Recommended browsers: Chrome, Edge, Firefox.
|
||||||
|
|
||||||
If you prefer a live-reload development workflow, use an editor extension such as Live Server (VS Code) or any simple static file server.
|
If you prefer a live-reload development workflow, use an editor extension such as Live Server (VS Code) or any simple static file server.
|
||||||
|
|
||||||
## Controls
|
---
|
||||||
|
|
||||||
- **Start** begins the live counter at zero.
|
## 🎮 Controls
|
||||||
- **Reset** stops the counter and sets the accumulated cost back to zero.
|
|
||||||
- The cost per second recalculates when inputs change; the accumulated cost updates once per second while running.
|
|
||||||
|
|
||||||
## Files of interest
|
| Button | Action |
|
||||||
|
|-----------|--------|
|
||||||
|
| ▶️ **Start** | Begins the live counter at zero. |
|
||||||
|
| 🔄 **Reset** | Stops the counter and sets the accumulated cost back to zero. |
|
||||||
|
|
||||||
- `meeting-tracker-de.html` / `meeting-tracker-en.html` — canonical builds (German / English)
|
The cost per second recalculates when inputs change; the accumulated cost updates once per second while running.
|
||||||
|
|
||||||
Experimental variants (moved to `variants/`):
|
|
||||||
|
|
||||||
- `variants/meeting-tracker-variant1.html`
|
|
||||||
- `variants/meeting-tracker-variant2.html`
|
|
||||||
- `variants/meeting-tracker-variant3.html`
|
|
||||||
|
|
||||||
- `meeting-cost-prompt.md` — internal prompt and notes used while developing the tracker
|
|
||||||
|
|
||||||
## Browser & dependencies
|
|
||||||
|
|
||||||
- No build tool or runtime dependencies. The pages use only vanilla HTML/CSS/JS.
|
|
||||||
- Google Fonts are used for typography (Inter / JetBrains Mono); pages work fine if fonts are blocked.
|
|
||||||
|
|
||||||
## Example (quick test)
|
|
||||||
|
|
||||||
- Developers: `8`
|
|
||||||
- Avg. monthly: `5000`
|
|
||||||
- Expected: annual per-dev = `60.000 €`, incl. employer ≈ `72.000 €`, total for 8 ≈ `576.000 €`, cost/sec ≈ `0.0909 €`, after 60s ≈ `5,45 €`.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## What it calculates
|
## 📁 Files of interest
|
||||||
|
|
||||||
The tracker estimates meeting cost from salary, employer overhead, team size, and elapsed time. The calculation is based on the idea that one developer’s salary can be converted into an annual employer cost, then into a working-hour and working-second cost. Similar meeting-cost tools commonly start from annual salary, convert it to an hourly rate, and multiply by meeting duration and headcount. [meetingtoll](https://www.meetingtoll.com/blog/meeting-cost-formula-per-employee)
|
| File | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `meeting-tracker-de.html` | 🇩🇪 Canonical build — German UI |
|
||||||
|
| `meeting-tracker-en.html` | 🇬🇧 Canonical build — English UI |
|
||||||
|
|
||||||
### Formula
|
### 🧪 Experimental variants (in `variants/`)
|
||||||
|
|
||||||
The app uses this formula:
|
- `meeting-tracker-variant1.html`
|
||||||
|
- `meeting-tracker-variant2.html`
|
||||||
|
- `meeting-tracker-variant3.html`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Browser & dependencies
|
||||||
|
|
||||||
|
- ✅ No build tool or runtime dependencies — only vanilla HTML/CSS/JS.
|
||||||
|
- 🔤 Google Fonts are used for typography (Inter / JetBrains Mono); pages work fine if fonts are blocked.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧮 Example (quick test)
|
||||||
|
|
||||||
|
| Input | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| 👥 Developers | `8` |
|
||||||
|
| 💶 Avg. monthly salary | `5000` |
|
||||||
|
|
||||||
|
**Expected output:**
|
||||||
|
|
||||||
|
- 📊 Annual per-dev = `60.000 €`
|
||||||
|
- 🏢 incl. employer ≈ `72.000 €`
|
||||||
|
- 🧾 Total for 8 ≈ `576.000 €`
|
||||||
|
- ⏱️ Cost/sec ≈ `0.0909 €`
|
||||||
|
- 💣 After 60s ≈ `5,45 €`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 What it calculates
|
||||||
|
|
||||||
|
The tracker estimates meeting cost from salary, employer overhead, team size, and elapsed time. The calculation is based on the idea that one developer's salary can be converted into an annual employer cost, then into a working-hour and working-second cost.
|
||||||
|
|
||||||
|
Similar meeting-cost tools commonly start from annual salary, convert it to an hourly rate, and multiply by meeting duration and headcount. 📐
|
||||||
|
|
||||||
|
> 🔗 [meetingtoll.com — Meeting cost formula per employee](https://www.meetingtoll.com/blog/meeting-cost-formula-per-employee)
|
||||||
|
|
||||||
|
### 📝 Formula
|
||||||
|
|
||||||
```
|
```
|
||||||
AnnualSalary = AvgMonthlySalary × 12
|
AnnualSalary = AvgMonthlySalary × 12
|
||||||
AnnualSalaryWithEmployerContribution = AnnualSalary × 1.2
|
AnnualSalaryWithEmployer = AnnualSalary × 1.2
|
||||||
TotalAnnualCost = AnnualSalaryWithEmployerContribution × DevelopersPresent
|
TotalAnnualCost = AnnualSalaryWithEmployer × DevelopersPresent
|
||||||
CostPerSecond = TotalAnnualCost ÷ 220 ÷ 8 ÷ 60 ÷ 60
|
CostPerSecond = TotalAnnualCost ÷ 220 ÷ 8 ÷ 60 ÷ 60
|
||||||
```
|
```
|
||||||
|
|
||||||
The 1.2 multiplier is a simplified overhead factor for employer contributions, which is in the same rough range as public Germany payroll references that place employer burden around 20–23% above gross salary. [boundlesshq](https://boundlesshq.com/blog/payroll-in-germany/)
|
The **1.2 multiplier** is a simplified overhead factor for employer contributions (~20% above gross salary). 📈
|
||||||
|
|
||||||
### Why 220 days
|
> 🔗 [boundlesshq.com — Payroll in Germany](https://boundlesshq.com/blog/payroll-in-germany/)
|
||||||
|
|
||||||
The app divides by 220 working days instead of 365 calendar days. That makes the result closer to actual working time, because meetings happen during paid work, not across the full calendar year. [capme](https://www.capme.app/meeting-cost-calculator)
|
### 🗓️ Why 220 days?
|
||||||
|
|
||||||
Using 220 days, 8 hours per day, and 60 minutes per hour is a practical simplification that turns annual cost into a live per-second burn rate. It is not a payroll-grade accounting model, but it is a useful way to visualize meeting cost in real time. [meetingking](https://meetingking.com/meeting-cost-calculator/)
|
The app divides by **220 working days** instead of 365 calendar days — making the result closer to actual working time, because meetings happen during paid work, not across the full calendar year. 🏖️
|
||||||
|
|
||||||
## Notes
|
> 🔗 [capme.app — Meeting Cost Calculator](https://www.capme.app/meeting-cost-calculator)
|
||||||
|
|
||||||
This tool is designed to make meeting costs visible, not to produce exact payroll accounting. In real companies, the true employer cost can vary by insurance rates, caps, bonuses, and other overhead, so the 1.2 factor should be understood as a simple approximation. [payrollgermany](https://payrollgermany.de/blog/employer-contributions-to-social-security-in-germany-a-comprehensive-guide/)
|
Using 220 days, 8 hours per day, and 60 minutes per hour is a practical simplification that turns annual cost into a live per-second burn rate. It is not a payroll-grade accounting model, but it is a useful way to visualize meeting cost in real time.
|
||||||
|
|
||||||
The result is best used as a conversation starter: it helps teams notice how quickly meeting time turns into money.
|
> 🔗 [meetingking.com — Meeting Cost Calculator](https://meetingking.com/meeting-cost-calculator/)
|
||||||
|
|
||||||
## Contributing
|
---
|
||||||
|
|
||||||
- Prefer small, focused pull requests. Create a branch named `feature/...` or `fix/...` for changes.
|
## 📌 Notes
|
||||||
- If you want me to push changes, tell me whether to create a PR or commit directly to `main`.
|
|
||||||
|
|
||||||
## License
|
This tool is designed to make meeting costs **visible**, not to produce exact payroll accounting. In real companies, the true employer cost can vary by insurance rates, caps, bonuses, and other overhead — so the 1.2 factor should be understood as a simple approximation. 🤏
|
||||||
|
|
||||||
This repository does not yet include a LICENSE file. If you want a permissive license, I can add an `MIT` license file — tell me if that is acceptable or specify another license.
|
> 🔗 [payrollgermany.de — Employer contributions in Germany](https://payrollgermany.de/blog/employer-contributions-to-social-security-in-germany-a-comprehensive-guide/)
|
||||||
|
|
||||||
## Author / Contact
|
The result is best used as a **conversation starter**: it helps teams notice how quickly meeting time turns into money. 💡
|
||||||
|
|
||||||
If you need changes, open an issue or contact the repository owner on GitHub.
|
---
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
- 🌿 Prefer small, focused pull requests.
|
||||||
|
- 🏷️ Create a branch named `feature/...` or `fix/...` for changes.
|
||||||
|
- 📩 If you want me to push changes, tell me whether to create a PR or commit directly to `main`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
Licensed under the [MIT License](LICENSE) — Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
Made with ❤️ for better meetings
|
||||||
|
</p>
|
||||||
|
|||||||
+10
-5
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de) -->
|
||||||
|
<!-- Licensed under the MIT License. See LICENSE file in project root for details. -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de" data-theme="light">
|
<html lang="de" data-theme="light">
|
||||||
<head>
|
<head>
|
||||||
@@ -208,7 +210,7 @@ function getParams(){
|
|||||||
var annualWithEmployer=annualSalary*1.2;
|
var annualWithEmployer=annualSalary*1.2;
|
||||||
var totalCost=annualWithEmployer*devCount;
|
var totalCost=annualWithEmployer*devCount;
|
||||||
var costPerSecond=totalCost/220/8/60/60;
|
var costPerSecond=totalCost/220/8/60/60;
|
||||||
return{devCount:devCount,avgSalary:avgSalary,totalCost:totalCost,costPerSecond:costPerSecond};
|
return{devCount:devCount,avgSalary:avgSalary,annualSalary:annualSalary,annualWithEmployer:annualWithEmployer,totalCost:totalCost,costPerSecond:costPerSecond};
|
||||||
}
|
}
|
||||||
function tick(){
|
function tick(){
|
||||||
var elapsed=pausedElapsed+(Date.now()-startTime)/1000;
|
var elapsed=pausedElapsed+(Date.now()-startTime)/1000;
|
||||||
@@ -220,10 +222,9 @@ function tick(){
|
|||||||
document.getElementById('tilePerMin').textContent='EUR '+fmt(p.costPerSecond*60);
|
document.getElementById('tilePerMin').textContent='EUR '+fmt(p.costPerSecond*60);
|
||||||
document.getElementById('tilePerHour').textContent='EUR '+fmt(p.costPerSecond*3600);
|
document.getElementById('tilePerHour').textContent='EUR '+fmt(p.costPerSecond*3600);
|
||||||
document.getElementById('tileTotal').textContent=p.devCount+' x EUR '+fmt(p.avgSalary);
|
document.getElementById('tileTotal').textContent=p.devCount+' x EUR '+fmt(p.avgSalary);
|
||||||
// show annual / employer / total year values (as integers)
|
var annual = p.annualSalary;
|
||||||
var annual = p.annualSalary || p.avgSalary*12;
|
var employer = p.annualWithEmployer;
|
||||||
var employer = p.annualWithEmployer || annual*1.2;
|
var annualTotal = p.totalCost;
|
||||||
var annualTotal = p.totalCost || employer * p.devCount;
|
|
||||||
document.getElementById('tileAnnual').textContent = annual.toLocaleString('de-DE',{minimumFractionDigits:0,maximumFractionDigits:0}) + ' €';
|
document.getElementById('tileAnnual').textContent = annual.toLocaleString('de-DE',{minimumFractionDigits:0,maximumFractionDigits:0}) + ' €';
|
||||||
document.getElementById('tileEmployer').textContent = employer.toLocaleString('de-DE',{minimumFractionDigits:0,maximumFractionDigits:0}) + ' €';
|
document.getElementById('tileEmployer').textContent = employer.toLocaleString('de-DE',{minimumFractionDigits:0,maximumFractionDigits:0}) + ' €';
|
||||||
document.getElementById('tileAnnualTotal').textContent = annualTotal.toLocaleString('de-DE',{minimumFractionDigits:0,maximumFractionDigits:0}) + ' €';
|
document.getElementById('tileAnnualTotal').textContent = annualTotal.toLocaleString('de-DE',{minimumFractionDigits:0,maximumFractionDigits:0}) + ' €';
|
||||||
@@ -268,6 +269,10 @@ function resetTimer(){
|
|||||||
document.getElementById('avgSalary').disabled=false;
|
document.getElementById('avgSalary').disabled=false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// input listeners: update per-second display when editing
|
||||||
|
document.getElementById('devCount').addEventListener('input', function(){ if(!isRunning){ var p=getParams(); if(p.costPerSecond>0) document.getElementById('tilePerSec').textContent='EUR '+p.costPerSecond.toLocaleString('de-DE',{minimumFractionDigits:4,maximumFractionDigits:4}); }});
|
||||||
|
document.getElementById('avgSalary').addEventListener('input', function(){ if(!isRunning){ var p=getParams(); if(p.costPerSecond>0) document.getElementById('tilePerSec').textContent='EUR '+p.costPerSecond.toLocaleString('de-DE',{minimumFractionDigits:4,maximumFractionDigits:4}); }});
|
||||||
|
|
||||||
// No auto-start: user must press Start. (Previously auto-started here.)
|
// No auto-start: user must press Start. (Previously auto-started here.)
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+223
-115
@@ -1,69 +1,129 @@
|
|||||||
|
<!-- Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de) -->
|
||||||
|
<!-- Licensed under the MIT License. See LICENSE file in project root for details. -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-theme="light">
|
<html lang="en" data-theme="light">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Meeting Cost Tracker — English</title>
|
<title>Meeting Cost Tracker — English</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300..700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300..700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
/* same styles as variant2 (kept compact) */
|
:root, [data-theme="light"] {
|
||||||
:root, [data-theme="light"] { --color-bg:#f7f6f2;--color-surface:#f9f8f5;--color-surface-2:#fbfbf9;--color-surface-offset:#f3f0ec;--color-divider:#dcd9d5;--color-border:#d4d1ca;--color-text:#28251d;--color-text-muted:#7a7974;--color-text-faint:#bab9b4;--color-primary:#01696f;--color-primary-hover:#0c4e54;--color-warning:#964219;--color-success:#437a22;--shadow-sm:0 1px 2px rgba(0,0,0,0.06);--shadow-md:0 4px 12px rgba(0,0,0,0.08);--radius-md:.5rem;--radius-lg:.75rem;--radius-xl:1rem;--text-xs:.75rem;--text-sm:0.9rem;--text-base:1rem;--space-3:.75rem;--space-4:1rem;--space-6:1.5rem;--space-8:2rem;--font-body:'Inter',system-ui,sans-serif;--font-mono:'JetBrains Mono',monospace}
|
--color-bg:#f7f6f2;--color-surface:#f9f8f5;--color-surface-2:#fbfbf9;
|
||||||
[data-theme="dark"]{--color-bg:#171614;--color-surface:#1c1b19;--color-text:#cdccca}
|
--color-surface-offset:#f3f0ec;--color-divider:#dcd9d5;--color-border:#d4d1ca;
|
||||||
*{box-sizing:border-box;margin:0;padding:0} body{min-height:100dvh;font-family:var(--font-body);font-size:var(--text-base);color:var(--color-text);background:var(--color-bg);display:flex;flex-direction:column;align-items:center;padding:var(--space-8) var(--space-4)}
|
--color-text:#28251d;--color-text-muted:#7a7974;--color-text-faint:#bab9b4;
|
||||||
.app-header{width:100%;max-width:720px;display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-8)}
|
--color-primary:#01696f;--color-primary-hover:#0c4e54;
|
||||||
.logo-text{font-size:1.125rem;font-weight:600}
|
--color-warning:#964219;--color-success:#437a22;
|
||||||
.logo-sub{font-size:0.85rem;color:var(--color-text-muted)}
|
--shadow-sm:0 1px 2px oklch(0.2 0.01 80/0.06);
|
||||||
.theme-toggle{width:36px;height:36px;border-radius:.5rem;display:flex;align-items:center;justify-content:center;border:1px solid rgba(0,0,0,0.06);background:transparent}
|
--shadow-md:0 4px 12px oklch(0.2 0.01 80/0.08);
|
||||||
.card{width:100%;max-width:720px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-xl);padding:var(--space-8);box-shadow:var(--shadow-md)}
|
--shadow-lg:0 12px 32px oklch(0.2 0.01 80/0.12);
|
||||||
.form-row{display:grid;grid-template-columns:1fr 1fr auto;gap:1rem;align-items:end;margin-bottom:1.5rem}
|
--radius-sm:.375rem;--radius-md:.5rem;--radius-lg:.75rem;--radius-xl:1rem;--radius-full:9999px;
|
||||||
.form-field{display:flex;flex-direction:column;gap:.5rem}
|
--text-xs:clamp(.75rem,.7rem + .25vw,.875rem);
|
||||||
.form-label{font-size:.75rem;color:var(--color-text-muted);text-transform:uppercase}
|
--text-sm:clamp(.875rem,.8rem + .35vw,1rem);
|
||||||
.form-input{padding:.75rem 1rem;background:var(--color-bg);border:1px solid var(--color-border);border-radius:.5rem}
|
--text-base:clamp(1rem,.95rem + .25vw,1.125rem);
|
||||||
.input-prefix{position:absolute;left:1rem;top:50%;transform:translateY(-50%);color:var(--color-text-muted)}
|
--text-lg:clamp(1.125rem,1rem + .75vw,1.5rem);
|
||||||
.btn-start{padding:.6rem 1.2rem;background:var(--color-primary);color:#fff;border-radius:.5rem;display:flex;align-items:center;gap:.5rem}
|
--text-xl:clamp(1.5rem,1.2rem + 1.25vw,2.25rem);
|
||||||
.btn-reset{padding:.4rem .75rem;border:1px solid var(--color-border);border-radius:.5rem}
|
--space-1:.25rem;--space-2:.5rem;--space-3:.75rem;--space-4:1rem;
|
||||||
.divider{height:1px;background:var(--color-divider);margin:1.25rem 0}
|
--space-5:1.25rem;--space-6:1.5rem;--space-8:2rem;
|
||||||
.status-badge{display:inline-flex;align-items:center;gap:.5rem;padding:.25rem .75rem;border-radius:9999px;background:var(--color-surface-offset);color:var(--color-text-muted);border:1px solid var(--color-border)}
|
--font-body:'Inter','Helvetica Neue',sans-serif;
|
||||||
.running-indicator{width:10px;height:10px;border-radius:9999px;background:currentColor}
|
--font-mono:'JetBrains Mono','Courier New',monospace;
|
||||||
.running-indicator.active{animation:pulse 1.2s infinite}@keyframes pulse{0%{opacity:1}50%{opacity:.3}100%{opacity:1}}
|
--transition:180ms cubic-bezier(.16,1,.3,1);
|
||||||
.cost-display-wrap{position:relative;background:var(--color-surface-offset);border:1px solid var(--color-border);border-radius:1rem;padding:2rem;text-align:center;min-height:160px;display:flex;flex-direction:column;align-items:center;justify-content:center}
|
}
|
||||||
.cost-display-amount{font-family:var(--font-mono);font-size:clamp(2.5rem,6vw,4.5rem);font-weight:700}
|
[data-theme="dark"] {
|
||||||
.info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:1rem;margin-top:1.5rem}
|
--color-bg:#171614;--color-surface:#1c1b19;--color-surface-2:#201f1d;
|
||||||
.info-tile{background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:.5rem;padding:1rem}
|
--color-surface-offset:#22211f;--color-divider:#262523;--color-border:#393836;
|
||||||
.info-tile-label{font-size:.75rem;color:var(--color-text-muted);text-transform:uppercase;margin-bottom:.5rem}
|
--color-text:#cdccca;--color-text-muted:#797876;--color-text-faint:#5a5957;
|
||||||
.info-tile-value{font-family:var(--font-mono);font-weight:600}
|
--color-primary:#4f98a3;--color-primary-hover:#227f8b;
|
||||||
</style>
|
--color-warning:#bb653b;--color-success:#6daa45;
|
||||||
|
--shadow-sm:0 1px 2px oklch(0 0 0/.2);
|
||||||
|
--shadow-md:0 4px 12px oklch(0 0 0/.3);
|
||||||
|
--shadow-lg:0 12px 32px oklch(0 0 0/.4);
|
||||||
|
}
|
||||||
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
html{-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
|
||||||
|
body{min-height:100dvh;font-family:var(--font-body);font-size:var(--text-base);color:var(--color-text);background:var(--color-bg);display:flex;flex-direction:column;align-items:center;padding:var(--space-8) var(--space-4)}
|
||||||
|
input,button{font:inherit;color:inherit}
|
||||||
|
a,button,input{transition:color var(--transition),background var(--transition),border-color var(--transition),box-shadow var(--transition)}
|
||||||
|
button{cursor:pointer;background:none;border:none}
|
||||||
|
.app-header{width:100%;max-width:720px;display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-8)}
|
||||||
|
.logo{display:flex;align-items:center;gap:var(--space-3)}
|
||||||
|
.logo-icon{color:var(--color-primary)}
|
||||||
|
.logo-text{font-size:var(--text-lg);font-weight:600;color:var(--color-text);letter-spacing:-.02em}
|
||||||
|
.logo-sub{font-size:var(--text-xs);color:var(--color-text-muted);margin-top:1px}
|
||||||
|
.theme-toggle{width:36px;height:36px;border-radius:var(--radius-md);display:flex;align-items:center;justify-content:center;color:var(--color-text-muted);border:1px solid oklch(from var(--color-text) l c h/.1)}
|
||||||
|
.theme-toggle:hover{color:var(--color-text);background:var(--color-surface)}
|
||||||
|
.card{width:100%;max-width:720px;background:var(--color-surface);border:1px solid oklch(from var(--color-text) l c h/.08);border-radius:var(--radius-xl);padding:var(--space-8);box-shadow:var(--shadow-md)}
|
||||||
|
.form-row{display:grid;grid-template-columns:1fr 1fr auto;gap:var(--space-4);align-items:end;margin-bottom:var(--space-6)}
|
||||||
|
@media(max-width:600px){.form-row{grid-template-columns:1fr}}
|
||||||
|
.form-field{display:flex;flex-direction:column;gap:var(--space-2)}
|
||||||
|
.form-label{font-size:var(--text-xs);font-weight:600;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:.06em}
|
||||||
|
.form-input{padding:var(--space-3) var(--space-4);background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius-md);font-size:var(--text-base);color:var(--color-text);outline:none;width:100%}
|
||||||
|
.form-input:focus{border-color:var(--color-primary);box-shadow:0 0 0 3px oklch(from var(--color-primary) l c h/.15)}
|
||||||
|
.form-input:disabled{opacity:.5;cursor:not-allowed}
|
||||||
|
.form-hint{font-size:var(--text-xs);color:var(--color-text-faint);margin-top:var(--space-1)}
|
||||||
|
.input-prefix-wrap{position:relative}
|
||||||
|
.input-prefix-wrap .form-input{padding-left:var(--space-8)}
|
||||||
|
.input-prefix{position:absolute;left:var(--space-3);top:50%;transform:translateY(-50%);color:var(--color-text-muted);font-size:var(--text-sm);pointer-events:none;line-height:1}
|
||||||
|
.btn-start{padding:var(--space-3) var(--space-6);background:var(--color-primary);color:#fff;border-radius:var(--radius-md);font-size:var(--text-sm);font-weight:600;white-space:nowrap;display:flex;align-items:center;gap:var(--space-2);box-shadow:var(--shadow-sm);height:44px}
|
||||||
|
.btn-start:hover{background:var(--color-primary-hover);box-shadow:var(--shadow-md)}
|
||||||
|
.btn-start:active{transform:scale(.98)}
|
||||||
|
.btn-reset{padding:var(--space-2) var(--space-4);border:1px solid var(--color-border);border-radius:var(--radius-md);font-size:var(--text-xs);color:var(--color-text-muted);font-weight:500;display:flex;align-items:center;gap:var(--space-2)}
|
||||||
|
.btn-reset:hover{color:var(--color-text);border-color:var(--color-text-muted)}
|
||||||
|
.divider{height:1px;background:var(--color-divider);margin:var(--space-6) 0}
|
||||||
|
.action-row{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--space-3);margin-bottom:var(--space-6)}
|
||||||
|
.status-badge{display:inline-flex;align-items:center;gap:var(--space-2);padding:var(--space-1) var(--space-3);border-radius:var(--radius-full);font-size:var(--text-xs);font-weight:600;letter-spacing:.04em;text-transform:uppercase;background:var(--color-surface-offset);color:var(--color-text-muted);border:1px solid var(--color-border)}
|
||||||
|
.status-badge.running{background:color-mix(in oklch,var(--color-warning) 12%,var(--color-surface));color:var(--color-warning);border-color:color-mix(in oklch,var(--color-warning) 30%,var(--color-border))}
|
||||||
|
.status-badge.stopped{background:color-mix(in oklch,var(--color-success) 10%,var(--color-surface));color:var(--color-success);border-color:color-mix(in oklch,var(--color-success) 30%,var(--color-border))}
|
||||||
|
.running-indicator{width:10px;height:10px;border-radius:var(--radius-full);background:currentColor;display:inline-block}
|
||||||
|
@keyframes pulse{0%{opacity:1}50%{opacity:.3}100%{opacity:1}}
|
||||||
|
.running-indicator.active{animation:pulse 1.2s ease-in-out infinite}
|
||||||
|
.cost-display-wrap{position:relative;background:var(--color-surface-offset);border:1px solid oklch(from var(--color-text) l c h/.06);border-radius:var(--radius-lg);padding:var(--space-8) var(--space-8) var(--space-6);text-align:center;min-height:160px;display:flex;flex-direction:column;align-items:center;justify-content:center}
|
||||||
|
.cost-display-label{font-size:var(--text-xs);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:.08em;font-weight:600;margin-bottom:var(--space-3)}
|
||||||
|
.cost-display-amount{font-family:var(--font-mono);font-size:clamp(2.5rem,6vw,4.5rem);font-weight:600;letter-spacing:-.03em;color:var(--color-text);line-height:1;transition:color .3s ease}
|
||||||
|
.cost-display-amount.running{color:var(--color-warning)}
|
||||||
|
.cost-display-currency{font-size:var(--text-xl);font-weight:400;color:var(--color-text-muted);vertical-align:super;margin-right:var(--space-2);font-family:var(--font-mono)}
|
||||||
|
.cost-display-elapsed{margin-top:var(--space-3);font-size:var(--text-xs);color:var(--color-text-faint);font-family:var(--font-mono)}
|
||||||
|
.info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:var(--space-3);margin-top:var(--space-6)}
|
||||||
|
.info-tile{background:var(--color-surface-2,var(--color-surface));border:1px solid oklch(from var(--color-text) l c h/.06);border-radius:var(--radius-md);padding:var(--space-4)}
|
||||||
|
.info-tile-label{font-size:var(--text-xs);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:.05em;font-weight:600;margin-bottom:var(--space-2)}
|
||||||
|
.info-tile-value{font-family:var(--font-mono);font-size:var(--text-sm);font-weight:600;color:var(--color-text)}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<div style="display:flex;align-items:center;gap:1rem">
|
<div class="logo">
|
||||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" aria-hidden="true"><circle cx="16" cy="16" r="14" stroke="currentColor" stroke-width="2"/><path d="M16 8v8l5 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle cx="16" cy="16" r="2" fill="currentColor"/></svg>
|
<svg class="logo-icon" width="32" height="32" viewBox="0 0 32 32" fill="none" aria-label="Meeting Cost Tracker">
|
||||||
|
<circle cx="16" cy="16" r="14" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<path d="M16 8v8l5 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<circle cx="16" cy="16" r="2" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
<div>
|
<div>
|
||||||
<div class="logo-text">Meeting Cost Tracker</div>
|
<div class="logo-text">Meeting Cost Tracker</div>
|
||||||
<div class="logo-sub">Live meeting cost overview</div>
|
<div class="logo-sub">Live meeting cost overview</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="theme-toggle" data-theme-toggle aria-label="Toggle theme">
|
<button class="theme-toggle" data-theme-toggle aria-label="Toggle theme">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/></svg>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
<main class="card">
|
||||||
<main class="card">
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-label" for="devCount">Developers present</label>
|
<label class="form-label" for="devCount">Developers present</label>
|
||||||
<input class="form-input" type="number" id="devCount" min="1" max="500" value="5" aria-label="Developers present">
|
<input class="form-input" type="number" id="devCount" min="1" max="500" value="5">
|
||||||
<div style="font-size:.85rem;color:var(--color-text-muted)">number of participants</div>
|
<div class="form-hint">Number of participants</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label class="form-label" for="avgSalary">Avg. monthly gross salary</label>
|
<label class="form-label" for="avgSalary">Avg. monthly gross salary</label>
|
||||||
<div style="position:relative">
|
<div class="input-prefix-wrap">
|
||||||
<span class="input-prefix">EUR</span>
|
<span class="input-prefix">EUR</span>
|
||||||
<input class="form-input" type="number" id="avgSalary" min="1000" max="50000" value="5000" style="padding-left:3.5rem" aria-label="Average monthly salary">
|
<input class="form-input" type="number" id="avgSalary" min="1000" max="50000" value="5000" style="padding-left:3.5rem">
|
||||||
</div>
|
</div>
|
||||||
<div style="font-size:.85rem;color:var(--color-text-muted)">brutto, avarage salary</div>
|
<div class="form-hint">Brutto, average salary</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn-start" id="mainBtn" onclick="handleBtn()">
|
<button class="btn-start" id="mainBtn" onclick="handleBtn()">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" id="btnIcon"><polygon points="5,3 19,12 5,21"/></svg>
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" id="btnIcon"><polygon points="5,3 19,12 5,21"/></svg>
|
||||||
@@ -71,100 +131,148 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
<div class="action-row">
|
||||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem">
|
<span class="status-badge" id="statusBadge">
|
||||||
<span class="status-badge" id="statusBadge"><span class="running-indicator" id="runIndicator"></span> Ready</span>
|
<span class="running-indicator" id="runIndicator"></span>
|
||||||
<button class="btn-reset" id="resetBtn" onclick="resetTimer()" style="display:none">Reset</button>
|
Ready
|
||||||
|
</span>
|
||||||
|
<button class="btn-reset" id="resetBtn" onclick="resetTimer()" style="display:none">
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cost-display-wrap">
|
<div class="cost-display-wrap">
|
||||||
<div style="font-size:.75rem;color:var(--color-text-muted);text-transform:uppercase;margin-bottom:.5rem">Total cost so far</div>
|
<div class="cost-display-label">Total cost so far</div>
|
||||||
<div class="cost-display-amount" id="costDisplay"><span style="font-family:var(--font-mono);font-size:1rem;margin-right:.4rem">EUR</span><span id="costValue">0.00</span></div>
|
<div class="cost-display-amount" id="costDisplay">
|
||||||
<div style="margin-top:.5rem;font-size:.85rem;color:var(--color-text-muted)" id="elapsedDisplay">Not started</div>
|
<span class="cost-display-currency">EUR</span><span id="costValue">0.00</span>
|
||||||
|
</div>
|
||||||
|
<div class="cost-display-elapsed" id="elapsedDisplay">Not started</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-grid" id="infoGrid" style="display:none">
|
<div class="info-grid" id="infoGrid" style="display:none">
|
||||||
<div class="info-tile"><div class="info-tile-label">Cost / second</div><div class="info-tile-value" id="tilePerSec">-</div></div>
|
<div class="info-tile">
|
||||||
<div class="info-tile"><div class="info-tile-label">Cost / minute</div><div class="info-tile-value" id="tilePerMin">-</div></div>
|
<div class="info-tile-label">Cost / second</div>
|
||||||
<div class="info-tile"><div class="info-tile-label">Cost / hour</div><div class="info-tile-value" id="tilePerHour">-</div></div>
|
<div class="info-tile-value" id="tilePerSec">-</div>
|
||||||
<div class="info-tile"><div class="info-tile-label">People × salary</div><div class="info-tile-value" id="tileTotal">-</div></div>
|
|
||||||
<div class="info-tile"><div class="info-tile-label">Annual salary (per person)</div><div class="info-tile-value" id="tileAnnual">-</div></div>
|
|
||||||
<div class="info-tile"><div class="info-tile-label">incl. employer contributions (×1.2)</div><div class="info-tile-value" id="tileEmployer">-</div></div>
|
|
||||||
<div class="info-tile"><div class="info-tile-label">Total annual cost</div><div class="info-tile-value" id="tileAnnualTotal">-</div></div>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
<div class="info-tile">
|
||||||
|
<div class="info-tile-label">Cost / minute</div>
|
||||||
<script>
|
<div class="info-tile-value" id="tilePerMin">-</div>
|
||||||
(function(){
|
</div>
|
||||||
var root=document.documentElement; var dark=false; var btn=document.querySelector('[data-theme-toggle]');
|
<div class="info-tile">
|
||||||
var sunSvg='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/></svg>';
|
<div class="info-tile-label">Cost / hour</div>
|
||||||
|
<div class="info-tile-value" id="tilePerHour">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-tile">
|
||||||
|
<div class="info-tile-label">People x salary</div>
|
||||||
|
<div class="info-tile-value" id="tileTotal">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-tile">
|
||||||
|
<div class="info-tile-label">Annual salary (per person)</div>
|
||||||
|
<div class="info-tile-value" id="tileAnnual">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-tile">
|
||||||
|
<div class="info-tile-label">incl. employer contributions (x1.2)</div>
|
||||||
|
<div class="info-tile-value" id="tileEmployer">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-tile">
|
||||||
|
<div class="info-tile-label">Total annual cost</div>
|
||||||
|
<div class="info-tile-value" id="tileAnnualTotal">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
var root=document.documentElement;
|
||||||
|
var dark=false;
|
||||||
|
var btn=document.querySelector('[data-theme-toggle]');
|
||||||
|
var sunSvg='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>';
|
||||||
var moonSvg='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
|
var moonSvg='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
|
||||||
if(btn){ root.setAttribute('data-theme', dark ? 'dark' : 'light'); btn.innerHTML = dark ? sunSvg : moonSvg; btn.addEventListener('click', function(){ dark=!dark; root.setAttribute('data-theme', dark ? 'dark' : 'light'); btn.innerHTML = dark ? sunSvg : moonSvg; }); }
|
if(btn){
|
||||||
})();
|
root.setAttribute('data-theme', dark ? 'dark' : 'light');
|
||||||
|
btn.innerHTML = dark ? sunSvg : moonSvg;
|
||||||
var timer=null,isRunning=false,startTime=null,pausedElapsed=0;
|
btn.addEventListener('click', function(){
|
||||||
// English formatting (GBP/GB style but currency EUR)
|
dark = !dark; root.setAttribute('data-theme', dark ? 'dark' : 'light'); btn.innerHTML = dark ? sunSvg : moonSvg;
|
||||||
function fmt(n){ return new Intl.NumberFormat('en-GB',{ style:'currency', currency:'EUR', minimumFractionDigits:2, maximumFractionDigits:2 }).format(n); }
|
});
|
||||||
function fmtPlain(n,d){ return n.toLocaleString('en-GB',{minimumFractionDigits:d,maximumFractionDigits:d}); }
|
}
|
||||||
function fmtElapsed(sec){ var h=Math.floor(sec/3600), m=Math.floor((sec%3600)/60), s=Math.floor(sec%60); if(h>0) return h + 'h ' + String(m).padStart(2,'0') + 'm ' + String(s).padStart(2,'0') + 's'; if(m>0) return m + 'm ' + String(s).padStart(2,'0') + 's'; return Math.floor(sec) + 's elapsed'; }
|
})();
|
||||||
|
var timer=null,isRunning=false,startTime=null,pausedElapsed=0;
|
||||||
function getParams(){
|
function fmt(n){return new Intl.NumberFormat('en-GB',{style:'currency',currency:'EUR',minimumFractionDigits:2,maximumFractionDigits:2}).format(n);}
|
||||||
|
function fmtPlain(n,d){return n.toLocaleString('en-GB',{minimumFractionDigits:d,maximumFractionDigits:d});}
|
||||||
|
function fmtElapsed(sec){
|
||||||
|
var h=Math.floor(sec/3600),m=Math.floor((sec%3600)/60),s=Math.floor(sec%60);
|
||||||
|
if(h>0)return h+'h '+String(m).padStart(2,'0')+'m '+String(s).padStart(2,'0')+'s';
|
||||||
|
if(m>0)return m+'m '+String(s).padStart(2,'0')+'s';
|
||||||
|
return Math.floor(sec)+'s elapsed';
|
||||||
|
}
|
||||||
|
function getParams(){
|
||||||
var devCount=Math.max(1,parseInt(document.getElementById('devCount').value)||1);
|
var devCount=Math.max(1,parseInt(document.getElementById('devCount').value)||1);
|
||||||
var avgSalary=Math.max(0,parseFloat(document.getElementById('avgSalary').value)||5000);
|
var avgSalary=Math.max(0,parseFloat(document.getElementById('avgSalary').value)||5000);
|
||||||
var annualSalary=avgSalary*12;
|
var annualSalary=avgSalary*12;
|
||||||
var annualWithEmployer=annualSalary*1.2;
|
var annualWithEmployer=annualSalary*1.2;
|
||||||
var totalCost=annualWithEmployer*devCount;
|
var totalCost=annualWithEmployer*devCount;
|
||||||
var costPerSecond=totalCost/220/8/60/60;
|
var costPerSecond=totalCost/220/8/60/60;
|
||||||
return { devCount:devCount, avgSalary:avgSalary, annualSalary:annualSalary, annualWithEmployer:annualWithEmployer, totalCost:totalCost, costPerSecond:costPerSecond };
|
return{devCount:devCount,avgSalary:avgSalary,annualSalary:annualSalary,annualWithEmployer:annualWithEmployer,totalCost:totalCost,costPerSecond:costPerSecond};
|
||||||
}
|
}
|
||||||
|
function tick(){
|
||||||
function tick(){
|
var elapsed=pausedElapsed+(Date.now()-startTime)/1000;
|
||||||
var elapsed = pausedElapsed + (Date.now()-startTime)/1000;
|
var p=getParams();
|
||||||
var p = getParams();
|
var spent=p.costPerSecond*elapsed;
|
||||||
var spent = p.costPerSecond * elapsed;
|
document.getElementById('costValue').textContent=fmtPlain(spent,2);
|
||||||
document.getElementById('costValue').textContent = fmtPlain(spent,2);
|
document.getElementById('elapsedDisplay').textContent=fmtElapsed(elapsed);
|
||||||
document.getElementById('elapsedDisplay').textContent = fmtElapsed(elapsed);
|
document.getElementById('tilePerSec').textContent='EUR '+p.costPerSecond.toLocaleString('en-GB',{minimumFractionDigits:4,maximumFractionDigits:4});
|
||||||
document.getElementById('tilePerSec').textContent = 'EUR ' + p.costPerSecond.toLocaleString('en-GB',{minimumFractionDigits:4,maximumFractionDigits:4});
|
document.getElementById('tilePerMin').textContent='EUR '+fmtPlain(p.costPerSecond*60,2);
|
||||||
document.getElementById('tilePerMin').textContent = 'EUR ' + fmtPlain(p.costPerSecond*60,2);
|
document.getElementById('tilePerHour').textContent='EUR '+fmtPlain(p.costPerSecond*3600,2);
|
||||||
document.getElementById('tilePerHour').textContent = 'EUR ' + fmtPlain(p.costPerSecond*3600,2);
|
document.getElementById('tileTotal').textContent=p.devCount+' x EUR '+fmtPlain(p.avgSalary,2);
|
||||||
document.getElementById('tileTotal').textContent = p.devCount + ' x EUR ' + fmtPlain(p.avgSalary,2);
|
var annual = p.annualSalary;
|
||||||
// annual / employer / total
|
var employer = p.annualWithEmployer;
|
||||||
document.getElementById('tileAnnual').textContent = fmtPlain(p.annualSalary,0) + ' €';
|
var annualTotal = p.totalCost;
|
||||||
document.getElementById('tileEmployer').textContent = fmtPlain(p.annualWithEmployer,0) + ' €';
|
document.getElementById('tileAnnual').textContent=fmtPlain(annual,0)+' EUR';
|
||||||
document.getElementById('tileAnnualTotal').textContent = fmtPlain(p.totalCost,0) + ' €';
|
document.getElementById('tileEmployer').textContent=fmtPlain(employer,0)+' EUR';
|
||||||
}
|
document.getElementById('tileAnnualTotal').textContent=fmtPlain(annualTotal,0)+' EUR';
|
||||||
|
}
|
||||||
function handleBtn(){ isRunning ? pauseTimer() : startTimer(); }
|
function handleBtn(){isRunning?pauseTimer():startTimer();}
|
||||||
|
function startTimer(){
|
||||||
function startTimer(){
|
isRunning=true;startTime=Date.now();
|
||||||
isRunning=true; startTime=Date.now(); timer=setInterval(tick,100);
|
timer=setInterval(tick,100);
|
||||||
document.getElementById('btnLabel').textContent='Pause';
|
document.getElementById('btnLabel').textContent='Pause';
|
||||||
document.getElementById('btnIcon').innerHTML='<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>';
|
document.getElementById('btnIcon').innerHTML='<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>';
|
||||||
document.getElementById('costDisplay').classList.add('running');
|
document.getElementById('costDisplay').classList.add('running');
|
||||||
document.getElementById('statusBadge').className='status-badge running';
|
document.getElementById('statusBadge').className='status-badge running';
|
||||||
document.getElementById('statusBadge').innerHTML='<span class="running-indicator active"></span> Running';
|
document.getElementById('statusBadge').innerHTML='<span class="running-indicator active"></span> Running';
|
||||||
document.getElementById('resetBtn').style.display='inline-flex';
|
document.getElementById('resetBtn').style.display='flex';
|
||||||
document.getElementById('infoGrid').style.display='grid';
|
document.getElementById('infoGrid').style.display='grid';
|
||||||
document.getElementById('devCount').disabled=true; document.getElementById('avgSalary').disabled=true;
|
document.getElementById('devCount').disabled=true;
|
||||||
|
document.getElementById('avgSalary').disabled=true;
|
||||||
tick();
|
tick();
|
||||||
}
|
}
|
||||||
|
function pauseTimer(){
|
||||||
function pauseTimer(){
|
isRunning=false;pausedElapsed+=(Date.now()-startTime)/1000;
|
||||||
isRunning=false; pausedElapsed += (Date.now()-startTime)/1000; clearInterval(timer); timer=null;
|
clearInterval(timer);timer=null;
|
||||||
document.getElementById('btnLabel').textContent='Resume';
|
document.getElementById('btnLabel').textContent='Resume';
|
||||||
document.getElementById('btnIcon').innerHTML='<polygon points="5,3 19,12 5,21"/>';
|
document.getElementById('btnIcon').innerHTML='<polygon points="5,3 19,12 5,21"/>';
|
||||||
document.getElementById('costDisplay').classList.remove('running');
|
document.getElementById('costDisplay').classList.remove('running');
|
||||||
document.getElementById('statusBadge').className='status-badge stopped';
|
document.getElementById('statusBadge').className='status-badge stopped';
|
||||||
document.getElementById('statusBadge').innerHTML='<span class="running-indicator"></span> Paused';
|
document.getElementById('statusBadge').innerHTML='<span class="running-indicator"></span> Paused';
|
||||||
}
|
}
|
||||||
|
function resetTimer(){
|
||||||
|
if(timer)clearInterval(timer);
|
||||||
|
timer=null;isRunning=false;pausedElapsed=0;startTime=null;
|
||||||
|
document.getElementById('costValue').textContent='0.00';
|
||||||
|
document.getElementById('elapsedDisplay').textContent='Not started';
|
||||||
|
document.getElementById('btnLabel').textContent='Start';
|
||||||
|
document.getElementById('btnIcon').innerHTML='<polygon points="5,3 19,12 5,21"/>';
|
||||||
|
document.getElementById('costDisplay').classList.remove('running');
|
||||||
|
document.getElementById('statusBadge').className='status-badge';
|
||||||
|
document.getElementById('statusBadge').innerHTML='<span class="running-indicator"></span> Ready';
|
||||||
|
document.getElementById('resetBtn').style.display='none';
|
||||||
|
document.getElementById('infoGrid').style.display='none';
|
||||||
|
document.getElementById('devCount').disabled=false;
|
||||||
|
document.getElementById('avgSalary').disabled=false;
|
||||||
|
}
|
||||||
|
|
||||||
function resetTimer(){ if(timer) clearInterval(timer); timer=null; isRunning=false; pausedElapsed=0; startTime=null; document.getElementById('costValue').textContent='0.00'; document.getElementById('elapsedDisplay').textContent='Not started'; document.getElementById('btnLabel').textContent='Start'; document.getElementById('btnIcon').innerHTML='<polygon points="5,3 19,12 5,21"/>'; document.getElementById('costDisplay').classList.remove('running'); document.getElementById('statusBadge').className='status-badge'; document.getElementById('statusBadge').innerHTML='<span class="running-indicator"></span> Ready'; document.getElementById('resetBtn').style.display='none'; document.getElementById('infoGrid').style.display='none'; document.getElementById('devCount').disabled=false; document.getElementById('avgSalary').disabled=false; }
|
// input listeners: update per-second display when editing
|
||||||
|
document.getElementById('devCount').addEventListener('input', function(){ if(!isRunning){ var p=getParams(); if(p.costPerSecond>0) document.getElementById('tilePerSec').textContent='EUR '+p.costPerSecond.toLocaleString('en-GB',{minimumFractionDigits:4,maximumFractionDigits:4}); }});
|
||||||
|
document.getElementById('avgSalary').addEventListener('input', function(){ if(!isRunning){ var p=getParams(); if(p.costPerSecond>0) document.getElementById('tilePerSec').textContent='EUR '+p.costPerSecond.toLocaleString('en-GB',{minimumFractionDigits:4,maximumFractionDigits:4}); }});
|
||||||
|
|
||||||
// input listeners: update per-second display when editing
|
// No auto-start: user must press Start.
|
||||||
document.getElementById('devCount').addEventListener('input', function(){ if(!isRunning){ var p=getParams(); if(p.costPerSecond>0) document.getElementById('tilePerSec').textContent='EUR '+p.costPerSecond.toLocaleString('en-GB',{minimumFractionDigits:4,maximumFractionDigits:4}); }});
|
</script>
|
||||||
document.getElementById('avgSalary').addEventListener('input', function(){ if(!isRunning){ var p=getParams(); if(p.costPerSecond>0) document.getElementById('tilePerSec').textContent='EUR '+p.costPerSecond.toLocaleString('en-GB',{minimumFractionDigits:4,maximumFractionDigits:4}); }});
|
|
||||||
|
|
||||||
// No auto-start: user must press Start.
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Generated
+201
@@ -0,0 +1,201 @@
|
|||||||
|
{
|
||||||
|
"name": "people-cost-counter",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "people-cost-counter",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"html-validate": "^11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@html-validate/stylish": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@html-validate/stylish/-/stylish-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-d1/lI3qIXhGVJF3+MmKEBn1dRX6U4Z+BDaOdDI3E6SXcO+OQ7hC/3mSo6BIjwQlcuV6NdDOKm6QCrzIQL1EezQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^22.16.0 || >= 24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@sidvind/better-ajv-errors": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@sidvind/better-ajv-errors/-/better-ajv-errors-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-imVsp5D3KxJ8+uUmu2dsLKnFg3qdX952v/L1gZoCa0NO2ivhQWHMpYM2KmpFrRXHWFjlqkNZ/935nxiTqXEi1A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": "^22.12 || >= 24.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"ajv": "^8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ajv": {
|
||||||
|
"version": "8.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||||||
|
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fast-uri": "^3.0.1",
|
||||||
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fast-deep-equal": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/fast-uri": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/html-validate": {
|
||||||
|
"version": "11.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-validate/-/html-validate-11.6.2.tgz",
|
||||||
|
"integrity": "sha512-dlAAEeWOOdv9ya/n3pdSFTzzzGlrw+ZI43J/39zBgCSqzSg24lyqrouuBP9MEkSFAtXVZm64F35mlL7m43jCNA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/html-validate"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@html-validate/stylish": "^6.0.0",
|
||||||
|
"@sidvind/better-ajv-errors": "7.0.0",
|
||||||
|
"ajv": "^8.0.0",
|
||||||
|
"kleur": "^4.1.0",
|
||||||
|
"prompts": "^2.0.0",
|
||||||
|
"semver": "^7.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"html-validate": "bin/html-validate.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^22.22.0 || >= 24.8.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@jest/globals": "^29.0.3 || ^30.0.0",
|
||||||
|
"@vitest/expect": "^3.2.0 || ^4.0.1",
|
||||||
|
"jest": "^29.0.3 || ^30.0.0",
|
||||||
|
"jest-snapshot": "^29.0.3 || ^30.0.0",
|
||||||
|
"vitest": "^3.2.0 || ^4.0.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@jest/globals": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@vitest/expect": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"jest-snapshot": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vitest": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/json-schema-traverse": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/kleur": {
|
||||||
|
"version": "4.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
||||||
|
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prompts": {
|
||||||
|
"version": "2.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||||
|
"integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"kleur": "^3.0.3",
|
||||||
|
"sisteransi": "^1.0.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prompts/node_modules/kleur": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/require-from-string": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "7.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||||
|
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/sisteransi": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"html-validate": "^8.8.0"
|
"html-validate": "^11.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint:html": "html-validate \"**/*.html\""
|
"lint:html": "html-validate \"**/*.html\""
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": ["config:recommended"],
|
||||||
|
"schedule": ["before 6am on Monday"],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchManagers": ["github-actions"],
|
||||||
|
"groupName": "GitHub Actions",
|
||||||
|
"automerge": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchManagers": ["dockerfile"],
|
||||||
|
"groupName": "Docker",
|
||||||
|
"automerge": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchManagers": ["npm"],
|
||||||
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
|
"groupName": "npm minor/patch",
|
||||||
|
"automerge": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
|
"automerge": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchUpdateTypes": ["major"],
|
||||||
|
"labels": ["major-update"],
|
||||||
|
"automerge": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de) -->
|
||||||
|
<!-- Licensed under the MIT License. See LICENSE file in project root for details. -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de" data-theme="dark">
|
<html lang="de" data-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de) -->
|
||||||
|
<!-- Licensed under the MIT License. See LICENSE file in project root for details. -->
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- Copyright (c) 2026 Stefan Koelle (https://stefankoelle.de) -->
|
||||||
|
<!-- Licensed under the MIT License. See LICENSE file in project root for details. -->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
|
|||||||
Reference in New Issue
Block a user