diff --git a/.github/tfg-in-work.gif b/.github/tfg-in-work.gif index 94d2f7e3..4f2e4e73 100644 Binary files a/.github/tfg-in-work.gif and b/.github/tfg-in-work.gif differ diff --git a/.github/window-formats.png b/.github/window-formats.png new file mode 100644 index 00000000..36b78eba Binary files /dev/null and b/.github/window-formats.png differ diff --git a/.github/window-presets.png b/.github/window-presets.png new file mode 100644 index 00000000..56f70216 Binary files /dev/null and b/.github/window-presets.png differ diff --git a/.github/window-settings.png b/.github/window-settings.png new file mode 100644 index 00000000..f6005277 Binary files /dev/null and b/.github/window-settings.png differ diff --git a/.github/window-several-batches.png b/.github/window-several-batches.png new file mode 100644 index 00000000..937a18a8 Binary files /dev/null and b/.github/window-several-batches.png differ diff --git a/.github/window.png b/.github/window.png index 6a7fa026..f8006334 100644 Binary files a/.github/window.png and b/.github/window.png differ diff --git a/README.md b/README.md index 1369f961..9228e86c 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ needs it finds out it exists. - **Cost nothing and stay out of your way** - GPL-3.0, and the files you generate are yours with no strings attached. -![The desktop window of Testing Files Generator building a set of files around an upload limit, and the files appearing in a folder as they are written](.github/tfg-in-work.gif) +![The desktop window of Testing Files Generator setting up a batch of log files and picking a format, then the size-boundaries and filename-handling presets writing their files into a folder beside it](.github/tfg-in-work.gif) ![The Star button at the top of this page, with a cursor pressing it](.github/star-the-repo.gif) @@ -56,17 +56,17 @@ This README is also the manual. The short version is above the line, the full reference is below it. - [What it can do](#-what-it-can-do) +- [Install](#-install) +- [Quick start](#-quick-start) - [Formats it generates](#-formats-it-generates) +- [Presets](#-presets) - [The problem it solves](#-the-problem-it-solves) - [What makes it different](#-what-makes-it-different) -- [Install](#-install) -- [Quick start](#-quick-start) - [Reference](#reference) - [Commands](#️-commands) - [Recipes](#-recipes) - [Formats in detail](#-formats-in-detail) - [The manifest](#-the-manifest) - - [Presets](#-presets) - [The desktop window](#️-the-desktop-window) - [Using it in CI](#️-using-it-in-ci) - [Questions](#-questions) @@ -74,6 +74,86 @@ reference is below it. - [Everything inside a generated file is made up](#-everything-inside-a-generated-file-is-made-up) - [Licence](#-licence) +## 📦 Install + +**Download a binary.** Take the archive for your system from the +[releases page](https://github.com/donislawdev/TestingFilesGenerator/releases), +unpack it and run it. `tfg` is the command line, `tfg-gui` is the desktop +window. The Windows and macOS downloads are signed, so they start without a +warning about an unknown developer. The Linux ones are not, because desktop +Linux has no equivalent to sign them with. + +**With Go installed:** + +``` +go install -tags noasm github.com/donislawdev/TestingFilesGenerator/cmd/tfg@latest +``` + +**From source.** Needs Go 1.27.0 or newer, and nothing else: + +``` +git clone https://github.com/donislawdev/TestingFilesGenerator +cd TestingFilesGenerator +go build -tags "$(cat .github/build-tags)" ./cmd/tfg +``` + +**The tag is not optional.** The AVIF encoder has an assembly path that reads +past the end of a buffer and takes the process down on some picture sizes, and +the tag turns it off. Building without it does not compile, and says so. The +files it produces are the same either way. + +The desktop window is a second binary, +`go build -tags "$(cat .github/build-tags)" ./cmd/tfg-gui`. It draws +through OpenGL and reaches it through C, so that one needs a C compiler and is +built natively on each system. Built without one it still compiles, and says on +start that it has no window in it and that everything is on the command line. + +The window needs OpenGL 2.1 to draw. On Windows the archive carries a +software renderer for machines whose graphics driver offers none - Mesa +llvmpipe, in an `opengl` folder next to `tfg-gui.exe` - and the window uses it +by itself when the driver refuses: a virtual machine without 3D acceleration, +a remote desktop, a server. Drawn that way it is slower and says so on its +About screen. On a machine with a driver the folder is not touched unless +you ask: `tfg-gui --software-gl` loads the renderer on any Windows machine, +which is the way to see the window as a machine without a driver sees it. Keep +the folder next to the program: without it, and without a driver, the window +says what it looked for in a dialog and on standard error, and exits 1. Linux +has Mesa in the system and macOS has never lacked what the toolkit needs, so +nothing of the kind ships there, and there the flag says so and changes +nothing. The command line needs no graphics driver and does everything the +window does. + +## 🚀 Quick start + +**1. Make a file.** One PNG, exactly two megabytes: + +``` +tfg generate --format png --size 2mb --out ./out +``` + +**2. Make a lot of files.** Ten thousand log files, each between one and eight +kilobytes, with the sizes drawn from the seed so tomorrow gives the same set. +**Give each run its own directory** - the manifest is the only record of what a +run wrote, so the tool refuses to write a second one over it: + +``` +tfg generate --format log --size-range 1kb-8kb --count 10000 --out ./logs +``` + +**3. Check them, then remove them.** + +``` +tfg verify ./logs/manifest.json +tfg cleanup ./logs/manifest.json --yes +``` + +``` +logs matches ./logs/manifest.json: 10000 files checked +``` + +**Sizes count in 1024s**, the way your file manager does, so `2mb` means +2097152 bytes. A plain byte count works too: `--size 2097152`. + ## 📁 Formats it generates Twenty six, and every one is a **real file of that format** - it opens in the @@ -94,6 +174,32 @@ Most of them take settings of their own - image dimensions, JPEG quality, PDF page count, rows and columns in a spreadsheet, what goes inside an archive. See [format settings](#per-format-settings). +## 🧪 Presets + +A preset is a ready-made set of files that answers one common testing question, +so you do not have to design the set yourself. Every file in it says what it is +for, and the manifest says how your system should react to it: + +| preset | the question it answers | +|---|---| +| `empty-and-minimal` | Does a file that is valid and as small as the format allows get through? | +| `filename-handling` | Will my system store, show and give back a file name it did not expect? | +| `size-boundaries` | Is a size limit enforced exactly where it is declared? | +| `tabular-import` | Does my table import survive what real tools export? | +| `text-encoding` | Does my reader know which encoding a file is in, or is it guessing? | +| `upload-validation` | Does my upload form take what it should and turn the rest away? | + +``` +tfg preset show size-boundaries +tfg generate --preset size-boundaries --limit 10mb --out ./limits +``` + +`show` tells you what the set would cost before you build it, and says outright +when a number is a placeholder of ours rather than a limit of yours. Presets are +ordinary recipes underneath - `tfg preset eject size-boundaries` prints the +recipe and you edit it from there. The Presets screen of the window offers the +same ones. + ## 🤔 The problem it solves You are testing software that accepts files from people. Sooner or later you @@ -162,86 +268,6 @@ And where the right answer genuinely depends on your own policy, the manifest says `unspecified` instead of inventing one. A generator that guesses produces false failures, and a suite that cries wolf gets switched off. -## 📦 Install - -**Download a binary.** Take the archive for your system from the -[releases page](https://github.com/donislawdev/TestingFilesGenerator/releases), -unpack it and run it. `tfg` is the command line, `tfg-gui` is the desktop -window. The Windows and macOS downloads are signed, so they start without a -warning about an unknown developer. The Linux ones are not, because desktop -Linux has no equivalent to sign them with. - -**With Go installed:** - -``` -go install -tags noasm github.com/donislawdev/TestingFilesGenerator/cmd/tfg@latest -``` - -**From source.** Needs Go 1.27.0 or newer, and nothing else: - -``` -git clone https://github.com/donislawdev/TestingFilesGenerator -cd TestingFilesGenerator -go build -tags "$(cat .github/build-tags)" ./cmd/tfg -``` - -**The tag is not optional.** The AVIF encoder has an assembly path that reads -past the end of a buffer and takes the process down on some picture sizes, and -the tag turns it off. Building without it does not compile, and says so. The -files it produces are the same either way. - -The desktop window is a second binary, -`go build -tags "$(cat .github/build-tags)" ./cmd/tfg-gui`. It draws -through OpenGL and reaches it through C, so that one needs a C compiler and is -built natively on each system. Built without one it still compiles, and says on -start that it has no window in it and that everything is on the command line. - -The window needs OpenGL 2.1 to draw. On Windows the archive carries a -software renderer for machines whose graphics driver offers none - Mesa -llvmpipe, in an `opengl` folder next to `tfg-gui.exe` - and the window uses it -by itself when the driver refuses: a virtual machine without 3D acceleration, -a remote desktop, a server. Drawn that way it is slower and says so on its -About screen. On a machine with a driver the folder is not touched unless -you ask: `tfg-gui --software-gl` loads the renderer on any Windows machine, -which is the way to see the window as a machine without a driver sees it. Keep -the folder next to the program: without it, and without a driver, the window -says what it looked for in a dialog and on standard error, and exits 1. Linux -has Mesa in the system and macOS has never lacked what the toolkit needs, so -nothing of the kind ships there, and there the flag says so and changes -nothing. The command line needs no graphics driver and does everything the -window does. - -## 🚀 Quick start - -**1. Make a file.** One PNG, exactly two megabytes: - -``` -tfg generate --format png --size 2mb --out ./out -``` - -**2. Make a lot of files.** Ten thousand log files, each between one and eight -kilobytes, with the sizes drawn from the seed so tomorrow gives the same set. -**Give each run its own directory** - the manifest is the only record of what a -run wrote, so the tool refuses to write a second one over it: - -``` -tfg generate --format log --size-range 1kb-8kb --count 10000 --out ./logs -``` - -**3. Check them, then remove them.** - -``` -tfg verify ./logs/manifest.json -tfg cleanup ./logs/manifest.json --yes -``` - -``` -logs matches ./logs/manifest.json: 10000 files checked -``` - -**Sizes count in 1024s**, the way your file manager does, so `2mb` means -2097152 bytes. A plain byte count works too: `--size 2097152`. - --- # Reference @@ -633,25 +659,6 @@ produced the file, and `summary.by_target` counts the files each target came to. A recipe with several targets can therefore be checked target by target without reading file names. -## 🧪 Presets - -A preset is a ready made set of files that answers a common testing question, so -you do not have to design the set yourself: - -``` -tfg preset list -tfg preset show size-boundaries -tfg generate --preset size-boundaries --limit 10mb --out ./limits -``` - -`show` tells you what the set would cost before you build it, and says outright -when a number is a placeholder of ours rather than a limit of yours. Presets are -ordinary recipes underneath - `tfg preset eject size-boundaries` prints the -recipe and you edit it from there. - -`tfg preset list` names every preset your build ships, and the Presets -screen of the window offers the same ones. - ## 🖥️ The desktop window The same engine with a window on it, for the testing that is not scripted. It is @@ -663,6 +670,15 @@ Four screens - one batch, presets, several batches at once, and about. It shows what a run would cost before writing anything, reports progress while it runs, and can be cancelled part way without leaving a half written file behind. +

+ The Presets screen of the window, with the list of presets open + One batch of log files in the window, with the settings the log format takes +

+

+ The Several batches screen of the window, where each batch has its own format and size + The list of formats in the window, grouped by kind and filtered as you type +

+ It does not open a recipe file yet. Recipes are a command line thing for now, and the window builds its batches in the form. diff --git a/internal/guard/edgeexample_test.go b/internal/guard/edgeexample_test.go new file mode 100644 index 00000000..3e1e223e --- /dev/null +++ b/internal/guard/edgeexample_test.go @@ -0,0 +1,130 @@ +package guard + +import ( + "bytes" + "context" + "encoding/json" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/donislawdev/TestingFilesGenerator/internal/cli" +) + +// The three files every first page shows - one byte under a 1 MB limit, the +// limit, one byte over - are what size-boundaries actually writes. +// +// They stand in four places a stranger reads before anything else: the social +// card, the README, and the site's first page in both languages. All four +// carry them as literals, and the card says outright that everything on it is +// real. Nothing asked the program. An outside review of #148 named it, and +// asking showed it was already half untrue: the card before that one printed +// "tfg generate --preset size-boundaries --limit 1mb", which the program +// refuses, because the default spread would need a file of 0 B. +// +// So this runs the command the README and the site print, as a dry run, and +// holds each place to the answer: every file on the line of its name, with its +// byte count and the outcome the manifest declares for it. +func TestTheLimitExampleIsWhatThePresetWrites(t *testing.T) { + const command = "tfg generate --preset size-boundaries --limit 1mb --spread 1B --format pdf --out ./edges" + args := append(strings.Fields(strings.TrimPrefix(command, "tfg ")), "--dry-run", "--json") + args[len(args)-3] = filepath.Join(t.TempDir(), "edges") + var out, errOut bytes.Buffer + if code := cli.Run(context.Background(), args, &out, &errOut); code != cli.ExitOK { + t.Fatalf("the command the pages print ended with %d:\n%s", code, errOut.String()) + } + var manifest struct { + Files []struct { + Name string `json:"name"` + Bytes int64 `json:"bytes"` + Expected struct { + Outcome string `json:"outcome"` + } `json:"expected"` + } `json:"files"` + } + if err := json.Unmarshal(out.Bytes(), &manifest); err != nil { + t.Fatalf("reading the dry run's manifest: %v", err) + } + if len(manifest.Files) != 3 { + t.Fatalf("the command writes %d files, and every page shows three", len(manifest.Files)) + } + + root := repoRoot(t) + places := []struct { + file string + spaced bool + outcomes map[string]string + command bool + }{ + {"web/templates/social.html", true, map[string]string{"accept": "accept", "reject": "reject"}, false}, + {"README.md", false, map[string]string{"accept": "**accept**", "reject": "**reject**"}, true}, + {"web/content/en/index.html", false, map[string]string{"accept": "accept", "reject": "reject"}, true}, + {"web/content/pl/index.html", false, map[string]string{"accept": "przyjąć", "reject": "odrzucić"}, true}, + } + for _, place := range places { + body, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(place.file))) + if err != nil { + t.Fatalf("reading %s: %v", place.file, err) + } + text := string(body) + if place.command && !strings.Contains(text, command) { + t.Errorf("%s no longer prints %q, so the files it shows come from a command this guard does not run", + place.file, command) + } + for _, f := range manifest.Files { + line := lineNaming(text, f.Name) + if line == "" { + t.Errorf("%s does not show %s, and the command writes it", place.file, f.Name) + continue + } + size := strconv.FormatInt(f.Bytes, 10) + if place.spaced { + size = spacedBytes(f.Bytes) + " B" + } + if !strings.Contains(line, size) { + t.Errorf("%s shows %s without its %s:\n%s", place.file, f.Name, size, line) + } + word, ok := place.outcomes[f.Expected.Outcome] + if !ok { + t.Errorf("the manifest declares %q for %s, and %s has no word for it", f.Expected.Outcome, f.Name, place.file) + continue + } + if !strings.Contains(line, word) { + t.Errorf("%s shows %s without the outcome the manifest declares (%s):\n%s", + place.file, f.Name, word, line) + } + } + } +} + +// lineNaming is the one line of a page that shows a file, or nothing when no +// line or more than one does - a name twice on a page is two claims, and this +// checks one. +func lineNaming(text, name string) string { + var found []string + for _, line := range strings.Split(text, "\n") { + if strings.Contains(line, name) { + found = append(found, line) + } + } + if len(found) != 1 { + return "" + } + return found[0] +} + +// spacedBytes writes a byte count the way the social card does, in groups of +// three. +func spacedBytes(n int64) string { + s := strconv.FormatInt(n, 10) + var b strings.Builder + for i, r := range s { + if i > 0 && (len(s)-i)%3 == 0 { + b.WriteByte(' ') + } + b.WriteRune(r) + } + return b.String() +} diff --git a/internal/guard/readmepresets_test.go b/internal/guard/readmepresets_test.go new file mode 100644 index 00000000..d6280fd5 --- /dev/null +++ b/internal/guard/readmepresets_test.go @@ -0,0 +1,86 @@ +package guard + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/donislawdev/TestingFilesGenerator/internal/preset" +) + +// The README's table of presets names every preset the program registers, +// with the question that preset answers, and names nothing else. +// +// Until 2026-09-29 the section named no preset at all and sent the reader to +// tfg preset list, because a list typed by hand goes stale and nothing +// compared one with the registry. The owner asked for the list on the first +// page a visitor reads - a generator of single files loses to the ones in a +// browser, and the presets are what those do not have. This is the comparison +// that makes the list safe to keep: a seventh preset turns it red until the +// table names it, and a preset renamed or removed turns it red until its row +// goes. The question is compared word for word, because it is the one sentence +// the program itself prints for each preset. +func TestTheReadmeListsEveryPresetItShips(t *testing.T) { + body, err := os.ReadFile(filepath.Join(repoRoot(t), "README.md")) + if err != nil { + t.Fatalf("reading the README: %v", err) + } + const heading = "## 🧪 Presets" + text := string(body) + start := strings.Index(text, heading) + if start < 0 { + t.Fatalf("the README has no %q heading, so this guard has nothing to read", heading) + } + end := strings.Index(text[start+len(heading):], "\n## ") + if end < 0 { + t.Fatal("the presets section runs to the end of the README, which means the heading after it moved") + } + section := text[start : start+len(heading)+end] + + rows := map[string]string{} + for _, line := range strings.Split(section, "\n") { + if !strings.HasPrefix(line, "| `") { + continue + } + cells := strings.Split(strings.Trim(line, "| "), " | ") + if len(cells) != 2 { + t.Errorf("a row of the presets table has %d cells, and it has two - the preset and "+ + "its question:\n%s", len(cells), line) + continue + } + id := strings.Trim(cells[0], "`") + if _, twice := rows[id]; twice { + t.Errorf("the presets table has two rows for %s, and the second would hide what the "+ + "first says from this guard", id) + } + rows[id] = strings.TrimSpace(cells[1]) + } + if len(rows) == 0 { + t.Fatal("the presets section has no table rows - this guard would pass against any README ever written") + } + + registered := preset.All() + if len(registered) == 0 { + t.Fatal("no preset is registered - this guard would pass without checking anything") + } + known := map[string]bool{} + for _, p := range registered { + known[p.ID] = true + question, ok := rows[p.ID] + if !ok { + t.Errorf("%s is registered and the table under %q does not name it, so the first page "+ + "a visitor reads offers fewer presets than the binary ships", p.ID, heading) + continue + } + if question != p.Question { + t.Errorf("the table says %s answers %q, and the preset says it answers %q", + p.ID, question, p.Question) + } + } + for id := range rows { + if !known[id] { + t.Errorf("the table under %q names %s, and no preset of that name is registered", heading, id) + } + } +} diff --git a/internal/guard/socialpicture_test.go b/internal/guard/socialpicture_test.go index b048f059..76bec17b 100644 --- a/internal/guard/socialpicture_test.go +++ b/internal/guard/socialpicture_test.go @@ -106,7 +106,10 @@ func TestTheSocialPictureShowsTheCardAsItIsNow(t *testing.T) { " TFG_WRITE_SITE=1 go test ./internal/guard/ -run TestTheSiteSaysWhatTheToolSays\n"+ " TFG_WRITE_SOCIAL_STAMP=1 go test ./internal/guard/ -run TestTheSocialPicture\n"+ "Then LOOK at web/assets/social-preview.png. If git says it did not change, "+ - "the camera photographed the old card.", + "the camera photographed the old card.\n"+ + "And upload it to GitHub by hand - Settings, General, Social preview. GitHub keeps "+ + "a copy of its own that nothing here writes to, and until 2026-09-29 it said 24 "+ + "formats while this picture said 26. tools/release-check.py compares the two.", was, now) } } diff --git a/web/assets/social-preview.png b/web/assets/social-preview.png index edc1df7f..26f103d0 100644 Binary files a/web/assets/social-preview.png and b/web/assets/social-preview.png differ diff --git a/web/content/en/index.html b/web/content/en/index.html index be6f03a9..52d613f9 100644 --- a/web/content/en/index.html +++ b/web/content/en/index.html @@ -12,7 +12,7 @@

Generate real test files at any exact size

- The desktop window of Testing Files Generator, set up to write a batch of test files
The desktop window, set up to write a batch of files. The same engine runs behind the command line.
diff --git a/web/content/pl/index.html b/web/content/pl/index.html index ce757e64..61fadb10 100644 --- a/web/content/pl/index.html +++ b/web/content/pl/index.html @@ -12,7 +12,7 @@

Generuj pliki testowe o zadanym rozmiarze

- Okno programu Testing Files Generator przygotowane do zapisania wsadu plików testowych
Okno programu przygotowane do zapisania wsadu plików. Za wierszem poleceń stoi ten sam silnik.
diff --git a/web/public/assets/social-preview.png b/web/public/assets/social-preview.png index edc1df7f..26f103d0 100644 Binary files a/web/public/assets/social-preview.png and b/web/public/assets/social-preview.png differ diff --git a/web/public/assets/window.png b/web/public/assets/window.png index 6a7fa026..f8006334 100644 Binary files a/web/public/assets/window.png and b/web/public/assets/window.png differ diff --git a/web/public/index.html b/web/public/index.html index 08bc4bea..c8e0f833 100644 --- a/web/public/index.html +++ b/web/public/index.html @@ -101,7 +101,7 @@

Generate real test files at any exact size

- The desktop window of Testing Files Generator, set up to write a batch of test files
The desktop window, set up to write a batch of files. The same engine runs behind the command line.
diff --git a/web/public/pl/index.html b/web/public/pl/index.html index 6aad5d7c..e5dc5e32 100644 --- a/web/public/pl/index.html +++ b/web/public/pl/index.html @@ -101,7 +101,7 @@

Generuj pliki testowe o zadanym rozmiarze

- Okno programu Testing Files Generator przygotowane do zapisania wsadu plików testowych
Okno programu przygotowane do zapisania wsadu plików. Za wierszem poleceń stoi ten sam silnik.
diff --git a/web/public/social.html b/web/public/social.html index bae00697..fe2720c1 100644 --- a/web/public/social.html +++ b/web/public/social.html @@ -17,188 +17,179 @@ body { background: #131417; background-image: - radial-gradient(900px 420px at 88% 12%, rgba(111, 183, 240, 0.10), transparent 62%), - radial-gradient(700px 380px at 6% 96%, rgba(111, 207, 127, 0.07), transparent 60%); + radial-gradient(900px 460px at 90% 10%, rgba(111, 183, 240, 0.13), transparent 62%), + radial-gradient(700px 380px at 4% 100%, rgba(111, 207, 127, 0.09), transparent 60%); color: #E8EAED; font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; display: grid; - grid-template-columns: 600px 680px; + grid-template-columns: 650px 630px; align-items: center; } -.left { padding: 0 0 0 62px; } +.left { padding-left: 60px; } .brand { display: flex; align-items: center; - gap: 16px; - margin-bottom: 8px; + gap: 15px; } -.brand img { width: 50px; height: 50px; border-radius: 10px; } +.brand img { width: 50px; height: 50px; } .brand span { font-size: 27px; font-weight: 650; - letter-spacing: -0.015em; - color: #E8EAED; -} - -.rule { - width: 92px; - height: 4px; - border-radius: 2px; - background: #6FCF7F; - margin: 18px 0 26px; } h1 { - font-size: 62px; - line-height: 1.06; - letter-spacing: -0.032em; - font-weight: 700; + margin-top: 26px; + font-size: 66px; + line-height: 1.02; + letter-spacing: -0.034em; + font-weight: 750; } h1 .accent { color: #6FB7F0; } -.lead { - margin-top: 22px; - font-size: 22px; - line-height: 1.45; - color: #B9C0CA; - max-width: 505px; +ul { + margin-top: 26px; + list-style: none; + max-width: 560px; } -.pills { - display: flex; - flex-wrap: wrap; - gap: 10px; - margin-top: 28px; - list-style: none; +li { + font-size: 23px; + line-height: 1.3; + color: #B9C0CA; + margin-top: 14px; + padding-left: 22px; + position: relative; } - -.pills li { - border: 1px solid #2E3238; - background: #1B1D21; - border-radius: 999px; - padding: 9px 15px; - font-size: 16.5px; - color: #D7DDE5; - white-space: nowrap; +li::before { + content: ""; + position: absolute; + left: 0; + top: 11px; + width: 10px; + height: 10px; + border-radius: 3px; + background: #6FB7F0; } +li b { color: #E8EAED; font-weight: 650; } + .foot { - margin-top: 30px; - font-size: 17px; + margin-top: 26px; + font-size: 19px; color: #949CA8; } -.foot b { color: #B9C0CA; font-weight: 600; } - +.brand .surfaces { + margin-left: 4px; + font-size: 17px; + font-weight: 650; + color: #6FB7F0; + border: 1px solid rgba(111, 183, 240, 0.5); + background: rgba(111, 183, 240, 0.10); + border-radius: 999px; + padding: 4px 12px; +} -.right { position: relative; height: 640px; } +.panel { width: 560px; } -.shot { - position: absolute; - right: 38px; - top: 128px; - width: 332px; - display: block; - border: 1px solid #3A404A; - border-radius: 13px; - box-shadow: 0 30px 70px rgba(0, 0, 0, 0.62); +.presets { + background: #1B1D21; + border: 1px solid #343943; + border-radius: 16px; + padding: 16px 20px 18px; + margin-bottom: 16px; + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.55); } -.stack { - position: absolute; - z-index: 2; - left: 0; - top: 148px; - width: 434px; +.label { + font-size: 17px; + color: #949CA8; + margin-bottom: 12px; } - -.command { +.chips { + display: flex; + flex-wrap: wrap; + gap: 9px; +} + +.chip { font-family: ui-monospace, "SFMono-Regular", Consolas, "Liberation Mono", monospace; - font-size: 13.5px; - color: #9DA3A8; - background: #16181C; - border-bottom: 1px solid #2E3238; - padding: 13px 20px; - white-space: nowrap; + font-size: 18px; + padding: 7px 12px; + border-radius: 9px; + border: 1px solid #343943; + color: #C9CFD8; + background: #22252A; } -.command b { color: #6FCF7F; font-weight: 600; } +.chip.picked { + border-color: #6FB7F0; + color: #0F1115; + background: #6FB7F0; + font-weight: 700; +} .card { background: #1B1D21; border: 1px solid #343943; - border-radius: 14px; + border-radius: 16px; overflow: hidden; - box-shadow: 0 26px 64px rgba(0, 0, 0, 0.62); + box-shadow: 0 30px 70px rgba(0, 0, 0, 0.6); } -.card-head { +.head { + padding: 13px 20px; background: #22252A; border-bottom: 1px solid #2E3238; - padding: 13px 20px; - font-size: 15px; + font-size: 17px; color: #949CA8; - font-family: ui-monospace, "SFMono-Regular", Consolas, "Liberation Mono", monospace; display: flex; justify-content: space-between; } -.card-head span:last-child { color: #6FB7F0; } - -table { width: 100%; border-collapse: collapse; } +.head span:last-child { color: #6FB7F0; } -td { - padding: 21px 20px; - border-bottom: 1px solid #24272C; - font-size: 16.5px; - vertical-align: middle; +.row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 15px 20px; + border-bottom: 1px solid #26292E; } -tr:last-child td { border-bottom: 0; } +.row:last-child { border-bottom: 0; } -.name { +.file { font-family: ui-monospace, "SFMono-Regular", Consolas, "Liberation Mono", monospace; + font-size: 21px; color: #DCE6F2; } .bytes { font-family: ui-monospace, "SFMono-Regular", Consolas, "Liberation Mono", monospace; - text-align: right; - color: #B9C0CA; - font-variant-numeric: tabular-nums; - white-space: nowrap; + font-size: 15px; + color: #949CA8; + margin-left: 12px; } -.verdict { text-align: right; white-space: nowrap; } - .tag { - display: inline-block; - border-radius: 7px; - padding: 5px 12px; - font-size: 15px; - font-weight: 650; + font-size: 19px; + font-weight: 700; + border-radius: 9px; + padding: 5px 14px; } -.accept { background: rgba(111, 207, 127, 0.16); color: #6FCF7F; } -.reject { background: rgba(241, 112, 122, 0.16); color: #F1707A; } - -.why { - border-top: 1px solid #2E3238; - background: #16181C; - padding: 13px 20px; - font-size: 15px; - color: #949CA8; - text-align: right; -} +.accept { background: rgba(111, 207, 127, 0.17); color: #6FCF7F; } +.reject { background: rgba(241, 112, 122, 0.17); color: #F1707A; } @@ -206,51 +197,34 @@
Testing Files Generator + GUI + CLI
-
-

Real test files.
At any exact size.

-

- 26 formats that open in the software that owns them, plus a manifest - saying how your system should react to each file. -

-