commit 2433b865da8394eb4b288d8cec68cde115a02243 Author: luxick Date: Wed Oct 15 10:01:47 2025 +0200 Init diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..fa755ff --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1 @@ +- Do not end with a summary or explanation \ No newline at end of file diff --git a/.github/go.instructions.md b/.github/go.instructions.md new file mode 100644 index 0000000..a956d62 --- /dev/null +++ b/.github/go.instructions.md @@ -0,0 +1,373 @@ +--- +description: 'Instructions for writing Go code following idiomatic Go practices and community standards' +applyTo: '**/*.go,**/go.mod,**/go.sum' +--- + +# Go Development Instructions + +Follow idiomatic Go practices and community standards when writing Go code. These instructions are based on [Effective Go](https://go.dev/doc/effective_go), [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments), and [Google's Go Style Guide](https://google.github.io/styleguide/go/). + +## General Instructions + +- Write simple, clear, and idiomatic Go code +- Favor clarity and simplicity over cleverness +- Follow the principle of least surprise +- Keep the happy path left-aligned (minimize indentation) +- Return early to reduce nesting +- Prefer early return over if-else chains; use `if condition { return }` pattern to avoid else blocks +- Make the zero value useful +- Write self-documenting code with clear, descriptive names +- Document exported types, functions, methods, and packages +- Use Go modules for dependency management +- Leverage the Go standard library instead of reinventing the wheel (e.g., use `strings.Builder` for string concatenation, `filepath.Join` for path construction) +- Prefer standard library solutions over custom implementations when functionality exists +- Write comments in English by default; translate only upon user request +- Avoid using emoji in code and comments + +## Naming Conventions + +### Packages + +- Use lowercase, single-word package names +- Avoid underscores, hyphens, or mixedCaps +- Choose names that describe what the package provides, not what it contains +- Avoid generic names like `util`, `common`, or `base` +- Package names should be singular, not plural + +#### Package Declaration Rules (CRITICAL): +- **NEVER duplicate `package` declarations** - each Go file must have exactly ONE `package` line +- When editing an existing `.go` file: + - **PRESERVE** the existing `package` declaration - do not add another one + - If you need to replace the entire file content, start with the existing package name +- When creating a new `.go` file: + - **BEFORE writing any code**, check what package name other `.go` files in the same directory use + - Use the SAME package name as existing files in that directory + - If it's a new directory, use the directory name as the package name + - Write **exactly one** `package ` line at the very top of the file +- When using file creation or replacement tools: + - **ALWAYS verify** the target file doesn't already have a `package` declaration before adding one + - If replacing file content, include only ONE `package` declaration in the new content + - **NEVER** create files with multiple `package` lines or duplicate declarations + +### Variables and Functions + +- Use mixedCaps or MixedCaps (camelCase) rather than underscores +- Keep names short but descriptive +- Use single-letter variables only for very short scopes (like loop indices) +- Exported names start with a capital letter +- Unexported names start with a lowercase letter +- Avoid stuttering (e.g., avoid `http.HTTPServer`, prefer `http.Server`) + +### Interfaces + +- Name interfaces with -er suffix when possible (e.g., `Reader`, `Writer`, `Formatter`) +- Single-method interfaces should be named after the method (e.g., `Read` → `Reader`) +- Keep interfaces small and focused + +### Constants + +- Use MixedCaps for exported constants +- Use mixedCaps for unexported constants +- Group related constants using `const` blocks +- Consider using typed constants for better type safety + +## Code Style and Formatting + +### Formatting + +- Always use `gofmt` to format code +- Use `goimports` to manage imports automatically +- Keep line length reasonable (no hard limit, but consider readability) +- Add blank lines to separate logical groups of code + +### Comments + +- Strive for self-documenting code; prefer clear variable names, function names, and code structure over comments +- Write comments only when necessary to explain complex logic, business rules, or non-obvious behavior +- Write comments in complete sentences in English by default +- Translate comments to other languages only upon specific user request +- Start sentences with the name of the thing being described +- Package comments should start with "Package [name]" +- Use line comments (`//`) for most comments +- Use block comments (`/* */`) sparingly, mainly for package documentation +- Document why, not what, unless the what is complex +- Avoid emoji in comments and code + +### Error Handling + +- Check errors immediately after the function call +- Don't ignore errors using `_` unless you have a good reason (document why) +- Wrap errors with context using `fmt.Errorf` with `%w` verb +- Create custom error types when you need to check for specific errors +- Place error returns as the last return value +- Name error variables `err` +- Keep error messages lowercase and don't end with punctuation + +## Architecture and Project Structure + +### Package Organization + +- Follow standard Go project layout conventions +- Keep `main` packages in `cmd/` directory +- Put reusable packages in `pkg/` or `internal/` +- Use `internal/` for packages that shouldn't be imported by external projects +- Group related functionality into packages +- Avoid circular dependencies + +### Dependency Management + +- Use Go modules (`go.mod` and `go.sum`) +- Keep dependencies minimal +- Regularly update dependencies for security patches +- Use `go mod tidy` to clean up unused dependencies +- Vendor dependencies only when necessary + +## Type Safety and Language Features + +### Type Definitions + +- Define types to add meaning and type safety +- Use struct tags for JSON, XML, database mappings +- Prefer explicit type conversions +- Use type assertions carefully and check the second return value +- Prefer generics over unconstrained types; when an unconstrained type is truly needed, use the predeclared alias `any` instead of `interface{}` (Go 1.18+) + +### Pointers vs Values + +- Use pointer receivers for large structs or when you need to modify the receiver +- Use value receivers for small structs and when immutability is desired +- Use pointer parameters when you need to modify the argument or for large structs +- Use value parameters for small structs and when you want to prevent modification +- Be consistent within a type's method set +- Consider the zero value when choosing pointer vs value receivers + +### Interfaces and Composition + +- Accept interfaces, return concrete types +- Keep interfaces small (1-3 methods is ideal) +- Use embedding for composition +- Define interfaces close to where they're used, not where they're implemented +- Don't export interfaces unless necessary + +## Concurrency + +### Goroutines + +- Be cautious about creating goroutines in libraries; prefer letting the caller control concurrency +- If you must create goroutines in libraries, provide clear documentation and cleanup mechanisms +- Always know how a goroutine will exit +- Use `sync.WaitGroup` or channels to wait for goroutines +- Avoid goroutine leaks by ensuring cleanup + +### Channels + +- Use channels to communicate between goroutines +- Don't communicate by sharing memory; share memory by communicating +- Close channels from the sender side, not the receiver +- Use buffered channels when you know the capacity +- Use `select` for non-blocking operations + +### Synchronization + +- Use `sync.Mutex` for protecting shared state +- Keep critical sections small +- Use `sync.RWMutex` when you have many readers +- Choose between channels and mutexes based on the use case: use channels for communication, mutexes for protecting state +- Use `sync.Once` for one-time initialization +- WaitGroup usage by Go version: + - If `go >= 1.25` in `go.mod`, use the new `WaitGroup.Go` method ([documentation](https://pkg.go.dev/sync#WaitGroup)): + ```go + var wg sync.WaitGroup + wg.Go(task1) + wg.Go(task2) + wg.Wait() + ``` + - If `go < 1.25`, use the classic `Add`/`Done` pattern + +## Error Handling Patterns + +### Creating Errors + +- Use `errors.New` for simple static errors +- Use `fmt.Errorf` for dynamic errors +- Create custom error types for domain-specific errors +- Export error variables for sentinel errors +- Use `errors.Is` and `errors.As` for error checking + +### Error Propagation + +- Add context when propagating errors up the stack +- Don't log and return errors (choose one) +- Handle errors at the appropriate level +- Consider using structured errors for better debugging + +## API Design + +### HTTP Handlers + +- Use `http.HandlerFunc` for simple handlers +- Implement `http.Handler` for handlers that need state +- Use middleware for cross-cutting concerns +- Set appropriate status codes and headers +- Handle errors gracefully and return appropriate error responses +- Router usage by Go version: + - If `go >= 1.22`, prefer the enhanced `net/http` `ServeMux` with pattern-based routing and method matching + - If `go < 1.22`, use the classic `ServeMux` and handle methods/paths manually (or use a third-party router when justified) + +### JSON APIs + +- Use struct tags to control JSON marshaling +- Validate input data +- Use pointers for optional fields +- Consider using `json.RawMessage` for delayed parsing +- Handle JSON errors appropriately + +### HTTP Clients + +- Keep the client struct focused on configuration and dependencies only (e.g., base URL, `*http.Client`, auth, default headers). It must not store per-request state +- Do not store or cache `*http.Request` inside the client struct, and do not persist request-specific state across calls; instead, construct a fresh request per method invocation +- Methods should accept `context.Context` and input parameters, assemble the `*http.Request` locally (or via a short-lived builder/helper created per call), then call `c.httpClient.Do(req)` +- If request-building logic is reused, factor it into unexported helper functions or a per-call builder type; never keep `http.Request` (URL params, body, headers) as fields on the long-lived client +- Ensure the underlying `*http.Client` is configured (timeouts, transport) and is safe for concurrent use; avoid mutating `Transport` after first use +- Always set headers on the request instance you’re sending, and close response bodies (`defer resp.Body.Close()`), handling errors appropriately + +## Performance Optimization + +### Memory Management + +- Minimize allocations in hot paths +- Reuse objects when possible (consider `sync.Pool`) +- Use value receivers for small structs +- Preallocate slices when size is known +- Avoid unnecessary string conversions + +### I/O: Readers and Buffers + +- Most `io.Reader` streams are consumable once; reading advances state. Do not assume a reader can be re-read without special handling +- If you must read data multiple times, buffer it once and recreate readers on demand: + - Use `io.ReadAll` (or a limited read) to obtain `[]byte`, then create fresh readers via `bytes.NewReader(buf)` or `bytes.NewBuffer(buf)` for each reuse + - For strings, use `strings.NewReader(s)`; you can `Seek(0, io.SeekStart)` on `*bytes.Reader` to rewind +- For HTTP requests, do not reuse a consumed `req.Body`. Instead: + - Keep the original payload as `[]byte` and set `req.Body = io.NopCloser(bytes.NewReader(buf))` before each send + - Prefer configuring `req.GetBody` so the transport can recreate the body for redirects/retries: `req.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(bytes.NewReader(buf)), nil }` +- To duplicate a stream while reading, use `io.TeeReader` (copy to a buffer while passing through) or write to multiple sinks with `io.MultiWriter` +- Reusing buffered readers: call `(*bufio.Reader).Reset(r)` to attach to a new underlying reader; do not expect it to “rewind” unless the source supports seeking +- For large payloads, avoid unbounded buffering; consider streaming, `io.LimitReader`, or on-disk temporary storage to control memory + +- Use `io.Pipe` to stream without buffering the whole payload: + - Write to `*io.PipeWriter` in a separate goroutine while the reader consumes + - Always close the writer; use `CloseWithError(err)` on failures + - `io.Pipe` is for streaming, not rewinding or making readers reusable + +- **Warning:** When using `io.Pipe` (especially with multipart writers), all writes must be performed in strict, sequential order. Do not write concurrently or out of order—multipart boundaries and chunk order must be preserved. Out-of-order or parallel writes can corrupt the stream and result in errors. + +- Streaming multipart/form-data with `io.Pipe`: + - `pr, pw := io.Pipe()`; `mw := multipart.NewWriter(pw)`; use `pr` as the HTTP request body + - Set `Content-Type` to `mw.FormDataContentType()` + - In a goroutine: write all parts to `mw` in the correct order; on error `pw.CloseWithError(err)`; on success `mw.Close()` then `pw.Close()` + - Do not store request/in-flight form state on a long-lived client; build per call + - Streamed bodies are not rewindable; for retries/redirects, buffer small payloads or provide `GetBody` + +### Profiling + +- Use built-in profiling tools (`pprof`) +- Benchmark critical code paths +- Profile before optimizing +- Focus on algorithmic improvements first +- Consider using `testing.B` for benchmarks + +## Testing + +### Test Organization + +- Keep tests in the same package (white-box testing) +- Use `_test` package suffix for black-box testing +- Name test files with `_test.go` suffix +- Place test files next to the code they test + +### Writing Tests + +- Use table-driven tests for multiple test cases +- Name tests descriptively using `Test_functionName_scenario` +- Use subtests with `t.Run` for better organization +- Test both success and error cases +- Consider using `testify` or similar libraries when they add value, but don't over-complicate simple tests + +### Test Helpers + +- Mark helper functions with `t.Helper()` +- Create test fixtures for complex setup +- Use `testing.TB` interface for functions used in tests and benchmarks +- Clean up resources using `t.Cleanup()` + +## Security Best Practices + +### Input Validation + +- Validate all external input +- Use strong typing to prevent invalid states +- Sanitize data before using in SQL queries +- Be careful with file paths from user input +- Validate and escape data for different contexts (HTML, SQL, shell) + +### Cryptography + +- Use standard library crypto packages +- Don't implement your own cryptography +- Use crypto/rand for random number generation +- Store passwords using bcrypt, scrypt, or argon2 (consider golang.org/x/crypto for additional options) +- Use TLS for network communication + +## Documentation + +### Code Documentation + +- Prioritize self-documenting code through clear naming and structure +- Document all exported symbols with clear, concise explanations +- Start documentation with the symbol name +- Write documentation in English by default +- Use examples in documentation when helpful +- Keep documentation close to code +- Update documentation when code changes +- Avoid emoji in documentation and comments + +### README and Documentation Files + +- Include clear setup instructions +- Document dependencies and requirements +- Provide usage examples +- Document configuration options +- Include troubleshooting section + +## Tools and Development Workflow + +### Essential Tools + +- `go fmt`: Format code +- `go vet`: Find suspicious constructs +- `golangci-lint`: Additional linting (golint is deprecated) +- `go test`: Run tests +- `go mod`: Manage dependencies +- `go generate`: Code generation + +### Development Practices + +- Run tests before committing +- Use pre-commit hooks for formatting and linting +- Keep commits focused and atomic +- Write meaningful commit messages +- Review diffs before committing + +## Common Pitfalls to Avoid + +- Not checking errors +- Ignoring race conditions +- Creating goroutine leaks +- Not using defer for cleanup +- Modifying maps concurrently +- Not understanding nil interfaces vs nil pointers +- Forgetting to close resources (files, connections) +- Using global variables unnecessarily +- Over-using unconstrained types (e.g., `any`); prefer specific types or generic type parameters with constraints. If an unconstrained type is required, use `any` rather than `interface{}` +- Not considering the zero value of types +- **Creating duplicate `package` declarations** - this is a compile error; always check existing files before adding package declarations diff --git a/.github/used-prompts/deploying.md b/.github/used-prompts/deploying.md new file mode 100644 index 0000000..aabe746 --- /dev/null +++ b/.github/used-prompts/deploying.md @@ -0,0 +1,107 @@ +## Overview + +Deploy `estus-shots` to the personal Fedora server manually—no CI/CD required. The application runs behind Nginx via a reverse proxy and is supervised by `systemd`. + +## Prerequisites + +- Fedora (server) with SSH access for a user that can `sudo` without a passwordless setup. +- Nim toolchain installed locally (for `nim`/`nimble`). +- Server already provisioned with Nim runtime dependencies. +- Existing Nginx site definition that proxies traffic to the `estus-shots` service port (defaults to `127.0.0.1:9000`). + +## One-time server setup + +1. Create deployment directories on the server: + ```fish + ssh user@server 'sudo mkdir -p /opt/estus-shots/bin /opt/estus-shots/config && sudo chown ${USER}:${USER} /opt/estus-shots -R' + ``` +2. Copy the `systemd` unit and enable it: + ```fish + printf '%s\n' "[Unit]" \ + "Description=estus-shots web service" \ + "After=network.target" \ + "" \ + "[Service]" \ + "Type=simple" \ + "ExecStart=/opt/estus-shots/bin/estus-shots" \ + "Restart=on-failure" \ + "RestartSec=5" \ + "User=www-data" \ + "WorkingDirectory=/opt/estus-shots" \ + "Environment=LOG_LEVEL=info" \ + "" \ + "[Install]" \ + "WantedBy=multi-user.target" \ + | ssh user@server 'sudo tee /etc/systemd/system/estus-shots.service' + + ssh user@server 'sudo systemctl daemon-reload && sudo systemctl enable estus-shots.service' + ``` +3. Ensure Nginx reverse proxy points to `127.0.0.1:9000` and reload it once configured: + ```fish + ssh user@server 'sudo systemctl restart nginx' + ``` + +## Deployment script + +Save the following as `scripts/deploy.fish` (or another preferred location) and make it executable with `chmod +x scripts/deploy.fish`. + +```fish +#!/usr/bin/env fish + +set -e + +set SERVER user@server +set TARGET_DIR /opt/estus-shots +set BIN_NAME estus-shots + +echo 'Building project locally (release)...' +nimble build -y + +echo 'Uploading binary...' +scp bin/$BIN_NAME $SERVER:$TARGET_DIR/bin/ + +echo 'Uploading static assets...' +scp -r src/static $SERVER:$TARGET_DIR/ + +echo 'Restarting services...' +ssh $SERVER 'sudo systemctl restart estus-shots.service' +ssh $SERVER 'sudo systemctl restart nginx' + +echo 'Deployment finished.' +``` + +> The script prompts for the SSH password and any required `sudo` password on the server. + +## Manual deployment steps + +1. Build the project locally: + ```fish + nimble build -y + ``` +2. Copy the new build output and static assets: + ```fish + scp bin/estus-shots user@server:/opt/estus-shots/bin/ + scp -r src/static user@server:/opt/estus-shots/ + ``` +3. Restart the service and, if needed, Nginx: + ```fish + ssh user@server 'sudo systemctl restart estus-shots.service' + ssh user@server 'sudo systemctl restart nginx' + ``` + +## Verification + +- Check service status: + ```fish + ssh user@server 'systemctl status estus-shots.service --no-pager' + ``` +- Review recent logs: + ```fish + ssh user@server 'journalctl -u estus-shots.service -n 50 --no-pager' + ``` + +## Troubleshooting tips + +- If the service fails, run it manually on the server to capture stdout/stderr: `bin/estus-shots`. +- Ensure SELinux contexts allow Nginx to proxy (`setsebool -P httpd_can_network_connect 1`). +- Validate reverse proxy config: `sudo nginx -t`. \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5033616 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Build outputs +bin/ +build/ + +# Go tool artifacts +*.test +coverage.out + +# Compiled binaries and objects +*.exe +*.dll +*.lib +*.obj +*.o +*.so +*.dylib +*.a +*.pdb +*.ilk +*.map + +# Logs and temporary files +*.log +*.out +*.err +*.tmp +*.temp + +# Editor and tool artifacts +*.swp +*~ +.DS_Store +Thumbs.db +.idea/ +.vscode/ + diff --git a/cmd/estus-shots/main.go b/cmd/estus-shots/main.go new file mode 100644 index 0000000..8859dce --- /dev/null +++ b/cmd/estus-shots/main.go @@ -0,0 +1,119 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "flag" + "log" + "net/http" + "os" + "os/signal" + "path/filepath" + "syscall" + "time" + + "estus-shots/internal/server" +) + +type appConfig struct { + MediaDir string `json:"media_dir"` +} + +func main() { + cfg := resolveConfig() + + srv, err := server.New(server.Config{MediaDir: cfg.MediaDir}) + if err != nil { + log.Fatalf("failed to configure server: %v", err) + } + + httpServer := &http.Server{ + Addr: srv.Address(), + Handler: srv.Router(), + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + IdleTimeout: 60 * time.Second, + } + + log.Printf( + "Estus Shots web server running on http://127.0.0.1%s (media: %s)", + srv.Address(), + cfg.MediaDir, + ) + + go func() { + if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("server error: %v", err) + } + }() + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + <-ctx.Done() + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := httpServer.Shutdown(shutdownCtx); err != nil { + log.Printf("graceful shutdown failed: %v", err) + } else { + log.Println("server stopped") + } +} + +func resolveConfig() appConfig { + mediaDirFlag := flag.String("media-dir", "", "path to the media directory served at /media") + configPath := flag.String("config", "", "optional path to a JSON config file") + flag.Parse() + + cfg := appConfig{} + if *configPath != "" { + fileCfg, err := loadConfig(*configPath) + if err != nil { + log.Fatalf("failed to read config file: %v", err) + } + cfg = fileCfg + } + + if *mediaDirFlag != "" { + cfg.MediaDir = *mediaDirFlag + } + + if cfg.MediaDir == "" { + cfg.MediaDir = defaultMediaDir() + } + + return cfg +} + +func loadConfig(path string) (appConfig, error) { + data, err := os.ReadFile(path) + if err != nil { + return appConfig{}, err + } + + cfg := appConfig{} + if err := json.Unmarshal(data, &cfg); err != nil { + return appConfig{}, err + } + return cfg, nil +} + +func defaultMediaDir() string { + if cwd, err := os.Getwd(); err == nil { + candidate := filepath.Join(cwd, "media") + if info, err := os.Stat(candidate); err == nil && info.IsDir() { + return candidate + } + } + + if exe, err := os.Executable(); err == nil { + candidate := filepath.Join(filepath.Dir(exe), "media") + if info, err := os.Stat(candidate); err == nil && info.IsDir() { + return candidate + } + } + + return filepath.Join(".", "media") +} diff --git a/cmd/luxtools/main.go b/cmd/luxtools/main.go new file mode 100644 index 0000000..8859dce --- /dev/null +++ b/cmd/luxtools/main.go @@ -0,0 +1,119 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "flag" + "log" + "net/http" + "os" + "os/signal" + "path/filepath" + "syscall" + "time" + + "estus-shots/internal/server" +) + +type appConfig struct { + MediaDir string `json:"media_dir"` +} + +func main() { + cfg := resolveConfig() + + srv, err := server.New(server.Config{MediaDir: cfg.MediaDir}) + if err != nil { + log.Fatalf("failed to configure server: %v", err) + } + + httpServer := &http.Server{ + Addr: srv.Address(), + Handler: srv.Router(), + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + IdleTimeout: 60 * time.Second, + } + + log.Printf( + "Estus Shots web server running on http://127.0.0.1%s (media: %s)", + srv.Address(), + cfg.MediaDir, + ) + + go func() { + if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatalf("server error: %v", err) + } + }() + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + <-ctx.Done() + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := httpServer.Shutdown(shutdownCtx); err != nil { + log.Printf("graceful shutdown failed: %v", err) + } else { + log.Println("server stopped") + } +} + +func resolveConfig() appConfig { + mediaDirFlag := flag.String("media-dir", "", "path to the media directory served at /media") + configPath := flag.String("config", "", "optional path to a JSON config file") + flag.Parse() + + cfg := appConfig{} + if *configPath != "" { + fileCfg, err := loadConfig(*configPath) + if err != nil { + log.Fatalf("failed to read config file: %v", err) + } + cfg = fileCfg + } + + if *mediaDirFlag != "" { + cfg.MediaDir = *mediaDirFlag + } + + if cfg.MediaDir == "" { + cfg.MediaDir = defaultMediaDir() + } + + return cfg +} + +func loadConfig(path string) (appConfig, error) { + data, err := os.ReadFile(path) + if err != nil { + return appConfig{}, err + } + + cfg := appConfig{} + if err := json.Unmarshal(data, &cfg); err != nil { + return appConfig{}, err + } + return cfg, nil +} + +func defaultMediaDir() string { + if cwd, err := os.Getwd(); err == nil { + candidate := filepath.Join(cwd, "media") + if info, err := os.Stat(candidate); err == nil && info.IsDir() { + return candidate + } + } + + if exe, err := os.Executable(); err == nil { + candidate := filepath.Join(filepath.Dir(exe), "media") + if info, err := os.Stat(candidate); err == nil && info.IsDir() { + return candidate + } + } + + return filepath.Join(".", "media") +} diff --git a/deploy/estus-shots.service b/deploy/estus-shots.service new file mode 100644 index 0000000..72a2244 --- /dev/null +++ b/deploy/estus-shots.service @@ -0,0 +1,19 @@ +[Unit] +Description=estus-shots web service +After=network.target + +[Service] +Type=simple +ExecStart=/opt/estus-shots/bin/estus-shots +WorkingDirectory=/opt/estus-shots +Restart=on-failure +RestartSec=5 +User=estus-shots +Group=estus-shots +Environment=LOG_LEVEL=info +Environment=PORT=9000 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/deploy/luxtools.service b/deploy/luxtools.service new file mode 100644 index 0000000..72a2244 --- /dev/null +++ b/deploy/luxtools.service @@ -0,0 +1,19 @@ +[Unit] +Description=estus-shots web service +After=network.target + +[Service] +Type=simple +ExecStart=/opt/estus-shots/bin/estus-shots +WorkingDirectory=/opt/estus-shots +Restart=on-failure +RestartSec=5 +User=estus-shots +Group=estus-shots +Environment=LOG_LEVEL=info +Environment=PORT=9000 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ea544b7 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module estus-shots + +go 1.22.0 diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..6d0d802 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,189 @@ +package server + +import ( + "fmt" + "html" + "html/template" + "io/fs" + "log" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" + + webbundle "estus-shots/web" +) + +const ( + address = ":5000" + htmlContentType = "text/html; charset=utf-8" + notFoundTemplate = ` +
+
+ Error +

Route %s not found.

+
+
+ ` +) + +// Config contains the runtime options for the HTTP server. +type Config struct { + MediaDir string +} + +// Server hosts the Estus Shots HTTP handlers. +type Server struct { + mux *http.ServeMux + templates *template.Template + staticFS fs.FS + mediaFS http.FileSystem + + counterMu sync.Mutex + clickCount int +} + +// New constructs a configured Server ready to serve requests. +func New(cfg Config) (*Server, error) { + if strings.TrimSpace(cfg.MediaDir) == "" { + return nil, fmt.Errorf("media directory is required") + } + + mediaDir := filepath.Clean(cfg.MediaDir) + + info, err := os.Stat(mediaDir) + if err != nil { + return nil, fmt.Errorf("media directory check failed: %w", err) + } + if !info.IsDir() { + return nil, fmt.Errorf("media directory must be a directory: %s", mediaDir) + } + + // Parse all templates + tmpl, err := template.ParseFS(webbundle.Content, "templates/*.html") + if err != nil { + return nil, fmt.Errorf("failed to parse templates: %w", err) + } + + staticFS, err := fs.Sub(webbundle.Content, "static") + if err != nil { + return nil, fmt.Errorf("failed to prepare static assets: %w", err) + } + + s := &Server{ + mux: http.NewServeMux(), + templates: tmpl, + staticFS: staticFS, + mediaFS: http.Dir(mediaDir), + } + + s.registerRoutes() + return s, nil +} + +// Router exposes the configured http.Handler for the server. +func (s *Server) Router() http.Handler { + return s.mux +} + +// Address returns the default listen address for the server. +func (s *Server) Address() string { + return address +} + +func (s *Server) registerRoutes() { + staticHandler := http.FileServer(http.FS(s.staticFS)) + mediaHandler := http.FileServer(s.mediaFS) + + s.mux.Handle("/static/", http.StripPrefix("/static/", allowGetHead(staticHandler))) + s.mux.Handle("/media/", http.StripPrefix("/media/", allowGetHead(mediaHandler))) + s.mux.HandleFunc("/", s.handleRequest) +} + +func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/": + data := DefaultMenuBar() + s.renderTemplate(w, http.StatusOK, "index.html", data) + case "/admin": + data := AdminMenuBar() + data.Title = "Admin Panel" + s.renderTemplate(w, http.StatusOK, "admin.html", data) + case "/counter": + s.handleCounter(w, r) + case "/time": + s.sendHTMLString(w, http.StatusOK, renderTime(time.Now())) + default: + s.sendHTMLString(w, http.StatusNotFound, fmt.Sprintf(notFoundTemplate, html.EscapeString(r.URL.Path))) + } +} + +func (s *Server) handleCounter(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost || r.Method == http.MethodPut { + s.counterMu.Lock() + s.clickCount++ + s.counterMu.Unlock() + } + + s.counterMu.Lock() + count := s.clickCount + s.counterMu.Unlock() + + s.sendHTMLString(w, http.StatusOK, renderCounter(count)) +} + +func (s *Server) renderTemplate(w http.ResponseWriter, status int, name string, data interface{}) { + w.Header().Set("Content-Type", htmlContentType) + w.WriteHeader(status) + + if err := s.templates.ExecuteTemplate(w, name, data); err != nil { + log.Printf("template execution failed: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } +} + +func (s *Server) sendHTML(w http.ResponseWriter, status int, body []byte) { + w.Header().Set("Content-Type", htmlContentType) + w.WriteHeader(status) + if len(body) > 0 { + if _, err := w.Write(body); err != nil { + log.Printf("write response failed: %v", err) + } + } +} + +func (s *Server) sendHTMLString(w http.ResponseWriter, status int, body string) { + s.sendHTML(w, status, []byte(body)) +} + +func renderCounter(count int) string { + return fmt.Sprintf(` +
+

Clicks: %d

+ +
+ `, count) +} + +func renderTime(now time.Time) string { + return fmt.Sprintf(` +
+

Server time: %s

+
+ `, now.Format("2006-01-02 15:04:05")) +} + +func allowGetHead(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet, http.MethodHead: + next.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} diff --git a/internal/server/server_test.go b/internal/server/server_test.go new file mode 100644 index 0000000..a09e614 --- /dev/null +++ b/internal/server/server_test.go @@ -0,0 +1,129 @@ +package server + +import ( + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestRenderCounter(t *testing.T) { + html := renderCounter(7) + if !strings.Contains(html, "Clicks:") { + t.Fatalf("expected counter markup to include label") + } + if !strings.Contains(html, "> 7<") { + t.Fatalf("expected counter value in markup, got %q", html) + } +} + +func TestRenderTime(t *testing.T) { + now := time.Date(2024, time.August, 1, 12, 34, 56, 0, time.UTC) + html := renderTime(now) + expected := "2024-08-01 12:34:56" + if !strings.Contains(html, expected) { + t.Fatalf("expected formatted timestamp %s in markup", expected) + } +} + +func TestCounterEndpoint(t *testing.T) { + srv := mustNewServer(t, nil) + + req := httptest.NewRequest(http.MethodPost, "/counter", nil) + resp := httptest.NewRecorder() + + srv.Router().ServeHTTP(resp, req) + + if resp.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", resp.Code) + } + + body := resp.Body.String() + if !strings.Contains(body, "> 1<") { + t.Fatalf("expected counter to increment to 1, body: %q", body) + } +} + +func TestNotFound(t *testing.T) { + srv := mustNewServer(t, nil) + + req := httptest.NewRequest(http.MethodGet, "/unknown", nil) + resp := httptest.NewRecorder() + + srv.Router().ServeHTTP(resp, req) + + if resp.Code != http.StatusNotFound { + t.Fatalf("expected 404, got %d", resp.Code) + } + if !strings.Contains(resp.Body.String(), "Route /unknown not found.") { + t.Fatalf("expected not found message, got %q", resp.Body.String()) + } +} + +func TestStaticFileServing(t *testing.T) { + srv := mustNewServer(t, nil) + + req := httptest.NewRequest(http.MethodGet, "/static/lib/htmx.2.0.7.min.js", nil) + resp := httptest.NewRecorder() + + srv.Router().ServeHTTP(resp, req) + + if resp.Code != http.StatusOK { + t.Fatalf("expected static asset to load, got status %d", resp.Code) + } + if resp.Body.Len() == 0 { + t.Fatalf("expected static asset response body") + } +} + +func TestMediaServing(t *testing.T) { + var demoName string + srv := mustNewServer(t, func(dir string) { + demoName = filepath.Join(dir, "demo.txt") + if err := os.WriteFile(demoName, []byte("demo media file"), 0o644); err != nil { + t.Fatalf("failed to seed media file: %v", err) + } + }) + + req := httptest.NewRequest(http.MethodGet, "/media/demo.txt", nil) + resp := httptest.NewRecorder() + + srv.Router().ServeHTTP(resp, req) + + if resp.Code != http.StatusOK { + t.Fatalf("expected public asset to load, got status %d", resp.Code) + } + body := resp.Body.String() + if !strings.Contains(body, "demo media file") { + t.Fatalf("unexpected asset body: %q", body) + } +} + +func TestNewRequiresMediaDir(t *testing.T) { + if _, err := New(Config{}); err == nil { + t.Fatalf("expected error when media dir is empty") + } + + missing := filepath.Join(t.TempDir(), "missing") + if _, err := New(Config{MediaDir: missing}); err == nil { + t.Fatalf("expected error when media dir does not exist") + } +} + +func mustNewServer(t *testing.T, setup func(string)) *Server { + t.Helper() + + mediaDir := t.TempDir() + if setup != nil { + setup(mediaDir) + } + + srv, err := New(Config{MediaDir: mediaDir}) + if err != nil { + t.Fatalf("failed to create server: %v", err) + } + return srv +} diff --git a/internal/server/template_data.go b/internal/server/template_data.go new file mode 100644 index 0000000..64d4bb5 --- /dev/null +++ b/internal/server/template_data.go @@ -0,0 +1,84 @@ +package server + +// This file defines the data structures and helper functions for the template system. +// +// The template system uses Go's html/template package to provide: +// - Dynamic menu bars that can be customized per page +// - Template inheritance via layout.html +// - Type-safe data passing via structs +// +// To create a new page with a custom menu: +// 1. Create a PageData struct with your menu configuration +// 2. Call s.renderTemplate(w, status, "yourpage.html", data) +// 3. In yourpage.html, use {{template "layout" .}} and define content blocks +// +// See TEMPLATE_GUIDE.md for detailed examples and usage patterns. + +// PageData holds the data passed to page templates. +type PageData struct { + Title string + MenuGroups []MenuGroup + ShowClock bool +} + +// MenuGroup represents a dropdown menu in the navbar. +type MenuGroup struct { + Label string + Items []MenuItem +} + +// MenuItem represents a single menu item. +type MenuItem struct { + Label string + URL string + IsDivider bool +} + +// DefaultMenuBar returns the standard menu configuration. +func DefaultMenuBar() PageData { + return PageData{ + ShowClock: true, + MenuGroups: []MenuGroup{ + { + Label: "File", + Items: []MenuItem{ + {Label: "New", URL: "#!"}, + {Label: "Open", URL: "#!"}, + {Label: "Save", URL: "#!"}, + {Label: "Save As", URL: "#!"}, + {IsDivider: true}, + {Label: "Exit", URL: "#!"}, + }, + }, + { + Label: "Edit", + Items: []MenuItem{ + {Label: "Cut", URL: "#!"}, + {Label: "Copy", URL: "#!"}, + {Label: "Paste", URL: "#!"}, + }, + }, + { + Label: "Help", + Items: []MenuItem{ + {Label: "Documentation", URL: "#!"}, + {Label: "About", URL: "#!"}, + }, + }, + }, + } +} + +// AdminMenuBar returns a menu configuration with admin options. +func AdminMenuBar() PageData { + data := DefaultMenuBar() + data.MenuGroups = append(data.MenuGroups, MenuGroup{ + Label: "Admin", + Items: []MenuItem{ + {Label: "Users", URL: "/admin/users"}, + {Label: "Settings", URL: "/admin/settings"}, + {Label: "Logs", URL: "/admin/logs"}, + }, + }) + return data +} diff --git a/media/demo.txt b/media/demo.txt new file mode 100644 index 0000000..18afc57 --- /dev/null +++ b/media/demo.txt @@ -0,0 +1 @@ +This is a demo media file served from the configurable media handler. diff --git a/web/embed.go b/web/embed.go new file mode 100644 index 0000000..ffc6adf --- /dev/null +++ b/web/embed.go @@ -0,0 +1,8 @@ +package web + +import "embed" + +// Content holds the HTML templates and static assets for the web UI. +// +//go:embed templates/* static +var Content embed.FS diff --git a/web/static/lib/htmx.2.0.7.min.js b/web/static/lib/htmx.2.0.7.min.js new file mode 100644 index 0000000..d49e662 --- /dev/null +++ b/web/static/lib/htmx.2.0.7.min.js @@ -0,0 +1 @@ +var htmx=function(){"use strict";const Q={onLoad:null,process:null,on:null,off:null,trigger:null,ajax:null,find:null,findAll:null,closest:null,values:function(e,t){const n=dn(e,t||"post");return n.values},remove:null,addClass:null,removeClass:null,toggleClass:null,takeClass:null,swap:null,defineExtension:null,removeExtension:null,logAll:null,logNone:null,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",inlineStyleNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",scrollBehavior:"instant",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get","delete"],selfRequestsOnly:true,ignoreTitle:false,scrollIntoViewOnBoost:true,triggerSpecsCache:null,disableInheritance:false,responseHandling:[{code:"204",swap:false},{code:"[23]..",swap:true},{code:"[45]..",swap:false,error:true}],allowNestedOobSwaps:true,historyRestoreAsHxRequest:true,reportValidityOfForms:false},parseInterval:null,location:location,_:null,version:"2.0.7"};Q.onLoad=V;Q.process=Mt;Q.on=xe;Q.off=be;Q.trigger=ae;Q.ajax=Ln;Q.find=f;Q.findAll=x;Q.closest=g;Q.remove=_;Q.addClass=K;Q.removeClass=G;Q.toggleClass=W;Q.takeClass=Z;Q.swap=ze;Q.defineExtension=_n;Q.removeExtension=zn;Q.logAll=j;Q.logNone=$;Q.parseInterval=d;Q._=e;const n={addTriggerHandler:St,bodyContains:se,canAccessLocalStorage:B,findThisElement:Se,filterValues:yn,swap:ze,hasAttribute:s,getAttributeValue:a,getClosestAttributeValue:ne,getClosestMatch:q,getExpressionVars:Tn,getHeaders:mn,getInputValues:dn,getInternalData:oe,getSwapSpecification:bn,getTriggerSpecs:st,getTarget:Ee,makeFragment:P,mergeObjects:le,makeSettleInfo:Sn,oobSwap:He,querySelectorExt:ue,settleImmediately:Yt,shouldCancel:ht,triggerEvent:ae,triggerErrorEvent:fe,withExtensions:Vt};const de=["get","post","put","delete","patch"];const T=de.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}let t=NaN;if(e.slice(-2)=="ms"){t=parseFloat(e.slice(0,-2))}else if(e.slice(-1)=="s"){t=parseFloat(e.slice(0,-1))*1e3}else if(e.slice(-1)=="m"){t=parseFloat(e.slice(0,-1))*1e3*60}else{t=parseFloat(e)}return isNaN(t)?undefined:t}function ee(e,t){return e instanceof Element&&e.getAttribute(t)}function s(e,t){return!!e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function a(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){const t=e.parentElement;if(!t&&e.parentNode instanceof ShadowRoot)return e.parentNode;return t}function te(){return document}function y(e,t){return e.getRootNode?e.getRootNode({composed:t}):te()}function q(e,t){while(e&&!t(e)){e=u(e)}return e||null}function o(e,t,n){const r=a(t,n);const o=a(t,"hx-disinherit");var i=a(t,"hx-inherit");if(e!==t){if(Q.config.disableInheritance){if(i&&(i==="*"||i.split(" ").indexOf(n)>=0)){return r}else{return null}}if(o&&(o==="*"||o.split(" ").indexOf(n)>=0)){return"unset"}}return r}function ne(t,n){let r=null;q(t,function(e){return!!(r=o(t,ce(e),n))});if(r!=="unset"){return r}}function h(e,t){return e instanceof Element&&e.matches(t)}function A(e){const t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;const n=t.exec(e);if(n){return n[1].toLowerCase()}else{return""}}function L(e){const t=new DOMParser;return t.parseFromString(e,"text/html")}function N(e,t){while(t.childNodes.length>0){e.append(t.childNodes[0])}}function r(e){const t=te().createElement("script");ie(e.attributes,function(e){t.setAttribute(e.name,e.value)});t.textContent=e.textContent;t.async=false;if(Q.config.inlineScriptNonce){t.nonce=Q.config.inlineScriptNonce}return t}function i(e){return e.matches("script")&&(e.type==="text/javascript"||e.type==="module"||e.type==="")}function I(e){Array.from(e.querySelectorAll("script")).forEach(e=>{if(i(e)){const t=r(e);const n=e.parentNode;try{n.insertBefore(t,e)}catch(e){R(e)}finally{e.remove()}}})}function P(e){const t=e.replace(/]*)?>[\s\S]*?<\/head>/i,"");const n=A(t);let r;if(n==="html"){r=new DocumentFragment;const i=L(e);N(r,i.body);r.title=i.title}else if(n==="body"){r=new DocumentFragment;const i=L(t);N(r,i.body);r.title=i.title}else{const i=L('");r=i.querySelector("template").content;r.title=i.title;var o=r.querySelector("title");if(o&&o.parentNode===r){o.remove();r.title=o.innerText}}if(r){if(Q.config.allowScriptTags){I(r)}else{r.querySelectorAll("script").forEach(e=>e.remove())}}return r}function re(e){if(e){e()}}function t(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function D(e){return typeof e==="function"}function k(e){return t(e,"Object")}function oe(e){const t="htmx-internal-data";let n=e[t];if(!n){n=e[t]={}}return n}function F(t){const n=[];if(t){for(let e=0;e=0}function se(e){return e.getRootNode({composed:true})===document}function X(e){return e.trim().split(/\s+/)}function le(e,t){for(const n in t){if(t.hasOwnProperty(n)){e[n]=t[n]}}return e}function v(e){try{return JSON.parse(e)}catch(e){R(e);return null}}function B(){const e="htmx:sessionStorageTest";try{sessionStorage.setItem(e,e);sessionStorage.removeItem(e);return true}catch(e){return false}}function U(e){const t=new URL(e,"http://x");if(t){e=t.pathname+t.search}if(e!="/"){e=e.replace(/\/+$/,"")}return e}function e(e){return On(te().body,function(){return eval(e)})}function V(t){const e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function j(){Q.logger=function(e,t,n){if(console){console.log(t,e,n)}}}function $(){Q.logger=null}function f(e,t){if(typeof e!=="string"){return e.querySelector(t)}else{return f(te(),e)}}function x(e,t){if(typeof e!=="string"){return e.querySelectorAll(t)}else{return x(te(),e)}}function b(){return window}function _(e,t){e=w(e);if(t){b().setTimeout(function(){_(e);e=null},t)}else{u(e).removeChild(e)}}function ce(e){return e instanceof Element?e:null}function z(e){return e instanceof HTMLElement?e:null}function J(e){return typeof e==="string"?e:null}function p(e){return e instanceof Element||e instanceof Document||e instanceof DocumentFragment?e:null}function K(e,t,n){e=ce(w(e));if(!e){return}if(n){b().setTimeout(function(){K(e,t);e=null},n)}else{e.classList&&e.classList.add(t)}}function G(e,t,n){let r=ce(w(e));if(!r){return}if(n){b().setTimeout(function(){G(r,t);r=null},n)}else{if(r.classList){r.classList.remove(t);if(r.classList.length===0){r.removeAttribute("class")}}}}function W(e,t){e=w(e);e.classList.toggle(t)}function Z(e,t){e=w(e);ie(e.parentElement.children,function(e){G(e,t)});K(ce(e),t)}function g(e,t){e=ce(w(e));if(e){return e.closest(t)}return null}function l(e,t){return e.substring(0,t.length)===t}function Y(e,t){return e.substring(e.length-t.length)===t}function pe(e){const t=e.trim();if(l(t,"<")&&Y(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function m(t,r,n){if(r.indexOf("global ")===0){return m(t,r.slice(7),true)}t=w(t);const o=[];{let t=0;let n=0;for(let e=0;e"){t--}}if(n0){const r=pe(o.shift());let e;if(r.indexOf("closest ")===0){e=g(ce(t),pe(r.slice(8)))}else if(r.indexOf("find ")===0){e=f(p(t),pe(r.slice(5)))}else if(r==="next"||r==="nextElementSibling"){e=ce(t).nextElementSibling}else if(r.indexOf("next ")===0){e=ge(t,pe(r.slice(5)),!!n)}else if(r==="previous"||r==="previousElementSibling"){e=ce(t).previousElementSibling}else if(r.indexOf("previous ")===0){e=me(t,pe(r.slice(9)),!!n)}else if(r==="document"){e=document}else if(r==="window"){e=window}else if(r==="body"){e=document.body}else if(r==="root"){e=y(t,!!n)}else if(r==="host"){e=t.getRootNode().host}else{s.push(r)}if(e){i.push(e)}}if(s.length>0){const e=s.join(",");const c=p(y(t,!!n));i.push(...F(c.querySelectorAll(e)))}return i}var ge=function(t,e,n){const r=p(y(t,n)).querySelectorAll(e);for(let e=0;e=0;e--){const o=r[e];if(o.compareDocumentPosition(t)===Node.DOCUMENT_POSITION_FOLLOWING){return o}}};function ue(e,t){if(typeof e!=="string"){return m(e,t)[0]}else{return m(te().body,e)[0]}}function w(e,t){if(typeof e==="string"){return f(p(t)||document,e)}else{return e}}function ye(e,t,n,r){if(D(t)){return{target:te().body,event:J(e),listener:t,options:n}}else{return{target:w(e),event:J(t),listener:n,options:r}}}function xe(t,n,r,o){Gn(function(){const e=ye(t,n,r,o);e.target.addEventListener(e.event,e.listener,e.options)});const e=D(n);return e?n:r}function be(t,n,r){Gn(function(){const e=ye(t,n,r);e.target.removeEventListener(e.event,e.listener)});return D(n)?n:r}const ve=te().createElement("output");function we(t,n){const e=ne(t,n);if(e){if(e==="this"){return[Se(t,n)]}else{const r=m(t,e);const o=/(^|,)(\s*)inherit(\s*)($|,)/.test(e);if(o){const i=ce(q(t,function(e){return e!==t&&s(ce(e),n)}));if(i){r.push(...we(i,n))}}if(r.length===0){R('The selector "'+e+'" on '+n+" returned no matches!");return[ve]}else{return r}}}}function Se(e,t){return ce(q(e,function(e){return a(ce(e),t)!=null}))}function Ee(e){const t=ne(e,"hx-target");if(t){if(t==="this"){return Se(e,"hx-target")}else{return ue(e,t)}}else{const n=oe(e);if(n.boosted){return te().body}else{return e}}}function Ce(e){return Q.config.attributesToSettle.includes(e)}function Oe(t,n){ie(Array.from(t.attributes),function(e){if(!n.hasAttribute(e.name)&&Ce(e.name)){t.removeAttribute(e.name)}});ie(n.attributes,function(e){if(Ce(e.name)){t.setAttribute(e.name,e.value)}})}function Re(t,e){const n=Jn(e);for(let e=0;e0){s=e.substring(0,e.indexOf(":"));n=e.substring(e.indexOf(":")+1)}else{s=e}o.removeAttribute("hx-swap-oob");o.removeAttribute("data-hx-swap-oob");const r=m(t,n,false);if(r.length){ie(r,function(e){let t;const n=o.cloneNode(true);t=te().createDocumentFragment();t.appendChild(n);if(!Re(s,e)){t=p(n)}const r={shouldSwap:true,target:e,fragment:t};if(!ae(e,"htmx:oobBeforeSwap",r))return;e=r.target;if(r.shouldSwap){qe(t);$e(s,e,e,t,i);Te()}ie(i.elts,function(e){ae(e,"htmx:oobAfterSwap",r)})});o.parentNode.removeChild(o)}else{o.parentNode.removeChild(o);fe(te().body,"htmx:oobErrorNoTarget",{content:o})}return e}function Te(){const e=f("#--htmx-preserve-pantry--");if(e){for(const t of[...e.children]){const n=f("#"+t.id);n.parentNode.moveBefore(t,n);n.remove()}e.remove()}}function qe(e){ie(x(e,"[hx-preserve], [data-hx-preserve]"),function(e){const t=a(e,"id");const n=te().getElementById(t);if(n!=null){if(e.moveBefore){let e=f("#--htmx-preserve-pantry--");if(e==null){te().body.insertAdjacentHTML("afterend","
");e=f("#--htmx-preserve-pantry--")}e.moveBefore(n,null)}else{e.parentNode.replaceChild(n,e)}}})}function Ae(l,e,c){ie(e.querySelectorAll("[id]"),function(t){const n=ee(t,"id");if(n&&n.length>0){const r=n.replace("'","\\'");const o=t.tagName.replace(":","\\:");const e=p(l);const i=e&&e.querySelector(o+"[id='"+r+"']");if(i&&i!==e){const s=t.cloneNode();Oe(t,i);c.tasks.push(function(){Oe(t,s)})}}})}function Le(e){return function(){G(e,Q.config.addedClass);Mt(ce(e));Ne(p(e));ae(e,"htmx:load")}}function Ne(e){const t="[autofocus]";const n=z(h(e,t)?e:e.querySelector(t));if(n!=null){n.focus()}}function c(e,t,n,r){Ae(e,n,r);while(n.childNodes.length>0){const o=n.firstChild;K(ce(o),Q.config.addedClass);e.insertBefore(o,t);if(o.nodeType!==Node.TEXT_NODE&&o.nodeType!==Node.COMMENT_NODE){r.tasks.push(Le(o))}}}function Ie(e,t){let n=0;while(n0}function ze(h,d,p,g){if(!g){g={}}let m=null;let n=null;let e=function(){re(g.beforeSwapCallback);h=w(h);const r=g.contextElement?y(g.contextElement,false):te();const e=document.activeElement;let t={};t={elt:e,start:e?e.selectionStart:null,end:e?e.selectionEnd:null};const o=Sn(h);if(p.swapStyle==="textContent"){h.textContent=d}else{let n=P(d);o.title=g.title||n.title;if(g.historyRequest){n=n.querySelector("[hx-history-elt],[data-hx-history-elt]")||n}if(g.selectOOB){const i=g.selectOOB.split(",");for(let t=0;t0){b().setTimeout(n,p.settleDelay)}else{n()}};let t=Q.config.globalViewTransitions;if(p.hasOwnProperty("transition")){t=p.transition}const r=g.contextElement||te();if(t&&ae(r,"htmx:beforeTransition",g.eventInfo)&&typeof Promise!=="undefined"&&document.startViewTransition){const o=new Promise(function(e,t){m=e;n=t});const i=e;e=function(){document.startViewTransition(function(){i();return o})}}try{if(p?.swapDelay&&p.swapDelay>0){b().setTimeout(e,p.swapDelay)}else{e()}}catch(e){fe(r,"htmx:swapError",g.eventInfo);re(n);throw e}}function Je(e,t,n){const r=e.getResponseHeader(t);if(r.indexOf("{")===0){const o=v(r);for(const i in o){if(o.hasOwnProperty(i)){let e=o[i];if(k(e)){n=e.target!==undefined?e.target:n}else{e={value:e}}ae(n,i,e)}}}else{const s=r.split(",");for(let e=0;e0){const s=o[0];if(s==="]"){e--;if(e===0){if(n===null){t=t+"true"}o.shift();t+=")})";try{const l=On(r,function(){return Function(t)()},function(){return true});l.source=t;return l}catch(e){fe(te().body,"htmx:syntax:error",{error:e,source:t});return null}}}else if(s==="["){e++}if(tt(s,n,i)){t+="(("+i+"."+s+") ? ("+i+"."+s+") : (window."+s+"))"}else{t=t+s}n=o.shift()}}}function O(e,t){let n="";while(e.length>0&&!t.test(e[0])){n+=e.shift()}return n}function rt(e){let t;if(e.length>0&&Ye.test(e[0])){e.shift();t=O(e,Qe).trim();e.shift()}else{t=O(e,E)}return t}const ot="input, textarea, select";function it(e,t,n){const r=[];const o=et(t);do{O(o,C);const l=o.length;const c=O(o,/[,\[\s]/);if(c!==""){if(c==="every"){const u={trigger:"every"};O(o,C);u.pollInterval=d(O(o,/[,\[\s]/));O(o,C);var i=nt(e,o,"event");if(i){u.eventFilter=i}r.push(u)}else{const f={trigger:c};var i=nt(e,o,"event");if(i){f.eventFilter=i}O(o,C);while(o.length>0&&o[0]!==","){const a=o.shift();if(a==="changed"){f.changed=true}else if(a==="once"){f.once=true}else if(a==="consume"){f.consume=true}else if(a==="delay"&&o[0]===":"){o.shift();f.delay=d(O(o,E))}else if(a==="from"&&o[0]===":"){o.shift();if(Ye.test(o[0])){var s=rt(o)}else{var s=O(o,E);if(s==="closest"||s==="find"||s==="next"||s==="previous"){o.shift();const h=rt(o);if(h.length>0){s+=" "+h}}}f.from=s}else if(a==="target"&&o[0]===":"){o.shift();f.target=rt(o)}else if(a==="throttle"&&o[0]===":"){o.shift();f.throttle=d(O(o,E))}else if(a==="queue"&&o[0]===":"){o.shift();f.queue=O(o,E)}else if(a==="root"&&o[0]===":"){o.shift();f[a]=rt(o)}else if(a==="threshold"&&o[0]===":"){o.shift();f[a]=O(o,E)}else{fe(e,"htmx:syntax:error",{token:o.shift()})}O(o,C)}r.push(f)}}if(o.length===l){fe(e,"htmx:syntax:error",{token:o.shift()})}O(o,C)}while(o[0]===","&&o.shift());if(n){n[t]=r}return r}function st(e){const t=a(e,"hx-trigger");let n=[];if(t){const r=Q.config.triggerSpecsCache;n=r&&r[t]||it(e,t,r)}if(n.length>0){return n}else if(h(e,"form")){return[{trigger:"submit"}]}else if(h(e,'input[type="button"], input[type="submit"]')){return[{trigger:"click"}]}else if(h(e,ot)){return[{trigger:"change"}]}else{return[{trigger:"click"}]}}function lt(e){oe(e).cancelled=true}function ct(e,t,n){const r=oe(e);r.timeout=b().setTimeout(function(){if(se(e)&&r.cancelled!==true){if(!pt(n,e,Bt("hx:poll:trigger",{triggerSpec:n,target:e}))){t(e)}ct(e,t,n)}},n.pollInterval)}function ut(e){return location.hostname===e.hostname&&ee(e,"href")&&ee(e,"href").indexOf("#")!==0}function ft(e){return g(e,Q.config.disableSelector)}function at(t,n,e){if(t instanceof HTMLAnchorElement&&ut(t)&&(t.target===""||t.target==="_self")||t.tagName==="FORM"&&String(ee(t,"method")).toLowerCase()!=="dialog"){n.boosted=true;let r,o;if(t.tagName==="A"){r="get";o=ee(t,"href")}else{const i=ee(t,"method");r=i?i.toLowerCase():"get";o=ee(t,"action");if(o==null||o===""){o=location.href}if(r==="get"&&o.includes("?")){o=o.replace(/\?[^#]+/,"")}}e.forEach(function(e){gt(t,function(e,t){const n=ce(e);if(ft(n)){S(n);return}he(r,o,n,t)},n,e,true)})}}function ht(e,t){if(e.type==="submit"&&t.tagName==="FORM"){return true}else if(e.type==="click"){const n=t.closest('input[type="submit"], button');if(n&&n.form&&n.type==="submit"){return true}const r=t.closest("a");const o=/^#.+/;if(r&&r.href&&!o.test(r.getAttribute("href"))){return true}}return false}function dt(e,t){return oe(e).boosted&&e instanceof HTMLAnchorElement&&t.type==="click"&&(t.ctrlKey||t.metaKey)}function pt(e,t,n){const r=e.eventFilter;if(r){try{return r.call(t,n)!==true}catch(e){const o=r.source;fe(te().body,"htmx:eventFilter:error",{error:e,source:o});return true}}return false}function gt(l,c,e,u,f){const a=oe(l);let t;if(u.from){t=m(l,u.from)}else{t=[l]}if(u.changed){if(!("lastValue"in a)){a.lastValue=new WeakMap}t.forEach(function(e){if(!a.lastValue.has(u)){a.lastValue.set(u,new WeakMap)}a.lastValue.get(u).set(e,e.value)})}ie(t,function(i){const s=function(e){if(!se(l)){i.removeEventListener(u.trigger,s);return}if(dt(l,e)){return}if(f||ht(e,i)){e.preventDefault()}if(pt(u,l,e)){return}const t=oe(e);t.triggerSpec=u;if(t.handledFor==null){t.handledFor=[]}if(t.handledFor.indexOf(l)<0){t.handledFor.push(l);if(u.consume){e.stopPropagation()}if(u.target&&e.target){if(!h(ce(e.target),u.target)){return}}if(u.once){if(a.triggeredOnce){return}else{a.triggeredOnce=true}}if(u.changed){const n=e.target;const r=n.value;const o=a.lastValue.get(u);if(o.has(n)&&o.get(n)===r){return}o.set(n,r)}if(a.delayed){clearTimeout(a.delayed)}if(a.throttle){return}if(u.throttle>0){if(!a.throttle){ae(l,"htmx:trigger");c(l,e);a.throttle=b().setTimeout(function(){a.throttle=null},u.throttle)}}else if(u.delay>0){a.delayed=b().setTimeout(function(){ae(l,"htmx:trigger");c(l,e)},u.delay)}else{ae(l,"htmx:trigger");c(l,e)}}};if(e.listenerInfos==null){e.listenerInfos=[]}e.listenerInfos.push({trigger:u.trigger,listener:s,on:i});i.addEventListener(u.trigger,s)})}let mt=false;let yt=null;function xt(){if(!yt){yt=function(){mt=true};window.addEventListener("scroll",yt);window.addEventListener("resize",yt);setInterval(function(){if(mt){mt=false;ie(te().querySelectorAll("[hx-trigger*='revealed'],[data-hx-trigger*='revealed']"),function(e){bt(e)})}},200)}}function bt(e){if(!s(e,"data-hx-revealed")&&M(e)){e.setAttribute("data-hx-revealed","true");const t=oe(e);if(t.initHash){ae(e,"revealed")}else{e.addEventListener("htmx:afterProcessNode",function(){ae(e,"revealed")},{once:true})}}}function vt(e,t,n,r){const o=function(){if(!n.loaded){n.loaded=true;ae(e,"htmx:trigger");t(e)}};if(r>0){b().setTimeout(o,r)}else{o()}}function wt(t,n,e){let i=false;ie(de,function(r){if(s(t,"hx-"+r)){const o=a(t,"hx-"+r);i=true;n.path=o;n.verb=r;e.forEach(function(e){St(t,e,n,function(e,t){const n=ce(e);if(ft(n)){S(n);return}he(r,o,n,t)})})}});return i}function St(r,e,t,n){if(e.trigger==="revealed"){xt();gt(r,n,t,e);bt(ce(r))}else if(e.trigger==="intersect"){const o={};if(e.root){o.root=ue(r,e.root)}if(e.threshold){o.threshold=parseFloat(e.threshold)}const i=new IntersectionObserver(function(t){for(let e=0;e0){t.polling=true;ct(ce(r),n,e)}else{gt(r,n,t,e)}}function Et(e){const t=ce(e);if(!t){return false}const n=t.attributes;for(let e=0;e", "+e).join(""));return o}else{return[]}}function Tt(e){const t=At(e.target);const n=Nt(e);if(n){n.lastButtonClicked=t}}function qt(e){const t=Nt(e);if(t){t.lastButtonClicked=null}}function At(e){return g(ce(e),"button, input[type='submit']")}function Lt(e){return e.form||g(e,"form")}function Nt(e){const t=At(e.target);if(!t){return}const n=Lt(t);if(!n){return}return oe(n)}function It(e){e.addEventListener("click",Tt);e.addEventListener("focusin",Tt);e.addEventListener("focusout",qt)}function Pt(t,e,n){const r=oe(t);if(!Array.isArray(r.onHandlers)){r.onHandlers=[]}let o;const i=function(e){On(t,function(){if(ft(t)){return}if(!o){o=new Function("event",n)}o.call(t,e)})};t.addEventListener(e,i);r.onHandlers.push({event:e,listener:i})}function Dt(t){De(t);for(let e=0;eQ.config.historyCacheSize){i.shift()}while(i.length>0){try{sessionStorage.setItem("htmx-history-cache",JSON.stringify(i));break}catch(e){fe(te().body,"htmx:historyCacheError",{cause:e,cache:i});i.shift()}}}function Jt(t){if(!B()){return null}t=U(t);const n=v(sessionStorage.getItem("htmx-history-cache"))||[];for(let e=0;e=200&&this.status<400){r.response=this.response;ae(te().body,"htmx:historyCacheMissLoad",r);ze(r.historyElt,r.response,n,{contextElement:r.historyElt,historyRequest:true});$t(r.path);ae(te().body,"htmx:historyRestore",{path:e,cacheMiss:true,serverResponse:r.response})}else{fe(te().body,"htmx:historyCacheMissLoadError",r)}};if(ae(te().body,"htmx:historyCacheMiss",r)){t.send()}}function en(e){Gt();e=e||location.pathname+location.search;const t=Jt(e);if(t){const n={swapStyle:"innerHTML",swapDelay:0,settleDelay:0,scroll:t.scroll};const r={path:e,item:t,historyElt:_t(),swapSpec:n};if(ae(te().body,"htmx:historyCacheHit",r)){ze(r.historyElt,t.content,n,{contextElement:r.historyElt,title:t.title});$t(r.path);ae(te().body,"htmx:historyRestore",r)}}else{if(Q.config.refreshOnHistoryMiss){Q.location.reload(true)}else{Qt(e)}}}function tn(e){let t=we(e,"hx-indicator");if(t==null){t=[e]}ie(t,function(e){const t=oe(e);t.requestCount=(t.requestCount||0)+1;e.classList.add.call(e.classList,Q.config.requestClass)});return t}function nn(e){let t=we(e,"hx-disabled-elt");if(t==null){t=[]}ie(t,function(e){const t=oe(e);t.requestCount=(t.requestCount||0)+1;e.setAttribute("disabled","");e.setAttribute("data-disabled-by-htmx","")});return t}function rn(e,t){ie(e.concat(t),function(e){const t=oe(e);t.requestCount=(t.requestCount||1)-1});ie(e,function(e){const t=oe(e);if(t.requestCount===0){e.classList.remove.call(e.classList,Q.config.requestClass)}});ie(t,function(e){const t=oe(e);if(t.requestCount===0){e.removeAttribute("disabled");e.removeAttribute("data-disabled-by-htmx")}})}function on(t,n){for(let e=0;en.indexOf(e)<0)}else{e=e.filter(e=>e!==n)}r.delete(t);ie(e,e=>r.append(t,e))}}function un(e){if(e instanceof HTMLSelectElement&&e.multiple){return F(e.querySelectorAll("option:checked")).map(function(e){return e.value})}if(e instanceof HTMLInputElement&&e.files){return F(e.files)}return e.value}function fn(t,n,r,e,o){if(e==null||on(t,e)){return}else{t.push(e)}if(sn(e)){const i=ee(e,"name");ln(i,un(e),n);if(o){an(e,r)}}if(e instanceof HTMLFormElement){ie(e.elements,function(e){if(t.indexOf(e)>=0){cn(e.name,un(e),n)}else{t.push(e)}if(o){an(e,r)}});new FormData(e).forEach(function(e,t){if(e instanceof File&&e.name===""){return}ln(t,e,n)})}}function an(e,t){const n=e;if(n.willValidate){ae(n,"htmx:validation:validate");if(!n.checkValidity()){if(ae(n,"htmx:validation:failed",{message:n.validationMessage,validity:n.validity})&&!t.length&&Q.config.reportValidityOfForms){n.reportValidity()}t.push({elt:n,message:n.validationMessage,validity:n.validity})}}}function hn(n,e){for(const t of e.keys()){n.delete(t)}e.forEach(function(e,t){n.append(t,e)});return n}function dn(e,t){const n=[];const r=new FormData;const o=new FormData;const i=[];const s=oe(e);if(s.lastButtonClicked&&!se(s.lastButtonClicked)){s.lastButtonClicked=null}let l=e instanceof HTMLFormElement&&e.noValidate!==true||a(e,"hx-validate")==="true";if(s.lastButtonClicked){l=l&&s.lastButtonClicked.formNoValidate!==true}if(t!=="get"){fn(n,o,i,Lt(e),l)}fn(n,r,i,e,l);if(s.lastButtonClicked||e.tagName==="BUTTON"||e.tagName==="INPUT"&&ee(e,"type")==="submit"){const u=s.lastButtonClicked||e;const f=ee(u,"name");ln(f,u.value,o)}const c=we(e,"hx-include");ie(c,function(e){fn(n,r,i,ce(e),l);if(!h(e,"form")){ie(p(e).querySelectorAll(ot),function(e){fn(n,r,i,e,l)})}});hn(r,o);return{errors:i,formData:r,values:kn(r)}}function pn(e,t,n){if(e!==""){e+="&"}if(String(n)==="[object Object]"){n=JSON.stringify(n)}const r=encodeURIComponent(n);e+=encodeURIComponent(t)+"="+r;return e}function gn(e){e=Pn(e);let n="";e.forEach(function(e,t){n=pn(n,t,e)});return n}function mn(e,t,n){const r={"HX-Request":"true","HX-Trigger":ee(e,"id"),"HX-Trigger-Name":ee(e,"name"),"HX-Target":a(t,"id"),"HX-Current-URL":location.href};Cn(e,"hx-headers",false,r);if(n!==undefined){r["HX-Prompt"]=n}if(oe(e).boosted){r["HX-Boosted"]="true"}return r}function yn(n,e){const t=ne(e,"hx-params");if(t){if(t==="none"){return new FormData}else if(t==="*"){return n}else if(t.indexOf("not ")===0){ie(t.slice(4).split(","),function(e){e=e.trim();n.delete(e)});return n}else{const r=new FormData;ie(t.split(","),function(t){t=t.trim();if(n.has(t)){n.getAll(t).forEach(function(e){r.append(t,e)})}});return r}}else{return n}}function xn(e){return!!ee(e,"href")&&ee(e,"href").indexOf("#")>=0}function bn(e,t){const n=t||ne(e,"hx-swap");const r={swapStyle:oe(e).boosted?"innerHTML":Q.config.defaultSwapStyle,swapDelay:Q.config.defaultSwapDelay,settleDelay:Q.config.defaultSettleDelay};if(Q.config.scrollIntoViewOnBoost&&oe(e).boosted&&!xn(e)){r.show="top"}if(n){const s=X(n);if(s.length>0){for(let e=0;e0?o.join(":"):null;r.scroll=u;r.scrollTarget=i}else if(l.indexOf("show:")===0){const f=l.slice(5);var o=f.split(":");const a=o.pop();var i=o.length>0?o.join(":"):null;r.show=a;r.showTarget=i}else if(l.indexOf("focus-scroll:")===0){const h=l.slice("focus-scroll:".length);r.focusScroll=h=="true"}else if(e==0){r.swapStyle=l}else{R("Unknown modifier in hx-swap: "+l)}}}}return r}function vn(e){return ne(e,"hx-encoding")==="multipart/form-data"||h(e,"form")&&ee(e,"enctype")==="multipart/form-data"}function wn(t,n,r){let o=null;Vt(n,function(e){if(o==null){o=e.encodeParameters(t,r,n)}});if(o!=null){return o}else{if(vn(n)){return hn(new FormData,Pn(r))}else{return gn(r)}}}function Sn(e){return{tasks:[],elts:[e]}}function En(e,t){const n=e[0];const r=e[e.length-1];if(t.scroll){var o=null;if(t.scrollTarget){o=ce(ue(n,t.scrollTarget))}if(t.scroll==="top"&&(n||o)){o=o||n;o.scrollTop=0}if(t.scroll==="bottom"&&(r||o)){o=o||r;o.scrollTop=o.scrollHeight}if(typeof t.scroll==="number"){b().setTimeout(function(){window.scrollTo(0,t.scroll)},0)}}if(t.show){var o=null;if(t.showTarget){let e=t.showTarget;if(t.showTarget==="window"){e="body"}o=ce(ue(n,e))}if(t.show==="top"&&(n||o)){o=o||n;o.scrollIntoView({block:"start",behavior:Q.config.scrollBehavior})}if(t.show==="bottom"&&(r||o)){o=o||r;o.scrollIntoView({block:"end",behavior:Q.config.scrollBehavior})}}}function Cn(r,e,o,i,s){if(i==null){i={}}if(r==null){return i}const l=a(r,e);if(l){let e=l.trim();let t=o;if(e==="unset"){return null}if(e.indexOf("javascript:")===0){e=e.slice(11);t=true}else if(e.indexOf("js:")===0){e=e.slice(3);t=true}if(e.indexOf("{")!==0){e="{"+e+"}"}let n;if(t){n=On(r,function(){if(s){return Function("event","return ("+e+")").call(r,s)}else{return Function("return ("+e+")").call(r)}},{})}else{n=v(e)}for(const c in n){if(n.hasOwnProperty(c)){if(i[c]==null){i[c]=n[c]}}}}return Cn(ce(u(r)),e,o,i,s)}function On(e,t,n){if(Q.config.allowEval){return t()}else{fe(e,"htmx:evalDisallowedError");return n}}function Rn(e,t,n){return Cn(e,"hx-vars",true,n,t)}function Hn(e,t,n){return Cn(e,"hx-vals",false,n,t)}function Tn(e,t){return le(Rn(e,t),Hn(e,t))}function qn(t,n,r){if(r!==null){try{t.setRequestHeader(n,r)}catch(e){t.setRequestHeader(n,encodeURIComponent(r));t.setRequestHeader(n+"-URI-AutoEncoded","true")}}}function An(t){if(t.responseURL){try{const e=new URL(t.responseURL);return e.pathname+e.search}catch(e){fe(te().body,"htmx:badResponseUrl",{url:t.responseURL})}}}function H(e,t){return t.test(e.getAllResponseHeaders())}function Ln(t,n,r){t=t.toLowerCase();if(r){if(r instanceof Element||typeof r==="string"){return he(t,n,null,null,{targetOverride:w(r)||ve,returnPromise:true})}else{let e=w(r.target);if(r.target&&!e||r.source&&!e&&!w(r.source)){e=ve}return he(t,n,w(r.source),r.event,{handler:r.handler,headers:r.headers,values:r.values,targetOverride:e,swapOverride:r.swap,select:r.select,returnPromise:true})}}else{return he(t,n,null,null,{returnPromise:true})}}function Nn(e){const t=[];while(e){t.push(e);e=e.parentElement}return t}function In(e,t,n){const r=new URL(t,location.protocol!=="about:"?location.href:window.origin);const o=location.protocol!=="about:"?location.origin:window.origin;const i=o===r.origin;if(Q.config.selfRequestsOnly){if(!i){return false}}return ae(e,"htmx:validateUrl",le({url:r,sameHost:i},n))}function Pn(e){if(e instanceof FormData)return e;const t=new FormData;for(const n in e){if(e.hasOwnProperty(n)){if(e[n]&&typeof e[n].forEach==="function"){e[n].forEach(function(e){t.append(n,e)})}else if(typeof e[n]==="object"&&!(e[n]instanceof Blob)){t.append(n,JSON.stringify(e[n]))}else{t.append(n,e[n])}}}return t}function Dn(r,o,e){return new Proxy(e,{get:function(t,e){if(typeof e==="number")return t[e];if(e==="length")return t.length;if(e==="push"){return function(e){t.push(e);r.append(o,e)}}if(typeof t[e]==="function"){return function(){t[e].apply(t,arguments);r.delete(o);t.forEach(function(e){r.append(o,e)})}}if(t[e]&&t[e].length===1){return t[e][0]}else{return t[e]}},set:function(e,t,n){e[t]=n;r.delete(o);e.forEach(function(e){r.append(o,e)});return true}})}function kn(o){return new Proxy(o,{get:function(e,t){if(typeof t==="symbol"){const r=Reflect.get(e,t);if(typeof r==="function"){return function(){return r.apply(o,arguments)}}else{return r}}if(t==="toJSON"){return()=>Object.fromEntries(o)}if(t in e){if(typeof e[t]==="function"){return function(){return o[t].apply(o,arguments)}}}const n=o.getAll(t);if(n.length===0){return undefined}else if(n.length===1){return n[0]}else{return Dn(e,t,n)}},set:function(t,n,e){if(typeof n!=="string"){return false}t.delete(n);if(e&&typeof e.forEach==="function"){e.forEach(function(e){t.append(n,e)})}else if(typeof e==="object"&&!(e instanceof Blob)){t.append(n,JSON.stringify(e))}else{t.append(n,e)}return true},deleteProperty:function(e,t){if(typeof t==="string"){e.delete(t)}return true},ownKeys:function(e){return Reflect.ownKeys(Object.fromEntries(e))},getOwnPropertyDescriptor:function(e,t){return Reflect.getOwnPropertyDescriptor(Object.fromEntries(e),t)}})}function he(t,n,r,o,i,k){let s=null;let l=null;i=i!=null?i:{};if(i.returnPromise&&typeof Promise!=="undefined"){var e=new Promise(function(e,t){s=e;l=t})}if(r==null){r=te().body}const F=i.handler||Vn;const M=i.select||null;if(!se(r)){re(s);return e}const c=i.targetOverride||ce(Ee(r));if(c==null||c==ve){fe(r,"htmx:targetError",{target:ne(r,"hx-target")});re(l);return e}let u=oe(r);const f=u.lastButtonClicked;if(f){const A=ee(f,"formaction");if(A!=null){n=A}const L=ee(f,"formmethod");if(L!=null){if(de.includes(L.toLowerCase())){t=L}else{re(s);return e}}}const a=ne(r,"hx-confirm");if(k===undefined){const K=function(e){return he(t,n,r,o,i,!!e)};const G={target:c,elt:r,path:n,verb:t,triggeringEvent:o,etc:i,issueRequest:K,question:a};if(ae(r,"htmx:confirm",G)===false){re(s);return e}}let h=r;let d=ne(r,"hx-sync");let p=null;let X=false;if(d){const N=d.split(":");const I=N[0].trim();if(I==="this"){h=Se(r,"hx-sync")}else{h=ce(ue(r,I))}d=(N[1]||"drop").trim();u=oe(h);if(d==="drop"&&u.xhr&&u.abortable!==true){re(s);return e}else if(d==="abort"){if(u.xhr){re(s);return e}else{X=true}}else if(d==="replace"){ae(h,"htmx:abort")}else if(d.indexOf("queue")===0){const W=d.split(" ");p=(W[1]||"last").trim()}}if(u.xhr){if(u.abortable){ae(h,"htmx:abort")}else{if(p==null){if(o){const P=oe(o);if(P&&P.triggerSpec&&P.triggerSpec.queue){p=P.triggerSpec.queue}}if(p==null){p="last"}}if(u.queuedRequests==null){u.queuedRequests=[]}if(p==="first"&&u.queuedRequests.length===0){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(p==="all"){u.queuedRequests.push(function(){he(t,n,r,o,i)})}else if(p==="last"){u.queuedRequests=[];u.queuedRequests.push(function(){he(t,n,r,o,i)})}re(s);return e}}const g=new XMLHttpRequest;u.xhr=g;u.abortable=X;const m=function(){u.xhr=null;u.abortable=false;if(u.queuedRequests!=null&&u.queuedRequests.length>0){const e=u.queuedRequests.shift();e()}};const B=ne(r,"hx-prompt");if(B){var y=prompt(B);if(y===null||!ae(r,"htmx:prompt",{prompt:y,target:c})){re(s);m();return e}}if(a&&!k){if(!confirm(a)){re(s);m();return e}}let x=mn(r,c,y);if(t!=="get"&&!vn(r)){x["Content-Type"]="application/x-www-form-urlencoded"}if(i.headers){x=le(x,i.headers)}const U=dn(r,t);let b=U.errors;const V=U.formData;if(i.values){hn(V,Pn(i.values))}const j=Pn(Tn(r,o));const v=hn(V,j);let w=yn(v,r);if(Q.config.getCacheBusterParam&&t==="get"){w.set("org.htmx.cache-buster",ee(c,"id")||"true")}if(n==null||n===""){n=location.href}const S=Cn(r,"hx-request");const $=oe(r).boosted;let E=Q.config.methodsThatUseUrlParams.indexOf(t)>=0;const C={boosted:$,useUrlParams:E,formData:w,parameters:kn(w),unfilteredFormData:v,unfilteredParameters:kn(v),headers:x,elt:r,target:c,verb:t,errors:b,withCredentials:i.credentials||S.credentials||Q.config.withCredentials,timeout:i.timeout||S.timeout||Q.config.timeout,path:n,triggeringEvent:o};if(!ae(r,"htmx:configRequest",C)){re(s);m();return e}n=C.path;t=C.verb;x=C.headers;w=Pn(C.parameters);b=C.errors;E=C.useUrlParams;if(b&&b.length>0){ae(r,"htmx:validation:halted",C);re(s);m();return e}const _=n.split("#");const z=_[0];const O=_[1];let R=n;if(E){R=z;const Z=!w.keys().next().done;if(Z){if(R.indexOf("?")<0){R+="?"}else{R+="&"}R+=gn(w);if(O){R+="#"+O}}}if(!In(r,R,C)){fe(r,"htmx:invalidPath",C);re(l);m();return e}g.open(t.toUpperCase(),R,true);g.overrideMimeType("text/html");g.withCredentials=C.withCredentials;g.timeout=C.timeout;if(S.noHeaders){}else{for(const D in x){if(x.hasOwnProperty(D)){const Y=x[D];qn(g,D,Y)}}}const H={xhr:g,target:c,requestConfig:C,etc:i,boosted:$,select:M,pathInfo:{requestPath:n,finalRequestPath:R,responsePath:null,anchor:O}};g.onload=function(){try{const t=Nn(r);H.pathInfo.responsePath=An(g);F(r,H);if(H.keepIndicators!==true){rn(T,q)}ae(r,"htmx:afterRequest",H);ae(r,"htmx:afterOnLoad",H);if(!se(r)){let e=null;while(t.length>0&&e==null){const n=t.shift();if(se(n)){e=n}}if(e){ae(e,"htmx:afterRequest",H);ae(e,"htmx:afterOnLoad",H)}}re(s)}catch(e){fe(r,"htmx:onLoadError",le({error:e},H));throw e}finally{m()}};g.onerror=function(){rn(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendError",H);re(l);m()};g.onabort=function(){rn(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:sendAbort",H);re(l);m()};g.ontimeout=function(){rn(T,q);fe(r,"htmx:afterRequest",H);fe(r,"htmx:timeout",H);re(l);m()};if(!ae(r,"htmx:beforeRequest",H)){re(s);m();return e}var T=tn(r);var q=nn(r);ie(["loadstart","loadend","progress","abort"],function(t){ie([g,g.upload],function(e){e.addEventListener(t,function(e){ae(r,"htmx:xhr:"+t,{lengthComputable:e.lengthComputable,loaded:e.loaded,total:e.total})})})});ae(r,"htmx:beforeSend",H);const J=E?null:wn(g,r,w);g.send(J);return e}function Fn(e,t){const n=t.xhr;let r=null;let o=null;if(H(n,/HX-Push:/i)){r=n.getResponseHeader("HX-Push");o="push"}else if(H(n,/HX-Push-Url:/i)){r=n.getResponseHeader("HX-Push-Url");o="push"}else if(H(n,/HX-Replace-Url:/i)){r=n.getResponseHeader("HX-Replace-Url");o="replace"}if(r){if(r==="false"){return{}}else{return{type:o,path:r}}}const i=t.pathInfo.finalRequestPath;const s=t.pathInfo.responsePath;const l=ne(e,"hx-push-url");const c=ne(e,"hx-replace-url");const u=oe(e).boosted;let f=null;let a=null;if(l){f="push";a=l}else if(c){f="replace";a=c}else if(u){f="push";a=s||i}if(a){if(a==="false"){return{}}if(a==="true"){a=s||i}if(t.pathInfo.anchor&&a.indexOf("#")===-1){a=a+"#"+t.pathInfo.anchor}return{type:f,path:a}}else{return{}}}function Mn(e,t){var n=new RegExp(e.code);return n.test(t.toString(10))}function Xn(e){for(var t=0;t`+`.${t}{opacity:0;visibility: hidden} `+`.${n} .${t}, .${n}.${t}{opacity:1;visibility: visible;transition: opacity 200ms ease-in}`+"")}}function Zn(){const e=te().querySelector('meta[name="htmx-config"]');if(e){return v(e.content)}else{return null}}function Yn(){const e=Zn();if(e){Q.config=le(Q.config,e)}}Gn(function(){Yn();Wn();let e=te().body;Mt(e);const t=te().querySelectorAll("[hx-trigger='restored'],[data-hx-trigger='restored']");e.addEventListener("htmx:abort",function(e){const t=e.target;const n=oe(t);if(n&&n.xhr){n.xhr.abort()}});const n=window.onpopstate?window.onpopstate.bind(window):null;window.onpopstate=function(e){if(e.state&&e.state.htmx){en();ie(t,function(e){ae(e,"htmx:restored",{document:te(),triggerEvent:ae})})}else{if(n){n(e)}}};b().setTimeout(function(){ae(e,"htmx:load",{});e=null},0)});return Q}(); \ No newline at end of file diff --git a/web/static/lib/tuicss/fonts/Perfect DOS VGA 437 Win.ttf b/web/static/lib/tuicss/fonts/Perfect DOS VGA 437 Win.ttf new file mode 100644 index 0000000..d03b1c5 Binary files /dev/null and b/web/static/lib/tuicss/fonts/Perfect DOS VGA 437 Win.ttf differ diff --git a/web/static/lib/tuicss/fonts/Perfect DOS VGA 437.ttf b/web/static/lib/tuicss/fonts/Perfect DOS VGA 437.ttf new file mode 100644 index 0000000..f5cbfc0 Binary files /dev/null and b/web/static/lib/tuicss/fonts/Perfect DOS VGA 437.ttf differ diff --git a/web/static/lib/tuicss/fonts/dos437.txt b/web/static/lib/tuicss/fonts/dos437.txt new file mode 100644 index 0000000..d4860e2 --- /dev/null +++ b/web/static/lib/tuicss/fonts/dos437.txt @@ -0,0 +1,72 @@ + / +/(_____________ ____ +\ /______)\ | | +:\ | / \:| |:::::::::: : .. . : .. . . :. . + \_____| / | \| |______ +___ / ________ \... . . . +\______________ \ | | /.. . . . . . + \ |__| / +--x--x-----x----\______ |-/_____/-x--x-xx--x-- - -x -- - - -- - - - +. . . . . . . . . . . .\____|. . . . . . +------------------------------------------------------------------------------- +>> perfect dos vga 437 - general information >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +------------------------------------------------------------------------------- + + "Perfect DOS VGA 437" and "Perfect DOS VGA 437 Win" are truetype fonts + designed to emulate the MS-DOS/Text mode standard font, used on VGA monitors, + with the 437 Codepage (standard US/International). This is a "bitmap" font, + meaning it emulates a bitmap font and can only be used at a given size (8 or + multiples of it like 16, 24, 32, etc). It's optimized for Flash too, so it + won't produce antialias if used at round positions. + + There are two fonts available. "Perfect DOS VGA 437" uses the original DOS + codepage 437. It should be used, for example, if you're opening DOS ASCII + files on notepad or another windows-based editor. Since it's faithful to the + original DOS codes, it won't accent correctly in windows ("" would produce + something different, not an "e" with an acute). + + There's also "Perfect DOS VGA 437 Win" which is the exactly same font adapted + to a windows codepage. This should use accented characters correctly but won't + work if you're opening a DOS-based text file. + + UPDATE: this is a new version, updated in august/2008. It has fixed leading + metrics for Mac systems. + +------------------------------------------------------------------------------- +>> perfect dos vga 437 - creation process >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +------------------------------------------------------------------------------- + + This font was created to be used on a Flash-based ANSi viewer I'm working. To + create it, I created a small Quick Basic program to write all characters on + screen, + + CLS + FOR l = 0 TO 255 + charWrite 1 + (l MOD 20), 1 + (l \ 20) * 6 + (l MOD 2), LTRIM$(RTRIM$(STR$(l))) + CHR$(l) + NEXT + SUB charWrite (lin, col, char$) + DEF SEG = &HB800 + FOR i = 1 TO LEN(char$) + POKE ((lin - 1) * 160) + ((col - 2 + i) * 2), ASC(MID$(char$, i, 1)) + IF (i = LEN(char$)) THEN POKE ((lin - 1) * 160) + ((col - 2 + i) * 2) + 1, 113 + NEXT + END SUB + + Then captured the text screen using SCREEN THIEF (a very, very old screen + capture TSR program which converts text screens to images accurately). I then + recreated the font polygon by polygon on Fontlab, while looking at the image + on Photoshop. No conversion took place. + +------------------------------------------------------------------------------- +>> author >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +------------------------------------------------------------------------------- + + zeh fernando remembers the old days. SMASH DAH FUCKING ENTAH. + + http://www.fatorcaos.com.br + + rorshack ^ maiden brazil + +------------------------------------------------------------------------------- +^zehPULLSdahTRICK^kudosOUTtoWHOkeepsITreal^smashDAHfuckingENTAH!!!^lowres4ever^ +------------------------------------------------------------------------------- diff --git a/web/static/lib/tuicss/images/bg-blue-black.png b/web/static/lib/tuicss/images/bg-blue-black.png new file mode 100644 index 0000000..aa55706 Binary files /dev/null and b/web/static/lib/tuicss/images/bg-blue-black.png differ diff --git a/web/static/lib/tuicss/images/bg-blue-white.png b/web/static/lib/tuicss/images/bg-blue-white.png new file mode 100644 index 0000000..45409ed Binary files /dev/null and b/web/static/lib/tuicss/images/bg-blue-white.png differ diff --git a/web/static/lib/tuicss/images/bg-cyan-black.png b/web/static/lib/tuicss/images/bg-cyan-black.png new file mode 100644 index 0000000..12db210 Binary files /dev/null and b/web/static/lib/tuicss/images/bg-cyan-black.png differ diff --git a/web/static/lib/tuicss/images/bg-cyan-white.png b/web/static/lib/tuicss/images/bg-cyan-white.png new file mode 100644 index 0000000..41ce181 Binary files /dev/null and b/web/static/lib/tuicss/images/bg-cyan-white.png differ diff --git a/web/static/lib/tuicss/images/bg-green-black.png b/web/static/lib/tuicss/images/bg-green-black.png new file mode 100644 index 0000000..2f83f3c Binary files /dev/null and b/web/static/lib/tuicss/images/bg-green-black.png differ diff --git a/web/static/lib/tuicss/images/bg-green-white.png b/web/static/lib/tuicss/images/bg-green-white.png new file mode 100644 index 0000000..85cd3f9 Binary files /dev/null and b/web/static/lib/tuicss/images/bg-green-white.png differ diff --git a/web/static/lib/tuicss/images/bg-orange-black.png b/web/static/lib/tuicss/images/bg-orange-black.png new file mode 100644 index 0000000..05e8662 Binary files /dev/null and b/web/static/lib/tuicss/images/bg-orange-black.png differ diff --git a/web/static/lib/tuicss/images/bg-orange-white.png b/web/static/lib/tuicss/images/bg-orange-white.png new file mode 100644 index 0000000..8c82141 Binary files /dev/null and b/web/static/lib/tuicss/images/bg-orange-white.png differ diff --git a/web/static/lib/tuicss/images/bg-purple-black.png b/web/static/lib/tuicss/images/bg-purple-black.png new file mode 100644 index 0000000..240217f Binary files /dev/null and b/web/static/lib/tuicss/images/bg-purple-black.png differ diff --git a/web/static/lib/tuicss/images/bg-purple-white.png b/web/static/lib/tuicss/images/bg-purple-white.png new file mode 100644 index 0000000..5ae0ca2 Binary files /dev/null and b/web/static/lib/tuicss/images/bg-purple-white.png differ diff --git a/web/static/lib/tuicss/images/bg-red-black.png b/web/static/lib/tuicss/images/bg-red-black.png new file mode 100644 index 0000000..dc2c0be Binary files /dev/null and b/web/static/lib/tuicss/images/bg-red-black.png differ diff --git a/web/static/lib/tuicss/images/bg-red-white.png b/web/static/lib/tuicss/images/bg-red-white.png new file mode 100644 index 0000000..4470d9b Binary files /dev/null and b/web/static/lib/tuicss/images/bg-red-white.png differ diff --git a/web/static/lib/tuicss/images/bg-yellow-black.png b/web/static/lib/tuicss/images/bg-yellow-black.png new file mode 100644 index 0000000..ace85f9 Binary files /dev/null and b/web/static/lib/tuicss/images/bg-yellow-black.png differ diff --git a/web/static/lib/tuicss/images/bg-yellow-white.png b/web/static/lib/tuicss/images/bg-yellow-white.png new file mode 100644 index 0000000..457edd3 Binary files /dev/null and b/web/static/lib/tuicss/images/bg-yellow-white.png differ diff --git a/web/static/lib/tuicss/images/scroll-blue.png b/web/static/lib/tuicss/images/scroll-blue.png new file mode 100644 index 0000000..4ac322b Binary files /dev/null and b/web/static/lib/tuicss/images/scroll-blue.png differ diff --git a/web/static/lib/tuicss/images/scroll-cyan.png b/web/static/lib/tuicss/images/scroll-cyan.png new file mode 100644 index 0000000..ca62812 Binary files /dev/null and b/web/static/lib/tuicss/images/scroll-cyan.png differ diff --git a/web/static/lib/tuicss/images/scroll-green.png b/web/static/lib/tuicss/images/scroll-green.png new file mode 100644 index 0000000..d09971f Binary files /dev/null and b/web/static/lib/tuicss/images/scroll-green.png differ diff --git a/web/static/lib/tuicss/images/scroll-orange.png b/web/static/lib/tuicss/images/scroll-orange.png new file mode 100644 index 0000000..ec20219 Binary files /dev/null and b/web/static/lib/tuicss/images/scroll-orange.png differ diff --git a/web/static/lib/tuicss/images/scroll-purple.png b/web/static/lib/tuicss/images/scroll-purple.png new file mode 100644 index 0000000..67a0550 Binary files /dev/null and b/web/static/lib/tuicss/images/scroll-purple.png differ diff --git a/web/static/lib/tuicss/images/scroll-red.png b/web/static/lib/tuicss/images/scroll-red.png new file mode 100644 index 0000000..1dccb6a Binary files /dev/null and b/web/static/lib/tuicss/images/scroll-red.png differ diff --git a/web/static/lib/tuicss/images/scroll-white.png b/web/static/lib/tuicss/images/scroll-white.png new file mode 100644 index 0000000..c972a5e Binary files /dev/null and b/web/static/lib/tuicss/images/scroll-white.png differ diff --git a/web/static/lib/tuicss/images/scroll-yellow.png b/web/static/lib/tuicss/images/scroll-yellow.png new file mode 100644 index 0000000..e1a915c Binary files /dev/null and b/web/static/lib/tuicss/images/scroll-yellow.png differ diff --git a/web/static/lib/tuicss/tuicss.css b/web/static/lib/tuicss/tuicss.css new file mode 100644 index 0000000..bde8521 --- /dev/null +++ b/web/static/lib/tuicss/tuicss.css @@ -0,0 +1,2704 @@ +@charset "UTF-8"; +/* Styles */ /* Font (Options: 'Lucida Console' or 'DOS') */ +/* Characters */ +/* Theme */ +/* Responsive */ +/* Scrool */ +/* Grid */ +@font-face { + font-family: "DOS"; + src: url("./fonts/Perfect DOS VGA 437 Win.ttf"); +} +/* Global Definitions */ +html { + font-family: "Lucida Console", "monospace"; + font-size: 18px; + box-sizing: border-box; +} + +body { + margin: 0px; +} + +*, *:before, *:after { + font-family: inherit; + font-size: inherit; + box-sizing: inherit; +} + +ul { + margin: 0px; + padding: 0px; + list-style-type: none; +} + +ul li { + list-style-type: none; +} + +ul li a { + display: block; +} + +a { + color: inherit; + text-decoration: none; +} + +span { + margin: 0px; +} + +hr { + border: none; + border-bottom: 2px solid rgb(255, 255, 255); +} + +input, select, textarea { + width: 200px; +} + +/* Font (Options: 'Lucida Console' or 'DOS') */ +/* Characters */ +/* Theme */ +/* Responsive */ +/* Scrool */ +/* Grid */ +@media only screen and (max-width : 600px) { + .hide-on-small-only, .hide-on-small-and-down { + display: none !important; + } +} + +@media only screen and (max-width : 992px) { + .hide-on-med-and-down { + display: none !important; + } +} + +@media only screen and (min-width : 601px) { + .hide-on-med-and-up { + display: none !important; + } +} + +@media only screen and (min-width: 600px) and (max-width: 992px) { + .hide-on-med-only { + display: none !important; + } +} + +@media only screen and (min-width : 993px) { + .hide-on-large-only { + display: none !important; + } +} + +@media only screen and (min-width : 1201px) { + .hide-on-extra-large-only { + display: none !important; + } +} + +@media only screen and (min-width : 1201px) { + .show-on-extra-large { + display: block !important; + } +} + +@media only screen and (min-width : 993px) { + .show-on-large { + display: block !important; + } +} + +@media only screen and (min-width: 600px) and (max-width: 992px) { + .show-on-medium { + display: block !important; + } +} + +@media only screen and (max-width : 600px) { + .show-on-small { + display: block !important; + } +} + +@media only screen and (min-width : 601px) { + .show-on-medium-and-up { + display: block !important; + } +} + +@media only screen and (max-width : 992px) { + .show-on-medium-and-down { + display: block !important; + } +} + +/* Font (Options: 'Lucida Console' or 'DOS') */ +/* Characters */ +/* Theme */ +/* Responsive */ +/* Scrool */ +/* Grid */ +/* Theme */ +.primary { + background-color: rgb(0, 0, 168); +} + +.primary-text { + color: rgb(0, 0, 168); +} + +.primary-border { + border-color: rgb(0, 0, 168); +} + +.primary-hover:hover { + background-color: rgb(0, 0, 168); +} + +.primary-text-hover:hover { + color: rgb(0, 0, 168); +} + +.primary-border-hover:hover { + border-color: rgb(0, 0, 168); +} + +.secondary { + background-color: rgb(168, 168, 168); +} + +.secondary-text { + color: rgb(168, 168, 168); +} + +.secondary-border { + border-color: rgb(168, 168, 168); +} + +.secondary-hover:hover { + background-color: rgb(168, 168, 168); +} + +.secondary-text-hover:hover { + color: rgb(168, 168, 168); +} + +.secondary-border-hover:hover { + border-color: rgb(168, 168, 168); +} + +.success { + background-color: rgb(0, 168, 0); +} + +.success-text { + color: rgb(0, 168, 0); +} + +.success-border { + border-color: rgb(0, 168, 0); +} + +.success-hover:hover { + background-color: rgb(0, 168, 0); +} + +.success-text-hover:hover { + color: rgb(0, 168, 0); +} + +.success-border-hover:hover { + border-color: rgb(0, 168, 0); +} + +.danger { + background-color: rgb(168, 0, 0); +} + +.danger-text { + color: rgb(168, 0, 0); +} + +.danger-border { + border-color: rgb(168, 0, 0); +} + +.danger-hover:hover { + background-color: rgb(168, 0, 0); +} + +.danger-text-hover:hover { + color: rgb(168, 0, 0); +} + +.danger-border-hover:hover { + border-color: rgb(168, 0, 0); +} + +.warning { + background-color: rgb(168, 168, 0); +} + +.warning-text { + color: rgb(168, 168, 0); +} + +.warning-border { + border-color: rgb(168, 168, 0); +} + +.warning-hover:hover { + background-color: rgb(168, 168, 0); +} + +.warning-text-hover:hover { + color: rgb(168, 168, 0); +} + +.warning-border-hover:hover { + border-color: rgb(168, 168, 0); +} + +.info { + background-color: rgb(0, 168, 168); +} + +.info-text { + color: rgb(0, 168, 168); +} + +.info-border { + border-color: rgb(0, 168, 168); +} + +.info-hover:hover { + background-color: rgb(0, 168, 168); +} + +.info-text-hover:hover { + color: rgb(0, 168, 168); +} + +.info-border-hover:hover { + border-color: rgb(0, 168, 168); +} + +/* 168 */ +.black-168 { + background-color: rgb(0, 0, 0) !important; +} + +.blue-168 { + background-color: rgb(0, 0, 168) !important; +} + +.green-168 { + background-color: rgb(0, 168, 0) !important; +} + +.cyan-168 { + background-color: rgb(0, 168, 168) !important; +} + +.red-168 { + background-color: rgb(168, 0, 0) !important; +} + +.purple-168 { + background-color: rgb(168, 0, 168) !important; +} + +.yellow-168 { + background-color: rgb(168, 168, 0) !important; +} + +.white-168 { + background-color: rgb(168, 168, 168) !important; +} + +.orange-168 { + background-color: rgb(168, 86, 0) !important; +} + +.black-168-text { + color: rgb(0, 0, 0) !important; +} + +.blue-168-text { + color: rgb(0, 0, 168) !important; +} + +.green-168-text { + color: rgb(0, 168, 0) !important; +} + +.cyan-168-text { + color: rgb(0, 168, 168) !important; +} + +.red-168-text { + color: rgb(168, 0, 0) !important; +} + +.purple-168-text { + color: rgb(168, 0, 168) !important; +} + +.yellow-168-text { + color: rgb(168, 168, 0) !important; +} + +.white-168-text { + color: rgb(168, 168, 168) !important; +} + +.orange-168-text { + color: rgb(168, 86, 0) !important; +} + +.black-168-border { + border-color: rgb(0, 0, 0) !important; +} + +.blue-168-border { + border-color: rgb(0, 0, 168) !important; +} + +.green-168-border { + border-color: rgb(0, 168, 0) !important; +} + +.cyan-168-border { + border-color: rgb(0, 168, 168) !important; +} + +.red-168-border { + border-color: rgb(168, 0, 0) !important; +} + +.purple-168-border { + border-color: rgb(168, 0, 168) !important; +} + +.yellow-168-border { + border-color: rgb(168, 168, 0) !important; +} + +.white-168-border { + border-color: rgb(168, 168, 168) !important; +} + +.orange-168-border { + border-color: rgb(168, 86, 0) !important; +} + +.black-168-hover:hover { + background-color: rgb(0, 0, 0) !important; +} + +.blue-168-hover:hover { + background-color: rgb(0, 0, 168) !important; +} + +.green-168-hover:hover { + background-color: rgb(0, 168, 0) !important; +} + +.cyan-168-hover:hover { + background-color: rgb(0, 168, 168) !important; +} + +.red-168-hover:hover { + background-color: rgb(168, 0, 0) !important; +} + +.purple-168-hover:hover { + background-color: rgb(168, 0, 168) !important; +} + +.yellow-168-hover:hover { + background-color: rgb(168, 168, 0) !important; +} + +.white-168-hover:hover { + background-color: rgb(168, 168, 168) !important; +} + +.orange-168-hover:hover { + background-color: rgb(168, 86, 0) !important; +} + +.black-168-text-hover:hover { + color: rgb(0, 0, 0) !important; +} + +.blue-168-text-hover:hover { + color: rgb(0, 0, 168) !important; +} + +.green-168-text-hover:hover { + color: rgb(0, 168, 0) !important; +} + +.cyan-168-text-hover:hover { + color: rgb(0, 168, 168) !important; +} + +.red-168-text-hover:hover { + color: rgb(168, 0, 0) !important; +} + +.purple-168-text-hover:hover { + color: rgb(168, 0, 168) !important; +} + +.yellow-168-text-hover:hover { + color: rgb(168, 168, 0) !important; +} + +.white-168-text-hover:hover { + color: rgb(168, 168, 168) !important; +} + +.orange-168-text-hover:hover { + color: rgb(168, 86, 0) !important; +} + +.black-168-border-hover:hover { + border-color: rgb(0, 0, 0) !important; +} + +.blue-168-border-hover:hover { + border-color: rgb(0, 0, 168) !important; +} + +.green-168-border-hover:hover { + border-color: rgb(0, 168, 0) !important; +} + +.cyan-168-border-hover:hover { + border-color: rgb(0, 168, 168) !important; +} + +.red-168-border-hover:hover { + border-color: rgb(168, 0, 0) !important; +} + +.purple-168-border-hover:hover { + border-color: rgb(168, 0, 168) !important; +} + +.yellow-168-border-hover:hover { + border-color: rgb(168, 168, 0) !important; +} + +.white-168-border-hover:hover { + border-color: rgb(168, 168, 168) !important; +} + +.orange-168-border-hover:hover { + border-color: rgb(168, 86, 0) !important; +} + +/* 255 */ +.black-255 { + background-color: rgb(0, 0, 0) !important; +} + +.blue-255 { + background-color: rgb(0, 0, 255) !important; +} + +.green-255 { + background-color: rgb(0, 255, 0) !important; +} + +.cyan-255 { + background-color: rgb(0, 255, 255) !important; +} + +.red-255 { + background-color: rgb(255, 0, 0) !important; +} + +.purple-255 { + background-color: rgb(255, 0, 255) !important; +} + +.yellow-255 { + background-color: rgb(255, 255, 0) !important; +} + +.white-255 { + background-color: rgb(255, 255, 255) !important; +} + +.orange-255 { + background-color: rgb(255, 168, 0) !important; +} + +.black-255-text { + color: rgb(0, 0, 0) !important; +} + +.blue-255-text { + color: rgb(0, 0, 255) !important; +} + +.green-255-text { + color: rgb(0, 255, 0) !important; +} + +.cyan-255-text { + color: rgb(0, 255, 255) !important; +} + +.red-255-text { + color: rgb(255, 0, 0) !important; +} + +.purple-255-text { + color: rgb(255, 0, 255) !important; +} + +.yellow-255-text { + color: rgb(255, 255, 0) !important; +} + +.white-255-text { + color: rgb(255, 255, 255) !important; +} + +.orange-255-text { + color: rgb(255, 168, 0) !important; +} + +.black-255-border { + border-color: rgb(0, 0, 0) !important; +} + +.blue-255-border { + border-color: rgb(0, 0, 255) !important; +} + +.green-255-border { + border-color: rgb(0, 255, 0) !important; +} + +.cyan-255-border { + border-color: rgb(0, 255, 255) !important; +} + +.red-255-border { + border-color: rgb(255, 0, 0) !important; +} + +.purple-255-border { + border-color: rgb(255, 0, 255) !important; +} + +.yellow-255-border { + border-color: rgb(255, 255, 0) !important; +} + +.white-255-border { + border-color: rgb(255, 255, 255) !important; +} + +.orange-255-border { + border-color: rgb(255, 168, 0) !important; +} + +.black-255-hover:hover { + background-color: rgb(0, 0, 0) !important; +} + +.blue-255-hover:hover { + background-color: rgb(0, 0, 255) !important; +} + +.green-255-hover:hover { + background-color: rgb(0, 255, 0) !important; +} + +.cyan-255-hover:hover { + background-color: rgb(0, 255, 255) !important; +} + +.red-255-hover:hover { + background-color: rgb(255, 0, 0) !important; +} + +.purple-255-hover:hover { + background-color: rgb(255, 0, 255) !important; +} + +.yellow-255-hover:hover { + background-color: rgb(255, 255, 0) !important; +} + +.white-255-hover:hover { + background-color: rgb(255, 255, 255) !important; +} + +.orange-255-hover:hover { + background-color: rgb(255, 168, 0) !important; +} + +.black-255-text-hover:hover { + color: rgb(0, 0, 0) !important; +} + +.blue-255-text-hover:hover { + color: rgb(0, 0, 255) !important; +} + +.green-255-text-hover:hover { + color: rgb(0, 255, 0) !important; +} + +.cyan-255-text-hover:hover { + color: rgb(0, 255, 255) !important; +} + +.red-255-text-hover:hover { + color: rgb(255, 0, 0) !important; +} + +.purple-255-text-hover:hover { + color: rgb(255, 0, 255) !important; +} + +.yellow-255-text-hover:hover { + color: rgb(255, 255, 0) !important; +} + +.white-255-text-hover:hover { + color: rgb(255, 255, 255) !important; +} + +.orange-255-text-hover:hover { + color: rgb(255, 168, 0) !important; +} + +.black-255-border-hover:hover { + border-color: rgb(0, 0, 0) !important; +} + +.blue-255-border-hover:hover { + border-color: rgb(0, 0, 255) !important; +} + +.green-255-border-hover:hover { + border-color: rgb(0, 255, 0) !important; +} + +.cyan-255-border-hover:hover { + border-color: rgb(0, 255, 255) !important; +} + +.red-255-border-hover:hover { + border-color: rgb(255, 0, 0) !important; +} + +.purple-255-border-hover:hover { + border-color: rgb(255, 0, 255) !important; +} + +.yellow-255-border-hover:hover { + border-color: rgb(255, 255, 0) !important; +} + +.white-255-border-hover:hover { + border-color: rgb(255, 255, 255) !important; +} + +.orange-255-border-hover:hover { + border-color: rgb(255, 168, 0) !important; +} + +/* Misc */ +.black { + background-color: black !important; +} + +.black-text { + color: black !important; +} + +.black-border { + border-color: black !important; +} + +.black-hover:hover { + background-color: black !important; +} + +.black-text-hover:hover { + color: black !important; +} + +.black-border-hover:hover { + border-color: black !important; +} + +.white { + background-color: white !important; +} + +.white-text { + color: white !important; +} + +.white-border { + border-color: white !important; +} + +.white-hover:hover { + background-color: white !important; +} + +.white-text-hover:hover { + color: white !important; +} + +.white-border-hover:hover { + border-color: white !important; +} + +.left { + float: left !important; +} + +.right { + float: right !important; +} + +.center { + text-align: center; +} + +.left-align { + text-align: left; +} + +.right-align { + text-align: right; +} + +.center-align { + text-align: center; +} + +.full-width { + width: 100% !important; +} + +.full-height { + height: 100% !important; +} + +.inline { + display: inline !important; +} + +.inline-block { + display: inline-block !important; +} + +.block { + display: block !important; +} + +.valign-top { + vertical-align: top !important; +} + +.valign-middle { + vertical-align: middle !important; +} + +.valign-bottom { + vertical-align: bottom !important; +} + +.fixed { + position: fixed !important; +} + +.absolute { + position: absolute !important; +} + +.relative { + position: relative !important; +} + +.static { + position: static !important; +} + +.no-shadow { + box-shadow: none !important; +} + +.no-padding { + padding: 0px !important; +} + +.no-border { + border: none !important; +} + +.content { + padding: 12px; +} + +.disable-select { + user-select: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +.cursor-pointer { + cursor: pointer !important; +} + +.cursor-default { + cursor: default !important; +} + +.disabled { + cursor: not-allowed !important; +} + +/* Components */ +.tui-button { + display: inline-block; + outline: 0; + padding: 1px 10px; + background-color: rgb(0, 168, 0); + color: black; + border: none; + cursor: pointer; + text-align: center; + box-shadow: 10px 10px black; + border-radius: 0px; + user-select: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} +.tui-button.disabled { + text-decoration: line-through; +} + +.tui-button:active { + background-color: rgb(0, 168, 168) !important; + color: black !important; + box-shadow: none !important; +} + +.tui-button:focus { + color: rgb(0, 255, 255) !important; +} + +input[type=button] { + width: initial; +} + +/* Font (Options: 'Lucida Console' or 'DOS') */ +/* Characters */ +/* Theme */ +/* Responsive */ +/* Scrool */ +/* Grid */ +.tui-checkbox { + display: block; + position: relative; + cursor: pointer; + color: white; + padding-left: 30px; + user-select: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} +.tui-checkbox.disabled { + color: rgb(168, 168, 168); +} + +.tui-checkbox input { + position: absolute; + opacity: 0; + cursor: pointer; + top: 0px; + left: 0px; + pointer-events: none; +} + +.tui-checkbox span { + position: absolute; + width: 10px; + height: 10px; + cursor: pointer; + top: 0px; + left: 0px; +} + +.tui-checkbox input:checked ~ span::after { + content: "[√]"; + color: rgb(0, 255, 255); +} + +.tui-checkbox input:not(checked) ~ span::after { + content: "[ ]"; +} + +.tui-divider { + border-bottom: 2px solid rgb(255, 255, 255); + display: block; +} + +.tui-black-divider { + border-bottom: 2px solid rgb(0, 0, 0); + display: block; +} + +.tui-dropdown { + position: relative; + display: inline-block; + cursor: pointer; + user-select: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +.tui-dropdown-content { + display: none; + position: absolute; + background-color: rgb(168, 168, 168); + min-width: 200px; + padding: 6px; + z-index: 9; +} + +.tui-dropdown-content ul { + border: 2px black solid; +} + +.tui-dropdown-content ul li { + display: block !important; + margin: 6px; +} + +.tui-dropdown-content ul li a:hover { + background-color: rgb(0, 168, 0); +} + +.tui-dropdown:hover > .tui-dropdown-content:first-of-type { + display: block; +} + +.tui-fieldset { + border: 6px white double; + padding: 12px; + background-color: inherit; + margin-bottom: 6px; +} +.tui-fieldset.no-legend { + margin-top: 6px; +} + +.tui-input-fieldset { + border-top: 6px white double; + border-bottom: 6px white double; + border-left: 2px white solid; + border-right: 2px white solid; + padding: 5px; + background-color: inherit; +} + +.tui-input-fieldset legend { + color: white; +} + +.tui-input-fieldset:hover { + border-color: yellow; +} + +.tui-input-fieldset:hover legend { + color: yellow; +} + +.tui-fieldset-button { + position: absolute; + top: 0px; + right: 16px; + color: white; + background-color: inherit; + z-index: 2; + border: none; + cursor: pointer; + outline: 0; + padding: 2px; + user-select: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} +.tui-fieldset-button.left { + right: initial; + left: 16px !important; +} +.tui-fieldset-button.bottom { + bottom: 0px; + top: initial; +} + +.tui-fieldset-text { + position: absolute; + bottom: 0px; + left: 16px; + color: white; + background-color: inherit; + z-index: 2; + padding: 2px; +} +.tui-fieldset-text.right { + left: initial; + right: 16px; +} +.tui-fieldset-text.top { + top: 0px; + bottom: initial; +} + +.tui-fieldset-button::before { + content: "["; +} + +.tui-fieldset-button::after { + content: "]"; +} + +.tui-fieldset-button:active { + color: rgb(0, 255, 255) !important; +} + +.tui-input { + background-color: rgb(0, 0, 0); + color: white; + outline: 0; + border: none; + border-radius: 0px; +} +.tui-input.disabled { + background-color: rgb(168, 168, 168); + color: black; +} + +.tui-input:focus { + background-color: rgb(255, 255, 0) !important; + color: black !important; +} + +.tui-nav { + width: 100%; + background-color: rgb(168, 168, 168); + padding: 0px 2px; + z-index: 9; + display: block; + position: fixed; +} + +.tui-nav ul li { + display: inline-block; + margin-left: 10px; + padding: 1px 3px; +} + +.tui-nav ul li a { + display: block; + user-select: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +.tui-nav ul li:hover { + background-color: rgb(0, 168, 0); +} + +.tui-panel { + background-color: rgb(0, 0, 168); + display: inline-block; + color: white; + box-shadow: 10px 10px black; +} + +.tui-panel-content { + padding: 12px; +} + +.tui-panel-header { + padding-top: 2px; + display: block; + background: white; + text-align: center; +} + +.tui-progress-bar { + display: block; + position: relative; + height: 20px; + width: 200px; + background-color: rgb(0, 168, 168); + overflow: hidden; +} + +.tui-progress { + position: absolute; + left: 0px; + background-color: rgb(0, 255, 255); + height: 100%; + display: inline-block; +} + +.tui-progress-bar .tui-indeterminate { + position: absolute; + left: 0px; + background-color: rgb(0, 255, 255); + height: 20px; + width: 20px; + display: inline-block; + animation: indeterminate 1s backwards; + animation-iteration-count: infinite; + animation-timing-function: linear; +} + +.tui-progress-label { + position: absolute; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); + z-index: 1; +} + +@keyframes indeterminate { + from { + margin-left: -10%; + } + to { + margin-left: 100%; + } +} +/* Font (Options: 'Lucida Console' or 'DOS') */ +/* Characters */ +/* Theme */ +/* Responsive */ +/* Scrool */ +/* Grid */ +.tui-radio { + display: block; + position: relative; + cursor: pointer; + color: white; + padding-left: 30px; + user-select: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} +.tui-radio.disabled { + color: rgb(168, 168, 168); +} + +.tui-radio input { + position: absolute; + opacity: 0; + cursor: pointer; + top: 0px; + left: 0px; + pointer-events: none; +} + +.tui-radio span { + position: absolute; + width: 10px; + height: 10px; + cursor: pointer; + top: 0px; + left: 0px; +} + +.tui-radio input:checked ~ span:after { + content: "(•)"; + color: rgb(0, 255, 255) !important; +} + +.tui-radio input:not(checked) ~ span:after { + content: "( )"; +} + +/* Font (Options: 'Lucida Console' or 'DOS') */ +/* Characters */ +/* Theme */ +/* Responsive */ +/* Scrool */ +/* Grid */ +/* Default */ +::-webkit-scrollbar { + width: 10px; +} + +::-webkit-scrollbar-track { + background-image: url(images/scroll-cyan.png); + background-repeat: repeat; +} + +::-webkit-scrollbar-thumb { + background-color: rgb(0, 168, 168); +} + +::-webkit-scrollbar-thumb:hover { + background-color: rgb(0, 168, 168); +} + +/* Styles */ +.tui-scroll-blue ::-webkit-scrollbar-track { + background-image: url(images/scroll-blue.png); +} +.tui-scroll-blue ::-webkit-scrollbar-thumb { + background-color: rgb(0, 0, 168); +} +.tui-scroll-blue ::-webkit-scrollbar-thumb:hover { + background-color: rgb(0, 0, 168); +} + +.tui-scroll-green ::-webkit-scrollbar-track { + background-image: url(images/scroll-green.png); +} +.tui-scroll-green ::-webkit-scrollbar-thumb { + background-color: rgb(0, 168, 0); +} +.tui-scroll-green ::-webkit-scrollbar-thumb:hover { + background-color: rgb(0, 168, 0); +} + +.tui-scroll-cyan ::-webkit-scrollbar-track { + background-image: url(images/scroll-cyan.png); +} +.tui-scroll-cyan ::-webkit-scrollbar-thumb { + background-color: rgb(0, 168, 168); +} +.tui-scroll-cyan ::-webkit-scrollbar-thumb:hover { + background-color: rgb(0, 168, 168); +} + +.tui-scroll-red ::-webkit-scrollbar-track { + background-image: url(images/scroll-red.png); +} +.tui-scroll-red ::-webkit-scrollbar-thumb { + background-color: rgb(168, 0, 0); +} +.tui-scroll-red ::-webkit-scrollbar-thumb:hover { + background-color: rgb(168, 0, 0); +} + +.tui-scroll-purple ::-webkit-scrollbar-track { + background-image: url(images/scroll-purple.png); +} +.tui-scroll-purple ::-webkit-scrollbar-thumb { + background-color: rgb(168, 0, 168); +} +.tui-scroll-purple ::-webkit-scrollbar-thumb:hover { + background-color: rgb(168, 0, 168); +} + +.tui-scroll-yellow ::-webkit-scrollbar-track { + background-image: url(images/scroll-yellow.png); +} +.tui-scroll-yellow ::-webkit-scrollbar-thumb { + background-color: rgb(168, 168, 0); +} +.tui-scroll-yellow ::-webkit-scrollbar-thumb:hover { + background-color: rgb(168, 168, 0); +} + +.tui-scroll-white ::-webkit-scrollbar-track { + background-image: url(images/scroll-white.png); +} +.tui-scroll-white ::-webkit-scrollbar-thumb { + background-color: rgb(168, 168, 168); +} +.tui-scroll-white ::-webkit-scrollbar-thumb:hover { + background-color: rgb(168, 168, 168); +} + +.tui-sidenav { + position: fixed; + top: 0px; + left: 0px; + background-color: rgb(0, 168, 168); + min-width: 200px; + box-shadow: 10px 10px black !important; + padding: 6px; + z-index: 10; + height: 100%; + z-index: 8; + display: none; +} +.tui-sidenav.right { + left: initial; + right: 0px; +} +.tui-sidenav.active { + display: block !important; +} + +.tui-sidenav ul { + margin-top: 20px; + border: 2px black solid; +} + +.tui-sidenav ul li { + display: block; + margin: 6px; +} + +.tui-sidenav ul li a { + display: block; + user-select: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +.tui-sidenav ul li:hover { + background-color: rgb(255, 255, 0); +} + +.tui-sidenav-button { + cursor: pointer; + user-select: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +.tui-statusbar { + width: 100%; + background-color: rgb(168, 168, 168); + padding: 0px 1px; + left: 0px; + bottom: 0px; + z-index: 9; + position: fixed; +} + +.tui-statusbar ul li { + display: inline-block; + margin-left: 10px; + padding: 2px 3px; +} + +.tui-statusbar ul li:active { + background-color: rgb(0, 0, 168); + color: white; +} + +.tui-statusbar ul li a { + user-select: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +.tui-statusbar-divider { + border-right: 2px black solid; + display: inline; + margin: 0px 3px; +} + +.tui-table { + border: 2px solid rgb(168, 168, 168); + padding: 5px; + border-collapse: collapse; +} +.tui-table.hovered-blue tbody tr:hover { + background-color: rgb(0, 0, 255) !important; + color: black; +} +.tui-table.hovered-green tbody tr:hover { + background-color: rgb(0, 255, 0) !important; + color: black; +} +.tui-table.hovered-cyan tbody tr:hover { + background-color: rgb(0, 255, 255) !important; + color: black; +} +.tui-table.hovered-red tbody tr:hover { + background-color: rgb(255, 0, 0) !important; + color: white; +} +.tui-table.hovered-purple tbody tr:hover { + background-color: rgb(255, 0, 255) !important; + color: white; +} +.tui-table.hovered-yellow tbody tr:hover { + background-color: rgb(255, 255, 0) !important; + color: black; +} +.tui-table.hovered-white tbody tr:hover { + background-color: rgb(255, 255, 255) !important; + color: black; +} +.tui-table.hovered-orange tbody tr:hover { + background-color: rgb(255, 168, 0) !important; + color: black; +} +.tui-table.hovered tbody tr:hover { + background-color: rgb(0, 255, 255) !important; + color: black; +} +.tui-table.striped-blue tbody tr:nth-child(even) { + background-color: rgb(0, 0, 168); +} +.tui-table.striped-green tbody tr:nth-child(even) { + background-color: rgb(0, 168, 0); +} +.tui-table.striped-cyan tbody tr:nth-child(even) { + background-color: rgb(0, 168, 168); +} +.tui-table.striped-red tbody tr:nth-child(even) { + background-color: rgb(168, 0, 0); +} +.tui-table.striped-purple tbody tr:nth-child(even) { + background-color: rgb(168, 0, 168); +} +.tui-table.striped-yellow tbody tr:nth-child(even) { + background-color: rgb(168, 168, 0); +} +.tui-table.striped-white tbody tr:nth-child(even) { + background-color: rgb(168, 168, 168); + color: black; +} +.tui-table.striped-orange tbody tr:nth-child(even) { + background-color: rgb(168, 86, 0); +} + +.tui-table tbody { + background-color: inherit; + color: white; +} + +.tui-table tbody tr td { + border-right: 2px solid rgb(168, 168, 168); + padding: 0px 2px; +} + +.tui-table thead { + background-color: inherit; + color: rgb(255, 255, 0); + text-align: center; +} + +.tui-table tfoot { + background-color: inherit; + color: rgb(255, 255, 0); + text-align: center; +} + +.tui-table-grid { + border-collapse: collapse; + width: 100%; +} + +.tui-table-grid thead tr td, +.tui-table-grid tbody tr td, +.tui-table-grid thead tr th, +.tui-table-grid tbody tr th { + border: 2px solid black; + padding: 10px; + vertical-align: top; +} + +.tui-tabs { + background-color: rgb(0, 0, 168); + width: 100%; + padding: 0px 10px 0px 10px; +} + +.tui-tabs ul li { + display: inline-block; +} + +.tui-tabs ul li a { + display: block; + user-select: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +.tui-tab { + padding: 2px 10px 0px 10px; + color: rgb(168, 168, 168); + cursor: pointer; +} +.tui-tab.active { + background-color: rgb(168, 168, 168); + color: rgb(0, 0, 168); +} +.tui-tab.disabled { + text-decoration: line-through; +} + +.tui-tab-content { + display: none; +} + +.tui-textarea { + background-color: inherit; + border: none; + padding: 0px; + color: rgb(255, 255, 0); + outline: none; +} +.tui-textarea.disabled { + background-color: rgb(168, 168, 168); + color: black; +} + +.tui-window { + background-color: rgb(0, 0, 168); + padding: 1px; + display: inline-block; + position: relative; + box-shadow: 10px 10px black; + color: white; +} + +.tui-screen-640-480 { + width: 640px; + height: 480px; +} + +.tui-screen-800-600 { + width: 800px; + height: 600px; +} + +.tui-screen-1024-768 { + width: 1024px; + height: 768px; +} + +.tui-screen-640-480, +.tui-screen-800-600, +.tui-screen-1024-768 { + position: relative; + overflow: hidden; +} +.tui-screen-640-480.bordered, +.tui-screen-800-600.bordered, +.tui-screen-1024-768.bordered { + border: 2px solid black; +} +.tui-screen-640-480.centered, +.tui-screen-800-600.centered, +.tui-screen-1024-768.centered { + margin: auto; + margin-top: 20px; +} + +.tui-datetime { + padding: 1px 0px 1px 0px; + margin-right: 10px; + float: right; +} + +.tui-shortcut { + float: right; +} + +.tui-shadow, .tui-shadow-1 { + box-shadow: 10px 10px black !important; +} + +.tui-shadow-2 { + box-shadow: 15px 15px black; +} + +.tui-shadow-3 { + box-shadow: 20px 20px black; +} + +.tui-shadow-4 { + box-shadow: 25px 25px black; +} + +.tui-shadow-5 { + box-shadow: 30px 30px black; +} + +.tui-shadow-left, .tui-shadow-left-1 { + box-shadow: -10px 10px black !important; +} + +.tui-shadow-left-2 { + box-shadow: -15px 15px black !important; +} + +.tui-shadow-left-3 { + box-shadow: -20px 20px black !important; +} + +.tui-shadow-left-4 { + box-shadow: -25px 25px black !important; +} + +.tui-shadow-left-5 { + box-shadow: -30px 30px black !important; +} + +.tui-no-shadow { + box-shadow: none !important; +} + +.tui-bg-blue-white { + background-image: url("./images/bg-blue-white.png"); + background-repeat: repeat; +} + +.tui-bg-blue-black { + background-image: url("./images/bg-blue-black.png"); + background-repeat: repeat; +} + +.tui-bg-green-white { + background-image: url("./images/bg-green-white.png"); + background-repeat: repeat; +} + +.tui-bg-green-black { + background-image: url("./images/bg-green-black.png"); + background-repeat: repeat; +} + +.tui-bg-cyan-white { + background-image: url("./images/bg-cyan-white.png"); + background-repeat: repeat; +} + +.tui-bg-cyan-black { + background-image: url("./images/bg-cyan-black.png"); + background-repeat: repeat; +} + +.tui-bg-red-white { + background-image: url("./images/bg-red-white.png"); + background-repeat: repeat; +} + +.tui-bg-red-black { + background-image: url("./images/bg-red-black.png"); + background-repeat: repeat; +} + +.tui-bg-purple-white { + background-image: url("./images/bg-purple-white.png"); + background-repeat: repeat; +} + +.tui-bg-purple-black { + background-image: url("./images/bg-purple-black.png"); + background-repeat: repeat; +} + +.tui-bg-yellow-white { + background-image: url("./images/bg-yellow-white.png"); + background-repeat: repeat; +} + +.tui-bg-yellow-black { + background-image: url("./images/bg-yellow-black.png"); + background-repeat: repeat; +} + +.tui-bg-orange-white { + background-image: url("./images/bg-orange-white.png"); + background-repeat: repeat; +} + +.tui-bg-orange-black { + background-image: url("./images/bg-orange-black.png"); + background-repeat: repeat; +} + +.tui-border-solid { + border-style: solid !important; + border-width: 2px !important; +} + +.tui-border-dashed { + border-style: dashed !important; + border-width: 2px !important; +} + +.tui-border-dotted { + border-style: dotted !important; + border-width: 2px !important; +} + +.tui-border-double { + border-style: double !important; + border-width: 6px !important; +} + +/* Font (Options: 'Lucida Console' or 'DOS') */ +/* Characters */ +/* Theme */ +/* Responsive */ +/* Scrool */ +/* Grid */ +.container { + margin: 0 auto; + max-width: 1280px; + width: 90%; +} + +@media only screen and (min-width : 601px) { + .container { + width: 85%; + } +} +@media only screen and (min-width : 993px) { + .container { + width: 70%; + } +} +.col .row { + margin-left: -0.75rem; + margin-right: -0.75rem; +} + +.section { + padding-top: 1rem; + padding-bottom: 1rem; +} +.section.no-pad { + padding: 0; +} +.section.no-pad-bot { + padding-bottom: 0; +} +.section.no-pad-top { + padding-top: 0; +} + +.row { + margin-left: auto; + margin-right: auto; + margin-bottom: 20px; +} +.row:after { + content: ""; + display: table; + clear: both; +} +.row .col { + float: left; + box-sizing: border-box; + padding: 0 0.75rem; + min-height: 1px; +} +.row .col[class*=push-], .row .col[class*=pull-] { + position: relative; +} +.row .col.s1 { + width: 8.3333333333%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s2 { + width: 16.6666666667%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s3 { + width: 25%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s4 { + width: 33.3333333333%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s5 { + width: 41.6666666667%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s6 { + width: 50%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s7 { + width: 58.3333333333%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s8 { + width: 66.6666666667%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s9 { + width: 75%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s10 { + width: 83.3333333333%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s11 { + width: 91.6666666667%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.s12 { + width: 100%; + margin-left: auto; + left: auto; + right: auto; +} +.row .col.offset-s1 { + margin-left: 8.3333333333%; +} +.row .col.pull-s1 { + right: 8.3333333333%; +} +.row .col.push-s1 { + left: 8.3333333333%; +} +.row .col.offset-s2 { + margin-left: 16.6666666667%; +} +.row .col.pull-s2 { + right: 16.6666666667%; +} +.row .col.push-s2 { + left: 16.6666666667%; +} +.row .col.offset-s3 { + margin-left: 25%; +} +.row .col.pull-s3 { + right: 25%; +} +.row .col.push-s3 { + left: 25%; +} +.row .col.offset-s4 { + margin-left: 33.3333333333%; +} +.row .col.pull-s4 { + right: 33.3333333333%; +} +.row .col.push-s4 { + left: 33.3333333333%; +} +.row .col.offset-s5 { + margin-left: 41.6666666667%; +} +.row .col.pull-s5 { + right: 41.6666666667%; +} +.row .col.push-s5 { + left: 41.6666666667%; +} +.row .col.offset-s6 { + margin-left: 50%; +} +.row .col.pull-s6 { + right: 50%; +} +.row .col.push-s6 { + left: 50%; +} +.row .col.offset-s7 { + margin-left: 58.3333333333%; +} +.row .col.pull-s7 { + right: 58.3333333333%; +} +.row .col.push-s7 { + left: 58.3333333333%; +} +.row .col.offset-s8 { + margin-left: 66.6666666667%; +} +.row .col.pull-s8 { + right: 66.6666666667%; +} +.row .col.push-s8 { + left: 66.6666666667%; +} +.row .col.offset-s9 { + margin-left: 75%; +} +.row .col.pull-s9 { + right: 75%; +} +.row .col.push-s9 { + left: 75%; +} +.row .col.offset-s10 { + margin-left: 83.3333333333%; +} +.row .col.pull-s10 { + right: 83.3333333333%; +} +.row .col.push-s10 { + left: 83.3333333333%; +} +.row .col.offset-s11 { + margin-left: 91.6666666667%; +} +.row .col.pull-s11 { + right: 91.6666666667%; +} +.row .col.push-s11 { + left: 91.6666666667%; +} +.row .col.offset-s12 { + margin-left: 100%; +} +.row .col.pull-s12 { + right: 100%; +} +.row .col.push-s12 { + left: 100%; +} +@media only screen and (min-width : 601px) { + .row .col.m1 { + width: 8.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m2 { + width: 16.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m3 { + width: 25%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m4 { + width: 33.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m5 { + width: 41.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m6 { + width: 50%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m7 { + width: 58.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m8 { + width: 66.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m9 { + width: 75%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m10 { + width: 83.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m11 { + width: 91.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.m12 { + width: 100%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.offset-m1 { + margin-left: 8.3333333333%; + } + .row .col.pull-m1 { + right: 8.3333333333%; + } + .row .col.push-m1 { + left: 8.3333333333%; + } + .row .col.offset-m2 { + margin-left: 16.6666666667%; + } + .row .col.pull-m2 { + right: 16.6666666667%; + } + .row .col.push-m2 { + left: 16.6666666667%; + } + .row .col.offset-m3 { + margin-left: 25%; + } + .row .col.pull-m3 { + right: 25%; + } + .row .col.push-m3 { + left: 25%; + } + .row .col.offset-m4 { + margin-left: 33.3333333333%; + } + .row .col.pull-m4 { + right: 33.3333333333%; + } + .row .col.push-m4 { + left: 33.3333333333%; + } + .row .col.offset-m5 { + margin-left: 41.6666666667%; + } + .row .col.pull-m5 { + right: 41.6666666667%; + } + .row .col.push-m5 { + left: 41.6666666667%; + } + .row .col.offset-m6 { + margin-left: 50%; + } + .row .col.pull-m6 { + right: 50%; + } + .row .col.push-m6 { + left: 50%; + } + .row .col.offset-m7 { + margin-left: 58.3333333333%; + } + .row .col.pull-m7 { + right: 58.3333333333%; + } + .row .col.push-m7 { + left: 58.3333333333%; + } + .row .col.offset-m8 { + margin-left: 66.6666666667%; + } + .row .col.pull-m8 { + right: 66.6666666667%; + } + .row .col.push-m8 { + left: 66.6666666667%; + } + .row .col.offset-m9 { + margin-left: 75%; + } + .row .col.pull-m9 { + right: 75%; + } + .row .col.push-m9 { + left: 75%; + } + .row .col.offset-m10 { + margin-left: 83.3333333333%; + } + .row .col.pull-m10 { + right: 83.3333333333%; + } + .row .col.push-m10 { + left: 83.3333333333%; + } + .row .col.offset-m11 { + margin-left: 91.6666666667%; + } + .row .col.pull-m11 { + right: 91.6666666667%; + } + .row .col.push-m11 { + left: 91.6666666667%; + } + .row .col.offset-m12 { + margin-left: 100%; + } + .row .col.pull-m12 { + right: 100%; + } + .row .col.push-m12 { + left: 100%; + } +} +@media only screen and (min-width : 993px) { + .row .col.l1 { + width: 8.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l2 { + width: 16.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l3 { + width: 25%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l4 { + width: 33.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l5 { + width: 41.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l6 { + width: 50%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l7 { + width: 58.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l8 { + width: 66.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l9 { + width: 75%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l10 { + width: 83.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l11 { + width: 91.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.l12 { + width: 100%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.offset-l1 { + margin-left: 8.3333333333%; + } + .row .col.pull-l1 { + right: 8.3333333333%; + } + .row .col.push-l1 { + left: 8.3333333333%; + } + .row .col.offset-l2 { + margin-left: 16.6666666667%; + } + .row .col.pull-l2 { + right: 16.6666666667%; + } + .row .col.push-l2 { + left: 16.6666666667%; + } + .row .col.offset-l3 { + margin-left: 25%; + } + .row .col.pull-l3 { + right: 25%; + } + .row .col.push-l3 { + left: 25%; + } + .row .col.offset-l4 { + margin-left: 33.3333333333%; + } + .row .col.pull-l4 { + right: 33.3333333333%; + } + .row .col.push-l4 { + left: 33.3333333333%; + } + .row .col.offset-l5 { + margin-left: 41.6666666667%; + } + .row .col.pull-l5 { + right: 41.6666666667%; + } + .row .col.push-l5 { + left: 41.6666666667%; + } + .row .col.offset-l6 { + margin-left: 50%; + } + .row .col.pull-l6 { + right: 50%; + } + .row .col.push-l6 { + left: 50%; + } + .row .col.offset-l7 { + margin-left: 58.3333333333%; + } + .row .col.pull-l7 { + right: 58.3333333333%; + } + .row .col.push-l7 { + left: 58.3333333333%; + } + .row .col.offset-l8 { + margin-left: 66.6666666667%; + } + .row .col.pull-l8 { + right: 66.6666666667%; + } + .row .col.push-l8 { + left: 66.6666666667%; + } + .row .col.offset-l9 { + margin-left: 75%; + } + .row .col.pull-l9 { + right: 75%; + } + .row .col.push-l9 { + left: 75%; + } + .row .col.offset-l10 { + margin-left: 83.3333333333%; + } + .row .col.pull-l10 { + right: 83.3333333333%; + } + .row .col.push-l10 { + left: 83.3333333333%; + } + .row .col.offset-l11 { + margin-left: 91.6666666667%; + } + .row .col.pull-l11 { + right: 91.6666666667%; + } + .row .col.push-l11 { + left: 91.6666666667%; + } + .row .col.offset-l12 { + margin-left: 100%; + } + .row .col.pull-l12 { + right: 100%; + } + .row .col.push-l12 { + left: 100%; + } +} +@media only screen and (min-width : 1201px) { + .row .col.xl1 { + width: 8.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl2 { + width: 16.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl3 { + width: 25%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl4 { + width: 33.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl5 { + width: 41.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl6 { + width: 50%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl7 { + width: 58.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl8 { + width: 66.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl9 { + width: 75%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl10 { + width: 83.3333333333%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl11 { + width: 91.6666666667%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.xl12 { + width: 100%; + margin-left: auto; + left: auto; + right: auto; + } + .row .col.offset-xl1 { + margin-left: 8.3333333333%; + } + .row .col.pull-xl1 { + right: 8.3333333333%; + } + .row .col.push-xl1 { + left: 8.3333333333%; + } + .row .col.offset-xl2 { + margin-left: 16.6666666667%; + } + .row .col.pull-xl2 { + right: 16.6666666667%; + } + .row .col.push-xl2 { + left: 16.6666666667%; + } + .row .col.offset-xl3 { + margin-left: 25%; + } + .row .col.pull-xl3 { + right: 25%; + } + .row .col.push-xl3 { + left: 25%; + } + .row .col.offset-xl4 { + margin-left: 33.3333333333%; + } + .row .col.pull-xl4 { + right: 33.3333333333%; + } + .row .col.push-xl4 { + left: 33.3333333333%; + } + .row .col.offset-xl5 { + margin-left: 41.6666666667%; + } + .row .col.pull-xl5 { + right: 41.6666666667%; + } + .row .col.push-xl5 { + left: 41.6666666667%; + } + .row .col.offset-xl6 { + margin-left: 50%; + } + .row .col.pull-xl6 { + right: 50%; + } + .row .col.push-xl6 { + left: 50%; + } + .row .col.offset-xl7 { + margin-left: 58.3333333333%; + } + .row .col.pull-xl7 { + right: 58.3333333333%; + } + .row .col.push-xl7 { + left: 58.3333333333%; + } + .row .col.offset-xl8 { + margin-left: 66.6666666667%; + } + .row .col.pull-xl8 { + right: 66.6666666667%; + } + .row .col.push-xl8 { + left: 66.6666666667%; + } + .row .col.offset-xl9 { + margin-left: 75%; + } + .row .col.pull-xl9 { + right: 75%; + } + .row .col.push-xl9 { + left: 75%; + } + .row .col.offset-xl10 { + margin-left: 83.3333333333%; + } + .row .col.pull-xl10 { + right: 83.3333333333%; + } + .row .col.push-xl10 { + left: 83.3333333333%; + } + .row .col.offset-xl11 { + margin-left: 91.6666666667%; + } + .row .col.pull-xl11 { + right: 91.6666666667%; + } + .row .col.push-xl11 { + left: 91.6666666667%; + } + .row .col.offset-xl12 { + margin-left: 100%; + } + .row .col.pull-xl12 { + right: 100%; + } + .row .col.push-xl12 { + left: 100%; + } +} + +.tui-modal { + position: absolute; + left: 0px; + right: 0px; + top: 100px; + z-index: 101; + display: none; +} +.tui-modal.active { + display: block !important; +} + +.tui-overlap { + position: absolute; + top: 0px; + left: 0px; + right: 0px; + bottom: 0px; + z-index: 100; + display: none; +} +.tui-overlap.active { + display: block !important; +} + +/* Chart container */ +.tui-chart-vertical { + position: relative; + background-color: black; +} + +.tui-chart-horizontal { + position: relative; + background-color: black; +} + +/* Chart display */ +.tui-chart-vertical .tui-chart-display { + display: flex; + position: absolute; + top: 0px; + left: 50px; + right: 0px; + bottom: 30px; + align-items: flex-end; + border-bottom: 2px solid white; + border-left: 2px solid white; +} +.tui-chart-vertical .tui-chart-display.no-x-axis { + bottom: 0px; +} +.tui-chart-vertical .tui-chart-display.no-y-axis { + left: 0px; +} + +.tui-chart-horizontal .tui-chart-display { + display: flex; + position: absolute; + flex-direction: column; + top: 0px; + left: 50px; + right: 0px; + bottom: 30px; + align-items: stretch; + border-bottom: 2px solid white; + border-left: 2px solid white; +} +.tui-chart-horizontal .tui-chart-display.no-x-axis { + bottom: 0px; +} +.tui-chart-horizontal .tui-chart-display.no-y-axis { + left: 0px; +} + +/* Chart X axis */ +.tui-chart-x-axis { + display: flex; + position: absolute; + height: 30px; + left: 50px; + right: 0px; + bottom: 0px; + line-height: 30px; +} + +/* Chart Y axis */ +.tui-chart-y-axis { + display: flex; + flex-direction: column; + position: absolute; + top: 0px; + left: 0px; + bottom: 30px; + width: 50px; +} + +/* Chart legends */ +.tui-chart-vertical .tui-chart-x-axis .tui-chart-legend { + flex: 0 1 100%; + text-align: center; +} + +.tui-chart-vertical .tui-chart-y-axis .tui-chart-legend { + flex: 1; + text-align: right; + padding-right: 2px; + display: flex; + align-items: flex-start; + justify-content: flex-end; +} + +.tui-chart-horizontal .tui-chart-x-axis .tui-chart-legend { + flex: 0 1 100%; + text-align: right; +} + +.tui-chart-horizontal .tui-chart-y-axis .tui-chart-legend { + flex: 1; + text-align: right; + padding-right: 2px; + display: flex; + align-items: center; + justify-content: flex-end; +} + +/* Chart value */ +.tui-chart-vertical .tui-chart-display .tui-chart-value { + flex: 0 1 100%; + text-align: center; + overflow: hidden; +} + +.tui-chart-horizontal .tui-chart-display .tui-chart-value { + flex: 1; + text-align: right; + display: flex; + align-items: center; + align-content: flex-start; + justify-content: flex-end; + overflow: hidden; +} \ No newline at end of file diff --git a/web/static/lib/tuicss/tuicss.js b/web/static/lib/tuicss/tuicss.js new file mode 100644 index 0000000..21c3b6d --- /dev/null +++ b/web/static/lib/tuicss/tuicss.js @@ -0,0 +1,256 @@ +/** + * Replacement for jQuery's $(document).ready() function. + * This is handy in making sure stuff fires after the DOM is ready to be touched. + * Stolen from:https://stackoverflow.com/a/53601942/344028 + * + * @param fn Callback. + */ +function domReady(fn) { + // If we're early to the party + document.addEventListener('DOMContentLoaded', fn); + + // If late; I mean on time. + if (document.readyState === 'interactive' || document.readyState === 'complete') { + fn(); + } +} + +/** + * TuiTabs controller + */ +function tabsController() { + // Get all the tab elements (typically li tags). + const tabs = document.getElementsByClassName('tui-tab'); + + if (!tabs.length) { + // No tabs found, return early and save a couple CPU cycles. + return; + } + + for (const tab of tabs) { + // Add click listeners to them. + tab.addEventListener('click', function (e) { + + // Check if the clicked tab is disabled + if(e.target.classList.contains("disabled")) { + return; + } + + // Remove the 'active' class from any and all tabs. + for (const otherTab of tabs) { + otherTab.classList.remove('active'); + } + + // Get the content element. + const tabContents = document.getElementsByClassName('tui-tab-content'); + + if (tabContents) { + for (const tabContent of tabContents) { + // Hide all tab contents. + tabContent.style.display = 'none'; + } + } else { + throw 'No tab content elements found.' + } + + // Get the id of the tab contents we want to show. + const tabContentId = e.target.getAttribute('data-tab-content'); + + if (tabContentId) { + const tabContent = document.getElementById(tabContentId); + if (tabContent) { + // Show the tab contents. + tabContent.style.display = 'block'; + } else { + throw 'No tab content element with id "' + tabContentId + '" found.'; + } + } + // We are not going to throw an error here, since we could make the tab do something else that a tab + // normally wouldn't do. + + // Set the clicked tab to have the 'active' class so we can use it in the next part. + e.target.classList.add('active'); + + }); + } + + // Grab the first tab with the active class. + const activeTab = document.querySelector('.tui-tab.active'); + if (activeTab) { + // Now click it 'click' it. + activeTab.click(); + } else { + // Nothing found, just click the first tab foud. + tabs[0].click() + } +} + +/** + * Date/time field controller + */ +function datetimeController() { + // Get date/time elements. + const clocks = document.getElementsByClassName('tui-datetime'); + + if (!clocks.length) { + // No date time elements found, return early and save a couple CPU cycles. + return; + } + + // Kick off our clock interval stuff. + datetimeInterval(); + + // Synchronize time and set interval to control the clocks + setTimeout(() => { + setInterval(datetimeInterval, 1000); + }, 1000 - new Date().getMilliseconds()); + + function datetimeInterval() { + for (const clock of clocks) { + if (clock === null) { + continue; + } + + // Get the format we want to display in the element. + let format = clock.getAttribute('data-format'); + + // parse out the date and time into constants. + const today = new Date(); + const month = (today.getMonth() + 1).toString().padStart(2, '0'); + const day = today.getDate().toString().padStart(2, '0'); + const dayOfWeek = (today.getDay() + 1).toString().padStart(2, '0'); + const year = today.getFullYear().toString(); + const hour = today.getHours().toString().padStart(2, '0'); + const hour12 = (parseInt(hour) + 24) % '12' || '12'; + const minute = today.getMinutes().toString().padStart(2, '0'); + const second = today.getSeconds().toString().padStart(2, '0'); + const ampm = parseInt(hour) >= 12 ? 'PM' : 'AM'; + + // Replace based on the format. + format = format.replace('M', month); + format = format.replace('d', day); + format = format.replace('e', dayOfWeek); + format = format.replace('y', year); + format = format.replace('H', hour); + format = format.replace('h', hour12); + format = format.replace('m', minute); + format = format.replace('s', second); + format = format.replace('a', ampm); + + // Show it in the element. + clock.innerHTML = format; + } + } +} + +/** + * Sidenav Controller + * There should only side navigation element at the moment. + */ +function sidenavController() { + // Get the side navigation button (there should be only one, but if not, we are getting the first one). + const sideNavButton = document.querySelector('.tui-sidenav-button'); + + if (!sideNavButton) { + // No side navigation button found, return early and save a couple CPU cycles. + return; + } + + // Add the click event listener to the buttons. + sideNavButton.addEventListener('click', () => { + // Get the side navigation element (there should be only one, but if not, we are getting the first one). + const sideNav = document.querySelector('.tui-sidenav'); + + if (sideNav) { + if (sideNav.classList.contains('active')) { + sideNav.classList.remove('active'); + } else { + sideNav.classList.add('active'); + } + } else { + throw 'No sidenav element found.' + } + }); +} + +/** + * Modal controller + */ +function modalController() { + // Get the overlap (overlay) element (there should be only one, but if not, we are getting the first one). + const tuiOverlap = document.querySelector('.tui-overlap'); + + if (!tuiOverlap) { + // No overlap found element, return early and save a couple CPU cycles. + return; + } + + // Find modal buttons. + const modalButtons = document.getElementsByClassName('tui-modal-button'); + for (const modalButton of modalButtons) { + // Add the click event listener to the buttons. + modalButton.addEventListener('click', (e) => { + // Show the overlap. + tuiOverlap.classList.add('active'); + + // Get the display element for the modal. + const modalId = e.target.getAttribute('data-modal'); + + if (modalId) { + const modal = document.getElementById(modalId); + + if (modal) { + // Show it. + modal.classList.add('active'); + } else { + throw 'No modal element with id of "' + modalId + '" found.'; + } + } else { + throw 'Modal close button data-modal attribute is empty or not set.' + } + }); + } + + // Find the close modal buttons. + const modalCloseButtons = document.getElementsByClassName('tui-modal-close-button'); + + if (modalButtons.length > 0 && !modalCloseButtons.length) { + // A modal without a close button, is a bad modal. + throw 'No modal close buttons found.' + } + + for (const modalCloseButton of modalCloseButtons) { + // Add the click event listener to the buttons. + modalCloseButton.addEventListener('click', (e) => { + // Hide the the overlap. + tuiOverlap.classList.remove('active'); + + // Get the display element id for the modal. + const modalId = e.target.getAttribute('data-modal'); + + if (modalId) { + // Get the modal element. + const modal = document.getElementById(modalId); + + if (modal) { + // Hide it. + modal.classList.remove('active'); + } else { + throw 'No modal element with id of "' + modalId + '" found.'; + } + } else { + throw 'Modal close button data-modal attribute is empty or not set.' + } + }); + } +} + +/** + * Init: This is at the bottom to make sure it is fired correctly. + */ +domReady(function () { + tabsController(); + datetimeController(); + sidenavController(); + modalController(); +}); diff --git a/web/static/lib/tuicss/tuicss.min.css b/web/static/lib/tuicss/tuicss.min.css new file mode 100644 index 0000000..a99923f --- /dev/null +++ b/web/static/lib/tuicss/tuicss.min.css @@ -0,0 +1 @@ +@charset "UTF-8";@font-face{font-family:DOS;src:url("fonts/Perfect DOS VGA 437 Win.ttf")}html{font-family:"Lucida Console",monospace;font-size:18px;box-sizing:border-box}body{margin:0}*,:after,:before{font-family:inherit;font-size:inherit;box-sizing:inherit}ul{margin:0;padding:0;list-style-type:none}ul li{list-style-type:none}ul li a{display:block}a{color:inherit;text-decoration:none}span{margin:0}hr{border:none;border-bottom:2px solid #fff}input,select,textarea{width:200px}@media only screen and (max-width :600px){.hide-on-small-and-down,.hide-on-small-only{display:none!important}}@media only screen and (max-width :992px){.hide-on-med-and-down{display:none!important}}@media only screen and (min-width :601px){.hide-on-med-and-up{display:none!important}}@media only screen and (min-width:600px) and (max-width:992px){.hide-on-med-only{display:none!important}}@media only screen and (min-width :993px){.hide-on-large-only{display:none!important}}@media only screen and (min-width :1201px){.hide-on-extra-large-only{display:none!important}}@media only screen and (min-width :1201px){.show-on-extra-large{display:block!important}}@media only screen and (min-width :993px){.show-on-large{display:block!important}}@media only screen and (min-width:600px) and (max-width:992px){.show-on-medium{display:block!important}}@media only screen and (max-width :600px){.show-on-small{display:block!important}}@media only screen and (min-width :601px){.show-on-medium-and-up{display:block!important}}@media only screen and (max-width :992px){.show-on-medium-and-down{display:block!important}}.primary{background-color:#0000a8}.primary-text{color:#0000a8}.primary-border{border-color:#0000a8}.primary-hover:hover{background-color:#0000a8}.primary-text-hover:hover{color:#0000a8}.primary-border-hover:hover{border-color:#0000a8}.secondary{background-color:#a8a8a8}.secondary-text{color:#a8a8a8}.secondary-border{border-color:#a8a8a8}.secondary-hover:hover{background-color:#a8a8a8}.secondary-text-hover:hover{color:#a8a8a8}.secondary-border-hover:hover{border-color:#a8a8a8}.success{background-color:#00a800}.success-text{color:#00a800}.success-border{border-color:#00a800}.success-hover:hover{background-color:#00a800}.success-text-hover:hover{color:#00a800}.success-border-hover:hover{border-color:#00a800}.danger{background-color:#a80000}.danger-text{color:#a80000}.danger-border{border-color:#a80000}.danger-hover:hover{background-color:#a80000}.danger-text-hover:hover{color:#a80000}.danger-border-hover:hover{border-color:#a80000}.warning{background-color:#a8a800}.warning-text{color:#a8a800}.warning-border{border-color:#a8a800}.warning-hover:hover{background-color:#a8a800}.warning-text-hover:hover{color:#a8a800}.warning-border-hover:hover{border-color:#a8a800}.info{background-color:#00a8a8}.info-text{color:#00a8a8}.info-border{border-color:#00a8a8}.info-hover:hover{background-color:#00a8a8}.info-text-hover:hover{color:#00a8a8}.info-border-hover:hover{border-color:#00a8a8}.black-168{background-color:#000!important}.blue-168{background-color:#0000a8!important}.green-168{background-color:#00a800!important}.cyan-168{background-color:#00a8a8!important}.red-168{background-color:#a80000!important}.purple-168{background-color:#a800a8!important}.yellow-168{background-color:#a8a800!important}.white-168{background-color:#a8a8a8!important}.orange-168{background-color:#a85600!important}.black-168-text{color:#000!important}.blue-168-text{color:#0000a8!important}.green-168-text{color:#00a800!important}.cyan-168-text{color:#00a8a8!important}.red-168-text{color:#a80000!important}.purple-168-text{color:#a800a8!important}.yellow-168-text{color:#a8a800!important}.white-168-text{color:#a8a8a8!important}.orange-168-text{color:#a85600!important}.black-168-border{border-color:#000!important}.blue-168-border{border-color:#0000a8!important}.green-168-border{border-color:#00a800!important}.cyan-168-border{border-color:#00a8a8!important}.red-168-border{border-color:#a80000!important}.purple-168-border{border-color:#a800a8!important}.yellow-168-border{border-color:#a8a800!important}.white-168-border{border-color:#a8a8a8!important}.orange-168-border{border-color:#a85600!important}.black-168-hover:hover{background-color:#000!important}.blue-168-hover:hover{background-color:#0000a8!important}.green-168-hover:hover{background-color:#00a800!important}.cyan-168-hover:hover{background-color:#00a8a8!important}.red-168-hover:hover{background-color:#a80000!important}.purple-168-hover:hover{background-color:#a800a8!important}.yellow-168-hover:hover{background-color:#a8a800!important}.white-168-hover:hover{background-color:#a8a8a8!important}.orange-168-hover:hover{background-color:#a85600!important}.black-168-text-hover:hover{color:#000!important}.blue-168-text-hover:hover{color:#0000a8!important}.green-168-text-hover:hover{color:#00a800!important}.cyan-168-text-hover:hover{color:#00a8a8!important}.red-168-text-hover:hover{color:#a80000!important}.purple-168-text-hover:hover{color:#a800a8!important}.yellow-168-text-hover:hover{color:#a8a800!important}.white-168-text-hover:hover{color:#a8a8a8!important}.orange-168-text-hover:hover{color:#a85600!important}.black-168-border-hover:hover{border-color:#000!important}.blue-168-border-hover:hover{border-color:#0000a8!important}.green-168-border-hover:hover{border-color:#00a800!important}.cyan-168-border-hover:hover{border-color:#00a8a8!important}.red-168-border-hover:hover{border-color:#a80000!important}.purple-168-border-hover:hover{border-color:#a800a8!important}.yellow-168-border-hover:hover{border-color:#a8a800!important}.white-168-border-hover:hover{border-color:#a8a8a8!important}.orange-168-border-hover:hover{border-color:#a85600!important}.black-255{background-color:#000!important}.blue-255{background-color:#00f!important}.green-255{background-color:#0f0!important}.cyan-255{background-color:#0ff!important}.red-255{background-color:red!important}.purple-255{background-color:#f0f!important}.yellow-255{background-color:#ff0!important}.white-255{background-color:#fff!important}.orange-255{background-color:#ffa800!important}.black-255-text{color:#000!important}.blue-255-text{color:#00f!important}.green-255-text{color:#0f0!important}.cyan-255-text{color:#0ff!important}.red-255-text{color:red!important}.purple-255-text{color:#f0f!important}.yellow-255-text{color:#ff0!important}.white-255-text{color:#fff!important}.orange-255-text{color:#ffa800!important}.black-255-border{border-color:#000!important}.blue-255-border{border-color:#00f!important}.green-255-border{border-color:#0f0!important}.cyan-255-border{border-color:#0ff!important}.red-255-border{border-color:red!important}.purple-255-border{border-color:#f0f!important}.yellow-255-border{border-color:#ff0!important}.white-255-border{border-color:#fff!important}.orange-255-border{border-color:#ffa800!important}.black-255-hover:hover{background-color:#000!important}.blue-255-hover:hover{background-color:#00f!important}.green-255-hover:hover{background-color:#0f0!important}.cyan-255-hover:hover{background-color:#0ff!important}.red-255-hover:hover{background-color:red!important}.purple-255-hover:hover{background-color:#f0f!important}.yellow-255-hover:hover{background-color:#ff0!important}.white-255-hover:hover{background-color:#fff!important}.orange-255-hover:hover{background-color:#ffa800!important}.black-255-text-hover:hover{color:#000!important}.blue-255-text-hover:hover{color:#00f!important}.green-255-text-hover:hover{color:#0f0!important}.cyan-255-text-hover:hover{color:#0ff!important}.red-255-text-hover:hover{color:red!important}.purple-255-text-hover:hover{color:#f0f!important}.yellow-255-text-hover:hover{color:#ff0!important}.white-255-text-hover:hover{color:#fff!important}.orange-255-text-hover:hover{color:#ffa800!important}.black-255-border-hover:hover{border-color:#000!important}.blue-255-border-hover:hover{border-color:#00f!important}.green-255-border-hover:hover{border-color:#0f0!important}.cyan-255-border-hover:hover{border-color:#0ff!important}.red-255-border-hover:hover{border-color:red!important}.purple-255-border-hover:hover{border-color:#f0f!important}.yellow-255-border-hover:hover{border-color:#ff0!important}.white-255-border-hover:hover{border-color:#fff!important}.orange-255-border-hover:hover{border-color:#ffa800!important}.black{background-color:#000!important}.black-text{color:#000!important}.black-border{border-color:#000!important}.black-hover:hover{background-color:#000!important}.black-text-hover:hover{color:#000!important}.black-border-hover:hover{border-color:#000!important}.white{background-color:#fff!important}.white-text{color:#fff!important}.white-border{border-color:#fff!important}.white-hover:hover{background-color:#fff!important}.white-text-hover:hover{color:#fff!important}.white-border-hover:hover{border-color:#fff!important}.left{float:left!important}.right{float:right!important}.center{text-align:center}.left-align{text-align:left}.right-align{text-align:right}.center-align{text-align:center}.full-width{width:100%!important}.full-height{height:100%!important}.inline{display:inline!important}.inline-block{display:inline-block!important}.block{display:block!important}.valign-top{vertical-align:top!important}.valign-middle{vertical-align:middle!important}.valign-bottom{vertical-align:bottom!important}.fixed{position:fixed!important}.absolute{position:absolute!important}.relative{position:relative!important}.static{position:static!important}.no-shadow{box-shadow:none!important}.no-padding{padding:0!important}.no-border{border:none!important}.content{padding:12px}.disable-select{user-select:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none}.cursor-pointer{cursor:pointer!important}.cursor-default{cursor:default!important}.disabled{cursor:not-allowed!important}.tui-button{display:inline-block;outline:0;padding:1px 10px;background-color:#00a800;color:#000;border:none;cursor:pointer;text-align:center;box-shadow:10px 10px #000;border-radius:0;user-select:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none}.tui-button.disabled{text-decoration:line-through}.tui-button:active{background-color:#00a8a8!important;color:#000!important;box-shadow:none!important}.tui-button:focus{color:#0ff!important}input[type=button]{width:initial}.tui-checkbox{display:block;position:relative;cursor:pointer;color:#fff;padding-left:30px;user-select:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none}.tui-checkbox.disabled{color:#a8a8a8}.tui-checkbox input{position:absolute;opacity:0;cursor:pointer;top:0;left:0;pointer-events:none}.tui-checkbox span{position:absolute;width:10px;height:10px;cursor:pointer;top:0;left:0}.tui-checkbox input:checked~span::after{content:"[√]";color:#0ff}.tui-checkbox input:not(checked)~span::after{content:"[ ]"}.tui-divider{border-bottom:2px solid #fff;display:block}.tui-black-divider{border-bottom:2px solid #000;display:block}.tui-dropdown{position:relative;display:inline-block;cursor:pointer;user-select:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none}.tui-dropdown-content{display:none;position:absolute;background-color:#a8a8a8;min-width:200px;padding:6px;z-index:9}.tui-dropdown-content ul{border:2px #000 solid}.tui-dropdown-content ul li{display:block!important;margin:6px}.tui-dropdown-content ul li a:hover{background-color:#00a800}.tui-dropdown:hover>.tui-dropdown-content:first-of-type{display:block}.tui-fieldset{border:6px #fff double;padding:12px;background-color:inherit;margin-bottom:6px}.tui-fieldset.no-legend{margin-top:6px}.tui-input-fieldset{border-top:6px #fff double;border-bottom:6px #fff double;border-left:2px #fff solid;border-right:2px #fff solid;padding:5px;background-color:inherit}.tui-input-fieldset legend{color:#fff}.tui-input-fieldset:hover{border-color:#ff0}.tui-input-fieldset:hover legend{color:#ff0}.tui-fieldset-button{position:absolute;top:0;right:16px;color:#fff;background-color:inherit;z-index:2;border:none;cursor:pointer;outline:0;padding:2px;user-select:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none}.tui-fieldset-button.left{right:initial;left:16px!important}.tui-fieldset-button.bottom{bottom:0;top:initial}.tui-fieldset-text{position:absolute;bottom:0;left:16px;color:#fff;background-color:inherit;z-index:2;padding:2px}.tui-fieldset-text.right{left:initial;right:16px}.tui-fieldset-text.top{top:0;bottom:initial}.tui-fieldset-button::before{content:"["}.tui-fieldset-button::after{content:"]"}.tui-fieldset-button:active{color:#0ff!important}.tui-input{background-color:#000;color:#fff;outline:0;border:none;border-radius:0}.tui-input.disabled{background-color:#a8a8a8;color:#000}.tui-input:focus{background-color:#ff0!important;color:#000!important}.tui-nav{width:100%;background-color:#a8a8a8;padding:0 2px;z-index:9;display:block;position:fixed}.tui-nav ul li{display:inline-block;margin-left:10px;padding:1px 3px}.tui-nav ul li a{display:block;user-select:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none}.tui-nav ul li:hover{background-color:#00a800}.tui-panel{background-color:#0000a8;display:inline-block;color:#fff;box-shadow:10px 10px #000}.tui-panel-content{padding:12px}.tui-panel-header{padding-top:2px;display:block;background:#fff;text-align:center}.tui-progress-bar{display:block;position:relative;height:20px;width:200px;background-color:#00a8a8;overflow:hidden}.tui-progress{position:absolute;left:0;background-color:#0ff;height:100%;display:inline-block}.tui-progress-bar .tui-indeterminate{position:absolute;left:0;background-color:#0ff;height:20px;width:20px;display:inline-block;animation:indeterminate 1s backwards;animation-iteration-count:infinite;animation-timing-function:linear}.tui-progress-label{position:absolute;top:50%;left:50%;transform:translateX(-50%) translateY(-50%);z-index:1}@keyframes indeterminate{from{margin-left:-10%}to{margin-left:100%}}.tui-radio{display:block;position:relative;cursor:pointer;color:#fff;padding-left:30px;user-select:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none}.tui-radio.disabled{color:#a8a8a8}.tui-radio input{position:absolute;opacity:0;cursor:pointer;top:0;left:0;pointer-events:none}.tui-radio span{position:absolute;width:10px;height:10px;cursor:pointer;top:0;left:0}.tui-radio input:checked~span:after{content:"(•)";color:#0ff!important}.tui-radio input:not(checked)~span:after{content:"( )"}::-webkit-scrollbar{width:10px}::-webkit-scrollbar-track{background-image:url(images/scroll-cyan.png);background-repeat:repeat}::-webkit-scrollbar-thumb{background-color:#00a8a8}::-webkit-scrollbar-thumb:hover{background-color:#00a8a8}.tui-scroll-blue ::-webkit-scrollbar-track{background-image:url(images/scroll-blue.png)}.tui-scroll-blue ::-webkit-scrollbar-thumb{background-color:#0000a8}.tui-scroll-blue ::-webkit-scrollbar-thumb:hover{background-color:#0000a8}.tui-scroll-green ::-webkit-scrollbar-track{background-image:url(images/scroll-green.png)}.tui-scroll-green ::-webkit-scrollbar-thumb{background-color:#00a800}.tui-scroll-green ::-webkit-scrollbar-thumb:hover{background-color:#00a800}.tui-scroll-cyan ::-webkit-scrollbar-track{background-image:url(images/scroll-cyan.png)}.tui-scroll-cyan ::-webkit-scrollbar-thumb{background-color:#00a8a8}.tui-scroll-cyan ::-webkit-scrollbar-thumb:hover{background-color:#00a8a8}.tui-scroll-red ::-webkit-scrollbar-track{background-image:url(images/scroll-red.png)}.tui-scroll-red ::-webkit-scrollbar-thumb{background-color:#a80000}.tui-scroll-red ::-webkit-scrollbar-thumb:hover{background-color:#a80000}.tui-scroll-purple ::-webkit-scrollbar-track{background-image:url(images/scroll-purple.png)}.tui-scroll-purple ::-webkit-scrollbar-thumb{background-color:#a800a8}.tui-scroll-purple ::-webkit-scrollbar-thumb:hover{background-color:#a800a8}.tui-scroll-yellow ::-webkit-scrollbar-track{background-image:url(images/scroll-yellow.png)}.tui-scroll-yellow ::-webkit-scrollbar-thumb{background-color:#a8a800}.tui-scroll-yellow ::-webkit-scrollbar-thumb:hover{background-color:#a8a800}.tui-scroll-white ::-webkit-scrollbar-track{background-image:url(images/scroll-white.png)}.tui-scroll-white ::-webkit-scrollbar-thumb{background-color:#a8a8a8}.tui-scroll-white ::-webkit-scrollbar-thumb:hover{background-color:#a8a8a8}.tui-sidenav{position:fixed;top:0;left:0;background-color:#00a8a8;min-width:200px;box-shadow:10px 10px #000!important;padding:6px;z-index:10;height:100%;z-index:8;display:none}.tui-sidenav.right{left:initial;right:0}.tui-sidenav.active{display:block!important}.tui-sidenav ul{margin-top:20px;border:2px #000 solid}.tui-sidenav ul li{display:block;margin:6px}.tui-sidenav ul li a{display:block;user-select:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none}.tui-sidenav ul li:hover{background-color:#ff0}.tui-sidenav-button{cursor:pointer;user-select:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none}.tui-statusbar{width:100%;background-color:#a8a8a8;padding:0 1px;left:0;bottom:0;z-index:9;position:fixed}.tui-statusbar ul li{display:inline-block;margin-left:10px;padding:2px 3px}.tui-statusbar ul li:active{background-color:#0000a8;color:#fff}.tui-statusbar ul li a{user-select:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none}.tui-statusbar-divider{border-right:2px #000 solid;display:inline;margin:0 3px}.tui-table{border:2px solid #a8a8a8;padding:5px;border-collapse:collapse}.tui-table.hovered-blue tbody tr:hover{background-color:#00f!important;color:#000}.tui-table.hovered-green tbody tr:hover{background-color:#0f0!important;color:#000}.tui-table.hovered-cyan tbody tr:hover{background-color:#0ff!important;color:#000}.tui-table.hovered-red tbody tr:hover{background-color:red!important;color:#fff}.tui-table.hovered-purple tbody tr:hover{background-color:#f0f!important;color:#fff}.tui-table.hovered-yellow tbody tr:hover{background-color:#ff0!important;color:#000}.tui-table.hovered-white tbody tr:hover{background-color:#fff!important;color:#000}.tui-table.hovered-orange tbody tr:hover{background-color:#ffa800!important;color:#000}.tui-table.hovered tbody tr:hover{background-color:#0ff!important;color:#000}.tui-table.striped-blue tbody tr:nth-child(even){background-color:#0000a8}.tui-table.striped-green tbody tr:nth-child(even){background-color:#00a800}.tui-table.striped-cyan tbody tr:nth-child(even){background-color:#00a8a8}.tui-table.striped-red tbody tr:nth-child(even){background-color:#a80000}.tui-table.striped-purple tbody tr:nth-child(even){background-color:#a800a8}.tui-table.striped-yellow tbody tr:nth-child(even){background-color:#a8a800}.tui-table.striped-white tbody tr:nth-child(even){background-color:#a8a8a8;color:#000}.tui-table.striped-orange tbody tr:nth-child(even){background-color:#a85600}.tui-table tbody{background-color:inherit;color:#fff}.tui-table tbody tr td{border-right:2px solid #a8a8a8;padding:0 2px}.tui-table thead{background-color:inherit;color:#ff0;text-align:center}.tui-table tfoot{background-color:inherit;color:#ff0;text-align:center}.tui-table-grid{border-collapse:collapse;width:100%}.tui-table-grid tbody tr td,.tui-table-grid tbody tr th,.tui-table-grid thead tr td,.tui-table-grid thead tr th{border:2px solid #000;padding:10px;vertical-align:top}.tui-tabs{background-color:#0000a8;width:100%;padding:0 10px 0 10px}.tui-tabs ul li{display:inline-block}.tui-tabs ul li a{display:block;user-select:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none}.tui-tab{padding:2px 10px 0 10px;color:#a8a8a8;cursor:pointer}.tui-tab.active{background-color:#a8a8a8;color:#0000a8}.tui-tab.disabled{text-decoration:line-through}.tui-tab-content{display:none}.tui-textarea{background-color:inherit;border:none;padding:0;color:#ff0;outline:0}.tui-textarea.disabled{background-color:#a8a8a8;color:#000}.tui-window{background-color:#0000a8;padding:1px;display:inline-block;position:relative;box-shadow:10px 10px #000;color:#fff}.tui-screen-640-480{width:640px;height:480px}.tui-screen-800-600{width:800px;height:600px}.tui-screen-1024-768{width:1024px;height:768px}.tui-screen-1024-768,.tui-screen-640-480,.tui-screen-800-600{position:relative;overflow:hidden}.tui-screen-1024-768.bordered,.tui-screen-640-480.bordered,.tui-screen-800-600.bordered{border:2px solid #000}.tui-screen-1024-768.centered,.tui-screen-640-480.centered,.tui-screen-800-600.centered{margin:auto;margin-top:20px}.tui-datetime{padding:1px 0 1px 0;margin-right:10px;float:right}.tui-shortcut{float:right}.tui-shadow,.tui-shadow-1{box-shadow:10px 10px #000!important}.tui-shadow-2{box-shadow:15px 15px #000}.tui-shadow-3{box-shadow:20px 20px #000}.tui-shadow-4{box-shadow:25px 25px #000}.tui-shadow-5{box-shadow:30px 30px #000}.tui-shadow-left,.tui-shadow-left-1{box-shadow:-10px 10px #000!important}.tui-shadow-left-2{box-shadow:-15px 15px #000!important}.tui-shadow-left-3{box-shadow:-20px 20px #000!important}.tui-shadow-left-4{box-shadow:-25px 25px #000!important}.tui-shadow-left-5{box-shadow:-30px 30px #000!important}.tui-no-shadow{box-shadow:none!important}.tui-bg-blue-white{background-image:url(images/bg-blue-white.png);background-repeat:repeat}.tui-bg-blue-black{background-image:url(images/bg-blue-black.png);background-repeat:repeat}.tui-bg-green-white{background-image:url(images/bg-green-white.png);background-repeat:repeat}.tui-bg-green-black{background-image:url(images/bg-green-black.png);background-repeat:repeat}.tui-bg-cyan-white{background-image:url(images/bg-cyan-white.png);background-repeat:repeat}.tui-bg-cyan-black{background-image:url(images/bg-cyan-black.png);background-repeat:repeat}.tui-bg-red-white{background-image:url(images/bg-red-white.png);background-repeat:repeat}.tui-bg-red-black{background-image:url(images/bg-red-black.png);background-repeat:repeat}.tui-bg-purple-white{background-image:url(images/bg-purple-white.png);background-repeat:repeat}.tui-bg-purple-black{background-image:url(images/bg-purple-black.png);background-repeat:repeat}.tui-bg-yellow-white{background-image:url(images/bg-yellow-white.png);background-repeat:repeat}.tui-bg-yellow-black{background-image:url(images/bg-yellow-black.png);background-repeat:repeat}.tui-bg-orange-white{background-image:url(images/bg-orange-white.png);background-repeat:repeat}.tui-bg-orange-black{background-image:url(images/bg-orange-black.png);background-repeat:repeat}.tui-border-solid{border-style:solid!important;border-width:2px!important}.tui-border-dashed{border-style:dashed!important;border-width:2px!important}.tui-border-dotted{border-style:dotted!important;border-width:2px!important}.tui-border-double{border-style:double!important;border-width:6px!important}.container{margin:0 auto;max-width:1280px;width:90%}@media only screen and (min-width :601px){.container{width:85%}}@media only screen and (min-width :993px){.container{width:70%}}.col .row{margin-left:-.75rem;margin-right:-.75rem}.section{padding-top:1rem;padding-bottom:1rem}.section.no-pad{padding:0}.section.no-pad-bot{padding-bottom:0}.section.no-pad-top{padding-top:0}.row{margin-left:auto;margin-right:auto;margin-bottom:20px}.row:after{content:"";display:table;clear:both}.row .col{float:left;box-sizing:border-box;padding:0 .75rem;min-height:1px}.row .col[class*=pull-],.row .col[class*=push-]{position:relative}.row .col.s1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.s4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.s7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.s10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.s11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.s12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-s1{margin-left:8.3333333333%}.row .col.pull-s1{right:8.3333333333%}.row .col.push-s1{left:8.3333333333%}.row .col.offset-s2{margin-left:16.6666666667%}.row .col.pull-s2{right:16.6666666667%}.row .col.push-s2{left:16.6666666667%}.row .col.offset-s3{margin-left:25%}.row .col.pull-s3{right:25%}.row .col.push-s3{left:25%}.row .col.offset-s4{margin-left:33.3333333333%}.row .col.pull-s4{right:33.3333333333%}.row .col.push-s4{left:33.3333333333%}.row .col.offset-s5{margin-left:41.6666666667%}.row .col.pull-s5{right:41.6666666667%}.row .col.push-s5{left:41.6666666667%}.row .col.offset-s6{margin-left:50%}.row .col.pull-s6{right:50%}.row .col.push-s6{left:50%}.row .col.offset-s7{margin-left:58.3333333333%}.row .col.pull-s7{right:58.3333333333%}.row .col.push-s7{left:58.3333333333%}.row .col.offset-s8{margin-left:66.6666666667%}.row .col.pull-s8{right:66.6666666667%}.row .col.push-s8{left:66.6666666667%}.row .col.offset-s9{margin-left:75%}.row .col.pull-s9{right:75%}.row .col.push-s9{left:75%}.row .col.offset-s10{margin-left:83.3333333333%}.row .col.pull-s10{right:83.3333333333%}.row .col.push-s10{left:83.3333333333%}.row .col.offset-s11{margin-left:91.6666666667%}.row .col.pull-s11{right:91.6666666667%}.row .col.push-s11{left:91.6666666667%}.row .col.offset-s12{margin-left:100%}.row .col.pull-s12{right:100%}.row .col.push-s12{left:100%}@media only screen and (min-width :601px){.row .col.m1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.m4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.m7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.m10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.m11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.m12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-m1{margin-left:8.3333333333%}.row .col.pull-m1{right:8.3333333333%}.row .col.push-m1{left:8.3333333333%}.row .col.offset-m2{margin-left:16.6666666667%}.row .col.pull-m2{right:16.6666666667%}.row .col.push-m2{left:16.6666666667%}.row .col.offset-m3{margin-left:25%}.row .col.pull-m3{right:25%}.row .col.push-m3{left:25%}.row .col.offset-m4{margin-left:33.3333333333%}.row .col.pull-m4{right:33.3333333333%}.row .col.push-m4{left:33.3333333333%}.row .col.offset-m5{margin-left:41.6666666667%}.row .col.pull-m5{right:41.6666666667%}.row .col.push-m5{left:41.6666666667%}.row .col.offset-m6{margin-left:50%}.row .col.pull-m6{right:50%}.row .col.push-m6{left:50%}.row .col.offset-m7{margin-left:58.3333333333%}.row .col.pull-m7{right:58.3333333333%}.row .col.push-m7{left:58.3333333333%}.row .col.offset-m8{margin-left:66.6666666667%}.row .col.pull-m8{right:66.6666666667%}.row .col.push-m8{left:66.6666666667%}.row .col.offset-m9{margin-left:75%}.row .col.pull-m9{right:75%}.row .col.push-m9{left:75%}.row .col.offset-m10{margin-left:83.3333333333%}.row .col.pull-m10{right:83.3333333333%}.row .col.push-m10{left:83.3333333333%}.row .col.offset-m11{margin-left:91.6666666667%}.row .col.pull-m11{right:91.6666666667%}.row .col.push-m11{left:91.6666666667%}.row .col.offset-m12{margin-left:100%}.row .col.pull-m12{right:100%}.row .col.push-m12{left:100%}}@media only screen and (min-width :993px){.row .col.l1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.l4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.l7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.l10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.l11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.l12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-l1{margin-left:8.3333333333%}.row .col.pull-l1{right:8.3333333333%}.row .col.push-l1{left:8.3333333333%}.row .col.offset-l2{margin-left:16.6666666667%}.row .col.pull-l2{right:16.6666666667%}.row .col.push-l2{left:16.6666666667%}.row .col.offset-l3{margin-left:25%}.row .col.pull-l3{right:25%}.row .col.push-l3{left:25%}.row .col.offset-l4{margin-left:33.3333333333%}.row .col.pull-l4{right:33.3333333333%}.row .col.push-l4{left:33.3333333333%}.row .col.offset-l5{margin-left:41.6666666667%}.row .col.pull-l5{right:41.6666666667%}.row .col.push-l5{left:41.6666666667%}.row .col.offset-l6{margin-left:50%}.row .col.pull-l6{right:50%}.row .col.push-l6{left:50%}.row .col.offset-l7{margin-left:58.3333333333%}.row .col.pull-l7{right:58.3333333333%}.row .col.push-l7{left:58.3333333333%}.row .col.offset-l8{margin-left:66.6666666667%}.row .col.pull-l8{right:66.6666666667%}.row .col.push-l8{left:66.6666666667%}.row .col.offset-l9{margin-left:75%}.row .col.pull-l9{right:75%}.row .col.push-l9{left:75%}.row .col.offset-l10{margin-left:83.3333333333%}.row .col.pull-l10{right:83.3333333333%}.row .col.push-l10{left:83.3333333333%}.row .col.offset-l11{margin-left:91.6666666667%}.row .col.pull-l11{right:91.6666666667%}.row .col.push-l11{left:91.6666666667%}.row .col.offset-l12{margin-left:100%}.row .col.pull-l12{right:100%}.row .col.push-l12{left:100%}}@media only screen and (min-width :1201px){.row .col.xl1{width:8.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl2{width:16.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl3{width:25%;margin-left:auto;left:auto;right:auto}.row .col.xl4{width:33.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl5{width:41.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl6{width:50%;margin-left:auto;left:auto;right:auto}.row .col.xl7{width:58.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl8{width:66.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl9{width:75%;margin-left:auto;left:auto;right:auto}.row .col.xl10{width:83.3333333333%;margin-left:auto;left:auto;right:auto}.row .col.xl11{width:91.6666666667%;margin-left:auto;left:auto;right:auto}.row .col.xl12{width:100%;margin-left:auto;left:auto;right:auto}.row .col.offset-xl1{margin-left:8.3333333333%}.row .col.pull-xl1{right:8.3333333333%}.row .col.push-xl1{left:8.3333333333%}.row .col.offset-xl2{margin-left:16.6666666667%}.row .col.pull-xl2{right:16.6666666667%}.row .col.push-xl2{left:16.6666666667%}.row .col.offset-xl3{margin-left:25%}.row .col.pull-xl3{right:25%}.row .col.push-xl3{left:25%}.row .col.offset-xl4{margin-left:33.3333333333%}.row .col.pull-xl4{right:33.3333333333%}.row .col.push-xl4{left:33.3333333333%}.row .col.offset-xl5{margin-left:41.6666666667%}.row .col.pull-xl5{right:41.6666666667%}.row .col.push-xl5{left:41.6666666667%}.row .col.offset-xl6{margin-left:50%}.row .col.pull-xl6{right:50%}.row .col.push-xl6{left:50%}.row .col.offset-xl7{margin-left:58.3333333333%}.row .col.pull-xl7{right:58.3333333333%}.row .col.push-xl7{left:58.3333333333%}.row .col.offset-xl8{margin-left:66.6666666667%}.row .col.pull-xl8{right:66.6666666667%}.row .col.push-xl8{left:66.6666666667%}.row .col.offset-xl9{margin-left:75%}.row .col.pull-xl9{right:75%}.row .col.push-xl9{left:75%}.row .col.offset-xl10{margin-left:83.3333333333%}.row .col.pull-xl10{right:83.3333333333%}.row .col.push-xl10{left:83.3333333333%}.row .col.offset-xl11{margin-left:91.6666666667%}.row .col.pull-xl11{right:91.6666666667%}.row .col.push-xl11{left:91.6666666667%}.row .col.offset-xl12{margin-left:100%}.row .col.pull-xl12{right:100%}.row .col.push-xl12{left:100%}}.tui-modal{position:absolute;left:0;right:0;top:100px;z-index:101;display:none}.tui-modal.active{display:block!important}.tui-overlap{position:absolute;top:0;left:0;right:0;bottom:0;z-index:100;display:none}.tui-overlap.active{display:block!important}.tui-chart-vertical{position:relative;background-color:#000}.tui-chart-horizontal{position:relative;background-color:#000}.tui-chart-vertical .tui-chart-display{display:flex;position:absolute;top:0;left:50px;right:0;bottom:30px;align-items:flex-end;border-bottom:2px solid #fff;border-left:2px solid #fff}.tui-chart-vertical .tui-chart-display.no-x-axis{bottom:0}.tui-chart-vertical .tui-chart-display.no-y-axis{left:0}.tui-chart-horizontal .tui-chart-display{display:flex;position:absolute;flex-direction:column;top:0;left:50px;right:0;bottom:30px;align-items:stretch;border-bottom:2px solid #fff;border-left:2px solid #fff}.tui-chart-horizontal .tui-chart-display.no-x-axis{bottom:0}.tui-chart-horizontal .tui-chart-display.no-y-axis{left:0}.tui-chart-x-axis{display:flex;position:absolute;height:30px;left:50px;right:0;bottom:0;line-height:30px}.tui-chart-y-axis{display:flex;flex-direction:column;position:absolute;top:0;left:0;bottom:30px;width:50px}.tui-chart-vertical .tui-chart-x-axis .tui-chart-legend{flex:0 1 100%;text-align:center}.tui-chart-vertical .tui-chart-y-axis .tui-chart-legend{flex:1;text-align:right;padding-right:2px;display:flex;align-items:flex-start;justify-content:flex-end}.tui-chart-horizontal .tui-chart-x-axis .tui-chart-legend{flex:0 1 100%;text-align:right}.tui-chart-horizontal .tui-chart-y-axis .tui-chart-legend{flex:1;text-align:right;padding-right:2px;display:flex;align-items:center;justify-content:flex-end}.tui-chart-vertical .tui-chart-display .tui-chart-value{flex:0 1 100%;text-align:center;overflow:hidden}.tui-chart-horizontal .tui-chart-display .tui-chart-value{flex:1;text-align:right;display:flex;align-items:center;align-content:flex-start;justify-content:flex-end;overflow:hidden} \ No newline at end of file diff --git a/web/static/lib/tuicss/tuicss.min.js b/web/static/lib/tuicss/tuicss.min.js new file mode 100644 index 0000000..84c6325 --- /dev/null +++ b/web/static/lib/tuicss/tuicss.min.js @@ -0,0 +1 @@ +function domReady(t){document.addEventListener("DOMContentLoaded",t),"interactive"!==document.readyState&&"complete"!==document.readyState||t()}function tabsController(){const t=document.getElementsByClassName("tui-tab");if(!t.length)return;for(const e of t)e.addEventListener("click",function(e){if(e.target.classList.contains("disabled"))return;for(const e of t)e.classList.remove("active");const o=document.getElementsByClassName("tui-tab-content");if(!o)throw"No tab content elements found.";for(const t of o)t.style.display="none";const n=e.target.getAttribute("data-tab-content");if(n){const t=document.getElementById(n);if(!t)throw'No tab content element with id "'+n+'" found.';t.style.display="block"}e.target.classList.add("active")});const e=document.querySelector(".tui-tab.active");e?e.click():t[0].click()}function datetimeController(){const t=document.getElementsByClassName("tui-datetime");function e(){for(const e of t){if(null===e)continue;let t=e.getAttribute("data-format");const o=new Date,n=(o.getMonth()+1).toString().padStart(2,"0"),a=o.getDate().toString().padStart(2,"0"),c=(o.getDay()+1).toString().padStart(2,"0"),s=o.getFullYear().toString(),i=o.getHours().toString().padStart(2,"0"),l=(parseInt(i)+24)%"12"||"12",r=o.getMinutes().toString().padStart(2,"0"),d=o.getSeconds().toString().padStart(2,"0"),u=parseInt(i)>=12?"PM":"AM";t=(t=(t=(t=(t=(t=(t=(t=(t=t.replace("M",n)).replace("d",a)).replace("e",c)).replace("y",s)).replace("H",i)).replace("h",l)).replace("m",r)).replace("s",d)).replace("a",u),e.innerHTML=t}}t.length&&(e(),setTimeout(()=>{setInterval(e,1e3)},1e3-(new Date).getMilliseconds()))}function sidenavController(){const t=document.querySelector(".tui-sidenav-button");t&&t.addEventListener("click",()=>{const t=document.querySelector(".tui-sidenav");if(!t)throw"No sidenav element found.";t.classList.contains("active")?t.classList.remove("active"):t.classList.add("active")})}function modalController(){const t=document.querySelector(".tui-overlap");if(!t)return;const e=document.getElementsByClassName("tui-modal-button");for(const o of e)o.addEventListener("click",e=>{t.classList.add("active");const o=e.target.getAttribute("data-modal");if(!o)throw"Modal close button data-modal attribute is empty or not set.";{const t=document.getElementById(o);if(!t)throw'No modal element with id of "'+o+'" found.';t.classList.add("active")}});const o=document.getElementsByClassName("tui-modal-close-button");if(e.length>0&&!o.length)throw"No modal close buttons found.";for(const e of o)e.addEventListener("click",e=>{t.classList.remove("active");const o=e.target.getAttribute("data-modal");if(!o)throw"Modal close button data-modal attribute is empty or not set.";{const t=document.getElementById(o);if(!t)throw'No modal element with id of "'+o+'" found.';t.classList.remove("active")}})}domReady(function(){tabsController(),datetimeController(),sidenavController(),modalController()}); \ No newline at end of file diff --git a/web/templates/admin.html b/web/templates/admin.html new file mode 100644 index 0000000..3ebe83a --- /dev/null +++ b/web/templates/admin.html @@ -0,0 +1,28 @@ +{{template "layout" .}} + +{{define "title"}}Admin Panel · Estus Shots{{end}} + +{{define "content"}} +
+ Admin Panel +

+ This is an example admin page with a different menu bar that includes admin-specific options. +

+ +
+

System Status

+
+

Server: Running

+

Users: 42 active

+

Uptime: 7 days, 3 hours

+
+
+ +
+

Quick Actions

+ + + +
+
+{{end}} diff --git a/web/templates/index.html b/web/templates/index.html new file mode 100755 index 0000000..f7da25d --- /dev/null +++ b/web/templates/index.html @@ -0,0 +1,45 @@ +{{template "layout" .}} + +{{define "title"}}Estus Shots · Go + HTMX demo{{end}} + +{{define "content"}} +
+ Estus Shots control panel +

+ Estus Shots demonstrates a Go backend compiled into a single binary. The UI + uses TUI.CSS for a retro feel and HTMX for + partial page updates without JavaScript glue code. +

+ +
+

Interactive counter

+

+ Click the button to trigger an hx-post request. The response + replaces only the counter panel. +

+
+
Loading…
+
+
+ +
+

Server time

+

+ A periodic hx-get refresh keeps this panel in sync with the + server clock. +

+
+
Loading…
+
+
+
+{{end}} diff --git a/web/templates/layout.html b/web/templates/layout.html new file mode 100644 index 0000000..a9274cf --- /dev/null +++ b/web/templates/layout.html @@ -0,0 +1,19 @@ +{{define "layout"}} + + + + + + {{block "title" .}}Estus Shots{{end}} + + + + + + {{block "menubar" .}}{{end}} +
+ {{block "content" .}}{{end}} +
+ + +{{end}} diff --git a/web/templates/menubar.html b/web/templates/menubar.html new file mode 100644 index 0000000..62c86be --- /dev/null +++ b/web/templates/menubar.html @@ -0,0 +1,25 @@ +{{define "menubar"}} + +{{end}}